@mycompbox/compbox-cli 1.19.0 → 1.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mycompbox/compbox-cli",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "CompBox CLI - Install, update, and manage Skill Packs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,33 +2,19 @@
2
2
  * Agent invoker (ENG-E23 / ENG-122-2) — the daemon's "heart": actually runs a
3
3
  * claimed job instead of immediately reporting it as unimplemented.
4
4
  *
5
- * Two agentModes are implemented, both for `epic_request` jobs only
6
- * (`start_execution` — running planner/orchestrator locally — is NOT
7
- * implemented here):
5
+ * Request conversations (`epic_request` jobs) run one AI reply per claim
6
+ * (invokeEpicRequestStep, on the shared runConversationStep): the reply is
7
+ * posted, the job is left waiting on the person, and the server hands it back
8
+ * when they write again. Nothing waits in between, so a conversation never
9
+ * times out and stays open after the request is saved. Two ways to reply:
8
10
  * - `api`: Anthropic Messages API, directly (Anthropic API key, billed
9
- * per-token). See invokeEpicRequestViaApi.
11
+ * per-token).
10
12
  * - `claude-subscription`: shells out to the local `claude` CLI
11
13
  * non-interactively, using whichever account is already logged in on
12
14
  * this machine (personal Pro/Max or a company Team seat) — no API key
13
- * ever touches CompBox. See invokeEpicRequestViaSubscription.
15
+ * ever touches CompBox.
14
16
  *
