@aiwg/cli 2026.10.0 → 2026.10.3

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 (168) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +270 -0
  2. package/agentic/code/providers/model-capabilities.v1.json +3091 -24
  3. package/bin/aiwg.mjs +86 -21
  4. package/dist/src/a2a/codecs.js +9 -0
  5. package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
  6. package/dist/src/artifacts/repair.js +9 -4
  7. package/dist/src/channel/manager.mjs +59 -10
  8. package/dist/src/cli/child-process-result.js +40 -0
  9. package/dist/src/cli/env.js +38 -16
  10. package/dist/src/cli/flag-parsing.js +236 -0
  11. package/dist/src/cli/handlers/decision.js +297 -2
  12. package/dist/src/cli/handlers/diagnose.js +9 -2
  13. package/dist/src/cli/handlers/feedback.js +14 -5
  14. package/dist/src/cli/handlers/init.js +245 -30
  15. package/dist/src/cli/handlers/mc.js +23 -0
  16. package/dist/src/cli/handlers/models.js +69 -6
  17. package/dist/src/cli/handlers/ralph.js +3 -0
  18. package/dist/src/cli/handlers/refresh.js +16 -0
  19. package/dist/src/cli/handlers/regenerate.js +14 -0
  20. package/dist/src/cli/handlers/runtime-info.js +24 -9
  21. package/dist/src/cli/handlers/script-runner.js +16 -6
  22. package/dist/src/cli/handlers/session.js +396 -62
  23. package/dist/src/cli/handlers/sessions.js +146 -40
  24. package/dist/src/cli/handlers/setup-manifest.js +439 -39
  25. package/dist/src/cli/handlers/setup.js +139 -39
  26. package/dist/src/cli/handlers/subcommands.js +3 -1
  27. package/dist/src/cli/handlers/team.js +18 -8
  28. package/dist/src/cli/handlers/use.js +13 -0
  29. package/dist/src/cli/handlers/utilities.js +106 -23
  30. package/dist/src/cli/prompt-utils.js +72 -0
  31. package/dist/src/cli/provider-resolution.js +11 -0
  32. package/dist/src/config/aiwg-config.js +268 -4
  33. package/dist/src/config/cli.js +146 -19
  34. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  35. package/dist/src/config/project-artifacts-health.mjs +14 -3
  36. package/dist/src/config/user-config.js +36 -10
  37. package/dist/src/decision/adapters/jev.js +80 -28
  38. package/dist/src/decision/ask.js +352 -0
  39. package/dist/src/decision/driver.js +4 -0
  40. package/dist/src/decision/entry.js +38 -6
  41. package/dist/src/decision/heldout/collector.js +10 -1
  42. package/dist/src/decision/index.js +3 -0
  43. package/dist/src/decision/interop.js +60 -8
  44. package/dist/src/decision/jev-credentials.js +121 -0
  45. package/dist/src/decision/ledger.js +839 -0
  46. package/dist/src/decision/validate.js +6 -3
  47. package/dist/src/effects/dispatch-verifier.js +252 -0
  48. package/dist/src/effects/index.js +1 -0
  49. package/dist/src/features/installer.js +2 -2
  50. package/dist/src/flow/graph-metadata.js +5 -0
  51. package/dist/src/gates/providers/loader.js +3 -1
  52. package/dist/src/installation/manager-command.mjs +1 -1
  53. package/dist/src/installation/manager.mjs +17 -2
  54. package/dist/src/lint/runner.js +2 -2
  55. package/dist/src/mcp/adapters/codex-runtime.js +360 -56
  56. package/dist/src/mcp/cli.mjs +587 -77
  57. package/dist/src/mcp/config-file.mjs +279 -0
  58. package/dist/src/mcp/config-layers.mjs +348 -0
  59. package/dist/src/mcp/credentials.mjs +484 -0
  60. package/dist/src/mcp/grok-build-config.mjs +16 -26
  61. package/dist/src/mcp/helpers.mjs +106 -2
  62. package/dist/src/mcp/omp-config.mjs +50 -29
  63. package/dist/src/mcp/profiles.js +97 -23
  64. package/dist/src/mcp/profiles.mjs +99 -27
  65. package/dist/src/mcp/registry.js +264 -57
  66. package/dist/src/mcp/registry.mjs +198 -45
  67. package/dist/src/mcp/server.mjs +403 -20
  68. package/dist/src/mcp/toml-string.mjs +9 -0
  69. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  70. package/dist/src/mcp/tool-filters.mjs +357 -0
  71. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  72. package/dist/src/mcp/tools/decision.mjs +58 -0
  73. package/dist/src/mcp/tools/orchestration.mjs +38 -0
  74. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  75. package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
  76. package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
  77. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  78. package/dist/src/memory/project-registry.js +2 -2
  79. package/dist/src/mission-protocol/codecs.js +639 -1
  80. package/dist/src/mission-protocol/types.js +110 -0
  81. package/dist/src/mission-protocol/worker-status.js +2 -0
  82. package/dist/src/models/config-loader.js +70 -13
  83. package/dist/src/models/grok-build-models.js +93 -41
  84. package/dist/src/models/model-capabilities.v1.json +3091 -24
  85. package/dist/src/models/model-discovery.js +862 -63
  86. package/dist/src/models/provider-policy.js +625 -11
  87. package/dist/src/models/qualification-digest.mjs +17 -0
  88. package/dist/src/models/resolver.js +259 -0
  89. package/dist/src/models/worker-direct-readiness.js +529 -0
  90. package/dist/src/models/worker-dispatch-offer.js +169 -0
  91. package/dist/src/models/worker-health-readiness.js +131 -0
  92. package/dist/src/models/worker-installation.mjs +879 -0
  93. package/dist/src/models/wrapper-route.js +18 -0
  94. package/dist/src/ops/registry.js +9 -14
  95. package/dist/src/plugins/standalone-packager.js +4 -3
  96. package/dist/src/process/bounded-child-process.mjs +387 -0
  97. package/dist/src/process/cli-command.mjs +75 -0
  98. package/dist/src/providers/capability-matrix.js +8 -0
  99. package/dist/src/providers/capability-matrix.yaml +270 -0
  100. package/dist/src/providers/grok-build-headless.js +43 -21
  101. package/dist/src/providers/provider-definitions.js +9 -1
  102. package/dist/src/providers/provider-definitions.mjs +9 -2
  103. package/dist/src/serve/executor-registry.js +16 -24
  104. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  105. package/dist/src/serve/mission-conductor.js +9 -0
  106. package/dist/src/serve/shared-host-scheduler.js +2 -0
  107. package/dist/src/serve/worker-invocation.js +538 -0
  108. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  109. package/dist/src/sessions/adapter-registry.js +45 -0
  110. package/dist/src/sessions/adapters/generic.js +37 -3
  111. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  112. package/dist/src/sessions/adapters/grokbot.js +423 -24
  113. package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
  114. package/dist/src/sessions/adapters/muse.js +68 -29
  115. package/dist/src/sessions/adapters/pi.js +1 -0
  116. package/dist/src/sessions/batch-contracts.js +13 -1
  117. package/dist/src/sessions/batch-import.js +23 -42
  118. package/dist/src/sessions/candidates.js +47 -11
  119. package/dist/src/sessions/contracts.js +35 -0
  120. package/dist/src/sessions/coverage-preflight.js +134 -0
  121. package/dist/src/sessions/discovery.js +3 -0
  122. package/dist/src/sessions/grok-build-cli.js +67 -5
  123. package/dist/src/sessions/importer.js +24 -8
  124. package/dist/src/sessions/index.js +2 -0
  125. package/dist/src/sessions/repository.js +54 -13
  126. package/dist/src/sessions/workspace-discovery.js +99 -37
  127. package/dist/src/skills/adapters/clawhub.js +10 -8
  128. package/dist/src/skills/adapters/openclaw.js +10 -7
  129. package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
  130. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  131. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  132. package/dist/src/smiths/mcpsmith/example.js +1 -1
  133. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
  134. package/dist/src/storage/backends/logseq.js +68 -7
  135. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  136. package/dist/src/testing/generators/test-code-generator.js +57 -30
  137. package/dist/src/uhp/client.js +11 -1
  138. package/dist/src/uhp/mission.js +5 -0
  139. package/dist/src/update/checker.mjs +5 -5
  140. package/dist/src/update/notifier.mjs +54 -2
  141. package/dist/src/writing/fidelity.js +18 -1
  142. package/package.json +18 -1
  143. package/schemas/mission-v1.schema.json +2429 -0
  144. package/schemas/models/model-catalog.v1.schema.json +301 -0
  145. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  146. package/schemas/models/model-policy.v1.schema.json +14 -0
  147. package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
  148. package/tools/_resolve-impl.mjs +39 -0
  149. package/tools/agents/deploy-agents.mjs +15 -2
  150. package/tools/agents/providers/base.mjs +63 -43
  151. package/tools/agents/providers/grokbot.mjs +11 -12
  152. package/tools/agents/providers/hermes.mjs +6 -3
  153. package/tools/agents/providers/omp.mjs +5 -2
  154. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  155. package/tools/providers/deepseek-harness-transport.mjs +358 -0
  156. package/tools/providers/omp-transport.mjs +192 -0
  157. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  158. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  159. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  160. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  161. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  162. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  163. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  164. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  165. package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
  166. package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
  167. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  168. package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
