@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,217 @@
1
+ // Validates and merges worker selection restrictions and constrained role aliases.
2
+ import { z } from 'zod';
3
+ import { validLocalOnlyPath } from './conductor-data-policy.mjs';
4
+ import { integer } from '../schema/zod-validation.mjs';
5
+ const uniqueStrings = z.array(z.string().min(1)).refine(values => new Set(values).size === values.length, 'Duplicate entries');
6
+ const WorkerDataPolicySchema = z.strictObject({
7
+ localOnlyPaths: uniqueStrings.refine(values => values.every(validLocalOnlyPath), 'Expected literal project-relative paths').optional(),
8
+ allowedProcessing: z.array(z.enum(['local', 'remote'])).refine(values => new Set(values).size === values.length).optional(),
9
+ allowProductImprovement: z.boolean().optional(),
10
+ allowUnknown: z.boolean().optional(),
11
+ maxRetentionDays: integer().nonnegative().optional(),
12
+ });
13
+ const baseRoles = ['efficiency', 'coding', 'reasoning'];
14
+ const WorkerRoleSchema = z.strictObject({
15
+ baseRole: z.enum(baseRoles).optional(),
16
+ groups: uniqueStrings.refine(values => values.length > 0),
17
+ tier: z.enum(['economy', 'standard', 'premium', 'max-quality']),
18
+ initialEffort: z.string().min(1).nullable(),
19
+ effortLadder: uniqueStrings,
20
+ escalateAfter: integer().positive().optional(),
21
+ maxEscalations: integer().nonnegative().optional(),
22
+ deescalateAfter: integer().positive().optional(),
23
+ allowNotApplicableEffort: z.boolean().optional(),
24
+ });
25
+ const reservedRoleKey = (key) => key === 'prototype' || Object.hasOwn(Object.prototype, key);
26
+ const roleGuidanceSchema = z.preprocess((input, context) => {
27
+ if (input !== null && typeof input === 'object')
28
+ for (const role of Object.keys(input)) {
29
+ if (reservedRoleKey(role))
30
+ context.addIssue({ code: 'custom', path: [role], message: `Reserved worker role alias key: ${role}` });
31
+ }
32
+ return input;
33
+ }, z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), WorkerRoleSchema));
34
+ /** Inherited object members never constitute configured role guidance. */
35
+ export function ownWorkerRoleGuidance(policy, role) {
36
+ return policy?.roleGuidance && Object.hasOwn(policy.roleGuidance, role) ? policy.roleGuidance[role] : undefined;
37
+ }
38
+ export const WorkerSelectionPolicySchema = z.strictObject({
39
+ roleGuidance: roleGuidanceSchema.optional(),
40
+ guards: z.strictObject({
41
+ deniedEfforts: uniqueStrings.optional(),
42
+ deniedModels: uniqueStrings.optional(),
43
+ denySelfDelegation: z.boolean().optional(),
44
+ requireExactModel: z.boolean().optional(),
45
+ allowedVisibility: z.array(z.enum(['listed', 'hidden'])).refine(v => new Set(v).size === v.length).optional(),
46
+ allowedLifecycle: z.array(z.enum(['active', 'deprecated'])).refine(v => new Set(v).size === v.length).optional(),
47
+ minimumIdentityEvidence: z.enum(['runtime-configured', 'backend-attested']).optional(),
48
+ }).optional(),
49
+ dataPolicy: WorkerDataPolicySchema.optional(),
50
+ dataClasses: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), WorkerDataPolicySchema).optional(),
51
+ freshness: z.strictObject({
52
+ warnHours: integer().positive().optional(),
53
+ blockHours: integer().positive().optional(),
54
+ policyDays: integer().positive().optional(),
55
+ }).optional(),
56
+ taskClasses: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), z.string().min(1)).optional(),
57
+ }).superRefine((policy, context) => {
58
+ for (const [role, guidance] of Object.entries(policy.roleGuidance ?? {})) {
59
+ if (baseRoles.includes(role) ? guidance.baseRole !== undefined : guidance.baseRole === undefined)
60
+ context.addIssue({ code: 'custom', path: ['roleGuidance', role], message: 'Unknown worker role requires a baseRole; base roles cannot be aliases' });
61
+ const base = guidance.baseRole && ownWorkerRoleGuidance(policy, guidance.baseRole);
62
+ if (base)
63
+ for (const message of aliasRestrictionErrors(guidance, base))
64
+ context.addIssue({ code: 'custom', path: ['roleGuidance', role], message });
65
+ if (guidance.effortLadder.includes('none') || (guidance.initialEffort === 'none' ? guidance.effortLadder.length !== 0 : guidance.initialEffort !== null && !guidance.effortLadder.includes(guidance.initialEffort))) {
66
+ context.addIssue({ code: 'custom', path: ['roleGuidance', role], message: 'Initial effort must belong to the ladder; real none requires an empty ladder' });
67
+ }
68
+ if (guidance.initialEffort === null && (guidance.allowNotApplicableEffort !== true || guidance.effortLadder.length !== 0)) {
69
+ context.addIssue({ code: 'custom', path: ['roleGuidance', role], message: 'Null effort requires an empty ladder and allowNotApplicableEffort=true' });
70
+ }
71
+ }
72
+ for (const key of policy.guards?.deniedModels ?? []) {
73
+ if (!isCanonicalWorkerModelKey(key))
74
+ context.addIssue({ code: 'custom', path: ['guards', 'deniedModels'], message: 'Noncanonical inference model key' });
75
+ }
76
+ if (policy.freshness?.warnHours !== undefined && policy.freshness.blockHours !== undefined && policy.freshness.warnHours > policy.freshness.blockHours) {
77
+ context.addIssue({ code: 'custom', path: ['freshness'], message: 'warnHours must not exceed blockHours' });
78
+ }
79
+ for (const role of Object.values(policy.taskClasses ?? {})) {
80
+ if (!baseRoles.includes(role) && !ownWorkerRoleGuidance(policy, role)?.baseRole)
81
+ context.addIssue({ code: 'custom', path: ['taskClasses'], message: 'Unknown worker role' });
82
+ }
83
+ });
84
+ /** Aliases may reorder/subset choices, never change base authority or escalation controls. */
85
+ function aliasRestrictionErrors(alias, base) {
86
+ const errors = [];
87
+ if (alias.groups.some(group => !base.groups.includes(group)))
88
+ errors.push('Alias groups widen base role');
89
+ if (alias.effortLadder.some(effort => !base.effortLadder.includes(effort))
90
+ || alias.initialEffort === 'none' && base.initialEffort !== 'none'
91
+ || alias.initialEffort === null && base.initialEffort !== null
92
+ || alias.allowNotApplicableEffort === true && base.allowNotApplicableEffort !== true)
93
+ errors.push('Alias effort choices widen base role');
94
+ if (alias.tier !== base.tier)
95
+ errors.push('Alias tier must retain base role authority');
96
+ if ((alias.maxEscalations ?? base.maxEscalations ?? 2) > (base.maxEscalations ?? 2))
97
+ errors.push('Alias maxEscalations widens base role');
98
+ for (const key of ['escalateAfter', 'deescalateAfter']) {
99
+ if (alias[key] !== undefined && alias[key] !== (base[key] ?? 2))
100
+ errors.push(`Alias ${key} must retain base role control`);
101
+ }
102
+ return errors;
103
+ }
104
+ /** Canonical RFC3986 identity; installation and account never enter the key. */
105
+ export function workerModelKey(inferenceProvider, apiModelId) {
106
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(inferenceProvider) || !apiModelId)
107
+ throw new Error('OG_CONFIG_REFERENCE: invalid inference model identity');
108
+ return `${inferenceProvider}/${encodeURIComponent(apiModelId).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`;
109
+ }
110
+ export function isCanonicalWorkerModelKey(key) {
111
+ const slash = key.indexOf('/');
112
+ try {
113
+ return slash > 0 && workerModelKey(key.slice(0, slash), decodeURIComponent(key.slice(slash + 1))) === key;
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ export const DEFAULT_WORKER_SELECTION = {
120
+ guards: { deniedEfforts: ['ultra'], deniedModels: [], denySelfDelegation: true, requireExactModel: true, allowedVisibility: ['listed'], allowedLifecycle: ['active'], minimumIdentityEvidence: 'runtime-configured' },
121
+ dataPolicy: { allowedProcessing: ['local', 'remote'], allowProductImprovement: false, allowUnknown: false },
122
+ freshness: { warnHours: 24, blockHours: 168, policyDays: 30 },
123
+ taskClasses: { inventory: 'efficiency', implementation: 'coding', architecture: 'reasoning', review: 'reasoning' },
124
+ };
125
+ /** Merge leaves, preserving pre-merge explicit origins and attempted relaxations. */
126
+ export function mergeWorkerSelection(layers) {
127
+ const policy = Object.create(null);
128
+ const origins = Object.create(null);
129
+ const diagnostics = [];
130
+ const visit = (target, input, parts, layer) => {
131
+ for (const [key, value] of Object.entries(input)) {
132
+ const path = [...parts, key];
133
+ const pointer = `/defaults/workerSelection/${path.map(p => p.replace(/~/g, '~0').replace(/\//g, '~1')).join('/')}`;
134
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
135
+ if (!Object.hasOwn(target, key))
136
+ target[key] = Object.create(null);
137
+ visit(target[key], value, path, layer);
138
+ continue;
139
+ }
140
+ const origin = { source: layer.source, authority: layer.authority, digest: layer.digest, pointer, contribution: structuredClone(value) };
141
+ const prior = origins[pointer] ?? [];
142
+ const explicit = prior.filter(o => o.authority !== 'builtin');
143
+ let effective = structuredClone(value);
144
+ let relaxation = false;
145
+ if (layer.authority !== 'builtin' && explicit.length > 0) {
146
+ const previous = target[key];
147
+ if (['deniedEfforts', 'deniedModels', 'localOnlyPaths'].includes(key))
148
+ effective = [...new Set([...previous, ...value])];
149
+ else if (['allowedVisibility', 'allowedLifecycle', 'allowedProcessing'].includes(key)) {
150
+ relaxation = value.some((v) => !previous.includes(v));
151
+ effective = previous.filter((v) => value.includes(v));
152
+ }
153
+ else if (['denySelfDelegation', 'requireExactModel'].includes(key)) {
154
+ relaxation = previous === true && value === false;
155
+ effective = previous || value;
156
+ }
157
+ else if (['allowUnknown', 'allowProductImprovement'].includes(key)) {
158
+ relaxation = previous === false && value === true;
159
+ effective = previous && value;
160
+ }
161
+ else if (['maxRetentionDays', 'warnHours', 'blockHours', 'policyDays'].includes(key)) {
162
+ relaxation = value > previous;
163
+ effective = Math.min(previous, value);
164
+ }
165
+ else if (key === 'minimumIdentityEvidence') {
166
+ relaxation = previous === 'backend-attested' && value !== previous;
167
+ effective = previous === 'backend-attested' ? previous : value;
168
+ }
169
+ }
170
+ if (relaxation)
171
+ diagnostics.push({ code: 'OG_POLICY_RELAXATION', pointer, message: 'Explicit restriction cannot relax an inherited restriction', origins: [...explicit, origin] });
172
+ target[key] = effective;
173
+ origins[pointer] = [...prior, origin];
174
+ }
175
+ };
176
+ for (const layer of layers)
177
+ if (layer.policy) {
178
+ for (const [role, guidance] of Object.entries(layer.policy.roleGuidance ?? {})) {
179
+ if (!guidance.baseRole)
180
+ continue;
181
+ const previous = ownWorkerRoleGuidance(policy, role);
182
+ if (layer.authority !== 'project' || previous?.baseRole && previous.baseRole !== guidance.baseRole)
183
+ diagnostics.push({
184
+ code: 'OG_POLICY_RELAXATION', pointer: `/defaults/workerSelection/roleGuidance/${role}/baseRole`,
185
+ message: 'Only project layers may declare constrained aliases; inherited aliases cannot change baseRole',
186
+ });
187
+ }
188
+ visit(policy, layer.policy, [], layer);
189
+ }
190
+ for (const [role, guidance] of Object.entries(policy.roleGuidance ?? {})) {
191
+ const base = guidance.baseRole && ownWorkerRoleGuidance(policy, guidance.baseRole);
192
+ if (base)
193
+ for (const field of ['maxEscalations', 'escalateAfter', 'deescalateAfter'])
194
+ guidance[field] ??= base[field];
195
+ if (guidance.baseRole && !base)
196
+ diagnostics.push({
197
+ code: 'OG_CONFIG_REFERENCE', pointer: `/defaults/workerSelection/roleGuidance/${role}/baseRole`, message: 'Alias base role has no guidance',
198
+ });
199
+ }
200
+ const validation = WorkerSelectionPolicySchema.safeParse(policy);
201
+ if (!validation.success)
202
+ for (const issue of validation.error.issues)
203
+ diagnostics.push({ code: 'OG_CONFIG_REFERENCE', pointer: `/defaults/workerSelection/${issue.path.join('/')}`, message: issue.message });
204
+ // Data classes are applied as further restrictions to the base path.
205
+ for (const dataClass of Object.values(policy.dataClasses ?? {})) {
206
+ const base = policy.dataPolicy ?? {};
207
+ if (dataClass.allowedProcessing)
208
+ dataClass.allowedProcessing = dataClass.allowedProcessing.filter(p => !base.allowedProcessing || base.allowedProcessing.includes(p));
209
+ for (const key of ['allowUnknown', 'allowProductImprovement'])
210
+ if (base[key] === false)
211
+ dataClass[key] = false;
212
+ if (base.maxRetentionDays !== undefined)
213
+ dataClass.maxRetentionDays = Math.min(dataClass.maxRetentionDays ?? base.maxRetentionDays, base.maxRetentionDays);
214
+ }
215
+ return { policy, origins, diagnostics };
216
+ }
217
+ //# sourceMappingURL=worker-selection-policy.js.map
@@ -156,7 +156,7 @@ export async function verifyModelWrapperDeployment(agentsPath, expectation = {})
156
156
  const missing = [];
157
157
  const mismatches = [];
158
158
  for (const [role, wrapper] of expected) {
159
- const matches = files.filter(file => path.basename(file).replace(/\.(?:md|toml|json|yaml|yml)$/i, '') === wrapper);
159
+ const matches = files.filter(file => path.basename(file).replace(/(?:\.agent)?\.(?:md|toml|json|yaml|yml)$/i, '') === wrapper);
160
160
  if (matches.length === 0) {
161
161
  missing.push(wrapper);
162
162
  continue;
@@ -170,13 +170,41 @@ const LEGACY_MANIFESTS = [
170
170
  '.aiwg/ops/security/repo-access.manifest.yaml',
171
171
  '.aiwg/security/repo-access.manifest.yaml',
172
172
  ];
173
+ /**
174
+ * Factory delivery defaults written by `aiwg init`/`aiwg use`
175
+ * (emptyConfig in src/config/aiwg-config.ts). Kept as a literal here —
176
+ * rather than importing the config module — because aiwg-config already
177
+ * imports this module for authorization validation, so a value import
178
+ * would be a cycle. A unit test pins this literal against
179
+ * emptyConfig().delivery (#3635).
180
+ */
181
+ const DEFAULT_DELIVERY_POLICY = {
182
+ mode: 'pr-required',
183
+ default_branch: 'main',
184
+ require_ci_green: true,
185
+ auto_close_issues: true,
186
+ issue_comment_on_cycle: true,
187
+ force_push_policy: 'never',
188
+ };
189
+ /** True when the delivery block is exactly what the tool writes for a fresh project. */
190
+ export function isDefaultDeliveryPolicy(delivery) {
191
+ if (!delivery || typeof delivery !== 'object')
192
+ return false;
193
+ const actual = delivery;
194
+ const actualKeys = Object.keys(actual);
195
+ return actualKeys.length === Object.keys(DEFAULT_DELIVERY_POLICY).length
196
+ && actualKeys.every((key) => actual[key] === DEFAULT_DELIVERY_POLICY[key]);
197
+ }
173
198
  export async function auditLegacyPermissions(projectDir, config) {
174
199
  const out = validateAuthorization(config?.authorization);
175
200
  if (!config?.authorization && config?.repos?.some(repo => repo.allowed?.length))
176
201
  out.push({ severity: 'warning', code: 'legacy-repos-allowed', source: '.aiwg/aiwg.config', message: 'repos[].allowed should be normalized into authorization roles and assignments.' });
177
202
  if (!config?.authorization && Object.keys(config?.repo_maintainer?.tiers ?? {}).length)
178
203
  out.push({ severity: 'warning', code: 'legacy-maintainer-tiers', source: '.aiwg/aiwg.config', message: 'repo_maintainer.tiers should be represented by normalized role assignments.' });
179
- if (!config?.authorization && config?.delivery)
204
+ // Tool-written delivery defaults are not operator legacy input: a fresh
205
+ // `aiwg use` must not produce something that needs migration (#3635).
206
+ // Operator-edited delivery (any key added, removed, or changed) still flags.
207
+ if (!config?.authorization && config?.delivery && !isDefaultDeliveryPolicy(config.delivery))
180
208
  out.push({ severity: 'warning', code: 'legacy-delivery-policy', source: '.aiwg/aiwg.config', message: 'delivery and signing operations should be normalized into scoped role assignments.' });
181
209
  for (const path of LEGACY_MANIFESTS) {
182
210
  try {
@@ -0,0 +1,15 @@
1
+ export interface AiwgProbeCommand {
2
+ file: string;
3
+ prefix: string[];
4
+ via: 'aiwg-bin' | 'entrypoint' | 'package' | 'path';
5
+ }
6
+
7
+ export function resolveAiwgProbeCommand(options?: {
8
+ env?: NodeJS.ProcessEnv;
9
+ argv1?: string | undefined;
10
+ execPath?: string;
11
+ cwd?: string;
12
+ packageRoot?: string | null;
13
+ }): AiwgProbeCommand;
14
+
15
+ export function probeNotOnPathDetail(): string;
@@ -0,0 +1,37 @@
1
+ // Resolves the aiwg invocation for doctor's discovery self-probes so they
2
+ // run the current entrypoint instead of failing when aiwg is not on PATH.
3
+ import { existsSync } from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ export function resolveAiwgProbeCommand({
7
+ env = process.env,
8
+ argv1 = process.argv[1],
9
+ execPath = process.execPath,
10
+ cwd = process.cwd(),
11
+ packageRoot = null,
12
+ } = {}) {
13
+ const override = env?.AIWG_BIN;
14
+ if (typeof override === 'string' && override.length > 0) {
15
+ return { file: override, prefix: [], via: 'aiwg-bin' };
16
+ }
17
+ const entry = typeof argv1 === 'string' && argv1.length > 0
18
+ ? (path.isAbsolute(argv1) ? argv1 : path.resolve(cwd, argv1))
19
+ : null;
20
+ // Only the aiwg launcher itself is a usable entrypoint; doctor also runs as
21
+ // `node tools/cli/doctor.mjs`, where argv[1] is doctor, not aiwg.
22
+ const isLauncher = entry && ['aiwg', 'aiwg.mjs'].includes(path.basename(entry));
23
+ if (entry && isLauncher && existsSync(entry)) {
24
+ return { file: execPath, prefix: [entry], via: 'entrypoint' };
25
+ }
26
+ if (typeof packageRoot === 'string' && packageRoot.length > 0) {
27
+ const bin = path.join(packageRoot, 'bin', 'aiwg.mjs');
28
+ if (existsSync(bin)) {
29
+ return { file: execPath, prefix: [bin], via: 'package' };
30
+ }
31
+ }
32
+ return { file: 'aiwg', prefix: [], via: 'path' };
33
+ }
34
+
35
+ export function probeNotOnPathDetail() {
36
+ return 'aiwg not on PATH (spawn ENOENT) — add the npm global bin to PATH or invoke doctor via the install entrypoint';
37
+ }
@@ -0,0 +1,116 @@
1
+ export function attemptBlock(snapshot, candidate, policy, now) {
2
+ const accounting = candidate.accounting;
3
+ const deny = (cause, kind = 'policy-exclusion') => ({ kind, cause, terminal: true });
4
+ const attempts = Object.values(snapshot.records).filter(record => record.accounting?.attemptNumber !== undefined);
5
+ const taskAttempts = attempts.filter(record => record.accounting.taskKey === accounting.taskKey);
6
+ if (taskAttempts.length) {
7
+ const previous = taskAttempts.sort((a, b) => a.accounting.attemptNumber - b.accounting.attemptNumber).at(-1);
8
+ if (candidate.allowance?.previousRequestId !== previous.requestId)
9
+ return deny('new attempt must reference the latest task attempt');
10
+ if (previous.state !== 'settled')
11
+ return deny('previous attempt requires proof-checked reconciliation before replacement');
12
+ const failure = previous.accounting.failure;
13
+ const purpose = candidate.allowance?.purpose ?? 'retry';
14
+ if (purpose === 'retry' || purpose === 'fallback' || failure?.class === 'rate-limit') {
15
+ if (!failure || !failure.retryable || !['rate-limit', 'unavailable', 'timeout'].includes(failure.class))
16
+ return deny('previous failure does not permit automatic retry');
17
+ if (!(policy.managed.retry?.on ?? ['rate-limit', 'unavailable', 'timeout']).includes(failure.class))
18
+ return deny(`${failure.class} retries are disabled`);
19
+ }
20
+ }
21
+ else if (candidate.allowance?.previousRequestId)
22
+ return deny('unknown previous task attempt');
23
+ for (const key of [accounting.runKey, accounting.taskKey]) {
24
+ const scope = snapshot.allowances[key];
25
+ if (Date.parse(scope.deadlineAt ?? '') <= now)
26
+ return deny('cumulative allowance deadline elapsed');
27
+ const prior = attempts.filter(record => record.accounting.runKey === key || record.accounting.taskKey === key);
28
+ if (scope.limits.maxAttempts !== undefined && prior.reduce((sum, record) => sum + record.accounting.attemptCount, 0) + accounting.attemptCount > scope.limits.maxAttempts)
29
+ return deny('cumulative attempt allowance exhausted', 'budget-denial');
30
+ for (const [dimension, limit] of [['tokens', scope.limits.maxTokens], ['costUsd', scope.limits.maxCostUsd]]) {
31
+ if (limit === undefined)
32
+ continue;
33
+ let used = 0;
34
+ for (const record of prior) {
35
+ const value = chargedUsage(record, dimension);
36
+ if (value === null)
37
+ return deny(`unknown ${dimension} charge has no finite reservation`, 'budget-denial');
38
+ used += value;
39
+ }
40
+ const reserved = accounting.reservation[dimension];
41
+ if (reserved === null || reserved === 0 || used + reserved > limit)
42
+ return deny(`cumulative ${dimension} reservation exceeds allowance`, 'budget-denial');
43
+ }
44
+ }
45
+ if (taskAttempts.some(record => Date.parse(record.accounting.notBefore ?? '') > now))
46
+ return { kind: 'policy-exclusion', cause: 'retry-backoff', terminal: false };
47
+ if (policy.managed.budgets?.unknownCost === 'deny' && accounting.reservation.costUsd === null)
48
+ return deny('unknown cost is denied by policy', 'budget-denial');
49
+ const quotaScope = candidate.managed?.cli && snapshot.hostBinding
50
+ ? policy.managed.upstream[candidate.managed.cli].quotaScope : candidate.managed?.quotaScope;
51
+ const quotas = quotaScope ? [`quota/${encodeURIComponent(quotaScope)}`]
52
+ : Object.keys(snapshot.cooldowns ?? {});
53
+ if (quotas.some(key => Date.parse(snapshot.cooldowns?.[key]?.until ?? '') > now))
54
+ return { kind: 'policy-exclusion', cause: 'quota-cooldown', terminal: false };
55
+ return undefined;
56
+ }
57
+ function chargedUsage(record, dimension) {
58
+ const accounting = record.accounting;
59
+ const usage = accounting.usage;
60
+ const measured = !usage || usage.quality === 'unknown' ? null : dimension === 'costUsd' ? usage.costUsd
61
+ : usage.inputTokens === null || usage.outputTokens === null || usage.cacheTokens === null || usage.reasoningTokens === null ? null
62
+ : usage.inputTokens + usage.outputTokens + usage.cacheTokens + usage.reasoningTokens;
63
+ const reservation = accounting.reservation[dimension];
64
+ // Uncertain execution can still accrue spend; estimates never refund a reserve.
65
+ if (record.state !== 'settled' || usage?.quality !== 'reported') {
66
+ return reservation === null || measured === null ? reservation : Math.max(reservation, measured);
67
+ }
68
+ return measured ?? reservation;
69
+ }
70
+ export function capacityBlock(snapshot, candidate, policy, now = -Infinity) {
71
+ const slots = policy.managed ? candidate.reservedSlots ?? 1 : 1;
72
+ const active = Object.values(snapshot.records).filter(record => record.state === 'admitted' || (policy.managed && record.state === 'uncertain'));
73
+ const limits = [
74
+ { dimension: 'global', cap: policy.maxConcurrent, matches: () => true },
75
+ ];
76
+ if (policy.managed)
77
+ for (const key of candidate.dimensions ?? []) {
78
+ const cap = policy.managed.dimensionCaps[key];
79
+ if (cap !== undefined)
80
+ limits.push({ dimension: key, cap, matches: record => !record.dimensions || record.dimensions.includes(key) });
81
+ }
82
+ for (const [field, quotas] of [
83
+ ['environment', policy.environmentQuotas], ['provider', policy.providerQuotas],
84
+ ['runtimeKind', { host: policy.defaultHostQuota ?? 1, ...policy.runtimeQuotas }],
85
+ ]) {
86
+ const cap = quotas?.[candidate[field]];
87
+ if (cap !== undefined)
88
+ limits.push({ dimension: `${field}/${candidate[field]}`, cap: policy.managed ? cap : Math.ceil(cap), matches: record => record[field] === candidate[field] });
89
+ }
90
+ const impossible = limits.find(limit => slots > limit.cap);
91
+ if (impossible)
92
+ return { kind: 'policy-exclusion', cause: 'request-exceeds-cap', dimension: impossible.dimension, terminal: false };
93
+ const used = (records, limit) => records.filter(limit.matches)
94
+ .reduce((sum, record) => sum + (policy.managed ? record.reservedSlots ?? 1 : 1), 0);
95
+ const blocked = limits.filter(limit => used(active, limit) + slots > limit.cap);
96
+ if (!blocked.length)
97
+ return undefined;
98
+ const dimensions = blocked.map(limit => limit.dimension);
99
+ const certain = active.filter(record => record.state === 'admitted' && !(Date.parse(record.leaseExpiresAt ?? '') <= now));
100
+ if (policy.managed && blocked.some(limit => used(certain, limit) + slots <= limit.cap)) {
101
+ return { kind: 'safety-blocked', cause: 'uncertain-lease', dimension: dimensions[0], dimensions, terminal: false };
102
+ }
103
+ return { kind: 'capacity-wait', cause: 'capacity-cap', dimension: dimensions[0], dimensions, terminal: false };
104
+ }
105
+ /** Past retry/cooldown exclusion does not count toward an eligible service gap. */
106
+ export function attemptReadyAt(snapshot, candidate, policy) {
107
+ const retry = Object.values(snapshot.records).filter(record => record.accounting?.attemptNumber !== undefined
108
+ && record.accounting.taskKey === candidate.accounting?.taskKey)
109
+ .reduce((latest, record) => Math.max(latest, Date.parse(record.accounting.notBefore ?? '') || 0), 0);
110
+ const quota = candidate.managed?.cli && snapshot.hostBinding
111
+ ? policy.managed.upstream[candidate.managed.cli].quotaScope : candidate.managed?.quotaScope;
112
+ const cooldown = Object.entries(snapshot.cooldowns ?? {}).filter(([key]) => !quota || key === `quota/${encodeURIComponent(quota)}`)
113
+ .reduce((latest, [, value]) => Math.max(latest, Date.parse(value.until) || 0), 0);
114
+ return Math.max(retry, cooldown);
115
+ }
116
+ //# sourceMappingURL=shared-host-admission-checks.js.map