@zhijiewang/openharness 2.15.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 +12 -1
- package/dist/harness/config.js +57 -35
- package/dist/harness/hooks.d.ts +23 -1
- package/dist/harness/hooks.js +88 -5
- package/dist/main.js +99 -6
- package/dist/tools/MemoryTool/index.d.ts +2 -2
- package/package.json +1 -1
package/dist/harness/config.d.ts
CHANGED
|
@@ -62,6 +62,10 @@ export type HooksConfig = {
|
|
|
62
62
|
postCompact?: HookDef[];
|
|
63
63
|
configChange?: HookDef[];
|
|
64
64
|
notification?: HookDef[];
|
|
65
|
+
/** Fires at the start of each top-level agent turn (after a user prompt is accepted, before model call). */
|
|
66
|
+
turnStart?: HookDef[];
|
|
67
|
+
/** Fires at the end of each top-level agent turn (after the model either completes or errors). Matches Claude Code's Stop hook. */
|
|
68
|
+
turnStop?: HookDef[];
|
|
65
69
|
};
|
|
66
70
|
export type ToolPermissionRule = {
|
|
67
71
|
tool: string;
|
|
@@ -148,6 +152,13 @@ export type OhConfig = {
|
|
|
148
152
|
};
|
|
149
153
|
/** Clear cached config (call after writes or to force re-read) */
|
|
150
154
|
export declare function invalidateConfigCache(): void;
|
|
151
|
-
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;
|
|
152
163
|
export declare function writeOhConfig(cfg: OhConfig, root?: string): void;
|
|
153
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/harness/hooks.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - prompt: LLM yes/no check via provider.complete()
|
|
11
11
|
*/
|
|
12
12
|
import type { HookDef } from "./config.js";
|
|
13
|
-
export type HookEvent = "sessionStart" | "sessionEnd" | "preToolUse" | "postToolUse" | "postToolUseFailure" | "userPromptSubmit" | "permissionRequest" | "fileChanged" | "cwdChanged" | "subagentStart" | "subagentStop" | "preCompact" | "postCompact" | "configChange" | "notification";
|
|
13
|
+
export type HookEvent = "sessionStart" | "sessionEnd" | "preToolUse" | "postToolUse" | "postToolUseFailure" | "userPromptSubmit" | "permissionRequest" | "fileChanged" | "cwdChanged" | "subagentStart" | "subagentStop" | "preCompact" | "postCompact" | "configChange" | "notification" | "turnStart" | "turnStop";
|
|
14
14
|
export type HookContext = {
|
|
15
15
|
toolName?: string;
|
|
16
16
|
toolArgs?: string;
|
|
@@ -38,6 +38,10 @@ export type HookContext = {
|
|
|
38
38
|
errorMessage?: string;
|
|
39
39
|
/** For permissionRequest: the decision OH would take absent the hook ("ask", "allow", "deny") — informational */
|
|
40
40
|
permissionAction?: "ask" | "allow" | "deny";
|
|
41
|
+
/** For turnStart/turnStop: zero-indexed turn number within the current session */
|
|
42
|
+
turnNumber?: string;
|
|
43
|
+
/** For turnStop: reason the turn ended ("completed", "max_turns", "error", "interrupted") */
|
|
44
|
+
turnReason?: string;
|
|
41
45
|
};
|
|
42
46
|
/** Clear hook cache (call after config changes) */
|
|
43
47
|
export declare function invalidateHookCache(): void;
|
|
@@ -92,4 +96,22 @@ export type HookOutcome = {
|
|
|
92
96
|
* from hooks is ignored — outcome.allowed is always true. additionalContext is still collected.
|
|
93
97
|
*/
|
|
94
98
|
export declare function emitHookWithOutcome(event: HookEvent, ctx?: HookContext): Promise<HookOutcome>;
|
|
99
|
+
export type HookDecisionNotification = {
|
|
100
|
+
event: HookEvent;
|
|
101
|
+
/** Present when the hook fired in a tool-specific context. */
|
|
102
|
+
tool?: string;
|
|
103
|
+
/** The effective decision. "ask" means defer to the user / default permission flow. */
|
|
104
|
+
decision: "allow" | "deny" | "ask";
|
|
105
|
+
/** Reason returned by the hook, if any. */
|
|
106
|
+
reason?: string;
|
|
107
|
+
};
|
|
108
|
+
type HookDecisionObserver = (n: HookDecisionNotification) => void;
|
|
109
|
+
/**
|
|
110
|
+
* Register (or clear) a single observer that receives one notification per
|
|
111
|
+
* `emitHookWithOutcome` call that produces a decision. Used by
|
|
112
|
+
* `oh run --output-format stream-json` to emit `hook_decision` NDJSON events
|
|
113
|
+
* so the Python SDK can surface permission outcomes in real time.
|
|
114
|
+
*/
|
|
115
|
+
export declare function setHookDecisionObserver(cb: HookDecisionObserver | null): void;
|
|
116
|
+
export {};
|
|
95
117
|
//# sourceMappingURL=hooks.d.ts.map
|
package/dist/harness/hooks.js
CHANGED
|
@@ -67,6 +67,10 @@ function buildEnv(event, ctx) {
|
|
|
67
67
|
env.OH_ERROR_MESSAGE = ctx.errorMessage;
|
|
68
68
|
if (ctx.permissionAction !== undefined)
|
|
69
69
|
env.OH_PERMISSION_ACTION = ctx.permissionAction;
|
|
70
|
+
if (ctx.turnNumber !== undefined)
|
|
71
|
+
env.OH_TURN_NUMBER = ctx.turnNumber;
|
|
72
|
+
if (ctx.turnReason !== undefined)
|
|
73
|
+
env.OH_TURN_REASON = ctx.turnReason;
|
|
70
74
|
return env;
|
|
71
75
|
}
|
|
72
76
|
/**
|
|
@@ -263,6 +267,50 @@ async function runHttpHook(url, event, ctx, timeoutMs = 10_000) {
|
|
|
263
267
|
return false;
|
|
264
268
|
}
|
|
265
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Run an HTTP hook and return its full structured response. POSTs `{event, ...ctx}`
|
|
272
|
+
* as JSON and parses the response body with the same jsonIO envelope shape used
|
|
273
|
+
* by command hooks:
|
|
274
|
+
* { decision?: "allow" | "deny", reason?: string,
|
|
275
|
+
* hookSpecificOutput?: { decision?: "allow" | "deny" | "ask", reason?, additionalContext? } }
|
|
276
|
+
*
|
|
277
|
+
* Also honors a legacy `{ allowed: boolean }` shape (downgrades to decision).
|
|
278
|
+
*
|
|
279
|
+
* Network errors / non-2xx responses / malformed JSON all return a "deny" outcome
|
|
280
|
+
* so the caller can fail closed.
|
|
281
|
+
*/
|
|
282
|
+
async function runHttpHookDetailed(url, event, ctx, timeoutMs = 10_000) {
|
|
283
|
+
try {
|
|
284
|
+
const body = JSON.stringify({ event, ...ctx });
|
|
285
|
+
const res = await fetch(url, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: { "Content-Type": "application/json" },
|
|
288
|
+
body,
|
|
289
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
290
|
+
});
|
|
291
|
+
if (!res.ok)
|
|
292
|
+
return { decision: "deny", reason: `hook HTTP ${res.status}` };
|
|
293
|
+
const text = await res.text();
|
|
294
|
+
if (!text.trim())
|
|
295
|
+
return {};
|
|
296
|
+
const parsed = parseJsonIoResponse(text);
|
|
297
|
+
// If the response only used the legacy `{ allowed: false }` shape, surface as deny.
|
|
298
|
+
if (!parsed.decision && !parsed.permissionDecision) {
|
|
299
|
+
try {
|
|
300
|
+
const legacy = JSON.parse(text);
|
|
301
|
+
if (legacy.allowed === false)
|
|
302
|
+
return { decision: "deny", reason: "hook denied" };
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// already handled by parseJsonIoResponse
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return parsed;
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return { decision: "deny", reason: "hook HTTP error" };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
266
314
|
/**
|
|
267
315
|
* Run a prompt hook. Uses an LLM to make a yes/no allow/deny decision.
|
|
268
316
|
*
|
|
@@ -503,8 +551,7 @@ async function runHookForOutcome(def, event, ctx) {
|
|
|
503
551
|
return mapEnvExitToOutcome(event, code === 0);
|
|
504
552
|
}
|
|
505
553
|
if (def.http) {
|
|
506
|
-
|
|
507
|
-
return mapEnvExitToOutcome(event, allowed);
|
|
554
|
+
return await runHttpHookDetailed(def.http, event, ctx, def.timeout ?? 10_000);
|
|
508
555
|
}
|
|
509
556
|
if (def.prompt) {
|
|
510
557
|
const allowed = await runPromptHook(def.prompt, ctx, def.timeout ?? 10_000);
|
|
@@ -537,20 +584,24 @@ export async function emitHookWithOutcome(event, ctx = {}) {
|
|
|
537
584
|
const parsed = await runHookForOutcome(def, event, ctx);
|
|
538
585
|
if (!notifyOnly) {
|
|
539
586
|
if (parsed.decision === "deny" || parsed.permissionDecision === "deny") {
|
|
540
|
-
|
|
587
|
+
const outcome = {
|
|
541
588
|
allowed: false,
|
|
542
589
|
reason: parsed.reason ?? reason,
|
|
543
590
|
permissionDecision: parsed.permissionDecision,
|
|
544
591
|
};
|
|
592
|
+
notifyHookDecision(event, ctx, outcome);
|
|
593
|
+
return outcome;
|
|
545
594
|
}
|
|
546
595
|
if (parsed.permissionDecision === "allow") {
|
|
547
596
|
if (parsed.additionalContext)
|
|
548
597
|
additionalContexts.push(parsed.additionalContext);
|
|
549
|
-
|
|
598
|
+
const outcome = {
|
|
550
599
|
allowed: true,
|
|
551
600
|
permissionDecision: "allow",
|
|
552
601
|
additionalContext: additionalContexts.length ? additionalContexts.join("\n\n") : undefined,
|
|
553
602
|
};
|
|
603
|
+
notifyHookDecision(event, ctx, outcome);
|
|
604
|
+
return outcome;
|
|
554
605
|
}
|
|
555
606
|
if (parsed.permissionDecision === "ask")
|
|
556
607
|
askSeen = true;
|
|
@@ -560,11 +611,43 @@ export async function emitHookWithOutcome(event, ctx = {}) {
|
|
|
560
611
|
if (!reason && parsed.reason)
|
|
561
612
|
reason = parsed.reason;
|
|
562
613
|
}
|
|
563
|
-
|
|
614
|
+
const outcome = {
|
|
564
615
|
allowed: true,
|
|
565
616
|
additionalContext: additionalContexts.length ? additionalContexts.join("\n\n") : undefined,
|
|
566
617
|
permissionDecision: askSeen ? "ask" : undefined,
|
|
567
618
|
reason,
|
|
568
619
|
};
|
|
620
|
+
notifyHookDecision(event, ctx, outcome);
|
|
621
|
+
return outcome;
|
|
622
|
+
}
|
|
623
|
+
let hookDecisionObserver = null;
|
|
624
|
+
/**
|
|
625
|
+
* Register (or clear) a single observer that receives one notification per
|
|
626
|
+
* `emitHookWithOutcome` call that produces a decision. Used by
|
|
627
|
+
* `oh run --output-format stream-json` to emit `hook_decision` NDJSON events
|
|
628
|
+
* so the Python SDK can surface permission outcomes in real time.
|
|
629
|
+
*/
|
|
630
|
+
export function setHookDecisionObserver(cb) {
|
|
631
|
+
hookDecisionObserver = cb;
|
|
632
|
+
}
|
|
633
|
+
function notifyHookDecision(event, ctx, outcome) {
|
|
634
|
+
if (!hookDecisionObserver)
|
|
635
|
+
return;
|
|
636
|
+
// Prefer the richer permissionDecision when present; fall back to deny if the hook blocked.
|
|
637
|
+
let decision = outcome.permissionDecision;
|
|
638
|
+
if (!decision) {
|
|
639
|
+
if (!outcome.allowed)
|
|
640
|
+
decision = "deny";
|
|
641
|
+
else if (outcome.reason)
|
|
642
|
+
decision = "allow";
|
|
643
|
+
}
|
|
644
|
+
if (!decision)
|
|
645
|
+
return;
|
|
646
|
+
try {
|
|
647
|
+
hookDecisionObserver({ event, tool: ctx.toolName, decision, reason: outcome.reason });
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
// Observer errors must not break the hook pipeline.
|
|
651
|
+
}
|
|
569
652
|
}
|
|
570
653
|
//# sourceMappingURL=hooks.js.map
|
package/dist/main.js
CHANGED
|
@@ -15,8 +15,8 @@ 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";
|
|
19
|
-
import { emitHook } from "./harness/hooks.js";
|
|
18
|
+
import { parseSettingSources, readOhConfig } from "./harness/config.js";
|
|
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";
|
|
22
22
|
import { discoverSkills, skillsToPrompt } from "./harness/plugins.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,50 @@ program
|
|
|
189
192
|
let fullOutput = "";
|
|
190
193
|
const toolResults = [];
|
|
191
194
|
const callIdToName = {};
|
|
192
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
219
|
+
setHookDecisionObserver((n) => {
|
|
220
|
+
console.log(JSON.stringify({
|
|
221
|
+
type: "hook_decision",
|
|
222
|
+
event: n.event,
|
|
223
|
+
tool: n.tool,
|
|
224
|
+
decision: n.decision,
|
|
225
|
+
reason: n.reason,
|
|
226
|
+
}));
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
emitHook("turnStart", {
|
|
230
|
+
turnNumber: "0",
|
|
231
|
+
model,
|
|
232
|
+
provider: typeof config.provider === "string" ? config.provider : undefined,
|
|
233
|
+
permissionMode,
|
|
234
|
+
});
|
|
235
|
+
if (outputFormat === "stream-json") {
|
|
236
|
+
console.log(JSON.stringify({ type: "turnStart", turnNumber: 0 }));
|
|
237
|
+
}
|
|
238
|
+
for await (const event of query(prompt, config, priorMessages)) {
|
|
193
239
|
if (event.type === "text_delta") {
|
|
194
240
|
fullOutput += event.content;
|
|
195
241
|
if (outputFormat === "text")
|
|
@@ -245,6 +291,10 @@ program
|
|
|
245
291
|
if (outputFormat === "stream-json") {
|
|
246
292
|
console.log(JSON.stringify({ type: "turn_complete", reason: event.reason }));
|
|
247
293
|
}
|
|
294
|
+
emitHook("turnStop", { turnNumber: "0", turnReason: event.reason, model, permissionMode });
|
|
295
|
+
if (outputFormat === "stream-json") {
|
|
296
|
+
console.log(JSON.stringify({ type: "turnStop", turnNumber: 0, reason: event.reason }));
|
|
297
|
+
}
|
|
248
298
|
if (event.reason !== "completed") {
|
|
249
299
|
process.exitCode = 1;
|
|
250
300
|
}
|
|
@@ -269,8 +319,11 @@ program
|
|
|
269
319
|
.option("--disallowed-tools <tools>", "Comma-separated disallowed tool names")
|
|
270
320
|
.option("--max-turns <n>", "Maximum turns per prompt", "20")
|
|
271
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).")
|
|
272
324
|
.action(async (opts) => {
|
|
273
|
-
const
|
|
325
|
+
const settingSources = parseSettingSources(opts.settingSources);
|
|
326
|
+
const savedConfig = readOhConfig(undefined, settingSources);
|
|
274
327
|
const permissionMode = (opts.permissionMode ??
|
|
275
328
|
savedConfig?.permissionMode ??
|
|
276
329
|
"trust");
|
|
@@ -303,9 +356,37 @@ program
|
|
|
303
356
|
model,
|
|
304
357
|
};
|
|
305
358
|
// Conversation history, shared across all prompts for this process.
|
|
359
|
+
// Seeded from a prior session when --resume <id> is passed.
|
|
306
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
|
+
}
|
|
374
|
+
let turnCounter = 0;
|
|
375
|
+
// Will be set to the current prompt id before each turn so hook_decision
|
|
376
|
+
// events can be demultiplexed by the client.
|
|
377
|
+
let activePromptId = "";
|
|
378
|
+
setHookDecisionObserver((n) => {
|
|
379
|
+
console.log(JSON.stringify({
|
|
380
|
+
id: activePromptId,
|
|
381
|
+
type: "hook_decision",
|
|
382
|
+
event: n.event,
|
|
383
|
+
tool: n.tool,
|
|
384
|
+
decision: n.decision,
|
|
385
|
+
reason: n.reason,
|
|
386
|
+
}));
|
|
387
|
+
});
|
|
307
388
|
// Announce readiness so the client can send the first prompt.
|
|
308
|
-
console.log(JSON.stringify({ type: "ready" }));
|
|
389
|
+
console.log(JSON.stringify({ type: "ready", sessionId }));
|
|
309
390
|
const readline = await import("node:readline");
|
|
310
391
|
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
311
392
|
for await (const rawLine of rl) {
|
|
@@ -333,6 +414,16 @@ program
|
|
|
333
414
|
const turnToolCalls = [];
|
|
334
415
|
const callIdToName = {};
|
|
335
416
|
const toolResults = [];
|
|
417
|
+
const turnIdx = turnCounter++;
|
|
418
|
+
const turnNumber = String(turnIdx);
|
|
419
|
+
activePromptId = id;
|
|
420
|
+
emitHook("turnStart", {
|
|
421
|
+
turnNumber,
|
|
422
|
+
model,
|
|
423
|
+
provider: typeof config.provider === "string" ? config.provider : undefined,
|
|
424
|
+
permissionMode,
|
|
425
|
+
});
|
|
426
|
+
console.log(JSON.stringify({ id, type: "turnStart", turnNumber: turnIdx }));
|
|
336
427
|
for await (const event of query(prompt, config, conversation)) {
|
|
337
428
|
if (event.type === "text_delta") {
|
|
338
429
|
assistantText += event.content;
|
|
@@ -374,6 +465,8 @@ program
|
|
|
374
465
|
}
|
|
375
466
|
else if (event.type === "turn_complete") {
|
|
376
467
|
console.log(JSON.stringify({ id, type: "turn_complete", reason: event.reason }));
|
|
468
|
+
emitHook("turnStop", { turnNumber, turnReason: event.reason, model, permissionMode });
|
|
469
|
+
console.log(JSON.stringify({ id, type: "turnStop", turnNumber: turnIdx, reason: event.reason }));
|
|
377
470
|
}
|
|
378
471
|
}
|
|
379
472
|
// Rebuild this turn's contribution to the conversation.
|
|
@@ -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;
|