indusagi-coding-agent 0.2.5 → 0.2.9

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 (224) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +9 -5
  3. package/dist/entry.js +14267 -24282
  4. package/dist/guardrails.js +924 -7867
  5. package/dist/index.js +13706 -23775
  6. package/package.json +3 -2
  7. package/src/_decls/entry.ts +18 -0
  8. package/src/_decls/guardrails.ts +35 -0
  9. package/src/_decls/index.ts +26 -0
  10. package/src/addons/contract.ts +236 -0
  11. package/src/addons/dispatch/event-dispatcher.ts +164 -0
  12. package/src/addons/dispatch/index.ts +25 -0
  13. package/src/addons/dispatch/tool-interceptor.ts +208 -0
  14. package/src/addons/host.ts +225 -0
  15. package/src/addons/index.ts +112 -0
  16. package/src/addons/manifest.ts +158 -0
  17. package/src/addons/sandbox.ts +170 -0
  18. package/src/addons/surface.ts +78 -0
  19. package/src/boot/auth-vault.ts +195 -0
  20. package/src/boot/boot.ts +138 -0
  21. package/src/boot/contract.ts +238 -0
  22. package/src/boot/heap.ts +59 -0
  23. package/src/boot/index.ts +28 -0
  24. package/src/boot/invocation.ts +93 -0
  25. package/src/boot/runners/addon-wiring.ts +153 -0
  26. package/src/boot/runners/checkpoint.ts +169 -0
  27. package/src/boot/runners/delegate-runner.ts +294 -0
  28. package/src/boot/runners/index.ts +13 -0
  29. package/src/boot/runners/link-runner.ts +45 -0
  30. package/src/boot/runners/memdir.ts +168 -0
  31. package/src/boot/runners/oneshot-runner.ts +58 -0
  32. package/src/boot/runners/read-state.ts +90 -0
  33. package/src/boot/runners/registry.ts +42 -0
  34. package/src/boot/runners/repl-runner.ts +143 -0
  35. package/src/boot/runners/server-mode.ts +121 -0
  36. package/src/boot/runners/session.ts +641 -0
  37. package/src/boot/server-token.ts +148 -0
  38. package/src/boot/stages.ts +167 -0
  39. package/src/boot/upgrade/apply.ts +94 -0
  40. package/src/boot/upgrade/index.ts +13 -0
  41. package/src/boot/upgrade/upgrades.ts +289 -0
  42. package/src/briefing/compose.ts +150 -0
  43. package/src/briefing/context-docs.ts +19 -0
  44. package/src/briefing/contract.ts +717 -0
  45. package/src/briefing/index.ts +31 -0
  46. package/src/briefing/macros.ts +97 -0
  47. package/src/briefing/skills.ts +47 -0
  48. package/src/capability-deck/bridge-ledger/index.ts +27 -0
  49. package/src/capability-deck/bridge-ledger/key.ts +67 -0
  50. package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
  51. package/src/capability-deck/bridge-ledger/network.ts +117 -0
  52. package/src/capability-deck/builtin-bridge.ts +312 -0
  53. package/src/capability-deck/cards/bg-process-card.ts +335 -0
  54. package/src/capability-deck/cards/index.ts +115 -0
  55. package/src/capability-deck/cards/memory-card.ts +146 -0
  56. package/src/capability-deck/cards/plan-file.ts +97 -0
  57. package/src/capability-deck/cards/plan-tools.ts +185 -0
  58. package/src/capability-deck/cards/saas-card.ts +183 -0
  59. package/src/capability-deck/cards/task-card.ts +207 -0
  60. package/src/capability-deck/cards/todo-card.ts +168 -0
  61. package/src/capability-deck/cards/workflow-card.ts +247 -0
  62. package/src/capability-deck/contract.ts +388 -0
  63. package/src/capability-deck/index.ts +48 -0
  64. package/src/capability-deck/manifest.ts +109 -0
  65. package/src/capability-deck/provision.ts +169 -0
  66. package/src/channels/contract.ts +191 -0
  67. package/src/channels/framer.ts +50 -0
  68. package/src/channels/index.ts +101 -0
  69. package/src/channels/link/dialog.ts +129 -0
  70. package/src/channels/link/driver.ts +190 -0
  71. package/src/channels/link/index.ts +34 -0
  72. package/src/channels/link/server.ts +134 -0
  73. package/src/channels/oneshot.ts +90 -0
  74. package/src/channels/ops.ts +65 -0
  75. package/src/channels/session-ops.ts +81 -0
  76. package/src/conductor/bash-guard.ts +599 -0
  77. package/src/conductor/catalog/catalog.ts +116 -0
  78. package/src/conductor/catalog/index.ts +8 -0
  79. package/src/conductor/catalog/matcher.ts +134 -0
  80. package/src/conductor/conductor.ts +234 -0
  81. package/src/conductor/contract.ts +842 -0
  82. package/src/conductor/diagnostics.ts +227 -0
  83. package/src/conductor/index.ts +33 -0
  84. package/src/conductor/permissions.ts +588 -0
  85. package/src/conductor/quota-error.ts +49 -0
  86. package/src/conductor/signal-hub/hub.ts +46 -0
  87. package/src/conductor/signal-hub/index.ts +2 -0
  88. package/src/conductor/signal-hub/translate.test.ts +81 -0
  89. package/src/conductor/signal-hub/translate.ts +74 -0
  90. package/src/conductor/skill-parse/index.ts +2 -0
  91. package/src/conductor/skill-parse/parse.ts +108 -0
  92. package/src/conductor/transcript-store/index.ts +22 -0
  93. package/src/conductor/transcript-store/serialize.ts +116 -0
  94. package/src/conductor/transcript-store/store.ts +205 -0
  95. package/src/console/auth-status.ts +56 -0
  96. package/src/console/components/AgentsView.ts +165 -0
  97. package/src/console/components/BackgroundAgents.ts +155 -0
  98. package/src/console/components/Banner.ts +334 -0
  99. package/src/console/components/Composer.ts +94 -0
  100. package/src/console/components/StatusBar.ts +49 -0
  101. package/src/console/components/TerminalConsole.ts +1090 -0
  102. package/src/console/components/WorkingIndicator.ts +98 -0
  103. package/src/console/components/banner-sweep.ts +24 -0
  104. package/src/console/components/welcome.ts +74 -0
  105. package/src/console/contract.ts +630 -0
  106. package/src/console/index.ts +34 -0
  107. package/src/console/input/complete.ts +127 -0
  108. package/src/console/input/dir-reader.ts +34 -0
  109. package/src/console/input/index.ts +23 -0
  110. package/src/console/input/keymap.ts +159 -0
  111. package/src/console/input/paste.ts +104 -0
  112. package/src/console/mount.ts +56 -0
  113. package/src/console/overlays/approval-queue.ts +57 -0
  114. package/src/console/overlays/approval.ts +130 -0
  115. package/src/console/overlays/auth.ts +342 -0
  116. package/src/console/overlays/boards.ts +308 -0
  117. package/src/console/overlays/host.ts +36 -0
  118. package/src/console/overlays/index.ts +26 -0
  119. package/src/console/overlays/pickers.ts +258 -0
  120. package/src/console/overlays/sessions.ts +190 -0
  121. package/src/console/reducer.ts +182 -0
  122. package/src/console/slash/builtins.ts +81 -0
  123. package/src/console/slash/commands/dynamic.ts +83 -0
  124. package/src/console/slash/commands/integrations.ts +695 -0
  125. package/src/console/slash/commands/shared.ts +75 -0
  126. package/src/console/slash/commands/transcript.ts +263 -0
  127. package/src/console/slash/commands/workbench.ts +246 -0
  128. package/src/console/slash/index.ts +15 -0
  129. package/src/console/slash/registry.ts +70 -0
  130. package/src/console/slash/resolve.ts +63 -0
  131. package/src/console/startup.ts +209 -0
  132. package/src/console/theme/adapter.ts +45 -0
  133. package/src/console/theme/index.ts +7 -0
  134. package/src/console/theme/palette.ts +68 -0
  135. package/src/console/theme/resolve.ts +39 -0
  136. package/src/console/theme/tokens.ts +71 -0
  137. package/src/entry.ts +55 -0
  138. package/src/guardrails.ts +37 -0
  139. package/src/index.ts +18 -0
  140. package/src/insight/channel.ts +88 -0
  141. package/src/insight/contract.ts +185 -0
  142. package/src/insight/index.ts +110 -0
  143. package/src/insight/recorder.ts +213 -0
  144. package/src/insight/redaction.ts +157 -0
  145. package/src/insight/replay.ts +158 -0
  146. package/src/insight/sampling.ts +70 -0
  147. package/src/insight/serialize.ts +50 -0
  148. package/src/insight/sinks/console.ts +64 -0
  149. package/src/insight/sinks/file.ts +40 -0
  150. package/src/insight/sinks/index.ts +24 -0
  151. package/src/insight/sinks/stream.ts +54 -0
  152. package/src/integrations/sarvam/attach.ts +239 -0
  153. package/src/integrations/sarvam/config.ts +156 -0
  154. package/src/integrations/sarvam/index.ts +25 -0
  155. package/src/integrations/sarvam/sarvam.test.ts +60 -0
  156. package/src/integrations/sarvam/types.ts +27 -0
  157. package/src/integrations/zoho/attach.ts +342 -0
  158. package/src/integrations/zoho/config.ts +125 -0
  159. package/src/integrations/zoho/index.ts +27 -0
  160. package/src/integrations/zoho/types.ts +21 -0
  161. package/src/integrations/zoho/zoho.test.ts +50 -0
  162. package/src/kit/clipboard-image.ts +107 -0
  163. package/src/kit/external-editor.ts +48 -0
  164. package/src/kit/image.ts +59 -0
  165. package/src/kit/index.ts +51 -0
  166. package/src/kit/shell.ts +19 -0
  167. package/src/kit/tool-fetch.ts +85 -0
  168. package/src/launch/catalog.ts +148 -0
  169. package/src/launch/contract.ts +187 -0
  170. package/src/launch/credentials.ts +625 -0
  171. package/src/launch/index.ts +98 -0
  172. package/src/launch/invocation/attachments.ts +179 -0
  173. package/src/launch/invocation/flags.ts +196 -0
  174. package/src/launch/invocation/index.ts +25 -0
  175. package/src/launch/invocation/read.ts +260 -0
  176. package/src/launch/invocation/usage.ts +67 -0
  177. package/src/launch/login.ts +324 -0
  178. package/src/launch/oauth.test.ts +18 -0
  179. package/src/launch/oauth.ts +203 -0
  180. package/src/launch/packages.ts +194 -0
  181. package/src/launch/pickers.ts +189 -0
  182. package/src/runtime-bridge/bridges/_drive.ts +96 -0
  183. package/src/runtime-bridge/bridges/builtins.ts +68 -0
  184. package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
  185. package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
  186. package/src/runtime-bridge/bridges/index.ts +33 -0
  187. package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
  188. package/src/runtime-bridge/broker.ts +227 -0
  189. package/src/runtime-bridge/contract.ts +122 -0
  190. package/src/runtime-bridge/index.ts +79 -0
  191. package/src/runtime-bridge/sink.ts +180 -0
  192. package/src/sessions/contract.ts +81 -0
  193. package/src/sessions/index.ts +13 -0
  194. package/src/sessions/library.ts +229 -0
  195. package/src/settings/contract.ts +114 -0
  196. package/src/settings/index.ts +32 -0
  197. package/src/settings/manager.ts +117 -0
  198. package/src/transcript-export/index.ts +45 -0
  199. package/src/transcript-export/publish.ts +260 -0
  200. package/src/transcript-export/sgr.ts +315 -0
  201. package/src/transcript-export/template.ts +272 -0
  202. package/src/transcript-export/theme-bridge.ts +150 -0
  203. package/src/window-budget/budget/estimate.ts +135 -0
  204. package/src/window-budget/budget/gate.ts +33 -0
  205. package/src/window-budget/budget/index.ts +16 -0
  206. package/src/window-budget/budget/slice.ts +56 -0
  207. package/src/window-budget/condenser.ts +58 -0
  208. package/src/window-budget/contract.ts +184 -0
  209. package/src/window-budget/index.ts +19 -0
  210. package/src/window-budget/microcompact.ts +95 -0
  211. package/src/window-budget/rehydrate.ts +136 -0
  212. package/src/window-budget/summarize/condense.ts +103 -0
  213. package/src/window-budget/summarize/index.ts +14 -0
  214. package/src/window-budget/summarize/prompt.ts +149 -0
  215. package/src/workflow-engine/agent-runner.ts +181 -0
  216. package/src/workflow-engine/display.ts +224 -0
  217. package/src/workflow-engine/engine.ts +294 -0
  218. package/src/workflow-engine/index.ts +23 -0
  219. package/src/workflow-engine/parse.ts +172 -0
  220. package/src/workflow-engine/structured-output.ts +35 -0
  221. package/src/workspace/brand.ts +29 -0
  222. package/src/workspace/index.ts +18 -0
  223. package/src/workspace/locator.ts +103 -0
  224. package/src/workspace/runtime-detect.ts +64 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Secret redaction — the attribute processor the pipeline runs before a sink.