@@ -0,0 +1,439 @@
1
+ /** Bounded execution observations, separate from worker claims and final parsing. */
2
+ import { createHash } from 'node:crypto';
3
+ import { constants } from 'node:fs';
4
+ import { open } from 'node:fs/promises';
5
+ import { execFileSync } from 'node:child_process';
6
+ import path from 'node:path';
7
+
8
+ const digest = value => `sha256:${createHash('sha256').update(value).digest('hex')}`;
9
+ const MAX_EVENTS = 64;
10
+ const MAX_KEYS = 512;
11
+ // Bounded replay horizon beyond the short dedup FIFO. Only exact replay of
12
+ // evicted identities counts as churn; fresh identities reset the streak.
13
+ export const PROGRESS_CHURN_LIMITS = Object.freeze({ historyKeys: 4096, repeatedEvictedKeys: 32 });
14
+ const DEFAULTS = { startupSeconds: 60, checkSeconds: 15, quietSeconds: 90, stallSeconds: 300,
15
+ diagnosticGraceSeconds: 60, terminateGraceSeconds: 5, hardTimeoutSeconds: 1800, maxSteps: 100, stepEnforcement: 'unavailable' };
16
+ const reasons = new Set(['OK', 'AWAITING_HANDSHAKE', 'PROCESS_ONLY', 'ACCEPTED', 'PROGRESS', 'QUIET', 'NO_PROGRESS',
17
+ 'STARTUP_DEADLINE', 'HARD_DEADLINE', 'STEP_LIMIT', 'AUTH', 'RATE_LIMIT', 'NETWORK', 'MODEL', 'TRUST', 'STDIN_WAIT',
18
+ 'SANDBOX', 'NOT_FOUND', 'RESOURCE', 'PROVIDER_ERROR', 'CRASH', 'EXIT_ERROR', 'FINAL_INVALID', 'CANCELLED',
19
+ 'DISCONNECTED', 'OBSERVER_GAP', 'CAPTURE_WRITE', 'OUTPUT_LIMIT', 'MALFORMED_FRAME', 'SOURCE_GAP', 'IDENTITY_CHANGED']);
20
+ const reason = value => reasons.has(value) ? value : 'PROVIDER_ERROR';
21
+ const field = (event, key) => key.split('.').reduce((node, part) => node?.[part], event);
22
+
23
+ /** Missing vendor identities stay unavailable; an observed mismatch never crosses runs. */
24
+ export function healthEventMatchesBinding(event, binding, fields = ['invocationId', 'requestId', 'sessionId', 'runId']) {
25
+ if (!event || typeof event !== 'object' || Array.isArray(event)) return false;
26
+ const keys = { session_id: 'sessionId', thread_id: 'sessionId', sessionID: 'sessionId', id: 'sessionId',
27
+ 'stream.id': 'sessionId', 'payload.run_id': 'runId', run_id: 'runId', request_id: 'requestId' };
28
+ return fields.every(key => {
29
+ const actual = field(event, key);
30
+ const expected = binding[keys[key] ?? key];
31
+ if (actual !== undefined && expected != null && actual !== expected) {
32
+ binding.onIdentityConflict?.();
33
+ return false;
34
+ }
35
+ return true;
36
+ });
37
+ }
38
+
39
+ /** Heuristic diagnoses are never terminal proof; arbitrary stdout prose is ignored. */
40
+ export function classifyWorkerHealthFailure(value, { source = {}, provider = '' } = {}) {
41
+ const typed = value && typeof value === 'object' && !Array.isArray(value);
42
+ if (source.kind !== 'stderr' && !typed) return null;
43
+ const text = (typed ? JSON.stringify(value) : String(value)).slice(0, 8192);
44
+ let code = null;
45
+ if (/\b(?:ENOENT|EACCES)\b|command not found|no such executable/i.test(text)) code = 'NOT_FOUND';
46
+ else if (/\b(?:EPERM|EROFS)\b|read-only file system|operation not permitted/i.test(text)) code = 'SANDBOX';
47
+ else if (/\b429\b|rate.limit|quota.exceeded/i.test(text)) code = 'RATE_LIMIT';
48
+ else if (/\b(?:401|403)\b|authentication (?:failed|expired)|unauthorized|invalid api key|login required/i.test(text)) code = 'AUTH';
49
+ else if (/model[_ -](?:not[_ -]found|unavailable)|unknown model|invalid (?:model|effort)/i.test(text)) code = 'MODEL';
50
+ else if (/Reading additional input from stdin|waiting for stdin|stdin.wait/i.test(text)) code = 'STDIN_WAIT';
51
+ else if (/trust (?:this )?(?:workspace|directory)|approval required|workspace (?:is )?untrusted/i.test(text)) code = 'TRUST';
52
+ else if (/\b(?:ENOSPC|ENOMEM|OOM)\b|out of memory|disk full/i.test(text)) code = 'RESOURCE';
53
+ else if (/\b(?:ECONNRESET|ENOTFOUND|ETIMEDOUT|EAI_AGAIN)\b|TLS (?:error|failed)|\b50[234]\b/i.test(text)) code = 'NETWORK';
54
+ else if (typed) code = 'PROVIDER_ERROR';
55
+ if (!code) return null;
56
+ const retry = /retry.after["\s:=]+(\d+(?:\.\d+)?)/i.exec(text);
57
+ return { kind: 'error', reasonCode: code, provider,
58
+ ...(retry ? { retryAfterSeconds: Math.min(86400, Number(retry[1])) } : {}) };
59
+ }
60
+
61
+ export function observeProviderHealthEvent(provider, event, source, binding) {
62
+ if (!healthEventMatchesBinding(event, binding)) return null;
63
+ if (provider === 'hermes') {
64
+ // Only an explicitly located, session-filtered log is qualified. Plain -z
65
+ // stdout/usage and agent.log growth cannot establish semantic progress.
66
+ if (source.kind !== 'session-log' || !binding.sessionId || event.session_id !== binding.sessionId) return null;
67
+ if (event.marker === 'turn_started') return { kind: 'accepted' };
68
+ if (event.marker === 'tool_completed' && typeof event.tool_call_id === 'string') return { kind: 'progress', key: event.tool_call_id };
69
+ if (event.marker === 'turn_completed') return { kind: 'terminal', success: true };
70
+ } else if (provider === 'antigravity') {
71
+ if (event.event === 'init') return { kind: 'setup', sessionId: event.session_id };
72
+ if (event.event === 'step_update' && event.step_update?.state === 'DONE' && Number.isSafeInteger(event.step_update.step_index)) return { kind: 'progress', key: `step:${event.step_update.step_index}` };
73
+ if (event.event === 'result') return { kind: 'terminal', success: event.result?.status === 'SUCCESS' };
74
+ if (event.event === 'error') return classifyWorkerHealthFailure(event, { source, provider });
75
+ } else if (provider === 'grok-build') {
76
+ // streaming-json is ACP NDJSON, not the Messages API stream. Missing
77
+ // identities stay unknown; a generic tool id is not a session binding.
78
+ if (source?.kind !== 'stdout') return null;
79
+ if (event.type === 'error') return { ...classifyWorkerHealthFailure({ type: 'error', message: event.message }, { source, provider }), fatal: true };
80
+ if (event.type === 'tool_call' || event.type === 'tool_call_update') {
81
+ if (event.status === 'failed') return { ...classifyWorkerHealthFailure({ type: 'error', message: `tool call ${event.toolCallId ?? 'unknown'} failed` }, { source, provider }), fatal: true };
82
+ return typeof event.toolCallId === 'string' && event.toolCallId
83
+ ? { kind: 'progress', key: event.toolCallId } : { kind: 'activity' };
84
+ }
85
+ if (['text', 'thought', 'usage', 'plan', 'available_commands'].includes(event.type)) return { kind: 'activity' };
86
+ if (event.type === 'end') return { kind: 'terminal', success: event.stopReason === 'end_turn', key: event.stopReason ?? 'end' };
87
+ } else if (provider === 'local-http' || provider === 'ollama') {
88
+ if (typeof event.response === 'string' && event.done !== true) return { kind: 'activity' };
89
+ if (event.done === true) return { kind: 'terminal', success: !event.error };
90
+ if (event.error) return { ...classifyWorkerHealthFailure(event, { source, provider }), fatal: true };
91
+ } else if (provider === 'copilot') {
92
+ // CONSERVATIVE GUARD ONLY (roctinam/aiwg#2901 round 1): Copilot's own
93
+ // JSONL event vocabulary is not documented in --help (see
94
+ // copilot-adapter.mjs module doc) and is never invented here. This
95
+ // branch defends only against the synthetic "error-shaped JSON at exit
96
+ // 0" shape — `type: "error"` or a top-level non-null `error` field — so
97
+ // it can never be mistaken for a completed/OK final. Mirrors
98
+ // copilot-adapter.mjs's parseOutput guard.
99
+ if (event.type === 'error' || (event.error !== undefined && event.error !== null)) {
100
+ return { ...classifyWorkerHealthFailure(event, { source, provider }), fatal: true };
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+
106
+ /** Parser/locator strings are identifiers only. No dynamic import/eval from profiles. */
107
+ export function createWorkerHealthObserver(options = {}) {
108
+ const profile = options.profile?.health ?? options.health ?? {};
109
+ const windows = { ...DEFAULTS, ...options.effectiveHealth };
110
+ const now = options.now ?? (() => Number(process.hrtime.bigint()) / 1e6);
111
+ const wallNow = options.wallNow ?? Date.now;
112
+ const started = options.startedAt ?? now();
113
+ const epoch = options.launchTime === undefined ? wallNow() : Date.parse(options.launchTime);
114
+ const utc = value => value === null ? null : new Date(epoch + value - started).toISOString();
115
+ const hardDeadline = started + windows.hardTimeoutSeconds * 1000;
116
+ const startupDeadline = started + windows.startupSeconds * 1000;
117
+ const binding = { invocationId: options.invocationId ?? 'unbound', requestId: options.requestId ?? options.invocationId,
118
+ sessionId: options.sessionId ?? null, runId: options.runId ?? null };
119
+ const normalize = options.normalizeEvent ?? ((event, source) => observeProviderHealthEvent(options.provider, event, source, binding));
120
+ const buffers = new Map();
121
+ const keys = new Set();
122
+ const progressHistory = new Set();
123
+ const replayedKeys = new Set();
124
+ const events = [];
125
+ const gaps = [];
126
+ let sequence = 0, generation = 0, steps = null, total = 0, lastCheck = started, maxGap = 0;
127
+ let observed = null, activity = null, progress = null, heartbeat = null, handshakeAt = null, stalledAt = null;
128
+ let state = 'launching', handshake = 'unobserved', confidence = 'unobserved', reasonCode = 'AWAITING_HANDSHAKE';
129
+ let termination = 'running', transitionCount = 0, live = null, disconnected = false, finalized = false;
130
+ let terminal = null, fatalFailure = false, cpuTimeMs = null, contentDigest = options.contentDigest ?? null;
131
+ const unavailable = new Set(profile.handshake === 'semantic' ? [] : ['semantic-start']);
132
+ if (!profile.progressEvidence?.length) unavailable.add('semantic-progress');
133
+ unavailable.add('steps');
134
+ const transition = (next, code, certainty = 'observed') => {
135
+ if (state !== next) { transitionCount = Math.min(1000000, transitionCount + 1); state = next; }
136
+ reasonCode = reason(code); confidence = certainty;
137
+ };
138
+ const sourceOf = source => ({ kind: source?.kind ?? 'stdout', id: source?.id ?? source?.kind ?? 'stdout',
139
+ generation: source?.generation ?? 0, offset: source?.offset ?? 0 });
140
+ const gap = (code = 'OBSERVER_GAP', source = {}) => {
141
+ generation++;
142
+ gaps.push({ sequence: ++sequence, reasonCode: reason(code), source: digest(sourceOf(source).id) });
143
+ if (gaps.length > 32) gaps.shift();
144
+ unavailable.add('observer-continuity');
145
+ };
146
+ const observe = (event, suppliedSource = {}) => {
147
+ if (finalized || !event || typeof event !== 'object') return;
148
+ const source = sourceOf(suppliedSource);
149
+ if (suppliedSource.invocationId != null && suppliedSource.invocationId !== binding.invocationId) return;
150
+ if (!['setup', 'accepted', 'progress', 'activity', 'terminal', 'error', 'heartbeat'].includes(event.kind)) return;
151
+ if (['setup', 'accepted'].includes(event.kind)) {
152
+ // Stdout identities provide intra-run correlation only, never authority
153
+ // over a process or filesystem. The first binding wins, atomically;
154
+ // conflicting later setup is an anomaly and cannot rebind either field.
155
+ if (['sessionId', 'runId'].some(key => event[key] != null && binding[key] != null && binding[key] !== event[key])) {
156
+ gap('IDENTITY_CHANGED', source);
157
+ return;
158
+ }
159
+ for (const key of ['sessionId', 'runId']) {
160
+ if (event[key] != null && source.kind === 'stdout' && binding[key] == null) binding[key] = event[key];
161
+ }
162
+ }
163
+ const time = now();
164
+ observed = time;
165
+ if (time >= hardDeadline) { reasonCode = 'HARD_DEADLINE'; return; }
166
+ if (fatalFailure && event.kind !== 'error' && event.kind !== 'heartbeat') return;
167
+ const rawKey = event.key ?? event.identity;
168
+ const keyDigest = typeof rawKey === 'string' && rawKey.length <= 4096 ? digest(rawKey) : null;
169
+ if (event.kind === 'progress') {
170
+ if (!keyDigest) return;
171
+ const qualified = handshakeAt !== null && (profile.progressEvidence?.length || source.kind === 'worktree');
172
+ if (qualified) activity = time;
173
+ if (source.kind !== 'worktree' && qualified) {
174
+ if (unavailable.has('progress-key-churn')) return;
175
+ const replay = progressHistory.has(keyDigest);
176
+ if (!replay) replayedKeys.clear();
177
+ else if (!keys.has(keyDigest)) replayedKeys.add(keyDigest);
178
+ // Refresh history on replay so a cycling set within this explicit
179
+ // horizon cannot become novel by evicting its oldest identities.
180
+ progressHistory.delete(keyDigest);
181
+ if (progressHistory.size >= PROGRESS_CHURN_LIMITS.historyKeys) progressHistory.delete(progressHistory.values().next().value);
182
+ progressHistory.add(keyDigest);
183
+ if (replayedKeys.size >= PROGRESS_CHURN_LIMITS.repeatedEvictedKeys) {
184
+ unavailable.add('semantic-progress');
185
+ unavailable.add('progress-key-churn');
186
+ }
187
+ if (replay) {
188
+ if (!keys.has(keyDigest)) {
189
+ if (keys.size >= MAX_KEYS) keys.delete(keys.values().next().value);
190
+ keys.add(keyDigest);
191
+ }
192
+ return;
193
+ }
194
+ }
195
+ if (keys.has(keyDigest)) return;
196
+ // Bound dedup memory without dropping new progress after saturation.
197
+ // Set insertion order makes this a FIFO window of recent identities.
198
+ if (keys.size >= MAX_KEYS) keys.delete(keys.values().next().value);
199
+ keys.add(keyDigest);
200
+ }
201
+ events.push({ sequence: ++sequence, kind: event.kind, source: digest(source.id), generation: source.generation,
202
+ offset: source.offset, keyDigest });
203
+ if (events.length > MAX_EVENTS) events.shift();
204
+ if (event.kind === 'heartbeat') heartbeat = time;
205
+ else if (event.kind === 'accepted') {
206
+ if (handshakeAt === null && profile.handshake === 'semantic') {
207
+ if (time >= startupDeadline) transition('failed-to-start', 'STARTUP_DEADLINE', 'suspected');
208
+ else { handshakeAt = time; handshake = 'semantic'; live = true; transition('started', 'ACCEPTED'); }
209
+ }
210
+ } else if (event.kind === 'progress') {
211
+ // Completed steps do not retroactively invent accepted-start evidence.
212
+ if (handshakeAt !== null && (profile.progressEvidence?.length || source.kind === 'worktree')) {
213
+ progress = time; activity = time;
214
+ // Tool/item completion establishes progress, not a model-turn count.
215
+ if (event.step === true && windows.stepEnforcement !== 'unavailable') { steps = (steps ?? 0) + 1; unavailable.delete('steps'); }
216
+ stalledAt = null; transition('active', 'PROGRESS');
217
+ }
218
+ } else if (event.kind === 'activity') activity = time;
219
+ else if (event.kind === 'error') {
220
+ if (event.fatal === true) fatalFailure = true;
221
+ terminal = event.fatal === true ? false : terminal;
222
+ reasonCode = reason(event.reasonCode);
223
+ if (handshakeAt === null) transition('failed-to-start', reasonCode, 'suspected');
224
+ } else if (event.kind === 'terminal') {
225
+ if (event.success !== true) fatalFailure = true;
226
+ terminal = event.success === true && !fatalFailure;
227
+ if (event.success !== true) reasonCode = reason(event.reasonCode);
228
+ }
229
+ };
230
+ // Adapter binding filters report conflicts even when they reject a raw
231
+ // setup frame before normalization reaches observe(). No raw ID is exported.
232
+ binding.onIdentityConflict = () => gap('IDENTITY_CHANGED');
233
+ // Read-only status resumes qualified clocks, never replays output as fresh
234
+ // progress. The caller validates the invocation, boot and timestamp bounds.
235
+ if (options.initialSnapshot) {
236
+ const initial = options.initialSnapshot;
237
+ const clock = value => value == null ? null : started + Date.parse(value) - epoch;
238
+ observed = clock(initial.lastObservedAt); activity = clock(initial.lastActivityAt);
239
+ progress = clock(initial.lastProgressAt); handshakeAt = clock(initial.handshakeAt);
240
+ stalledAt = clock(initial.stalledAt);
241
+ handshake = initial.handshake ?? 'unobserved';
242
+ state = initial.state ?? 'launching'; reasonCode = reason(initial.reasonCode);
243
+ generation = initial.observerGeneration ?? 0; transitionCount = initial.transitionCount ?? 0;
244
+ maxGap = initial.maxCheckGapSeconds ?? 0;
245
+ for (const signal of initial.unavailableSignals ?? []) unavailable.add(signal);
246
+ if (observed !== null) lastCheck = observed;
247
+ }
248
+ const feed = (bytes, suppliedSource = {}) => {
249
+ if (finalized) return;
250
+ const source = sourceOf(suppliedSource);
251
+ if (suppliedSource.offset === undefined) source.offset = buffers.get(source.id)?.nextOffset ?? 0;
252
+ const input = Buffer.from(bytes);
253
+ total += input.length;
254
+ const maxBytes = Math.min(options.maxOutputBytes ?? 10485760, 10485760);
255
+ if (total > maxBytes) { gap('OUTPUT_LIMIT', source); return; }
256
+ if (source.kind !== 'session-log') { activity = now(); observed = activity; }
257
+ const previous = buffers.get(source.id);
258
+ if (previous && (previous.generation !== source.generation || source.offset < previous.nextOffset)) {
259
+ gap('SOURCE_GAP', source); buffers.delete(source.id);
260
+ }
261
+ if (!buffers.has(source.id) && buffers.size >= 8) { gap('SOURCE_GAP', source); return; }
262
+ let cursor = buffers.get(source.id);
263
+ if (!cursor) { cursor = { bytes: Buffer.alloc(0), generation: source.generation, nextOffset: source.offset, discarding: false }; buffers.set(source.id, cursor); }
264
+ if (source.offset > cursor.nextOffset) { gap('SOURCE_GAP', source); cursor.bytes = Buffer.alloc(0); }
265
+ cursor.nextOffset = source.offset + input.length;
266
+ // stderr diagnostics are line framed regardless of stdout framing.
267
+ const framing = profile.outputFraming ?? 'opaque';
268
+ if (source.kind !== 'stderr' && source.kind !== 'session-log' && !['jsonl', 'http-stream'].includes(framing)) return;
269
+ const maxFrame = Math.min(options.maxFrameBytes ?? 65536, 1048576);
270
+ for (let start = 0; start < input.length;) {
271
+ const newline = input.indexOf(10, start);
272
+ const end = newline < 0 ? input.length : newline;
273
+ const piece = input.subarray(start, end);
274
+ if (!cursor.discarding && cursor.bytes.length + piece.length > maxFrame) {
275
+ cursor.bytes = Buffer.alloc(0); cursor.discarding = true; gap('OUTPUT_LIMIT', source);
276
+ }
277
+ if (!cursor.discarding) cursor.bytes = Buffer.concat([cursor.bytes, piece]);
278
+ if (newline >= 0) {
279
+ if (!cursor.discarding && cursor.bytes.length) {
280
+ try {
281
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(cursor.bytes);
282
+ if (source.kind === 'stderr') observe(classifyWorkerHealthFailure(text, { source, provider: options.provider }), source);
283
+ else {
284
+ const event = JSON.parse(text);
285
+ const normalized = normalize(event, source, binding);
286
+ for (const item of Array.isArray(normalized) ? normalized : [normalized]) observe(item, { ...source, offset: source.offset + newline });
287
+ }
288
+ } catch { gap('MALFORMED_FRAME', source); }
289
+ }
290
+ cursor.bytes = Buffer.alloc(0); cursor.discarding = false;
291
+ }
292
+ start = end + 1;
293
+ }
294
+ };
295
+ const check = (time = now()) => {
296
+ if (time < lastCheck) { gap('OBSERVER_GAP'); time = lastCheck; }
297
+ const elapsed = time - lastCheck;
298
+ maxGap = Math.max(maxGap, elapsed / 1000);
299
+ if (elapsed > windows.checkSeconds * 1000) gap('OBSERVER_GAP');
300
+ lastCheck = time; observed = time;
301
+ if (finalized) return snapshot();
302
+ if (time >= hardDeadline) { reasonCode = 'HARD_DEADLINE'; return snapshot(); }
303
+ if (fatalFailure) return snapshot();
304
+ if (handshakeAt === null) {
305
+ if (time >= startupDeadline) transition('failed-to-start', reasonCode === 'AWAITING_HANDSHAKE' ? 'STARTUP_DEADLINE' : reasonCode, 'suspected');
306
+ } else {
307
+ const noProgress = time - (progress ?? handshakeAt);
308
+ if (noProgress >= windows.stallSeconds * 1000) {
309
+ stalledAt ??= (progress ?? handshakeAt) + windows.stallSeconds * 1000;
310
+ if (time - stalledAt >= windows.diagnosticGraceSeconds * 1000 && live === true && !disconnected) transition('hung', 'NO_PROGRESS');
311
+ else transition('stalled', disconnected ? 'DISCONNECTED' : 'NO_PROGRESS', unavailable.has('semantic-progress') || disconnected ? 'suspected' : 'observed');
312
+ } else if (time - (activity ?? handshakeAt) >= windows.quietSeconds * 1000) transition('quiet', 'QUIET', 'suspected');
313
+ else if (progress !== null) transition('active', 'PROGRESS');
314
+ }
315
+ if (windows.stepEnforcement !== 'unavailable' && steps !== null && steps >= windows.maxSteps) reasonCode = 'STEP_LIMIT';
316
+ return snapshot();
317
+ };
318
+ const snapshot = () => ({ version: 1, state,
319
+ coverage: profile.coverage === 'unobserved' ? 'unobserved' : profile.coverage === 'full' && !gaps.length && !unavailable.has('progress-key-churn') ? 'full' : 'degraded',
320
+ handshake, lastObservedAt: utc(observed), lastActivityAt: utc(activity), lastProgressAt: utc(progress), reasonCode,
321
+ termination, transitionCount, restartCount: 0, unavailableSignals: [...unavailable].slice(0, 32), transitionRefs: [],
322
+ observerGeneration: generation, startupDeadlineAt: utc(startupDeadline), hardDeadlineAt: utc(hardDeadline),
323
+ maxCheckGapSeconds: maxGap, confidence, handshakeAt: utc(handshakeAt), stalledAt: utc(stalledAt) });
324
+ return {
325
+ feed, observe, gap, check, snapshot,
326
+ shouldStop() { return fatalFailure; },
327
+ processStarted(identity) {
328
+ if (options.transport === 'mcp') return; // shared server process is not this request
329
+ if (!identity || !Number.isSafeInteger(identity.pid) || !identity.startTicks || !identity.executable) return;
330
+ live = true; observed = now();
331
+ if (profile.handshake === 'process-only' && handshakeAt === null) {
332
+ if (now() >= startupDeadline) transition('failed-to-start', 'STARTUP_DEADLINE', 'suspected');
333
+ else { handshakeAt = now(); handshake = 'process-only'; transition('started', 'PROCESS_ONLY'); }
334
+ }
335
+ },
336
+ sampleActivity({ running = null, cpu = null, content = null } = {}) {
337
+ if (finalized) return;
338
+ observed = now(); live = running;
339
+ if (options.transport !== 'mcp' && cpu !== null && Number.isFinite(cpu) && cpu >= 0) {
340
+ if (cpuTimeMs !== null && cpu > cpuTimeMs) activity = now();
341
+ cpuTimeMs = cpu;
342
+ } else unavailable.add('cpu');
343
+ if (content !== null) {
344
+ if (contentDigest !== null && contentDigest !== content) observe({ kind: 'progress', key: `content:${sequence}:${content}` }, { kind: 'worktree', id: 'owned-worktree' });
345
+ contentDigest = content;
346
+ } else unavailable.add('worktree-content');
347
+ },
348
+ disconnect() { disconnected = true; live = null; termination = 'uncertain'; gap('DISCONNECTED'); reasonCode = 'DISCONNECTED'; },
349
+ finish(result = {}) {
350
+ if (finalized) return snapshot();
351
+ for (const [id, cursor] of buffers) if (cursor.bytes.length || cursor.discarding) gap('MALFORMED_FRAME', { id });
352
+ finalized = true; observed = now(); termination = result.termination ?? 'uncertain';
353
+ if (termination !== 'confirmed' || disconnected) {
354
+ termination = 'uncertain';
355
+ if (result.signal || result.exitCode != null && result.exitCode !== 0) transition(handshakeAt === null ? 'failed-to-start' : 'crashed', 'CRASH');
356
+ } else if (result.cancelled || result.timedOut || now() >= hardDeadline) transition(result.cancelled ? 'cancelled' : 'failed', result.cancelled ? 'CANCELLED' : reason(result.stopReason ?? 'HARD_DEADLINE'));
357
+ else if (state === 'failed-to-start') transition('failed', reasonCode);
358
+ else if (fatalFailure) transition('failed', reasonCode);
359
+ else if (!result.errorCode && result.finalValid && result.exitCode === 0 && terminal !== false) transition('completed', 'OK');
360
+ else if (!result.errorCode && result.finalValid && options.transport === 'mcp' && result.exitCode === null && terminal === true) transition('completed', 'OK');
361
+ else transition(handshakeAt === null ? 'failed-to-start' : 'failed', result.errorCode ? reason(result.errorCode) : result.exitCode !== 0 ? 'EXIT_ERROR' : 'FINAL_INVALID');
362
+ return snapshot();
363
+ },
364
+ nextDelayMs() {
365
+ const time = now();
366
+ const pending = [time + windows.checkSeconds * 1000, hardDeadline];
367
+ if (handshakeAt === null && state !== 'failed-to-start') pending.push(startupDeadline);
368
+ if (handshakeAt !== null) {
369
+ const quiet = (activity ?? handshakeAt) + windows.quietSeconds * 1000;
370
+ const stall = (progress ?? handshakeAt) + windows.stallSeconds * 1000;
371
+ if (quiet > time) pending.push(quiet);
372
+ if (stall > time) pending.push(stall);
373
+ if (stalledAt !== null && stalledAt + windows.diagnosticGraceSeconds * 1000 > time) pending.push(stalledAt + windows.diagnosticGraceSeconds * 1000);
374
+ }
375
+ return Math.max(1, Math.min(...pending) - time);
376
+ },
377
+ evidence() { return { invocationId: binding.invocationId, sequence, observerGeneration: generation,
378
+ lastWorkerHeartbeatAt: utc(heartbeat), cpuTimeMs, steps: windows.stepEnforcement === 'unavailable' ? null : steps,
379
+ gaps: gaps.map(item => ({ ...item })), events: events.map(item => ({ ...item })) }; },
380
+ };
381
+ }
382
+
383
+ /** Same-file edits matter. Export only a digest of bounded, owned content. */
384
+ export async function sampleOwnedWorktreeContent(root, { files, maxFiles = 128, maxBytes = 1048576 } = {}) {
385
+ let candidates = files;
386
+ try {
387
+ if (!candidates) {
388
+ const status = execFileSync('git', ['--no-optional-locks', '-c', 'core.fsmonitor=false', '-c', 'core.untrackedCache=false',
389
+ '-C', root, 'status', '--porcelain=v1', '-z', '--untracked-files=all'],
390
+ { timeout: 1000, maxBuffer: 65536, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: { ...process.env, GIT_OPTIONAL_LOCKS: '0', GIT_CONFIG_COUNT: '0' } });
391
+ candidates = [];
392
+ const records = status.split('\0');
393
+ for (let i = 0; i < records.length; i++) {
394
+ if (!records[i]) continue;
395
+ if (records[i][0] === 'R' || records[i][1] === 'R' || records[i][0] === 'C') return null;
396
+ candidates.push(records[i].slice(3));
397
+ }
398
+ }
399
+ if (!Array.isArray(candidates) || candidates.length > maxFiles) return null;
400
+ const hash = createHash('sha256');
401
+ let total = 0;
402
+ for (const name of [...new Set(candidates)].sort()) {
403
+ if (typeof name !== 'string' || path.isAbsolute(name) || name.split('/').some(part => part === '..' || !part)
404
+ || name.includes('\0')) return null;
405
+ if (name.split('/').some(part => ['.git', 'node_modules', '.cache', '__pycache__', '.aiwg'].includes(part)) || /\.(?:log|jsonl)$/.test(name)) continue;
406
+ if (!path.isAbsolute(root) || root.includes('\0') || root.split('/').includes('..')) return null;
407
+ let directory = await open('/', constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
408
+ let handle;
409
+ try {
410
+ for (const component of root.split('/').filter(Boolean)) {
411
+ const next = await open(`/proc/self/fd/${directory.fd}/${component}`, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
412
+ await directory.close(); directory = next;
413
+ }
414
+ const parts = name.split('/');
415
+ const leaf = parts.pop();
416
+ for (const component of parts) {
417
+ const next = await open(`/proc/self/fd/${directory.fd}/${component}`, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
418
+ await directory.close(); directory = next;
419
+ }
420
+ handle = await open(`/proc/self/fd/${directory.fd}/${leaf}`, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
421
+ const stat = await handle.stat();
422
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size + total > maxBytes) return null;
423
+ const buffer = Buffer.alloc(maxBytes - total + 1);
424
+ let length = 0;
425
+ while (length < buffer.length) {
426
+ const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
427
+ if (!bytesRead) break;
428
+ length += bytesRead;
429
+ }
430
+ const bytes = buffer.subarray(0, length);
431
+ total += length;
432
+ if (total > maxBytes) return null;
433
+ hash.update(name); hash.update('\0'); hash.update(bytes); hash.update('\0');
434
+ } catch (error) { if (error.code !== 'ENOENT') return null; hash.update(name); hash.update(':deleted\0'); }
435
+ finally { await handle?.close(); await directory.close(); }
436
+ }
437
+ return `sha256:${hash.digest('hex')}`;
438
+ } catch { return null; }
439
+ }