15
- * Common to both:
16
- * - The interview is driven by the shared, declarative INTERVIEW_DEFINITION
17
- * (@mycompbox/shared, ENG-143) so the machine/mailbox interview reaches the
18
- * SAME depth as the CLI request-formulator: it covers all seven sections
19
- * (problem, solution, impact, success metrics, target audience, technical
20
- * context, alternatives) rather than only the 3 required fields. The system
21
- * prompt is rendered from that definition (buildInterviewSystemPrompt), and
22
- * the completion object carries the required fields plus any optional ones
23
- * the employee provided; there is no shortened variant left in this path.
24
- *
25
- * Turn relay: the mailbox has no "wait for a new turn" primitive for a
26
- * machine caller (next-job's "resume active job" path returns immediately
27
- * with whatever the job's current state is — see jobService.ts tryClaimOnce).
28
- * So after posting an agent question, this module polls next-job on a short
29
- * client-side interval until it observes a new trailing user turn, with a
30
- * bounded overall timeout so an abandoned interview eventually fails cleanly
31
- * instead of polling forever.
17
+ * `start_execution` jobs run the planner/orchestrator headless (below).
32
18
  */
33
19
  import Anthropic from '@anthropic-ai/sdk';
34
20
  import { spawn } from 'node:child_process';
@@ -98,6 +84,13 @@ export interface InvokeResult {
98
84
  status: 'done' | 'failed';
99
85
  result: Record<string, unknown>;
100
86
  }
87
+ /**
88
+ * A request conversation step that left the job open (waiting on the person):
89
+ * the job must NOT be completed.
90
+ */
91
+ export interface YieldResult {
92
+ status: 'yield';
93
+ }
101
94
  /**
102
95
  * ENG-149 — parse a claimed job's `payload` as an interview-CONTINUATION seed
103
96
  * (the ENG-148 dispatch shape), or return null for a normal fresh-start job
@@ -107,11 +100,6 @@ export interface InvokeResult {
107
100
  * jobB). Exported for unit testing.
108
101
  */
109
102
  export declare function parseSeedPayload(payload: unknown): InterviewSeedPayload | null;
110
- /**
111
- * ENG-149 — the interview system prompt for a job: a seed-aware RESUME/ENRICH
112
- * prompt for a continuation, otherwise the module-level fresh-start prompt.
113
- */
114
- export declare function resolveSystemPrompt(seed: InterviewSeedPayload | null): string;
115
103
  /**
116
104
  * Submit the current draft and read back the server-computed compliance score
117
105
  * (bug #2 — audit-aware finishing). Fresh interview: the FIRST submit
@@ -128,30 +116,27 @@ export declare function submitAndScore(ctx: {
128
116
  compliance: ComplianceSummary | null;
129
117
  }>;
130
118
  /**
131
- * Runs the epic_request interview end-to-end: converses with the user via
132
- * post-turn/next-job until it has a title + the 3 required structured-desc
133
- * fields, then creates a personal-scope epic request via the real API
134
- * (POST /api/v1/me/epic-requests) using the user's own stored token.
119
+ * True when the person's Claude plan can't use the requested model (for
120
+ * example Fable needs usage credits on some plans): retry on their default.
135
121
  */
136
- export declare function invokeEpicRequestViaApi(job: MailboxJob, ctx: {
137
- mailboxClient: MailboxClient;
138
- machineToken: string;
139
- apiBaseUrl: string;
140
- userApiToken: string;
141
- anthropicApiKey: string;
142
- }): Promise<InvokeResult>;
122
+ export declare function needsDefaultModel(error: unknown): boolean;
143
123
  /**
144
- * Runs the epic_request interview end-to-end using the local `claude` CLI
145
- * (the employee's own Claude Code login) instead of the Anthropic API.
146
- * Mirrors invokeEpicRequestViaApi's turn loop and completion handling
147
- * exactly — only how a single assistant turn is produced differs.
124
+ * Takes one step of a request conversation on this computer: the AI's reply to
125
+ * the person's latest message, which may save or update their request. Then
126
+ * the job is left waiting on the person (never completed), so the chat stays
127
+ * open: there is no reply timeout, and the computer is free for other work in
128
+ * the meantime. The server hands the job back when they write again.
129
+ *
130
+ * `anthropicApiKey` set → the Anthropic API; otherwise the local `claude` CLI
131
+ * on the person's own Claude sign-in.
148
132
  */
149
- export declare function invokeEpicRequestViaSubscription(job: MailboxJob, ctx: {
133
+ export declare function invokeEpicRequestStep(job: MailboxJob, ctx: {
150
134
  mailboxClient: MailboxClient;
151
135
  machineToken: string;
152
136
  apiBaseUrl: string;
153
137
  userApiToken: string;
154
- }): Promise<InvokeResult>;
138
+ anthropicApiKey?: string;
139
+ }): Promise<YieldResult>;
155
140
  /**
156
141
  * Per-run wall-clock budget for the headless planner child (H4). Deliberately
157
142
  * FAR larger than the interview's {@link CLAUDE_CLI_TURN_TIMEOUT_MS} (~2 min):
@@ -1 +1 @@
1
- {"version":3,"file":"agent-invoker.d.ts","sourceRoot":"","sources":["../../src/utils/agent-invoker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent-invoker.d.ts","sourceRoot":"","sources":["../../src/utils/agent-invoker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAa,MAAM,oBAAoB,CAAC;AAItD,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,qBAAqB,EACrB,0BAA0B,EAC1B,qBAAqB,EACrB,kBAAkB,EAClB,6BAA6B,EAI7B,KAAK,iBAAiB,EAEtB,KAAK,gBAAgB,EAEtB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,qBAAqB,EACrB,kBAAkB,EAClB,6BAA6B,GAC9B,CAAC;AACF,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC;AAOpD,OAAO,EAAgD,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AACzF,OAAO,KAAK,EAAE,YAAY,EAAgB,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC1F,OAAO,KAAK,EACV,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,cAAc,EACf,MAAM,qBAAqB,CAAC;AAU7B;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,aAAa,GACrB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAItC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,CAAC,UAAU,EACvB,OAAO,EAAE,aAAa,GACrB,MAAM,CAAC,UAAU,CAInB;AAyBD,4FAA4F;AAC5F,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAiB/C,4EAA4E;AAC5E,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,iBAAiB,EAAE,GAAG,SAAS,GAC3C,iBAAiB,EAAE,CAarB;AAED,wFAAwF;AACxF,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,iBAAiB,EAAE,GAAG,SAAS,GAAG,MAAM,CAGzF;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,iBAAiB,EAAE,EAChC,IAAI,GAAE,MAAoB,EAC1B,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,SAAS,CAAC,eAAe,EAAE,CAAC,CAgCtC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,SAAS,CAAC,eAAe,EAAE,GACvC,MAAM,GAAG,SAAS,CAAC,iBAAiB,EAAE,CAGxC;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,aAAa,CAAC,EACrD,IAAI,GAAE,MAAoB,EAC1B,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,iBAAiB,EAAE,CAAC,CAKjD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAK7E;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,CAAC;CACjB;AAGD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,oBAAoB,GAAG,IAAI,CAuB9E;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,EACjD,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,KAAK,EAAE,gBAAgB,GACtB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAA;CAAE,CAAC,CAYjE;AAuMD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGzD;AAQD;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,UAAU,EACf,GAAG,EAAE;IACH,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GACA,OAAO,CAAC,WAAW,CAAC,CActB;AA4DD;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AAEtD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,6BAA6B,QAAa,CAAC;AAQxD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE;IAAE,aAAa,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAC1E,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAO3C;AAYD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAUhE;AAED,gGAAgG;AAChG,wBAAgB,8BAA8B,IAAI,MAAM,CASvD;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAUhE;AAED,2EAA2E;AAC3E,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ/D;AAwCD,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAcxE;AAuBD,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,aAAa,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,iGAAiG;IACjG,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,iFAAiF;IACjF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,6FAA6F;IAC7F,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACrC,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAClD,0GAA0G;IAC1G,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,qBAAqB,EAC1B,IAAI,GAAE,sBAA2B,GAChC,OAAO,CAAC,YAAY,CAAC,CA8KvB;AA+BD;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,QAAsB,CAAC;AAE/D,4FAA4F;AAC5F,eAAO,MAAM,kCAAkC,QAAgC,CAAC;AAKhF;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAS9D;AAED,mFAAmF;AACnF,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOhE;AAED,gFAAgF;AAChF,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED,mFAAmF;AACnF,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM3D;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAU3D;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAa7D;AAED,MAAM,WAAW,4BAA4B;IAC3C,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,6BAA6B;IAC5C,iGAAiG;IACjG,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,6FAA6F;IAC7F,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACrC,sFAAsF;IACtF,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAClD,+GAA+G;IAC/G,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,QAAQ,CAAC,CAAC;CAC7D;AAED,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAM9D;AAQD;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC1C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,+BAA+B,CACnD,GAAG,EAAE,4BAA4B,EACjC,IAAI,GAAE,6BAAkC,GACvC,OAAO,CAAC,YAAY,CAAC,CAiNvB"}
@@ -2,40 +2,26 @@
2
2
  * Agent invoker (ENG-E23 / ENG-122-2) — the daemon's "heart": actually runs a
3
3
  * claimed job instead of immediately reporting it as unimplemented.
4
4
  *
5
- * Two agentModes are implemented, both for `epic_request` jobs only
6
- * (`start_execution` — running planner/orchestrator locally — is NOT
7
- * implemented here):
5
+ * Request conversations (`epic_request` jobs) run one AI reply per claim
6
+ * (invokeEpicRequestStep, on the shared runConversationStep): the reply is
7
+ * posted, the job is left waiting on the person, and the server hands it back
8
+ * when they write again. Nothing waits in between, so a conversation never
9
+ * times out and stays open after the request is saved. Two ways to reply:
8
10
  * - `api`: Anthropic Messages API, directly (Anthropic API key, billed
9
- * per-token). See invokeEpicRequestViaApi.
11
+ * per-token).
10
12
  * - `claude-subscription`: shells out to the local `claude` CLI
11
13
  * non-interactively, using whichever account is already logged in on
12
14
  * this machine (personal Pro/Max or a company Team seat) — no API key
13
- * ever touches CompBox. See invokeEpicRequestViaSubscription.
15
+ * ever touches CompBox.
14
16
  *
15
- * Common to both:
16
- * - The interview is driven by the shared, declarative INTERVIEW_DEFINITION
17
- * (@mycompbox/shared, ENG-143) so the machine/mailbox interview reaches the
18
- * SAME depth as the CLI request-formulator: it covers all seven sections
19
- * (problem, solution, impact, success metrics, target audience, technical
20
- * context, alternatives) rather than only the 3 required fields. The system
21
- * prompt is rendered from that definition (buildInterviewSystemPrompt), and
22
- * the completion object carries the required fields plus any optional ones
23
- * the employee provided; there is no shortened variant left in this path.
24
- *
25
- * Turn relay: the mailbox has no "wait for a new turn" primitive for a
26
- * machine caller (next-job's "resume active job" path returns immediately
27
- * with whatever the job's current state is — see jobService.ts tryClaimOnce).
28
- * So after posting an agent question, this module polls next-job on a short
29
- * client-side interval until it observes a new trailing user turn, with a
30
- * bounded overall timeout so an abandoned interview eventually fails cleanly
31
- * instead of polling forever.
17
+ * `start_execution` jobs run the planner/orchestrator headless (below).
32
18
  */
33
19
  import Anthropic from '@anthropic-ai/sdk';
34
20
  import { spawn, spawnSync } from 'node:child_process';
35
21
  import { mkdirSync } from 'node:fs';
36
22
  import { homedir } from 'node:os';
37
23
  import { join } from 'node:path';
38
- import { resolveRouting, buildInterviewSystemPrompt, INTERVIEW_DEFINITION, extractCompletionJson, looksLikeCompletionAttempt, meetsComplianceTarget, buildAuditFeedback, COMPLETION_REPAIR_INSTRUCTION, } from '@mycompbox/shared';
24
+ import { resolveRouting, extractCompletionJson, looksLikeCompletionAttempt, meetsComplianceTarget, buildAuditFeedback, COMPLETION_REPAIR_INSTRUCTION, renderConversationPrompt, runConversationStep, savedContextFromRecord, } from '@mycompbox/shared';
39
25
  // Completion parsing lives in @mycompbox/shared so CompBox's server-side
40
26
  // (API-key) interview finishes the same way; re-exported for existing callers.
41
27
  export { extractCompletionJson, looksLikeCompletionAttempt, meetsComplianceTarget, buildAuditFeedback, COMPLETION_REPAIR_INSTRUCTION, };
@@ -47,22 +33,8 @@ import { keyRunArgs, keyRunEnv, parseClaudeJsonResult } from './key-run.js';
47
33
  // quality loss. Opus/Sonnet are deliberately NOT used here — pxpipe passes
48
34
  // Opus through uncompressed, defeating the token savings.
49
35
  const ANTHROPIC_MODEL = 'claude-fable-5';
50
- // Turn budget for the interview loop. Raised from 8: a full 7-section interview
51
- // (problem / solution / impact + success metrics / audience / technical context /
52
- // alternatives) plus follow-ups AND audit-quality rounds needs more headroom
53
- // than 8 (deep interviews were being cut off mid-way — bug #3).
54
- const MAX_INTERVIEW_TURNS = 18;
55
- // Audit-quality gate (bug #2): the interview must reach the `pass` tier
56
- // (>=90, zero critical issues) before it may finish. After the model says
57
- // "done", we submit, read the server-computed compliance score, and if it is
58
- // not yet `pass` we feed the failing rules back and keep going. This bounds how
59
- // many re-submit/re-audit rounds we allow before finalizing with the best draft
60
- // (so we never loop forever, and never strand the request at a failing score).
61
- const MAX_QUALITY_ROUNDS = 4;
62
- const REPLY_POLL_INTERVAL_MS = 3_000;
63
- const REPLY_TIMEOUT_MS = 10 * 60 * 1_000; // 10 minutes
64
36
  /** Per-turn wall-clock budget for a single `claude -p` child process. */
65
- const CLAUDE_CLI_TURN_TIMEOUT_MS = 2 * 60 * 1_000; // 2 minutes
37
+ const CLAUDE_CLI_TURN_TIMEOUT_MS = 4 * 60 * 1_000; // 4 minutes: a full write-up can take a while
66
38
  /**
67
39
  * pxpipe routing (ENG-136). Anthropic SDK options carrying the proxy `baseURL`
68
40
  * when routed through pxpipe, otherwise just the API key. Pure — unit tested.
@@ -228,13 +200,6 @@ export function appendAttachmentNote(text, imageCount) {
228
200
  const note = `[${String(imageCount)} ${noun} attached — not shown in subscription mode; vision is handled when the daemon runs in API mode]`;
229
201
  return text.trim().length > 0 ? `${text}\n\n${note}` : note;
230
202
  }
231
- /**
232
- * The interview system prompt, rendered from the shared, declarative
233
- * INTERVIEW_DEFINITION (ENG-143). This is the single wiring point that gives
234
- * the machine/mailbox interview the same section depth as the CLI
235
- * request-formulator — there is no shortened hardcoded variant anymore.
236
- */
237
- const SYSTEM_PROMPT = buildInterviewSystemPrompt();
238
203
  /**
239
204
  * ENG-149 — parse a claimed job's `payload` as an interview-CONTINUATION seed
240
205
  * (the ENG-148 dispatch shape), or return null for a normal fresh-start job
@@ -263,17 +228,6 @@ export function parseSeedPayload(payload) {
263
228
  priorTranscript,
264
229
  };
265
230
  }
266
- /**
267
- * ENG-149 — the interview system prompt for a job: a seed-aware RESUME/ENRICH
268
- * prompt for a continuation, otherwise the module-level fresh-start prompt.
269
- */
270
- export function resolveSystemPrompt(seed) {
271
- return seed
272
- ? buildInterviewSystemPrompt(INTERVIEW_DEFINITION, {
273
- priorTranscript: seed.priorTranscript,
274
- })
275
- : SYSTEM_PROMPT;
276
- }
277
231
  /**
278
232
  * Submit the current draft and read back the server-computed compliance score
279
233
  * (bug #2 — audit-aware finishing). Fresh interview: the FIRST submit
@@ -290,162 +244,6 @@ export async function submitAndScore(ctx, currentErId, draft) {
290
244
  const created = await createPersonalEpicRequest(ctx.apiBaseUrl, ctx.userApiToken, draft);
291
245
  return { erId: created.id, compliance: created.compliance };
292
246
  }
293
- /** How many times to ask the model to fix a malformed completion before failing. */
294
- const MAX_JSON_REPAIRS = 3;
295
- function sleep(ms) {
296
- return new Promise((resolve) => setTimeout(resolve, ms));
297
- }
298
- /**
299
- * Polls next-job (the "resume active job" path) until a new trailing user
300
- * turn appears, or the timeout elapses. Returns the updated job, or null on
301
- * timeout.
302
- */
303
- async function waitForUserReply(client, machineToken, jobId, turnsSoFar) {
304
- const deadline = Date.now() + REPLY_TIMEOUT_MS;
305
- while (Date.now() < deadline) {
306
- await sleep(REPLY_POLL_INTERVAL_MS);
307
- const job = await client.nextJob(machineToken, 0);
308
- if (job?.id !== jobId)
309
- continue;
310
- const last = job.turns[job.turns.length - 1];
311
- if (job.turns.length > turnsSoFar && last?.role === 'user') {
312
- return job;
313
- }
314
- }
315
- return null;
316
- }
317
- /**
318
- * Runs the epic_request interview end-to-end: converses with the user via
319
- * post-turn/next-job until it has a title + the 3 required structured-desc
320
- * fields, then creates a personal-scope epic request via the real API
321
- * (POST /api/v1/me/epic-requests) using the user's own stored token.
322
- */
323
- export async function invokeEpicRequestViaApi(job, ctx) {
324
- // pxpipe (ENG-136): route API-mode calls through the proxy when enabled+healthy.
325
- const routing = await resolveRouting();
326
- const anthropic = new Anthropic(anthropicClientOptions(ctx.anthropicApiKey, routing));
327
- // ENG-149: a continuation job carries a seed → resume/enrich prompt + update
328
- // the target ER in place; a fresh job → normal prompt + create a new ER.
329
- const seed = parseSeedPayload(job.payload);
330
- const systemPrompt = resolveSystemPrompt(seed);
331
- // Audit-gate state (bug #2): the ER we are building (a continuation starts with
332
- // its target), and how many re-submit/re-audit rounds we've spent.
333
- let currentErId = seed?.targetEpicRequestId ?? null;
334
- let qualityRounds = 0;
335
- // Malformed-completion recovery: a model can emit "done" JSON we can't parse.
336
- let jsonRepairs = 0;
337
- const messages = [];
338
- // Seed the conversation with any turns already on the job (e.g. a resumed
339
- // daemon restart), so we don't re-ask questions already answered. A seeded
340
- // USER turn may carry image attachments (ENG-153) — fetch + attach them as
341
- // vision blocks; agent turns and image-free user turns are unchanged.
342
- for (const turn of job.turns) {
343
- if (turn.role === 'agent') {
344
- messages.push({ role: 'assistant', content: turn.content });
345
- }
346
- else {
347
- messages.push({ role: 'user', content: await apiUserTurnContent(turn) });
348
- }
349
- }
350
- let turnsSoFar = job.turns.length;
351
- for (let i = 0; i < MAX_INTERVIEW_TURNS; i++) {
352
- let reply;
353
- try {
354
- const response = await anthropic.messages.create({
355
- model: ANTHROPIC_MODEL,
356
- max_tokens: 1024,
357
- system: systemPrompt,
358
- messages: messages.length > 0 ? messages : [{ role: 'user', content: 'Let\'s get started.' }],
359
- });
360
- const textBlock = response.content.find((b) => b.type === 'text');
361
- reply = textBlock?.type === 'text' ? textBlock.text : '';
362
- }
363
- catch (error) {
364
- const message = error instanceof Error ? error.message : 'Anthropic API request failed';
365
- return { status: 'failed', result: { error: `Agent invocation failed: ${message}` } };
366
- }
367
- const draft = extractCompletionJson(reply);
368
- if (draft) {
369
- let submitted;
370
- try {
371
- submitted = await submitAndScore(ctx, currentErId, draft);
372
- }
373
- catch (error) {
374
- const message = error instanceof Error ? error.message : 'Failed to save the epic request';
375
- return { status: 'failed', result: { error: message } };
376
- }
377
- currentErId = submitted.erId;
378
- const compliance = submitted.compliance;
379
- // Finish only once the request passes the audit (>=90, no criticals), or
380
- // when the quality-round budget is spent — best-effort, never stranded.
381
- if (meetsComplianceTarget(compliance) || qualityRounds >= MAX_QUALITY_ROUNDS) {
382
- return { status: 'done', result: { epicRequestId: currentErId } };
383
- }
384
- // Not good enough: feed the failing rules back (a model-facing round, no
385
- // user turn) and loop — the model re-emits a richer completion or asks the
386
- // employee a targeted question next iteration. (compliance is non-null here:
387
- // meetsComplianceTarget(null) is true, so a null score would have finished.)
388
- qualityRounds++;
389
- if (compliance !== null) {
390
- messages.push({ role: 'assistant', content: reply });
391
- messages.push({ role: 'user', content: buildAuditFeedback(compliance) });
392
- }
393
- continue;
394
- }
395
- // The model TRIED to finish but the JSON was unusable (bug: malformed
396
- // completion relayed to the employee as a question, looping forever). Correct
397
- // the model instead of posting raw JSON — bounded, then fail cleanly.
398
- if (looksLikeCompletionAttempt(reply)) {
399
- if (jsonRepairs >= MAX_JSON_REPAIRS) {
400
- return {
401
- status: 'failed',
402
- result: {
403
- error: 'The interview produced malformed completion data and could not be finalized after several attempts.',
404
- },
405
- };
406
- }
407
- jsonRepairs++;
408
- messages.push({ role: 'assistant', content: reply });
409
- messages.push({ role: 'user', content: COMPLETION_REPAIR_INSTRUCTION });
410
- continue;
411
- }
412
- // Not done yet — `reply` is the next question. Post it and wait for the answer.
413
- messages.push({ role: 'assistant', content: reply });
414
- let updated;
415
- try {
416
- updated = await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, reply);
417
- }
418
- catch (error) {
419
- const message = error instanceof Error ? error.message : 'post-turn failed';
420
- return { status: 'failed', result: { error: message } };
421
- }
422
- turnsSoFar = updated.turns.length;
423
- const answered = await waitForUserReply(ctx.mailboxClient, ctx.machineToken, job.id, turnsSoFar);
424
- if (!answered) {
425
- return {
426
- status: 'failed',
427
- result: { error: 'Timed out waiting for a reply — no response within 10 minutes.' },
428
- };
429
- }
430
- const lastTurn = answered.turns[answered.turns.length - 1];
431
- // ENG-153: the employee's reply may include image attachments — attach them
432
- // as vision blocks (image-free replies stay a plain string, as before).
433
- messages.push({
434
- role: 'user',
435
- content: lastTurn ? await apiUserTurnContent(lastTurn) : '',
436
- });
437
- turnsSoFar = answered.turns.length;
438
- }
439
- // Turn budget exhausted. If we already produced an ER (fresh create or a
440
- // continuation's target), finalize with it best-effort rather than losing the
441
- // work (bug #3); only fail outright if nothing was ever submitted.
442
- return currentErId
443
- ? { status: 'done', result: { epicRequestId: currentErId } }
444
- : {
445
- status: 'failed',
446
- result: { error: `Interview did not converge after ${String(MAX_INTERVIEW_TURNS)} turns.` },
447
- };
448
- }
449
247
  /**
450
248
  * Fixed working directory for every `claude` CLI invocation made by the
451
249
  * subscription-mode invoker. Two reasons this must be a stable, dedicated
@@ -503,11 +301,11 @@ function claudeCliSessionCwd() {
503
301
  */
