crewhaus 0.2.3 → 0.3.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.
@@ -0,0 +1,87 @@
1
+ import type { ProviderAdapter } from "@crewhaus/adapter-anthropic";
2
+ import { CrewhausError } from "@crewhaus/errors";
3
+ import { type Spec } from "@crewhaus/spec";
4
+ /** The saved-interview hint appended to every terminal-failure report and to
5
+ * the incomplete-interview error (design §2.9's exact promise). */
6
+ export declare const INIT_INTERVIEW_SAVED_NOTE = "Your interview is saved \u2014 `crewhaus init --interactive --resume` continues where it stopped.";
7
+ /** The synthetic kick-off turn a `--resume` boot runs so the FIRST assistant
8
+ * message is the resume summary — the user never re-types anything. */
9
+ export declare const RESUME_KICKOFF_MESSAGE: string;
10
+ /** The interview ended (exit/EOF/no emit) without a validated spec. Message
11
+ * carries the resume hint; `kind: "config"` keeps die()'s one-liner shape. */
12
+ export declare class InterviewIncompleteError extends CrewhausError {
13
+ readonly name = "InterviewIncompleteError";
14
+ constructor(message: string);
15
+ }
16
+ /**
17
+ * v0.3.0 §2.9 — TTY gate for the conversational path, mirroring the CLI's
18
+ * destructive-verb convention (`memory clear`, `state restore`): interactive
19
+ * conversation needs an interactive terminal, and `--yes` explicitly asks
20
+ * for the non-conversational path. Pure; `index.ts` threads the real
21
+ * `process.stdin.isTTY`.
22
+ */
23
+ export declare function conversationalPathAllowed(opts: {
24
+ readonly stdinIsTTY: boolean;
25
+ readonly assumeYes: boolean;
26
+ }): {
27
+ readonly allowed: boolean;
28
+ readonly reason?: string;
29
+ };
30
+ /**
31
+ * Extract the REQ → spec-field mapping lines from the interview transcript:
32
+ * the LAST assistant message containing `REQ-nnn … →` lines wins (the
33
+ * discipline mandates the mapping in the reply that precedes `emit_spec`).
34
+ * Best-effort and pure — the CLI prints whatever the model actually
35
+ * produced; correctness of the written spec never depends on it.
36
+ */
37
+ export declare function extractReqMappingLines(rawJsonl: string): string[];
38
+ export type ConversationalInterviewResult = {
39
+ readonly yaml: string;
40
+ readonly spec: Spec;
41
+ readonly sessionId: string;
42
+ /** The REQ → spec-field mapping lines from the model's final reply
43
+ * (empty when the model produced none — best-effort surface). */
44
+ readonly mappingLines: readonly string[];
45
+ };
46
+ export type ConversationalInterviewOptions = {
47
+ /** The harness directory: the spec lands here, and the interview's own
48
+ * continuity state + session live under `<targetDir>/.crewhaus/`. */
49
+ readonly targetDir: string;
50
+ /** Continuity/session scope name — `init-<dirname>` by convention. */
51
+ readonly specName: string;
52
+ /** Model string for the interview (the router grammar). */
53
+ readonly model: string;
54
+ /** `--resume`: restart the most recent persisted interview session. */
55
+ readonly resume?: boolean;
56
+ /** Status-line sink; defaults to process.stdout. */
57
+ readonly log?: (line: string) => void;
58
+ /** Scripted ProviderAdapter — bypasses the model router. */
59
+ readonly _adapter?: ProviderAdapter;
60
+ /** Compaction summarizer adapter (defaults to `_adapter`/router). */
61
+ readonly _compactionAdapter?: ProviderAdapter;
62
+ /** Input stream override (defaults to process.stdin). */
63
+ readonly input?: NodeJS.ReadableStream;
64
+ /** Home directory for skill/command discovery isolation. */
65
+ readonly homeDir?: string;
66
+ /** Compaction sizing overrides (motivating-failure tests). */
67
+ readonly _compaction?: {
68
+ readonly contextLimit?: number;
69
+ readonly compactionThreshold?: number;
70
+ readonly snipKeepHead?: number;
71
+ readonly snipKeepTail?: number;
72
+ };
73
+ };
74
+ /**
75
+ * Run the conversational interview to completion: boots a persisted
76
+ * `runChatLoop` session with the continuity fabric on, lets the model
77
+ * interview the user (ask_user) and submit drafts (emit_spec, validated
78
+ * in-conversation), and returns the first spec that parses. The caller
79
+ * writes the YAML — this function performs no writes outside
80
+ * `<targetDir>/.crewhaus/`.
81
+ *
82
+ * Throws {@link InterviewIncompleteError} when the conversation ends
83
+ * (exit/EOF) without a validated spec, and lets `RunFailedError` from the
84
+ * runtime propagate so the CLI can render the classified FailureReport with
85
+ * {@link INIT_INTERVIEW_SAVED_NOTE}.
86
+ */
87
+ export declare function runConversationalInterview(opts: ConversationalInterviewOptions): Promise<ConversationalInterviewResult>;
@@ -0,0 +1,306 @@
1
+ /**
2
+ * v0.3.0 §2.9 / PR 18 — the conversational `crewhaus init --interactive`
3
+ * (the scene of the release's motivating failure, rebuilt).
4
+ *
5
+ * The old model path was a single-shot forced `emit_spec` call: one stdin
6
+ * line, `toolChoice: { type: "tool" }`, extra typed lines silently
7
+ * discarded, nothing persisted. This module replaces it with a REAL
8
+ * conversation on `runChatLoop`:
9
+ *
10
+ * - TWO tools, NO forced toolChoice: `ask_user` (the question is surfaced
11
+ * on the terminal and the answer arrives as the next user message via
12
+ * the multi-turn REPL — nothing discarded) and `emit_spec` (the same
13
+ * `{ yaml }` contract as before; `parseSpec` failures return to the
14
+ * conversation as tool errors the model fixes IN-CONTEXT, not via blind
15
+ * re-attempts with an error ledger).
16
+ * - The continuity fabric is ON for the interview itself: `wireMemory`
17
+ * with a continuity fragment, spec-scoped under the TARGET directory as
18
+ * `init-<dirname>` — so REQ pinning (FocusWrite), the verbatim
19
+ * requirements ledger, compaction protection, and the teardown handoff
20
+ * all apply to the creator conversation.
21
+ * - The session is persisted and RESUMABLE: `--resume` replays the
22
+ * transcript, reseeds the ledger from `context_evicted` events, and a
23
+ * kick-off turn makes the first assistant message the resume summary
24
+ * ("Resuming: N requirements confirmed, M open questions …") — no work
25
+ * redone, no question re-asked.
26
+ * - A terminal failure (billing/auth/token exhaustion) propagates as the
27
+ * PR-3 `RunFailedError`; the CLI renders the FailureReport plus
28
+ * {@link INIT_INTERVIEW_SAVED_NOTE}.
29
+ *
30
+ * Testable by construction: the model adapter, the input stream, the home
31
+ * directory (skill discovery), and the compaction sizing are all injectable,
32
+ * so a mock adapter can drive fully scripted conversations.
33
+ */
34
+ import { readFileSync } from "node:fs";
35
+ import { join } from "node:path";
36
+ import { CrewhausError } from "@crewhaus/errors";
37
+ import { wireMemory } from "@crewhaus/memory-service";
38
+ import { BUILTIN_DEFAULT_RULES, } from "@crewhaus/permission-engine";
39
+ import { createRunContext } from "@crewhaus/run-context";
40
+ import { runChatLoop } from "@crewhaus/runtime-core";
41
+ import { createSessionStore } from "@crewhaus/session-store";
42
+ import { createSkillTool } from "@crewhaus/skills-registry";
43
+ import { parseSpec } from "@crewhaus/spec";
44
+ import { buildTool } from "@crewhaus/tool-builder";
45
+ import { z } from "zod";
46
+ import { ASK_USER_TOOL, EMIT_SPEC_TOOL, buildInterviewSystemPrompt } from "./init-interactive";
47
+ /** The saved-interview hint appended to every terminal-failure report and to
48
+ * the incomplete-interview error (design §2.9's exact promise). */
49
+ export const INIT_INTERVIEW_SAVED_NOTE = "Your interview is saved — `crewhaus init --interactive --resume` continues where it stopped.";
50
+ /** The synthetic kick-off turn a `--resume` boot runs so the FIRST assistant
51
+ * message is the resume summary — the user never re-types anything. */
52
+ export const RESUME_KICKOFF_MESSAGE = "Resume the interview from the saved session. Start your reply with the resume summary " +
53
+ '("Resuming: N requirements confirmed, M open questions …") computed from the requirements ' +
54
+ "ledger and focus, then continue where the interview stopped — never re-ask a confirmed requirement.";
55
+ /** The interview ended (exit/EOF/no emit) without a validated spec. Message
56
+ * carries the resume hint; `kind: "config"` keeps die()'s one-liner shape. */
57
+ export class InterviewIncompleteError extends CrewhausError {
58
+ name = "InterviewIncompleteError";
59
+ constructor(message) {
60
+ super("config", message);
61
+ }
62
+ }
63
+ /**
64
+ * v0.3.0 §2.9 — TTY gate for the conversational path, mirroring the CLI's
65
+ * destructive-verb convention (`memory clear`, `state restore`): interactive
66
+ * conversation needs an interactive terminal, and `--yes` explicitly asks
67
+ * for the non-conversational path. Pure; `index.ts` threads the real
68
+ * `process.stdin.isTTY`.
69
+ */
70
+ export function conversationalPathAllowed(opts) {
71
+ if (opts.assumeYes) {
72
+ return { allowed: false, reason: "--yes requested the scripted questionnaire" };
73
+ }
74
+ if (!opts.stdinIsTTY) {
75
+ return {
76
+ allowed: false,
77
+ reason: "the conversational interview needs an interactive terminal (TTY)",
78
+ };
79
+ }
80
+ return { allowed: true };
81
+ }
82
+ /** Tools the interview pre-allows (flag-source `alwaysAllow`) so the REPL
83
+ * never stalls on an approval prompt mid-conversation. Deliberately NOT
84
+ * MemoryClear — destructive, and an interview has no business clearing
85
+ * stores; it stays behind the default ask gate. */
86
+ const INTERVIEW_ALLOWED_TOOLS = [
87
+ ASK_USER_TOOL.name,
88
+ EMIT_SPEC_TOOL.name,
89
+ "FocusRead",
90
+ "FocusWrite",
91
+ "PlanRead",
92
+ "PlanUpdate",
93
+ "PlanComplete",
94
+ "GoalWrite",
95
+ "GoalUpdate",
96
+ "GoalList",
97
+ "Skill",
98
+ ];
99
+ function interviewRuleSet() {
100
+ const flag = INTERVIEW_ALLOWED_TOOLS.map((pattern) => ({
101
+ type: "alwaysAllow",
102
+ pattern,
103
+ source: "flag",
104
+ }));
105
+ return { flag, settings: [], yaml: [], hooks: [], builtin: [...BUILTIN_DEFAULT_RULES] };
106
+ }
107
+ /**
108
+ * Extract the REQ → spec-field mapping lines from the interview transcript:
109
+ * the LAST assistant message containing `REQ-nnn … →` lines wins (the
110
+ * discipline mandates the mapping in the reply that precedes `emit_spec`).
111
+ * Best-effort and pure — the CLI prints whatever the model actually
112
+ * produced; correctness of the written spec never depends on it.
113
+ */
114
+ export function extractReqMappingLines(rawJsonl) {
115
+ const mapRe = /REQ-\d+.*(?:→|->)/;
116
+ let lines = [];
117
+ for (const line of rawJsonl.split("\n")) {
118
+ if (line.trim() === "")
119
+ continue;
120
+ let parsed;
121
+ try {
122
+ parsed = JSON.parse(line);
123
+ }
124
+ catch {
125
+ continue; // torn tail line from a crashed writer
126
+ }
127
+ if (parsed.kind !== "assistant_message")
128
+ continue;
129
+ const content = parsed.payload?.content;
130
+ const text = typeof content === "string"
131
+ ? content
132
+ : Array.isArray(content)
133
+ ? content
134
+ .map((b) => {
135
+ const block = b;
136
+ return block.type === "text" && typeof block.text === "string" ? block.text : "";
137
+ })
138
+ .join("\n")
139
+ : "";
140
+ const matching = text
141
+ .split("\n")
142
+ .map((l) => l.trim())
143
+ .filter((l) => mapRe.test(l));
144
+ if (matching.length > 0)
145
+ lines = matching;
146
+ }
147
+ return lines;
148
+ }
149
+ /**
150
+ * Run the conversational interview to completion: boots a persisted
151
+ * `runChatLoop` session with the continuity fabric on, lets the model
152
+ * interview the user (ask_user) and submit drafts (emit_spec, validated
153
+ * in-conversation), and returns the first spec that parses. The caller
154
+ * writes the YAML — this function performs no writes outside
155
+ * `<targetDir>/.crewhaus/`.
156
+ *
157
+ * Throws {@link InterviewIncompleteError} when the conversation ends
158
+ * (exit/EOF) without a validated spec, and lets `RunFailedError` from the
159
+ * runtime propagate so the CLI can render the classified FailureReport with
160
+ * {@link INIT_INTERVIEW_SAVED_NOTE}.
161
+ */
162
+ export async function runConversationalInterview(opts) {
163
+ const log = opts.log ?? ((line) => void process.stdout.write(line));
164
+ const sessionRootDir = join(opts.targetDir, ".crewhaus", "sessions");
165
+ // --resume resolves to the most-recently-updated persisted interview for
166
+ // this spec name — the same resolution `crewhaus run --continue` uses.
167
+ let resumeId;
168
+ if (opts.resume === true) {
169
+ const store = createSessionStore({ rootDir: sessionRootDir });
170
+ const sessions = await store.list();
171
+ const match = sessions.find((s) => s.name === opts.specName);
172
+ if (match === undefined) {
173
+ throw new InterviewIncompleteError(`no saved interview for "${opts.specName}" under ${sessionRootDir} — start one with: crewhaus init --interactive`);
174
+ }
175
+ resumeId = match.id;
176
+ log(`[interview] resuming session ${match.id} (last updated ${match.updatedAt})\n`);
177
+ }
178
+ // The abort controller is the loop's clean exit: once a spec validates,
179
+ // the turn_end subscriber aborts and the REPL breaks BEFORE prompting for
180
+ // another line (the in-flight turn — the model's closing confirmation —
181
+ // always completes first).
182
+ const controller = new AbortController();
183
+ const runContext = createRunContext({
184
+ abortSignal: controller.signal,
185
+ ...(resumeId !== undefined ? { sessionId: resumeId } : {}),
186
+ });
187
+ const sessionId = runContext.sessionId;
188
+ // v0.3.0 §2.9 — continuity fabric ON for the interview itself: the SAME
189
+ // wireMemory composition-root call every harness makes, spec-scoped under
190
+ // the target directory. Registers Focus/Plan/Goal tools, returns the
191
+ // ledger/tail/handoff seam, and merges the builtin `continuity` skill +
192
+ // slash commands at lowest precedence.
193
+ const tools = [];
194
+ const wired = await wireMemory({ specName: opts.specName, continuity: {} }, {
195
+ catalog: {
196
+ register: (tool) => {
197
+ tools.push(tool);
198
+ },
199
+ },
200
+ cwd: opts.targetDir,
201
+ sessionRootDir,
202
+ log,
203
+ ...(opts.homeDir !== undefined ? { homeDir: opts.homeDir } : {}),
204
+ });
205
+ // The two interview tools — free-form conversation, no forced toolChoice.
206
+ let emitted;
207
+ tools.push(buildTool({
208
+ name: ASK_USER_TOOL.name,
209
+ description: ASK_USER_TOOL.description,
210
+ inputSchema: z.object({ question: z.string().min(1) }),
211
+ readOnly: true,
212
+ destructive: false,
213
+ concurrencySafe: false,
214
+ execute: async ({ question }) => {
215
+ // Surface the question distinctly; the answer arrives through the
216
+ // REPL's next `you>` line — multi-turn stdin, nothing discarded.
217
+ log(`\n[interviewer] ${question}\n`);
218
+ return ("Question delivered. End your turn now — the user's answer arrives as the " +
219
+ "next user message. Never re-ask a requirement already recorded as confirmed.");
220
+ },
221
+ }), buildTool({
222
+ name: EMIT_SPEC_TOOL.name,
223
+ description: EMIT_SPEC_TOOL.description,
224
+ inputSchema: z.object({ yaml: z.string().min(1) }),
225
+ readOnly: true,
226
+ destructive: false,
227
+ concurrencySafe: false,
228
+ execute: async ({ yaml }) => {
229
+ // parseSpec throws SpecParseError on an invalid draft; tool-executor
230
+ // turns the throw into an `is_error` tool result, so the validation
231
+ // error goes BACK into the conversation and the model revises
232
+ // in-context — the ledger and Q&A history stay in play.
233
+ const spec = parseSpec(yaml);
234
+ emitted = { yaml, spec };
235
+ return ("Spec validated against the live schema — the interview is complete and the " +
236
+ "CLI writes crewhaus.yaml next. Reply with a one-line confirmation.");
237
+ },
238
+ }));
239
+ const skills = wired.options.skills ?? [];
240
+ if (skills.length > 0)
241
+ tools.push(createSkillTool(skills));
242
+ const unsubscribe = runContext.eventBus.subscribe((event) => {
243
+ if (event.kind === "turn_end" && emitted !== undefined)
244
+ controller.abort();
245
+ });
246
+ const common = {
247
+ model: opts.model,
248
+ instructions: buildInterviewSystemPrompt(),
249
+ tools,
250
+ permissionMode: "default",
251
+ permissionRules: interviewRuleSet(),
252
+ runContext,
253
+ sessionRootDir,
254
+ sessionName: opts.specName,
255
+ sessionTarget: "init",
256
+ ...(wired.options.continuity !== undefined ? { continuity: wired.options.continuity } : {}),
257
+ ...(skills.length > 0 ? { skills } : {}),
258
+ ...(wired.options.slashCommands !== undefined
259
+ ? { slashCommands: wired.options.slashCommands }
260
+ : {}),
261
+ ...(opts._adapter !== undefined ? { _adapter: opts._adapter } : {}),
262
+ ...(opts._compactionAdapter !== undefined
263
+ ? { _compactionAdapter: opts._compactionAdapter }
264
+ : {}),
265
+ ...(opts._compaction ?? {}),
266
+ ...(opts.input !== undefined ? { input: opts.input } : {}),
267
+ };
268
+ try {
269
+ if (resumeId !== undefined) {
270
+ // Kick-off turn: replay the transcript + rebuild the ledger from the
271
+ // logged `context_evicted` events, then run ONE seeded turn so the
272
+ // first assistant message is the resume summary — the user types
273
+ // nothing to get back to where the interview stopped.
274
+ await runChatLoop({
275
+ ...common,
276
+ singleTurn: true,
277
+ resume: { sessionId: resumeId },
278
+ seedMessages: [{ role: "user", content: RESUME_KICKOFF_MESSAGE }],
279
+ });
280
+ // Continue conversationally unless the kick-off already emitted the
281
+ // spec (everything was confirmed before the original session died).
282
+ if (emitted === undefined) {
283
+ await runChatLoop({ ...common, resume: { sessionId: resumeId } });
284
+ }
285
+ }
286
+ else {
287
+ await runChatLoop(common);
288
+ }
289
+ }
290
+ finally {
291
+ unsubscribe();
292
+ }
293
+ if (emitted === undefined) {
294
+ throw new InterviewIncompleteError(`the interview ended without a validated spec — nothing was written. ${INIT_INTERVIEW_SAVED_NOTE}`);
295
+ }
296
+ // Best-effort: the REQ → spec-field mapping the model listed before
297
+ // emit_spec, read back from the persisted transcript.
298
+ let mappingLines = [];
299
+ try {
300
+ mappingLines = extractReqMappingLines(readFileSync(join(sessionRootDir, `${sessionId}.jsonl`), "utf-8"));
301
+ }
302
+ catch {
303
+ // No transcript / unreadable — the mapping line is a bonus, never a gate.
304
+ }
305
+ return { yaml: emitted.yaml, spec: emitted.spec, sessionId, mappingLines };
306
+ }
@@ -1,22 +1,25 @@
1
- import { type Spec, SpecParseError } from "@crewhaus/spec";
1
+ import { type Spec } from "@crewhaus/spec";
2
2
  /**
3
- * Item 39 — `crewhaus init --interactive`. Folds the demos harness-designer
4
- * pattern (an agent that interviews the user in plain English and emits a
5
- * validated crewhaus.yaml) into core. Two design commitments:
3
+ * Item 39 / v0.3.0 §2.9 — `crewhaus init --interactive`. Folds the demos
4
+ * harness-designer pattern (an agent that interviews the user in plain
5
+ * English and emits a validated crewhaus.yaml) into core. Design commitments:
6
6
  *
7
7
  * 1. NO dependency on the demos repo. The shape-decision guidance is
8
8
  * bundled inline here ({@link SHAPE_GUIDANCE} / {@link buildInterviewSystemPrompt}),
9
9
  * and validation is `parseSpec` called in-process — the single source of
10
10
  * truth for what compiles.
11
11
  * 2. Every draft the model emits is validated against the LIVE Zod union via
12
- * `parseSpec`, and on a structured error the error text is fed back for a
13
- * retry. So `init --interactive` cannot emit a spec that won't parse.
14
- *
15
- * Everything here is side-effect-free and unit-testable: the interview loop is
16
- * driven by an INJECTED `proposeSpec` callback (the CLI wires the model
17
- * adapter behind it), and the credential-free fallback is a pure
18
- * {@link buildScriptedSpec} over stdin answers. The CLI wrapper in `index.ts`
19
- * owns all I/O.
12
+ * `parseSpec`, and on a structured error the error text goes back into
13
+ * the conversation as a tool error the model fixes in-context. So
14
+ * `init --interactive` cannot emit a spec that won't parse.
15
+ * 3. The interview is a REAL conversation (v0.3.0 PR 18): a `runChatLoop`
16
+ * session with `ask_user` + `emit_spec` and NO forced toolChoice
17
+ * persisted, ledger-protected, and resumable. The conversational loop
18
+ * lives in `init-conversation.ts`; this module keeps the pure,
19
+ * side-effect-free pieces: the shape catalog, the interviewer system
20
+ * prompt, the tool contracts, and the credential-free fallback
21
+ * ({@link buildScriptedSpec} over stdin answers). The CLI wrapper in
22
+ * `index.ts` owns all I/O.
20
23
  */
