@aiwg/cli 2026.10.6 → 2026.10.8

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 (100) hide show
  1. package/bin/aiwg.mjs +4 -2
  2. package/dist/src/a2a/agent-card.js +11 -0
  3. package/dist/src/api/index.d.ts +1 -0
  4. package/dist/src/api/index.js +1 -0
  5. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  6. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  7. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  8. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  9. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  10. package/dist/src/artifacts/index-preparation.js +2 -0
  11. package/dist/src/auth/client.js +5 -1
  12. package/dist/src/auth/credential-store.js +5 -1
  13. package/dist/src/auth/sensitive-input.js +151 -0
  14. package/dist/src/channel/manager.mjs +3 -0
  15. package/dist/src/cli/handlers/agentcard.js +47 -25
  16. package/dist/src/cli/handlers/auth.js +22 -2
  17. package/dist/src/cli/handlers/cost-report.js +7 -1
  18. package/dist/src/cli/handlers/decision.js +36 -10
  19. package/dist/src/cli/handlers/effect.js +4 -84
  20. package/dist/src/cli/handlers/guided-setup.js +1 -1
  21. package/dist/src/cli/handlers/init.js +1 -1
  22. package/dist/src/cli/handlers/install.js +6 -0
  23. package/dist/src/cli/handlers/local-executor.js +33 -6
  24. package/dist/src/cli/handlers/marketplace.js +15 -4
  25. package/dist/src/cli/handlers/setup.js +24 -20
  26. package/dist/src/cli/handlers/use.js +57 -15
  27. package/dist/src/cli/prompt-utils.js +73 -0
  28. package/dist/src/config/gitignore.js +4 -0
  29. package/dist/src/config/workspace.js +2 -1
  30. package/dist/src/cost/fleet-report.js +20 -22
  31. package/dist/src/decision/adapters/jev.js +20 -31
  32. package/dist/src/decision/ask.js +6 -1
  33. package/dist/src/decision/benchmark.js +19 -6
  34. package/dist/src/decision/driver.js +2 -2
  35. package/dist/src/decision/jev-credentials.js +18 -5
  36. package/dist/src/decision/ledger.js +230 -34
  37. package/dist/src/decision/qualification/gate-evidence.js +6 -4
  38. package/dist/src/decision/qualification/load-staged.js +47 -10
  39. package/dist/src/decision/qualification/runner.js +7 -3
  40. package/dist/src/discovery/interview.js +10 -5
  41. package/dist/src/discovery/plan-apply.js +26 -30
  42. package/dist/src/discovery/plan-validation.js +2 -2
  43. package/dist/src/effects/host.js +91 -0
  44. package/dist/src/extensions/commands/definitions.js +2 -2
  45. package/dist/src/extensions/project-local-gitignore.js +2 -0
  46. package/dist/src/issues/cli.js +16 -5
  47. package/dist/src/issues/live.js +44 -12
  48. package/dist/src/marketplace/exchange.js +18 -6
  49. package/dist/src/marketplace/provenance.js +3 -2
  50. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  51. package/dist/src/mcp/cli.mjs +122 -109
  52. package/dist/src/mcp/credentials.d.mts +4 -0
  53. package/dist/src/mcp/credentials.mjs +44 -3
  54. package/dist/src/mcp/grok-build-config.mjs +4 -1
  55. package/dist/src/mcp/muse-native-config.mjs +2 -1
  56. package/dist/src/mcp/omp-config.mjs +2 -0
  57. package/dist/src/mcp/registry.js +19 -15
  58. package/dist/src/mcp/registry.mjs +2 -18
  59. package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
  60. package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
  61. package/dist/src/memory/project-registry.js +2 -1
  62. package/dist/src/mission-protocol/worker-status.js +91 -1
  63. package/dist/src/models/grok-build-models.js +20 -0
  64. package/dist/src/models/worker-installation.d.mts +1 -1
  65. package/dist/src/models/worker-installation.mjs +65 -4
  66. package/dist/src/packages/adapters/git.js +99 -23
  67. package/dist/src/packages/adapters/gitea.js +8 -27
  68. package/dist/src/packages/adapters/github.js +7 -25
  69. package/dist/src/packages/registry.js +4 -4
  70. package/dist/src/process/cli-command.d.mts +5 -0
  71. package/dist/src/process/cli-command.mjs +42 -0
  72. package/dist/src/providers/provider-definitions.js +2 -2
  73. package/dist/src/serve/shared-host-scheduler.js +4 -1
  74. package/dist/src/serve/worker-invocation.js +10 -0
  75. package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
  76. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  77. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  78. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  79. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  80. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  81. package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
  82. package/dist/src/storage/backends/fortemi.js +107 -49
  83. package/dist/src/tracker/project-inputs.js +2 -0
  84. package/dist/src/update/notifier.mjs +2 -4
  85. package/package.json +1 -1
  86. package/tools/agents/providers/base.mjs +5 -1
  87. package/tools/agents/providers/claude.mjs +5 -2
  88. package/tools/agents/providers/codex.mjs +4 -1
  89. package/tools/agents/providers/context-write.mjs +28 -0
  90. package/tools/agents/providers/copilot.mjs +5 -2
  91. package/tools/agents/providers/hermes.mjs +5 -2
  92. package/tools/agents/providers/omp.mjs +3 -1
  93. package/tools/agents/providers/rule-delivery.mjs +29 -12
  94. package/tools/agents/providers/warp.mjs +4 -1
  95. package/tools/agents/providers/windsurf.mjs +6 -3
  96. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  97. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  98. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  99. package/tools/ralph-external/process-monitor.mjs +42 -4
  100. package/tools/ralph-external/session-launcher.mjs +125 -3
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
+ import { sensitiveInputOptions } from '../../auth/sensitive-input.js';
12
13
  import { searchSkills } from '../../skills/registry.js';
