@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.
- package/agentic/code/providers/model-capabilities.v1.json +275 -17
- package/bin/aiwg.mjs +8 -0
- package/dist/src/cli/handlers/mc-admission.js +39 -0
- package/dist/src/cli/handlers/mc.js +2 -31
- package/dist/src/cli/handlers/models-help.js +52 -0
- package/dist/src/cli/handlers/models.js +11 -81
- package/dist/src/cli/handlers/sessions.js +198 -15
- package/dist/src/cli/handlers/use-addon-profile.js +72 -0
- package/dist/src/cli/handlers/use.js +203 -234
- package/dist/src/cli/ui.js +1 -0
- package/dist/src/config/aiwg-config.js +38 -184
- package/dist/src/config/cli-help.js +54 -0
- package/dist/src/config/cli.js +5 -69
- package/dist/src/config/config-normalization.js +10 -0
- package/dist/src/config/worker-activation-gate.js +26 -0
- package/dist/src/config/worker-adoption-cli.js +102 -0
- package/dist/src/config/worker-adoption.js +368 -0
- package/dist/src/config/worker-config-digest.js +14 -0
- package/dist/src/config/worker-context.js +337 -0
- package/dist/src/config/worker-group-templates.js +150 -0
- package/dist/src/config/worker-guidance-warning.js +28 -0
- package/dist/src/config/worker-owner-lock.js +152 -0
- package/dist/src/config/worker-parallelism.js +154 -0
- package/dist/src/config/worker-write.js +75 -0
- package/dist/src/decision/telemetry/exporter.js +2 -1
- package/dist/src/decision/telemetry/index.js +1 -0
- package/dist/src/decision/telemetry/mapping.js +43 -0
- package/dist/src/decision/telemetry/otlp-http.js +5 -2
- package/dist/src/decision/telemetry/redaction.js +6 -1
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/telemetry/worker.js +69 -0
- package/dist/src/lint/rule-activation.js +15 -7
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/server.mjs +2 -11
- package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
- package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
- package/dist/src/mcp/tools/worker-invoke.mjs +114 -16
- package/dist/src/mission-protocol/codecs.js +8 -38
- package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
- package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
- package/dist/src/models/conductor-data-policy.d.mts +8 -0
- package/dist/src/models/conductor-data-policy.mjs +190 -0
- package/dist/src/models/config-loader.js +19 -3
- package/dist/src/models/config-writer.js +27 -0
- package/dist/src/models/model-capabilities.v1.json +275 -17
- package/dist/src/models/provider-policy.js +14 -155
- package/dist/src/models/resolver.js +2 -293
- package/dist/src/models/worker-artifact-binding.mjs +12 -0
- package/dist/src/models/worker-direct-readiness-policy.js +155 -0
- package/dist/src/models/worker-direct-readiness.js +9 -218
- package/dist/src/models/worker-health-readiness.js +105 -26
- package/dist/src/models/worker-installation.mjs +12 -14
- package/dist/src/models/worker-member-references.js +64 -0
- package/dist/src/models/worker-plan.js +317 -0
- package/dist/src/models/worker-selection-policy.js +217 -0
- package/dist/src/models/wrapper-deployment.js +1 -1
- package/dist/src/policy/authorization.js +29 -1
- package/dist/src/process/aiwg-probe-command.d.mts +15 -0
- package/dist/src/process/aiwg-probe-command.mjs +37 -0
- package/dist/src/serve/shared-host-admission-checks.js +116 -0
- package/dist/src/serve/shared-host-scheduler.js +25 -107
- package/dist/src/serve/shared-host-starvation.js +73 -0
- package/dist/src/serve/worker-artifacts.d.mts +4 -0
- package/dist/src/serve/worker-artifacts.mjs +56 -0
- package/dist/src/serve/worker-command-request.js +68 -0
- package/dist/src/serve/worker-invocation-paths.js +144 -0
- package/dist/src/serve/worker-invocation-preparation.js +169 -0
- package/dist/src/serve/worker-invocation.js +24 -291
- package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
- package/dist/src/serve/worker-stdin-frame.mjs +6 -6
- package/dist/src/sessions/adapters/grokbot.js +7 -0
- package/dist/src/sessions/batch-import.js +34 -6
- package/dist/src/sessions/contracts.js +19 -0
- package/dist/src/sessions/coverage-preflight.js +7 -1
- package/dist/src/sessions/fortemi-export-mapping.js +33 -3
- package/dist/src/sessions/host-catalog.js +157 -0
- package/dist/src/sessions/image-blobs.js +141 -0
- package/dist/src/sessions/importer.js +23 -3
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/refresh.js +25 -2
- package/dist/src/sessions/repository.js +164 -22
- package/dist/src/sessions/workspace-discovery.js +5 -2
- package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
- package/package.json +15 -1
- package/schemas/mission-v1.schema.json +20 -19
- package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
- package/tools/agents/providers/claude-post-deploy.mjs +35 -0
- package/tools/agents/providers/claude.mjs +3 -30
- package/tools/agents/providers/codex-post-deploy.mjs +13 -0
- package/tools/agents/providers/codex.mjs +2 -8
- package/tools/agents/providers/conductor-data-guard.mjs +61 -0
- package/tools/agents/providers/rule-delivery.mjs +3 -16
- package/tools/agents/providers/rule-presentation.mjs +38 -0
- package/tools/plugin/package-plugins.mjs +1 -1
- package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
- package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
- package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
- package/tools/ralph-external/lib/session-attempt.mjs +85 -0
- package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
- package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
- package/tools/ralph-external/lib/worker-context.mjs +237 -0
- package/tools/ralph-external/lib/worker-final.mjs +143 -0
- package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
- package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
- package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
- package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +19 -73
- package/tools/ralph-external/lib/worker-launch-cli.mjs +49 -0
- package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
- package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
- package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
- package/tools/ralph-external/lib/worker-termination.mjs +82 -0
- package/tools/ralph-external/session-launcher.mjs +124 -313
- package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { MODELS_HELP } from './models-help.js';
|
|
2
|
+
import { updateJson } from '../../models/config-writer.js';
|
|
3
|
+
import { atomicConfigWrite as atomicWrite } from '../../config/worker-write.js';
|
|
4
|
+
import { handleWorkerAdoptionArgs } from '../../config/worker-adoption-cli.js';
|
|
5
|
+
import { resolveParallelism } from '../../config/aiwg-config.js';
|
|
1
6
|
/**
|
|
2
7
|
* Safe model-policy audit, resolution, and mutation commands.
|
|
3
8
|
* @implements #1804
|
|
@@ -8,7 +13,7 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
8
13
|
import path from 'node:path';
|
|
9
14
|
import { glob } from 'glob';
|
|
10
15
|
import { load as loadYaml } from 'js-yaml';
|
|
11
|
-
import { compileModelPolicy, validateCanonicalModelPolicy,
|
|
16
|
+
import { compileModelPolicy, validateCanonicalModelPolicy, loadProviderModelCapabilities, WorkerPolicyError, assertWorkerRoleEffort, } from '../../models/provider-policy.js';
|
|
12
17
|
import { resolveGrokBuildRoleModel } from '../../models/grok-build-models.js';
|
|
13
18
|
import { readAiwgConfig, emptyConfig } from '../../config/aiwg-config.js';
|
|
14
19
|
import { ConfigLoader } from '../../models/config-loader.js';
|
|
@@ -34,22 +39,21 @@ export async function loadWorkerInputs(ctx) {
|
|
|
34
39
|
const models = await loader.load(config.parallelism);
|
|
35
40
|
const catalog = await effectiveCatalog(ctx, ctx.cwd, false, parseArgs(['resolve']));
|
|
36
41
|
const registry = loadProviderModelCapabilities();
|
|
37
|
-
// Let the resolver explain the dispatch gate before requiring host qualification (#3070).
|
|
38
42
|
if (!config.parallelism?.worker_dispatch) {
|
|
39
43
|
return { snapshot: { config, models, modelDigests: loader.getInputDigests(), catalog,
|
|
40
44
|
registry, installations: {} }, drivers: {} };
|
|
41
45
|
}
|
|
42
46
|
// Explain missing policy before a recorded approval's digest check can
|
|
43
47
|
// mask it. The host owner first preserves its missing-installation errors.
|
|
44
|
-
const parallelism = config.parallelism;
|
|
48
|
+
const parallelism = resolveParallelism(config.parallelism, config.providers[0]);
|
|
45
49
|
const precheckPolicy = () => {
|
|
46
50
|
if (!ctx.args.some(arg => arg === '--role' || arg.startsWith('--role=')))
|
|
47
51
|
return;
|
|
48
52
|
const request = parseWorkerCommandArgs(ctx.args.slice(1));
|
|
49
53
|
const members = Object.entries(parallelism.groups ?? {})
|
|
50
54
|
.filter(([id]) => !request.group || request.group === id)
|
|
51
|
-
.flatMap(([, group]) => group.members).filter(member => member.enabled !== false
|
|
52
|
-
&& parallelism.clis?.[member.cli]?.enabled !== false && member.roles.includes(request.role));
|
|
55
|
+
.flatMap(([, group]) => group.members ?? []).filter(member => member.enabled !== false
|
|
56
|
+
&& parallelism.clis?.[member.cli]?.enabled !== false && member.roles.includes((models.defaults.workerSelection?.roleGuidance?.[request.role]?.baseRole ?? request.role)));
|
|
53
57
|
const requiresExplicit = members.length > 0 && members.every(member => {
|
|
54
58
|
// Health metadata can identify the provider before an operator has
|
|
55
59
|
// qualified its dispatch adapter. It grants no dispatch authority.
|
|
@@ -269,35 +273,6 @@ function print(value, json) {
|
|
|
269
273
|
else
|
|
270
274
|
console.log(JSON.stringify(value, null, 2));
|
|
271
275
|
}
|
|
272
|
-
async function atomicWrite(file, content) {
|
|
273
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
274
|
-
const temporary = `${file}.tmp-${process.pid}`;
|
|
275
|
-
let mode = 0o600;
|
|
276
|
-
try {
|
|
277
|
-
mode = (await fs.stat(file)).mode;
|
|
278
|
-
}
|
|
279
|
-
catch { /* new file */ }
|
|
280
|
-
await fs.writeFile(temporary, content, { encoding: 'utf8', mode });
|
|
281
|
-
await fs.rename(temporary, file);
|
|
282
|
-
}
|
|
283
|
-
async function updateJson(file, mutate, dryRun) {
|
|
284
|
-
let value = {};
|
|
285
|
-
try {
|
|
286
|
-
value = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
287
|
-
}
|
|
288
|
-
catch (error) {
|
|
289
|
-
if (error?.code !== 'ENOENT')
|
|
290
|
-
throw error;
|
|
291
|
-
}
|
|
292
|
-
mutate(value);
|
|
293
|
-
const validation = validateUserProjectModelConfig(value);
|
|
294
|
-
if (!validation.valid)
|
|
295
|
-
throw new Error(validation.diagnostics.map(item => item.message).join('; '));
|
|
296
|
-
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
297
|
-
if (!dryRun)
|
|
298
|
-
await atomicWrite(file, content);
|
|
299
|
-
return { file, beforeWrite: value, dryRun };
|
|
300
|
-
}
|
|
301
276
|
async function effectiveCatalog(ctx, target, allowNetwork, parsed, provider) {
|
|
302
277
|
const { collectProviderInventory } = await import('../../providers/provider-inventory.js');
|
|
303
278
|
const { resolveDynamicModelCatalog } = await import('../../models/model-discovery.js');
|
|
@@ -350,53 +325,6 @@ function replaceFrontmatter(raw, item, tier) {
|
|
|
350
325
|
}
|
|
351
326
|
return `---\n${frontmatter}\n---\n${match[2]}`;
|
|
352
327
|
}
|
|
353
|
-
const MODELS_HELP = `Usage: aiwg models <subcommand> [options]
|
|
354
|
-
|
|
355
|
-
Subcommands:
|
|
356
|
-
list List discovered agent and skill model policies
|
|
357
|
-
audit Compile selected policies and report diagnostics
|
|
358
|
-
resolve Resolve artifact policies, or a direct worker invocation
|
|
359
|
-
validate Validate selected artifact policies (not worker configuration)
|
|
360
|
-
sources Show the effective model catalog without network refresh
|
|
361
|
-
refresh Refresh model discovery (may access the network)
|
|
362
|
-
set-default <TIER> Write defaults.tier to models.json
|
|
363
|
-
set --tier <TIER> Update selected agent/skill frontmatter; requires a selector or --all
|
|
364
|
-
migrate Add canonical role/tier hints to legacy skills
|
|
365
|
-
|
|
366
|
-
Artifact selectors and options:
|
|
367
|
-
--agent <name> --skill <name> --glob <pattern> --role <role>
|
|
368
|
-
--tier <tier> --framework <name> --all
|
|
369
|
-
--provider <provider> Provider for artifact compilation (default: claude)
|
|
370
|
-
--model <id> Explicit Grok Build model for artifact resolve
|
|
371
|
-
--target <directory> Artifact/project root (default: current directory)
|
|
372
|
-
--scope project|user set-default destination (default: project)
|
|
373
|
-
--dry-run Preview mutations without writing
|
|
374
|
-
--json Emit JSON
|
|
375
|
-
--drift Include catalog drift for sources/refresh
|
|
376
|
-
--url <url> Remote catalog URL for refresh
|
|
377
|
-
--help, -h Show this reference
|
|
378
|
-
TIER: economy | standard | premium | max-quality
|
|
379
|
-
|
|
380
|
-
Direct worker resolution (plans an invocation; does not launch it):
|
|
381
|
-
aiwg models resolve --role <role> --task <class> --group <group>
|
|
382
|
-
--brief <file> --workdir <tree> --final-output <file>
|
|
383
|
-
[--transport shell|mcp] [--format json|shell] [--authorize-premium]
|
|
384
|
-
--role efficiency | coding | reasoning
|
|
385
|
-
--task Task class from defaults.workerSelection.taskClasses
|
|
386
|
-
Built-ins: inventory, implementation, architecture, review
|
|
387
|
-
--group Group from parallelism.groups and the role's guidance
|
|
388
|
-
--brief Absolute path to the task brief
|
|
389
|
-
--workdir Absolute path to the worker tree
|
|
390
|
-
--final-output Absolute path for the final result
|
|
391
|
-
--transport Must match group execution transport (default: shell)
|
|
392
|
-
--format JSON plan or POSIX shell command (default: json)
|
|
393
|
-
--json Alias for --format json
|
|
394
|
-
--authorize-premium Authorize premium for this resolution only
|
|
395
|
-
Agents require explicit human confirmation of escalation
|
|
396
|
-
|
|
397
|
-
Worker groups live in .aiwg/aiwg.config; role guidance lives in project-root models.json.
|
|
398
|
-
Configure both, then run aiwg doctor --probe-workers --qualify.
|
|
399
|
-
See docs/getting-started/worker-dispatch.md for a complete example.`;
|
|
400
328
|
async function execute(ctx, services = { load: loadWorkerInputs }) {
|
|
401
329
|
if (ctx.args.length === 0 || ctx.args[0] === 'help' || ctx.args.includes('--help') || ctx.args.includes('-h')) {
|
|
402
330
|
print(MODELS_HELP, false);
|
|
@@ -406,6 +334,8 @@ async function execute(ctx, services = { load: loadWorkerInputs }) {
|
|
|
406
334
|
if (ctx.args[0] === 'resolve' && ctx.args.slice(1).some(arg => /^--(?:task|group|brief|workdir|final-output|transport|format|authorize-premium)(?:=|$)/.test(arg))) {
|
|
407
335
|
return resolveDirect(ctx, services);
|
|
408
336
|
}
|
|
337
|
+
if (await handleWorkerAdoptionArgs(ctx.args, ctx.cwd, 'models', ctx.signal, ctx.dryRun))
|
|
338
|
+
return { exitCode: 0 };
|
|
409
339
|
const parsed = parseArgs(ctx.args);
|
|
410
340
|
if (parsed.subcommand === 'resolve') {
|
|
411
341
|
const supported = new Set(['agent', 'skill', 'glob', 'role', 'tier', 'framework', 'all', 'target', 'provider', 'model', 'json', 'help', 'dry-run']);
|
|
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { basename, dirname, isAbsolute, join, resolve, } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { providerScopeWarnings } from '../../sessions/coverage-preflight.js';
|
|
6
|
-
import { CLAUDE_ADAPTER_VERSION, ClaudeSessionAdapter, CODEX_ADAPTER_VERSION, CodexSessionAdapter, COPILOT_ADAPTER_VERSION, CopilotSessionAdapter, CURSOR_ADAPTER_VERSION, CursorSessionAdapter, FACTORY_ADAPTER_VERSION, FactorySessionAdapter, HERMES_ADAPTER_VERSION, HermesSessionAdapter, OPENCODE_ADAPTER_VERSION, OpenCodeSessionAdapter, OPENCLAW_ADAPTER_VERSION, OpenClawSessionAdapter, OPENHUMAN_ADAPTER_VERSION, OpenHumanSessionAdapter, GROKBOT_ADAPTER_VERSION, GROKBOT_NATIVE_REPLICA_LOCATOR_CLASS, GrokbotSessionAdapter, GROK_BUILD_ADAPTER_VERSION, GROK_BUILD_CLI_EXPORT_LOCATOR_CLASS, GrokBuildSessionAdapter, grokBuildList, grokBuildSearch, grokBuildExport, grokHeadlessSessionIdFromFile, MUSE_ADAPTER_VERSION, MuseSessionAdapter, PI_ADAPTER_VERSION, PiSessionAdapter, DEEPSEEK_HARNESS_ADAPTER_VERSION, DeepSeekHarnessSessionAdapter, OmpSessionAdapter, OMP_ADAPTER_VERSION, WARP_ADAPTER_VERSION, WarpSessionAdapter, DEVIN_DESKTOP_ADAPTER_VERSION, DevinDesktopSessionAdapter, CandidateExtractionService, GENERIC_ADAPTER_VERSION, GenericSessionInterchangeAdapter, IncrementalSessionImporter, ImportLeaseContentionError, FilesystemMemoryDestination, FilesystemPromotionDispositionCoordinator, MemoryPromotionGateway, SESSION_CONTRACT_VERSION, SessionContractError, SessionRepository, SessionSourceSchema, StructuralCandidateExtractor, resolveMemoryConsumerManifest, assertSessionProviderId, acquireImportLease, defaultDiscoveryManifestPath, discoverWorkspaceHistories, createSessionAdapterRegistry, coveragePreflight, expectedSessionProviders, deriveSessionTimeline, importDiscoveryManifest, previewDiscoveryImport, publicDiscoveryManifest, readDiscoveryManifest, redactSourceLocator, sha256, parseTimelineGap, writeDiscoveryManifest, buildSessionExportPlan, reverifySessionExportPlan, buildSessionAiwgFortemiIndexExport, recoverAiwgFortemiIndexFromFullV1Shard, embedAttachmentsInFullV1Shard, recoverEmbeddedAttachments, SESSION_EXPORT_PLAN_SCHEMA_VERSION, } from '../../sessions/index.js';
|
|
6
|
+
import { CLAUDE_ADAPTER_VERSION, ClaudeSessionAdapter, CODEX_ADAPTER_VERSION, CodexSessionAdapter, COPILOT_ADAPTER_VERSION, CopilotSessionAdapter, CURSOR_ADAPTER_VERSION, CursorSessionAdapter, FACTORY_ADAPTER_VERSION, FactorySessionAdapter, HERMES_ADAPTER_VERSION, HermesSessionAdapter, OPENCODE_ADAPTER_VERSION, OpenCodeSessionAdapter, OPENCLAW_ADAPTER_VERSION, OpenClawSessionAdapter, OPENHUMAN_ADAPTER_VERSION, OpenHumanSessionAdapter, GROKBOT_ADAPTER_VERSION, GROKBOT_NATIVE_REPLICA_LOCATOR_CLASS, GrokbotSessionAdapter, GROK_BUILD_ADAPTER_VERSION, GROK_BUILD_CLI_EXPORT_LOCATOR_CLASS, GrokBuildSessionAdapter, grokBuildList, grokBuildSearch, grokBuildExport, grokHeadlessSessionIdFromFile, MUSE_ADAPTER_VERSION, MuseSessionAdapter, PI_ADAPTER_VERSION, PiSessionAdapter, DEEPSEEK_HARNESS_ADAPTER_VERSION, DeepSeekHarnessSessionAdapter, OmpSessionAdapter, OMP_ADAPTER_VERSION, WARP_ADAPTER_VERSION, WarpSessionAdapter, DEVIN_DESKTOP_ADAPTER_VERSION, DevinDesktopSessionAdapter, CandidateExtractionService, GENERIC_ADAPTER_VERSION, GenericSessionInterchangeAdapter, IncrementalSessionImporter, ImportLeaseContentionError, FilesystemMemoryDestination, FilesystemPromotionDispositionCoordinator, MemoryPromotionGateway, SESSION_CONTRACT_VERSION, SessionContractError, SessionRepository, SessionSourceSchema, StructuralCandidateExtractor, resolveMemoryConsumerManifest, assertSessionProviderId, acquireImportLease, countUniqueSessionsAcrossCatalogs, defaultDiscoveryManifestPath, defaultHostCatalogPath, HOST_CATALOG_WORKSPACE_ID, indexIdentitiesBySessionId, manifestNeedsHostCatalog, mergeCrossCatalogRows, mergeSessionLists, resolveHostReferences, summarizeSessionAnalyticsFacts, discoverWorkspaceHistories, createSessionAdapterRegistry, coveragePreflight, expectedSessionProviders, deriveSessionTimeline, importDiscoveryManifest, previewDiscoveryImport, publicDiscoveryManifest, readDiscoveryManifest, redactSourceLocator, sha256, parseTimelineGap, writeDiscoveryManifest, buildSessionExportPlan, reverifySessionExportPlan, buildSessionAiwgFortemiIndexExport, recoverAiwgFortemiIndexFromFullV1Shard, embedAttachmentsInFullV1Shard, recoverEmbeddedAttachments, SESSION_EXPORT_PLAN_SCHEMA_VERSION, } from '../../sessions/index.js';
|
|
7
7
|
import { resolveImportExclusions } from '../../sessions/import-exclusions.js';
|
|
8
8
|
import { resolveImportLimits } from '../../sessions/import-limits.js';
|
|
9
9
|
import { classifyGrokbotBlobName } from '../../sessions/adapters/grokbot-locator.js';
|
|
@@ -227,18 +227,35 @@ async function executeCommand(ctx, args) {
|
|
|
227
227
|
limit,
|
|
228
228
|
cursor,
|
|
229
229
|
});
|
|
230
|
+
// Account-scoped sessions live once in the host catalog; the
|
|
231
|
+
// workspace holds references (#3232). Read through them on the
|
|
232
|
+
// opening page so the workspace lists its referenced sessions.
|
|
233
|
+
// Cursor pages continue the local snapshot. Totals dedupe legacy
|
|
234
|
+
// copied rows against references, counting each session once.
|
|
235
|
+
const hostView = cursor
|
|
236
|
+
? null
|
|
237
|
+
: readHostReferencedSessions(repository, { provider, tag: args.values.get('--tag') });
|
|
238
|
+
const items = hostView && hostView.sessions.length > 0
|
|
239
|
+
? mergeSessionLists(result.items, hostView.sessions).slice(0, limit)
|
|
240
|
+
: result.items;
|
|
241
|
+
const total = hostView && hostView.references.length > 0
|
|
242
|
+
? countUniqueSessionsAcrossCatalogs([{
|
|
243
|
+
identities: repository.listSessionIdentities(workspaceId),
|
|
244
|
+
references: hostView.resolvedReferences,
|
|
245
|
+
}])
|
|
246
|
+
: result.total;
|
|
230
247
|
const coverage = {
|
|
231
248
|
...repository.getCoverage(workspaceId),
|
|
232
249
|
catalogScope: catalogScope(ctx, args, repository, workspaceId),
|
|
233
250
|
};
|
|
234
251
|
const response = {
|
|
235
|
-
items
|
|
252
|
+
items,
|
|
236
253
|
page: {
|
|
237
254
|
limit,
|
|
238
255
|
cursor: cursor ?? null,
|
|
239
256
|
nextCursor: result.nextCursor,
|
|
240
257
|
snapshotRowid: result.snapshotRowid,
|
|
241
|
-
total
|
|
258
|
+
total,
|
|
242
259
|
},
|
|
243
260
|
coverage,
|
|
244
261
|
};
|
|
@@ -314,10 +331,15 @@ async function executeCommand(ctx, args) {
|
|
|
314
331
|
const session = matches[0];
|
|
315
332
|
if (!session)
|
|
316
333
|
throw new CliError('SESSION_NOT_FOUND', `session not found: ${id}${scope.hint ? `; ${scope.hint}` : ''}`, EXIT.unavailable, { catalogScope: scope });
|
|
334
|
+
// Image references resolve against the blob store: available blobs
|
|
335
|
+
// report their reference, missing ones an explicit unavailable
|
|
336
|
+
// state. Bytes stay out of the response; fetch by digest (#3265).
|
|
337
|
+
const events = repository.listEvents(session.sessionId, session.workspaceId)
|
|
338
|
+
.map((event) => ({ ...event, images: repository.resolveEventImages(event) }));
|
|
317
339
|
return ok(command, {
|
|
318
340
|
session,
|
|
319
341
|
tags: repository.listTags(session.sessionId, session.workspaceId),
|
|
320
|
-
events
|
|
342
|
+
events,
|
|
321
343
|
catalogScope: catalogScope(ctx, args, repository, session.workspaceId),
|
|
322
344
|
});
|
|
323
345
|
}
|
|
@@ -326,7 +348,8 @@ async function executeCommand(ctx, args) {
|
|
|
326
348
|
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
327
349
|
const providerInput = args.values.get('--provider');
|
|
328
350
|
const provider = providerInput ? assertSessionProviderId(providerInput) : undefined;
|
|
329
|
-
const
|
|
351
|
+
const limit = boundedInteger(args.values.get('--limit'), 50, 1, 500, '--limit');
|
|
352
|
+
const searchOptions = {
|
|
330
353
|
query,
|
|
331
354
|
workspaceId,
|
|
332
355
|
providers: provider ? [provider] : undefined,
|
|
@@ -341,13 +364,24 @@ async function executeCommand(ctx, args) {
|
|
|
341
364
|
sensitivity: args.values.get('--sensitivity'),
|
|
342
365
|
extractionState: args.values.get('--extraction-state'),
|
|
343
366
|
controlEvents: controlEventMode(args.values.get('--control-events')),
|
|
344
|
-
limit
|
|
367
|
+
limit,
|
|
345
368
|
cursor: args.values.get('--cursor'),
|
|
346
|
-
}
|
|
369
|
+
};
|
|
370
|
+
const result = repository.search(searchOptions);
|
|
371
|
+
// Read through host references on the opening page so workspace
|
|
372
|
+
// search finds referenced account-scoped sessions. Each session
|
|
373
|
+
// counts once: host hits for sessions already present locally
|
|
374
|
+
// (legacy copied rows) are dropped by provider + native session.
|
|
375
|
+
const hostRead = searchOptions.cursor
|
|
376
|
+
? null
|
|
377
|
+
: searchHostReferencedSessions(repository, searchOptions, provider);
|
|
378
|
+
const items = hostRead && hostRead.hits.length > 0
|
|
379
|
+
? mergeCrossCatalogRows(result.items, hostRead.hits, indexIdentitiesBySessionId(repository.listSessionIdentities(workspaceId)), indexIdentitiesBySessionId(hostRead.hostIdentities), (hit) => [hit.provider, hit.eventId].join(' ')).slice(0, limit)
|
|
380
|
+
: result.items;
|
|
347
381
|
return ok(command, {
|
|
348
|
-
items
|
|
382
|
+
items,
|
|
349
383
|
catalogScope: catalogScope(ctx, args, repository, workspaceId),
|
|
350
|
-
page: { limit
|
|
384
|
+
page: { limit,
|
|
351
385
|
nextCursor: result.nextCursor },
|
|
352
386
|
});
|
|
353
387
|
}
|
|
@@ -355,14 +389,22 @@ async function executeCommand(ctx, args) {
|
|
|
355
389
|
const view = requiredPositional(args, 0, 'analytics view');
|
|
356
390
|
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
357
391
|
const query = analyticsQuery(args, workspaceId);
|
|
392
|
+
// Host references resolve to facts stored under the host catalog;
|
|
393
|
+
// each session counts once by provider + native session id. Without
|
|
394
|
+
// references the single-catalog path below is unchanged.
|
|
395
|
+
const mergedFacts = readWorkspaceAnalyticsFacts(repository, query);
|
|
358
396
|
if (view === 'summary') {
|
|
359
397
|
return ok(command, {
|
|
360
398
|
view,
|
|
361
|
-
summary:
|
|
399
|
+
summary: mergedFacts
|
|
400
|
+
? summarizeSessionAnalyticsFacts(mergedFacts, workspaceId)
|
|
401
|
+
: repository.analyticsSummary(query),
|
|
362
402
|
});
|
|
363
403
|
}
|
|
364
404
|
const categories = analyticsCategories(view);
|
|
365
|
-
const items =
|
|
405
|
+
const items = mergedFacts
|
|
406
|
+
? mergedFacts.filter((fact) => categories.includes(fact.category))
|
|
407
|
+
: repository.listAnalyticsFacts({ ...query, categories });
|
|
366
408
|
return ok(command, {
|
|
367
409
|
analyticsVersion: '1.0.0',
|
|
368
410
|
view,
|
|
@@ -1075,7 +1117,10 @@ async function exportBuild(ctx, args, repository, command) {
|
|
|
1075
1117
|
...output,
|
|
1076
1118
|
bytesEmbedded: includeBytes && outputBytesByRegistrationId.has(output.registrationId),
|
|
1077
1119
|
}));
|
|
1078
|
-
const index = buildSessionAiwgFortemiIndexExport(plan.workspaceId, sessions, eventsBySessionId, outputs, catalogTagsBySessionId
|
|
1120
|
+
const index = buildSessionAiwgFortemiIndexExport(plan.workspaceId, sessions, eventsBySessionId, outputs, catalogTagsBySessionId,
|
|
1121
|
+
// Resolve image references against the live catalog: available blobs
|
|
1122
|
+
// export as references, missing ones as explicit unavailable (#3265).
|
|
1123
|
+
(event) => repository.resolveEventImages(event));
|
|
1079
1124
|
// aiwgFortemiIndexToKnowledgeShardWithReport targets the declared
|
|
1080
1125
|
// full-v1/2.0.0 archive contract and validates losslessness -- unlike the
|
|
1081
1126
|
// plain aiwgFortemiIndexToKnowledgeShard (core-v1), which silently embeds
|
|
@@ -1355,18 +1400,33 @@ async function importDiscovered(ctx, args) {
|
|
|
1355
1400
|
...(carryPath ? { carryForward: redactSourceLocator(carryPath) } : {}),
|
|
1356
1401
|
});
|
|
1357
1402
|
}
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
1403
|
+
const waitMs = boundedInteger(args.values.get('--lock-wait-ms'), 5_000, 0, 300_000, '--lock-wait-ms');
|
|
1404
|
+
const lease = await acquireImportLease(databasePath(ctx, args), sha256(['discovered-import', manifest.manifestId, manifest.workspaceId].join('\0')), { waitMs });
|
|
1405
|
+
let hostLease;
|
|
1361
1406
|
try {
|
|
1362
1407
|
const previous = carryPath ? new SessionRepository(carryPath, { readonly: true }) : undefined;
|
|
1363
1408
|
let repository;
|
|
1409
|
+
let hostRepository;
|
|
1364
1410
|
try {
|
|
1365
1411
|
repository = openRepository(ctx, args);
|
|
1412
|
+
// Account-scoped providers (Grok Bot) import once into the host
|
|
1413
|
+
// catalog; the workspace catalog records a reference (#3232). The
|
|
1414
|
+
// host catalog is only opened when the manifest actually holds an
|
|
1415
|
+
// account-scoped source.
|
|
1416
|
+
const hostCatalogPath = defaultHostCatalogPath();
|
|
1417
|
+
if (manifestNeedsHostCatalog(manifest.sources)) {
|
|
1418
|
+
mkdirSync(dirname(hostCatalogPath), { recursive: true, mode: 0o700 });
|
|
1419
|
+
// The workspace lease does not cover the shared host catalog; serialize
|
|
1420
|
+
// host writers the same way the hourly refresh does.
|
|
1421
|
+
hostLease = await acquireImportLease(hostCatalogPath, `host-import-${process.pid}`, { waitMs });
|
|
1422
|
+
hostRepository = new SessionRepository(hostCatalogPath);
|
|
1423
|
+
}
|
|
1366
1424
|
const interruptedRuns = repository.interruptRunningBatchImports();
|
|
1367
1425
|
const receipt = await importDiscoveryManifest({
|
|
1368
1426
|
manifest,
|
|
1369
1427
|
repository,
|
|
1428
|
+
hostRepository,
|
|
1429
|
+
hostCatalogPath,
|
|
1370
1430
|
carryForward: previous,
|
|
1371
1431
|
limits,
|
|
1372
1432
|
exclude,
|
|
@@ -1388,10 +1448,12 @@ async function importDiscovered(ctx, args) {
|
|
|
1388
1448
|
}
|
|
1389
1449
|
finally {
|
|
1390
1450
|
repository?.close();
|
|
1451
|
+
hostRepository?.close();
|
|
1391
1452
|
previous?.close();
|
|
1392
1453
|
}
|
|
1393
1454
|
}
|
|
1394
1455
|
finally {
|
|
1456
|
+
await hostLease?.release();
|
|
1395
1457
|
await lease.release();
|
|
1396
1458
|
}
|
|
1397
1459
|
}
|
|
@@ -1718,6 +1780,127 @@ function catalogScope(ctx, args, repository, workspaceId) {
|
|
|
1718
1780
|
: null,
|
|
1719
1781
|
};
|
|
1720
1782
|
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Host-catalog read-through for account-scoped sessions (#3232). The
|
|
1785
|
+
* workspace catalog holds references instead of copies; each reference
|
|
1786
|
+
* records its host catalog path. Every referenced catalog opens read-only
|
|
1787
|
+
* and closes before returning. Unreadable catalogs contribute nothing, so
|
|
1788
|
+
* local rows still list when the host catalog is missing.
|
|
1789
|
+
*/
|
|
1790
|
+
function eachReferencedHostCatalog(repository, provider, read) {
|
|
1791
|
+
const references = repository.listHostSessionReferences()
|
|
1792
|
+
.filter((reference) => !provider || reference.provider === provider);
|
|
1793
|
+
if (references.length === 0)
|
|
1794
|
+
return [];
|
|
1795
|
+
const byCatalog = new Map();
|
|
1796
|
+
for (const reference of references) {
|
|
1797
|
+
const group = byCatalog.get(reference.hostCatalog);
|
|
1798
|
+
if (group)
|
|
1799
|
+
group.push(reference);
|
|
1800
|
+
else
|
|
1801
|
+
byCatalog.set(reference.hostCatalog, [reference]);
|
|
1802
|
+
}
|
|
1803
|
+
const reads = [];
|
|
1804
|
+
for (const [catalogPath, group] of byCatalog) {
|
|
1805
|
+
let host;
|
|
1806
|
+
try {
|
|
1807
|
+
host = new SessionRepository(catalogPath, { readonly: true });
|
|
1808
|
+
}
|
|
1809
|
+
catch {
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
try {
|
|
1813
|
+
reads.push({ references: group, value: read(host, group) });
|
|
1814
|
+
}
|
|
1815
|
+
finally {
|
|
1816
|
+
host.close();
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
return reads;
|
|
1820
|
+
}
|
|
1821
|
+
function readHostReferencedSessions(repository, filters) {
|
|
1822
|
+
const reads = eachReferencedHostCatalog(repository, filters.provider, (host, group) => {
|
|
1823
|
+
const page = host.listSessions({
|
|
1824
|
+
provider: filters.provider,
|
|
1825
|
+
workspaceId: HOST_CATALOG_WORKSPACE_ID,
|
|
1826
|
+
limit: group.length,
|
|
1827
|
+
});
|
|
1828
|
+
const sessions = resolveHostReferences(group, page.items)
|
|
1829
|
+
.filter((session) => !filters.tag || host.listTags(session.sessionId).includes(filters.tag));
|
|
1830
|
+
const resolvedSourceIds = new Set(sessions.map((session) => session.sourceId));
|
|
1831
|
+
return {
|
|
1832
|
+
sessions,
|
|
1833
|
+
resolvedReferences: group.filter((reference) => resolvedSourceIds.has(reference.sourceId)),
|
|
1834
|
+
};
|
|
1835
|
+
});
|
|
1836
|
+
return {
|
|
1837
|
+
references: reads.flatMap((read) => read.references),
|
|
1838
|
+
resolvedReferences: reads.flatMap((read) => read.value.resolvedReferences),
|
|
1839
|
+
sessions: reads.flatMap((read) => read.value.sessions),
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
function searchHostReferencedSessions(repository, searchOptions, provider) {
|
|
1843
|
+
const reads = eachReferencedHostCatalog(repository, provider, (host) => {
|
|
1844
|
+
const result = host.search({
|
|
1845
|
+
...searchOptions,
|
|
1846
|
+
workspaceId: HOST_CATALOG_WORKSPACE_ID,
|
|
1847
|
+
cursor: undefined,
|
|
1848
|
+
});
|
|
1849
|
+
return {
|
|
1850
|
+
hits: result.items,
|
|
1851
|
+
hostIdentities: host.listSessionIdentities(HOST_CATALOG_WORKSPACE_ID),
|
|
1852
|
+
};
|
|
1853
|
+
});
|
|
1854
|
+
return {
|
|
1855
|
+
hits: reads.flatMap((read) => read.value.hits),
|
|
1856
|
+
hostIdentities: reads.flatMap((read) => read.value.hostIdentities),
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
function readHostReferencedAnalyticsFacts(repository, query) {
|
|
1860
|
+
// query.sessionId is a workspace-local internal id; host rows carry host
|
|
1861
|
+
// stable ids, so resolve it to a provider + native session key first.
|
|
1862
|
+
let sessionKey = null;
|
|
1863
|
+
if (query.sessionId) {
|
|
1864
|
+
const session = repository.getSession(query.sessionId, query.workspaceId);
|
|
1865
|
+
if (!session)
|
|
1866
|
+
return { facts: [], hostIdentities: [] };
|
|
1867
|
+
sessionKey = `${session.provider} ${session.nativeSessionId}`;
|
|
1868
|
+
}
|
|
1869
|
+
const reads = eachReferencedHostCatalog(repository, query.provider, (host, group) => {
|
|
1870
|
+
const wanted = new Set(group.map((reference) => `${reference.provider} ${reference.nativeSessionId}`));
|
|
1871
|
+
const identities = host.listSessionIdentities(HOST_CATALOG_WORKSPACE_ID);
|
|
1872
|
+
const hostSessionIds = new Set(identities
|
|
1873
|
+
.filter((identity) => wanted.has(`${identity.provider} ${identity.nativeSessionId}`))
|
|
1874
|
+
.map((identity) => identity.sessionId));
|
|
1875
|
+
const { sessionId: _localSessionId, ...hostQuery } = query;
|
|
1876
|
+
const facts = host.listAnalyticsFacts({ ...hostQuery, workspaceId: HOST_CATALOG_WORKSPACE_ID })
|
|
1877
|
+
.filter((fact) => hostSessionIds.has(fact.sessionId)
|
|
1878
|
+
&& (!sessionKey || `${fact.provider} ${hostSessionKey(fact.sessionId, identities)}` === sessionKey));
|
|
1879
|
+
return { facts, identities };
|
|
1880
|
+
});
|
|
1881
|
+
return {
|
|
1882
|
+
facts: reads.flatMap((read) => read.value.facts),
|
|
1883
|
+
hostIdentities: reads.flatMap((read) => read.value.identities),
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
function hostSessionKey(sessionId, identities) {
|
|
1887
|
+
return identities.find((identity) => identity.sessionId === sessionId)?.nativeSessionId ?? null;
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Workspace analytics facts with host references resolved. Returns null
|
|
1891
|
+
* when the workspace holds no references so callers keep the exact
|
|
1892
|
+
* single-catalog query path (SQL limit/filter semantics unchanged).
|
|
1893
|
+
*/
|
|
1894
|
+
function readWorkspaceAnalyticsFacts(repository, query) {
|
|
1895
|
+
const references = repository.listHostSessionReferences()
|
|
1896
|
+
.filter((reference) => !query.provider || reference.provider === query.provider);
|
|
1897
|
+
if (references.length === 0)
|
|
1898
|
+
return null;
|
|
1899
|
+
const hostRead = readHostReferencedAnalyticsFacts(repository, query);
|
|
1900
|
+
if (hostRead.facts.length === 0)
|
|
1901
|
+
return null;
|
|
1902
|
+
return mergeCrossCatalogRows(repository.listAnalyticsFacts(query), hostRead.facts, indexIdentitiesBySessionId(repository.listSessionIdentities(query.workspaceId)), indexIdentitiesBySessionId(hostRead.hostIdentities), (fact) => `${fact.provider} ${fact.factId}`);
|
|
1903
|
+
}
|
|
1721
1904
|
async function configuredImportLimits(ctx, args, workspace) {
|
|
1722
1905
|
const selectedWorkspace = workspace ?? (args.values.has('--workspace')
|
|
1723
1906
|
? resolve(ctx.cwd, args.values.get('--workspace')) : ctx.cwd);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Select and persist an optional bundled addon memory profile.
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import * as ui from '../ui.js';
|
|
5
|
+
export async function selectAddonProfile(options) {
|
|
6
|
+
const { addonSource, framework, target, args, dryRunAddon, bulkIntent, signal } = options;
|
|
7
|
+
try {
|
|
8
|
+
const profileManifestPath = path.join(addonSource, 'manifest.json');
|
|
9
|
+
const profileManifestContent = await fs.readFile(profileManifestPath, 'utf-8');
|
|
10
|
+
const profileManifest = JSON.parse(profileManifestContent);
|
|
11
|
+
const topology = profileManifest.memory?.topology;
|
|
12
|
+
const templates = profileManifest.templates;
|
|
13
|
+
if (topology && templates && templates.length > 1) {
|
|
14
|
+
const profileIdx = args.findIndex(a => a === '--profile');
|
|
15
|
+
let selectedProfile;
|
|
16
|
+
if (profileIdx >= 0 && args[profileIdx + 1]) {
|
|
17
|
+
// Explicit --profile flag
|
|
18
|
+
selectedProfile = args[profileIdx + 1];
|
|
19
|
+
const templateFile = templates.find((t) => t.replace('.md', '') === selectedProfile || t === selectedProfile);
|
|
20
|
+
if (!templateFile) {
|
|
21
|
+
ui.warn(`Unknown profile "${selectedProfile}". Available: ${templates.map((t) => t.replace('.md', '')).join(', ')}`);
|
|
22
|
+
selectedProfile = undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (bulkIntent || !process.stdin.isTTY) {
|
|
26
|
+
// Bulk/automation or non-TTY: silently pick 'generic' default.
|
|
27
|
+
// The profile picker is annoying during `aiwg use all`.
|
|
28
|
+
selectedProfile = 'generic';
|
|
29
|
+
}
|
|
30
|
+
else if (process.stdin.isTTY) {
|
|
31
|
+
// Interactive profile selection via the shared `listSelect` helper
|
|
32
|
+
// (POC for spike #926). One call renders the option list, handles
|
|
33
|
+
// number-or-name matching, threads `signal` for Ctrl-C
|
|
34
|
+
// cancellation, and resolves to the fallback on timeout or empty
|
|
35
|
+
// input. The hand-rolled parse-and-branch that used to live here
|
|
36
|
+
// is now a one-liner.
|
|
37
|
+
const { createPromptInterface, listSelect } = await import('../prompt-utils.js');
|
|
38
|
+
ui.blank();
|
|
39
|
+
ui.header(' Select a topology profile:');
|
|
40
|
+
const templateNames = templates.map((t) => t.replace('.md', ''));
|
|
41
|
+
const options = templateNames.map((name) => ({
|
|
42
|
+
label: name === 'generic' ? `${name} (default)` : name,
|
|
43
|
+
value: name,
|
|
44
|
+
}));
|
|
45
|
+
const rl = createPromptInterface();
|
|
46
|
+
try {
|
|
47
|
+
selectedProfile = await listSelect(rl, ' Enter number or name [generic]: ', options, 'generic', signal);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
rl.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Write profile config to project namespace
|
|
54
|
+
if (selectedProfile && !dryRunAddon) {
|
|
55
|
+
const namespace = topology.namespace || `.aiwg/${framework}`;
|
|
56
|
+
const configDir = path.join(target, namespace);
|
|
57
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
58
|
+
const profileConfig = {
|
|
59
|
+
profile: selectedProfile,
|
|
60
|
+
pageTemplate: `templates/${selectedProfile}.md`,
|
|
61
|
+
selectedAt: new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
await fs.writeFile(path.join(configDir, 'config.json'), JSON.stringify(profileConfig, null, 2) + '\n');
|
|
64
|
+
ui.success(`Profile "${selectedProfile}" selected → ${namespace}/config.json`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Profile selection is optional — don't fail deployment
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=use-addon-profile.js.map
|