21
24
  /** The fourteen target shapes, with the one-line "pick me when" guidance the
22
25
  * interviewer uses. Bundled inline so core never reaches into the demos repo. */
@@ -32,14 +35,17 @@ export declare const SCRIPTED_SHAPES: readonly ["cli", "workflow", "research"];
32
35
  export type ScriptedShape = (typeof SCRIPTED_SHAPES)[number];
33
36
  export declare function isScriptedShape(s: string): s is ScriptedShape;
34
37
  /**
35
- * The interviewer's system prompt. Bundles the shape catalog + the hard rules
36
- * the emitted YAML must obey (single-line-safe names, the `$UPPER_SNAKE_CASE`
38
+ * The interviewer's system prompt a focused variant of the `continuity`
39
+ * discipline (v0.3.0 §2.9) on top of the shape catalog + the hard rules the
40
+ * emitted YAML must obey (single-line-safe names, the `$UPPER_SNAKE_CASE`
37
41
  * secret convention, no `permissions.mode: bypass`). Kept as a builder so a
38
42
  * test can assert the guidance is present without shipping a golden file.
39
43
  */
40
44
  export declare function buildInterviewSystemPrompt(): string;
41
- /** The tool the interviewer calls to submit a candidate spec. Exported so the
42
- * CLI wires the same schema into the adapter request. */
45
+ /** The tool the interviewer calls to submit a candidate spec the SAME
46
+ * contract the pre-0.3.0 single-shot interview used (`{ yaml: string }`).
47
+ * Exported so tests can pin the schema and `init-conversation.ts` builds
48
+ * the RegisteredTool from it. */
43
49
  export declare const EMIT_SPEC_TOOL: {
44
50
  name: string;
45
51
  description: string;
@@ -54,30 +60,24 @@ export declare const EMIT_SPEC_TOOL: {
54
60
  required: string[];
55
61
  };
56
62
  };
