humanish 0.114.0 → 0.116.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 +41 -0
- package/CONTEXT.md +1 -1
- package/README.md +7 -6
- package/TELEMETRY.md +6 -0
- package/dist/actors/closing-report-limits.d.ts +11 -0
- package/dist/actors/closing-report-limits.js +13 -0
- package/dist/actors/closing-report.d.ts +68 -0
- package/dist/actors/closing-report.js +55 -0
- package/dist/actors/codex/app-server-ui.js +11 -4
- package/dist/actors/codex/restricted-participant-policy.d.ts +4 -47
- package/dist/actors/codex/restricted-participant-policy.js +12 -40
- package/dist/actors/computer-use/loop/debrief.d.ts +1 -10
- package/dist/actors/computer-use/loop/debrief.js +68 -70
- package/dist/actors/computer-use/loop/impressions.d.ts +2 -2
- package/dist/actors/computer-use/loop/impressions.js +6 -1
- package/dist/actors/computer-use/loop.d.ts +0 -1
- package/dist/actors/computer-use/loop.js +3 -3
- package/dist/actors/computer-use/openai-conversation.d.ts +1 -1
- package/dist/actors/computer-use/openai-conversation.js +3 -29
- package/dist/actors/computer-use/openai-provider.js +11 -5
- package/dist/actors/contract.d.ts +2 -20
- package/dist/actors/contract.js +0 -13
- package/dist/actors/scripted-browser/steps.js +6 -5
- package/dist/analysis/admission.d.ts +14 -4
- package/dist/analysis/admission.js +14 -9
- package/dist/analysis/analysis-limits.d.ts +35 -0
- package/dist/analysis/analysis-limits.js +41 -0
- package/dist/analysis/execute.d.ts +5 -0
- package/dist/analysis/execute.js +3 -0
- package/dist/analysis/finding-lead.d.ts +27 -0
- package/dist/analysis/finding-lead.js +23 -0
- package/dist/analysis/service.js +5 -3
- package/dist/analysis/validation.js +14 -21
- package/dist/cli/commands/analyze.js +2 -1
- package/dist/cli/commands/notes.js +5 -3
- package/dist/cli/commands/run-signals.js +5 -1
- package/dist/cli/commands/runs.js +2 -1
- package/dist/cli/commands/setup.js +3 -3
- package/dist/cli/commands/study-format.d.ts +5 -2
- package/dist/cli/commands/study-format.js +29 -9
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-scripted.js +1 -1
- package/dist/cli/commands/tui.js +59 -17
- package/dist/cli/doctor.js +9 -2
- package/dist/cli/findings-text.d.ts +5 -0
- package/dist/cli/findings-text.js +169 -0
- package/dist/cli/findings.d.ts +0 -4
- package/dist/cli/findings.js +11 -195
- package/dist/cli/io.js +10 -8
- package/dist/cli/telemetry.js +10 -22
- package/dist/cli/update-check.js +10 -19
- package/dist/cli/user-config.d.ts +11 -0
- package/dist/cli/user-config.js +25 -0
- package/dist/comms/connections.js +4 -3
- package/dist/comms/email-catch.js +7 -31
- package/dist/comms/setup.js +18 -12
- package/dist/evidence/encoded-text.d.ts +12 -11
- package/dist/evidence/encoded-text.js +81 -70
- package/dist/evidence/redaction.d.ts +11 -0
- package/dist/evidence/redaction.js +30 -11
- package/dist/evidence/secret-scrub.d.ts +26 -3
- package/dist/evidence/secret-scrub.js +172 -38
- package/dist/feedback/draft.d.ts +1 -1
- package/dist/feedback/draft.js +13 -16
- package/dist/feedback/export-bundle.js +21 -17
- package/dist/feedback/export.js +32 -46
- package/dist/feedback/feedback.js +31 -26
- package/dist/guest/desktop-keys.js +59 -5
- package/dist/keys/key-resolution.d.ts +9 -0
- package/dist/keys/key-resolution.js +62 -55
- package/dist/keys/key-status.d.ts +2 -0
- package/dist/keys/key-status.js +9 -16
- package/dist/observer/artifact.d.ts +1 -1
- package/dist/observer/http.d.ts +7 -0
- package/dist/observer/http.js +19 -1
- package/dist/observer/notes-route.d.ts +0 -1
- package/dist/observer/notes-route.js +24 -44
- package/dist/observer/pinned-files.js +4 -19
- package/dist/observer/render.js +6 -4
- package/dist/observer/run-routes.js +13 -28
- package/dist/observer/serve.js +2 -1
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/bundle-parts.d.ts +4 -4
- package/dist/routes/computer-use/bundle-parts.js +2 -2
- package/dist/routes/computer-use/bundle.d.ts +4 -5
- package/dist/routes/computer-use/bundle.js +1 -3
- package/dist/routes/computer-use/fanout-records.js +10 -11
- package/dist/routes/computer-use/live-phase.d.ts +1 -1
- package/dist/routes/computer-use/live-phase.js +0 -1
- package/dist/routes/computer-use/participant-execution.d.ts +3 -2
- package/dist/routes/computer-use/participant-runs.d.ts +6 -2
- package/dist/routes/computer-use/participant-runs.js +9 -5
- package/dist/routes/computer-use/plan.d.ts +3 -0
- package/dist/routes/computer-use/plan.js +14 -11
- package/dist/routes/computer-use/rerun-selection.js +2 -2
- package/dist/routes/computer-use/result.js +3 -6
- package/dist/routes/computer-use/setup.d.ts +4 -6
- package/dist/routes/computer-use/setup.js +2 -6
- package/dist/routes/computer-use/single-bundle.d.ts +4 -5
- package/dist/routes/computer-use/single-bundle.js +22 -25
- package/dist/routes/computer-use/subject-projection.d.ts +4 -8
- package/dist/routes/computer-use/subject-projection.js +9 -31
- package/dist/routes/computer-use/types.d.ts +4 -41
- package/dist/routes/scripted/scenario.js +28 -12
- package/dist/routes/scripted/session-result.js +6 -3
- package/dist/routes/shared-world/bundle-records.js +2 -8
- package/dist/routes/shared-world/participant-specs.d.ts +0 -2
- package/dist/routes/shared-world/participant-specs.js +1 -20
- package/dist/routes/shared-world/plan.d.ts +3 -0
- package/dist/routes/shared-world/plan.js +23 -3
- package/dist/routes/shared-world/setup.js +2 -2
- package/dist/routes/terminal/ledger.d.ts +9 -9
- package/dist/routes/terminal/ledger.js +63 -40
- package/dist/routes/terminal/live-finish.js +7 -10
- package/dist/routes/terminal/plan.d.ts +3 -0
- package/dist/routes/terminal/plan.js +8 -5
- package/dist/routes/terminal/token-usage.d.ts +1 -2
- package/dist/routes/terminal/token-usage.js +4 -6
- package/dist/routes/terminal/types.d.ts +9 -1
- package/dist/run/bundle-shape.js +6 -1
- package/dist/run/bundle.d.ts +5 -3
- package/dist/run/contained-output.d.ts +59 -10
- package/dist/run/contained-output.js +160 -68
- package/dist/run/detail.d.ts +2 -1
- package/dist/run/detail.js +12 -7
- package/dist/run/escape-sequences.d.ts +11 -0
- package/dist/run/escape-sequences.js +93 -0
- package/dist/run/evidence-files.d.ts +8 -1
- package/dist/run/evidence-files.js +24 -18
- package/dist/run/locate.d.ts +32 -3
- package/dist/run/locate.js +45 -40
- package/dist/run/note-files.d.ts +26 -45
- package/dist/run/note-files.js +86 -58
- package/dist/run/note-shape.d.ts +35 -0
- package/dist/run/note-shape.js +20 -0
- package/dist/run/notes.d.ts +3 -14
- package/dist/run/notes.js +23 -75
- package/dist/run/outcomes.d.ts +24 -0
- package/dist/run/outcomes.js +19 -0
- package/dist/run/participant-records.d.ts +2 -0
- package/dist/run/participant-records.js +17 -2
- package/dist/run/paths.d.ts +2 -0
- package/dist/run/paths.js +5 -1
- package/dist/run/preflight-receipts.js +13 -8
- package/dist/run/project.js +7 -4
- package/dist/run/reclaim-outcomes.d.ts +8 -6
- package/dist/run/reclaim-outcomes.js +10 -6
- package/dist/run/reclaim.d.ts +5 -0
- package/dist/run/reclaim.js +43 -16
- package/dist/run/run-clock.d.ts +50 -0
- package/dist/run/run-clock.js +66 -0
- package/dist/run/run.js +5 -2
- package/dist/run/sandbox-ids.d.ts +7 -2
- package/dist/run/sandbox-ids.js +27 -11
- package/dist/run/secrets.d.ts +2 -1
- package/dist/run/secrets.js +18 -22
- package/dist/run/stored-runs.js +14 -7
- package/dist/run/transient-comms-secrets.d.ts +6 -5
- package/dist/run/transient-comms-secrets.js +23 -39
- package/dist/run-study.js +22 -13
- package/dist/study/adapter-scorer-loader.js +6 -4
- package/dist/study/discover.js +9 -6
- package/dist/study/init-paths.js +7 -5
- package/dist/study/persona-resolve.d.ts +2 -2
- package/dist/study/persona-resolve.js +9 -5
- package/dist/study/plan-types.d.ts +2 -2
- package/dist/study/plan.d.ts +5 -3
- package/dist/study/plan.js +18 -9
- package/dist/study/preflight-probes.js +11 -3
- package/dist/study/preflight.js +9 -5
- package/dist/study/summary.d.ts +12 -7
- package/dist/study/summary.js +24 -24
- package/dist/study/warnings.d.ts +19 -12
- package/dist/study/warnings.js +39 -17
- package/dist/substrates/e2b/connection.d.ts +12 -0
- package/dist/substrates/e2b/connection.js +35 -0
- package/dist/substrates/e2b/desktop-executor.d.ts +1 -3
- package/dist/substrates/e2b/desktop-executor.js +42 -9
- package/dist/substrates/e2b/lifetime.d.ts +24 -4
- package/dist/substrates/e2b/lifetime.js +41 -4
- package/dist/substrates/e2b/sandbox.d.ts +4 -3
- package/dist/substrates/e2b/sandbox.js +13 -18
- package/dist/substrates/e2b/sdk.d.ts +20 -9
- package/dist/substrates/local/capacity.d.ts +18 -2
- package/dist/substrates/local/capacity.js +21 -2
- package/dist/substrates/local/runtime-host.js +21 -14
- package/dist/substrates/local/runtime.js +3 -11
- package/dist/tui/contract.d.ts +30 -5
- package/dist/tui/contract.js +4 -1
- package/dist/tui-app.js +149 -147
- package/dist/verify/artifacts.d.ts +9 -2
- package/dist/verify/artifacts.js +37 -13
- package/dist/verify/liveness.js +19 -5
- package/dist/verify/verify.js +54 -27
- package/docs/architecture/actor-contract.md +3 -3
- package/docs/architecture/guest-desktop.md +7 -2
- package/docs/architecture/restricted-codex-analysis.md +1 -1
- package/docs/architecture/state-driven-executor.md +5 -3
- package/docs/contracts/run-bundle.md +25 -13
- package/docs/contracts/schemas.md +38 -29
- package/docs/contracts/study-analysis.md +22 -5
- package/docs/product/automatic-analysis.md +7 -1
- package/package.json +8 -7
- package/skills/humanish/SKILL.md +1 -2
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// The text of a run's `humanish.analysis-findings.v1` view (findings.ts): the full block `review`
|
|
2
|
+
// and `analyze show` print, and the summary at the end of a live `run`. It reads only the view.
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { findingLead } from "../analysis/finding-lead.js";
|
|
5
|
+
import { plural } from "../run/text.js";
|
|
6
|
+
const IMPACT_TEXT = {
|
|
7
|
+
blocked_task: "blocked task",
|
|
8
|
+
friction: "friction",
|
|
9
|
+
recovery: "recovery",
|
|
10
|
+
uncertain: "uncertain",
|
|
11
|
+
};
|
|
12
|
+
const RECOVERY_TEXT = {
|
|
13
|
+
recovered: "recovered",
|
|
14
|
+
not_observed: "no recovery observed",
|
|
15
|
+
unknown: "recovery unknown",
|
|
16
|
+
};
|
|
17
|
+
/** `+1:05` from milliseconds since the first retained capture. */
|
|
18
|
+
function elapsedText(ms) {
|
|
19
|
+
const seconds = Math.floor(ms / 1000);
|
|
20
|
+
const minutes = Math.floor(seconds / 60);
|
|
21
|
+
const hours = Math.floor(minutes / 60);
|
|
22
|
+
const ss = String(seconds % 60).padStart(2, "0");
|
|
23
|
+
return hours > 0
|
|
24
|
+
? `+${hours}:${String(minutes % 60).padStart(2, "0")}:${ss}`
|
|
25
|
+
: `+${minutes}:${ss}`;
|
|
26
|
+
}
|
|
27
|
+
/** One participant's cited moments: each frame once, at its earliest cited time. */
|
|
28
|
+
function momentsText(evidence) {
|
|
29
|
+
const frames = new Map();
|
|
30
|
+
let unframed = 0;
|
|
31
|
+
for (const entry of evidence) {
|
|
32
|
+
if (entry.frame === null) {
|
|
33
|
+
unframed++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const earlier = frames.get(entry.frame);
|
|
37
|
+
if (earlier === undefined ||
|
|
38
|
+
(entry.elapsedMs !== null && (earlier ?? Infinity) > entry.elapsedMs))
|
|
39
|
+
frames.set(entry.frame, entry.elapsedMs);
|
|
40
|
+
}
|
|
41
|
+
const parts = [...frames]
|
|
42
|
+
.sort(([a], [b]) => a - b)
|
|
43
|
+
.map(([frame, ms]) => `frame ${frame}${ms === null ? "" : ` at ${elapsedText(ms)}`}`);
|
|
44
|
+
if (unframed > 0)
|
|
45
|
+
parts.push(`${plural(unframed, "item")} with no frame`);
|
|
46
|
+
return parts.join(", ");
|
|
47
|
+
}
|
|
48
|
+
/** The capture files, relative to the run directory, in frame order. */
|
|
49
|
+
function captureFiles(evidence, runRoot) {
|
|
50
|
+
return [
|
|
51
|
+
...new Set(evidence
|
|
52
|
+
.filter((entry) => entry.capture !== null)
|
|
53
|
+
.sort((a, b) => (a.frame ?? 0) - (b.frame ?? 0))
|
|
54
|
+
.map((entry) => (runRoot ? path.relative(runRoot, entry.capture) : entry.capture))),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
function findingLines(finding, runRoot) {
|
|
58
|
+
const cited = [...new Set(finding.evidence.map((entry) => entry.streamId))];
|
|
59
|
+
const labels = new Map(finding.affected.map((p) => [p.streamId, p.label]));
|
|
60
|
+
const affectedIds = finding.affected.map((p) => p.streamId);
|
|
61
|
+
const streams = [...affectedIds, ...cited.filter((id) => !labels.has(id))];
|
|
62
|
+
const captures = captureFiles(finding.evidence, runRoot);
|
|
63
|
+
const correction = finding.correction;
|
|
64
|
+
const lead = findingLead(finding, correction);
|
|
65
|
+
return [
|
|
66
|
+
...(lead === null
|
|
67
|
+
? [`${finding.id} ${finding.title}`]
|
|
68
|
+
: [
|
|
69
|
+
`${finding.id} ${lead.headline}`,
|
|
70
|
+
...(lead.account === null ? [] : [` ${lead.account}`]),
|
|
71
|
+
` evidence: ${finding.title}`,
|
|
72
|
+
]),
|
|
73
|
+
` impact: ${IMPACT_TEXT[finding.impact]} · confidence: ${finding.confidence} · recovery: ${RECOVERY_TEXT[finding.recovery]}`,
|
|
74
|
+
` ${finding.summary}`,
|
|
75
|
+
` affected: ${finding.affected.length} of ${plural(finding.exposedCount, "exposed participant")}`,
|
|
76
|
+
...streams.map((streamId) => {
|
|
77
|
+
const moments = momentsText(finding.evidence.filter((entry) => entry.streamId === streamId));
|
|
78
|
+
const label = `${labels.get(streamId) ?? streamId}${labels.has(streamId) ? "" : " (cited, not affected)"}`;
|
|
79
|
+
return ` - ${label}${moments ? `: ${moments}` : ""}`;
|
|
80
|
+
}),
|
|
81
|
+
...(captures.length === 0 ? [] : [` captures: ${captures.join(", ")}`]),
|
|
82
|
+
` next step: ${finding.nextStep}`,
|
|
83
|
+
...(correction === null
|
|
84
|
+
? []
|
|
85
|
+
: [
|
|
86
|
+
` human review: ${correction.status}, ${correction.reason}${correction.replacementClaim ? ` Amended claim: ${correction.replacementClaim}` : ""}`,
|
|
87
|
+
]),
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
function designFindingLines(finding, runRoot) {
|
|
91
|
+
const captures = captureFiles(finding.evidence, runRoot);
|
|
92
|
+
return [
|
|
93
|
+
`${finding.id} ${finding.severity}: ${finding.headline}`,
|
|
94
|
+
` screen: ${finding.screen}`,
|
|
95
|
+
` notice: ${finding.notice}`,
|
|
96
|
+
` why it matters: ${finding.whyItMatters}`,
|
|
97
|
+
` suggestion: ${finding.suggestion}`,
|
|
98
|
+
` seen by: ${finding.seenBy.map((p) => p.label).join(", ")} · confidence: ${finding.confidence}`,
|
|
99
|
+
...(captures.length === 0 ? [] : [` captures: ${captures.join(", ")}`]),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
/** Nothing for an analysis written before design findings. */
|
|
103
|
+
function designSection(view) {
|
|
104
|
+
const design = view.designFindings;
|
|
105
|
+
if (design === null)
|
|
106
|
+
return [];
|
|
107
|
+
if (design.length === 0)
|
|
108
|
+
return ["", "design findings: none in the reviewed captures"];
|
|
109
|
+
return [
|
|
110
|
+
"",
|
|
111
|
+
`design findings: ${design.length}, most severe first`,
|
|
112
|
+
...design.flatMap((finding, index) => [
|
|
113
|
+
...(index === 0 ? [] : [""]),
|
|
114
|
+
...designFindingLines(finding, view.runPath ?? ""),
|
|
115
|
+
]),
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
/** The full findings block: the analysis, every finding with its evidence, and its limitations. */
|
|
119
|
+
export function formatFindings(view) {
|
|
120
|
+
if (view.state !== "ready" && view.state !== "stale")
|
|
121
|
+
return [`findings: none. ${view.message}`, ...(view.next ? [`next: ${view.next}`] : [])];
|
|
122
|
+
const model = [view.provider, view.model].filter(Boolean).join(" ");
|
|
123
|
+
const cost = view.estimatedCostUsd === null ? "" : `, estimated $${view.estimatedCostUsd.toFixed(2)}`;
|
|
124
|
+
return [
|
|
125
|
+
`findings: ${view.findings.length} from analysis ${view.analysisId} (${view.status}, ${model}${cost})`,
|
|
126
|
+
...(view.state === "stale" ? [`warning: ${view.message}`, `next: ${view.next}`] : []),
|
|
127
|
+
...(view.summary ? [view.summary] : []),
|
|
128
|
+
...view.findings.flatMap((finding) => ["", ...findingLines(finding, view.runPath ?? "")]),
|
|
129
|
+
...designSection(view),
|
|
130
|
+
...(view.limitations.length === 0
|
|
131
|
+
? []
|
|
132
|
+
: ["", "limitations:", ...view.limitations.map((limitation) => `- ${limitation}`)]),
|
|
133
|
+
...view.warnings.map((warning) => `warning: ${warning}`),
|
|
134
|
+
"",
|
|
135
|
+
`analysis: ${view.path}`,
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
/** At most `limit` findings, one line each, and the command that prints all of them. */
|
|
139
|
+
export function formatFindingsSummary(view, fullCommand, limit = 3) {
|
|
140
|
+
if (view.state !== "ready" && view.state !== "stale")
|
|
141
|
+
return [`findings: ${view.message}`, ...(view.next ? [`next: ${view.next}`] : [])];
|
|
142
|
+
const shown = view.findings.slice(0, limit);
|
|
143
|
+
const design = view.designFindings;
|
|
144
|
+
return [
|
|
145
|
+
`findings: ${view.findings.length === 0 ? "none" : view.findings.length}${view.state === "stale" ? " (stale)" : ""}`,
|
|
146
|
+
...shown.map((finding) => {
|
|
147
|
+
const qualities = `${IMPACT_TEXT[finding.impact]}, ${finding.confidence} confidence, ${RECOVERY_TEXT[finding.recovery]}`;
|
|
148
|
+
const lead = findingLead(finding, finding.correction);
|
|
149
|
+
if (lead === null)
|
|
150
|
+
return `- ${finding.id} ${qualities}: ${finding.title}`;
|
|
151
|
+
return `- ${finding.id} ${lead.headline} (${lead.corrected ? "corrected in human review" : qualities})`;
|
|
152
|
+
}),
|
|
153
|
+
...(view.findings.length > limit
|
|
154
|
+
? [`- and ${plural(view.findings.length - limit, "more finding")}`]
|
|
155
|
+
: []),
|
|
156
|
+
...(design === null
|
|
157
|
+
? []
|
|
158
|
+
: [
|
|
159
|
+
`design findings: ${design.length === 0 ? "none" : design.length}`,
|
|
160
|
+
...design
|
|
161
|
+
.slice(0, limit)
|
|
162
|
+
.map((finding) => `- ${finding.id} ${finding.severity}: ${finding.headline}`),
|
|
163
|
+
...(design.length > limit
|
|
164
|
+
? [`- and ${plural(design.length - limit, "more design finding")}`]
|
|
165
|
+
: []),
|
|
166
|
+
]),
|
|
167
|
+
`all findings: ${fullCommand}`,
|
|
168
|
+
];
|
|
169
|
+
}
|
package/dist/cli/findings.d.ts
CHANGED
|
@@ -137,10 +137,6 @@ export declare function readRunAnalysis(cwd: string, run: string, analysisId?: s
|
|
|
137
137
|
export declare function missingRunFindings(cwd: string, run: string): AnalysisFindings;
|
|
138
138
|
/** The findings of a run's selected analysis, or why it has none. */
|
|
139
139
|
export declare function readRunFindings(cwd: string, run: string, analysisId?: string): Promise<AnalysisFindings>;
|
|
140
|
-
/** The full findings block: the analysis, every finding with its evidence, and its limitations. */
|
|
141
|
-
export declare function formatFindings(view: AnalysisFindings): string[];
|
|
142
|
-
/** At most `limit` findings, one line each, and the command that prints all of them. */
|
|
143
|
-
export declare function formatFindingsSummary(view: AnalysisFindings, fullCommand: string, limit?: number): string[];
|
|
144
140
|
/**
|
|
145
141
|
* The end of a live run's human output, after its `analysis:` line: the findings of the analysis
|
|
146
142
|
* the run just completed. Prints nothing for --json, or when the run's analysis did not complete,
|
package/dist/cli/findings.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// A run's analysis findings for `review`, `analyze show` and the end of a live `run`: the
|
|
2
|
-
// `humanish.analysis-findings.v1` projection
|
|
3
|
-
// this reads the version `analyze show` selects and never
|
|
2
|
+
// `humanish.analysis-findings.v1` projection, read from the run. findings-text.ts prints it. The
|
|
3
|
+
// analysis artifact stays the record; this reads the version `analyze show` selects and never
|
|
4
|
+
// starts an analysis.
|
|
4
5
|
import path from "node:path";
|
|
5
|
-
import {
|
|
6
|
+
import { costRefusal } from "../analysis/admission.js";
|
|
6
7
|
import { DEFAULT_ANALYSIS_MAX_COST_USD } from "../analysis/automatic-config.js";
|
|
8
|
+
import { bySeverity } from "../analysis/finding-lead.js";
|
|
7
9
|
import { loadAnalysis } from "../analysis/load.js";
|
|
8
10
|
import { projectShareCheckedAnalysis } from "../analysis/sharing.js";
|
|
9
11
|
import { loadRunBundlePrepared, resolveRunPath } from "../run/locate.js";
|
|
@@ -14,6 +16,7 @@ import { routeOf } from "../study/routing.js";
|
|
|
14
16
|
import { plural } from "../run/text.js";
|
|
15
17
|
import { shellQuote } from "../substrates/shell.js";
|
|
16
18
|
import { analysisOutcomeText, wantsJson } from "./io.js";
|
|
19
|
+
import { formatFindingsSummary } from "./findings-text.js";
|
|
17
20
|
import { cli } from "./invocation.js";
|
|
18
21
|
export const ANALYSIS_FINDINGS_SCHEMA = "humanish.analysis-findings.v1";
|
|
19
22
|
/** ` --cwd <dir>`, or "" when `cwd` is the current directory. */
|
|
@@ -64,20 +67,13 @@ function evidenceOf(finding, analysis, runRoot) {
|
|
|
64
67
|
...entry,
|
|
65
68
|
}));
|
|
66
69
|
}
|
|
67
|
-
const SEVERITY_RANK = {
|
|
68
|
-
major: 0,
|
|
69
|
-
moderate: 1,
|
|
70
|
-
minor: 2,
|
|
71
|
-
};
|
|
72
70
|
/** Most severe first; equal severity keeps the analysis order. */
|
|
73
71
|
function designFindingViews(analysis, runRoot) {
|
|
74
72
|
const designFindings = analysis.result?.designFindings;
|
|
75
73
|
if (designFindings === undefined)
|
|
76
74
|
return null;
|
|
77
75
|
const labels = new Map(analysis.participants.map((p) => [p.streamId, p.label]));
|
|
78
|
-
return
|
|
79
|
-
.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])
|
|
80
|
-
.map((finding) => ({
|
|
76
|
+
return bySeverity(designFindings).map((finding) => ({
|
|
81
77
|
id: finding.id,
|
|
82
78
|
headline: finding.headline,
|
|
83
79
|
screen: finding.screen,
|
|
@@ -200,8 +196,10 @@ export function analysisFindings(source) {
|
|
|
200
196
|
const outcome = analysisOutcomeText(job);
|
|
201
197
|
return withoutFindings(source, "failed", job.reason, `The automatic analysis ${outcome.startsWith("failed") ? "" : "was "}${outcome}.`, analyzeCommand(source, job.reason === "AUTOMATIC_ANALYSIS_CODEX_UNAVAILABLE" ? "codex" : undefined));
|
|
202
198
|
}
|
|
203
|
-
if (job?.state === "skipped" && job.admission)
|
|
204
|
-
|
|
199
|
+
if (job?.state === "skipped" && job.admission) {
|
|
200
|
+
const refusal = costRefusal(job.admission, `--run ${source.runId}${source.cwdFlag}`, cli);
|
|
201
|
+
return withoutFindings(source, "skipped", job.reason, `The automatic analysis was refused before it started. ${refusal.text}`, refusal.command);
|
|
202
|
+
}
|
|
205
203
|
if (job?.state === "skipped") {
|
|
206
204
|
const refused = job.reason === "AUTOMATIC_ANALYSIS_ADMISSION_REFUSED";
|
|
207
205
|
return withoutFindings(source, "skipped", job.reason, `The automatic analysis was ${analysisOutcomeText(job)}.${refused ? " The dry run says why, and for a cost refusal names the --max-cost that admits it." : ""}`, job.reason === "AUTOMATIC_ANALYSIS_NO_PARTICIPANT_EVIDENCE"
|
|
@@ -291,188 +289,6 @@ export async function readRunFindings(cwd, run, analysisId) {
|
|
|
291
289
|
const source = await readRunAnalysis(cwd, run, analysisId);
|
|
292
290
|
return source ? analysisFindings(source) : missingRunFindings(cwd, run);
|
|
293
291
|
}
|
|
294
|
-
const IMPACT_TEXT = {
|
|
295
|
-
blocked_task: "blocked task",
|
|
296
|
-
friction: "friction",
|
|
297
|
-
recovery: "recovery",
|
|
298
|
-
uncertain: "uncertain",
|
|
299
|
-
};
|
|
300
|
-
const RECOVERY_TEXT = {
|
|
301
|
-
recovered: "recovered",
|
|
302
|
-
not_observed: "no recovery observed",
|
|
303
|
-
unknown: "recovery unknown",
|
|
304
|
-
};
|
|
305
|
-
/** `+1:05` from milliseconds since the first retained capture. */
|
|
306
|
-
function elapsedText(ms) {
|
|
307
|
-
const seconds = Math.floor(ms / 1000);
|
|
308
|
-
const minutes = Math.floor(seconds / 60);
|
|
309
|
-
const hours = Math.floor(minutes / 60);
|
|
310
|
-
const ss = String(seconds % 60).padStart(2, "0");
|
|
311
|
-
return hours > 0
|
|
312
|
-
? `+${hours}:${String(minutes % 60).padStart(2, "0")}:${ss}`
|
|
313
|
-
: `+${minutes}:${ss}`;
|
|
314
|
-
}
|
|
315
|
-
/** One participant's cited moments: each frame once, at its earliest cited time. */
|
|
316
|
-
function momentsText(evidence) {
|
|
317
|
-
const frames = new Map();
|
|
318
|
-
let unframed = 0;
|
|
319
|
-
for (const entry of evidence) {
|
|
320
|
-
if (entry.frame === null) {
|
|
321
|
-
unframed++;
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
const earlier = frames.get(entry.frame);
|
|
325
|
-
if (earlier === undefined ||
|
|
326
|
-
(entry.elapsedMs !== null && (earlier ?? Infinity) > entry.elapsedMs))
|
|
327
|
-
frames.set(entry.frame, entry.elapsedMs);
|
|
328
|
-
}
|
|
329
|
-
const parts = [...frames]
|
|
330
|
-
.sort(([a], [b]) => a - b)
|
|
331
|
-
.map(([frame, ms]) => `frame ${frame}${ms === null ? "" : ` at ${elapsedText(ms)}`}`);
|
|
332
|
-
if (unframed > 0)
|
|
333
|
-
parts.push(`${plural(unframed, "item")} with no frame`);
|
|
334
|
-
return parts.join(", ");
|
|
335
|
-
}
|
|
336
|
-
/** The capture files, relative to the run directory, in frame order. */
|
|
337
|
-
function captureFiles(evidence, runRoot) {
|
|
338
|
-
return [
|
|
339
|
-
...new Set(evidence
|
|
340
|
-
.filter((entry) => entry.capture !== null)
|
|
341
|
-
.sort((a, b) => (a.frame ?? 0) - (b.frame ?? 0))
|
|
342
|
-
.map((entry) => (runRoot ? path.relative(runRoot, entry.capture) : entry.capture))),
|
|
343
|
-
];
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* What a reader sees first: the headline, or the reviewer's claim when a review amended the finding,
|
|
347
|
-
* since the headline and experience describe the claim it replaced. Null before headlines existed.
|
|
348
|
-
*/
|
|
349
|
-
function leadOf(finding) {
|
|
350
|
-
const amended = finding.correction?.status === "amended" ? finding.correction : null;
|
|
351
|
-
if (finding.headline === null)
|
|
352
|
-
return null;
|
|
353
|
-
return amended?.replacementClaim
|
|
354
|
-
? { headline: amended.replacementClaim, corrected: true }
|
|
355
|
-
: { headline: finding.headline, corrected: false };
|
|
356
|
-
}
|
|
357
|
-
function findingLines(finding, runRoot) {
|
|
358
|
-
const cited = [...new Set(finding.evidence.map((entry) => entry.streamId))];
|
|
359
|
-
const labels = new Map(finding.affected.map((p) => [p.streamId, p.label]));
|
|
360
|
-
const affectedIds = finding.affected.map((p) => p.streamId);
|
|
361
|
-
const streams = [...affectedIds, ...cited.filter((id) => !labels.has(id))];
|
|
362
|
-
const captures = captureFiles(finding.evidence, runRoot);
|
|
363
|
-
const correction = finding.correction;
|
|
364
|
-
const lead = leadOf(finding);
|
|
365
|
-
return [
|
|
366
|
-
...(lead === null
|
|
367
|
-
? [`${finding.id} ${finding.title}`]
|
|
368
|
-
: [
|
|
369
|
-
`${finding.id} ${lead.headline}`,
|
|
370
|
-
...(lead.corrected
|
|
371
|
-
? [
|
|
372
|
-
" Corrected in human review. The reviewer's claim replaces the original headline and account.",
|
|
373
|
-
]
|
|
374
|
-
: finding.experience === null
|
|
375
|
-
? []
|
|
376
|
-
: [` ${finding.experience}`]),
|
|
377
|
-
` evidence: ${finding.title}`,
|
|
378
|
-
]),
|
|
379
|
-
` impact: ${IMPACT_TEXT[finding.impact]} · confidence: ${finding.confidence} · recovery: ${RECOVERY_TEXT[finding.recovery]}`,
|
|
380
|
-
` ${finding.summary}`,
|
|
381
|
-
` affected: ${finding.affected.length} of ${plural(finding.exposedCount, "exposed participant")}`,
|
|
382
|
-
...streams.map((streamId) => {
|
|
383
|
-
const moments = momentsText(finding.evidence.filter((entry) => entry.streamId === streamId));
|
|
384
|
-
const label = `${labels.get(streamId) ?? streamId}${labels.has(streamId) ? "" : " (cited, not affected)"}`;
|
|
385
|
-
return ` - ${label}${moments ? `: ${moments}` : ""}`;
|
|
386
|
-
}),
|
|
387
|
-
...(captures.length === 0 ? [] : [` captures: ${captures.join(", ")}`]),
|
|
388
|
-
` next step: ${finding.nextStep}`,
|
|
389
|
-
...(correction === null
|
|
390
|
-
? []
|
|
391
|
-
: [
|
|
392
|
-
` human review: ${correction.status}, ${correction.reason}${correction.replacementClaim ? ` Amended claim: ${correction.replacementClaim}` : ""}`,
|
|
393
|
-
]),
|
|
394
|
-
];
|
|
395
|
-
}
|
|
396
|
-
function designFindingLines(finding, runRoot) {
|
|
397
|
-
const captures = captureFiles(finding.evidence, runRoot);
|
|
398
|
-
return [
|
|
399
|
-
`${finding.id} ${finding.severity}: ${finding.headline}`,
|
|
400
|
-
` screen: ${finding.screen}`,
|
|
401
|
-
` notice: ${finding.notice}`,
|
|
402
|
-
` why it matters: ${finding.whyItMatters}`,
|
|
403
|
-
` suggestion: ${finding.suggestion}`,
|
|
404
|
-
` seen by: ${finding.seenBy.map((p) => p.label).join(", ")} · confidence: ${finding.confidence}`,
|
|
405
|
-
...(captures.length === 0 ? [] : [` captures: ${captures.join(", ")}`]),
|
|
406
|
-
];
|
|
407
|
-
}
|
|
408
|
-
/** Nothing for an analysis written before design findings. */
|
|
409
|
-
function designSection(view) {
|
|
410
|
-
const design = view.designFindings;
|
|
411
|
-
if (design === null)
|
|
412
|
-
return [];
|
|
413
|
-
if (design.length === 0)
|
|
414
|
-
return ["", "design findings: none in the reviewed captures"];
|
|
415
|
-
return [
|
|
416
|
-
"",
|
|
417
|
-
`design findings: ${design.length}, most severe first`,
|
|
418
|
-
...design.flatMap((finding, index) => [
|
|
419
|
-
...(index === 0 ? [] : [""]),
|
|
420
|
-
...designFindingLines(finding, view.runPath ?? ""),
|
|
421
|
-
]),
|
|
422
|
-
];
|
|
423
|
-
}
|
|
424
|
-
/** The full findings block: the analysis, every finding with its evidence, and its limitations. */
|
|
425
|
-
export function formatFindings(view) {
|
|
426
|
-
if (view.state !== "ready" && view.state !== "stale")
|
|
427
|
-
return [`findings: none. ${view.message}`, ...(view.next ? [`next: ${view.next}`] : [])];
|
|
428
|
-
const model = [view.provider, view.model].filter(Boolean).join(" ");
|
|
429
|
-
const cost = view.estimatedCostUsd === null ? "" : `, estimated $${view.estimatedCostUsd.toFixed(2)}`;
|
|
430
|
-
return [
|
|
431
|
-
`findings: ${view.findings.length} from analysis ${view.analysisId} (${view.status}, ${model}${cost})`,
|
|
432
|
-
...(view.state === "stale" ? [`warning: ${view.message}`, `next: ${view.next}`] : []),
|
|
433
|
-
...(view.summary ? [view.summary] : []),
|
|
434
|
-
...view.findings.flatMap((finding) => ["", ...findingLines(finding, view.runPath ?? "")]),
|
|
435
|
-
...designSection(view),
|
|
436
|
-
...(view.limitations.length === 0
|
|
437
|
-
? []
|
|
438
|
-
: ["", "limitations:", ...view.limitations.map((limitation) => `- ${limitation}`)]),
|
|
439
|
-
...view.warnings.map((warning) => `warning: ${warning}`),
|
|
440
|
-
"",
|
|
441
|
-
`analysis: ${view.path}`,
|
|
442
|
-
];
|
|
443
|
-
}
|
|
444
|
-
/** At most `limit` findings, one line each, and the command that prints all of them. */
|
|
445
|
-
export function formatFindingsSummary(view, fullCommand, limit = 3) {
|
|
446
|
-
if (view.state !== "ready" && view.state !== "stale")
|
|
447
|
-
return [`findings: ${view.message}`, ...(view.next ? [`next: ${view.next}`] : [])];
|
|
448
|
-
const shown = view.findings.slice(0, limit);
|
|
449
|
-
const design = view.designFindings;
|
|
450
|
-
return [
|
|
451
|
-
`findings: ${view.findings.length === 0 ? "none" : view.findings.length}${view.state === "stale" ? " (stale)" : ""}`,
|
|
452
|
-
...shown.map((finding) => {
|
|
453
|
-
const qualities = `${IMPACT_TEXT[finding.impact]}, ${finding.confidence} confidence, ${RECOVERY_TEXT[finding.recovery]}`;
|
|
454
|
-
const lead = leadOf(finding);
|
|
455
|
-
if (lead === null)
|
|
456
|
-
return `- ${finding.id} ${qualities}: ${finding.title}`;
|
|
457
|
-
return `- ${finding.id} ${lead.headline} (${lead.corrected ? "corrected in human review" : qualities})`;
|
|
458
|
-
}),
|
|
459
|
-
...(view.findings.length > limit
|
|
460
|
-
? [`- and ${plural(view.findings.length - limit, "more finding")}`]
|
|
461
|
-
: []),
|
|
462
|
-
...(design === null
|
|
463
|
-
? []
|
|
464
|
-
: [
|
|
465
|
-
`design findings: ${design.length === 0 ? "none" : design.length}`,
|
|
466
|
-
...design
|
|
467
|
-
.slice(0, limit)
|
|
468
|
-
.map((finding) => `- ${finding.id} ${finding.severity}: ${finding.headline}`),
|
|
469
|
-
...(design.length > limit
|
|
470
|
-
? [`- and ${plural(design.length - limit, "more design finding")}`]
|
|
471
|
-
: []),
|
|
472
|
-
]),
|
|
473
|
-
`all findings: ${fullCommand}`,
|
|
474
|
-
];
|
|
475
|
-
}
|
|
476
292
|
/**
|
|
477
293
|
* The end of a live run's human output, after its `analysis:` line: the findings of the analysis
|
|
478
294
|
* the run just completed. Prints nothing for --json, or when the run's analysis did not complete,
|
package/dist/cli/io.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { automaticAnalysisSucceeded, } from "../analysis/automatic-completion.js";
|
|
2
|
-
import {
|
|
2
|
+
import { costRefusal, refusedCost } from "../analysis/admission.js";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
4
|
import { loadEnvFile, recordDotenvNames } from "../keys/env-file.js";
|
|
5
5
|
import { discoverProviderKeys } from "../keys/key-resolution.js";
|
|
@@ -186,11 +186,17 @@ export function writeResult(command, io, result, formatHuman) {
|
|
|
186
186
|
}
|
|
187
187
|
else {
|
|
188
188
|
writeHuman(command, io, formatHuman(output));
|
|
189
|
-
|
|
189
|
+
// A run refused before it was created has no analysis to report, so its output ends with the
|
|
190
|
+
// refusal. Its JSON keeps the skip record.
|
|
191
|
+
if (output !== null &&
|
|
192
|
+
typeof output === "object" &&
|
|
193
|
+
"automaticAnalysis" in output &&
|
|
194
|
+
output.runId !== "not-created") {
|
|
190
195
|
const analysis = output.automaticAnalysis;
|
|
191
196
|
const cost = analysis?.result?.admission && refusedCost(analysis.result.admission);
|
|
192
|
-
|
|
193
|
-
|
|
197
|
+
const refusal = cost && costRefusal(cost, `--run ${analysis.result?.run ?? "latest"}`, cli);
|
|
198
|
+
if (refusal)
|
|
199
|
+
io.writeOut(`analysis: refused before it started. ${refusal.text} ${refusal.command}\n`);
|
|
194
200
|
else if (analysis)
|
|
195
201
|
io.writeOut(`analysis: ${analysisOutcomeText(analysis)}\n`);
|
|
196
202
|
const rejected = analysis?.result?.rejectedOutputPath;
|
|
@@ -306,10 +312,6 @@ export function analysisOutcomeText(analysis) {
|
|
|
306
312
|
return analysis.state;
|
|
307
313
|
return ANALYSIS_REASON_TEXT[analysis.reason] ?? `${analysis.state} (${analysis.reason})`;
|
|
308
314
|
}
|
|
309
|
-
/** What a refused analysis would cost and the command that runs it with a cap that admits it. */
|
|
310
|
-
function refusedAnalysisText(cost, runId) {
|
|
311
|
-
return `${costRefusalText(cost)} To run it, raise the cap: ${cli(`analyze --run ${runId} --max-cost ${admittingMaxCost(cost)}`)}`;
|
|
312
|
-
}
|
|
313
315
|
/** Preserve the run's own result while making requested post-processing failures machine-visible. */
|
|
314
316
|
export function automaticAnalysisEnvelope(result) {
|
|
315
317
|
if (result === null ||
|
package/dist/cli/telemetry.js
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
import { isCuaDiagnosticCategory, isCuaDiagnosticStopCause, } from "../routes/computer-use/diagnostics.js";
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
15
|
-
import { homedir } from "node:os";
|
|
16
15
|
import path from "node:path";
|
|
17
16
|
import { STUDY_RESULT_SCHEMA } from "../run/study-result.js";
|
|
17
|
+
import { envFlag, humanishConfigFile } from "./user-config.js";
|
|
18
18
|
/** Write-only PostHog project key. Public by design: it can ingest, and can read nothing. */
|
|
19
19
|
const INGEST_KEY = "phc_oeMeBqxDZhZ9tCHMSnuDFimLqHpU5Myc847WD33hAh4C";
|
|
20
20
|
const INGEST_HOST = "https://us.i.posthog.com";
|
|
@@ -26,26 +26,16 @@ const STARTER_STUDIES = new Set([
|
|
|
26
26
|
"local-browser",
|
|
27
27
|
"lobby-trivia-3player",
|
|
28
28
|
]);
|
|
29
|
-
export function telemetryStatePath(env = process.env, home
|
|
30
|
-
|
|
31
|
-
// Same XDG rule the key store uses: a relative value must be ignored, or state becomes
|
|
32
|
-
// cwd-relative and follows people between projects.
|
|
33
|
-
const configHome = declared !== undefined && declared !== "" && path.isAbsolute(declared)
|
|
34
|
-
? declared
|
|
35
|
-
: path.join(home, ".config");
|
|
36
|
-
return path.join(configHome, "humanish", "telemetry.json");
|
|
29
|
+
export function telemetryStatePath(env = process.env, home) {
|
|
30
|
+
return humanishConfigFile(env, "telemetry.json", home);
|
|
37
31
|
}
|
|
38
32
|
/**
|
|
39
33
|
* Off means off, from any of the three switches people already expect: the cross-tool
|
|
40
34
|
* `DO_NOT_TRACK` standard, our own env var, and the persisted opt-out.
|
|
41
35
|
*/
|
|
42
36
|
export function disabledByEnvironment(env) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
value.trim() !== "0" &&
|
|
46
|
-
value.trim().toLowerCase() !== "false";
|
|
47
|
-
return (truthy(env.DO_NOT_TRACK) ||
|
|
48
|
-
truthy(env.HUMANISH_TELEMETRY_DISABLED) ||
|
|
37
|
+
return (envFlag(env.DO_NOT_TRACK) ||
|
|
38
|
+
envFlag(env.HUMANISH_TELEMETRY_DISABLED) ||
|
|
49
39
|
// Our own development and test runs must never reach the adoption dataset. In the first two
|
|
50
40
|
// days after telemetry shipped, 82% of events (4,042 of 4,932, from 49 of 59 anonymous ids)
|
|
51
41
|
// came from humanish's own CI and suite: ~50 ids each running nearly every subcommand about
|
|
@@ -55,7 +45,7 @@ export function disabledByEnvironment(env) {
|
|
|
55
45
|
// Deliberately not keyed on CI. An adopter running humanish in their pipeline is real usage
|
|
56
46
|
// and stays countable; the `ci` property already separates it, which is what Next.js does.
|
|
57
47
|
// This keys on being inside the humanish source tree, which only we ever are.
|
|
58
|
-
|
|
48
|
+
envFlag(env.HUMANISH_DEV));
|
|
59
49
|
}
|
|
60
50
|
/**
|
|
61
51
|
* True when this process is running from a humanish source checkout rather than an install.
|
|
@@ -101,7 +91,7 @@ export function inHumanishCheckout(startDir, readFileSyncFn) {
|
|
|
101
91
|
export function isOwnCheckoutRun(cwd, cliDir, readFileSyncFn) {
|
|
102
92
|
return inHumanishCheckout(cwd, readFileSyncFn) || inHumanishCheckout(cliDir, readFileSyncFn);
|
|
103
93
|
}
|
|
104
|
-
export async function readTelemetryState(env = process.env, home
|
|
94
|
+
export async function readTelemetryState(env = process.env, home) {
|
|
105
95
|
try {
|
|
106
96
|
const raw = await readFile(telemetryStatePath(env, home), "utf8");
|
|
107
97
|
const parsed = JSON.parse(raw);
|
|
@@ -115,7 +105,7 @@ export async function readTelemetryState(env = process.env, home = homedir()) {
|
|
|
115
105
|
return { enabled: true, anonymousId: randomUUID(), noticed: false };
|
|
116
106
|
}
|
|
117
107
|
}
|
|
118
|
-
export async function writeTelemetryState(state, env = process.env, home
|
|
108
|
+
export async function writeTelemetryState(state, env = process.env, home) {
|
|
119
109
|
const file = telemetryStatePath(env, home);
|
|
120
110
|
await mkdir(path.dirname(file), { recursive: true });
|
|
121
111
|
await writeFile(file, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
@@ -162,15 +152,13 @@ export function buildPayload(args) {
|
|
|
162
152
|
version: args.version,
|
|
163
153
|
os: args.platform ?? process.platform,
|
|
164
154
|
node: (args.nodeVersion ?? process.version).split(".")[0].replace("v", ""),
|
|
165
|
-
ci: env.CI
|
|
155
|
+
ci: envFlag(env.CI),
|
|
166
156
|
// A humanish study participant is a real cold install on a machine we do not own, so it emits
|
|
167
157
|
// like any other new adopter and is indistinguishable from one. It is also the exact
|
|
168
158
|
// population we are trying to count, so a busy self-study day reads as an adoption spike.
|
|
169
159
|
// Stamped rather than suppressed, the same shape as `ci`: participant runs stay visible and
|
|
170
160
|
// stay separable, and we keep a genuine measurement of what a cold install does.
|
|
171
|
-
studyParticipant: env.HUMANISH_STUDY_PARTICIPANT
|
|
172
|
-
env.HUMANISH_STUDY_PARTICIPANT !== "" &&
|
|
173
|
-
env.HUMANISH_STUDY_PARTICIPANT !== "0",
|
|
161
|
+
studyParticipant: envFlag(env.HUMANISH_STUDY_PARTICIPANT),
|
|
174
162
|
};
|
|
175
163
|
const given = args.properties ?? {};
|
|
176
164
|
if (given.command !== undefined)
|
package/dist/cli/update-check.js
CHANGED
|
@@ -4,20 +4,12 @@
|
|
|
4
4
|
// the command that started it.
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { homedir } from "node:os";
|
|
8
7
|
import path from "node:path";
|
|
9
8
|
import { fileURLToPath } from "node:url";
|
|
10
9
|
import { disabledByEnvironment } from "./telemetry.js";
|
|
10
|
+
import { envFlag, humanishConfigFile } from "./user-config.js";
|
|
11
11
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
12
12
|
const REGISTRY_DIST_TAGS = "https://registry.npmjs.org/-/package/humanish/dist-tags";
|
|
13
|
-
/** Next to telemetry.json, under the same XDG rule the key store uses. */
|
|
14
|
-
function updateCachePath(env, home = homedir()) {
|
|
15
|
-
const declared = env.XDG_CONFIG_HOME?.trim();
|
|
16
|
-
const configHome = declared !== undefined && declared !== "" && path.isAbsolute(declared)
|
|
17
|
-
? declared
|
|
18
|
-
: path.join(home, ".config");
|
|
19
|
-
return path.join(configHome, "humanish", "update-check.json");
|
|
20
|
-
}
|
|
21
13
|
function readUpdateCache(cachePath) {
|
|
22
14
|
try {
|
|
23
15
|
const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
@@ -65,24 +57,23 @@ function dayPassed(timestamp, now) {
|
|
|
65
57
|
const then = timestamp === undefined ? Number.NaN : Date.parse(timestamp);
|
|
66
58
|
return Number.isNaN(then) || now - then >= DAY_MS;
|
|
67
59
|
}
|
|
68
|
-
const truthy = (value) => value !== undefined &&
|
|
69
|
-
value.trim() !== "" &&
|
|
70
|
-
value.trim() !== "0" &&
|
|
71
|
-
value.trim().toLowerCase() !== "false";
|
|
72
60
|
/**
|
|
73
61
|
* Only a person at a terminal is told, and only when nothing has switched the check off. A pipe,
|
|
74
62
|
* CI and a JSON document have a program reading them. A study participant runs the version the
|
|
75
63
|
* study chose, and a notice would send it to another. The telemetry switches turn this off too:
|
|
76
|
-
* someone who opted out of telemetry expects no unprompted requests.
|
|
64
|
+
* someone who opted out of telemetry expects no unprompted requests. NO_UPDATE_NOTIFIER is the
|
|
65
|
+
* switch the update-notifier package reads, and it is read the same way, so any value, even `0`,
|
|
66
|
+
* turns the check off for someone who set it for the CLIs built on that package.
|
|
77
67
|
*/
|
|
78
68
|
function checkAllowed(context) {
|
|
79
69
|
const env = context.env;
|
|
80
70
|
return (context.terminal &&
|
|
81
71
|
!context.json &&
|
|
82
72
|
!context.ownCheckout &&
|
|
83
|
-
!
|
|
84
|
-
!
|
|
85
|
-
|
|
73
|
+
!envFlag(env.CI) &&
|
|
74
|
+
!envFlag(env.HUMANISH_NO_UPDATE_CHECK) &&
|
|
75
|
+
env.NO_UPDATE_NOTIFIER === undefined &&
|
|
76
|
+
!envFlag(env.HUMANISH_STUDY_PARTICIPANT) &&
|
|
86
77
|
!disabledByEnvironment(env));
|
|
87
78
|
}
|
|
88
79
|
/**
|
|
@@ -94,7 +85,7 @@ export function checkForUpdate(context) {
|
|
|
94
85
|
try {
|
|
95
86
|
if (!checkAllowed(context))
|
|
96
87
|
return undefined;
|
|
97
|
-
const cachePath =
|
|
88
|
+
const cachePath = humanishConfigFile(context.env, "update-check.json", context.home);
|
|
98
89
|
const cache = readUpdateCache(cachePath);
|
|
99
90
|
const stamp = new Date(context.now).toISOString();
|
|
100
91
|
const next = { ...cache };
|
|
@@ -126,7 +117,7 @@ export function checkForUpdate(context) {
|
|
|
126
117
|
* cache only; undefined until a check has succeeded on this machine.
|
|
127
118
|
*/
|
|
128
119
|
export function recordedVersion(installed, env, home) {
|
|
129
|
-
const cache = readUpdateCache(
|
|
120
|
+
const cache = readUpdateCache(humanishConfigFile(env, "update-check.json", home));
|
|
130
121
|
if (cache.latest === undefined)
|
|
131
122
|
return undefined;
|
|
132
123
|
const checked = cache.checkedAt === undefined ? "" : ` (checked ${cache.checkedAt.slice(0, 10)})`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `$XDG_CONFIG_HOME/humanish/<fileName>`, or `<home>/.config/humanish/<fileName>`. The XDG spec
|
|
3
|
+
* says to ignore a relative XDG_CONFIG_HOME. Honoring one would put the file in whichever project
|
|
4
|
+
* is open, and for the key store that writes a secret into the repo.
|
|
5
|
+
*/
|
|
6
|
+
export declare function humanishConfigFile(env: NodeJS.ProcessEnv, fileName: string, home?: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Whether a flag variable such as `CI` or `DO_NOT_TRACK` is on. Unset, blank, `0` and `false` (any
|
|
9
|
+
* case, surrounding spaces ignored) are off, as is-in-ci reads `CI`; any other value is on.
|
|
10
|
+
*/
|
|
11
|
+
export declare function envFlag(value: string | undefined): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Where humanish keeps per-user files (the key store, telemetry state, the update-check cache) and
|
|
2
|
+
// how it reads an on/off environment variable. The key store, telemetry and the update check each
|
|
3
|
+
// had their own copy of both.
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
/**
|
|
7
|
+
* `$XDG_CONFIG_HOME/humanish/<fileName>`, or `<home>/.config/humanish/<fileName>`. The XDG spec
|
|
8
|
+
* says to ignore a relative XDG_CONFIG_HOME. Honoring one would put the file in whichever project
|
|
9
|
+
* is open, and for the key store that writes a secret into the repo.
|
|
10
|
+
*/
|
|
11
|
+
export function humanishConfigFile(env, fileName, home = homedir()) {
|
|
12
|
+
const declared = env.XDG_CONFIG_HOME?.trim();
|
|
13
|
+
const configHome = declared !== undefined && declared !== "" && path.isAbsolute(declared)
|
|
14
|
+
? declared
|
|
15
|
+
: path.join(home, ".config");
|
|
16
|
+
return path.join(configHome, "humanish", fileName);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether a flag variable such as `CI` or `DO_NOT_TRACK` is on. Unset, blank, `0` and `false` (any
|
|
20
|
+
* case, surrounding spaces ignored) are off, as is-in-ci reads `CI`; any other value is on.
|
|
21
|
+
*/
|
|
22
|
+
export function envFlag(value) {
|
|
23
|
+
const trimmed = value?.trim() ?? "";
|
|
24
|
+
return trimmed !== "" && trimmed !== "0" && trimmed.toLowerCase() !== "false";
|
|
25
|
+
}
|