@alisio/core 0.5.2 → 0.7.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.
Files changed (204) hide show
  1. package/dist/analysis/janitor.d.ts +48 -0
  2. package/dist/analysis/janitor.js +170 -18
  3. package/dist/analysis/jobs.d.ts +11 -2
  4. package/dist/analysis/jobs.js +13 -0
  5. package/dist/analysis/oci.d.ts +11 -2
  6. package/dist/analysis/oci.js +20 -4
  7. package/dist/analysis/python/sources.js +2 -1
  8. package/dist/analysis/rerun.d.ts +1 -1
  9. package/dist/analysis/status.d.ts +3 -1
  10. package/dist/analysis/status.js +3 -0
  11. package/dist/application.d.ts +28 -2
  12. package/dist/application.js +189 -4
  13. package/dist/changelog/data.js +42 -0
  14. package/dist/config.d.ts +110 -2
  15. package/dist/config.js +188 -6
  16. package/dist/core/call-internals.d.ts +22 -0
  17. package/dist/core/call-internals.js +9 -0
  18. package/dist/core/contracts.d.ts +2 -2
  19. package/dist/core/runner.js +111 -38
  20. package/dist/eval/canonical.d.ts +14 -0
  21. package/dist/eval/canonical.js +40 -0
  22. package/dist/eval/cassette.d.ts +180 -0
  23. package/dist/eval/cassette.js +424 -0
  24. package/dist/eval/compare.d.ts +147 -0
  25. package/dist/eval/compare.js +436 -0
  26. package/dist/eval/engine.d.ts +144 -0
  27. package/dist/eval/engine.js +728 -0
  28. package/dist/eval/export.d.ts +40 -0
  29. package/dist/eval/export.js +90 -0
  30. package/dist/eval/fingerprint.d.ts +88 -0
  31. package/dist/eval/fingerprint.js +186 -0
  32. package/dist/eval/normalize.d.ts +68 -0
  33. package/dist/eval/normalize.js +138 -0
  34. package/dist/eval/paths.d.ts +11 -0
  35. package/dist/eval/paths.js +50 -0
  36. package/dist/eval/plan.d.ts +26 -0
  37. package/dist/eval/plan.js +44 -0
  38. package/dist/eval/report.d.ts +53 -0
  39. package/dist/eval/report.js +258 -0
  40. package/dist/eval/run-suite.d.ts +68 -0
  41. package/dist/eval/run-suite.js +94 -0
  42. package/dist/eval/secrets.d.ts +12 -0
  43. package/dist/eval/secrets.js +45 -0
  44. package/dist/eval/stats.d.ts +17 -0
  45. package/dist/eval/stats.js +50 -0
  46. package/dist/eval/suite.d.ts +20 -0
  47. package/dist/eval/suite.js +165 -0
  48. package/dist/eval/variants.d.ts +35 -0
  49. package/dist/eval/variants.js +137 -0
  50. package/dist/eval/verifiers.d.ts +49 -0
  51. package/dist/eval/verifiers.js +264 -0
  52. package/dist/forge/broker.d.ts +51 -0
  53. package/dist/forge/broker.js +244 -0
  54. package/dist/forge/budget.d.ts +27 -0
  55. package/dist/forge/budget.js +60 -0
  56. package/dist/forge/catalog-view.d.ts +24 -0
  57. package/dist/forge/catalog-view.js +43 -0
  58. package/dist/forge/errors.d.ts +26 -0
  59. package/dist/forge/errors.js +49 -0
  60. package/dist/forge/guides/javascript.d.ts +25 -0
  61. package/dist/forge/guides/javascript.js +90 -0
  62. package/dist/forge/guides/provider.d.ts +42 -0
  63. package/dist/forge/guides/provider.js +96 -0
  64. package/dist/forge/guides/python-environment.d.ts +15 -0
  65. package/dist/forge/guides/python-environment.js +81 -0
  66. package/dist/forge/guides/python.d.ts +38 -0
  67. package/dist/forge/guides/python.js +170 -0
  68. package/dist/forge/javascript/engine.d.ts +44 -0
  69. package/dist/forge/javascript/engine.js +229 -0
  70. package/dist/forge/javascript/hosts/factory.d.ts +25 -0
  71. package/dist/forge/javascript/hosts/factory.js +21 -0
  72. package/dist/forge/javascript/hosts/framing.d.ts +17 -0
  73. package/dist/forge/javascript/hosts/framing.js +49 -0
  74. package/dist/forge/javascript/hosts/isolate-main.d.ts +9 -0
  75. package/dist/forge/javascript/hosts/isolate-main.js +63 -0
  76. package/dist/forge/javascript/hosts/process-entry.d.ts +1 -0
  77. package/dist/forge/javascript/hosts/process-entry.js +3 -0
  78. package/dist/forge/javascript/hosts/process-host.d.ts +33 -0
  79. package/dist/forge/javascript/hosts/process-host.js +160 -0
  80. package/dist/forge/javascript/hosts/worker-entry.d.ts +1 -0
  81. package/dist/forge/javascript/hosts/worker-entry.js +3 -0
  82. package/dist/forge/javascript/hosts/worker-host.d.ts +27 -0
  83. package/dist/forge/javascript/hosts/worker-host.js +116 -0
  84. package/dist/forge/javascript/prelude.d.ts +2 -0
  85. package/dist/forge/javascript/prelude.js +333 -0
  86. package/dist/forge/javascript/probe.d.ts +2 -0
  87. package/dist/forge/javascript/probe.js +53 -0
  88. package/dist/forge/javascript/runtime-manifest.d.ts +14 -0
  89. package/dist/forge/javascript/runtime-manifest.js +14 -0
  90. package/dist/forge/javascript/vm-runner.d.ts +21 -0
  91. package/dist/forge/javascript/vm-runner.js +301 -0
  92. package/dist/forge/javascript/wasm-asset.d.ts +8 -0
  93. package/dist/forge/javascript/wasm-asset.js +8 -0
  94. package/dist/forge/javascript/wasm.d.ts +15 -0
  95. package/dist/forge/javascript/wasm.js +54 -0
  96. package/dist/forge/journal.d.ts +105 -0
  97. package/dist/forge/journal.js +174 -0
  98. package/dist/forge/kernel.d.ts +76 -0
  99. package/dist/forge/kernel.js +339 -0
  100. package/dist/forge/limits.d.ts +38 -0
  101. package/dist/forge/limits.js +73 -0
  102. package/dist/forge/nested.d.ts +96 -0
  103. package/dist/forge/nested.js +214 -0
  104. package/dist/forge/ports.d.ts +120 -0
  105. package/dist/forge/ports.js +1 -0
  106. package/dist/forge/progress.d.ts +24 -0
  107. package/dist/forge/progress.js +76 -0
  108. package/dist/forge/protocol.d.ts +110 -0
  109. package/dist/forge/protocol.js +171 -0
  110. package/dist/forge/python/backend-selector.d.ts +38 -0
  111. package/dist/forge/python/backend-selector.js +111 -0
  112. package/dist/forge/python/backend.d.ts +89 -0
  113. package/dist/forge/python/backend.js +1 -0
  114. package/dist/forge/python/engine.d.ts +84 -0
  115. package/dist/forge/python/engine.js +279 -0
  116. package/dist/forge/python/frame-parser.d.ts +24 -0
  117. package/dist/forge/python/frame-parser.js +107 -0
  118. package/dist/forge/python/frame-transport.d.ts +40 -0
  119. package/dist/forge/python/frame-transport.js +101 -0
  120. package/dist/forge/python/import-scan.d.ts +22 -0
  121. package/dist/forge/python/import-scan.js +133 -0
  122. package/dist/forge/python/managed-backend.d.ts +30 -0
  123. package/dist/forge/python/managed-backend.js +111 -0
  124. package/dist/forge/python/oci-backend.d.ts +27 -0
  125. package/dist/forge/python/oci-backend.js +84 -0
  126. package/dist/forge/python/pyodide-backend.d.ts +40 -0
  127. package/dist/forge/python/pyodide-backend.js +289 -0
  128. package/dist/forge/python/pyodide-install.d.ts +41 -0
  129. package/dist/forge/python/pyodide-install.js +167 -0
  130. package/dist/forge/python/pyodide-manifest.d.ts +42 -0
  131. package/dist/forge/python/pyodide-manifest.js +376 -0
  132. package/dist/forge/python/pyodide-runner-source.d.ts +2 -0
  133. package/dist/forge/python/pyodide-runner-source.js +3 -0
  134. package/dist/forge/python/rss.d.ts +2 -0
  135. package/dist/forge/python/rss.js +33 -0
  136. package/dist/forge/python/scratch-copy.d.ts +6 -0
  137. package/dist/forge/python/scratch-copy.js +61 -0
  138. package/dist/forge/python/settle.d.ts +6 -0
  139. package/dist/forge/python/settle.js +21 -0
  140. package/dist/forge/python/status.d.ts +8 -0
  141. package/dist/forge/python/status.js +15 -0
  142. package/dist/forge/python/wasm-node.d.ts +59 -0
  143. package/dist/forge/python/wasm-node.js +220 -0
  144. package/dist/forge/python/wiring.d.ts +39 -0
  145. package/dist/forge/python/wiring.js +47 -0
  146. package/dist/forge/registry.d.ts +26 -0
  147. package/dist/forge/registry.js +83 -0
  148. package/dist/forge/result.d.ts +58 -0
  149. package/dist/forge/result.js +86 -0
  150. package/dist/forge/routing-rule.d.ts +19 -0
  151. package/dist/forge/routing-rule.js +31 -0
  152. package/dist/forge/runtime.d.ts +64 -0
  153. package/dist/forge/runtime.js +88 -0
  154. package/dist/forge/scratch-files.d.ts +36 -0
  155. package/dist/forge/scratch-files.js +94 -0
  156. package/dist/forge/scratch-provider.d.ts +41 -0
  157. package/dist/forge/scratch-provider.js +79 -0
  158. package/dist/forge/scratch-quota.d.ts +8 -0
  159. package/dist/forge/scratch-quota.js +25 -0
  160. package/dist/forge/selector.d.ts +35 -0
  161. package/dist/forge/selector.js +51 -0
  162. package/dist/forge/session-dir.d.ts +59 -0
  163. package/dist/forge/session-dir.js +239 -0
  164. package/dist/forge/session-path.d.ts +31 -0
  165. package/dist/forge/session-path.js +61 -0
  166. package/dist/forge/slots.d.ts +16 -0
  167. package/dist/forge/slots.js +47 -0
  168. package/dist/forge/state-delta.d.ts +54 -0
  169. package/dist/forge/state-delta.js +99 -0
  170. package/dist/forge/state-machine.d.ts +20 -0
  171. package/dist/forge/state-machine.js +40 -0
  172. package/dist/forge/state-store.d.ts +25 -0
  173. package/dist/forge/state-store.js +50 -0
  174. package/dist/forge/task-log.d.ts +9 -0
  175. package/dist/forge/task-log.js +42 -0
  176. package/dist/forge/verdict.d.ts +20 -0
  177. package/dist/forge/verdict.js +28 -0
  178. package/dist/forge-isolate.d.ts +6 -0
  179. package/dist/forge-isolate.js +6 -0
  180. package/dist/index.d.ts +23 -1
  181. package/dist/index.js +23 -1
  182. package/dist/permissions/decide.d.ts +42 -0
  183. package/dist/permissions/decide.js +24 -0
  184. package/dist/plan/exit-plan.js +2 -1
  185. package/dist/resources/context.d.ts +2 -1
  186. package/dist/resources/context.js +2 -1
  187. package/dist/runtime/access.d.ts +34 -0
  188. package/dist/runtime/access.js +80 -0
  189. package/dist/runtime/ids.d.ts +21 -0
  190. package/dist/runtime/ids.js +41 -1
  191. package/dist/runtime/process.d.ts +2 -1
  192. package/dist/runtime/process.js +5 -5
  193. package/dist/runtime/store.d.ts +11 -0
  194. package/dist/runtime/store.js +106 -2
  195. package/dist/tools/analysis.d.ts +25 -2
  196. package/dist/tools/analysis.js +315 -267
  197. package/dist/tools/execute.js +6 -7
  198. package/dist/tools/forge-guide.d.ts +6 -0
  199. package/dist/tools/forge-guide.js +60 -0
  200. package/dist/tools/js-run.d.ts +19 -0
  201. package/dist/tools/js-run.js +126 -0
  202. package/dist/tools/standard.d.ts +7 -1
  203. package/dist/tools/standard.js +44 -43
  204. package/package.json +7 -2
