@zhijiewang/openharness 2.16.0 → 2.17.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.
@@ -152,6 +152,13 @@ export type OhConfig = {
152
152
  };
153
153
  /** Clear cached config (call after writes or to force re-read) */
154
154
  export declare function invalidateConfigCache(): void;
155
- export declare function readOhConfig(root?: string): OhConfig | null;
155
+ export type SettingSource = "user" | "project" | "local";
156
+ export declare function readOhConfig(root?: string, sources?: readonly SettingSource[]): OhConfig | null;
157
+ /**
158
+ * Parse the `--setting-sources` CLI flag (comma-separated source names).
159
+ * Returns `undefined` when the flag is absent or empty (caller uses defaults).
160
+ * Unknown names are silently dropped.
161
+ */
162
+ export declare function parseSettingSources(raw: string | undefined): SettingSource[] | undefined;
156
163
  export declare function writeOhConfig(cfg: OhConfig, root?: string): void;
157
164
  //# sourceMappingURL=config.d.ts.map
@@ -34,50 +34,72 @@ function readGlobalConfig() {
34
34
  return null;
35
35
  }
36
36
  }
37
- export function readOhConfig(root) {
37
+ const ALL_SOURCES = ["user", "project", "local"];
38
+ export function readOhConfig(root, sources) {
38
39
  const effectiveRoot = root ?? ".";
39
- if (_configCache !== undefined && _configCacheRoot === effectiveRoot)
40
+ // Only cache when merging the full default set. Callers that pass a subset
41
+ // are expressing a request-scoped intent and shouldn't poison the cache.
42
+ const usingDefaults = sources === undefined;
43
+ if (usingDefaults && _configCache !== undefined && _configCacheRoot === effectiveRoot)
40
44
  return _configCache;
41
- const p = configPath(root);
42
- // Layer 1: Global defaults from ~/.oh/config.yaml
43
- const globalCfg = readGlobalConfig();
44
- // Layer 2: Project config from .oh/config.yaml
45
+ const enabled = new Set(sources ?? ALL_SOURCES);
46
+ // Layer 1: Global defaults from ~/.oh/config.yaml (source: "user")
47
+ const globalCfg = enabled.has("user") ? readGlobalConfig() : null;
48
+ // Layer 2: Project config from .oh/config.yaml (source: "project")
45
49
  let projectCfg = null;
46
- if (existsSync(p)) {
47
- try {
48
- projectCfg = parse(readFileSync(p, "utf-8"));
49
- }
50
- catch {
51
- /* ignore malformed project config */
50
+ if (enabled.has("project")) {
51
+ const p = configPath(root);
52
+ if (existsSync(p)) {
53
+ try {
54
+ projectCfg = parse(readFileSync(p, "utf-8"));
55
+ }
56
+ catch {
57
+ /* ignore malformed project config */
58
+ }
52
59
  }
53
60
  }
54
- // If neither exists, no config
55
- if (!globalCfg && !projectCfg) {
56
- _configCache = null;
57
- _configCacheRoot = effectiveRoot;
58
- return null;
59
- }
60
- // Merge: global → project (project overrides global)
61
- const base = { ...globalCfg, ...projectCfg };
62
- // Layer 3: Local overrides from .oh/config.local.yaml (gitignored personal settings)
63
- const localPath = join(root ?? ".", ".oh", "config.local.yaml");
64
- if (existsSync(localPath)) {
65
- try {
66
- const local = parse(readFileSync(localPath, "utf-8"));
67
- if (local) {
68
- const merged = { ...base, ...local };
69
- _configCache = merged;
70
- _configCacheRoot = effectiveRoot;
71
- return merged;
61
+ // Layer 3: Local overrides from .oh/config.local.yaml (source: "local")
62
+ let localCfg = null;
63
+ if (enabled.has("local")) {
64
+ const localPath = join(root ?? ".", ".oh", "config.local.yaml");
65
+ if (existsSync(localPath)) {
66
+ try {
67
+ localCfg = parse(readFileSync(localPath, "utf-8"));
68
+ }
69
+ catch {
70
+ /* ignore malformed local config */
72
71
  }
73
72
  }
74
- catch {
75
- /* ignore malformed local config */
73
+ }
74
+ if (!globalCfg && !projectCfg && !localCfg) {
75
+ if (usingDefaults) {
76
+ _configCache = null;
77
+ _configCacheRoot = effectiveRoot;
76
78
  }
79
+ return null;
80
+ }
81
+ // Precedence: local > project > user
82
+ const merged = { ...(globalCfg ?? {}), ...(projectCfg ?? {}), ...(localCfg ?? {}) };
83
+ if (usingDefaults) {
84
+ _configCache = merged;
85
+ _configCacheRoot = effectiveRoot;
77
86
  }
78
- _configCache = base;
79
- _configCacheRoot = effectiveRoot;
80
- return base;
87
+ return merged;
88
+ }
89
+ /**
90
+ * Parse the `--setting-sources` CLI flag (comma-separated source names).
91
+ * Returns `undefined` when the flag is absent or empty (caller uses defaults).
92
+ * Unknown names are silently dropped.
93
+ */
94
+ export function parseSettingSources(raw) {
95
+ if (!raw)
96
+ return undefined;
97
+ const valid = new Set(["user", "project", "local"]);
98
+ const out = raw
99
+ .split(",")
100
+ .map((s) => s.trim())
101
+ .filter((s) => valid.has(s));
102
+ return out.length > 0 ? out : undefined;
81
103
  }
82
104
  export function writeOhConfig(cfg, root) {
83
105
  invalidateConfigCache();
package/dist/main.js CHANGED
@@ -15,7 +15,7 @@ import { homedir } from "node:os";
15
15
  import { join } from "node:path";
16
16
  import { Command, Option } from "commander";
17
17
  import { render } from "ink";
18
- import { readOhConfig } from "./harness/config.js";
18
+ import { parseSettingSources, readOhConfig } from "./harness/config.js";
19
19
  import { emitHook, setHookDecisionObserver } from "./harness/hooks.js";
20
20
  import { loadActiveMemories, memoriesToPrompt, userProfileToPrompt } from "./harness/memory.js";
21
21
  import { detectProject, projectContextToPrompt } from "./harness/onboarding.js";
@@ -120,6 +120,8 @@ program
120
120
  .option("--append-system-prompt <text>", "Append text to the system prompt")
121
121
  .option("--allowed-tools <tools>", "Comma-separated list of allowed tools")
122
122
  .option("--disallowed-tools <tools>", "Comma-separated list of disallowed tools")
123
+ .option("--resume <id>", "Resume a saved session (replays its message history before this prompt)")
124
+ .option("--setting-sources <sources>", "Comma-separated list of setting sources to merge (e.g. 'user,project,local'). Mirrors Claude Code's setting_sources.")
123
125
  .action(async (promptArg, opts) => {
124
126
  // Read from stdin if prompt is "-" or omitted and stdin is not a TTY
125
127
  let prompt;
@@ -137,7 +139,8 @@ program
137
139
  else {
138
140
  prompt = promptArg;
139
141
  }
140
- const savedConfig = readOhConfig();
142
+ const settingSources = parseSettingSources(opts.settingSources);
143
+ const savedConfig = readOhConfig(undefined, settingSources);
141
144
  const permissionMode = (opts.trust
142
145
  ? "trust"
143
146
  : opts.deny
@@ -189,7 +192,30 @@ program
189
192
  let fullOutput = "";
190
193
  const toolResults = [];
191
194
  const callIdToName = {};
195
+ // Resume a saved session if --resume <id> was passed. Replays its message
196
+ // history into the conversation before the new prompt. If the session can't
197
+ // be loaded (missing file, malformed JSON), fail early with a clear error
198
+ // rather than silently starting fresh.
199
+ let priorMessages;
200
+ let sessionId;
201
+ if (opts.resume) {
202
+ const { loadSession } = await import("./harness/session.js");
203
+ try {
204
+ const src = loadSession(opts.resume);
205
+ priorMessages = src.messages;
206
+ sessionId = src.id;
207
+ }
208
+ catch {
209
+ process.stderr.write(`Error: could not load session '${opts.resume}'\n`);
210
+ process.exit(1);
211
+ }
212
+ }
192
213
  if (outputFormat === "stream-json") {
214
+ // Emit a session_start event so SDK callers can capture the id for
215
+ // later resume (fires once, before turnStart).
216
+ if (sessionId) {
217
+ console.log(JSON.stringify({ type: "session_start", sessionId }));
218
+ }
193
219
  setHookDecisionObserver((n) => {
194
220
  console.log(JSON.stringify({
195
221
  type: "hook_decision",
@@ -209,7 +235,7 @@ program
209
235
  if (outputFormat === "stream-json") {
210
236
  console.log(JSON.stringify({ type: "turnStart", turnNumber: 0 }));
211
237
  }
212
- for await (const event of query(prompt, config)) {
238
+ for await (const event of query(prompt, config, priorMessages)) {
213
239
  if (event.type === "text_delta") {
214
240
  fullOutput += event.content;
215
241
  if (outputFormat === "text")
@@ -293,8 +319,11 @@ program
293
319
  .option("--disallowed-tools <tools>", "Comma-separated disallowed tool names")
294
320
  .option("--max-turns <n>", "Maximum turns per prompt", "20")
295
321
  .option("--system-prompt <prompt>", "Override the system prompt")
322
+ .option("--resume <id>", "Resume a saved session (seeds the conversation with its prior message history)")
323
+ .option("--setting-sources <sources>", "Comma-separated list of setting sources to merge (mirrors Claude Code's setting_sources).")
296
324
  .action(async (opts) => {
297
- const savedConfig = readOhConfig();
325
+ const settingSources = parseSettingSources(opts.settingSources);
326
+ const savedConfig = readOhConfig(undefined, settingSources);
298
327
  const permissionMode = (opts.permissionMode ??
299
328
  savedConfig?.permissionMode ??
300
329
  "trust");
@@ -327,7 +356,21 @@ program
327
356
  model,
328
357
  };
329
358
  // Conversation history, shared across all prompts for this process.
359
+ // Seeded from a prior session when --resume <id> is passed.
330
360
  const conversation = [];
361
+ let sessionId;
362
+ if (opts.resume) {
363
+ const { loadSession } = await import("./harness/session.js");
364
+ try {
365
+ const src = loadSession(opts.resume);
366
+ conversation.push(...src.messages);
367
+ sessionId = src.id;
368
+ }
369
+ catch {
370
+ console.log(JSON.stringify({ type: "error", message: `could not load session '${opts.resume}'` }));
371
+ return;
372
+ }
373
+ }
331
374
  let turnCounter = 0;
332
375
  // Will be set to the current prompt id before each turn so hook_decision
333
376
  // events can be demultiplexed by the client.
@@ -343,7 +386,7 @@ program
343
386
  }));
344
387
  });
345
388
  // Announce readiness so the client can send the first prompt.
346
- console.log(JSON.stringify({ type: "ready" }));
389
+ console.log(JSON.stringify({ type: "ready", sessionId }));
347
390
  const readline = await import("node:readline");
348
391
  const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
349
392
  for await (const rawLine of rl) {
@@ -11,7 +11,7 @@ declare const inputSchema: z.ZodObject<{
11
11
  }, "strip", z.ZodTypeAny, {
12
12
  action: "search" | "save" | "list";
13
13
  content?: string | undefined;
14
- type?: "user" | "convention" | "preference" | "project" | "debugging" | "feedback" | "reference" | undefined;
14
+ type?: "user" | "project" | "convention" | "preference" | "debugging" | "feedback" | "reference" | undefined;
15
15
  name?: string | undefined;
16
16
  description?: string | undefined;
17
17
  global?: boolean | undefined;
@@ -19,7 +19,7 @@ declare const inputSchema: z.ZodObject<{
19
19
  }, {
20
20
  action: "search" | "save" | "list";
21
21
  content?: string | undefined;
22
- type?: "user" | "convention" | "preference" | "project" | "debugging" | "feedback" | "reference" | undefined;
22
+ type?: "user" | "project" | "convention" | "preference" | "debugging" | "feedback" | "reference" | undefined;
23
23
  name?: string | undefined;
24
24
  description?: string | undefined;
25
25
  global?: boolean | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhijiewang/openharness",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "Open-source terminal coding agent. Works with any LLM.",
5
5
  "type": "module",
6
6
  "bin": {