humanish 0.110.0 → 0.110.2

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.
Files changed (178) hide show
  1. package/ARCHITECTURE.md +15 -12
  2. package/CHANGELOG.md +26 -0
  3. package/CONTEXT.md +8 -6
  4. package/README.md +103 -53
  5. package/dist/actors/computer-use/loop/ending.js +4 -4
  6. package/dist/actors/local-agent/claude-participant.d.ts +74 -0
  7. package/dist/actors/local-agent/claude-participant.js +243 -0
  8. package/dist/actors/local-agent/claude-session.d.ts +16 -3
  9. package/dist/actors/local-agent/claude-session.js +64 -23
  10. package/dist/actors/local-agent/cli.d.ts +17 -0
  11. package/dist/actors/local-agent/cli.js +111 -24
  12. package/dist/actors/local-agent/readiness.d.ts +2 -2
  13. package/dist/actors/local-agent/readiness.js +20 -12
  14. package/dist/cli/commands/analyze.js +21 -5
  15. package/dist/cli/commands/observe.js +15 -2
  16. package/dist/cli/commands/run-signals.d.ts +2 -0
  17. package/dist/cli/commands/run-signals.js +106 -45
  18. package/dist/cli/commands/runs.d.ts +5 -0
  19. package/dist/cli/commands/runs.js +113 -74
  20. package/dist/cli/commands/setup.js +23 -14
  21. package/dist/cli/commands/study-route-computer-use.js +9 -3
  22. package/dist/cli/commands/study-route-open.js +1 -0
  23. package/dist/cli/commands/study-route-preview.js +1 -0
  24. package/dist/cli/commands/study-route-scripted.js +2 -0
  25. package/dist/cli/commands/study-route-shared-world.js +6 -2
  26. package/dist/cli/commands/study-route-terminal.js +2 -0
  27. package/dist/cli/commands/watch.js +14 -4
  28. package/dist/cli/doctor.d.ts +4 -1
  29. package/dist/cli/doctor.js +51 -10
  30. package/dist/cli/findings.d.ts +103 -0
  31. package/dist/cli/findings.js +317 -0
  32. package/dist/cli/first-run-path.d.ts +32 -11
  33. package/dist/cli/first-run-path.js +113 -69
  34. package/dist/cli/invocation.d.ts +7 -0
  35. package/dist/cli/invocation.js +32 -0
  36. package/dist/cli/io.d.ts +3 -1
  37. package/dist/cli/io.js +8 -15
  38. package/dist/cli/observer-follow.d.ts +20 -0
  39. package/dist/cli/observer-follow.js +36 -1
  40. package/dist/cli/orientation.js +16 -14
  41. package/dist/cli/product-sentence.d.ts +3 -3
  42. package/dist/cli/product-sentence.js +3 -3
  43. package/dist/cli/version.d.ts +1 -0
  44. package/dist/cli/version.js +11 -0
  45. package/dist/evidence/encoded-text.d.ts +9 -6
  46. package/dist/evidence/encoded-text.js +84 -19
  47. package/dist/evidence/redaction.d.ts +2 -0
  48. package/dist/evidence/redaction.js +123 -21
  49. package/dist/feedback/export-bundle.js +2 -2
  50. package/dist/keys/env-file.d.ts +4 -0
  51. package/dist/keys/env-file.js +17 -0
  52. package/dist/keys/key-resolution.js +6 -5
  53. package/dist/observer/data.d.ts +14 -1
  54. package/dist/observer/data.js +19 -9
  55. package/dist/observer/exposure.js +1 -1
  56. package/dist/observer/library.d.ts +4 -0
  57. package/dist/observer/library.js +21 -17
  58. package/dist/observer/render.js +10 -3
  59. package/dist/observer/run-routes.d.ts +2 -0
  60. package/dist/observer/run-routes.js +51 -35
  61. package/dist/observer/serve.js +2 -1
  62. package/dist/observer-app.html +3 -3
  63. package/dist/routes/computer-use/bundle.js +3 -1
  64. package/dist/routes/computer-use/fanout-records.js +1 -1
  65. package/dist/routes/computer-use/participant-execution.js +3 -1
  66. package/dist/routes/computer-use/participant-model.js +5 -2
  67. package/dist/routes/computer-use/plan.js +9 -4
  68. package/dist/routes/computer-use/preflight.js +1 -1
  69. package/dist/routes/computer-use/result.js +17 -28
  70. package/dist/routes/computer-use/single-bundle.js +7 -1
  71. package/dist/routes/scripted/bundle.js +4 -2
  72. package/dist/routes/scripted/plan.js +2 -2
  73. package/dist/routes/scripted/result.d.ts +3 -5
  74. package/dist/routes/scripted/result.js +16 -22
  75. package/dist/routes/scripted/scenario.js +4 -4
  76. package/dist/routes/shared-world/bundle-records.js +6 -1
  77. package/dist/routes/shared-world/bundle.js +2 -0
  78. package/dist/routes/shared-world/plan.js +6 -2
  79. package/dist/routes/shared-world/result.js +22 -22
  80. package/dist/routes/terminal/bundle.js +6 -3
  81. package/dist/routes/terminal/credentials.d.ts +5 -5
  82. package/dist/routes/terminal/credentials.js +14 -6
  83. package/dist/routes/terminal/dry-run.js +8 -12
  84. package/dist/routes/terminal/ledger.d.ts +3 -3
  85. package/dist/routes/terminal/ledger.js +6 -6
  86. package/dist/routes/terminal/live-finish.js +15 -13
  87. package/dist/routes/terminal/plan.js +12 -4
  88. package/dist/routes/terminal/result.d.ts +6 -7
  89. package/dist/routes/terminal/result.js +6 -22
  90. package/dist/routes/terminal/route.js +5 -4
  91. package/dist/routes/terminal/runtime-auth.d.ts +6 -0
  92. package/dist/routes/terminal/runtime-auth.js +5 -0
  93. package/dist/routes/terminal/session.js +1 -1
  94. package/dist/run/bundle-shape.d.ts +3 -1
  95. package/dist/run/bundle-shape.js +25 -1
  96. package/dist/run/bundle.d.ts +24 -0
  97. package/dist/run/display.d.ts +59 -0
  98. package/dist/run/display.js +106 -0
  99. package/dist/run/dry-run.js +13 -8
  100. package/dist/run/judge.d.ts +3 -2
  101. package/dist/run/judge.js +12 -1
  102. package/dist/run/local-only-files.d.ts +13 -0
  103. package/dist/run/local-only-files.js +35 -0
  104. package/dist/run/pricing.js +1 -1
  105. package/dist/run/projection.d.ts +0 -2
  106. package/dist/run/projection.js +0 -11
  107. package/dist/run/reclaim-outcomes.d.ts +89 -0
  108. package/dist/run/reclaim-outcomes.js +213 -0
  109. package/dist/run/reclaim.d.ts +23 -17
  110. package/dist/run/reclaim.js +213 -114
  111. package/dist/run/results.d.ts +10 -20
  112. package/dist/run/results.js +2 -2
  113. package/dist/run/run-index.d.ts +11 -1
  114. package/dist/run/run-index.js +11 -0
  115. package/dist/run/run-not-found.js +3 -2
  116. package/dist/run/run.d.ts +34 -18
  117. package/dist/run/run.js +166 -67
  118. package/dist/run/sandbox-creates.d.ts +59 -0
  119. package/dist/run/sandbox-creates.js +140 -0
  120. package/dist/run/sandbox-receipts.d.ts +20 -5
  121. package/dist/run/sandbox-receipts.js +56 -7
  122. package/dist/run/stats.d.ts +5 -0
  123. package/dist/run/stats.js +8 -4
  124. package/dist/run/status.d.ts +6 -9
  125. package/dist/run/status.js +7 -23
  126. package/dist/run/stored-runs.d.ts +5 -14
  127. package/dist/run/stored-runs.js +31 -146
  128. package/dist/run/streams.d.ts +3 -2
  129. package/dist/study/agents-section.d.ts +21 -0
  130. package/dist/study/agents-section.js +86 -0
  131. package/dist/study/doctor.d.ts +1 -1
  132. package/dist/study/doctor.js +8 -4
  133. package/dist/study/init-templates.js +0 -95
  134. package/dist/study/init.js +45 -37
  135. package/dist/study/keys.d.ts +12 -0
  136. package/dist/study/keys.js +32 -0
  137. package/dist/study/local-browser-host.d.ts +11 -0
  138. package/dist/study/local-browser-host.js +42 -0
  139. package/dist/study/parse/actors.js +9 -1
  140. package/dist/study/parse/subject.js +12 -0
  141. package/dist/study/parse/url-credentials.d.ts +9 -0
  142. package/dist/study/parse/url-credentials.js +50 -0
  143. package/dist/study/persona.d.ts +4 -0
  144. package/dist/study/persona.js +4 -2
  145. package/dist/study/preflight-probes.js +2 -1
  146. package/dist/study/types.d.ts +8 -6
  147. package/dist/study/url-credentials.d.ts +6 -0
  148. package/dist/study/url-credentials.js +34 -0
  149. package/dist/substrates/e2b/peer-install.d.ts +51 -0
  150. package/dist/substrates/e2b/peer-install.js +161 -0
  151. package/dist/substrates/e2b/sandbox.d.ts +34 -6
  152. package/dist/substrates/e2b/sandbox.js +141 -30
  153. package/dist/substrates/e2b/sdk.d.ts +33 -0
  154. package/dist/substrates/e2b/sdk.js +21 -21
  155. package/dist/substrates/local/firecracker-desktop.js +3 -2
  156. package/dist/substrates/local/runtime-host.js +5 -4
  157. package/dist/substrates/local/runtime.js +5 -4
  158. package/dist/substrates/shell.d.ts +2 -0
  159. package/dist/substrates/shell.js +4 -0
  160. package/dist/tui/contract.js +5 -4
  161. package/dist/tui-app.js +133 -133
  162. package/dist/verify/liveness.d.ts +19 -3
  163. package/dist/verify/liveness.js +47 -25
  164. package/dist/verify/verify.d.ts +3 -0
  165. package/dist/verify/verify.js +8 -3
  166. package/docs/architecture/actor-contract.md +2 -1
  167. package/docs/architecture/observer.md +22 -7
  168. package/docs/architecture/task-protocol-support.md +13 -13
  169. package/docs/architecture/terminal-product-route.md +15 -13
  170. package/docs/contracts/run-bundle.md +95 -10
  171. package/docs/contracts/schemas.md +44 -26
  172. package/docs/contracts/study-analysis.md +45 -0
  173. package/docs/contracts/study-costs.md +8 -0
  174. package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
  175. package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
  176. package/docs/principles/invariants-and-defaults.md +11 -8
  177. package/package.json +6 -3
  178. package/skills/humanish/SKILL.md +38 -20
