@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.
- package/dist/harness/config.d.ts +8 -1
- package/dist/harness/config.js +57 -35
- package/dist/main.js +48 -5
- package/dist/tools/MemoryTool/index.d.ts +2 -2
- package/package.json +1 -1
package/dist/harness/config.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/harness/config.js
CHANGED
|
@@ -34,50 +34,72 @@ function readGlobalConfig() {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
const ALL_SOURCES = ["user", "project", "local"];
|
|
38
|
+
export function readOhConfig(root, sources) {
|
|
38
39
|
const effectiveRoot = root ?? ".";
|
|
39
|
-
|
|
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
|
|
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 (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
|
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" | "
|
|
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" | "
|
|
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;
|