@aiwg/cli 2026.10.9 → 2026.10.11

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 (116) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +275 -17
  2. package/bin/aiwg.mjs +8 -0
  3. package/dist/src/cli/handlers/mc-admission.js +39 -0
  4. package/dist/src/cli/handlers/mc.js +2 -31
  5. package/dist/src/cli/handlers/models-help.js +52 -0
  6. package/dist/src/cli/handlers/models.js +11 -81
  7. package/dist/src/cli/handlers/sessions.js +198 -15
  8. package/dist/src/cli/handlers/use-addon-profile.js +72 -0
  9. package/dist/src/cli/handlers/use.js +203 -234
  10. package/dist/src/cli/ui.js +1 -0
  11. package/dist/src/config/aiwg-config.js +38 -184
  12. package/dist/src/config/cli-help.js +54 -0
  13. package/dist/src/config/cli.js +5 -69
  14. package/dist/src/config/config-normalization.js +10 -0
  15. package/dist/src/config/worker-activation-gate.js +26 -0
  16. package/dist/src/config/worker-adoption-cli.js +102 -0
  17. package/dist/src/config/worker-adoption.js +368 -0
  18. package/dist/src/config/worker-config-digest.js +14 -0
  19. package/dist/src/config/worker-context.js +337 -0
  20. package/dist/src/config/worker-group-templates.js +150 -0
  21. package/dist/src/config/worker-guidance-warning.js +28 -0
  22. package/dist/src/config/worker-owner-lock.js +152 -0
  23. package/dist/src/config/worker-parallelism.js +154 -0
  24. package/dist/src/config/worker-write.js +75 -0
  25. package/dist/src/decision/telemetry/exporter.js +2 -1
  26. package/dist/src/decision/telemetry/index.js +1 -0
  27. package/dist/src/decision/telemetry/mapping.js +43 -0
  28. package/dist/src/decision/telemetry/otlp-http.js +5 -2
  29. package/dist/src/decision/telemetry/redaction.js +6 -1
  30. package/dist/src/decision/telemetry/types.js +3 -0
  31. package/dist/src/decision/telemetry/worker.js +69 -0
  32. package/dist/src/lint/rule-activation.js +15 -7
  33. package/dist/src/lint/runner.js +2 -2
  34. package/dist/src/mcp/server.mjs +2 -11
  35. package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
  36. package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
  37. package/dist/src/mcp/tools/worker-invoke.mjs +114 -16
  38. package/dist/src/mission-protocol/codecs.js +8 -38
  39. package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
  40. package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
  41. package/dist/src/models/conductor-data-policy.d.mts +8 -0
  42. package/dist/src/models/conductor-data-policy.mjs +190 -0
  43. package/dist/src/models/config-loader.js +19 -3
  44. package/dist/src/models/config-writer.js +27 -0
  45. package/dist/src/models/model-capabilities.v1.json +275 -17
  46. package/dist/src/models/provider-policy.js +14 -155
  47. package/dist/src/models/resolver.js +2 -293
  48. package/dist/src/models/worker-artifact-binding.mjs +12 -0
  49. package/dist/src/models/worker-direct-readiness-policy.js +155 -0
  50. package/dist/src/models/worker-direct-readiness.js +9 -218
  51. package/dist/src/models/worker-health-readiness.js +105 -26
  52. package/dist/src/models/worker-installation.mjs +12 -14
  53. package/dist/src/models/worker-member-references.js +64 -0
  54. package/dist/src/models/worker-plan.js +317 -0
  55. package/dist/src/models/worker-selection-policy.js +217 -0
  56. package/dist/src/models/wrapper-deployment.js +1 -1
  57. package/dist/src/policy/authorization.js +29 -1
  58. package/dist/src/process/aiwg-probe-command.d.mts +15 -0
  59. package/dist/src/process/aiwg-probe-command.mjs +37 -0
  60. package/dist/src/serve/shared-host-admission-checks.js +116 -0
  61. package/dist/src/serve/shared-host-scheduler.js +25 -107
  62. package/dist/src/serve/shared-host-starvation.js +73 -0
  63. package/dist/src/serve/worker-artifacts.d.mts +4 -0
  64. package/dist/src/serve/worker-artifacts.mjs +56 -0
  65. package/dist/src/serve/worker-command-request.js +68 -0
  66. package/dist/src/serve/worker-invocation-paths.js +144 -0
  67. package/dist/src/serve/worker-invocation-preparation.js +169 -0
  68. package/dist/src/serve/worker-invocation.js +24 -291
  69. package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
  70. package/dist/src/serve/worker-stdin-frame.mjs +6 -6
  71. package/dist/src/sessions/adapters/grokbot.js +7 -0
  72. package/dist/src/sessions/batch-import.js +34 -6
  73. package/dist/src/sessions/contracts.js +19 -0
  74. package/dist/src/sessions/coverage-preflight.js +7 -1
  75. package/dist/src/sessions/fortemi-export-mapping.js +33 -3
  76. package/dist/src/sessions/host-catalog.js +157 -0
  77. package/dist/src/sessions/image-blobs.js +141 -0
  78. package/dist/src/sessions/importer.js +23 -3
  79. package/dist/src/sessions/index.js +2 -0
  80. package/dist/src/sessions/refresh.js +25 -2
  81. package/dist/src/sessions/repository.js +164 -22
  82. package/dist/src/sessions/workspace-discovery.js +5 -2
  83. package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
  84. package/package.json +15 -1
  85. package/schemas/mission-v1.schema.json +20 -19
  86. package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
  87. package/tools/agents/providers/claude-post-deploy.mjs +35 -0
  88. package/tools/agents/providers/claude.mjs +3 -30
  89. package/tools/agents/providers/codex-post-deploy.mjs +13 -0
  90. package/tools/agents/providers/codex.mjs +2 -8
  91. package/tools/agents/providers/conductor-data-guard.mjs +61 -0
  92. package/tools/agents/providers/rule-delivery.mjs +3 -16
  93. package/tools/agents/providers/rule-presentation.mjs +38 -0
  94. package/tools/plugin/package-plugins.mjs +1 -1
  95. package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
  96. package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
  97. package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
  98. package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
  99. package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
  100. package/tools/ralph-external/lib/session-attempt.mjs +85 -0
  101. package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
  102. package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
  103. package/tools/ralph-external/lib/worker-context.mjs +237 -0
  104. package/tools/ralph-external/lib/worker-final.mjs +143 -0
  105. package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
  106. package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
  107. package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
  108. package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
  109. package/tools/ralph-external/lib/worker-isolation.mjs +19 -73
  110. package/tools/ralph-external/lib/worker-launch-cli.mjs +49 -0
  111. package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
  112. package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
  113. package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
  114. package/tools/ralph-external/lib/worker-termination.mjs +82 -0
  115. package/tools/ralph-external/session-launcher.mjs +124 -313
  116. package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
