humanish 0.111.4 → 0.112.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/CHANGELOG.md +18 -0
- package/dist/actors/computer-use/openai-conversation.d.ts +91 -0
- package/dist/actors/computer-use/openai-conversation.js +209 -0
- package/dist/actors/computer-use/openai-provider.js +25 -41
- package/dist/actors/contract.d.ts +2 -0
- package/dist/analysis/diagnostics.js +7 -12
- package/dist/analysis/execute.d.ts +1 -1
- package/dist/analysis/execute.js +35 -5
- package/dist/analysis/service.js +7 -1
- package/dist/analysis/types.d.ts +23 -0
- package/dist/analysis/validation.d.ts +80 -35
- package/dist/analysis/validation.js +58 -11
- package/dist/cli/commands/runs.js +4 -10
- package/dist/cli/findings.d.ts +34 -3
- package/dist/cli/findings.js +136 -18
- package/dist/cli/io.d.ts +2 -1
- package/dist/cli/io.js +3 -2
- package/dist/feedback/draft.js +14 -4
- package/dist/observer/run-routes.js +8 -4
- package/dist/observer-app.html +4 -4
- package/dist/routes/computer-use/bundle.d.ts +1 -1
- package/dist/routes/computer-use/bundle.js +6 -16
- package/dist/routes/computer-use/fanout-bundle.js +40 -37
- package/dist/routes/computer-use/fanout-records.d.ts +2 -0
- package/dist/routes/computer-use/fanout-records.js +18 -26
- package/dist/routes/computer-use/participant-execution.js +4 -11
- package/dist/routes/computer-use/participant-facts.d.ts +2 -2
- package/dist/routes/computer-use/participant-facts.js +10 -1
- package/dist/routes/computer-use/result.js +7 -15
- package/dist/routes/computer-use/single-bundle.d.ts +2 -0
- package/dist/routes/computer-use/single-bundle.js +81 -37
- package/dist/routes/scripted/bundle.d.ts +1 -1
- package/dist/routes/scripted/bundle.js +9 -19
- package/dist/routes/scripted/surface-records.d.ts +0 -1
- package/dist/routes/scripted/surface-records.js +27 -13
- package/dist/routes/shared-world/bundle-records.d.ts +2 -2
- package/dist/routes/shared-world/bundle-records.js +21 -28
- package/dist/routes/shared-world/bundle.d.ts +1 -1
- package/dist/routes/shared-world/bundle.js +70 -78
- package/dist/routes/shared-world/participant-specs.d.ts +1 -2
- package/dist/routes/shared-world/participant-specs.js +0 -8
- package/dist/routes/shared-world/result.js +8 -12
- package/dist/routes/terminal/bundle.d.ts +1 -1
- package/dist/routes/terminal/bundle.js +10 -19
- package/dist/routes/terminal/dry-run.js +1 -0
- package/dist/routes/terminal/live-sandbox.js +9 -2
- package/dist/routes/terminal/plan.js +1 -0
- package/dist/routes/terminal/runtime.d.ts +1 -0
- package/dist/routes/terminal/runtime.js +1 -0
- package/dist/routes/terminal/session.js +1 -0
- package/dist/run/costs.js +9 -20
- package/dist/run/detail.d.ts +1 -1
- package/dist/run/detail.js +10 -1
- package/dist/run/display.d.ts +2 -2
- package/dist/run/display.js +15 -18
- package/dist/run/dry-run.js +10 -25
- package/dist/run/judge.d.ts +24 -0
- package/dist/run/judge.js +54 -0
- package/dist/run/liveness.d.ts +18 -0
- package/dist/run/liveness.js +24 -0
- package/dist/run/outcomes.d.ts +8 -1
- package/dist/run/outcomes.js +58 -12
- package/dist/run/participant-caption.d.ts +30 -0
- package/dist/run/participant-caption.js +50 -0
- package/dist/run/participant-records.d.ts +1 -1
- package/dist/run/review-markdown.d.ts +11 -0
- package/dist/run/review-markdown.js +22 -0
- package/dist/run/run-cost.d.ts +7 -0
- package/dist/run/run-cost.js +25 -5
- package/dist/run/run-index.d.ts +4 -50
- package/dist/run/run-index.js +57 -87
- package/dist/run/run.d.ts +2 -2
- package/dist/run/run.js +5 -3
- package/dist/run/stats.js +2 -12
- package/dist/run/transient-comms-secrets.d.ts +8 -3
- package/dist/run/transient-comms-secrets.js +21 -1
- package/dist/study/keys.d.ts +1 -0
- package/dist/study/keys.js +1 -1
- package/dist/study/parse/execution.js +7 -1
- package/dist/study/plan-types.d.ts +1 -0
- package/dist/study/types.d.ts +4 -1
- package/dist/study/warnings.d.ts +2 -1
- package/dist/study/warnings.js +3 -2
- package/dist/tui-app.js +73 -73
- package/dist/verify/liveness.d.ts +2 -2
- package/dist/verify/liveness.js +7 -4
- package/docs/architecture/terminal-product-route.md +14 -0
- package/docs/contracts/run-bundle.md +10 -0
- package/docs/contracts/schemas.md +9 -1
- package/docs/contracts/study-analysis.md +71 -22
- package/package.json +2 -1
- package/dist/actors/computer-use/openai-requests.d.ts +0 -62
- package/dist/actors/computer-use/openai-requests.js +0 -144
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,24 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.112.0: Plain finding headlines, design findings, plain captions, DO_NOT_TRACK for terminal participants (2026-10-07)
|
|
12
|
+
|
|
13
|
+
humanish 0.112.0 writes findings and captions for someone who did not watch the session, and
|
|
14
|
+
terminal participants send `DO_NOT_TRACK=1` by default. Each analysis finding leads with a plain
|
|
15
|
+
headline and an account of what the person tried, what got in their way and how it seemed to feel;
|
|
16
|
+
its title, summary and observations stay underneath as the evidence. Analyses also report design
|
|
17
|
+
findings: problems a product designer would notice in the captures, each with a severity and at
|
|
18
|
+
least one cited capture. `review --json` carries them as `analysis.designFindings`, and the
|
|
19
|
+
analysis prompt is `study-evidence-7`. Participant captions name the person, such as "Lobby host,
|
|
20
|
+
phone", and every route's run summary says how many participants took part and how many reached
|
|
21
|
+
the goal. Terminal participant and product setup commands run with `DO_NOT_TRACK=1`; set
|
|
22
|
+
`execution.terminal.doNotTrack: false` to study a product's telemetry. `humanish runs` and the
|
|
23
|
+
Observer now read a run with no outcome and a running simulation as interrupted, as the TUI,
|
|
24
|
+
`stats` and `verify` already did. The `--env-file` and `cleanup` aliases are removed in the first
|
|
25
|
+
minor release on or after 2026-11-03.
|
|
26
|
+
|
|
27
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.112.0)
|
|
28
|
+
|
|
11
29
|
## 0.111.4: Encoded known values scrubbed, scripted traces scrubbed, a live command after a dry run (2026-10-06)
|
|
12
30
|
|
|
13
31
|
humanish 0.111.4 scrubs a run's known values from more of its evidence and fixes three messages.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { CuaTurn, CuaTurnRequest } from "./loop.js";
|
|
2
|
+
import type { ReasoningEffort } from "../reasoning-effort.js";
|
|
3
|
+
import type { ActorConversation } from "../contract.js";
|
|
4
|
+
import { type OpenAiReasoningSummary } from "./openai-wire.js";
|
|
5
|
+
/** How a conversation starts. The model settings stay fixed for the whole session. */
|
|
6
|
+
export interface OpenAiConversationSettings {
|
|
7
|
+
readonly model: string;
|
|
8
|
+
readonly reasoningEffort: ReasoningEffort;
|
|
9
|
+
readonly maxOutputTokens?: number;
|
|
10
|
+
readonly safetyIdentifier?: string;
|
|
11
|
+
/** The summaries to ask for until the account or model rejects them; undefined asks for none. */
|
|
12
|
+
readonly reasoningSummary: OpenAiReasoningSummary | undefined;
|
|
13
|
+
/** Carry the conversation on the client from the first request (explicit_context). */
|
|
14
|
+
readonly zeroDataRetention: boolean;
|
|
15
|
+
/** The clock the trace's record reads. */
|
|
16
|
+
readonly now: () => number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* One request of the conversation. Its new items are built once, so every attempt at it, and the
|
|
20
|
+
* reply it accepts, see the same ones.
|
|
21
|
+
*/
|
|
22
|
+
export interface ConversationRequest {
|
|
23
|
+
/** The body as the conversation stands now, so an attempt after a rejection shows its effect. */
|
|
24
|
+
body(): Record<string, unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* The request was answered: records a participant request with what it carried, moves the
|
|
27
|
+
* conversation on to the reply and returns the reply's turn.
|
|
28
|
+
*/
|
|
29
|
+
accept(reply: unknown): CuaTurn;
|
|
30
|
+
}
|
|
31
|
+
/** Which answer told the provider that the organization keeps no server-side conversation. */
|
|
32
|
+
export type ZdrRejection = NonNullable<ActorConversation["rejection"]>;
|
|
33
|
+
export declare class OpenAiConversation {
|
|
34
|
+
private readonly settings;
|
|
35
|
+
private lastResponseId;
|
|
36
|
+
/** computer_call ids the next request owes an output for. */
|
|
37
|
+
private pendingCallIds;
|
|
38
|
+
/** Accepted replies so far; the next request is number `replies + 1`. */
|
|
39
|
+
private replies;
|
|
40
|
+
/** The whole conversation, which explicit-context requests carry within its token budget. */
|
|
41
|
+
private readonly carried;
|
|
42
|
+
private mode;
|
|
43
|
+
/** Undefined once the account or model rejected reasoning summaries. */
|
|
44
|
+
private reasoningSummary;
|
|
45
|
+
private readonly evidence;
|
|
46
|
+
constructor(settings: OpenAiConversationSettings);
|
|
47
|
+
/**
|
|
48
|
+
* The next request, carrying `req`'s screen: the opening request, or the outputs the latest
|
|
49
|
+
* reply's computer calls are owed. A closing report asks for a strict JSON report and no tools.
|
|
50
|
+
*/
|
|
51
|
+
request(req: CuaTurnRequest, closing?: boolean): ConversationRequest;
|
|
52
|
+
/**
|
|
53
|
+
* The organization rejected server-side state: carry the conversation on the client from the
|
|
54
|
+
* request being sent, and record which answer made the switch. False when the conversation is
|
|
55
|
+
* already carried, so the rejection stands.
|
|
56
|
+
*/
|
|
57
|
+
switchToExplicitContext(rejection: ZdrRejection): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The account or model rejected reasoning summaries: stop asking for the rest of the session,
|
|
60
|
+
* from the request being sent. False when none are asked for, so the rejection stands.
|
|
61
|
+
*/
|
|
62
|
+
dropReasoningSummaries(): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the server holds the whole session, so a closing report threaded on it reads every
|
|
65
|
+
* turn. An explicit-context conversation summarizes its oldest turns past its budget, configured
|
|
66
|
+
* or switched to at runtime.
|
|
67
|
+
*/
|
|
68
|
+
get serverHoldsSession(): boolean;
|
|
69
|
+
/** The trace's account of how the conversation was carried (ActorConversation). */
|
|
70
|
+
record(): ActorConversation;
|
|
71
|
+
/**
|
|
72
|
+
* The items a request adds to the conversation: the opening message with the first screen, or
|
|
73
|
+
* an output for each pending computer_call with the latest screenshot and any hint.
|
|
74
|
+
*/
|
|
75
|
+
private newItems;
|
|
76
|
+
private context;
|
|
77
|
+
/**
|
|
78
|
+
* A participant request: the opening request, or a continuation carrying `sent`. A threaded
|
|
79
|
+
* continuation names the previous response; an explicit-context one carries the conversation
|
|
80
|
+
* ahead of `sent`.
|
|
81
|
+
*/
|
|
82
|
+
private turnBody;
|
|
83
|
+
/** The read-only closing report: no tools, a small output limit, a strict schema. */
|
|
84
|
+
private closingBody;
|
|
85
|
+
/**
|
|
86
|
+
* Move the conversation on to a reply to a request that added `sent`. A participant reply cut
|
|
87
|
+
* off by the output limit is set aside, so the next request is the same one again
|
|
88
|
+
* (outputLimitRetry); its actions never run, so no call output is owed for them.
|
|
89
|
+
*/
|
|
90
|
+
private accept;
|
|
91
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { CarriedConversation, estimateTokens } from "./openai-context.js";
|
|
2
|
+
import { buildCallOutput, buildContinuationRequest, buildInitialRequest, hintItems, openingMessage, parseOpenAiResponse, } from "./openai-wire.js";
|
|
3
|
+
/** The closing report's output limit: its own cap, or the declared limit when that is lower. */
|
|
4
|
+
const CLOSING_OUTPUT_LIMIT = 1024;
|
|
5
|
+
export class OpenAiConversation {
|
|
6
|
+
settings;
|
|
7
|
+
lastResponseId;
|
|
8
|
+
/** computer_call ids the next request owes an output for. */
|
|
9
|
+
pendingCallIds = [];
|
|
10
|
+
/** Accepted replies so far; the next request is number `replies + 1`. */
|
|
11
|
+
replies = 0;
|
|
12
|
+
/** The whole conversation, which explicit-context requests carry within its token budget. */
|
|
13
|
+
carried = new CarriedConversation();
|
|
14
|
+
mode;
|
|
15
|
+
/** Undefined once the account or model rejected reasoning summaries. */
|
|
16
|
+
reasoningSummary;
|
|
17
|
+
evidence;
|
|
18
|
+
constructor(settings) {
|
|
19
|
+
this.settings = settings;
|
|
20
|
+
this.mode = settings.zeroDataRetention ? "explicit_context" : "threaded";
|
|
21
|
+
this.reasoningSummary = settings.reasoningSummary;
|
|
22
|
+
this.evidence = new ConversationRecord(settings.zeroDataRetention, settings.now);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The next request, carrying `req`'s screen: the opening request, or the outputs the latest
|
|
26
|
+
* reply's computer calls are owed. A closing report asks for a strict JSON report and no tools.
|
|
27
|
+
*/
|
|
28
|
+
request(req, closing = false) {
|
|
29
|
+
const sent = this.newItems(req);
|
|
30
|
+
return {
|
|
31
|
+
body: () => (closing ? this.closingBody(req, sent) : this.turnBody(req, sent)),
|
|
32
|
+
accept: (reply) => this.accept(reply, closing, sent),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The organization rejected server-side state: carry the conversation on the client from the
|
|
37
|
+
* request being sent, and record which answer made the switch. False when the conversation is
|
|
38
|
+
* already carried, so the rejection stands.
|
|
39
|
+
*/
|
|
40
|
+
switchToExplicitContext(rejection) {
|
|
41
|
+
if (this.mode === "explicit_context")
|
|
42
|
+
return false;
|
|
43
|
+
this.evidence.switched(rejection);
|
|
44
|
+
this.mode = "explicit_context";
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The account or model rejected reasoning summaries: stop asking for the rest of the session,
|
|
49
|
+
* from the request being sent. False when none are asked for, so the rejection stands.
|
|
50
|
+
*/
|
|
51
|
+
dropReasoningSummaries() {
|
|
52
|
+
if (this.reasoningSummary === undefined)
|
|
53
|
+
return false;
|
|
54
|
+
this.reasoningSummary = undefined;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Whether the server holds the whole session, so a closing report threaded on it reads every
|
|
59
|
+
* turn. An explicit-context conversation summarizes its oldest turns past its budget, configured
|
|
60
|
+
* or switched to at runtime.
|
|
61
|
+
*/
|
|
62
|
+
get serverHoldsSession() {
|
|
63
|
+
return this.mode === "threaded" && this.lastResponseId !== undefined;
|
|
64
|
+
}
|
|
65
|
+
/** The trace's account of how the conversation was carried (ActorConversation). */
|
|
66
|
+
record() {
|
|
67
|
+
return this.evidence.snapshot(this.mode);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The items a request adds to the conversation: the opening message with the first screen, or
|
|
71
|
+
* an output for each pending computer_call with the latest screenshot and any hint.
|
|
72
|
+
*/
|
|
73
|
+
newItems(req) {
|
|
74
|
+
if (this.replies === 0)
|
|
75
|
+
return [openingMessage(req.instructions, req.observation.screenshot)];
|
|
76
|
+
return [
|
|
77
|
+
...this.pendingCallIds.map((id) => buildCallOutput(id, req.observation.screenshot, req.acknowledgedSafetyChecks)),
|
|
78
|
+
...hintItems(req.contextHint),
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
context(instructions) {
|
|
82
|
+
const { settings } = this;
|
|
83
|
+
return {
|
|
84
|
+
stateless: this.mode === "explicit_context",
|
|
85
|
+
model: settings.model,
|
|
86
|
+
instructions,
|
|
87
|
+
reasoningEffort: settings.reasoningEffort,
|
|
88
|
+
...(settings.maxOutputTokens === undefined
|
|
89
|
+
? {}
|
|
90
|
+
: { maxOutputTokens: settings.maxOutputTokens }),
|
|
91
|
+
...(this.reasoningSummary === undefined ? {} : { reasoningSummary: this.reasoningSummary }),
|
|
92
|
+
...(settings.safetyIdentifier === undefined
|
|
93
|
+
? {}
|
|
94
|
+
: { safetyIdentifier: settings.safetyIdentifier }),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A participant request: the opening request, or a continuation carrying `sent`. A threaded
|
|
99
|
+
* continuation names the previous response; an explicit-context one carries the conversation
|
|
100
|
+
* ahead of `sent`.
|
|
101
|
+
*/
|
|
102
|
+
turnBody(req, sent) {
|
|
103
|
+
const ctx = this.context(req.instructions);
|
|
104
|
+
if (this.replies === 0)
|
|
105
|
+
return buildInitialRequest(ctx, req.observation.screenshot);
|
|
106
|
+
return buildContinuationRequest({
|
|
107
|
+
ctx,
|
|
108
|
+
previousResponseId: this.lastResponseId,
|
|
109
|
+
callOutputs: [...sent],
|
|
110
|
+
...(this.mode === "explicit_context" ? { explicitContextItems: this.carried.carried() } : {}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** The read-only closing report: no tools, a small output limit, a strict schema. */
|
|
114
|
+
closingBody(req, sent) {
|
|
115
|
+
return {
|
|
116
|
+
...this.turnBody(req, sent),
|
|
117
|
+
tool_choice: "none",
|
|
118
|
+
max_output_tokens: Math.min(this.settings.maxOutputTokens ?? CLOSING_OUTPUT_LIMIT, CLOSING_OUTPUT_LIMIT),
|
|
119
|
+
text: {
|
|
120
|
+
format: {
|
|
121
|
+
type: "json_schema",
|
|
122
|
+
name: "participant_closing_report",
|
|
123
|
+
strict: true,
|
|
124
|
+
schema: {
|
|
125
|
+
type: "object",
|
|
126
|
+
additionalProperties: false,
|
|
127
|
+
required: ["summary", "frictionReports"],
|
|
128
|
+
properties: {
|
|
129
|
+
summary: { type: "string" },
|
|
130
|
+
frictionReports: { type: "array", items: { type: "string" } },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Move the conversation on to a reply to a request that added `sent`. A participant reply cut
|
|
139
|
+
* off by the output limit is set aside, so the next request is the same one again
|
|
140
|
+
* (outputLimitRetry); its actions never run, so no call output is owed for them.
|
|
141
|
+
*/
|
|
142
|
+
accept(reply, closing, sent) {
|
|
143
|
+
// Recorded before the reply is accepted: accepting can cut the carried history, and the record
|
|
144
|
+
// counts what this request carried.
|
|
145
|
+
if (!closing)
|
|
146
|
+
this.evidence.requested(this.mode, this.carried, sent);
|
|
147
|
+
const parsed = parseOpenAiResponse(reply);
|
|
148
|
+
if (!closing && parsed.turn.interruption === "output_limit")
|
|
149
|
+
return parsed.turn;
|
|
150
|
+
if (parsed.turn.responseId !== undefined)
|
|
151
|
+
this.lastResponseId = parsed.turn.responseId;
|
|
152
|
+
this.pendingCallIds = parsed.callIds;
|
|
153
|
+
this.replies += 1;
|
|
154
|
+
this.carried.accept(this.replies, sent, parsed.outputItems);
|
|
155
|
+
return parsed.turn;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** The trace's account of how a session carried its conversation (ActorConversation). */
|
|
159
|
+
class ConversationRecord {
|
|
160
|
+
now;
|
|
161
|
+
explicitReason;
|
|
162
|
+
rejection;
|
|
163
|
+
switchedAt;
|
|
164
|
+
switchedAtRequest;
|
|
165
|
+
summarizedTurns = 0;
|
|
166
|
+
requests = [];
|
|
167
|
+
constructor(configured, now) {
|
|
168
|
+
this.now = now;
|
|
169
|
+
if (configured)
|
|
170
|
+
this.explicitReason = "configured";
|
|
171
|
+
}
|
|
172
|
+
/** The organization rejected server-side state, so the request being sent is explicit_context. */
|
|
173
|
+
switched(rejection) {
|
|
174
|
+
this.explicitReason = "zdr_rejection";
|
|
175
|
+
this.rejection = rejection;
|
|
176
|
+
this.switchedAt = new Date(this.now()).toISOString();
|
|
177
|
+
// Counted like requests[], which includes replies set aside at their output limit.
|
|
178
|
+
this.switchedAtRequest = this.requests.length + 1;
|
|
179
|
+
}
|
|
180
|
+
/** A participant request went out with `sent` after `conversation`. */
|
|
181
|
+
requested(mode, conversation, sent) {
|
|
182
|
+
if (mode !== "explicit_context") {
|
|
183
|
+
this.requests.push({ mode: "threaded" });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const carried = conversation.size();
|
|
187
|
+
// Only summaries a request carried count; the conversation also trims after the last reply.
|
|
188
|
+
this.summarizedTurns = carried.notedTurns;
|
|
189
|
+
this.requests.push({
|
|
190
|
+
mode: "explicit_context",
|
|
191
|
+
carriedExchanges: carried.exchanges,
|
|
192
|
+
carriedScreenshots: carried.screenshots,
|
|
193
|
+
estimatedInputTokens: carried.estimatedTokens + estimateTokens(sent),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
snapshot(mode) {
|
|
197
|
+
return {
|
|
198
|
+
mode,
|
|
199
|
+
...(this.explicitReason === undefined ? {} : { explicitReason: this.explicitReason }),
|
|
200
|
+
...(this.rejection === undefined ? {} : { rejection: this.rejection }),
|
|
201
|
+
...(this.switchedAt === undefined ? {} : { switchedAt: this.switchedAt }),
|
|
202
|
+
...(this.switchedAtRequest === undefined
|
|
203
|
+
? {}
|
|
204
|
+
: { switchedAtRequest: this.switchedAtRequest }),
|
|
205
|
+
summarizedTurns: this.summarizedTurns,
|
|
206
|
+
requests: this.requests.map((request) => ({ ...request })),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { validClosingReport } from "./loop.js";
|
|
2
2
|
import { ComputerUseAdmissionLimitError, isComputerUseAdmissionLimitError, } from "./admission-limit.js";
|
|
3
3
|
import { ComputerUsePromptRefusedError } from "./provider-error.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { asRecord, optionalString, parseOpenAiResponse, } from "./openai-wire.js";
|
|
4
|
+
import { OpenAiConversation, } from "./openai-conversation.js";
|
|
5
|
+
import { asRecord, optionalString } from "./openai-wire.js";
|
|
7
6
|
import { redactText } from "../../evidence/redaction.js";
|
|
8
7
|
import { isMaxOutputTokens } from "../output-token-limit.js";
|
|
9
8
|
import { prepareContainedOutputFile, prepareSelectedOutputDirectory, writeContainedOutputFile, } from "../../run/contained-output.js";
|
|
10
9
|
import { OPENAI_RESPONSES_URL } from "../openai-endpoint.js";
|
|
11
10
|
// A public-safe re-derivation of the OpenAI Responses API computer-use provider,
|
|
12
11
|
// behind the CuaProvider port from src/actors/computer-use/loop.ts. The pure wire mapping
|
|
13
|
-
// (openAiActionToCua, parseOpenAiResponse and the request builders) is in openai-wire.ts
|
|
12
|
+
// (openAiActionToCua, parseOpenAiResponse and the request builders) is in openai-wire.ts, and
|
|
13
|
+
// what each request carries, with the trace's record of it, is in openai-conversation.ts.
|
|
14
14
|
// This module is the live shim: createOpenAiResponsesProvider does a raw POST to the
|
|
15
15
|
// Responses endpoint (no SDK dependency) through an injectable FetchLike seam, so the retry,
|
|
16
16
|
// ZDR fallback, and state threading are testable with a fake.
|
|
@@ -412,24 +412,17 @@ export function createOpenAiResponsesProvider(options) {
|
|
|
412
412
|
// An unset or empty capture variable leaves capture off.
|
|
413
413
|
const capture = createWireCapture(optionalString((options.env ?? process.env)[WIRE_CAPTURE_ENV]?.trim()));
|
|
414
414
|
let interactionUsageIncomplete = false;
|
|
415
|
-
const
|
|
415
|
+
const conversation = new OpenAiConversation({
|
|
416
416
|
model,
|
|
417
417
|
reasoningEffort,
|
|
418
418
|
...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
|
|
419
419
|
...(options.safetyIdentifier === undefined
|
|
420
420
|
? {}
|
|
421
421
|
: { safetyIdentifier: options.safetyIdentifier }),
|
|
422
|
-
};
|
|
423
|
-
const state = {
|
|
424
|
-
lastResponseId: undefined,
|
|
425
|
-
pendingCallIds: [],
|
|
426
|
-
replies: 0,
|
|
427
|
-
conversation: new CarriedConversation(),
|
|
428
|
-
mode: options.zeroDataRetention ? "explicit_context" : "previous_response_id",
|
|
429
|
-
// Latches to undefined (stop asking) for the rest of the session when the
|
|
430
|
-
// account/model rejects the summary request; see OpenAiResponsesProviderOptions.
|
|
431
422
|
reasoningSummary: options.reasoningSummary === "off" ? undefined : (options.reasoningSummary ?? "auto"),
|
|
432
|
-
|
|
423
|
+
zeroDataRetention: options.zeroDataRetention === true,
|
|
424
|
+
now: options.now ?? Date.now,
|
|
425
|
+
});
|
|
433
426
|
const transport = {
|
|
434
427
|
endpoint: options.endpoint ?? OPENAI_RESPONSES_URL,
|
|
435
428
|
apiKey: options.apiKey,
|
|
@@ -441,25 +434,21 @@ export function createOpenAiResponsesProvider(options) {
|
|
|
441
434
|
},
|
|
442
435
|
};
|
|
443
436
|
const post = (body, signal, retries = maxRetries, interaction = true, spend) => postResponse(transport, body, signal, retries, interaction, spend);
|
|
444
|
-
const record = new ConversationRecord(options.zeroDataRetention === true, options.now ?? Date.now);
|
|
445
437
|
// POST with the recoverable-policy latches: a ZDR rejection switches to explicit-context mode; a
|
|
446
438
|
// reasoning-summary rejection latches summaries off. Each latch can flip only once, so the loop
|
|
447
439
|
// is bounded; anything else rethrows. The body is rebuilt per attempt so a flipped latch shows.
|
|
448
|
-
const postTurn = async (
|
|
440
|
+
const postTurn = async (request, signal, spend) => {
|
|
449
441
|
if (options.singleDispatch === true)
|
|
450
|
-
return post(
|
|
442
|
+
return post(request.body(), signal, 0);
|
|
451
443
|
for (;;) {
|
|
452
444
|
try {
|
|
453
|
-
return await post(
|
|
445
|
+
return await post(request.body(), signal, maxRetries, true, spend);
|
|
454
446
|
}
|
|
455
447
|
catch (error) {
|
|
456
|
-
if (error instanceof SummaryRejectionError &&
|
|
457
|
-
state.reasoningSummary = undefined;
|
|
448
|
+
if (error instanceof SummaryRejectionError && conversation.dropReasoningSummaries()) {
|
|
458
449
|
continue;
|
|
459
450
|
}
|
|
460
|
-
if (error instanceof ZdrError &&
|
|
461
|
-
record.switched(error.rejection);
|
|
462
|
-
state.mode = "explicit_context";
|
|
451
|
+
if (error instanceof ZdrError && conversation.switchToExplicitContext(error.rejection)) {
|
|
463
452
|
continue;
|
|
464
453
|
}
|
|
465
454
|
throw error;
|
|
@@ -468,29 +457,26 @@ export function createOpenAiResponsesProvider(options) {
|
|
|
468
457
|
};
|
|
469
458
|
const requestTurn = async (req, signal, closing = false, spend) => {
|
|
470
459
|
await capture.prepareNext();
|
|
471
|
-
const
|
|
460
|
+
const request = conversation.request(req, closing);
|
|
472
461
|
// A closing report makes exactly one request: no HTTP or policy-latch retries.
|
|
473
462
|
const raw = closing
|
|
474
|
-
? await post(
|
|
475
|
-
: await postTurn(
|
|
476
|
-
|
|
477
|
-
record.requested(state, sent);
|
|
478
|
-
const parsed = parseOpenAiResponse(raw);
|
|
479
|
-
acceptReply(state, parsed, closing, sent);
|
|
463
|
+
? await post(request.body(), signal, 0, false)
|
|
464
|
+
: await postTurn(request, signal, spend);
|
|
465
|
+
const turn = request.accept(raw);
|
|
480
466
|
if (closing) {
|
|
481
467
|
// Refusals, incomplete output, malformed JSON, and invalid shapes remain no-report results.
|
|
482
468
|
// Never promote raw JSON or a fallback paragraph into a structured finding.
|
|
483
469
|
try {
|
|
484
|
-
const report = JSON.parse(
|
|
470
|
+
const report = JSON.parse(turn.message ?? "");
|
|
485
471
|
if (asRecord(raw).status === "completed" && validClosingReport(report)) {
|
|
486
|
-
return { ...
|
|
472
|
+
return { ...turn, closingReport: report };
|
|
487
473
|
}
|
|
488
474
|
}
|
|
489
475
|
catch {
|
|
490
476
|
/* A failed optional closing account keeps its usage and no report. */
|
|
491
477
|
}
|
|
492
478
|
}
|
|
493
|
-
return
|
|
479
|
+
return turn;
|
|
494
480
|
};
|
|
495
481
|
return {
|
|
496
482
|
id: "openai-responses-cu",
|
|
@@ -512,15 +498,13 @@ export function createOpenAiResponsesProvider(options) {
|
|
|
512
498
|
},
|
|
513
499
|
nextTurn: (req, signal, spend) => requestTurn(req, signal, false, spend),
|
|
514
500
|
get conversation() {
|
|
515
|
-
return record
|
|
501
|
+
return conversation.record();
|
|
516
502
|
},
|
|
517
|
-
//
|
|
518
|
-
// hold the whole session a retrospective report needs. This getter follows both configured
|
|
519
|
-
// ZDR and a runtime policy latch.
|
|
503
|
+
// A retrospective report reads the session the server holds; see serverHoldsSession.
|
|
520
504
|
get debrief() {
|
|
521
|
-
return
|
|
522
|
-
?
|
|
523
|
-
:
|
|
505
|
+
return conversation.serverHoldsSession
|
|
506
|
+
? (req, signal) => requestTurn(req, signal, true)
|
|
507
|
+
: undefined;
|
|
524
508
|
},
|
|
525
509
|
};
|
|
526
510
|
}
|
|
@@ -191,6 +191,8 @@ export interface ParticipantClosingReport {
|
|
|
191
191
|
export interface ActorRuntimeProvenance {
|
|
192
192
|
schema: "humanish.actor-runtime.v1";
|
|
193
193
|
package: string;
|
|
194
|
+
/** Terminal command telemetry opt-out; absent in older bundles. */
|
|
195
|
+
doNotTrack?: boolean;
|
|
194
196
|
requestedVersion: string;
|
|
195
197
|
observedVersion?: string;
|
|
196
198
|
versionStatus: "unobserved" | "verified" | "failed";
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { lstat, readdir, rmdir, unlink } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { decodeEscapes } from "../evidence/encoded-text.js";
|
|
4
3
|
import { redactText } from "../evidence/redaction.js";
|
|
5
4
|
import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, bindExistingManagedHumanishOutputDirectory, prepareManagedHumanishOutputDirectory, writeContainedOutputFile, } from "../run/contained-output.js";
|
|
6
|
-
import {
|
|
5
|
+
import { transientCommsKnownValueScrub } from "../run/transient-comms-secrets.js";
|
|
7
6
|
import { ANALYSIS_ID_PATTERN } from "./types.js";
|
|
8
7
|
// A rejected analyst response is kept for diagnosis under .humanish/analysis-diagnostics/<run>/,
|
|
9
8
|
// outside every run directory. Export, verify and the Observer read run directories only, so the
|
|
@@ -14,19 +13,15 @@ const ANALYSIS_DIAGNOSTIC_SCHEMA = "humanish.analysis-rejected-output.v1";
|
|
|
14
13
|
/** How many rejected outputs are kept across all runs; older ones are removed on each write. */
|
|
15
14
|
export const MAX_ANALYSIS_DIAGNOSTICS = 20;
|
|
16
15
|
const REDACTED = "[REDACTED_SECRET]";
|
|
17
|
-
const markers = (text) => text.split(REDACTED).length - 1;
|
|
18
16
|
/**
|
|
19
|
-
* Scrub every string, key and scalar in the output. The
|
|
20
|
-
* percent-encoded, escaped or base64-encoded
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* Scrub every string, key and scalar in the output. The known-value scrub finds a value written
|
|
18
|
+
* percent-encoded, escaped or base64-encoded; a string where it finds nothing keeps its original
|
|
19
|
+
* spelling, so an escape in a rejected quote stays visible. A number or boolean equal to a known
|
|
20
|
+
* value becomes the marker.
|
|
23
21
|
*/
|
|
24
22
|
function scrubber() {
|
|
25
|
-
const
|
|
26
|
-
const text = (value) =>
|
|
27
|
-
const found = encoded(value);
|
|
28
|
-
return redactText(markers(found) > markers(decodeEscapes(value)) ? found : scrubTransientCommsText(value));
|
|
29
|
-
};
|
|
23
|
+
const known = transientCommsKnownValueScrub();
|
|
24
|
+
const text = (value) => redactText(known(value));
|
|
30
25
|
const scrub = (value) => {
|
|
31
26
|
if (typeof value === "string")
|
|
32
27
|
return text(value);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AnalysisArtifact, type AnalysisConfig, type AnalysisInput } from "./types.js";
|
|
2
2
|
import { type AnalysisFetch, type AnalysisProvider } from "./provider.js";
|
|
3
3
|
import type { RejectedAnalysisOutput } from "./diagnostics.js";
|
|
4
|
-
export declare const ANALYSIS_PROMPT_VERSION = "study-evidence-
|
|
4
|
+
export declare const ANALYSIS_PROMPT_VERSION = "study-evidence-7";
|
|
5
5
|
export declare const isSupportedAnalysisModel: (model: string) => boolean;
|
|
6
6
|
/** The OpenAI output limit when a study omits maxOutputTokens, and the most it may ask for. */
|
|
7
7
|
export declare const DEFAULT_ANALYSIS_MAX_OUTPUT_TOKENS = 16384;
|
package/dist/analysis/execute.js
CHANGED
|
@@ -3,14 +3,14 @@ import { highDetailImageTokens } from "./image-tokens.js";
|
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { estimateActorCost, MODEL_RATES } from "../run/pricing.js";
|
|
5
5
|
import { containsSensitive } from "../evidence/redaction.js";
|
|
6
|
-
import {
|
|
6
|
+
import { transientCommsKnownValueScrub } from "../run/transient-comms-secrets.js";
|
|
7
7
|
import { ANALYSIS_ID_PATTERN, ANALYSIS_SCHEMA, } from "./types.js";
|
|
8
8
|
import { INPUT_IMAGE_DATA_URL, createAnalysisProvider, } from "./provider.js";
|
|
9
9
|
import { checkAnalysisResult, hashAnalysisValue, analysisResponseSchema, analysisResultJsonSchema, validateAnalysisInputMetadata, } from "./validation.js";
|
|
10
10
|
import { EVIDENCE_LIMITS } from "./evidence.js";
|
|
11
11
|
import { truncatedFrameWarning, unknownNotificationsWarning, } from "../actors/codex/restricted-notifications.js";
|
|
12
12
|
import { protocolAdditionsWarning, protocolIncompatibilityMessage, } from "../actors/codex/protocol-compat.js";
|
|
13
|
-
export const ANALYSIS_PROMPT_VERSION = "study-evidence-
|
|
13
|
+
export const ANALYSIS_PROMPT_VERSION = "study-evidence-7";
|
|
14
14
|
const SUPPORTED_ANALYSIS_MODELS = Object.freeze([
|
|
15
15
|
"gpt-6-astra",
|
|
16
16
|
"gpt-5.5",
|
|
@@ -55,7 +55,11 @@ Harness records describe the machinery running the study. Provisioning, runtime
|
|
|
55
55
|
|
|
56
56
|
For each finding, consolidate repeated observations into one bounded problem or recovery. Each observation states one claim, labels its basis (visual, action, participant_statement, inference), cites supporting evidence, and declares its limitation. High confidence does not turn an inference into an observation. Show which distinct participants were affected and which were demonstrably exposed to the relevant interaction. Affected IDs must be a subset of exposed IDs, and both sets must be within included participants. Explain exposure; never automatically use the full panel as a denominator. Each affected participant must have supporting observation evidence.
|
|
57
57
|
|
|
58
|
-
Keep every field as qualified as its evidence. This includes
|
|
58
|
+
Keep every evidence field as qualified as its evidence. This includes titles, summaries, outcomes, impact, exposure, recovery, confidence and the premises of proposed next checks. A cautious observation cannot support an unqualified title. When a useful concern is established only as participant feedback, say it was reported wherever the concern is summarized. A dispatched action establishes an attempt; its success needs resulting evidence. Use recovered only when the improvement is supported for the participants being grouped. If recovery differs across participants, describe those differences and use unknown for the combined recovery instead of erasing an unresolved case. Keep distinct problems separate, especially an unresolved result and a different problem that was corrected. Reversible exploration and unmeasured pauses are not automatically product defects.
|
|
59
|
+
|
|
60
|
+
Write for the person who will act on the report: a designer, product manager or developer who did not watch the session and does not know this tool's vocabulary. Give every finding a headline and an experience. The headline is one plain sentence about what happened to the people involved, as a colleague would say it out loud. The experience is one to three plain sentences: what the person was trying to do, what got in their way, and how it seemed to feel, using the participant's own words where a quote exists. Do not use participant stream IDs, evidence IDs, or this report's evidence vocabulary (exposure, exposed, stream, capture, corroborated, established, basis, provenance) in the headline or experience. Plain is not unqualified: a concern known only from what participants said reads as something they said ("Two players said they lost track of which control had focus"), and a cause the evidence does not show is not stated as fact. The title, summary and observations keep their current precision underneath.
|
|
61
|
+
|
|
62
|
+
Separately, review the supplied captures as an experienced product designer would and report designFindings: problems a designer would notice in what the screens show, whether or not any participant mentioned them. Look at the size of text and controls (too small to read or to hit comfortably at this window size), layout and density (spread too wide or packed too tight for the task), visual hierarchy, legibility and contrast, wording and labels, consistency between screens, feedback after actions, and anything that makes the product look unfinished or untrustworthy. Judge the screens as the participants' personas would meet them: who they are and what they came to do. Each design finding names the screen in plain words, says what a designer notices, why it matters to a person using the product, and one concrete suggestion. Each must cite at least one supplied capture that shows the problem, and seenByStreamIds lists only participants whose cited captures show it. Rate severity by the effect on a person using the product: major when it misleads or blocks, moderate when it slows or confuses, minor when it is polish. Report only what the captures show; no capture, no design finding. Use D1, D2, and so on as IDs. An empty designFindings array is correct when the captures show no design problem worth a designer's time. Write design findings in the same plain register as headlines.
|
|
59
63
|
|
|
60
64
|
Order findings by observed task impact, replication among exposed participants, and recovery. Preserve severity and confidence as separate fields. Do not compute a numeric frustration score or universal priority score. Use F1, F2, and so on as local finding IDs. Explain ordering with concrete evidence. Provide a short, testable next check for each finding. Say when recovery was not observed rather than claiming it was impossible. Keep titles and summaries concise and specific. The overall summary must be grounded in participant reviews and observations, including successful outcomes and evidence limits.`;
|
|
61
65
|
function instructions(config) {
|
|
@@ -213,9 +217,10 @@ export function preferLargerAnalysisOutput(input, config) {
|
|
|
213
217
|
const expanded = { ...config, maxOutputTokens: MAX_ANALYSIS_OUTPUT_TOKENS };
|
|
214
218
|
return estimateAnalysisAdmission(input, expanded).allowed ? expanded : config;
|
|
215
219
|
}
|
|
216
|
-
/** Scrub only generated prose. Source evidence, provenance and integrity hashes remain exact.
|
|
220
|
+
/** Scrub only generated prose. Source evidence, provenance and integrity hashes remain exact. A
|
|
221
|
+
* known value is found as written and in its encoded forms. */
|
|
217
222
|
function scrubGeneratedNarrative(result) {
|
|
218
|
-
const scrub =
|
|
223
|
+
const scrub = transientCommsKnownValueScrub();
|
|
219
224
|
const observation = (value) => ({
|
|
220
225
|
...value,
|
|
221
226
|
claim: scrub(value.claim),
|
|
@@ -239,6 +244,13 @@ function scrubGeneratedNarrative(result) {
|
|
|
239
244
|
...value.exposedStreamIds,
|
|
240
245
|
...value.observations.flatMap((item) => [item.basis, ...item.evidenceIds]),
|
|
241
246
|
]),
|
|
247
|
+
...(result.designFindings ?? []).flatMap((value) => [
|
|
248
|
+
value.id,
|
|
249
|
+
value.severity,
|
|
250
|
+
value.confidence,
|
|
251
|
+
...value.seenByStreamIds,
|
|
252
|
+
...value.evidenceIds,
|
|
253
|
+
]),
|
|
242
254
|
...(result.concernReviews ?? []).flatMap((value) => [
|
|
243
255
|
value.basis,
|
|
244
256
|
value.disposition,
|
|
@@ -263,12 +275,26 @@ function scrubGeneratedNarrative(result) {
|
|
|
263
275
|
findings: result.findings.map((value) => ({
|
|
264
276
|
...value,
|
|
265
277
|
title: scrub(value.title),
|
|
278
|
+
...(value.headline === undefined ? {} : { headline: scrub(value.headline) }),
|
|
279
|
+
...(value.experience === undefined ? {} : { experience: scrub(value.experience) }),
|
|
266
280
|
summary: scrub(value.summary),
|
|
267
281
|
exposureReason: scrub(value.exposureReason),
|
|
268
282
|
nextStep: scrub(value.nextStep),
|
|
269
283
|
priorityReason: scrub(value.priorityReason),
|
|
270
284
|
observations: value.observations.map(observation),
|
|
271
285
|
})),
|
|
286
|
+
...(result.designFindings === undefined
|
|
287
|
+
? {}
|
|
288
|
+
: {
|
|
289
|
+
designFindings: result.designFindings.map((value) => ({
|
|
290
|
+
...value,
|
|
291
|
+
headline: scrub(value.headline),
|
|
292
|
+
screen: scrub(value.screen),
|
|
293
|
+
notice: scrub(value.notice),
|
|
294
|
+
whyItMatters: scrub(value.whyItMatters),
|
|
295
|
+
suggestion: scrub(value.suggestion),
|
|
296
|
+
})),
|
|
297
|
+
}),
|
|
272
298
|
...(result.concernReviews === undefined
|
|
273
299
|
? {}
|
|
274
300
|
: {
|
|
@@ -281,6 +307,10 @@ function scrubGeneratedNarrative(result) {
|
|
|
281
307
|
}
|
|
282
308
|
const VALIDATION_FAILURES = Object.freeze({
|
|
283
309
|
ANALYSIS_RESULT_SCHEMA_INVALID: "analysis_validation_failed_schema_invalid",
|
|
310
|
+
ANALYSIS_DESIGN_FINDING_ID_DUPLICATE: "analysis_validation_failed_design_finding_id_duplicate",
|
|
311
|
+
ANALYSIS_DESIGN_REFERENCE_INVALID: "analysis_validation_failed_design_reference_invalid",
|
|
312
|
+
ANALYSIS_DESIGN_WITHOUT_CAPTURE: "analysis_validation_failed_design_without_capture",
|
|
313
|
+
ANALYSIS_DESIGN_MEMBERSHIP_INVALID: "analysis_validation_failed_design_membership_invalid",
|
|
284
314
|
ANALYSIS_INPUT_DUPLICATES: "analysis_validation_failed_input_duplicates",
|
|
285
315
|
ANALYSIS_PARTICIPANT_COVERAGE_INVALID: "analysis_validation_failed_participant_coverage_invalid",
|
|
286
316
|
ANALYSIS_PARTICIPANT_REFERENCE_INVALID: "analysis_validation_failed_participant_reference_invalid",
|
package/dist/analysis/service.js
CHANGED
|
@@ -5,6 +5,7 @@ import { lstat, mkdir, realpath, rmdir } from "node:fs/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { renderObserver } from "../observer/render.js";
|
|
7
7
|
import { containsSensitive } from "../evidence/redaction.js";
|
|
8
|
+
import { transientCommsKnownValueScrub } from "../run/transient-comms-secrets.js";
|
|
8
9
|
import { verifyRunPrepared } from "../verify/verify.js";
|
|
9
10
|
import { loadRunBundlePrepared, resolveRunPath } from "../run/locate.js";
|
|
10
11
|
import { physicalCwdOf, runIdOf, resolvePhysicalCwd, validatePreparedRunRootIdentity, } from "../run/paths.js";
|
|
@@ -397,7 +398,12 @@ export async function correctAnalysis(cwd, run, options) {
|
|
|
397
398
|
const finding = analysis?.result?.findings.find((item) => item.id === options.findingId);
|
|
398
399
|
if (loaded.state !== "ready" || !analysis || !finding)
|
|
399
400
|
throw new Error("ANALYSIS_CORRECTION_SOURCE_UNAVAILABLE");
|
|
400
|
-
|
|
401
|
+
// A reviewer's claim replaces the finding's headline in every view, so it gets the same known
|
|
402
|
+
// value check as generated text: as written and encoded. Only a caller inside the run's secret
|
|
403
|
+
// scope holds those values; a separate `analyze correct` has only the pattern check.
|
|
404
|
+
const known = transientCommsKnownValueScrub();
|
|
405
|
+
const unsafe = (text) => containsSensitive(text) || known(text) !== text;
|
|
406
|
+
if (unsafe(options.reason) || unsafe(options.replacementClaim ?? ""))
|
|
401
407
|
throw new Error("ANALYSIS_CORRECTION_TEXT_UNSAFE");
|
|
402
408
|
const correction = {
|
|
403
409
|
schema: ANALYSIS_CORRECTION_SCHEMA,
|