3
+ */
4
+ import {
5
+ REDACTED_TOKEN,
6
+ type Probe,
7
+ type ProbeAttributes,
8
+ type ProbeFault,
9
+ type RedactionOptions,
10
+ type RedactionRule,
11
+ type SecretRedactor,
12
+ } from "./contract.js";
13
+
14
+ const SECRET_KEY_FRAGMENTS = [
15
+ "apikey",
16
+ "api_key",
17
+ "secret",
18
+ "password",
19
+ "passwd",
20
+ "authorization",
21
+ "auth_token",
22
+ "access_token",
23
+ "refresh_token",
24
+ "session_token",
25
+ "private_key",
26
+ "credential",
27
+ "cookie",
28
+ "set-cookie",
29
+ "bearer",
30
+ ];
31
+
32
+ const SECRET_KEY_PATTERN = new RegExp(`(${SECRET_KEY_FRAGMENTS.join("|")})`, "i");
33
+
34
+ const SECRET_VALUE_PATTERNS: RegExp[] = [
35
+ /\b(?:Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{8,}/g,
36
+ /\b[a-z]{1,5}-(?:live|test|proj)?[-_]?[A-Za-z0-9]{16,}\b/g,
37
+ /\b(?:gh[opsu]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g,
38
+ /\bAKIA[0-9A-Z]{12,}\b/g,
39
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
40
+ /-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?-----END[ A-Z]*PRIVATE KEY-----/g,
41
+ ];
42
+
43
+ export const DEFAULT_REDACTION_RULES: readonly RedactionRule[] = Object.freeze([
44
+ Object.freeze({ id: "secret-key", key: SECRET_KEY_PATTERN }),
45
+ ...SECRET_VALUE_PATTERNS.map((value, i) =>
46
+ Object.freeze({ id: `secret-value-${i + 1}`, value }),
47
+ ),
48
+ ]);
49
+
50
+ const DEFAULT_MAX_STRING_LENGTH = 4096;
51
+ const TRUNCATION_SUFFIX = "...[insight:truncated]";
52
+
53
+ function keyMatches(rule: RedactionRule, key: string): boolean {
54
+ const matcher = rule.key;
55
+ if (matcher === undefined) return false;
56
+ if (typeof matcher === "string") {
57
+ return key.toLowerCase().includes(matcher.toLowerCase());
58
+ }
59
+ return matcher.test(key);
60
+ }
61
+
62
+ function applyValueRules(rules: readonly RedactionRule[], text: string): string {
63
+ let out = text;
64
+ for (const rule of rules) {
65
+ const pattern = rule.value;
66
+ if (pattern === undefined) continue;
67
+ if (pattern.global || pattern.sticky) pattern.lastIndex = 0;
68
+ out = out.replace(pattern, REDACTED_TOKEN);
69
+ }
70
+ return out;
71
+ }
72
+
73
+ export function createRedactor(
74
+ rules: readonly RedactionRule[] = DEFAULT_REDACTION_RULES,
75
+ options: RedactionOptions = {},
76
+ ): SecretRedactor {
77
+ const frozenRules = Object.freeze([...rules]);
78
+ const maxLen = options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH;
79
+ const omit = new Set((options.omitKeys ?? []).map((k) => k.toLowerCase()));
80
+
81
+ function isOmitted(key: string): boolean {
82
+ return omit.has(key.toLowerCase());
83
+ }
84
+
85
+ function isSecretKey(key: string): boolean {
86
+ for (const rule of frozenRules) {
87
+ if (keyMatches(rule, key)) return true;
88
+ }
89
+ return false;
90
+ }
91
+
92
+ function capLength(text: string): string {
93
+ if (text.length <= maxLen) return text;
94
+ return text.slice(0, maxLen) + TRUNCATION_SUFFIX;
95
+ }
96
+
97
+ function scrubNode(value: unknown, key: string | undefined): unknown {
98
+ if (key !== undefined && isSecretKey(key)) {
99
+ return REDACTED_TOKEN;
100
+ }
101
+ if (typeof value === "string") {
102
+ return capLength(applyValueRules(frozenRules, value));
103
+ }
104
+ if (Array.isArray(value)) {
105
+ return Object.freeze(value.map((item) => scrubNode(item, undefined)));
106
+ }
107
+ if (value !== null && typeof value === "object") {
108
+ const out: Record<string, unknown> = {};
109
+ for (const [k, v] of Object.entries(value)) {
110
+ if (isOmitted(k)) continue;
111
+ out[k] = scrubNode(v, k);
112
+ }
113
+ return Object.freeze(out);
114
+ }
115
+ return value;
116
+ }
117
+
118
+ function scrubValue(value: unknown): unknown {
119
+ return scrubNode(value, undefined);
120
+ }
121
+
122
+ function scrubAttributes(attributes: ProbeAttributes): ProbeAttributes {
123
+ const out: Record<string, unknown> = {};
124
+ for (const [k, v] of Object.entries(attributes)) {
125
+ if (isOmitted(k)) continue;
126
+ out[k] = scrubNode(v, k);
127
+ }
128
+ return Object.freeze(out);
129
+ }
130
+
131
+ function scrubFault(fault: ProbeFault): ProbeFault {
132
+ const scrubbed: { message: string; name?: string; stack?: string } = {
133
+ message: capLength(applyValueRules(frozenRules, fault.message)),
134
+ };
135
+ if (fault.name !== undefined) scrubbed.name = fault.name;
136
+ if (fault.stack !== undefined) scrubbed.stack = capLength(applyValueRules(frozenRules, fault.stack));
137
+ return Object.freeze(scrubbed);
138
+ }
139
+
140
+ function scrub(probe: Probe): Probe {
141
+ const next: Probe = {
142
+ ...probe,
143
+ attributes: scrubAttributes(probe.attributes),
144
+ ...(probe.fault !== undefined ? { fault: scrubFault(probe.fault) } : {}),
145
+ };
146
+ return Object.freeze(next);
147
+ }
148
+
149
+ return Object.freeze({
150
+ rules: frozenRules,
151
+ scrubValue,
152
+ scrubAttributes,
153
+ scrub,
154
+ });
155
+ }
156
+
157
+ export const DEFAULT_REDACTOR: SecretRedactor = createRedactor();
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Replay — read an NDJSON trace file back into probes and trails.
3
+ */
4
+ import {
5
+ isClosedProbe,
6
+ type Probe,
7
+ type ProbeId,
8
+ type Signal,
9
+ type TrailId,
10
+ } from "./contract.js";
11
+ import { decodeRecord, LINE_FEED, RECORD_VERSION, type TraceRecord } from "./serialize.js";
12
+
13
+ export type ChunkSource = AsyncIterable<string | Uint8Array> | Iterable<string | Uint8Array>;
14
+
15
+ export interface ReplayOptions {
16
+ readonly strict?: boolean;
17
+ }
18
+
19
+ const TEXT_DECODER = new TextDecoder();
20
+
21
+ function chunkToText(chunk: string | Uint8Array): string {
22
+ return typeof chunk === "string" ? chunk : TEXT_DECODER.decode(chunk, { stream: true });
23
+ }
24
+
25
+ export async function* readLines(
26
+ source: ChunkSource,
27
+ ): AsyncGenerator<string, void, unknown> {
28
+ let buffer = "";
29
+ for await (const chunk of source) {
30
+ buffer += chunkToText(chunk);
31
+ let at = buffer.indexOf(LINE_FEED);
32
+ while (at !== -1) {
33
+ const line = buffer.slice(0, at);
34
+ buffer = buffer.slice(at + 1);
35
+ if (line.trim().length > 0) yield line;
36
+ at = buffer.indexOf(LINE_FEED);
37
+ }
38
+ }
39
+ buffer += TEXT_DECODER.decode();
40
+ if (buffer.trim().length > 0) yield buffer;
41
+ }
42
+
43
+ function isValidRecord(value: unknown): value is TraceRecord {
44
+ if (value === null || typeof value !== "object") return false;
45
+ const r = value as Record<string, unknown>;
46
+ return (
47
+ r.v === RECORD_VERSION &&
48
+ (r.phase === "open" || r.phase === "update" || r.phase === "close") &&
49
+ typeof r.at === "number" &&
50
+ r.probe !== null &&
51
+ typeof r.probe === "object"
52
+ );
53
+ }
54
+
55
+ export async function* readRecords(
56
+ source: ChunkSource,
57
+ options: ReplayOptions = {},
58
+ ): AsyncGenerator<TraceRecord, void, unknown> {
59
+ for await (const line of readLines(source)) {
60
+ let record: TraceRecord;
61
+ try {
62
+ record = decodeRecord(line);
63
+ } catch (error) {
64
+ if (options.strict) throw error;
65
+ continue;
66
+ }
67
+ if (!isValidRecord(record)) {
68
+ if (options.strict) {
69
+ throw new Error("insight replay: malformed trace record");
70
+ }
71
+ continue;
72
+ }
73
+ yield record;
74
+ }
75
+ }
76
+
77
+ export async function* replaySignals(
78
+ source: ChunkSource,
79
+ options: ReplayOptions = {},
80
+ ): AsyncGenerator<Signal, void, unknown> {
81
+ for await (const record of readRecords(source, options)) {
82
+ const frozen = Object.freeze({ ...record.probe });
83
+ if (record.phase === "close") {
84
+ yield { phase: "close", probe: frozen as Probe & { endedAt: number; status: "ok" | "error" }, at: record.at };
85
+ } else {
86
+ yield { phase: record.phase, probe: frozen, at: record.at };
87
+ }
88
+ }
89
+ }
90
+
91
+ export async function replayProbes(
92
+ source: ChunkSource,
93
+ options: ReplayOptions = {},
94
+ ): Promise<Map<ProbeId, Probe>> {
95
+ const byId = new Map<ProbeId, Probe>();
96
+ for await (const record of readRecords(source, options)) {
97
+ byId.set(record.probe.id, Object.freeze({ ...record.probe }));
98
+ }
99
+ return byId;
100
+ }
101
+
102
+ export interface ReplayedTrail {
103
+ readonly trailId: TrailId;
104
+ readonly root: Probe | null;
105
+ readonly probes: ReadonlyMap<ProbeId, Probe>;
106
+ readonly children: ReadonlyMap<ProbeId | null, readonly ProbeId[]>;
107
+ readonly complete: boolean;
108
+ }
109
+
110
+ async function* finalProbes(
111
+ source: ChunkSource,
112
+ options: ReplayOptions,
113
+ ): AsyncGenerator<Probe, void, unknown> {
114
+ const final = await replayProbes(source, options);
115
+ for (const probe of final.values()) yield probe;
116
+ }
117
+
118
+ function buildTrail(
119
+ trailId: TrailId,
120
+ probes: ReadonlyMap<ProbeId, Probe>,
121
+ ): ReplayedTrail {
122
+ const children = new Map<ProbeId | null, ProbeId[]>();
123
+ let root: Probe | null = null;
124
+ let complete = true;
125
+ for (const probe of probes.values()) {
126
+ const key = probe.parentId;
127
+ const bucket = children.get(key);
128
+ if (bucket === undefined) children.set(key, [probe.id]);
129
+ else bucket.push(probe.id);
130
+ if (probe.parentId === null) root = probe;
131
+ if (!isClosedProbe(probe)) complete = false;
132
+ }
133
+ return {
134
+ trailId,
135
+ root,
136
+ probes,
137
+ children,
138
+ complete,
139
+ };
140
+ }
141
+
142
+ export async function replayTrails(
143
+ source: ChunkSource,
144
+ options: ReplayOptions = {},
145
+ ): Promise<ReplayedTrail[]> {
146
+ const probesByTrail = new Map<TrailId, Map<ProbeId, Probe>>();
147
+ const order: TrailId[] = [];
148
+ for await (const probe of finalProbes(source, options)) {
149
+ let bucket = probesByTrail.get(probe.trailId);
150
+ if (bucket === undefined) {
151
+ bucket = new Map();
152
+ probesByTrail.set(probe.trailId, bucket);
153
+ order.push(probe.trailId);
154
+ }
155
+ bucket.set(probe.id, probe);
156
+ }
157
+ return order.map((trailId) => buildTrail(trailId, probesByTrail.get(trailId) ?? new Map()));
158
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Sampling — the admission gate that decides which trails are recorded.
3
+ */
4
+ import type { FixedGates, SampleGate, TrailId } from "./contract.js";
5
+
6
+ export const alwaysGate: SampleGate = Object.freeze({
7
+ admit(): boolean {
8
+ return true;
9
+ },
10
+ });
11
+
12
+ export const neverGate: SampleGate = Object.freeze({
13
+ admit(): boolean {
14
+ return false;
15
+ },
16
+ });
17
+
18
+ export const FIXED_GATES: FixedGates = Object.freeze({
19
+ always: alwaysGate,
20
+ never: neverGate,
21
+ });
22
+
23
+ const FNV_OFFSET_BASIS = 2166136261;
24
+ const UINT32_SPAN = 4294967296;
25
+
26
+ export function hash32(text: string): number {
27
+ let h = FNV_OFFSET_BASIS;
28
+ for (let i = 0; i < text.length; i++) {
29
+ h ^= text.charCodeAt(i);
30
+ h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
31
+ }
32
+ return h >>> 0;
33
+ }
34
+
35
+ export function trailScore(trailId: TrailId): number {
36
+ return hash32(trailId) / UINT32_SPAN;
37
+ }
38
+
39
+ function clampFraction(fraction: number): number {
40
+ if (Number.isNaN(fraction)) return 0;
41
+ if (fraction <= 0) return 0;
42
+ if (fraction >= 1) return 1;
43
+ return fraction;
44
+ }
45
+
46
+ export interface RatioGate extends SampleGate {
47
+ readonly fraction: number;
48
+ verdict(trailId: TrailId): boolean;
49
+ }
50
+
51
+ export function ratioGate(fraction: number): RatioGate {
52
+ const clamped = clampFraction(fraction);
53
+ return Object.freeze({
54
+ fraction: clamped,
55
+ verdict(trailId: TrailId): boolean {
56
+ if (clamped <= 0) return false;
57
+ if (clamped >= 1) return true;
58
+ return trailScore(trailId) < clamped;
59
+ },
60
+ admit(rootName: string): boolean {
61
+ if (clamped <= 0) return false;
62
+ if (clamped >= 1) return true;
63
+ return hash32(rootName) / UINT32_SPAN < clamped;
64
+ },
65
+ });
66
+ }
67
+
68
+ export function isRatioGate(gate: SampleGate): gate is RatioGate {
69
+ return typeof (gate as Partial<RatioGate>).verdict === "function";
70
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Serialization — the on-disk NDJSON record shape shared by the file sink and
3
+ * the replay reader.
4
+ */
5
+ import type { ClosedProbe, Probe, Signal, SignalPhase } from "./contract.js";
6
+
7
+ export const RECORD_VERSION = 1 as const;
8
+
9
+ export interface TraceRecord {
10
+ readonly v: typeof RECORD_VERSION;
11
+ readonly phase: SignalPhase;
12
+ readonly probe: Probe;
13
+ readonly at: number;
14
+ }
15
+
16
+ export const LINE_FEED = "\n";
17
+ const LINE_SEPARATOR_ESCAPE = "\\u2028";
18
+ const PARAGRAPH_SEPARATOR_ESCAPE = "\\u2029";
19
+ const SEPARATOR_PATTERN = /[\u2028\u2029]/g;
20
+
21
+ function escapeSeparators(json: string): string {
22
+ return json.replace(
23
+ SEPARATOR_PATTERN,
24
+ (ch) =>
25
+ ch.charCodeAt(0) === 0x2028 ? LINE_SEPARATOR_ESCAPE : PARAGRAPH_SEPARATOR_ESCAPE,
26
+ );
27
+ }
28
+
29
+ export function signalToRecord(signal: Signal): TraceRecord {
30
+ return {
31
+ v: RECORD_VERSION,
32
+ phase: signal.phase,
33
+ probe: signal.probe,
34
+ at: signal.at,
35
+ };
36
+ }
37
+
38
+ export function encodeRecord(signal: Signal): string {
39
+ return escapeSeparators(JSON.stringify(signalToRecord(signal))) + LINE_FEED;
40
+ }
41
+
42
+ export function decodeRecord(line: string): TraceRecord {
43
+ return JSON.parse(line) as TraceRecord;
44
+ }
45
+
46
+ export function recordIsClose(
47
+ record: TraceRecord,
48
+ ): record is TraceRecord & { probe: ClosedProbe } {
49
+ return record.phase === "close";
50
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Console sink — drains signals to a writable, one human-readable line each.
3
+ */
4
+ import type { Sink } from "../contract.js";
5
+
6
+ export interface LineWriter {
7
+ write(chunk: string): unknown;
8
+ readonly isTTY?: boolean;
9
+ }
10
+
11
+ export interface ConsoleSinkOptions {
12
+ readonly writer?: LineWriter;
13
+ readonly color?: boolean;
14
+ readonly id?: string;
15
+ }
16
+
17
+ const SGR = {
18
+ reset: "\x1B[0m",
19
+ dim: "\x1B[2m",
20
+ green: "\x1B[32m",
21
+ red: "\x1B[31m",
22
+ cyan: "\x1B[36m",
23
+ };
24
+
25
+ function paint(on: boolean, code: string, text: string): string {
26
+ return on ? code + text + SGR.reset : text;
27
+ }
28
+
29
+ function label(probe: { kind: string; name: string }): string {
30
+ return probe.kind + "/" + probe.name;
31
+ }
32
+
33
+ function formatSignal(
34
+ signal: import("../contract.js").Signal,
35
+ color: boolean,
36
+ ): string {
37
+ const p = signal.probe;
38
+ const ids = p.trailId.slice(0, 8) + ":" + p.id.slice(0, 6);
39
+ if (signal.phase === "close") {
40
+ const closed = p;
41
+ const ms = (closed.endedAt ?? 0) - closed.startedAt;
42
+ const ok = closed.status === "ok";
43
+ const tag = paint(color, ok ? SGR.green : SGR.red, closed.status);
44
+ const dur = paint(color, SGR.dim, `${ms}ms`);
45
+ return `close ${tag} ${label(closed)} ${dur} ${ids}`;
46
+ }
47
+ const head = paint(color, SGR.cyan, signal.phase);
48
+ const body = paint(color, SGR.dim, `${label(p)} ${ids}`);
49
+ return `${head} ${body}`;
50
+ }
51
+
52
+ export function createConsoleSink(options: ConsoleSinkOptions = {}): Sink {
53
+ const writer: LineWriter = options.writer ?? (process.stdout as unknown as LineWriter);
54
+ const color = options.color ?? writer.isTTY ?? false;
55
+ const id = options.id ?? "console";
56
+ return {
57
+ id,
58
+ async drain(stream) {
59
+ for await (const signal of stream) {
60
+ writer.write(formatSignal(signal, color) + "\n");
61
+ }
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * File sink — drains signals to an append-only NDJSON trace file.
3
+ */
4
+ import { mkdir, open } from "node:fs/promises";
5
+ import { dirname } from "node:path";
6
+ import type { Sink } from "../contract.js";
7
+ import { encodeRecord } from "../serialize.js";
8
+
9
+ export interface FileSinkOptions {
10
+ readonly path: string;
11
+ readonly id?: string;
12
+ readonly mkdirp?: boolean;
13
+ readonly truncate?: boolean;
14
+ }
15
+
16
+ export function createFileSink(options: FileSinkOptions): Sink {
17
+ const id = options.id ?? "file";
18
+ const mode = options.truncate ? "w" : "a";
19
+ return {
20
+ id,
21
+ async drain(stream) {
22
+ let handle: Awaited<ReturnType<typeof open>> | null = null;
23
+ try {
24
+ for await (const signal of stream) {
25
+ if (handle === null) {
26
+ if (options.mkdirp ?? true) {
27
+ await mkdir(dirname(options.path), { recursive: true });
28
+ }
29
+ handle = await open(options.path, mode);
30
+ }
31
+ await handle.write(encodeRecord(signal));
32
+ }
33
+ } finally {
34
+ if (handle !== null) {
35
+ await handle.close();
36
+ }
37
+ }
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,24 @@
1
+ // @ts-nocheck
2
+ // Type declarations recovered from dist/types (no runtime body)
3
+ /**
4
+ * Sinks barrel — the terminal consumers of a {@link Signal} stream.
5
+ *
6
+ * Three concrete {@link Sink} factories, each of which drains an independent
7
+ * {@link SignalStream} obtained from the recorder's channel:
8
+ *
9
+ * - {@link createConsoleSink} — one human line per signal to a TTY / writer;
10
+ * - {@link createFileSink} — append-only NDJSON to a path (durable);
11
+ * - {@link createStreamSink} — NDJSON forwarded to an injected byte writer;
12
+ * - {@link createCollectorSink} — an in-memory array (tests / inspection).
13
+ *
14
+ * All four are framework-agnostic and share the record format in `serialize.ts`.
15
+ */
16
+ export { createConsoleSink, type ConsoleSinkOptions, type LineWriter } from "./console.js";
17
+ export { createFileSink, type FileSinkOptions } from "./file.js";
18
+ export {
19
+ createStreamSink,
20
+ createCollectorSink,
21
+ type StreamSinkOptions,
22
+ type SignalWriter,
23
+ type CollectorSink,
24
+ } from "./stream.js";
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Stream sink — forwards signals to a network/byte stream, or collects them.
3
+ */
4
+ import type { Signal, Sink } from "../contract.js";
5
+ import { encodeRecord } from "../serialize.js";
6
+
7
+ export interface SignalWriter {
8
+ write(line: string): unknown | Promise<unknown>;
9
+ end?(): unknown | Promise<unknown>;
10
+ }
11
+
12
+ export interface StreamSinkOptions {
13
+ readonly writer: SignalWriter;
14
+ readonly id?: string;
15
+ readonly endOnFinish?: boolean;
16
+ }
17
+
18
+ export function createStreamSink(options: StreamSinkOptions): Sink {
19
+ const id = options.id ?? "stream";
20
+ const endOnFinish = options.endOnFinish ?? true;
21
+ return {
22
+ id,
23
+ async drain(stream) {
24
+ try {
25
+ for await (const signal of stream) {
26
+ await options.writer.write(encodeRecord(signal));
27
+ }
28
+ } finally {
29
+ if (endOnFinish && options.writer.end !== undefined) {
30
+ await options.writer.end();
31
+ }
32
+ }
33
+ },
34
+ };
35
+ }
36
+
37
+ export interface CollectorSink extends Sink {
38
+ readonly signals: readonly Signal[];
39
+ }
40
+
41
+ export function createCollectorSink(id: string = "collector"): CollectorSink {
42
+ const signals: Signal[] = [];
43
+ return {
44
+ id,
45
+ get signals(): readonly Signal[] {
46
+ return signals;
47
+ },
48
+ async drain(stream) {
49
+ for await (const signal of stream) {
50
+ signals.push(signal);
51
+ }
52
+ },
53
+ };
54
+ }