504
302
  function runClaudeCliTurn(prompt, opts) {
505
303
  return new Promise((resolve, reject) => {
304
+ const model = opts.model === undefined ? ANTHROPIC_MODEL : opts.model;
506
305
  const args = [
507
306
  '-p',
508
307
  prompt,
509
- '--model',
510
- ANTHROPIC_MODEL,
308
+ ...(model ? ['--model', model] : []),
511
309
  '--output-format',
512
310
  'json',
513
311
  '--tools',
@@ -595,123 +393,91 @@ function runClaudeCliTurn(prompt, opts) {
595
393
  });
596
394
  });
597
395
  }
396
+ // ---------------------------------------------------------------------
397
+ // Request conversations (always open): one AI reply per claim.
398
+ // ---------------------------------------------------------------------
399
+ /** The request this conversation already saved, read with the person's token. */
400
+ async function loadSavedRequest(apiBaseUrl, userApiToken, epicRequestId) {
401
+ const res = await fetch(`${apiBaseUrl.replace(/\/+$/, '')}/api/v1/epic-requests/${encodeURIComponent(epicRequestId)}`, { headers: { Authorization: `Bearer ${userApiToken}` } });
402
+ if (!res.ok)
403
+ return null;
404
+ const body = (await res.json().catch(() => null));
405
+ return savedContextFromRecord(body?.data);
406
+ }
407
+ /**
408
+ * True when the person's Claude plan can't use the requested model (for
409
+ * example Fable needs usage credits on some plans): retry on their default.
410
+ */
411
+ export function needsDefaultModel(error) {
412
+ const message = error instanceof Error ? error.message : String(error);
413
+ return /requires usage credits|switch to another model|model.*not available|not have access to/i.test(message);
414
+ }
415
+ function savedRequestIdOf(job) {
416
+ const result = job.result;
417
+ if (typeof result?.epicRequestId === 'string')
418
+ return result.epicRequestId;
419
+ return parseSeedPayload(job.payload)?.targetEpicRequestId ?? null;
420
+ }
598
421
  /**
599
- * Runs the epic_request interview end-to-end using the local `claude` CLI
600
- * (the employee's own Claude Code login) instead of the Anthropic API.
601
- * Mirrors invokeEpicRequestViaApi's turn loop and completion handling
602
- * exactly — only how a single assistant turn is produced differs.
422
+ * Takes one step of a request conversation on this computer: the AI's reply to
423
+ * the person's latest message, which may save or update their request. Then
424
+ * the job is left waiting on the person (never completed), so the chat stays
425
+ * open: there is no reply timeout, and the computer is free for other work in
426
+ * the meantime. The server hands the job back when they write again.
427
+ *
428
+ * `anthropicApiKey` set → the Anthropic API; otherwise the local `claude` CLI
429
+ * on the person's own Claude sign-in.
603
430
  */