57
- /** One turn of the interview: the injected model proposes a spec (the `yaml`
58
- * string it passed to `emit_spec`), given the running feedback log of prior
59
- * validation errors. Returns undefined if the model declined to emit. */
60
- export type ProposeSpec = (feedback: readonly string[]) => Promise<string | undefined>;
61
- export type InterviewResult = {
62
- readonly yaml: string;
63
- readonly spec: Spec;
64
- /** How many drafts were tried before one validated (1 = first try). */
65
- readonly attempts: number;
63
+ /** v0.3.0 §2.9 the interviewer's clarifying-question tool. The loop
64
+ * surfaces the question on the terminal; the user's answer arrives as the
65
+ * next user message through the multi-turn REPL (nothing typed is ever
66
+ * discarded the exact failure the single-shot path had). */
67
+ export declare const ASK_USER_TOOL: {
68
+ name: string;
69
+ description: string;
70
+ input_schema: {
71
+ type: "object";
72
+ properties: {
73
+ question: {
74
+ type: "string";
75
+ description: string;
76
+ };
77
+ };
78
+ required: string[];
79
+ };
66
80
  };
67
- export declare class InterviewError extends SpecParseError {
68
- }
69
- /**
70
- * Drive the validate-and-retry interview loop. Each attempt asks the injected
71
- * `proposeSpec` for a draft, runs it through `parseSpec` (the LIVE union), and
72
- * on a `SpecParseError` appends the error to the feedback log for the next
73
- * attempt. Succeeds on the first draft that validates; throws
74
- * {@link InterviewError} after `maxAttempts` failed drafts (or if the model
75
- * declines to emit). Pure given its `proposeSpec` seam — no I/O.
76
- */
77
- export declare function runInterview(opts: {
78
- readonly proposeSpec: ProposeSpec;
79
- readonly maxAttempts?: number;
80
- }): Promise<InterviewResult>;
81
81
  /** The answers the scripted (no-credentials) questionnaire collects. Every
82
82
  * field is a plain string/list so the CLI can gather them over stdin and a
83
83
  * test can drive {@link buildScriptedSpec} directly. */