13
14
  import { installPackage, listInstalledPackages } from '../../packages/registry.js';
14
15
  import { GitAdapter } from '../../packages/adapters/git.js';
@@ -24,7 +25,7 @@ const USAGE = [
24
25
  ' aiwg marketplace verify <package|lock> Verify cached content and receipts offline',
25
26
  ' aiwg marketplace export <package> --output <archive.json>',
26
27
  ' aiwg marketplace import <archive.json> [--project-local]',
27
- ' aiwg marketplace publish <source> --key <pem> --publisher <id>',
28
+ ' aiwg marketplace publish <source> --publisher <id> [--key-file <pem> | --key-stdin]',
28
29
  ' aiwg marketplace remove <catalog-id>',
29
30
  ' aiwg marketplace list List installed marketplace packages',
30
31
  '',
@@ -33,6 +34,11 @@ const USAGE = [
33
34
  ' --project-local Store/read state below the target project .aiwg directory',
34
35
  ' --global Store/read state in the user AIWG directory (default)',
35
36
  ' --policy <name> Named trust policy or JSON policy path',
37
+ 'Git fetch token sources:',
38
+ ' --token-interactive | --token-stdin | --token-file <path> | --token-store <name> | --token-env <name>',
39
+ 'Publish key sources (multiline PEM):',
40
+ ' --key-interactive | --key-stdin | --key-file <path> | --key-store <name> | --key-env <name>',
41
+ ' --key <path> Compatibility alias for --key-file (private file required)',
36
42
  ].join('\n');
37
43
  function parseFlag(args, flag) {
38
44
  const idx = args.indexOf(flag);
@@ -105,7 +111,10 @@ async function handleAdd(ctx) {
105
111
  const requestedRef = parseFlag(ctx.args, '--ref') ?? 'HEAD';
106
112
  const adapter = new GitAdapter();
107
113
  const source = { gitUrl: argument, ref: requestedRef, label: argument };
108
- const cachePath = await adapter.fetch(source, { refresh: hasFlag(ctx.args, '--refresh') });
114
+ const cachePath = await adapter.fetch(source, {
115
+ refresh: hasFlag(ctx.args, '--refresh'),
116
+ tokenInput: sensitiveInputOptions(ctx.args, 'token'),
117
+ });
109
118
  const identity = await inspectGitCheckout(cachePath);
110
119
  const candidates = [
111
120
  path.join(cachePath, 'aiwg-marketplace-catalog.json'),
@@ -251,6 +260,7 @@ async function handleInstall(ctx) {
251
260
  }
252
261
  const installed = await installPackage(rawRef, {
253
262
  configDir,
263
+ tokenInput: sensitiveInputOptions(ctx.args, 'token'),
254
264
  ref,
255
265
  packageSelector: parseFlag(ctx.args, '--package'),
256
266
  refresh: hasFlag(ctx.args, '--refresh'),
@@ -342,8 +352,8 @@ async function handlePublish(ctx) {
342
352
  return argument;
343
353
  const privateKeyPath = parseFlag(ctx.args, '--key');
344
354
  const publisher = parseFlag(ctx.args, '--publisher');
345
- if (!privateKeyPath || !publisher) {
346
- return { exitCode: 1, message: 'Error: Usage: aiwg marketplace publish <source> --key <ed25519.pem> --publisher <id> [--output <dir>]' };
355
+ if (!publisher) {
356
+ return { exitCode: 1, message: 'Error: Usage: aiwg marketplace publish <source> [--key-file <ed25519.pem> | --key-stdin] --publisher <id> [--output <dir>]' };
347
357
  }
348
358
  try {
349
359
  const outputDir = parseFlag(ctx.args, '--output') ?? path.join(path.resolve(argument), '.aiwg', 'marketplace');
@@ -355,6 +365,7 @@ async function handlePublish(ctx) {
355
365
  sourceDir: argument,
356
366
  outputDir,
357
367
  privateKeyPath,
368
+ keyInput: sensitiveInputOptions(ctx.args, 'key'),
358
369
  publicKeyPath: parseFlag(ctx.args, '--public-key'),
359
370
  keyId: parseFlag(ctx.args, '--key-id'),
360
371
  publisher,
@@ -1,4 +1,7 @@
1
1
  import { boundedSpawnSync } from '../../process/bounded-child-process.mjs';
2
+ import { helperEnvironment, HELPER_ENV_ALLOWLIST } from '../../process/cli-command.mjs';
3
+ import { readPrivateValueFile } from '../../auth/sensitive-input.js';
4
+ import { setupJev } from '../../decision/driver.js';
2
5
  import { existsSync } from 'fs';
3
6
  import { emptyConfig, FORCE_PUSH_POLICY_ALIAS_NOTE, normalizeForcePushPolicy as normalizeForcePushPolicyShared, getConfigPath, getProjectDir, readAiwgConfig, resolveRemoteProvider, VALID_PROVIDERS, writeAiwgConfig, } from '../../config/aiwg-config.js';
4
7
  import { AiwgError, EXIT_CODES } from '../errors.js';
@@ -133,6 +136,7 @@ export function parseSetupProjectOptions(ctx) {
133
136
  }
134
137
  export function detectGitRemotes(projectDir) {
135
138
  const result = boundedSpawnSync('git', ['-C', projectDir, 'remote', '-v'], 'quick', {
139
+ env: helperEnvironment(),
136
140
  encoding: 'utf-8',
137
141
  stdio: ['ignore', 'pipe', 'ignore'],
138
142
  });
@@ -156,6 +160,7 @@ export function detectGitRemotes(projectDir) {
156
160
  }
157
161
  function detectDefaultBranch(projectDir) {
158
162
  const symbolic = boundedSpawnSync('git', ['-C', projectDir, 'symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], 'quick', {
163
+ env: helperEnvironment(),
159
164
  encoding: 'utf-8',
160
165
  stdio: ['ignore', 'pipe', 'ignore'],
161
166
  });
@@ -163,6 +168,7 @@ function detectDefaultBranch(projectDir) {
163
168
  if (symbolic.status === 0 && fromOrigin)
164
169
  return fromOrigin;
165
170
  const branch = boundedSpawnSync('git', ['-C', projectDir, 'branch', '--show-current'], 'quick', {
171
+ env: helperEnvironment(),
166
172
  encoding: 'utf-8',
167
173
  stdio: ['ignore', 'pipe', 'ignore'],
168
174
  });
@@ -170,6 +176,7 @@ function detectDefaultBranch(projectDir) {
170
176
  }
171
177
  function gitConfig(projectDir, key) {
172
178
  const result = boundedSpawnSync('git', ['-C', projectDir, 'config', '--get', key], 'quick', {
179
+ env: helperEnvironment(),
173
180
  encoding: 'utf-8',
174
181
  stdio: ['ignore', 'pipe', 'ignore'],
175
182
  });
@@ -656,7 +663,7 @@ function defaultGuidedApplyIO(plan, fresh, ctx, projectDir, frameworkRoot, canPr
656
663
  chunks.push(Buffer.from(chunk));
657
664
  return Buffer.concat(chunks).toString('utf8');
658
665
  } } : {}),
659
- readCredentialFile: async (path) => readFile(path, 'utf8'),
666
+ readCredentialFile: async (path) => readPrivateValueFile(path, '--credential'),
660
667
  // stdin-at-apply prompts for the value without echoing it anywhere: the
661
668
  // label names the consumer only, and the value never reaches output.
662
669
  ...(canPrompt ? { promptCredential: async (label) => {
@@ -674,26 +681,20 @@ function defaultGuidedApplyIO(plan, fresh, ctx, projectDir, frameworkRoot, canPr
674
681
  await writeSetupPlan(projectDir, next);
675
682
  },
676
683
  owners: {
677
- configureJev: async ({ action, token, options, credential }) => {
684
+ configureJev: async ({ action, token, options }) => {
678
685
  if (options != null)
679
686
  validateJevOptions(options);
680
687
  const settings = (options ?? {});
681
- const args = ['decision', 'setup', 'jev', ...(settings.remove === true ? ['--remove'] : ['--token-stdin']),
682
- ...(typeof settings.region === 'string' ? ['--region', settings.region] : []),
683
- ...(typeof settings.endpoint === 'string' ? ['--endpoint', settings.endpoint] : []),
684
- ...(settings.verify === true ? ['--verify'] : [])];
685
- const environment = { ...process.env };
686
- delete environment.JEV_API_KEY;
687
- delete environment.AIWG_DECISION_JEV_API_KEY;
688
- if (credential?.source === 'env')
689
- delete environment[credential.name];
690
- const child = boundedSpawnSync(process.execPath, [join(frameworkRoot, 'bin', 'aiwg.mjs'), ...args], 'network', {
691
- cwd: action.workspace, env: environment, input: token ? `${token}\n` : '', encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
692
- maxBuffer: 1024 * 1024,
693
- });
694
- if (child.error || child.status !== 0)
695
- throw new Error('Jev setup failed.');
696
- if (settings.verify === true && settings.remove !== true && JSON.parse(child.stdout).verified !== true) {
688
+ // Invoke the existing owner directly. spawnSync's input option is
689
+ // rendered by NODE_DEBUG=child_process before the child can read it.
690
+ const result = await setupJev({
691
+ token,
692
+ ...(typeof settings.region === 'string' ? { region: settings.region } : {}),
693
+ ...(typeof settings.endpoint === 'string' ? { endpoint: settings.endpoint } : {}),
694
+ verify: settings.verify === true,
695
+ remove: settings.remove === true,
696
+ }, { env: process.env, cwd: action.workspace });
697
+ if (settings.verify === true && settings.remove !== true && result.verified !== true) {
697
698
  throw new Error('Jev verification incomplete.');
698
699
  }
699
700
  },
@@ -711,7 +712,10 @@ function defaultGuidedApplyIO(plan, fresh, ctx, projectDir, frameworkRoot, canPr
711
712
  await writeAiwgConfig(action.workspace, next);
712
713
  const child = boundedSpawnSync(process.execPath, [join(frameworkRoot, 'bin', 'aiwg.mjs'),
713
714
  'doctor', '--probe-workers', '--qualify', '--yes', '--json'], 'long', {
714
- cwd: action.workspace, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 1024 * 1024,
715
+ cwd: action.workspace, env: helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST,
716
+ 'AIWG_ROOT', 'AIWG_HOME', 'AIWG_CONFIG', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME',
717
+ 'AIWG_ARTIFACTS_PATH', 'AIWG_PROJECT_ARTIFACTS_PATH', 'AIWG_PROJECT_AIWG_DIR',
718
+ 'AIWG_PROJECT_MEMORY_HOME', 'NO_UPDATE_NOTIFIER', 'NO_COLOR', 'CI']), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 1024 * 1024,
715
719
  });
716
720
  if (child.error || child.status !== 0)
717
721
  throw new Error('Worker qualification failed.');
@@ -813,7 +817,7 @@ Options:
813
817
  --intent code|docs|ops|mixed Workspace usage intent
814
818
  --jev enable|decline|defer Optional external decision service (default off)
815
819
  --region <r> --endpoint <url> --verify --remove --token-stdin Existing Jev options
816
- --credential <provider>=env:NAME|file:PATH|stdin Credential reference only
820
+ --credential <provider>=stdin|file:PATH|store:NAME|env:NAME Credential reference only
817
821
  --yes Confirm writes without prompting
818
822
  --dry-run Preview the setup plan
819
823
  --review Render the saved guided plan per workspace/provider (writes nothing)
@@ -1,3 +1,4 @@
1
+ import { ContextWriteJournal } from '../../smiths/context-pipeline/context-file-safety.js';
1
2
  import { trackTemporaryDirectory } from '../../utils/temporary-directory.js';
2
3
  /**
3
4
  * Use Command Handler
@@ -1675,6 +1676,11 @@ export async function countPiAgentFiles(target, agentsPath) {
1675
1676
  // and additive passes can describe the same destination more than once.
1676
1677
  let usePreviewActions;
1677
1678
  // Recursive framework/addon/provider passes share the boundary's resolved policy.
1679
+ async function generateDeploymentContextFiles(options) {
1680
+ const result = await generateContextFiles(options);
1681
+ useDeployment?.contextFiles.push(result);
1682
+ return result;
1683
+ }
1678
1684
  let useDeployment;
1679
1685
  function useDeploymentArgs(args) {
1680
1686
  const forwarded = [...args];
@@ -2118,7 +2124,7 @@ async function generateGlobalProjectContext(opts) {
2118
2124
  skills: userPaths.skills,
2119
2125
  behaviors: userPaths.behaviors,
2120
2126
  });
2121
- await generateContextFiles({
2127
+ await generateDeploymentContextFiles({
2122
2128
  provider: opts.provider,
2123
2129
  projectPath: opts.projectPath,
2124
2130
  sections,
@@ -2181,7 +2187,7 @@ async function ensurePostDeployPhases(opts) {
2181
2187
  behaviors: paths.behaviors,
2182
2188
  });
2183
2189
  try {
2184
- await generateContextFiles({
2190
+ await generateDeploymentContextFiles({
2185
2191
  provider: opts.provider,
2186
2192
  projectPath: opts.projectPath,
2187
2193
  sections,
@@ -2194,8 +2200,8 @@ async function ensurePostDeployPhases(opts) {
2194
2200
  },
2195
2201
  });
2196
2202
  }
2197
- catch {
2198
- // The shared verifier reports context or provider-wiring failures.
2203
+ catch (error) {
2204
+ throw new Error(`Context generation failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
2199
2205
  }
2200
2206
  }
2201
2207
  async function deploySourceDirectory(opts) {
@@ -2544,11 +2550,40 @@ export class UseHandler {
2544
2550
  useDeployment = {
2545
2551
  version: previousDeployment?.version ?? resolveDeployVersion(ctx.frameworkRoot || await getFrameworkRoot()),
2546
2552
  skipContextFiles: previousDeployment?.skipContextFiles || ctx.args.includes('--no-context-files'),
2553
+ contextFiles: previousDeployment?.contextFiles ?? [],
2547
2554
  };
2555
+ const journal = previousDeployment ? undefined : new ContextWriteJournal();
2548
2556
  try {
2549
- return await this.executeDeployment(ctx);
2557
+ let result;
2558
+ try {
2559
+ result = await this.executeDeployment(ctx);
2560
+ }
2561
+ catch (error) {
2562
+ result = { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
2563
+ }
2564
+ if (journal) {
2565
+ const rollback = result.exitCode !== 0 ? journal.rollback() : undefined;
2566
+ if (ctx.args.includes('--json')) {
2567
+ let structured;
2568
+ try {
2569
+ structured = JSON.parse(result.message || '{}');
2570
+ }
2571
+ catch {
2572
+ structured = { exitCode: result.exitCode, message: result.message };
2573
+ }
2574
+ structured.contextFiles = useDeployment.contextFiles;
2575
+ if (rollback)
2576
+ structured.contextRollback = rollback;
2577
+ result = { ...result, message: JSON.stringify(structured, null, 2), rawOutput: true };
2578
+ }
2579
+ else if (rollback?.conflicts.length) {
2580
+ result.message = [result.message, ...rollback.conflicts.map(conflict => `Context rollback conflict: ${conflict.path}: ${conflict.reason}`)].filter(Boolean).join('\n');
2581
+ }
2582
+ }
2583
+ return result;
2550
2584
  }
2551
2585
  finally {
2586
+ journal?.dispose();
2552
2587
  useDeployment = previousDeployment;
2553
2588
  }
2554
2589
  }
@@ -2639,6 +2674,9 @@ export class UseHandler {
2639
2674
  try {
2640
2675
  coreResult = await this.executeCore({ ...ctx, args: coreArgs });
2641
2676
  }
2677
+ catch (error) {
2678
+ coreResult = { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
2679
+ }
2642
2680
  finally {
2643
2681
  usePreviewActions = previousPreview;
2644
2682
  if (json || dryRun) {
@@ -2664,12 +2702,18 @@ export class UseHandler {
2664
2702
  else {
2665
2703
  if (coreResult.exitCode === 0 && !dryRun && !VALID_FRAMEWORKS.includes(requestedBundle)) {
2666
2704
  for (const provider of providers) {
2667
- await ensurePostDeployPhases({
2668
- frameworkRoot,
2669
- projectPath: projectDir,
2670
- provider,
2671
- args: remainingArgs,
2672
- });
2705
+ try {
2706
+ await ensurePostDeployPhases({
2707
+ frameworkRoot,
2708
+ projectPath: projectDir,
2709
+ provider,
2710
+ args: remainingArgs,
2711
+ });
2712
+ }
2713
+ catch (error) {
2714
+ coreResult = { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
2715
+ break;
2716
+ }
2673
2717
  }
2674
2718
  }
2675
2719
  const providerResults = [];
@@ -4377,7 +4421,7 @@ export class UseHandler {
4377
4421
  skills: paths.skills,
4378
4422
  behaviors: paths.behaviors,
4379
4423
  });
4380
- const ctxResult = await generateContextFiles({
4424
+ const ctxResult = await generateDeploymentContextFiles({
4381
4425
  provider: provider,
4382
4426
  projectPath: target,
4383
4427
  sections,
@@ -4431,10 +4475,8 @@ export class UseHandler {
4431
4475
  }
4432
4476
  }
4433
4477
  catch (err) {
4434
- // Non-fatal: context-pipeline emission must not block deployment.
4435
- // Operator can re-run aiwg use to retry.
4436
4478
  const msg = err instanceof Error ? err.message : String(err);
4437
- console.error(`Warning: context-pipeline emission failed: ${msg}`);
4479
+ return { exitCode: 1, message: `Context generation failed: ${msg}` };
4438
4480
  }
4439
4481
  }
4440
4482
  span.end('use:complete', { framework });
@@ -88,7 +88,16 @@ export function askWithTimeout(rl, prompt, parse, fallback, fallbackLabel, timeo
88
88
  const cleanup = () => {
89
89
  clearTimeout(timer);
90
90
  signal?.removeEventListener('abort', onAbort);
91
+ rl.removeListener('close', onClose);
91
92
  };
93
+ const onClose = () => {
94
+ if (settled)
95
+ return;
96
+ settled = true;
97
+ cleanup();
98
+ resolve(fallback);
99
+ };
100
+ rl.on('close', onClose);
92
101
  if (signal?.aborted) {
93
102
  // Fire synchronously so the caller's try/catch sees the reject without
94
103
  // awaiting a tick.
@@ -112,6 +121,70 @@ export function askWithTimeout(rl, prompt, parse, fallback, fallbackLabel, timeo
112
121
  export async function askString(rl, prompt, fallback = '', signal) {
113
122
  return askWithTimeout(rl, prompt, (a) => a.trim(), fallback, fallback || '(empty)', promptTimeoutMs(), signal);
114
123
  }
124
+ /** Collect a hidden paste through an empty line, with one overall prompt deadline. */
125
+ export function askMultilineSecret(rl, signal) {
126
+ return new Promise((resolve, reject) => {
127
+ const lines = [];
128
+ let length = 0;
129
+ let settled = false;
130
+ const cleanup = () => {
131
+ clearTimeout(timer);
132
+ signal?.removeEventListener('abort', onAbort);
133
+ rl.removeListener('line', onLine);
134
+ rl.removeListener('close', onClose);
135
+ lines.length = 0;
136
+ };
137
+ const finish = (value) => {
138
+ if (settled)
139
+ return;
140
+ settled = true;
141
+ cleanup();
142
+ resolve(value);
143
+ };
144
+ const onLine = (line) => {
145
+ if (!line) {
146
+ finish(lines.join('\n'));
147
+ return;
148
+ }
149
+ length += line.length + (lines.length ? 1 : 0);
150
+ if (length > 16384) {
151
+ settled = true;
152
+ cleanup();
153
+ reject(new Error('Sensitive input exceeds 16384 characters.'));
154
+ return;
155
+ }
156
+ lines.push(line);
157
+ };
158
+ // A closed terminal or timeout discards incomplete input, never echoes it.
159
+ const onClose = () => finish('');
160
+ const onAbort = () => {
161
+ if (settled)
162
+ return;
163
+ settled = true;
164
+ cleanup();
165
+ try {
166
+ rl.close();
167
+ }
168
+ catch { /* already closed */ }
169
+ reject(signal.reason ?? new Error('Aborted'));
170
+ };
171
+ const timeoutMs = promptTimeoutMs();
172
+ const timer = setTimeout(() => {
173
+ if (settled)
174
+ return;
175
+ ui.warn(` No input completed within ${Math.round(timeoutMs / 1000)}s — discarding incomplete sensitive input`);
176
+ finish('');
177
+ }, timeoutMs);
178
+ timer.unref?.();
179
+ rl.on('line', onLine);
180
+ rl.on('close', onClose);
181
+ if (signal?.aborted) {
182
+ onAbort();
183
+ return;
184
+ }
185
+ signal?.addEventListener('abort', onAbort, { once: true });
186
+ });
187
+ }
115
188
  /**
116
189
  * Prompt for a yes/no answer with timeout and default. Answers starting with
117
190
  * 'y' (case-insensitive) are considered yes; everything else is no. On
@@ -13,6 +13,7 @@ import * as path from 'path';
13
13
  import { execFile } from 'child_process';
14
14
  import { promisify } from 'util';
15
15
  import { boundedSpawnSync } from '../process/bounded-child-process.mjs';
16
+ import { helperEnvironment, HELPER_ENV_ALLOWLIST } from '../process/cli-command.mjs';
16
17
  const execFileAsync = promisify(execFile);
17
18
  const GITIGNORE_PROBE_BASENAME = '.aiwg-ignore-probe';
18
19
  // ===========================
@@ -94,6 +95,7 @@ async function isGitWorktree(projectRoot) {
94
95
  try {
95
96
  const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], {
96
97
  cwd: projectRoot,
98
+ env: helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, 'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR']),
97
99
  windowsHide: true,
98
100
  });
99
101
  return stdout.trim() === 'true';
@@ -117,6 +119,7 @@ async function resolveCoveredPatterns(projectRoot, patterns, lines) {
117
119
  try {
118
120
  await execFileAsync('git', ['check-ignore', '--quiet', '--', probePath(pattern)], {
119
121
  cwd: projectRoot,
122
+ env: helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, 'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR']),
120
123
  windowsHide: true,
121
124
  });
122
125
  return true;
@@ -239,6 +242,7 @@ export function dropGitignored(targetDir, files) {
239
242
  }
240
243
  const result = boundedSpawnSync('git', ['check-ignore', '-z', '--stdin'], 'quick', {
241
244
  cwd: targetDir,
245
+ env: helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, 'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR']),
242
246
  input: `${files.join('\0')}\0`,
243
247
  encoding: 'utf8',
244
248
  });
@@ -13,6 +13,7 @@ import { existsSync, statSync } from 'fs';
13
13
  import { homedir } from 'os';
14
14
  import { dirname, isAbsolute, relative, resolve } from 'path';
15
15
  import { promisify } from 'util';
16
+ import { helperEnvironment } from '../process/cli-command.mjs';
16
17
  import { getConfigPath, readAiwgConfig, resolveDelivery, resolveRemoteProvider, resolveRemotes, } from './aiwg-config.js';
17
18
  const execFileAsync = promisify(execFile);
18
19
  function expandHome(value) {
@@ -96,7 +97,7 @@ async function readRemoteUrl(repoPath, remote) {
96
97
  if (remote.includes('://') || /^[^@/\s]+@[^:/\s]+:/.test(remote))
97
98
  return remote;
98
99
  try {
99
- const { stdout } = await execFileAsync('git', ['-C', repoPath, 'remote', 'get-url', remote], { encoding: 'utf8' });
100
+ const { stdout } = await execFileAsync('git', ['-C', repoPath, 'remote', 'get-url', remote], { encoding: 'utf8', env: helperEnvironment() });
100
101
  return stdout.trim() || undefined;
101
102
  }
102
103
  catch {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * OpenRouter fleet spend observation and local activity correlation.
3
3
  *
4
- * Secrets are resolved only from the process environment or the user's
4
+ * Secrets are resolved from named sources or the user's
5
5
  * ~/.config/aiwg/keys directory. Fleet configuration contains references,
6
6
  * never credential values.
7
7
  *
@@ -11,6 +11,7 @@ import { promises as fs } from 'node:fs';
11
11
  import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import { load as loadYaml } from 'js-yaml';
14
+ import { resolveSensitiveInput } from '../auth/sensitive-input.js';
14
15
  const OPENROUTER_API = 'https://openrouter.ai/api/v1';
15
16
  const KEY_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
16
17
  const SECRET_FIELD_PATTERN = /(?:api[_-]?key|token|secret|credential|authorization)/i;
@@ -60,6 +61,9 @@ function validateFleetConfig(value) {
60
61
  if (!KEY_REF_PATTERN.test(entry.key_ref)) {
61
62
  throw new Error(`fleet[${index}].key_ref contains unsupported characters.`);
62
63
  }
64
+ if (entry.key_store !== undefined && (typeof entry.key_store !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(entry.key_store))) {
65
+ throw new Error(`fleet[${index}].key_store must name a native-store entry (1–128 letters, digits, dots, underscores or hyphens).`);
66
+ }
63
67
  if (typeof entry.monthly_cap !== 'number' || !Number.isFinite(entry.monthly_cap) || entry.monthly_cap < 0) {
64
68
  throw new Error(`fleet[${index}].monthly_cap must be a non-negative number.`);
65
69
  }
@@ -73,6 +77,7 @@ function validateFleetConfig(value) {
73
77
  bot: entry.bot.trim(),
74
78
  machine: entry.machine.trim(),
75
79
  key_ref: entry.key_ref,
80
+ ...(entry.key_store !== undefined ? { key_store: entry.key_store } : {}),
76
81
  monthly_cap: entry.monthly_cap,
77
82
  };
78
83
  });
@@ -101,10 +106,16 @@ export async function loadFleetKey(keyRef, options = {}) {
101
106
  if (!KEY_REF_PATTERN.test(keyRef))
102
107
  throw new Error('Invalid key_ref.');
103
108
  const env = options.env ?? process.env;
109
+ const dependencies = { environment: env, readStore: options.readStore, isTTY: false };
110
+ // An explicit reference is scoped to this entry. Never probe a global key or
111
+ // another bot's store when the selected source is unavailable.
112
+ if (options.store !== undefined) {
113
+ return resolveSensitiveInput({ prefix: 'key', store: options.store }, dependencies);
114
+ }
104
115
  const environmentName = keyEnvironmentName(keyRef);
105
116
  const environmentValue = env[environmentName]?.trim();
106
117
  if (environmentValue)
107
- return environmentValue;
118
+ return resolveSensitiveInput({ prefix: 'key', env: environmentName }, dependencies);
108
119
  const homeDir = options.homeDir ?? os.homedir();
109
120
  const keyDirectory = path.resolve(homeDir, '.config', 'aiwg', 'keys');
110
121
  const keyPath = path.resolve(keyDirectory, keyRef);
@@ -116,7 +127,7 @@ export async function loadFleetKey(keyRef, options = {}) {
116
127
  }
117
128
  catch (error) {
118
129
  if (error.code === 'ENOENT') {
119
- throw new Error(`No secure credential directory exists for key_ref '${keyRef}'; use ${environmentName} or create ~/.config/aiwg/keys with mode 0700.`);
130
+ throw new Error(`No secure credential directory exists for key_ref '${keyRef}'; use ${environmentName}, --key-store <name> (fleet: key_store), or create ~/.config/aiwg/keys with mode 0700.`);
120
131
  }
121
132
  throw error;
122
133
  }
@@ -129,27 +140,12 @@ export async function loadFleetKey(keyRef, options = {}) {
129
140
  if (typeof process.getuid === 'function' && directoryStat.uid !== process.getuid()) {
130
141
  throw new Error('The AIWG credential directory must be owned by the current user.');
131
142
  }
132
- let stat;
133
143
  try {
134
- stat = await fs.lstat(keyPath);
144
+ return await resolveSensitiveInput({ prefix: 'key', file: keyPath }, dependencies);
135
145
  }
136
146
  catch (error) {
137
- if (error.code === 'ENOENT') {
138
- throw new Error(`No credential found for key_ref '${keyRef}' in the secure key directory or ${environmentName}.`);
139
- }
140
- throw error;
141
- }
142
- if (stat.isSymbolicLink() || !stat.isFile())
143
- throw new Error(`Credential for key_ref '${keyRef}' must be a regular file, not a symlink.`);
144
- if ((stat.mode & 0o077) !== 0)
145
- throw new Error(`Credential file for key_ref '${keyRef}' must not be accessible by group or other users.`);
146
- if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
147
- throw new Error(`Credential file for key_ref '${keyRef}' must be owned by the current user.`);
147
+ throw new Error(`Credential for key_ref '${keyRef}': ${error.message} Use ${environmentName}, --key-store <name> (fleet: key_store), or a private ~/.config/aiwg/keys/<key_ref> file.`);
148
148
  }
149
- const value = (await fs.readFile(keyPath, 'utf8')).trim();
150
- if (!value)
151
- throw new Error(`Credential file for key_ref '${keyRef}' is empty.`);
152
- return value;
153
149
  }
154
150
  function parseActivityLog(raw) {
155
151
  const entries = [];
@@ -202,7 +198,9 @@ async function openRouterGet(endpoint, key, options) {
202
198
  }
203
199
  if (!payload || typeof payload !== 'object' || !payload.data)
204
200
  throw new Error('OpenRouter returned an invalid response.');
205
- return payload.data;
201
+ // Generation metadata can reflect a request header in free-text fields. Keep
202
+ // such content out of both structured and human-readable reports.
203
+ return JSON.parse(JSON.stringify(payload.data, (_name, value) => (typeof value === 'string' ? value.split(key).join('[REDACTED]') : value)));
206
204
  }
207
205
  function correlateGenerations(records) {
208
206
  const sessions = new Map();
@@ -223,7 +221,7 @@ function correlateGenerations(records) {
223
221
  }
224
222
  async function reportBot(entry, activity, options) {
225
223
  try {
226
- const key = await loadFleetKey(entry.key_ref, options);
224
+ const key = await loadFleetKey(entry.key_ref, { ...options, store: entry.key_store });
227
225
  const keyData = await openRouterGet('/key', key, options);
228
226
  const spend = Number(keyData.usage_monthly ?? keyData.usage ?? 0);
229
227
  if (!Number.isFinite(spend) || spend < 0)
@@ -44,22 +44,15 @@ export class JevDecisionAdapter {
44
44
  this.region = options.region?.trim() ? options.region : null;
45
45
  this.observeResponseBody = options.observeResponseBody;
46
46
  }
47
- /** Used only for non-2xx error bodies, where there is no separate successful parse to stay consistent with. */
48
- observeResponse(body) {
49
- if (!this.observeResponseBody)
50
- return;
51
- let parsed = {};
52
- try {
53
- parsed = asRecord(parseBoundedJson(body));
54
- }
55
- catch { /* Invalid answers still have bounded body evidence. */ }
56
- let usage = emptyUsage();
47
+ /** An error response may still report billed usage and correlation metadata. */
48
+ parseErrorResponse(body) {
49
+ let parsed;
57
50
  try {
58
- usage = normalizeUsage(parsed.usage);
51
+ parsed = parseBoundedJson(body);
59
52
  }
60
- catch { /* Missing usage is unknown, including on auth errors. */ }
61
- this.observeResponseBody(body, { actualModel: typeof parsed.model === 'string' && parsed.model ? parsed.model : null,
62
- usage });
53
+ catch { /* Error classification does not require valid JSON. */ }
54
+ this.observeParsed(body, parsed);
55
+ return parsed;
63
56
  }
64
57
  /**
65
58
  * Derives the observer's model/usage from a value `parseBoundedJson` has already produced (or `undefined`
@@ -81,6 +74,15 @@ export class JevDecisionAdapter {
81
74
  catch { /* Missing usage is unknown, including on auth errors. */ }
82
75
  return { actualModel: typeof record.model === 'string' && record.model ? record.model : null, usage };
83
76
  }
77
+ responseMetadata(parsed, metadata) {
78
+ let body = {};
79
+ try {
80
+ body = asRecord(parsed);
81
+ }
82
+ catch { /* No body correlation metadata. */ }
83
+ const id = metadata.requestId ? null : safeRequestId(body.request_id ?? body.requestId);
84
+ return { ...metadata, ...this.observationMetadataOf(parsed), ...(id ? { requestId: id, requestIdSource: 'body' } : {}) };
85
+ }
84
86
  /** Observes exactly once, from the single parse attempt `parsed` already represents (successful or not). */
85
87
  observeParsed(body, parsed) {
86
88
  this.observeResponseBody?.(body, this.observationMetadataOf(parsed));
@@ -209,22 +211,7 @@ export class JevDecisionAdapter {
209
211
  if (!response.ok) {
210
212
  try {
211
213
  const errorBody = await readBoundedBody(response, signal);
212
- this.observeResponse(errorBody);
213
- if (!metadata.requestId && response.headers.get('content-type')?.includes('json')) {
214
- let parsedError;
215
- try {
216
- parsedError = parseBoundedJson(errorBody);
217
- }
218
- catch {
219
- parsedError = null;
220
- }
221
- if (parsedError && typeof parsedError === 'object' && !Array.isArray(parsedError)) {
222
- const field = parsedError.request_id ?? parsedError.requestId;
223
- const safe = safeRequestId(field);
224
- if (safe)
225
- metadata = { ...metadata, requestId: safe, requestIdSource: 'body' };
226
- }
227
- }
214
+ metadata = this.responseMetadata(this.parseErrorResponse(errorBody), metadata);
228
215
  }
229
216
  catch {
230
217
  if (request.signal.aborted)
@@ -248,6 +235,7 @@ export class JevDecisionAdapter {
248
235
  finally {
249
236
  this.observeParsed(encoded, parsed);
250
237
  }
238
+ metadata = this.responseMetadata(parsed, metadata);
251
239
  if (request.signal.aborted)
252
240
  return externalInterruption(request, 'terminal-response', metadata);
253
241
  if (deadline.aborted)
@@ -353,7 +341,7 @@ export class JevDecisionAdapter {
353
341
  }
354
342
  if (!response.ok) {
355
343
  try {
356
- this.observeResponse(await readBoundedBody(response, signal));
344
+ metadata = this.responseMetadata(this.parseErrorResponse(await readBoundedBody(response, signal)), metadata);
357
345
  }
358
346
  catch {
359
347
  return batchFailure(requests, failure('invalid-output', metadata));
@@ -371,6 +359,7 @@ export class JevDecisionAdapter {
371
359
  finally {
372
360
  this.observeParsed(encoded, parsed);
373
361
  }
362
+ metadata = this.responseMetadata(parsed, metadata);
374
363
  const normalized = normalizeBatchResponse(requests, parsed).map(answer => ({
375
364
  questionId: answer.questionId,
376
365
  observation: { ...answer.observation, ...metadata },