604
- export async function invokeEpicRequestViaSubscription(job, ctx) {
605
- let sessionId;
606
- let turnsSoFar = job.turns.length;
607
- // pxpipe (ENG-136): resolve routing once per invocation; reused for every turn.
431
+ export async function invokeEpicRequestStep(job, ctx) {
432
+ try {
433
+ return await takeConversationStep(job, ctx);
434
+ }
435
+ catch (error) {
436
+ // Leave the conversation waiting on the person rather than retrying a
437
+ // broken step over and over; their next message tries again.
438
+ const reason = error instanceof Error ? error.message : 'something went wrong';
439
+ await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, `I couldn't continue: ${reason}\n\nSend another message (for example "try again") and I'll pick up where we left off.`);
440
+ return { status: 'yield' };
441
+ }
442
+ }
443
+ async function takeConversationStep(job, ctx) {
608
444
  const routing = await resolveRouting();
609
- // ENG-149: continuation-aware system prompt + submit path (see api-mode above).
610
- const seed = parseSeedPayload(job.payload);
611
- const systemPrompt = resolveSystemPrompt(seed);
612
- // Audit-gate state (bug #2) — mirrors api-mode.
613
- let currentErId = seed?.targetEpicRequestId ?? null;
614
- let qualityRounds = 0;
615
- let jsonRepairs = 0;
616
- // If the daemon restarted mid-interview, there's no local `claude` session
617
- // left to --resume (session IDs aren't persisted across daemon restarts
618
- // today) — replay the transcript so far as the seed prompt instead of
619
- // silently losing context, mirroring how the api-key path seeds its
620
- // Anthropic `messages` array from job.turns.
621
- let nextPrompt = job.turns.length > 0
622
- ? job.turns
623
- .map((t) => t.role === 'agent'
624
- ? `You asked: ${t.content}`
625
- : // ENG-153 (A4): subscription mode has no inline-image channel, so a
626
- // user turn's images degrade to an explicit textual note.
627
- `They replied: ${appendAttachmentNote(t.content, imageAttachmentCount(t.attachments))}`)
628
- .join('\n') +
629
- '\n\nContinue the interview from exactly where this left off — ask the next question, or if you already have everything, respond with the completion JSON.'
630
- : "Let's begin the interview.";
631
- for (let i = 0; i < MAX_INTERVIEW_TURNS; i++) {
632
- let turn;
633
- try {
634
- turn = await runClaudeCliTurn(nextPrompt, {
635
- systemPrompt,
636
- resumeSessionId: sessionId,
637
- routing,
638
- });
639
- }
640
- catch (error) {
641
- const message = error instanceof Error ? error.message : 'claude CLI invocation failed';
642
- return { status: 'failed', result: { error: `Agent invocation failed: ${message}` } };
643
- }
644
- sessionId = turn.sessionId;
645
- const draft = extractCompletionJson(turn.text);
646
- if (draft) {
647
- let submitted;
445
+ const requestId = savedRequestIdOf(job);
446
+ const saved = requestId ? await loadSavedRequest(ctx.apiBaseUrl, ctx.userApiToken, requestId) : null;
447
+ const turns = job.turns.map((t) => ({
448
+ role: t.role,
449
+ content: t.role === 'user' ? appendAttachmentNote(t.content, imageAttachmentCount(t.attachments)) : t.content,
450
+ }));
451
+ const anthropic = ctx.anthropicApiKey ? new Anthropic(anthropicClientOptions(ctx.anthropicApiKey, routing)) : null;
452
+ let cliModel = ANTHROPIC_MODEL;
453
+ const outcome = await runConversationStep({ turns, saved }, {
454
+ reply: async (system, messages) => {
455
+ if (anthropic) {
456
+ const response = await anthropic.messages.create({ model: ANTHROPIC_MODEL, max_tokens: 4096, system, messages });
457
+ const text = response.content.find((b) => b.type === 'text');
458
+ return text?.type === 'text' ? text.text : '';
459
+ }
460
+ const prompt = renderConversationPrompt(messages);
648
461
  try {
649
- submitted = await submitAndScore(ctx, currentErId, draft);
462
+ return (await runClaudeCliTurn(prompt, { systemPrompt: system, resumeSessionId: undefined, routing, model: cliModel })).text;
650
463
  }
651
464
  catch (error) {
652
- const message = error instanceof Error ? error.message : 'Failed to save the epic request';
653
- return { status: 'failed', result: { error: message } };
654
- }
655
- currentErId = submitted.erId;
656
- const compliance = submitted.compliance;
657
- if (meetsComplianceTarget(compliance) || qualityRounds >= MAX_QUALITY_ROUNDS) {
658
- return { status: 'done', result: { epicRequestId: currentErId } };
659
- }
660
- // Feed the failing rules back into the SAME resumed session (no user turn);
661
- // the model re-emits a richer completion or asks a targeted question next.
662
- // (compliance is non-null here — see the api-mode note above.)
663
- qualityRounds++;
664
- if (compliance !== null) {
665
- nextPrompt = buildAuditFeedback(compliance);
465
+ if (cliModel === null || !needsDefaultModel(error))
466
+ throw error;
467
+ cliModel = null;
468
+ return (await runClaudeCliTurn(prompt, { systemPrompt: system, resumeSessionId: undefined, routing, model: null })).text;
666
469
  }
667
- continue;
668
- }
669
- // Malformed completion (see api-mode): correct the model rather than relaying
670
- // raw JSON to the employee; bounded, then fail cleanly.
671
- if (looksLikeCompletionAttempt(turn.text)) {
672
- if (jsonRepairs >= MAX_JSON_REPAIRS) {
673
- return {
674
- status: 'failed',
675
- result: {
676
- error: 'The interview produced malformed completion data and could not be finalized after several attempts.',
677
- },
678
- };
679
- }
680
- jsonRepairs++;
681
- nextPrompt = COMPLETION_REPAIR_INSTRUCTION;
682
- continue;
683
- }
684
- // Not done yet — turn.text is the next question. Post it and wait for the answer.
685
- let updated;
686
- try {
687
- updated = await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, turn.text);
688
- }
689
- catch (error) {
690
- const message = error instanceof Error ? error.message : 'post-turn failed';
691
- return { status: 'failed', result: { error: message } };
692
- }
693
- turnsSoFar = updated.turns.length;
694
- const answered = await waitForUserReply(ctx.mailboxClient, ctx.machineToken, job.id, turnsSoFar);
695
- if (!answered) {
696
- return {
697
- status: 'failed',
698
- result: { error: 'Timed out waiting for a reply — no response within 10 minutes.' },
699
- };
700
- }
701
- const lastTurn = answered.turns[answered.turns.length - 1];
702
- // ENG-153 (A4): note any attached images textually (subscription-mode degradation).
703
- nextPrompt = lastTurn
704
- ? appendAttachmentNote(lastTurn.content, imageAttachmentCount(lastTurn.attachments))
705
- : '';
706
- turnsSoFar = answered.turns.length;
470
+ },
471
+ save: async (existingId, draft) => {
472
+ const submitted = await submitAndScore(ctx, existingId, draft);
473
+ return { id: submitted.erId, title: draft.title, compliance: submitted.compliance };
474
+ },
475
+ });
476
+ if (outcome.kind === 'saved') {
477
+ await ctx.mailboxClient.markRequestSaved(ctx.machineToken, job.id, outcome.saved.id);
707
478
  }
708
- // Budget exhausted — finalize best-effort if we produced an ER (bug #3).
709
- return currentErId
710
- ? { status: 'done', result: { epicRequestId: currentErId } }
711
- : {
712
- status: 'failed',
713
- result: { error: `Interview did not converge after ${String(MAX_INTERVIEW_TURNS)} turns.` },
714
- };
479
+ await ctx.mailboxClient.postTurn(ctx.machineToken, job.id, outcome.text);
480
+ return { status: 'yield' };
715
481
  }
716
482
  // ---------------------------------------------------------------------
717
483
  // start_execution / kind:'plan' (ENG-164) — run the local @planner headless