@@ -0,0 +1,154 @@
1
+ // Defines and validates direct worker parallelism configuration before use.
2
+ import Ajv from 'ajv';
3
+ import addFormats from 'ajv-formats';
4
+ import { readFileSync, existsSync } from 'node:fs';
5
+ import { dirname, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { PROVIDER_IDS } from '../providers/provider-definitions.js';
8
+ import { isCanonicalWorkerModelKey, loadProviderModelCapabilities, resolveWorkerInstallation } from '../models/provider-policy.js';
9
+ import { resolveDispatchHostThresholds } from './dispatch-thresholds-runtime.mjs';
10
+ import { expandWorkerGroupTemplates } from './worker-group-templates.js';
11
+ let parallelismValidator;
12
+ /** Offline shape and declaration validation. Qualification is checked at use time. */
13
+ export function validateParallelismConfig(value) {
14
+ if (value === undefined)
15
+ return [];
16
+ const raw = value;
17
+ const expansion = expandWorkerGroupTemplates(value);
18
+ if (expansion.errors.length)
19
+ return expansion.errors;
20
+ value = expansion.parallelism;
21
+ if (!parallelismValidator) {
22
+ const here = dirname(fileURLToPath(import.meta.url));
23
+ const candidates = [resolve(here, '../../vscode-extension/schemas/aiwg.config.v1.json'), resolve(here, '../../../vscode-extension/schemas/aiwg.config.v1.json')];
24
+ const file = candidates.find(existsSync);
25
+ if (!file)
26
+ throw new Error('Parallelism schema is unavailable');
27
+ const schema = JSON.parse(readFileSync(file, 'utf8'));
28
+ const ajv = addFormats(new Ajv({ strict: true, allErrors: true }));
29
+ parallelismValidator = ajv.compile({ ...schema.properties.parallelism, definitions: schema.definitions });
30
+ }
31
+ if (!parallelismValidator(raw))
32
+ return (parallelismValidator.errors ?? []).map(error => `OG_CONFIG_REFERENCE /parallelism${error.instancePath}: ${error.message}`);
33
+ if (!parallelismValidator(value))
34
+ return (parallelismValidator.errors ?? []).map(error => `OG_CONFIG_REFERENCE /parallelism${error.instancePath}: ${error.message}`);
35
+ const policy = value;
36
+ const errors = [];
37
+ const error = (pointer, message) => { errors.push(`OG_CONFIG_REFERENCE /parallelism/${pointer}: ${message}`); };
38
+ const builtinIds = new Set(PROVIDER_IDS.filter(id => id !== 'generic'));
39
+ for (const [id, installation] of Object.entries(policy.clis ?? {})) {
40
+ if (installation.adapter.startsWith('providers:') && !builtinIds.has(installation.adapter.slice(10)))
41
+ error(`clis/${id}/adapter`, 'Unknown provider binding');
42
+ }
43
+ for (const [id, group] of Object.entries(policy.groups ?? {})) {
44
+ if (group.execution?.launchMode === 'native-sandbox'
45
+ && (!['read-only', 'workspace-write'].includes(group.execution.sandbox ?? '')
46
+ || (group.execution.transport ?? 'shell') !== 'shell')) {
47
+ error(`groups/${id}/execution/launchMode`, 'native-sandbox requires an enforcing read-only or workspace-write shell sandbox');
48
+ }
49
+ if (group.execution?.launchMode === 'native-sandbox') {
50
+ if (group.execution.approval === 'on-request')
51
+ error(`groups/${id}/execution/approval`, 'native-sandbox requires approval="never"');
52
+ if (group.execution.network === 'allowlisted')
53
+ error(`groups/${id}/execution/network`, 'native-sandbox requires network="deny"');
54
+ }
55
+ const ids = new Set();
56
+ for (const member of (group.members ?? [])) {
57
+ if (ids.has(member.id))
58
+ error(`groups/${id}/members`, 'Duplicate member ID');
59
+ ids.add(member.id);
60
+ if (!policy.clis?.[member.cli] && !builtinIds.has(member.cli))
61
+ error(`groups/${id}/members/${member.id}/cli`, 'Unknown installation');
62
+ if (group.execution?.launchMode === 'native-sandbox'
63
+ && (policy.clis?.[member.cli]?.adapter ?? `providers:${member.cli}`) !== 'providers:codex') {
64
+ error(`groups/${id}/members/${member.id}/cli`, 'native-sandbox requires a codex adapter');
65
+ }
66
+ }
67
+ for (const member of group.fallback ?? [])
68
+ if (!ids.has(member))
69
+ error(`groups/${id}/fallback`, 'Unknown fallback member');
70
+ for (const role of group.requiredRoles ?? []) {
71
+ if (!(group.members ?? []).some(m => m.enabled !== false && policy.clis?.[m.cli]?.enabled !== false && m.roles.includes(role)))
72
+ error(`groups/${id}/requiredRoles`, `No enabled member covers ${role}`);
73
+ }
74
+ }
75
+ const admission = policy.admission;
76
+ for (const group of Object.keys(admission?.groupCaps ?? {}))
77
+ if (!policy.groups?.[group])
78
+ error('admission/groupCaps', 'Unknown group');
79
+ for (const cli of Object.keys(admission?.cliCaps ?? {}))
80
+ if (!policy.clis?.[cli] && !builtinIds.has(cli))
81
+ error('admission/cliCaps', 'Unknown installation');
82
+ for (const key of Object.keys(admission?.memberCaps ?? {})) {
83
+ const [group, member, extra] = key.split('/');
84
+ if (extra !== undefined || !policy.groups?.[group]?.members?.some(m => m.id === member))
85
+ error('admission/memberCaps', 'Unknown group/member');
86
+ }
87
+ for (const key of Object.keys(admission?.modelCaps ?? {}))
88
+ if (!isCanonicalWorkerModelKey(key))
89
+ error('admission/modelCaps', 'Noncanonical inference model key');
90
+ if (admission?.hostResources !== undefined) {
91
+ try {
92
+ resolveDispatchHostThresholds(undefined, admission.hostResources);
93
+ }
94
+ catch (validation) {
95
+ error('admission/hostResources', validation instanceof Error ? validation.message : String(validation));
96
+ }
97
+ }
98
+ errors.push(...validateWorkerHealthConfig(policy.health));
99
+ return errors;
100
+ }
101
+ /** Cross-field health preferences are validated offline; controls are qualified at dispatch. */
102
+ export function validateWorkerHealthConfig(health) {
103
+ if (health === undefined)
104
+ return [];
105
+ const errors = [];
106
+ const error = (field, message) => { errors.push(`OG_CONFIG_REFERENCE /parallelism/health/${field}: ${message}`); };
107
+ const checkSeconds = health.checkSeconds ?? 15;
108
+ if (checkSeconds > (health.startupSeconds ?? 60))
109
+ error('checkSeconds', 'Must not exceed startupSeconds');
110
+ if ((health.quietSeconds ?? 90) >= (health.stallSeconds ?? 300))
111
+ error('quietSeconds', 'Must be less than stallSeconds');
112
+ if ((health.cooldownSeconds ?? 120) > (health.maxCooldownSeconds ?? 900))
113
+ error('cooldownSeconds', 'Must not exceed maxCooldownSeconds');
114
+ for (const role of Object.keys(health.roleScale ?? {})) {
115
+ if (!['efficiency', 'coding', 'reasoning'].includes(role))
116
+ error('roleScale', `Unknown role ${role}`);
117
+ }
118
+ for (const field of ['modelScale', 'modelStartupSeconds']) {
119
+ for (const [key, value] of Object.entries(health[field] ?? {})) {
120
+ if (!isCanonicalWorkerModelKey(key))
121
+ error(field, 'Noncanonical inference model key');
122
+ if (field === 'modelStartupSeconds' && checkSeconds > value)
123
+ error(field, 'Model startup must cover checkSeconds');
124
+ }
125
+ }
126
+ return errors;
127
+ }
128
+ /** Resolve one enabled installation through trusted metadata, without probes.
129
+ * Implicit canonical IDs inherit current-account auth; no account/quota identity
130
+ * is invented. Profile qualification remains the invocation owner's obligation.
131
+ */
132
+ export function resolveWorkerGroupInstallation(parallelism, installationId, registry = loadProviderModelCapabilities()) {
133
+ const errors = validateParallelismConfig(parallelism);
134
+ if (errors.length)
135
+ throw new Error(errors.join('; '));
136
+ if (!parallelism.worker_dispatch)
137
+ throw new Error('OG_CONFIG_REFERENCE /parallelism/worker_dispatch: Worker dispatch is disabled');
138
+ const explicit = parallelism.clis?.[installationId];
139
+ const installation = explicit
140
+ ? structuredClone(explicit)
141
+ : { adapter: `providers:${installationId}`, enabled: true };
142
+ if (!explicit && (!PROVIDER_IDS.includes(installationId) || installationId === 'generic')) {
143
+ throw new Error(`OG_CONFIG_REFERENCE /parallelism/clis/${installationId}: Unknown canonical installation`);
144
+ }
145
+ if (installation.enabled === false)
146
+ throw new Error(`OG_CONFIG_REFERENCE /parallelism/clis/${installationId}: Installation is disabled`);
147
+ try {
148
+ return { installation: { ...installation, enabled: true }, ...resolveWorkerInstallation(registry, installation) };
149
+ }
150
+ catch (error) {
151
+ throw new Error(`OG_ADAPTER_UNQUALIFIED /parallelism/clis/${installationId}: ${error}`);
152
+ }
153
+ }
154
+ //# sourceMappingURL=worker-parallelism.js.map
@@ -0,0 +1,75 @@
1
+ // Shares atomic replacement and serialization across worker configuration owners.
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ import { mkdir, writeFile, rename, unlink, stat } from 'node:fs/promises';
4
+ import { statSync, readFileSync } from 'node:fs';
5
+ import { randomBytes } from 'node:crypto';
6
+ import { dirname, join } from 'node:path';
7
+ import { acquireWorkerWriteLock, workerWriteLock } from './worker-owner-lock.js';
8
+ export { acquireWorkerWriteLock, reclaimWorkerWriteLock, workerWriteLock } from './worker-owner-lock.js';
9
+ const maintenance = new AsyncLocalStorage();
10
+ export const runWorkerMaintenance = (body) => maintenance.run(true, body);
11
+ export const isWorkerMaintenance = () => maintenance.getStore() === true;
12
+ export function workerDispatchBlocked(project) {
13
+ try {
14
+ statSync(join(workerWriteLock(project), 'blocked'));
15
+ return true;
16
+ }
17
+ catch (error) {
18
+ if (error.code === 'ENOENT')
19
+ return false;
20
+ throw error;
21
+ }
22
+ }
23
+ export async function atomicConfigWrite(file, content, assertOwned) {
24
+ assertOwned?.();
25
+ await mkdir(dirname(file), { recursive: true });
26
+ const temporary = `${file}.${randomBytes(6).toString('hex')}.tmp`;
27
+ let mode = 0o600;
28
+ try {
29
+ mode = (await stat(file)).mode;
30
+ }
31
+ catch (error) {
32
+ if (error.code !== 'ENOENT')
33
+ throw error;
34
+ }
35
+ try {
36
+ assertOwned?.();
37
+ await writeFile(temporary, content, { encoding: 'utf8', mode });
38
+ assertOwned?.();
39
+ await rename(temporary, file);
40
+ }
41
+ finally {
42
+ await unlink(temporary).catch(error => { if (error.code !== 'ENOENT')
43
+ throw error; });
44
+ }
45
+ }
46
+ export function workerRecoveryDiagnostic(project) {
47
+ const record = join(workerWriteLock(project), 'recovery.json');
48
+ let digest = 'sha256:<original-proposal-digest>';
49
+ try {
50
+ const retained = JSON.parse(readFileSync(record, 'utf8')).proposal?.digest;
51
+ if (typeof retained === 'string')
52
+ digest = retained;
53
+ }
54
+ catch {
55
+ try {
56
+ const retained = JSON.parse(readFileSync(join(workerWriteLock(project), 'owner.json'), 'utf8')).proposalDigest;
57
+ if (typeof retained === 'string')
58
+ digest = retained;
59
+ }
60
+ catch { /* Legacy or unreadable records require the retained review's digest. */ }
61
+ }
62
+ const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
63
+ return `Worker dispatch is closed for project ${project}; recovery record: ${record}. Recover with: aiwg config set --project --target ${quote(project)} --recover --authorize ${quote(digest)}`;
64
+ }
65
+ export async function withWorkerWriteLock(project, body) {
66
+ const lock = await acquireWorkerWriteLock(project);
67
+ try {
68
+ lock.assertOwned();
69
+ return await body();
70
+ }
71
+ finally {
72
+ await lock.release();
73
+ }
74
+ }
75
+ //# sourceMappingURL=worker-write.js.map
@@ -63,7 +63,8 @@ export class BoundedDecisionTraceExporter {
63
63
  await Promise.race([this.sink.export(trace, controller.signal), deadline]);
64
64
  }
65
65
  catch (error) {
66
- this.record(controller.signal.aborted ? 'timeout' : 'failed', error instanceof Error ? error.message : 'export failed');
66
+ this.record(controller.signal.aborted ? 'timeout' : 'failed', this.options.redactErrors
67
+ ? 'telemetry export failed' : error instanceof Error ? error.message : 'export failed');
67
68
  }
68
69
  finally {
69
70
  if (timeout)
@@ -10,4 +10,5 @@ export * from './retention.js';
10
10
  export * from './runtime.js';
11
11
  export * from './trace.js';
12
12
  export * from './types.js';
13
+ export * from './worker.js';
13
14
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,47 @@
1
1
  import { sanitizeOpaqueValue } from './redaction.js';
2
+ import { createHash } from 'node:crypto';
3
+ import { GEN_AI_SEMCONV_REVISION } from './types.js';
4
+ /** Correlation is opaque on export, including when hostile content is planted in an ID. */
5
+ function workerCorrelation(value) {
6
+ return typeof value === 'string' && value.length > 0 && value.length <= 1024
7
+ ? createHash('sha256').update(value).digest('hex') : null;
8
+ }
9
+ /** Only operator-listed model names, fixed enums and finite numbers leave this mapping. */
10
+ export function mapWorkerReceipt(receipt, correlation, allowedModels = []) {
11
+ const out = { attributes: {}, provenance: {} };
12
+ const model = (value) => typeof value === 'string' && value.length <= 128
13
+ && /^[a-zA-Z0-9][a-zA-Z0-9._:/-]*$/.test(value)
14
+ && !/(?:sk[-_]|gh[pousr]_|github_pat_|AKIA|bearer|secret|password)/i.test(value)
15
+ && allowedModels.includes(value) ? value : null;
16
+ const confidence = ['unavailable', 'runtime-configured-only', 'backend-attested'].includes(String(receipt.identityConfidence))
17
+ ? String(receipt.identityConfidence) : 'unavailable';
18
+ const observed = confidence === 'unavailable' ? null : model(receipt.observed?.model);
19
+ const observedProvenance = observed === null ? 'unknown' : confidence === 'backend-attested' ? 'provider-fact' : 'client-derived';
20
+ const quality = receipt.usage?.quality === 'reported' || receipt.usage?.quality === 'estimated' ? receipt.usage.quality : 'unknown';
21
+ const usageProvenance = quality === 'reported' ? 'provider-fact' : quality === 'estimated' ? 'estimate' : 'unknown';
22
+ put(out, 'aiwg.otel.gen_ai_semconv_revision', GEN_AI_SEMCONV_REVISION, 'client-derived');
23
+ for (const [key, value] of Object.entries({ 'aiwg.conductor.id': correlation.conductorId,
24
+ 'aiwg.run.id': correlation.runId, 'aiwg.task.id': receipt.taskId, 'aiwg.invocation.id': receipt.invocationId })) {
25
+ put(out, key, workerCorrelation(value), 'client-derived');
26
+ }
27
+ put(out, 'aiwg.worker.transport', receipt.transport === 'shell' || receipt.transport === 'mcp' ? receipt.transport : null, 'client-derived');
28
+ put(out, 'aiwg.worker.termination', receipt.termination === 'confirmed' || receipt.termination === 'uncertain' ? receipt.termination : null, 'client-derived');
29
+ put(out, 'aiwg.worker.identity_confidence', confidence, 'client-derived');
30
+ put(out, 'gen_ai.request.model', model(receipt.requested?.model), 'client-derived');
31
+ put(out, 'gen_ai.response.model', observed, observedProvenance);
32
+ put(out, 'aiwg.worker.observed_model_provenance', observedProvenance, 'client-derived');
33
+ put(out, 'aiwg.usage.quality', quality, 'client-derived');
34
+ for (const [key, value] of [['input_tokens', receipt.usage?.inputTokens], ['output_tokens', receipt.usage?.outputTokens]]) {
35
+ const count = quality !== 'unknown' && typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
36
+ put(out, `gen_ai.usage.${key}`, count, usageProvenance);
37
+ put(out, `aiwg.usage.${key}_provenance`, count === null ? 'unknown' : usageProvenance, 'client-derived');
38
+ }
39
+ const cost = receipt.usage?.costUsd;
40
+ const knownCost = quality !== 'unknown' && typeof cost === 'number' && Number.isFinite(cost) && cost >= 0 ? cost : null;
41
+ put(out, 'aiwg.usage.cost_usd', knownCost, usageProvenance);
42
+ put(out, 'aiwg.usage.cost_provenance', knownCost === null ? 'unknown' : usageProvenance, 'client-derived');
43
+ return out;
44
+ }
2
45
  function put(target, key, value, provenance) {
3
46
  target.attributes[key] = value;
4
47
  target.provenance[key] = value === null ? 'unknown' : provenance;
@@ -28,7 +28,8 @@ export class DecisionOtlpHttpSink {
28
28
  }
29
29
  async export(trace, signal) {
30
30
  const safe = sanitizedTelemetryExport(trace);
31
- const payload = JSON.stringify({ resourceSpans: [{ scopeSpans: [{ scope: { name: 'aiwg.decision', version: '1' },
31
+ const payload = JSON.stringify({ resourceSpans: [{ scopeSpans: [{ ...(safe.schemaUrl ? { schemaUrl: safe.schemaUrl } : {}),
32
+ scope: { name: safe.spans.every(span => span.name === 'worker.attempt') ? 'aiwg.worker' : 'aiwg.decision', version: '1' },
32
33
  spans: safe.spans.map(toOtlpSpan) }] }] });
33
34
  if (Buffer.byteLength(payload, 'utf8') > this.maxPayloadBytes)
34
35
  throw new Error('OTLP payload exceeds configured bound');
@@ -172,7 +173,9 @@ function toOtlpSpan(span) {
172
173
  name: span.name,
173
174
  startTimeUnixNano: String(Math.trunc(span.startTimeUnixMs * 1_000_000)),
174
175
  endTimeUnixNano: String(Math.trunc(span.endTimeUnixMs * 1_000_000)),
175
- attributes: Object.entries(span.attributes).map(([key, value]) => toOtlpAttribute(key, value)),
176
+ // GenAI unknowns have no measurement; never serialize a token count as zero or a string.
177
+ attributes: Object.entries(span.attributes).filter(([key, value]) => value !== null || !key.startsWith('gen_ai.'))
178
+ .map(([key, value]) => toOtlpAttribute(key, value)),
176
179
  links: span.links.map(link => ({ traceId: link.traceId, spanId: link.spanId,
177
180
  attributes: Object.entries(link.attributes ?? {}).map(([key, value]) => toOtlpAttribute(key, value)) })),
178
181
  events: span.events.map(event => ({ name: event.name, timeUnixNano: String(Math.trunc(event.timeUnixMs * 1_000_000)),
@@ -5,6 +5,10 @@ const SAFE_EVENT_NAMES = new Set(['retry.scheduled', 'attempt.terminated', 'brea
5
5
  // plausible custom key can still carry body text or PII in its value.
6
6
  const SAFE_ATTRIBUTE_KEYS = new Set(`
7
7
  aiwg.run.id aiwg.invocation.id aiwg.workflow.status aiwg.evaluation.count
8
+ aiwg.conductor.id aiwg.task.id aiwg.worker.transport aiwg.worker.identity_confidence
9
+ aiwg.worker.termination aiwg.otel.gen_ai_semconv_revision aiwg.usage.quality
10
+ aiwg.usage.input_tokens_provenance aiwg.usage.output_tokens_provenance
11
+ aiwg.worker.observed_model_provenance
8
12
  aiwg.decision.id aiwg.decision.version aiwg.decision.alias aiwg.decision.status aiwg.decision.reason
9
13
  aiwg.ruleset.id aiwg.ruleset.version aiwg.binding.id aiwg.binding.version
10
14
  aiwg.adapter.id aiwg.adapter.version aiwg.attempt.ordinal aiwg.attempt.count
@@ -39,7 +43,8 @@ for (const prefix of ['aiwg.definition', 'aiwg.policy', 'aiwg.calibration']) {
39
43
  // Declared keys whose names match PROTECTED_KEY but carry only fixed metadata.
40
44
  // `aiwg.link.state` is the deletion tombstone marker; dropping it would turn an
41
45
  // explicit tombstone into an unexplained link in exports.
42
- const PROTECTED_KEY_EXEMPTIONS = new Set(['gen_ai.response.model', 'http.response.status_code', 'aiwg.link.state']);
46
+ const PROTECTED_KEY_EXEMPTIONS = new Set(['gen_ai.response.model', 'http.response.status_code', 'aiwg.link.state',
47
+ 'aiwg.usage.input_tokens_provenance', 'aiwg.usage.output_tokens_provenance']);
43
48
  export const DEFAULT_ATTRIBUTE_VALUE_LIMIT = 256;
44
49
  export function sanitizeOpaqueValue(value, maximum = DEFAULT_ATTRIBUTE_VALUE_LIMIT) {
45
50
  return value.replace(CONTROL, '').slice(0, maximum);
@@ -1,3 +1,6 @@
1
1
  /** OpenTelemetry-compatible, dependency-free decision telemetry contract. */
2
2
  export const DECISION_TELEMETRY_SCHEMA_VERSION = 'decision-telemetry/v1';
3
+ /** Adopted GenAI attributes from semantic-conventions v1.37.0 (development). */
4
+ export const GEN_AI_SEMCONV_REVISION = '1.37.0';
5
+ export const GEN_AI_SCHEMA_URL = `https://opentelemetry.io/schemas/${GEN_AI_SEMCONV_REVISION}`;
3
6
  //# sourceMappingURL=types.js.map
@@ -0,0 +1,69 @@
1
+ import { createTelemetryContext, extractTraceContext } from './context.js';
2
+ import { BoundedDecisionTraceExporter } from './exporter.js';
3
+ import { mapWorkerReceipt } from './mapping.js';
4
+ import { DECISION_TELEMETRY_SCHEMA_VERSION, GEN_AI_SCHEMA_URL } from './types.js';
5
+ /** Optional observer for attempts sharing one task policy; retains no receipts or completion state. */
6
+ export class WorkerReceiptTelemetry {
7
+ exporter;
8
+ options;
9
+ root;
10
+ constructor(options) {
11
+ this.options = structuredClone({ ...options, sink: undefined });
12
+ this.root = createTelemetryContext();
13
+ if (options.enabled === true && options.sink) {
14
+ this.exporter = new BoundedDecisionTraceExporter(options.sink, {
15
+ capacity: options.capacity ?? 32, timeoutMs: options.timeoutMs ?? 1000,
16
+ maximumTraceBytes: options.maximumTraceBytes, maximumDiagnostics: options.maximumDiagnostics,
17
+ canaries: options.canaries, redactErrors: true,
18
+ });
19
+ }
20
+ }
21
+ get diagnostics() { return this.exporter?.diagnostics ?? []; }
22
+ /** Gate before mapping or enqueue. Unknown policy and local-only tasks suppress all export. */
23
+ offer(receipt, timing) {
24
+ try {
25
+ if (!this.exporter || !this.externalExportAllowed())
26
+ return false;
27
+ const now = Date.now();
28
+ const start = timing?.startTimeUnixMs ?? now;
29
+ const end = timing?.endTimeUnixMs ?? now;
30
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start)
31
+ return false;
32
+ const parent = this.options.parent;
33
+ const safeParent = parent ? extractTraceContext({ traceparent: `00-${parent.traceId}-${parent.spanId}-${parent.traceFlags}` }) : null;
34
+ if (parent && !safeParent)
35
+ return false;
36
+ const context = createTelemetryContext(undefined, safeParent ?? this.root);
37
+ const mapped = mapWorkerReceipt(receipt, this.options, this.options.allowedModels);
38
+ const span = {
39
+ schemaVersion: DECISION_TELEMETRY_SCHEMA_VERSION, name: 'worker.attempt', context,
40
+ parentSpanId: safeParent?.spanId ?? null, startTimeUnixMs: start, endTimeUnixMs: end,
41
+ // A confirmed stop is not proof of acceptance or success.
42
+ status: 'unset', attributes: mapped.attributes, provenance: mapped.provenance, links: [],
43
+ events: [{ name: 'attempt.terminated', timeUnixMs: end, attributes: {
44
+ 'aiwg.worker.termination': mapped.attributes['aiwg.worker.termination'] ?? null,
45
+ } }],
46
+ };
47
+ return this.exporter.offer({ schemaVersion: DECISION_TELEMETRY_SCHEMA_VERSION,
48
+ schemaUrl: GEN_AI_SCHEMA_URL, traceId: context.traceId, spans: [span] });
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ async shutdown() { await this.exporter?.shutdown(); }
55
+ externalExportAllowed() {
56
+ const selection = this.options.selection;
57
+ if (!selection?.dataPolicy?.allowedProcessing?.includes('remote'))
58
+ return false;
59
+ if (this.options.dataClass !== undefined) {
60
+ if (!selection.dataClasses || !Object.hasOwn(selection.dataClasses, this.options.dataClass))
61
+ return false;
62
+ const narrowed = selection.dataClasses?.[this.options.dataClass];
63
+ if (!narrowed || (narrowed.allowedProcessing !== undefined && !narrowed.allowedProcessing.includes('remote')))
64
+ return false;
65
+ }
66
+ return true;
67
+ }
68
+ }
69
+ //# sourceMappingURL=worker.js.map
@@ -1,11 +1,14 @@
1
1
  /** Activation checks within the existing lint runner, not a delivery selector. */
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { loadLocalCapabilityEntries, resolveRuleBindingTarget } from '../artifacts/query-engine.js';
4
+ import { BUILTIN_GRAPH_CONFIGS } from '../artifacts/types.js';
5
+ import { resolveRuleBindingTarget } from '../artifacts/query-engine.js';
6
+ import { collectRuleBindingEntries } from '../artifacts/index-builder.js';
7
+ import { resolveFrameworkIndexRoot } from '../artifacts/framework-index-paths.js';
5
8
  import { parseRuleFrontmatter, RULE_BINDING_TYPES, coreBodyPath, validateCoreBody } from '../rules/activation.mjs';
6
9
  export const ruleActivationRuleset = {
7
10
  id: 'rule-activation', name: 'Rule activation', framework: 'core', version: '1.0.0',
8
- description: 'Validate rule and behavior activation contracts and exact indexed bindings',
11
+ description: 'Validate rule and behavior activation contracts and exact source bindings',
9
12
  rules: [{
10
13
  id: 'rules/activation', name: 'Rule activation contract', severity: 'error',
11
14
  description: 'Validate activation, summaries, paths, bindings, and migration fallback',
@@ -13,7 +16,14 @@ export const ruleActivationRuleset = {
13
16
  checks: [{ type: 'rule-activation' }],
14
17
  }],
15
18
  };
16
- export function lintRuleActivation(content, file, projectRoot, entries, absolutePath) {
19
+ /** Load the index builder's canonical binding metadata without reading or writing an index. */
20
+ export async function loadRuleActivationEntries(projectRoot) {
21
+ const root = resolveFrameworkIndexRoot(projectRoot);
22
+ const snapshots = await Promise.all(BUILTIN_GRAPH_CONFIGS.framework.scanDirs.map(scope => collectRuleBindingEntries(root, scope)));
23
+ return snapshots.flat();
24
+ }
25
+ /** Validate one rule against the source snapshot supplied by the lint runner. */
26
+ export function lintRuleActivation(content, file, projectRoot, entries = [], absolutePath) {
17
27
  // Directory documentation and generated indexes are not authored rules.
18
28
  if (/^(?:README|RULES-INDEX|RULES-ONDEMAND)\.md$/.test(file.split('/').pop()))
19
29
  return [];
@@ -39,13 +49,11 @@ export function lintRuleActivation(content, file, projectRoot, entries, absolute
39
49
  const bindings = result.rule.bound_to;
40
50
  if (!Object.values(bindings).some(list => list.length > 0))
41
51
  return diagnostics;
42
- // Reuse existing artifact index ownership and show precedence. No title guessing,
43
- // corpus directory resolver, or extra binding registry.
44
- const indexed = entries ?? loadLocalCapabilityEntries(projectRoot);
52
+ // Preserve exact identities and show precedence using current source metadata.
45
53
  for (const key of Object.keys(RULE_BINDING_TYPES)) {
46
54
  for (const id of bindings[key]) {
47
55
  try {
48
- resolveRuleBindingTarget(indexed, RULE_BINDING_TYPES[key], id);
56
+ resolveRuleBindingTarget(entries, RULE_BINDING_TYPES[key], id);
49
57
  }
50
58
  catch (error) {
51
59
  diagnostics.push({ ruleId: 'rules/activation', ruleName: 'Rule activation contract',
@@ -7,7 +7,7 @@
7
7
  * @issue #810
8
8
  */
9
9
  import fs from 'fs';
10
- import { lintRuleActivation } from './rule-activation.js';
10
+ import { lintRuleActivation, loadRuleActivationEntries } from './rule-activation.js';
11
11
  import { guardRulesInlineBudget } from '../rules/inline-budget.mjs';
12
12
  import { dropGitignored } from '../config/gitignore.js';
13
13
  import fsp from 'fs/promises';
@@ -563,7 +563,7 @@ async function runRule(rule, targetDir, allFiles, referenceIndex, projectRoot, a
563
563
  if (check.type === 'id-unique')
564
564
  continue; // Already handled
565
565
  if (check.type === 'rule-activation') {
566
- diagnostics.push(...lintRuleActivation(content, file, projectRoot, artifactEntries, absPath));
566
+ diagnostics.push(...lintRuleActivation(content, file, projectRoot, artifactEntries ??= await loadRuleActivationEntries(projectRoot), absPath));
567
567
  continue;
568
568
  }
569
569
  const checkDiagnostics = runCheck(check, content, frontmatter, file, targetDir, allFiles, referenceIndex, projectRoot);
@@ -691,17 +691,8 @@ If retry fails, prepare structured escalation:
691
691
  export async function startServer() {
692
692
  const server = await createServer();
693
693
  const transport = new StdioServerTransport();
694
-
695
- console.error('[AIWG MCP] Starting server...');
696
- await server.connect(transport);
697
- console.error('[AIWG MCP] Server connected via stdio transport');
698
-
699
- // Handle graceful shutdown
700
- process.on('SIGINT', async () => {
701
- console.error('[AIWG MCP] Shutting down...');
702
- await server.close();
703
- process.exit(0);
704
- });
694
+ const { serveStdioUntilClosed } = await import('./stdio-lifecycle.mjs');
695
+ await serveStdioUntilClosed(server, transport);
705
696
  }
706
697
 
707
698
  // Run if executed directly
@@ -0,0 +1,27 @@
1
+ // Keep the CLI dispatch pending for the lifetime of its stdio MCP transport.
2
+ export async function serveStdioUntilClosed(server, transport, { input = process.stdin, signals = process } = {}) {
3
+ let finish;
4
+ let fail;
5
+ const closed = new Promise((resolve, reject) => { finish = resolve; fail = reject; });
6
+ const previousClose = server.server.onclose;
7
+ const onClose = () => {
8
+ try { previousClose?.(); } finally { finish(); }
9
+ };
10
+ const shutdown = () => { Promise.resolve().then(() => server.close()).catch(fail); };
11
+ server.server.onclose = onClose;
12
+ input.once('end', shutdown);
13
+ signals.once('SIGINT', shutdown);
14
+ signals.once('SIGTERM', shutdown);
15
+ try {
16
+ console.error('[AIWG MCP] Starting server...');
17
+ await server.connect(transport);
18
+ console.error('[AIWG MCP] Server connected via stdio transport');
19
+ if (input.readableEnded) shutdown();
20
+ await closed;
21
+ } finally {
22
+ input.removeListener('end', shutdown);
23
+ signals.removeListener('SIGINT', shutdown);
24
+ signals.removeListener('SIGTERM', shutdown);
25
+ if (server.server.onclose === onClose) server.server.onclose = previousClose;
26
+ }
27
+ }
@@ -0,0 +1,9 @@
1
+ // Keep contract failures actionable without publishing parser messages or payload values.
2
+ export function workerFailureDiagnostics(error, definition, fallback) {
3
+ const code = typeof error?.code === 'string' ? error.code : fallback;
4
+ if (code !== 'OG_CONTRACT_INVALID') return [code];
5
+ const pointer = typeof error.pointer === 'string' && error.pointer.startsWith(`/${definition}/`)
6
+ && error.pointer.length <= 1024 && /^\/[A-Za-z0-9_~/-]+$/.test(error.pointer)
7
+ ? error.pointer : `/${definition}`;
8
+ return [code, { code, pointer, message: 'Worker contract validation failed at this field.' }];
9
+ }