@@ -0,0 +1,50 @@
1
+ import { DEFAULT_STATE_TOTAL_BYTES, planCommit, } from "./state-delta.js";
2
+ export class SqliteStateStore {
3
+ #db;
4
+ #now;
5
+ #maxTotalBytes;
6
+ constructor(db, options = {}) {
7
+ this.#db = db;
8
+ this.#now = options.now ?? Date.now;
9
+ const max = options.maxTotalBytes ?? DEFAULT_STATE_TOTAL_BYTES;
10
+ this.#maxTotalBytes = typeof max === "function" ? max : () => max;
11
+ }
12
+ /** Live values and every revision (tombstones included) of a root session. */
13
+ snapshot(rootSessionId) {
14
+ const snapshot = { values: {}, revisions: {} };
15
+ for (const [key, entry] of this.#read(rootSessionId)) {
16
+ snapshot.revisions[key] = entry.revision;
17
+ if (entry.valueJson !== "")
18
+ snapshot.values[key] = JSON.parse(entry.valueJson);
19
+ }
20
+ return snapshot;
21
+ }
22
+ /**
23
+ * Applies a delta against the snapshot it was built from. Throws `STATE_CONFLICT` or
24
+ * `BUDGET_EXCEEDED` without writing anything. Returns the number of rows written.
25
+ */
26
+ commit(rootSessionId, snapshot, ops) {
27
+ return this.#db.transaction(() => {
28
+ const plan = planCommit({
29
+ current: this.#read(rootSessionId),
30
+ snapshot,
31
+ ops,
32
+ maxTotalBytes: this.#maxTotalBytes(),
33
+ });
34
+ const upsert = this.#db.prepare(`INSERT INTO forge_state(root_session_id,key,value_json,revision,updated_at)
35
+ VALUES(?,?,?,?,?)
36
+ ON CONFLICT(root_session_id,key) DO UPDATE SET
37
+ value_json=excluded.value_json, revision=excluded.revision, updated_at=excluded.updated_at`);
38
+ const now = this.#now();
39
+ for (const write of plan.writes)
40
+ upsert.run(rootSessionId, write.key, write.valueJson, write.revision, now);
41
+ return plan.writes.length;
42
+ });
43
+ }
44
+ #read(rootSessionId) {
45
+ const rows = this.#db
46
+ .prepare("SELECT key, value_json, revision FROM forge_state WHERE root_session_id=?")
47
+ .all(rootSessionId);
48
+ return new Map(rows.map((row) => [row.key, { valueJson: row.value_json, revision: Number(row.revision) }]));
49
+ }
50
+ }
@@ -0,0 +1,9 @@
1
+ export declare const TASK_LOG_MAX_BYTES: number;
2
+ export declare class TaskLog {
3
+ #private;
4
+ constructor(path: string, max?: number);
5
+ /** Appends one line (a newline is added). Over the bound, one marker line is written and the rest dropped. */
6
+ write(line: string): void;
7
+ /** Resolves when everything written so far reached the file. */
8
+ flush(): Promise<void>;
9
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The live log of an execution, `<sessionDir>/tasks/<executionId>.log` (spec §9.4): bounded,
3
+ * created `0600`, best effort. The model can `tail` it while a long script runs. A failure to
4
+ * write never affects the execution.
5
+ */
6
+ import { appendFile, mkdir } from "node:fs/promises";
7
+ import { dirname } from "node:path";
8
+ export const TASK_LOG_MAX_BYTES = 256 * 1024;
9
+ export class TaskLog {
10
+ #path;
11
+ #max;
12
+ #written = 0;
13
+ #closed = false;
14
+ #chain = Promise.resolve();
15
+ constructor(path, max = TASK_LOG_MAX_BYTES) {
16
+ this.#path = path;
17
+ this.#max = max;
18
+ }
19
+ /** Appends one line (a newline is added). Over the bound, one marker line is written and the rest dropped. */
20
+ write(line) {
21
+ if (this.#closed)
22
+ return;
23
+ const text = line.endsWith("\n") ? line : `${line}\n`;
24
+ const bytes = Buffer.byteLength(text);
25
+ let data = text;
26
+ if (this.#written + bytes > this.#max) {
27
+ this.#closed = true;
28
+ data = "[log truncated: size limit reached]\n";
29
+ }
30
+ this.#written += Buffer.byteLength(data);
31
+ this.#chain = this.#chain
32
+ .then(async () => {
33
+ await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
34
+ await appendFile(this.#path, data, { mode: 0o600 });
35
+ })
36
+ .catch(() => { });
37
+ }
38
+ /** Resolves when everything written so far reached the file. */
39
+ async flush() {
40
+ await this.#chain;
41
+ }
42
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Who decides how an execution ends (spec §5.2, D5). Pure: no I/O.
3
+ *
4
+ * The host's own stop (a user cancellation, or its deadline) decides the terminal state whatever
5
+ * the guest said afterwards; a guest that merely noticed the same thing first reports the same
6
+ * code; anything else keeps the guest's own outcome. A guest that says TIMEOUT or CANCELLED when
7
+ * the host stopped nothing is only reporting a failure: the terminal states are the host's alone.
8
+ */
9
+ import type { ForgeErrorCode } from "./errors.ts";
10
+ import type { EngineOutcome } from "./ports.ts";
11
+ import type { ForgeStatus } from "./result.ts";
12
+ export interface Verdict {
13
+ status: ForgeStatus;
14
+ error?: {
15
+ code: ForgeErrorCode;
16
+ message: string;
17
+ };
18
+ value?: unknown;
19
+ }
20
+ export declare function verdictOf(outcome: EngineOutcome, stop: "cancelled" | "timeout" | undefined, timeoutMs: number): Verdict;
@@ -0,0 +1,28 @@
1
+ const timedOut = (timeoutMs) => ({
2
+ status: "timed_out",
3
+ error: { code: "TIMEOUT", message: `Execution exceeded ${timeoutMs} ms` },
4
+ });
5
+ const cancelled = () => ({
6
+ status: "cancelled",
7
+ error: { code: "CANCELLED", message: "Execution was cancelled" },
8
+ });
9
+ export function verdictOf(outcome, stop, timeoutMs) {
10
+ if (stop === "timeout")
11
+ return timedOut(timeoutMs);
12
+ if (stop === "cancelled")
13
+ return cancelled();
14
+ if (outcome.kind === "completed")
15
+ return {
16
+ status: "succeeded",
17
+ ...(outcome.value !== undefined ? { value: outcome.value } : {}),
18
+ };
19
+ if (outcome.code === "TIMEOUT" || outcome.code === "CANCELLED")
20
+ return {
21
+ status: "failed",
22
+ error: {
23
+ code: "SCRIPT_RUNTIME",
24
+ message: `the script reported ${outcome.code} on its own: ${outcome.message}`,
25
+ },
26
+ };
27
+ return { status: "failed", error: { code: outcome.code, message: outcome.message } };
28
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The entry the compiled `alisio` binary runs for its hidden `__forge-isolate` command (spec D-21):
3
+ * the body of a Forja process isolate. It is a separate export on purpose, so the CLI loads it
4
+ * without loading the application, the configuration or SQLite.
5
+ */
6
+ export { runProcessIsolate } from "./forge/javascript/hosts/isolate-main.ts";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The entry the compiled `alisio` binary runs for its hidden `__forge-isolate` command (spec D-21):
3
+ * the body of a Forja process isolate. It is a separate export on purpose, so the CLI loads it
4
+ * without loading the application, the configuration or SQLite.
5
+ */
6
+ export { runProcessIsolate } from "./forge/javascript/hosts/isolate-main.js";
package/dist/index.d.ts CHANGED
@@ -41,7 +41,29 @@ export { ToolRegistry } from "./core/registry.ts";
41
41
  export { RunClock } from "./core/run-clock.ts";
42
42
  export { AgentRunner, type CompactionResult, defaultTokenBudget, type RunnerOptions, type RunnerSettingsPatch, type RunOptions, } from "./core/runner.ts";
43
43
  export { CircuitBreaker, type CircuitBreakerOptions, type CircuitState, DECISION_LIMITS, DEFAULT_DECISIONS_CONFIG, type DecisionAttempt, DecisionError, type DecisionMetricEntry, DecisionMetrics, type DecisionMetricsSnapshot, type DecisionOutcome, DecisionRegistry, DecisionService, type DecisionServiceDeps, type DecisionStatus, type DecisionsConfig, formatDecisionStatsSection, formatDecisionsReport, HEALTH_TIMEOUT_MS, type RegisteredDecisionProvider, splitSupported, validateAnswers, validateRequest, } from "./decisions/index.ts";
44
+ export { canonicalJson, hashCanonical } from "./eval/canonical.ts";
45
+ export { CASSETTE_SUFFIX, type Cassette, type CassetteCall, CassetteFormatError, type CassetteHeader, type CassetteMeta, CassetteRecorder, CassetteWriteError, MAX_CASSETTE_BYTES, MAX_CASSETTE_DIR_BYTES, parseCassette, RecordingProvider, ReplayDivergenceError, ReplayExhausted, ReplayProvider, ReplayUnconsumedError, readCassette, serializeCassette, type WriteOptions, writeCassette, } from "./eval/cassette.ts";
46
+ export { type Conditions, compareReports, compareVariants, EvalCompareError, type EvalComparison, type Interval, type MetricDelta, parseReport, renderComparisonMarkdown, renderVariantComparisonMarkdown, type TaskComparison, type TokenComparison, type VariantComparison, type Verdict, } from "./eval/compare.ts";
47
+ export { CONFIG_HASH_COVERAGE, type ConfigHashExtras, checkTaskPolicy, computeConfigHash, type EvalApplication, EvalBudgetExceeded, type EvalRunMode, EvalRunRefusedError, type InvokerPermissions, type RunTaskOptions, runTask, } from "./eval/engine.ts";
48
+ export { buildExportBundle, type EvalExportBundle, EvalExportRefusedError, type ExportContext, } from "./eval/export.ts";
49
+ export { computeFingerprint, type FingerprintDiff, type FingerprintDiffEntry, } from "./eval/fingerprint.ts";
50
+ export { EvalPathError, resolveInside } from "./eval/paths.ts";
51
+ export { interleaveVariants, mulberry32, type PlannedRun, planRuns, type VariantStep, } from "./eval/plan.ts";
52
+ export { type BuildReportOptions, buildReport, EvalReportError, estimateRunCost, lowPowerTasks, MIN_POWER_K, renderReportMarkdown, } from "./eval/report.ts";
53
+ export { type RunEvaluationOptions, type RunEvaluationResult, type RunVariantsOptions, runEvaluation, runVariantEvaluations, } from "./eval/run-suite.ts";
54
+ export { findSecrets, redactSecrets } from "./eval/secrets.ts";
55
+ export { median, passAtK, passPowK, sampleVariance, wilsonInterval, } from "./eval/stats.ts";
56
+ export { EvalSuiteError, type LoadedSuite, loadSuite, MAX_VERIFIER_TIMEOUT_MS, } from "./eval/suite.ts";
57
+ export { applyConfigOverrides, EvalVariantError, loadVariantsFile, resolveVariants, type VariantsFile, } from "./eval/variants.ts";
58
+ export { runVerifier, runVerifiers, snapshotTree, type ToolEvent, type TreeSnapshot, type VerifierContext, type VerifierResult, type VerifierStatus, } from "./eval/verifiers.ts";
44
59
  export { type ExtensionConflict, ExtensionRegistry } from "./extensions/registry.ts";
60
+ export { ForgeError } from "./forge/errors.ts";
61
+ export { downloadSize as pyodideDownloadSize, inspectPyodide, installPyodide, PyodideIntegrityError, pyodideDir, } from "./forge/python/pyodide-install.ts";
62
+ export { PYODIDE_MANIFEST, type PyodideManifest, type PyodideProfile, } from "./forge/python/pyodide-manifest.ts";
63
+ export { describePythonBackends } from "./forge/python/status.ts";
64
+ export { createPythonBackends } from "./forge/python/wiring.ts";
65
+ export { describeForge, type ForgeStatus, forgeEngineFor } from "./forge/runtime.ts";
66
+ export { emptyScratch, listScratch, openScratchFile, SCRATCH_LIST_LIMIT, type ScratchFileInfo, } from "./forge/scratch-files.ts";
45
67
  export * from "./goal/index.ts";
46
68
  export { HerdrBridge } from "./integrations/herdr.ts";
47
69
  export { McpConnector, type McpServerInfo, type McpStatus, type McpToolInfo, } from "./mcp/connector.ts";
@@ -62,7 +84,7 @@ export { type ExternalDirectoryDecision, type ExternalDirectoryHandler, type Ext
62
84
  export { BlobStore, type BlobStoreOptions, BlobTooLarge } from "./runtime/blobs.ts";
63
85
  export { clipLines, unifiedPatch } from "./runtime/diff.ts";
64
86
  export { exists, fileSize, readHead, readJson, readText, which } from "./runtime/fs.ts";
65
- export { newId, ulid } from "./runtime/ids.ts";
87
+ export { type IdSource, newId, newUuid, seededIdSource, setIdSource, ulid, } from "./runtime/ids.ts";
66
88
  export { defaultGlobalRoots, isPathSpec, PLUGIN_KEYWORD, resolvePluginSpec, } from "./runtime/modules.ts";
67
89
  export { findWorkspace, inside, outsideRootsMessage, safePath, workspaceKey, } from "./runtime/paths.ts";
68
90
  export { isMissingCommand, type ProcessResult, RIPGREP_INSTALL_HINT, runProcess, } from "./runtime/process.ts";
package/dist/index.js CHANGED
@@ -40,7 +40,29 @@ export { ToolRegistry } from "./core/registry.js";
40
40
  export { RunClock } from "./core/run-clock.js";
41
41
  export { AgentRunner, defaultTokenBudget, } from "./core/runner.js";
42
42
  export { CircuitBreaker, DECISION_LIMITS, DEFAULT_DECISIONS_CONFIG, DecisionError, DecisionMetrics, DecisionRegistry, DecisionService, formatDecisionStatsSection, formatDecisionsReport, HEALTH_TIMEOUT_MS, splitSupported, validateAnswers, validateRequest, } from "./decisions/index.js";
43
+ export { canonicalJson, hashCanonical } from "./eval/canonical.js";
44
+ export { CASSETTE_SUFFIX, CassetteFormatError, CassetteRecorder, CassetteWriteError, MAX_CASSETTE_BYTES, MAX_CASSETTE_DIR_BYTES, parseCassette, RecordingProvider, ReplayDivergenceError, ReplayExhausted, ReplayProvider, ReplayUnconsumedError, readCassette, serializeCassette, writeCassette, } from "./eval/cassette.js";
45
+ export { compareReports, compareVariants, EvalCompareError, parseReport, renderComparisonMarkdown, renderVariantComparisonMarkdown, } from "./eval/compare.js";
46
+ export { CONFIG_HASH_COVERAGE, checkTaskPolicy, computeConfigHash, EvalBudgetExceeded, EvalRunRefusedError, runTask, } from "./eval/engine.js";
47
+ export { buildExportBundle, EvalExportRefusedError, } from "./eval/export.js";
48
+ export { computeFingerprint, } from "./eval/fingerprint.js";
49
+ export { EvalPathError, resolveInside } from "./eval/paths.js";
50
+ export { interleaveVariants, mulberry32, planRuns, } from "./eval/plan.js";
51
+ export { buildReport, EvalReportError, estimateRunCost, lowPowerTasks, MIN_POWER_K, renderReportMarkdown, } from "./eval/report.js";
52
+ export { runEvaluation, runVariantEvaluations, } from "./eval/run-suite.js";
53
+ export { findSecrets, redactSecrets } from "./eval/secrets.js";
54
+ export { median, passAtK, passPowK, sampleVariance, wilsonInterval, } from "./eval/stats.js";
55
+ export { EvalSuiteError, loadSuite, MAX_VERIFIER_TIMEOUT_MS, } from "./eval/suite.js";
56
+ export { applyConfigOverrides, EvalVariantError, loadVariantsFile, resolveVariants, } from "./eval/variants.js";
57
+ export { runVerifier, runVerifiers, snapshotTree, } from "./eval/verifiers.js";
43
58
  export { ExtensionRegistry } from "./extensions/registry.js";
59
+ export { ForgeError } from "./forge/errors.js";
60
+ export { downloadSize as pyodideDownloadSize, inspectPyodide, installPyodide, PyodideIntegrityError, pyodideDir, } from "./forge/python/pyodide-install.js";
61
+ export { PYODIDE_MANIFEST, } from "./forge/python/pyodide-manifest.js";
62
+ export { describePythonBackends } from "./forge/python/status.js";
63
+ export { createPythonBackends } from "./forge/python/wiring.js";
64
+ export { describeForge, forgeEngineFor } from "./forge/runtime.js";
65
+ export { emptyScratch, listScratch, openScratchFile, SCRATCH_LIST_LIMIT, } from "./forge/scratch-files.js";
44
66
  export * from "./goal/index.js";
45
67
  export { HerdrBridge } from "./integrations/herdr.js";
46
68
  export { McpConnector, } from "./mcp/connector.js";
@@ -61,7 +83,7 @@ export { PathAccess, } from "./runtime/access.js";
61
83
  export { BlobStore, BlobTooLarge } from "./runtime/blobs.js";
62
84
  export { clipLines, unifiedPatch } from "./runtime/diff.js";
63
85
  export { exists, fileSize, readHead, readJson, readText, which } from "./runtime/fs.js";
64
- export { newId, ulid } from "./runtime/ids.js";
86
+ export { newId, newUuid, seededIdSource, setIdSource, ulid, } from "./runtime/ids.js";
65
87
  export { defaultGlobalRoots, isPathSpec, PLUGIN_KEYWORD, resolvePluginSpec, } from "./runtime/modules.js";
66
88
  export { findWorkspace, inside, outsideRootsMessage, safePath, workspaceKey, } from "./runtime/paths.js";
67
89
  export { isMissingCommand, RIPGREP_INSTALL_HINT, runProcess, } from "./runtime/process.js";
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The effect gate of a tool call as a pure decision (spec D-25). The runner, `availableTools`, the
3
+ * legacy `execute` tool and the Forja nested-call dispatcher all ask this one function, so a call
4
+ * made from inside a script gets exactly the decision a direct call would get; there is no second
5
+ * permission system.
6
+ *
7
+ * Rules, kept from the runner they were extracted from:
8
+ * - `read` and `internal` always run; `write`, `process` and `external` run when the policy says so.
9
+ * - A tool with no declared effect is `external`.
10
+ * - A capability tool (`analysis.run`) also runs on the `--allow-analysis` flag alone, and on a
11
+ * stored grant; the grant never widens the effect.
12
+ * - Otherwise the call asks, but only for `write`, `process` and `external`, and only when a
13
+ * person can be asked. Anything else is denied.
14
+ *
15
+ * The policy is read by reference on purpose: an "allow for the session" answer widens the very
16
+ * same object, and the next decision must see it.
17
+ */
18
+ import type { ToolDefinition } from "@alisio/sdk";
19
+ import type { Policy } from "../core/contracts.ts";
20
+ export type EffectDecision = {
21
+ kind: "allow";
22
+ via: "effect" | "flag" | "grant";
23
+ } | {
24
+ kind: "ask";
25
+ } | {
26
+ kind: "deny";
27
+ };
28
+ export interface DecideContext {
29
+ /** Whether an interactive approval could be opened for this call. */
30
+ approvals: boolean;
31
+ /** Whether the tool's capability holds a stored grant for the session. */
32
+ capabilityGranted: boolean;
33
+ }
34
+ /** The tool's effect; a tool that declares none is treated as `external`. */
35
+ export declare const effectOf: (tool: Pick<ToolDefinition, "effect">) => string;
36
+ /** Whether the policy lets an effect run without asking. */
37
+ export declare const effectAllowed: (policy: Policy, effect: string) => boolean;
38
+ /** Whether an effect can ever be put to a person (`read` and `internal` never are). */
39
+ export declare const effectAskable: (effect: string) => effect is "write" | "process" | "external";
40
+ /** A tool's capability is pre-allowed by the policy alone (`--allow-analysis`). */
41
+ export declare const capabilityFlagAllowed: (policy: Policy, tool: Pick<ToolDefinition, "capability">) => boolean;
42
+ export declare function decideEffect(policy: Policy, tool: Pick<ToolDefinition, "effect" | "capability">, context: DecideContext): EffectDecision;
@@ -0,0 +1,24 @@
1
+ /** The tool's effect; a tool that declares none is treated as `external`. */
2
+ export const effectOf = (tool) => tool.effect ?? "external";
3
+ /** Whether the policy lets an effect run without asking. */
4
+ export const effectAllowed = (policy, effect) => effect === "read" ||
5
+ effect === "internal" ||
6
+ ((effect === "write" || effect === "process" || effect === "external") && !!policy[effect]);
7
+ /** Whether an effect can ever be put to a person (`read` and `internal` never are). */
8
+ export const effectAskable = (effect) => effect === "write" || effect === "process" || effect === "external";
9
+ /** A tool's capability is pre-allowed by the policy alone (`--allow-analysis`). */
10
+ export const capabilityFlagAllowed = (policy, tool) => tool.capability === "analysis.run" && !!policy.analysis;
11
+ export function decideEffect(policy, tool, context) {
12
+ const effect = effectOf(tool);
13
+ const byEffect = effectAllowed(policy, effect);
14
+ if (tool.capability) {
15
+ // A broad process grant or --allow-analysis: allowed, audited as a flag decision.
16
+ if (byEffect || capabilityFlagAllowed(policy, tool))
17
+ return { kind: "allow", via: "flag" };
18
+ if (context.capabilityGranted)
19
+ return { kind: "allow", via: "grant" };
20
+ }
21
+ else if (byEffect)
22
+ return { kind: "allow", via: "effect" };
23
+ return context.approvals && effectAskable(effect) ? { kind: "ask" } : { kind: "deny" };
24
+ }
@@ -15,6 +15,7 @@ import { tmpdir } from "node:os";
15
15
  import { join } from "node:path";
16
16
  import { textResult } from "@alisio/sdk";
17
17
  import { EXIT_PLAN_TOOL } from "../core/opt-in.js";
18
+ import { newUuid } from "../runtime/ids.js";
18
19
  import { objectSchema } from "../tools/standard.js";
19
20
  import { DIAGRAM_MAX_BYTES, DIAGRAM_MAX_NODES, validateDiagrams, } from "./diagrams.js";
20
21
  import { buildPlanManifest } from "./manifest.js";
@@ -118,7 +119,7 @@ export function registerExitPlan(registry, deps) {
118
119
  throw new Error("exit_plan needs the whole plan in the `plan` field");
119
120
  const sessionId = context.session;
120
121
  const title = planTitle(plan, input.title);
121
- const planId = crypto.randomUUID();
122
+ const planId = newUuid();
122
123
  const reviewable = !!sessionId && deps.ui.interactive() && !deps.readOnly;
123
124
  const revision = sessionId ? nextPlanRevision(deps.store, sessionId) : 1;
124
125
  const previous = sessionId
@@ -16,7 +16,8 @@ export interface ProjectContextOptions {
16
16
  }
17
17
  export declare class ProjectContext implements ContextSource {
18
18
  readonly workspace: string;
19
- extras: Array<() => Promise<string>>;
19
+ /** Extra system-prompt sections; `sessionId` lets one depend on the session (the scratchpad path). */
20
+ extras: Array<(sessionId?: string) => Promise<string>>;
20
21
  readonly cwd: string;
21
22
  private options;
22
23
  /** Per session: nested instruction file path → fingerprint already attached. */
@@ -18,6 +18,7 @@ const PREAMBLE = "You are Alisio, a coding assistant. Follow the user's task. To
18
18
  const HEADER = "# Project instructions (AGENTS.md)\nFiles are listed from the global and repository root down to the current directory; when they conflict, the closest file wins. Explicit user prompts override these instructions. Instructions for nested directories are attached when you touch paths below them.";
19
19
  export class ProjectContext {
20
20
  workspace;
21
+ /** Extra system-prompt sections; `sessionId` lets one depend on the session (the scratchpad path). */
21
22
  extras = [];
22
23
  cwd;
23
24
  options;
@@ -143,7 +144,7 @@ export class ProjectContext {
143
144
  if (files.length)
144
145
  chunks.push(HEADER, ...this.capped(files));
145
146
  for (const extra of this.extras)
146
- chunks.push(await extra());
147
+ chunks.push(await extra(sessionId));
147
148
  return chunks.join("\n\n");
148
149
  }
149
150
  /** Keeps the closest files within the byte budget; drops or truncates the farthest ones. */
@@ -23,6 +23,15 @@ export interface PathAccessOptions {
23
23
  approve?: ExternalDirectoryHandler;
24
24
  /** Fully locked: declared roots are ignored and no external directory is ever approvable. */
25
25
  readOnly?: boolean;
26
+ /**
27
+ * The verified scratchpad of a session's ROOT session (Forja, spec §9.4), created on first use.
28
+ * When given, that folder is an authorized root: a path inside it needs no directory approval.
29
+ * Absent when Forja is off, so nothing changes; ignored under `--read-only`. A failure to
30
+ * provide it just means "no scratchpad".
31
+ */
32
+ scratchpad?: (session: string) => Promise<string | undefined>;
33
+ /** `forge.scratch.maxBytes`, read on every check: the quota of one scratchpad. */
34
+ scratchMaxBytes?: () => number;
26
35
  }
27
36
  export interface ResolvePathOptions {
28
37
  /** Workspace to resolve a relative path against (child sessions may use a worktree). */
@@ -41,11 +50,36 @@ export declare class PathAccess {
41
50
  private readonly roots;
42
51
  private readonly approve?;
43
52
  private readonly readOnly;
53
+ private readonly scratchpad?;
54
+ private readonly scratchMaxBytes?;
44
55
  /** Session-approved directory subtrees (allow for the session). */
45
56
  private readonly approved;
46
57
  /** Canonicalized roots, memoized; a nonexistent declared root never matches. */
47
58
  private readonly canonical;
48
59
  constructor(options: PathAccessOptions);
60
+ /**
61
+ * `target` made safe inside the session's scratchpad, or undefined when it is not there: outside
62
+ * it, no session, no scratchpad, or a symlink on the way (an escaping link is "outside").
63
+ */
64
+ private scratchTarget;
65
+ /** Whether an already resolved path lies in the session's verified scratchpad (never read-only). */
66
+ isScratchPath(target: string, session?: string): Promise<boolean>;
67
+ /** Scratchpad writers in flight, one queue per scratchpad directory (see `lockScratch`). */
68
+ private scratchLocks;
69
+ /**
70
+ * Serializes the quota check and the write that follows it (L1): two parallel writes that each
71
+ * fit would otherwise both pass the check and together overflow. Resolves to the release. Keyed
72
+ * by the scratchpad directory, so a child session shares the lock of its root session.
73
+ */
74
+ lockScratch(session: string | undefined): Promise<() => void>;
75
+ /**
76
+ * Why a write of about `incomingBytes` into the session's scratchpad must be refused (spec §9.4,
77
+ * FJ-AC-34), or undefined when it fits. Nothing is ever deleted to make room: the model is told,
78
+ * plainly, that the scratchpad is full.
79
+ */
80
+ scratchQuotaProblem(session: string | undefined, incomingBytes: number,
81
+ /** Files the write replaces: their current size is not counted twice. */
82
+ replacing?: readonly string[]): Promise<string | undefined>;
49
83
  /** Extra roots actually in force (always empty under `--read-only`). */
50
84
  get extraRoots(): readonly string[];
51
85
  /** Directory subtrees approved for the session (diagnostics and tests). */
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { lstat, realpath } from "node:fs/promises";
15
15
  import { dirname, resolve } from "node:path";
16
+ import { scratchUsage } from "../forge/session-dir.js";
16
17
  import { inside, outsideRootsMessage, safePath } from "./paths.js";
17
18
  /** Nearest existing ancestor directory of `target`, canonicalized; used as the approval boundary. */
18
19
  async function nearestExistingDirectory(target) {
@@ -38,6 +39,8 @@ export class PathAccess {
38
39
  roots;
39
40
  approve;
40
41
  readOnly;
42
+ scratchpad;
43
+ scratchMaxBytes;
41
44
  /** Session-approved directory subtrees (allow for the session). */
42
45
  approved = new Set();
43
46
  /** Canonicalized roots, memoized; a nonexistent declared root never matches. */
@@ -48,6 +51,80 @@ export class PathAccess {
48
51
  this.readOnly = !!options.readOnly;
49
52
  if (options.approve && !options.readOnly)
50
53
  this.approve = options.approve;
54
+ if (options.scratchpad && !options.readOnly)
55
+ this.scratchpad = options.scratchpad;
56
+ if (options.scratchMaxBytes)
57
+ this.scratchMaxBytes = options.scratchMaxBytes;
58
+ }
59
+ /**
60
+ * `target` made safe inside the session's scratchpad, or undefined when it is not there: outside
61
+ * it, no session, no scratchpad, or a symlink on the way (an escaping link is "outside").
62
+ */
63
+ async scratchTarget(target, session) {
64
+ if (!this.scratchpad || !session)
65
+ return undefined;
66
+ try {
67
+ const base = await this.scratchpad(session);
68
+ if (!base || !inside(base, target))
69
+ return undefined;
70
+ return await safePath(base, target);
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ }
76
+ /** Whether an already resolved path lies in the session's verified scratchpad (never read-only). */
77
+ async isScratchPath(target, session) {
78
+ return (await this.scratchTarget(target, session)) !== undefined;
79
+ }
80
+ /** Scratchpad writers in flight, one queue per scratchpad directory (see `lockScratch`). */
81
+ scratchLocks = new Map();
82
+ /**
83
+ * Serializes the quota check and the write that follows it (L1): two parallel writes that each
84
+ * fit would otherwise both pass the check and together overflow. Resolves to the release. Keyed
85
+ * by the scratchpad directory, so a child session shares the lock of its root session.
86
+ */
87
+ async lockScratch(session) {
88
+ const base = this.scratchpad && session
89
+ ? await this.scratchpad(session).catch(() => undefined)
90
+ : undefined;
91
+ const key = base ?? "";
92
+ const previous = this.scratchLocks.get(key) ?? Promise.resolve();
93
+ let release = () => { };
94
+ const mine = new Promise((resolve) => {
95
+ release = resolve;
96
+ });
97
+ const tail = previous.then(() => mine);
98
+ this.scratchLocks.set(key, tail);
99
+ await previous;
100
+ return () => {
101
+ release();
102
+ if (this.scratchLocks.get(key) === tail)
103
+ this.scratchLocks.delete(key);
104
+ };
105
+ }
106
+ /**
107
+ * Why a write of about `incomingBytes` into the session's scratchpad must be refused (spec §9.4,
108
+ * FJ-AC-34), or undefined when it fits. Nothing is ever deleted to make room: the model is told,
109
+ * plainly, that the scratchpad is full.
110
+ */
111
+ async scratchQuotaProblem(session, incomingBytes,
112
+ /** Files the write replaces: their current size is not counted twice. */
113
+ replacing = []) {
114
+ if (!this.scratchpad || !this.scratchMaxBytes || !session)
115
+ return undefined;
116
+ const base = await this.scratchpad(session).catch(() => undefined);
117
+ if (!base)
118
+ return undefined;
119
+ const max = this.scratchMaxBytes();
120
+ let used = await scratchUsage(base);
121
+ for (const path of replacing)
122
+ used -= (await lstat(path).catch(() => undefined))?.size ?? 0;
123
+ used = Math.max(0, used);
124
+ if (used + incomingBytes <= max)
125
+ return undefined;
126
+ return (`The session scratchpad is full: ${used} of ${max} bytes used and this write needs about ` +
127
+ `${incomingBytes}. Nothing was deleted; remove files you no longer need, or ask the user to clear it.`);
51
128
  }
52
129
  /** Extra roots actually in force (always empty under `--read-only`). */
53
130
  get extraRoots() {
@@ -88,6 +165,9 @@ export class PathAccess {
88
165
  const root = await this.rootFor(target, workspace);
89
166
  if (root)
90
167
  return safePath(root, target);
168
+ const scratch = await this.scratchTarget(target, options.session);
169
+ if (scratch)
170
+ return scratch;
91
171
  if (this.readOnly)
92
172
  throw new Error(outsideRootsMessage(target, "--read-only disables access outside the workspace; run without it to allow it"));
93
173
  const boundary = await nearestExistingDirectory(target);
@@ -6,3 +6,24 @@
6
6
  export declare function ulid(now?: number): string;
7
7
  /** A prefixed id such as `art_01J…` or `exec_01J…`. */
8
8
  export declare const newId: (prefix: string, now?: number) => string;
9
+ /**
10
+ * A replacement for the random identifiers the runtime generates (session ids, plan ids, forge
11
+ * execution ids, `ulid`-based ids). The reliability lab installs a seeded one while it records or
12
+ * replays a cassette so the requests a run sends do not depend on fresh randomness. Nothing
13
+ * installs one in normal use, and then every function here behaves exactly as before.
14
+ */
15
+ export interface IdSource {
16
+ /** A UUID (the shape of `crypto.randomUUID()`). */
17
+ uuid(): string;
18
+ /** The 26-character body of a `ulid` (the argument is the caller's clock, which a source may ignore). */
19
+ ulid(now?: number): string;
20
+ }
21
+ /** Installs (or, with `undefined`, removes) the process-wide id source. */
22
+ export declare function setIdSource(source?: IdSource): void;
23
+ /** A random v4 UUID, or the next one of the installed id source. */
24
+ export declare const newUuid: () => string;
25
+ /**
26
+ * A deterministic source: the n-th UUID (and the n-th ULID) depends only on `seed`. UUIDs are
27
+ * v4-shaped; ULIDs carry a counter in their time part, so they stay sortable by creation order.
28
+ */
29
+ export declare function seededIdSource(seed: string): IdSource;
@@ -1,4 +1,4 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  /** Crockford base32 (no I, L, O, U), the ULID alphabet. */
3
3
  const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
4
4
  /**
@@ -7,6 +7,8 @@ const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
7
7
  * millisecond sort arbitrarily, which is fine for row ids and folder names.
8
8
  */
9
9
  export function ulid(now = Date.now()) {
10
+ if (installed)
11
+ return installed.ulid(now);
10
12
  let time = "";
11
13
  let t = Math.max(0, Math.floor(now));
12
14
  for (let i = 0; i < 10; i++) {
@@ -30,3 +32,41 @@ export function ulid(now = Date.now()) {
30
32
  }
31
33
  /** A prefixed id such as `art_01J…` or `exec_01J…`. */
32
34
  export const newId = (prefix, now) => `${prefix}_${ulid(now)}`;
35
+ let installed;
36
+ /** Installs (or, with `undefined`, removes) the process-wide id source. */
37
+ export function setIdSource(source) {
38
+ installed = source;
39
+ }
40
+ /** A random v4 UUID, or the next one of the installed id source. */
41
+ export const newUuid = () => (installed ? installed.uuid() : randomUUID());
42
+ /**
43
+ * A deterministic source: the n-th UUID (and the n-th ULID) depends only on `seed`. UUIDs are
44
+ * v4-shaped; ULIDs carry a counter in their time part, so they stay sortable by creation order.
45
+ */
46
+ export function seededIdSource(seed) {
47
+ let uuids = 0;
48
+ let ulids = 0;
49
+ const digest = (kind, n) => createHash("sha256").update(`${seed}\0${kind}\0${n}`).digest();
50
+ return {
51
+ uuid() {
52
+ const bytes = digest("uuid", uuids++).subarray(0, 16);
53
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
54
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
55
+ const hex = bytes.toString("hex");
56
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
57
+ },
58
+ ulid() {
59
+ let time = "";
60
+ let t = ulids;
61
+ for (let i = 0; i < 10; i++) {
62
+ time = (ALPHABET[t % 32] ?? "0") + time;
63
+ t = Math.floor(t / 32);
64
+ }
65
+ const bytes = digest("ulid", ulids++);
66
+ let random = "";
67
+ for (let i = 0; i < 16; i++)
68
+ random += ALPHABET[(bytes[i] ?? 0) & 31];
69
+ return time + random;
70
+ },
71
+ };
72
+ }
@@ -27,7 +27,8 @@ export declare function runProcess(command: string, args: string[], options: {
27
27
  timeoutMs?: number;
28
28
  maxBytes?: number;
29
29
  env?: Record<string, string>;
30
- onData?: (chunk: string) => void;
30
+ /** Live text; the second argument says which stream it came from. */
31
+ onData?: (chunk: string, stream: "stdout" | "stderr") => void;
31
32
  /** On cancellation: SIGTERM the process group, then SIGKILL after this grace (default 5s). */
32
33
  killGraceMs?: number;
33
34
  /**