@@ -1,14 +1,16 @@
1
- const STATUS_TONES = {
2
- passed: "#3fb970",
3
- running: "#4f8ff7",
4
- failed: "#e5534b",
5
- blocked: "#d29922",
6
- timed_out: "#d29922",
1
+ import { cli } from "../cli/invocation.js";
2
+ // The pip color for each RunDisplay tone; a run with no display gets the neutral gray.
3
+ const DISPLAY_TONES = {
4
+ pass: "#3fb970",
5
+ live: "#4f8ff7",
6
+ fail: "#e5534b",
7
+ warn: "#d29922",
8
+ neutral: "#57606f",
7
9
  };
8
10
  export function renderLibraryHtml(history, opts) {
9
11
  const emptyState = opts.safe
10
- ? "No share_ready runs yet. Run `humanish verify` to see why."
11
- : "No runs yet. Run `humanish watch` to create one.";
12
+ ? `No share_ready runs yet. Run \`${cli("verify")}\` to see why.`
13
+ : `No runs yet. Run \`${cli("run first-run")}\` to create one.`;
12
14
  return `<!doctype html>
13
15
  <html lang="en" data-theme="dark">
14
16
  <head>
@@ -83,7 +85,7 @@ main { padding: 1rem 1.1rem 3rem; max-width: 44rem; margin: 0 auto; }
83
85
  }
84
86
  // prose-check: script (the library page's JavaScript)
85
87
  function libraryClientJs() {
86
- const tones = JSON.stringify(STATUS_TONES);
88
+ const tones = JSON.stringify(DISPLAY_TONES);
87
89
  return `
88
90
  (function () {
89
91
  var TONES = ${tones};
@@ -103,9 +105,11 @@ function libraryClientJs() {
103
105
  link.href = run.href;
104
106
  var top = document.createElement("div");
105
107
  top.className = "run-top";
108
+ var display = run.display || null;
109
+ var live = display !== null && display.state === "running";
106
110
  var pip = document.createElement("span");
107
- pip.className = run.status === "running" ? "pip running" : "pip";
108
- pip.style.background = TONES[run.status] || "#57606f";
111
+ pip.className = live ? "pip running" : "pip";
112
+ pip.style.background = (display && TONES[display.tone]) || TONES.neutral;
109
113
  var id = document.createElement("span");
110
114
  id.className = "run-id";
111
115
  id.textContent = run.runId;
@@ -116,15 +120,15 @@ function libraryClientJs() {
116
120
  latest.className = "badge latest";
117
121
  latest.textContent = "latest";
118
122
  top.appendChild(latest);
119
- } else if (run.status === "running") {
120
- var live = document.createElement("span");
121
- live.className = "badge";
122
- live.textContent = "live";
123
- top.appendChild(live);
123
+ } else if (live) {
124
+ var badge = document.createElement("span");
125
+ badge.className = "badge";
126
+ badge.textContent = "live";
127
+ top.appendChild(badge);
124
128
  }
125
129
  var meta = document.createElement("div");
126
130
  meta.className = "run-meta";
127
- var metaBits = [run.mode || "unknown", run.streamCount + " participants", run.createdAt || ""];
131
+ var metaBits = [display ? display.label : "", run.mode || "unknown", run.streamCount + " participants", run.createdAt || ""];
128
132
  // Labeled cost token: ALWAYS "~$X est." (never a bare "$X"), so the library never implies an
129
133
  // authoritative charge. Null = omitted (advisory, fail-open on display).
130
134
  if (run.costLabel) {
@@ -11,7 +11,8 @@ import { projectShareCheckedAnalysis } from "../analysis/sharing.js";
11
11
  import { loadAnalysis } from "../analysis/load.js";
12
12
  import { physicalCwdOf, runIdOf, bindExistingRunArtifactPaths, isSafeRunIdSegment, resolveLatestRunDirectory, validatePreparedRunArtifactPaths, } from "../run/paths.js";
13
13
  import { writeContainedOutputFile } from "../run/contained-output.js";
14
- import { loadRunBundlePrepared } from "../run/locate.js";
14
+ import { loadRunBundlePrepared, readRunJsonIfExists } from "../run/locate.js";
15
+ import { RUN_STATUS_FILE } from "../run/status.js";
15
16
  import { verifyRunPrepared } from "../verify/verify.js";
16
17
  import { renderObserverHtml } from "./artifact.js";
17
18
  import { buildObserverData } from "./data.js";
@@ -19,6 +20,7 @@ import { buildServeSecurityHeaders, hostAllowed, parsePublicOrigin } from "./htt
19
20
  import { listenOnLoopback } from "./listen.js";
20
21
  import { pinDirectChildDirectory, pinDirectory, readContainedFile, } from "./pinned-files.js";
21
22
  import { buildHistoryIndex, matchRunRoute, serveRunPath, writeResponse, } from "./run-routes.js";
23
+ import { cli } from "../cli/invocation.js";
22
24
  const OBSERVER_SCHEMA = "humanish.observer-result.v1";
23
25
  const observerRuntimeStreamUrls = new WeakMap();
24
26
  const observerPreparedRunPaths = new WeakMap();
@@ -99,7 +101,12 @@ export async function renderObserver(cwdInput, runInput, options = {}) {
99
101
  ...projectShareCheckedAnalysis(await loadAnalysis(preparedRunPaths)),
100
102
  ...(spend === null ? {} : { spend }),
101
103
  };
102
- const observerData = buildObserverData(loaded.bundle);
104
+ // A run.json without an outcome is in progress, interrupted, or older than the field: its
105
+ // status.json says which, and holds an older run's ok.
106
+ const statusRecord = loaded.bundle.outcome === undefined
107
+ ? await readRunJsonIfExists(preparedRunPaths, RUN_STATUS_FILE)
108
+ : undefined;
109
+ const observerData = buildObserverData(loaded.bundle, undefined, statusRecord);
103
110
  observerData.publicSafety.share = {
104
111
  status: verified.shareSafety.status,
105
112
  verifiedAt: new Date().toISOString(),
@@ -130,7 +137,7 @@ export async function renderObserver(cwdInput, runInput, options = {}) {
130
137
  loaded.bundle.mode === "live"
131
138
  ? "Observer renders verified local evidence artifacts; runtime stream auth URLs are not persisted."
132
139
  : "This is a dry run: its participants are synthetic and no product behavior was tested.",
133
- "Before filing public feedback, use `humanish feedback issue` so redaction and public-safety checks gate the payload.",
140
+ `Before filing public feedback, use \`${cli("feedback issue")}\` so redaction and public-safety checks gate the payload.`,
134
141
  ...(openResult.warning ? [openResult.warning] : []),
135
142
  ],
136
143
  };
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { RunDisplay } from "../run/display.js";
2
3
  import { type ObserverData } from "./data.js";
3
4
  import { type PinnedDirectory } from "./pinned-files.js";
4
5
  export interface ObserverRuntimeStreamUrl {
@@ -22,6 +23,7 @@ export declare function buildHistoryIndex(proofRoot: PinnedDirectory, admitRun?:
22
23
  mode: string | null;
23
24
  href: string;
24
25
  status: string;
26
+ display?: RunDisplay;
25
27
  runtimeState?: NonNullable<ObserverData["runtime"]>["state"];
26
28
  streamCount: number;
27
29
  estimatedCostUsd: number | null;
@@ -4,11 +4,12 @@
4
4
  import path from "node:path";
5
5
  import { isAnalysisRecordPath, projectShareCheckedAnalysis } from "../analysis/sharing.js";
6
6
  import { loadAnalysis } from "../analysis/load.js";
7
+ import { isLocalOnlyRunFile, isLocalOnlyRunFileIdentity } from "../run/local-only-files.js";
7
8
  import { listRuns } from "../run/stored-runs.js";
8
9
  import { readAnalysisAccounting } from "../run/costs.js";
9
10
  import { bindExistingRunArtifactPaths, isPathInside, isSafeRunIdSegment, } from "../run/paths.js";
10
11
  import { analysisCostOf, runCost, runCostLabel } from "../run/run-cost.js";
11
- import { RUN_STATUS_FILE, RUN_STATUS_STALE_MS, isRunStatusRecord } from "../run/status.js";
12
+ import { RUN_STATUS_FILE, RUN_STATUS_STALE_MS, isRunStatusRecord, } from "../run/status.js";
12
13
  import { renderObserverHtml } from "./artifact.js";
13
14
  import { buildObserverData, recordedStreamEmbed, withObserverEndings, } from "./data.js";
14
15
  import { buildArtifactSecurityHeaders } from "./http.js";
@@ -70,9 +71,11 @@ export async function serveRunPath(runRoot, relativePath, response, runtimeStrea
70
71
  // file serving bypass its current-content checks or a warmed source-only admission cache.
71
72
  const derivedRoot = cleanedRelativePath.split("/")[0];
72
73
  const derivedLeaf = path.posix.basename(cleanedRelativePath);
74
+ // Local-only files (raw sandbox ids, the recording pid) are read in process, never served.
73
75
  if (derivedRoot === ".analysis-lock" ||
74
76
  derivedLeaf.startsWith(".humanish-write-") ||
75
- isAnalysisRecordPath(cleanedRelativePath)) {
77
+ isAnalysisRecordPath(cleanedRelativePath) ||
78
+ isLocalOnlyRunFile(cleanedRelativePath)) {
76
79
  writeResponse(response, 404, "Not found", "text/plain; charset=utf-8");
77
80
  return;
78
81
  }
@@ -103,6 +106,10 @@ export async function serveRunPath(runRoot, relativePath, response, runtimeStrea
103
106
  await serveContainedMedia(runRoot, filePath, response, request);
104
107
  return;
105
108
  }
109
+ if (await isLocalOnlyRunFileIdentity(root, filePath)) {
110
+ writeResponse(response, 404, "Not found", "text/plain; charset=utf-8");
111
+ return;
112
+ }
106
113
  try {
107
114
  const body = await readContainedFile(runRoot, filePath);
108
115
  if (!body) {
@@ -120,6 +127,7 @@ export async function serveRunPath(runRoot, relativePath, response, runtimeStrea
120
127
  }
121
128
  }
122
129
  async function readObserverData(runRoot, runtimeStreamUrls = []) {
130
+ const record = await readLocalRunStatus(runRoot);
123
131
  // Best-effort load from either source. Both reads swallow all errors on
124
132
  // purpose: this runs on every browser poll of a live run, where run.json may
125
133
  // be absent, still being written (a partial-JSON parse error), or superseded
@@ -130,57 +138,64 @@ async function readObserverData(runRoot, runtimeStreamUrls = []) {
130
138
  if (!bundleBytes)
131
139
  throw new Error("run.json unavailable");
132
140
  const bundle = JSON.parse(bundleBytes.toString("utf8"));
133
- return withRuntimeStreamUrls(await withLocalRunStatus(runRoot, buildObserverData(bundle)), runtimeStreamUrls);
141
+ return withRuntimeStreamUrls(withLocalRunStatus(record, buildObserverData(bundle, undefined, record)), runtimeStreamUrls);
134
142
  }
135
143
  catch { }
136
144
  try {
137
145
  const observerBytes = await readContainedFile(runRoot, path.join(runRoot.physicalPath, "observer", "observer-data.json"));
138
146
  if (!observerBytes)
139
147
  throw new Error("observer-data.json unavailable");
140
- return withRuntimeStreamUrls(await withLocalRunStatus(runRoot, withObserverEndings(JSON.parse(observerBytes.toString("utf8")))), runtimeStreamUrls);
148
+ return withRuntimeStreamUrls(withLocalRunStatus(record, withObserverEndings(JSON.parse(observerBytes.toString("utf8")))), runtimeStreamUrls);
141
149
  }
142
150
  catch { }
143
151
  return null;
144
152
  }
145
- /**
146
- * Liveness is a current read of a contained local status record, separate from run evidence.
147
- * A stale heartbeat means unknown: neither an old timestamp nor a persisted PID proves that a
148
- * process died (the evidence may have been copied from another machine). No PID is served/probed.
149
- */
150
- async function withLocalRunStatus(runRoot, input) {
151
- // A served observation must never be inherited from a persisted projection or export.
152
- const { runtime: _persistedRuntime, ...data } = input;
153
+ /** The run directory's own status record, when it is well formed and names that directory. */
154
+ async function readLocalRunStatus(runRoot) {
153
155
  try {
154
156
  const bytes = await readContainedFile(runRoot, path.join(runRoot.physicalPath, RUN_STATUS_FILE));
155
157
  if (!bytes)
156
- return data;
158
+ return undefined;
157
159
  const record = JSON.parse(bytes.toString("utf8"));
158
- if (!isRunStatusRecord(record) ||
159
- record.runId !== data.run.runId ||
160
- record.mode !== data.run.mode ||
161
- record.runId !== path.basename(runRoot.physicalPath))
162
- return data;
163
- const now = Date.now();
164
- const started = Date.parse(record.startedAt);
165
- const updated = Date.parse(record.updatedAt);
166
- const timestampsValid = Number.isFinite(started) && Number.isFinite(updated) && started <= updated && updated <= now;
167
- let state = "unknown";
168
- if (timestampsValid) {
169
- if (record.state === "finished") {
170
- state = "finished";
171
- }
172
- else if (record.state === "running" && now - updated <= RUN_STATUS_STALE_MS) {
173
- state = "running";
174
- }
175
- }
176
- return {
177
- ...data,
178
- runtime: { state, observedAt: new Date(now).toISOString(), source: "local-run-status" },
179
- };
160
+ return isRunStatusRecord(record) && record.runId === path.basename(runRoot.physicalPath)
161
+ ? record
162
+ : undefined;
180
163
  }
181
164
  catch {
165
+ return undefined;
166
+ }
167
+ }
168
+ /**
169
+ * Liveness is a current read of a contained local status record, separate from run evidence.
170
+ * A stale heartbeat means unknown: neither an old timestamp nor a persisted PID proves that a
171
+ * process died (the evidence may have been copied from another machine). A record its stopped
172
+ * process wrote is interrupted. No PID is served/probed.
173
+ */
174
+ function withLocalRunStatus(record, input) {
175
+ // A served observation must never be inherited from a persisted projection or export.
176
+ const { runtime: _persistedRuntime, ...data } = input;
177
+ if (record === undefined || record.runId !== data.run.runId || record.mode !== data.run.mode)
182
178
  return data;
179
+ const now = Date.now();
180
+ const started = Date.parse(record.startedAt);
181
+ const updated = Date.parse(record.updatedAt);
182
+ const timestampsValid = Number.isFinite(started) && Number.isFinite(updated) && started <= updated && updated <= now;
183
+ let state = "unknown";
184
+ if (timestampsValid) {
185
+ if (record.state === "finished") {
186
+ state = "finished";
187
+ }
188
+ else if (record.state === "interrupted") {
189
+ state = "interrupted";
190
+ }
191
+ else if (record.state === "running" && now - updated <= RUN_STATUS_STALE_MS) {
192
+ state = "running";
193
+ }
183
194
  }
195
+ return {
196
+ ...data,
197
+ runtime: { state, observedAt: new Date(now).toISOString(), source: "local-run-status" },
198
+ };
184
199
  }
185
200
  /** internal: exported for the stream lifecycle tests. */
186
201
  export function withRuntimeStreamUrls(data, runtimeStreamUrls) {
@@ -249,6 +264,7 @@ admitRun) {
249
264
  mode: admitRun ? (data?.run.mode ?? null) : run.mode,
250
265
  href: `/_humanish/runs/${encodeURIComponent(run.runId)}/observer/index.html`,
251
266
  status: data?.run.status ?? "unknown",
267
+ ...(data?.run.display === undefined ? {} : { display: data.run.display }),
252
268
  ...(data?.runtime ? { runtimeState: data.runtime.state } : {}),
253
269
  streamCount: data?.streams.length ?? 0,
254
270
  // Labeled run-total cost estimate (advisory; null when the run carries no cost summary).
@@ -10,6 +10,7 @@ import { buildServeSecurityHeaders, hostAllowed, parsePublicOrigin, } from "./ht
10
10
  import { isSafeRunIdSegment } from "../run/paths.js";
11
11
  import { listRuns } from "../run/stored-runs.js";
12
12
  import { verifyRun } from "../verify/verify.js";
13
+ import { cli } from "../cli/invocation.js";
13
14
  export const SERVE_SCHEMA = "humanish.serve-result.v1";
14
15
  export function createShareSafetyAdmission(cwd, options = {}) {
15
16
  const verifyImpl = options.verifyImpl ?? verifyRun;
@@ -196,7 +197,7 @@ export async function serveObserverLibrary(cwdInput, options) {
196
197
  ok: false,
197
198
  error: {
198
199
  code: "HUMANISH_RUN_NOT_FOUND",
199
- message: "No run library found under .humanish/runs. Run `humanish watch` to create the first run.",
200
+ message: `No run library found under .humanish/runs. Run \`${cli("run first-run")}\` to create the first run.`,
200
201
  },
201
202
  };
202
203
  }