@dungle-scrubs/harness-cli-normalizer 0.5.5 → 0.5.7
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/README.md +3 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +16 -8
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +11 -4
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/help.d.ts +3 -3
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +32 -23
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +10 -7
- package/dist/cli/inspect.js.map +1 -1
- package/dist/cli/resume-guard.d.ts.map +1 -1
- package/dist/cli/resume-guard.js +59 -2
- package/dist/cli/resume-guard.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +31 -32
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/session-json.d.ts +5 -2
- package/dist/cli/session-json.d.ts.map +1 -1
- package/dist/cli/session-json.js +5 -3
- package/dist/cli/session-json.js.map +1 -1
- package/dist/cli/session.d.ts.map +1 -1
- package/dist/cli/session.js +103 -36
- package/dist/cli/session.js.map +1 -1
- package/dist/execution/events.d.ts +8 -1
- package/dist/execution/events.d.ts.map +1 -1
- package/dist/execution/events.js.map +1 -1
- package/dist/execution/open-session.d.ts +7 -6
- package/dist/execution/open-session.d.ts.map +1 -1
- package/dist/execution/open-session.js +88 -59
- package/dist/execution/open-session.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +4 -7
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +43 -22
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +9 -5
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +2 -7
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/capabilities.d.ts +20 -0
- package/dist/interpretation/capabilities.d.ts.map +1 -1
- package/dist/interpretation/capabilities.js +27 -0
- package/dist/interpretation/capabilities.js.map +1 -1
- package/dist/interpretation/hints.js +1 -1
- package/dist/interpretation/hints.js.map +1 -1
- package/dist/interpretation/question.d.ts +6 -2
- package/dist/interpretation/question.d.ts.map +1 -1
- package/dist/interpretation/question.js +13 -4
- package/dist/interpretation/question.js.map +1 -1
- package/dist/interpretation/refusal.d.ts +1 -1
- package/dist/interpretation/refusal.d.ts.map +1 -1
- package/dist/interpretation/refusal.js +1 -1
- package/dist/interpretation/refusal.js.map +1 -1
- package/dist/interpretation/skills-selection.d.ts +18 -1
- package/dist/interpretation/skills-selection.d.ts.map +1 -1
- package/dist/interpretation/skills-selection.js +33 -6
- package/dist/interpretation/skills-selection.js.map +1 -1
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +21 -0
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +26 -7
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +39 -12
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/descriptor.js +1 -0
- package/dist/knowledge/descriptor.js.map +1 -1
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +7 -0
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +23 -4
- package/dist/knowledge/pi.js.map +1 -1
- package/package.json +3 -2
- package/src/cli/args.ts +16 -6
- package/src/cli/config.ts +13 -4
- package/src/cli/help.ts +32 -23
- package/src/cli/inspect.ts +8 -6
- package/src/cli/resume-guard.ts +54 -2
- package/src/cli/run.ts +32 -36
- package/src/cli/session-json.ts +11 -5
- package/src/cli/session.ts +122 -40
- package/src/execution/events.ts +9 -1
- package/src/execution/open-session.ts +99 -72
- package/src/execution/stream-turn.ts +53 -30
- package/src/interpretation/argv.ts +11 -12
- package/src/interpretation/capabilities.ts +50 -0
- package/src/interpretation/hints.ts +1 -1
- package/src/interpretation/question.ts +17 -7
- package/src/interpretation/refusal.ts +2 -1
- package/src/interpretation/skills-selection.ts +46 -8
- package/src/knowledge/claude-code.ts +21 -0
- package/src/knowledge/codex.ts +26 -7
- package/src/knowledge/descriptor.ts +40 -12
- package/src/knowledge/muse.ts +7 -0
- package/src/knowledge/pi.ts +23 -4
package/src/cli/help.ts
CHANGED
|
@@ -48,12 +48,12 @@ Options:
|
|
|
48
48
|
--no-write Disable write
|
|
49
49
|
--shell Enable shell (muse)
|
|
50
50
|
--no-shell Disable shell
|
|
51
|
-
--
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
done cause "awaiting-input", exit 0)
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
--questions <ask|assume|none>
|
|
52
|
+
Which preamble to inject: ask = escalation
|
|
53
|
+
protocol (DEFAULT, question event +
|
|
54
|
+
done cause "awaiting-input", exit 0), assume =
|
|
55
|
+
never ask, state assumption, none = inject
|
|
56
|
+
nothing
|
|
57
57
|
--system-prompt <text> Replace the built-in system prompt (claude, pi:
|
|
58
58
|
flag; codex: -c instructions=<text-or-path>;
|
|
59
59
|
muse refuses. claude pairs the dynamic-section
|
|
@@ -78,12 +78,15 @@ Options:
|
|
|
78
78
|
--no-skills Disable skills discovery facet
|
|
79
79
|
--cwd <path> Working directory for spawn
|
|
80
80
|
--env KEY=VAL Environment (repeatable; KEY= deletes)
|
|
81
|
-
--resume <uuid> Resume session id (UUID).
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
--resume <uuid> Resume session id (UUID). Continues the
|
|
82
|
+
conversation where it left off. The answer
|
|
83
|
+
path for question escalation: resume with the
|
|
84
|
+
chosen answer as the prompt; id continuity per
|
|
84
85
|
harness (claude stable, pi/muse caller-assigned,
|
|
85
86
|
codex minted via identity event)
|
|
86
|
-
Note:
|
|
87
|
+
Note: hcn refuses an unknown id before spawn
|
|
88
|
+
(exit 2) for harnesses that would otherwise
|
|
89
|
+
create a fresh session silently (pi, muse)
|
|
87
90
|
--session-id <uuid> Alias for --resume (mutually exclusive with --resume)
|
|
88
91
|
-- Passthrough: native harness args verbatim
|
|
89
92
|
(failures surface as labeled native errors)
|
|
@@ -102,9 +105,8 @@ Defaults with no flags:
|
|
|
102
105
|
profile). toolMap is config-only (no flag) - canonical -> native
|
|
103
106
|
mapping per harness. A bare pi run renders no --tools list: pi's list
|
|
104
107
|
is a strict allowlist and would drop extension and MCP tools; name
|
|
105
|
-
them through toolMap or native:<name> when you grant. Question
|
|
106
|
-
|
|
107
|
-
"escalateQuestions"; it is a prompt preamble, never a harness flag).
|
|
108
|
+
them through toolMap or native:<name> when you grant. Question mode defaults to ask (config keys
|
|
109
|
+
"questions"; prompt preamble, never a harness flag).
|
|
108
110
|
Provenance prints to stderr on every run; see
|
|
109
111
|
'hcn inspect <harness>' for the resolved argv of a bare run.
|
|
110
112
|
`;
|
|
@@ -120,8 +122,9 @@ Options:
|
|
|
120
122
|
--json Machine surface: NDJSON events on stdout, NDJSON
|
|
121
123
|
commands on stdin ({"op":"send","id":..,"text":..},
|
|
122
124
|
"answer", "close"). Every send is answered with one
|
|
123
|
-
disposition (started |
|
|
124
|
-
|
|
125
|
+
disposition (started | rejected); every send is
|
|
126
|
+
handed to the harness, and the turn that consumes it
|
|
127
|
+
carries its id. The
|
|
125
128
|
stream opens with a session event and ends with a
|
|
126
129
|
closed event. Exit 0 clean, 1 otherwise, 2 refusal.
|
|
127
130
|
--stall <seconds> Per-turn inactivity budget; the turn ends and the
|
|
@@ -129,12 +132,18 @@ Options:
|
|
|
129
132
|
(default: no limit)
|
|
130
133
|
--provider <value> Provider (pi only)
|
|
131
134
|
--model <id> Model for the session
|
|
132
|
-
--
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
--resume <uuid> Resume session id (UUID). Continues the
|
|
136
|
+
conversation where it left off. --session-id
|
|
137
|
+
is an alias (mutually exclusive with --resume).
|
|
138
|
+
If no id is given, a fresh session is started
|
|
139
|
+
with a new id. hcn refuses an unknown id before
|
|
140
|
+
spawn (exit 2) for harnesses that would otherwise
|
|
141
|
+
create a fresh session silently (pi)
|
|
142
|
+
--session-id <uuid> Alias for --resume (mutually exclusive with --resume)
|
|
143
|
+
--questions <ask|assume|none>
|
|
144
|
+
Which preamble to inject: ask = worker may
|
|
145
|
+
ask (DEFAULT, pickable menu), assume = never
|
|
146
|
+
ask, none = inject nothing
|
|
138
147
|
--cwd <path> Working directory
|
|
139
148
|
--help Show help
|
|
140
149
|
--version Show version
|
|
@@ -167,11 +176,11 @@ Options:
|
|
|
167
176
|
--autonomy / --no-autonomy
|
|
168
177
|
--write / --no-write
|
|
169
178
|
--shell / --no-shell
|
|
170
|
-
--
|
|
179
|
+
--questions <ask|assume|none>
|
|
171
180
|
(accepted; renders nothing - rides the run prompt)
|
|
172
181
|
--max-steps <n>
|
|
173
182
|
--no-tools, --no-instruction-files, --no-extensions, --no-skills
|
|
174
|
-
--
|
|
183
|
+
--questions <ask|assume|none>
|
|
175
184
|
Accepted; renders nothing in argv (the mode
|
|
176
185
|
rides the run prompt, not a harness flag)
|
|
177
186
|
--cwd <path>
|
package/src/cli/inspect.ts
CHANGED
|
@@ -210,13 +210,16 @@ export const inspect = async (harnessName: string, rawArgs: string[]): Promise<v
|
|
|
210
210
|
try {
|
|
211
211
|
const { resolveSkillNames, listKnownSkills } = await import("./skills-root.js");
|
|
212
212
|
const resolvedSkills = resolveSkillNames(rawSkills, h.name);
|
|
213
|
-
const
|
|
213
|
+
const skillTokens: string[] = [];
|
|
214
214
|
if (h.name === "claude") {
|
|
215
215
|
const { claudeSkillOverridesArg } = await import("../interpretation/skills-selection.js");
|
|
216
|
-
|
|
216
|
+
skillTokens.push(...claudeSkillOverridesArg(listKnownSkills(), resolvedSkills));
|
|
217
|
+
} else if (h.name === "codex") {
|
|
218
|
+
const { codexSkillConfigArg } = await import("../interpretation/skills-selection.js");
|
|
219
|
+
skillTokens.push(...codexSkillConfigArg(listKnownSkills(), resolvedSkills));
|
|
217
220
|
}
|
|
218
221
|
(turnOpts as unknown as Record<string, unknown>).skills = resolvedSkills;
|
|
219
|
-
(turnOpts as unknown as Record<string, unknown>).
|
|
222
|
+
(turnOpts as unknown as Record<string, unknown>).__skillTokens = skillTokens;
|
|
220
223
|
} catch (err) {
|
|
221
224
|
if (err instanceof ArgvRefusalError) {
|
|
222
225
|
process.stderr.write(`${err.message}\n`);
|
|
@@ -301,9 +304,8 @@ export const inspect = async (harnessName: string, rawArgs: string[]): Promise<v
|
|
|
301
304
|
let argv: string[];
|
|
302
305
|
try {
|
|
303
306
|
argv = buildLaunchArgv(h, fullOpts);
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
if (claudeTokens !== undefined && claudeTokens.length > 0) argv.push(...claudeTokens);
|
|
307
|
+
const skillTokens = (effectiveRest as unknown as { __skillTokens?: string[] }).__skillTokens;
|
|
308
|
+
if (skillTokens !== undefined && skillTokens.length > 0) argv.push(...skillTokens);
|
|
307
309
|
} catch (err) {
|
|
308
310
|
if (err instanceof ArgvRefusalError) {
|
|
309
311
|
refuse(refusalOf(err as ArgvRefusalError), false);
|
package/src/cli/resume-guard.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { existsSync, realpathSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { storePath } from "../interpretation/store.js";
|
|
3
4
|
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
4
5
|
|
|
@@ -31,5 +32,56 @@ export const resumeStore = (
|
|
|
31
32
|
} catch {
|
|
32
33
|
path = null;
|
|
33
34
|
}
|
|
34
|
-
return { path, exists: path !== null &&
|
|
35
|
+
return { path, exists: path !== null && sessionExistsAt(path, opts.sessionId) };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** A store template that resolves to a FILE names the session directly and
|
|
39
|
+
* existence is the file's. One that resolves to a DIRECTORY names where the
|
|
40
|
+
* harness files every session, and the session is an entry somewhere under it
|
|
41
|
+
* whose name carries the id - not at a position a template can express:
|
|
42
|
+
*
|
|
43
|
+
* pi <root>/<cwdSlug>/<timestamp>_<id>.jsonl one level down
|
|
44
|
+
* muse <root>/YYYY/MM/DD/<id>/session.jsonl three levels down
|
|
45
|
+
*
|
|
46
|
+
* The first version of this search looked one level only, written against
|
|
47
|
+
* pi, and refused every muse resume (#103) for a session that was right
|
|
48
|
+
* there. It now walks the tree, bounded: the id's own directory is a match
|
|
49
|
+
* and is not descended, and depth is capped so a pathological store cannot
|
|
50
|
+
* turn a pre-spawn check into a crawl. */
|
|
51
|
+
const MAX_STORE_DEPTH = 4;
|
|
52
|
+
|
|
53
|
+
const sessionExistsAt = (path: string, sessionId: string): boolean => {
|
|
54
|
+
if (!existsSync(path)) return false;
|
|
55
|
+
let isDir = false;
|
|
56
|
+
try {
|
|
57
|
+
isDir = statSync(path).isDirectory();
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (!isDir) return true;
|
|
62
|
+
return containsSessionId(path, sessionId, 0);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const containsSessionId = (dir: string, sessionId: string, depth: number): boolean => {
|
|
66
|
+
if (depth > MAX_STORE_DEPTH) return false;
|
|
67
|
+
let entries: string[];
|
|
68
|
+
try {
|
|
69
|
+
entries = readdirSync(dir);
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
for (const name of entries) {
|
|
74
|
+
if (name.includes(sessionId)) return true;
|
|
75
|
+
}
|
|
76
|
+
for (const name of entries) {
|
|
77
|
+
const child = join(dir, name);
|
|
78
|
+
let childIsDir = false;
|
|
79
|
+
try {
|
|
80
|
+
childIsDir = statSync(child).isDirectory();
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (childIsDir && containsSessionId(child, sessionId, depth + 1)) return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
35
87
|
};
|
package/src/cli/run.ts
CHANGED
|
@@ -185,13 +185,16 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
185
185
|
try {
|
|
186
186
|
const { resolveSkillNames, listKnownSkills } = await import("./skills-root.js");
|
|
187
187
|
const resolvedSkills = resolveSkillNames(rawSkills, h.name);
|
|
188
|
-
const
|
|
188
|
+
const skillTokens: string[] = [];
|
|
189
189
|
if (h.name === "claude") {
|
|
190
190
|
const { claudeSkillOverridesArg } = await import("../interpretation/skills-selection.js");
|
|
191
|
-
|
|
191
|
+
skillTokens.push(...claudeSkillOverridesArg(listKnownSkills(), resolvedSkills));
|
|
192
|
+
} else if (h.name === "codex") {
|
|
193
|
+
const { codexSkillConfigArg } = await import("../interpretation/skills-selection.js");
|
|
194
|
+
skillTokens.push(...codexSkillConfigArg(listKnownSkills(), resolvedSkills));
|
|
192
195
|
}
|
|
193
196
|
(turnOpts as unknown as Record<string, unknown>).skills = resolvedSkills;
|
|
194
|
-
(turnOpts as unknown as Record<string, unknown>).
|
|
197
|
+
(turnOpts as unknown as Record<string, unknown>).__skillTokens = skillTokens;
|
|
195
198
|
} catch (err) {
|
|
196
199
|
if (err instanceof ArgvRefusalError) {
|
|
197
200
|
refuse(refusalOf(err), wantJson);
|
|
@@ -209,9 +212,9 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
209
212
|
project?: Partial<ReturnType<typeof parseTurnOptions>>;
|
|
210
213
|
} = {};
|
|
211
214
|
// Config files load on EVERY run, launch or resume: the tiers feed the
|
|
212
|
-
// defaults profile on launch, and issue #41's
|
|
215
|
+
// defaults profile on launch, and issue #41's questions (a
|
|
213
216
|
// behavior instruction, not a turn option) resolves from them on resume
|
|
214
|
-
// too - otherwise a no-
|
|
217
|
+
// too - otherwise a no-ask session would flip its preamble on the
|
|
215
218
|
// answer turn. Resolution of TURN options stays launch-only.
|
|
216
219
|
const tiers = resolvedTiers;
|
|
217
220
|
{
|
|
@@ -256,39 +259,34 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
256
259
|
writeProvenance(h.name, provenance, unrenderable);
|
|
257
260
|
}
|
|
258
261
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
: userEscalate !== undefined
|
|
274
|
-
? userEscalate
|
|
275
|
-
: true;
|
|
276
|
-
const escalateTier =
|
|
277
|
-
turnOpts.escalateQuestions !== undefined
|
|
262
|
+
// question mode precedence arg > project > user > default (ask)
|
|
263
|
+
const projectQuestions = (resolvedTiers.project as { questions?: string } | undefined)?.questions;
|
|
264
|
+
const userQuestions = (resolvedTiers.user as { questions?: string } | undefined)?.questions;
|
|
265
|
+
const rawMode = (turnOpts as { questions?: string }).questions;
|
|
266
|
+
const questionMode =
|
|
267
|
+
rawMode !== undefined
|
|
268
|
+
? (rawMode as import("../interpretation/question.js").QuestionMode)
|
|
269
|
+
: projectQuestions !== undefined
|
|
270
|
+
? (projectQuestions as import("../interpretation/question.js").QuestionMode)
|
|
271
|
+
: userQuestions !== undefined
|
|
272
|
+
? (userQuestions as import("../interpretation/question.js").QuestionMode)
|
|
273
|
+
: ("ask" as const);
|
|
274
|
+
const questionTier =
|
|
275
|
+
rawMode !== undefined
|
|
278
276
|
? "arg"
|
|
279
|
-
:
|
|
277
|
+
: projectQuestions !== undefined
|
|
280
278
|
? "project-config"
|
|
281
|
-
:
|
|
279
|
+
: userQuestions !== undefined
|
|
282
280
|
? "user-config"
|
|
283
281
|
: "default";
|
|
284
282
|
|
|
285
283
|
const fullOpts = {
|
|
286
284
|
...effectiveTurnOpts,
|
|
287
|
-
prompt: composeEscalatedPrompt(prompt,
|
|
285
|
+
prompt: composeEscalatedPrompt(prompt, questionMode),
|
|
288
286
|
cwd: extra.cwd,
|
|
289
287
|
env: extra.env,
|
|
290
288
|
resume: extra.resume,
|
|
291
|
-
|
|
289
|
+
questions: questionMode,
|
|
292
290
|
...(passthrough.length > 0 ? { passthrough } : {}),
|
|
293
291
|
...(isExplicit ? { __explicitPrompt: true as const } : {}),
|
|
294
292
|
} as Parameters<typeof streamTurn>[1] & {
|
|
@@ -304,7 +302,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
304
302
|
if (fullOpts.resume) {
|
|
305
303
|
// Resume never carries TURN-option profile resolution (launch-only
|
|
306
304
|
// rule), so it builds from the raw turn options; hcn-owned behavior
|
|
307
|
-
// (
|
|
305
|
+
// (questions preamble, timeout budget) still applies.
|
|
308
306
|
preArgv = buildResumeArgv(h, {
|
|
309
307
|
...(turnOpts as object),
|
|
310
308
|
prompt: fullOpts.prompt,
|
|
@@ -321,10 +319,10 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
321
319
|
prompt: fullOpts.prompt,
|
|
322
320
|
__explicitPrompt: isExplicit,
|
|
323
321
|
} as never);
|
|
324
|
-
const
|
|
325
|
-
.
|
|
326
|
-
if (
|
|
327
|
-
preArgv.push(...
|
|
322
|
+
const skillTokens = (effectiveTurnOpts as unknown as { __skillTokens?: string[] })
|
|
323
|
+
.__skillTokens;
|
|
324
|
+
if (skillTokens !== undefined && skillTokens.length > 0) {
|
|
325
|
+
preArgv.push(...skillTokens);
|
|
328
326
|
}
|
|
329
327
|
}
|
|
330
328
|
_validated = true;
|
|
@@ -349,9 +347,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
349
347
|
}
|
|
350
348
|
// issue #41: the escalation mode rides stderr as provenance, like
|
|
351
349
|
// every other resolution the turn depends on.
|
|
352
|
-
process.stderr.write(
|
|
353
|
-
`provenance: escalateQuestions = ${escalateQuestions} (${escalateTier})\n`,
|
|
354
|
-
);
|
|
350
|
+
process.stderr.write(`provenance: questions = ${questionMode} (${questionTier})\n`);
|
|
355
351
|
// On a harness whose include flag is not a strict allowlist (claude),
|
|
356
352
|
// a name outside the curated set passes through ungated; say which
|
|
357
353
|
// ones so a wrong-case name is visible. A grant with no known name at
|
package/src/cli/session-json.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* with the control events (`session`, `turn`, `disposition`, `closed`) that
|
|
5
5
|
* a program needs to drive a session it does not own the timing of. RFC-01.
|
|
6
6
|
*
|
|
7
|
-
* This owns the wire framing only. The turn lifecycle
|
|
7
|
+
* This owns the wire framing only. The turn lifecycle and the id
|
|
8
8
|
* correlation live in `openSession`; this reads the id off the yielded turn
|
|
9
9
|
* rather than shadowing the runner's delivery order.
|
|
10
10
|
*/
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type SessionHandle,
|
|
17
17
|
type SessionSendResult,
|
|
18
18
|
} from "../execution/open-session.js";
|
|
19
|
+
import type { QuestionMode } from "../interpretation/question.js";
|
|
19
20
|
|
|
20
21
|
/** What the CLI reads back after a close, captured from the runner's
|
|
21
22
|
* `session_close` boundary log. */
|
|
@@ -24,15 +25,18 @@ export interface CloseInfo {
|
|
|
24
25
|
cause: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
export type SessionOrigin = "fresh" | "resumed";
|
|
29
|
+
|
|
27
30
|
export interface JsonSessionArgs {
|
|
28
31
|
readonly handle: SessionHandle;
|
|
29
32
|
readonly sessionId: string;
|
|
30
33
|
readonly harness: string;
|
|
31
34
|
readonly hcnVersion: string;
|
|
32
|
-
readonly
|
|
35
|
+
readonly questions?: QuestionMode;
|
|
36
|
+
readonly origin: SessionOrigin;
|
|
33
37
|
/** Read after close - the runner's final exitCode and cause. */
|
|
34
38
|
readonly getCloseInfo: () => CloseInfo;
|
|
35
|
-
/** Read after close - ids the runner accepted
|
|
39
|
+
/** Read after close - ids the runner accepted and never
|
|
36
40
|
* delivered. Each owes the consumer a rejection (RFC S003). */
|
|
37
41
|
readonly getDroppedIds?: () => readonly string[];
|
|
38
42
|
/** Injected for tests; defaults to process.stdin / process.stdout. */
|
|
@@ -116,12 +120,14 @@ export const runJsonSession = async (a: JsonSessionArgs): Promise<number> => {
|
|
|
116
120
|
return chain;
|
|
117
121
|
};
|
|
118
122
|
|
|
123
|
+
const qMode: QuestionMode = a.questions ?? "ask";
|
|
119
124
|
await emit({
|
|
120
125
|
kind: "session",
|
|
121
126
|
sessionId: a.sessionId,
|
|
122
127
|
harness: a.harness,
|
|
123
128
|
hcn: a.hcnVersion,
|
|
124
|
-
|
|
129
|
+
questions: qMode,
|
|
130
|
+
origin: a.origin,
|
|
125
131
|
});
|
|
126
132
|
|
|
127
133
|
// Shared between the pumps: the last question asked, whether the last turn
|
|
@@ -242,7 +248,7 @@ export const runJsonSession = async (a: JsonSessionArgs): Promise<number> => {
|
|
|
242
248
|
}
|
|
243
249
|
unanswered.clear();
|
|
244
250
|
|
|
245
|
-
// Every input the runner accepted
|
|
251
|
+
// Every input the runner accepted and then lost gets its own
|
|
246
252
|
// rejection, before the terminal line (RFC S003).
|
|
247
253
|
for (const id of a.getDroppedIds?.() ?? []) {
|
|
248
254
|
await emit({ kind: "disposition", id, disposition: "rejected", reason: "closed" });
|
package/src/cli/session.ts
CHANGED
|
@@ -57,45 +57,96 @@ export const session = async (harnessName: string, rawArgs: string[]): Promise<v
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const values = parsed.values as Record<string, unknown>;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
// --resume and --session-id are aliases for one concept: resume an existing
|
|
61
|
+
// session. Passing both is refused, matching hcn run's parseRunExtra shape
|
|
62
|
+
// (src/cli/args.ts) rather than inventing a second parser.
|
|
63
|
+
if (values.resume !== undefined && values["session-id"] !== undefined) {
|
|
64
|
+
const { ArgvRefusalError: AliasError } = await import("../interpretation/refusal.js");
|
|
65
|
+
const err = new AliasError({
|
|
66
|
+
issue: "mutually-exclusive-options",
|
|
67
|
+
harness: h.name,
|
|
68
|
+
supported: ["--resume or --session-id, not both (--session-id is an alias for --resume)"],
|
|
69
|
+
detail: "both --resume and --session-id given",
|
|
70
|
+
});
|
|
71
|
+
const { refusalOf: aliasRefusalOf, refuse: aliasRefuse } = await import("./refuse.js");
|
|
72
|
+
aliasRefuse(aliasRefusalOf(err), jsonMode, "closed");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const resumeId =
|
|
76
|
+
(values.resume as string | undefined) ?? (values["session-id"] as string | undefined);
|
|
77
|
+
// The two flags are aliases for "use this session". Whether that means
|
|
78
|
+
// RESUME is decided by the store, not by which spelling was typed: an id
|
|
79
|
+
// that exists is resumed (resumeFlag rendered), an id that does not exist
|
|
80
|
+
// names a fresh session (idFlag rendered). --resume on an id that does not
|
|
81
|
+
// exist is the one combination refused, below - a caller who said "resume"
|
|
82
|
+
// and would silently get a fresh conversation is issue #86 exactly.
|
|
83
|
+
const explicitResume = values.resume !== undefined;
|
|
84
|
+
// A caller-supplied id must be the shape this harness's store files
|
|
85
|
+
// sessions under (issue #95). hcn run already holds this line through
|
|
86
|
+
// parse-resume; session only checked the safe-filename rule, so "bogus"
|
|
87
|
+
// reached the harness, rode the session and identity events, and on a
|
|
88
|
+
// create-on-unknown harness became a real file nobody can find again.
|
|
89
|
+
if (resumeId !== undefined && !h.resume.idShape.test(resumeId)) {
|
|
90
|
+
const flag = explicitResume ? "--resume" : "--session-id";
|
|
91
|
+
const { refuse: shapeRefuse } = await import("./refuse.js");
|
|
92
|
+
shapeRefuse(
|
|
93
|
+
{
|
|
94
|
+
message: `${flag} ${JSON.stringify(resumeId.slice(0, 64))} is not a ${h.name} session id`,
|
|
95
|
+
issue: "invalid-option-value",
|
|
96
|
+
supported: [`a session id matching ${String(h.resume.idShape)}`],
|
|
97
|
+
},
|
|
98
|
+
jsonMode,
|
|
99
|
+
"closed",
|
|
100
|
+
);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const sessionId = resumeId ?? randomUUID();
|
|
104
|
+
let isResume = false;
|
|
64
105
|
const model = values.model as string | undefined;
|
|
65
106
|
const cwd = values.cwd as string | undefined;
|
|
66
107
|
const provider = values.provider as string | undefined;
|
|
67
108
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
109
|
+
// question mode precedence arg > project > user > default (ask)
|
|
110
|
+
const rawArgMode = values.questions !== undefined ? String(values.questions) : undefined;
|
|
111
|
+
if (rawArgMode !== undefined && !["ask", "assume", "none"].includes(rawArgMode)) {
|
|
112
|
+
const { refuse, refusalOf } = await import("./refuse.js");
|
|
113
|
+
const { ArgvRefusalError } = await import("../interpretation/refusal.js");
|
|
114
|
+
refuse(
|
|
115
|
+
refusalOf(
|
|
116
|
+
new ArgvRefusalError({
|
|
117
|
+
issue: "invalid-option-value",
|
|
118
|
+
harness: h.name,
|
|
119
|
+
option: "questions",
|
|
120
|
+
supported: ["ask", "assume", "none"],
|
|
121
|
+
detail: rawArgMode,
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
jsonMode,
|
|
125
|
+
"closed",
|
|
126
|
+
);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let questionMode: import("../interpretation/question.js").QuestionMode;
|
|
130
|
+
let questionTier: "arg" | "project-config" | "user-config" | "default";
|
|
79
131
|
try {
|
|
80
132
|
const { loadUserConfig, loadProjectConfig } = await import("./config.js");
|
|
81
|
-
const user = loadUserConfig()?.config as {
|
|
82
|
-
const project = loadProjectConfig()?.config as {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
: "default";
|
|
133
|
+
const user = loadUserConfig()?.config as { questions?: string } | undefined;
|
|
134
|
+
const project = loadProjectConfig()?.config as { questions?: string } | undefined;
|
|
135
|
+
const userMode = user?.questions;
|
|
136
|
+
const projectMode = project?.questions;
|
|
137
|
+
if (rawArgMode !== undefined) {
|
|
138
|
+
questionMode = rawArgMode as import("../interpretation/question.js").QuestionMode;
|
|
139
|
+
questionTier = "arg";
|
|
140
|
+
} else if (projectMode !== undefined) {
|
|
141
|
+
questionMode = projectMode as import("../interpretation/question.js").QuestionMode;
|
|
142
|
+
questionTier = "project-config";
|
|
143
|
+
} else if (userMode !== undefined) {
|
|
144
|
+
questionMode = userMode as import("../interpretation/question.js").QuestionMode;
|
|
145
|
+
questionTier = "user-config";
|
|
146
|
+
} else {
|
|
147
|
+
questionMode = "ask";
|
|
148
|
+
questionTier = "default";
|
|
149
|
+
}
|
|
99
150
|
} catch (configErr) {
|
|
100
151
|
process.stderr.write(`config error: ${(configErr as Error).message}\n`);
|
|
101
152
|
if (jsonMode) {
|
|
@@ -112,9 +163,38 @@ export const session = async (harnessName: string, rawArgs: string[]): Promise<v
|
|
|
112
163
|
process.exitCode = 2;
|
|
113
164
|
return;
|
|
114
165
|
}
|
|
115
|
-
process.stderr.write(`provenance:
|
|
166
|
+
process.stderr.write(`provenance: questions = ${questionMode} (${questionTier})\n`);
|
|
116
167
|
|
|
117
168
|
// Validate sessionId shape? let openSession handle via assertUsableSessionId
|
|
169
|
+
// Unknown-id refusal reuses the run resume guard (src/cli/resume-guard.ts):
|
|
170
|
+
// harnesses that create on unknown (pi, muse with onMissing === "create")
|
|
171
|
+
// are refused before spawn with the same message shape hcn run uses.
|
|
172
|
+
// claude (onMissing === "error") refuses on its own; no store check.
|
|
173
|
+
if (resumeId !== undefined) {
|
|
174
|
+
const { resumeStore: checkResumeStore } = await import("./resume-guard.js");
|
|
175
|
+
const { path: storePath, exists } = checkResumeStore(h, {
|
|
176
|
+
home: process.env.HOME ?? process.env.USERPROFILE ?? "",
|
|
177
|
+
cwd: cwd ?? process.cwd(),
|
|
178
|
+
sessionId,
|
|
179
|
+
});
|
|
180
|
+
// Exists in the store: resume it, whichever alias was typed. Absent and the
|
|
181
|
+
// caller only named an id: a fresh session under that id, as before.
|
|
182
|
+
isResume = storePath !== null && exists;
|
|
183
|
+
if (explicitResume && storePath !== null && !exists) {
|
|
184
|
+
const { refuse: guardRefuse } = await import("./refuse.js");
|
|
185
|
+
guardRefuse(
|
|
186
|
+
{
|
|
187
|
+
message: `no ${h.name} session ${sessionId} found at ${storePath}`,
|
|
188
|
+
issue: "invalid-option-value",
|
|
189
|
+
supported: [`a session id that exists in ${h.name}'s store`],
|
|
190
|
+
},
|
|
191
|
+
jsonMode,
|
|
192
|
+
"closed",
|
|
193
|
+
);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
118
198
|
delete (process.env as Record<string, string | undefined>).HERDR_ENV;
|
|
119
199
|
|
|
120
200
|
const wantJson = values.json === true;
|
|
@@ -164,7 +244,11 @@ export const session = async (harnessName: string, rawArgs: string[]): Promise<v
|
|
|
164
244
|
|
|
165
245
|
let handle: ReturnType<typeof openSession>;
|
|
166
246
|
try {
|
|
167
|
-
handle = openSession(
|
|
247
|
+
handle = openSession(
|
|
248
|
+
h,
|
|
249
|
+
{ sessionId, model, cwd, questions: questionMode, provider, isResume },
|
|
250
|
+
deps,
|
|
251
|
+
);
|
|
168
252
|
} catch (err) {
|
|
169
253
|
if (err instanceof ArgvRefusalError) {
|
|
170
254
|
const { refusalOf, refuse } = await import("./refuse.js");
|
|
@@ -196,7 +280,8 @@ export const session = async (harnessName: string, rawArgs: string[]): Promise<v
|
|
|
196
280
|
sessionId,
|
|
197
281
|
harness: h.name,
|
|
198
282
|
hcnVersion: getVersion(),
|
|
199
|
-
|
|
283
|
+
questions: questionMode,
|
|
284
|
+
origin: isResume ? "resumed" : "fresh",
|
|
200
285
|
getCloseInfo: () => closeInfo,
|
|
201
286
|
getDroppedIds: () => droppedIds,
|
|
202
287
|
});
|
|
@@ -258,10 +343,7 @@ export const session = async (harnessName: string, rawArgs: string[]): Promise<v
|
|
|
258
343
|
const trimmed = line.trim();
|
|
259
344
|
if (trimmed === "" || trimmed === "exit") break;
|
|
260
345
|
|
|
261
|
-
|
|
262
|
-
if (result.disposition === "queued") {
|
|
263
|
-
process.stderr.write(`disposition: queued (turn in progress)\n`);
|
|
264
|
-
}
|
|
346
|
+
handle.send({ id: `you-${++sendCount}`, text: line });
|
|
265
347
|
|
|
266
348
|
const turn = (await turns.next()).value as
|
|
267
349
|
| AsyncIterable<import("../execution/events.js").HarnessEvent>
|
package/src/execution/events.ts
CHANGED
|
@@ -25,10 +25,17 @@ export type ExitCause =
|
|
|
25
25
|
| "stall"
|
|
26
26
|
| "killed"
|
|
27
27
|
| "failed"
|
|
28
|
-
/** issue #41: the turn ended by asking (
|
|
28
|
+
/** issue #41: the turn ended by asking (questions ask) - a
|
|
29
29
|
* SUCCESSFUL turn (process exit 0); the caller resumes with the answer. */
|
|
30
30
|
| "awaiting-input";
|
|
31
31
|
|
|
32
|
+
export type EscalationMode = "ask" | "assume" | "none";
|
|
33
|
+
export type EscalationDetection = "block" | "malformed" | "none";
|
|
34
|
+
export interface EscalationRecord {
|
|
35
|
+
readonly mode: EscalationMode;
|
|
36
|
+
readonly detection: EscalationDetection;
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
export type HarnessEvent =
|
|
33
40
|
| {
|
|
34
41
|
readonly kind: "identity";
|
|
@@ -60,6 +67,7 @@ export type HarnessEvent =
|
|
|
60
67
|
readonly exitCode: number | null;
|
|
61
68
|
readonly cause: ExitCause;
|
|
62
69
|
readonly failure?: FailureSummary;
|
|
70
|
+
readonly escalation: EscalationRecord;
|
|
63
71
|
};
|
|
64
72
|
|
|
65
73
|
export const DROPPABLE_KINDS = new Set(["token", "progress", "context"]);
|