@aiwg/cli 2026.10.0 → 2026.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3091 -24
- package/bin/aiwg.mjs +86 -21
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/channel/manager.mjs +59 -10
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/decision.js +297 -2
- package/dist/src/cli/handlers/diagnose.js +9 -2
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/init.js +245 -30
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +69 -6
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +16 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/runtime-info.js +24 -9
- package/dist/src/cli/handlers/script-runner.js +16 -6
- package/dist/src/cli/handlers/session.js +396 -62
- package/dist/src/cli/handlers/sessions.js +146 -40
- package/dist/src/cli/handlers/setup-manifest.js +439 -39
- package/dist/src/cli/handlers/setup.js +139 -39
- package/dist/src/cli/handlers/subcommands.js +3 -1
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/use.js +13 -0
- package/dist/src/cli/handlers/utilities.js +106 -23
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/config/aiwg-config.js +268 -4
- package/dist/src/config/cli.js +146 -19
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/user-config.js +36 -10
- package/dist/src/decision/adapters/jev.js +80 -28
- package/dist/src/decision/ask.js +352 -0
- package/dist/src/decision/driver.js +4 -0
- package/dist/src/decision/entry.js +38 -6
- package/dist/src/decision/heldout/collector.js +10 -1
- package/dist/src/decision/index.js +3 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/jev-credentials.js +121 -0
- package/dist/src/decision/ledger.js +839 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +3 -1
- package/dist/src/installation/manager-command.mjs +1 -1
- package/dist/src/installation/manager.mjs +17 -2
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/adapters/codex-runtime.js +360 -56
- package/dist/src/mcp/cli.mjs +587 -77
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.mjs +16 -26
- package/dist/src/mcp/helpers.mjs +106 -2
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +264 -57
- package/dist/src/mcp/registry.mjs +198 -45
- package/dist/src/mcp/server.mjs +403 -20
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +58 -0
- package/dist/src/mcp/tools/orchestration.mjs +38 -0
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +639 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +70 -13
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3091 -24
- package/dist/src/models/model-discovery.js +862 -63
- package/dist/src/models/provider-policy.js +625 -11
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +259 -0
- package/dist/src/models/worker-direct-readiness.js +529 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +131 -0
- package/dist/src/models/worker-installation.mjs +879 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.mjs +387 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/grok-build-headless.js +43 -21
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +9 -0
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +538 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +45 -0
- package/dist/src/sessions/adapters/generic.js +37 -3
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +423 -24
- package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
- package/dist/src/sessions/adapters/muse.js +68 -29
- package/dist/src/sessions/adapters/pi.js +1 -0
- package/dist/src/sessions/batch-contracts.js +13 -1
- package/dist/src/sessions/batch-import.js +23 -42
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +35 -0
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/importer.js +24 -8
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/repository.js +54 -13
- package/dist/src/sessions/workspace-discovery.js +99 -37
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +18 -1
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
- package/tools/_resolve-impl.mjs +39 -0
- package/tools/agents/deploy-agents.mjs +15 -2
- package/tools/agents/providers/base.mjs +63 -43
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +6 -3
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/providers/deepseek-harness-transport.mjs +358 -0
- package/tools/providers/omp-transport.mjs +192 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* @implements #539
|
|
9
9
|
* @see https://docs.openclaw.ai
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { boundedExecFileSync } from '../../process/bounded-child-process.mjs';
|
|
12
|
+
import { cliCommand } from '../../process/cli-command.mjs';
|
|
12
13
|
import fs from 'fs';
|
|
13
14
|
import path from 'path';
|
|
14
15
|
import { getProviderDefinition, resolveProviderPathValue, } from '../../providers/provider-definitions.js';
|
|
@@ -26,7 +27,8 @@ function resolveSkillFallbackPath(target, projectDir, name) {
|
|
|
26
27
|
*/
|
|
27
28
|
function runOpenClaw(args) {
|
|
28
29
|
try {
|
|
29
|
-
|
|
30
|
+
const [file, argv] = cliCommand('openclaw', args);
|
|
31
|
+
return boundedExecFileSync(file, argv, 'quick', {
|
|
30
32
|
encoding: 'utf-8',
|
|
31
33
|
timeout: 30000,
|
|
32
34
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -142,7 +144,8 @@ export class OpenClawAdapter {
|
|
|
142
144
|
if (fs.existsSync(openclawDir))
|
|
143
145
|
return true;
|
|
144
146
|
try {
|
|
145
|
-
|
|
147
|
+
const [file, args] = cliCommand('openclaw', ['--version']);
|
|
148
|
+
boundedExecFileSync(file, args, 'quick', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
146
149
|
return true;
|
|
147
150
|
}
|
|
148
151
|
catch {
|
|
@@ -153,7 +156,7 @@ export class OpenClawAdapter {
|
|
|
153
156
|
if (this.cachedSkills)
|
|
154
157
|
return this.cachedSkills;
|
|
155
158
|
// Try openclaw CLI with JSON output first
|
|
156
|
-
const jsonOutput = runOpenClaw('skills list --json
|
|
159
|
+
const jsonOutput = runOpenClaw(['skills', 'list', '--json']);
|
|
157
160
|
if (jsonOutput) {
|
|
158
161
|
try {
|
|
159
162
|
const parsed = JSON.parse(jsonOutput);
|
|
@@ -178,7 +181,7 @@ export class OpenClawAdapter {
|
|
|
178
181
|
}
|
|
179
182
|
async search(query) {
|
|
180
183
|
// Try CLI delegation first
|
|
181
|
-
const cliOutput = runOpenClaw(
|
|
184
|
+
const cliOutput = runOpenClaw(['skills', 'search', query, '--json']);
|
|
182
185
|
if (cliOutput) {
|
|
183
186
|
try {
|
|
184
187
|
const parsed = JSON.parse(cliOutput);
|
|
@@ -207,7 +210,7 @@ export class OpenClawAdapter {
|
|
|
207
210
|
const skillMdPath = skillDir ? path.join(skillDir, 'SKILL.md') : '';
|
|
208
211
|
if (!skillMdPath || !fs.existsSync(skillMdPath)) {
|
|
209
212
|
// Try CLI
|
|
210
|
-
const cliOutput = runOpenClaw(
|
|
213
|
+
const cliOutput = runOpenClaw(['skills', 'info', name, '--json']);
|
|
211
214
|
if (cliOutput) {
|
|
212
215
|
try {
|
|
213
216
|
const parsed = JSON.parse(cliOutput);
|
|
@@ -268,7 +271,7 @@ export class OpenClawAdapter {
|
|
|
268
271
|
}
|
|
269
272
|
async install(name, options) {
|
|
270
273
|
// Try openclaw CLI install first
|
|
271
|
-
const cliResult = runOpenClaw(
|
|
274
|
+
const cliResult = runOpenClaw(['skills', 'install', name]);
|
|
272
275
|
if (cliResult !== null) {
|
|
273
276
|
// CLI handled it — if target is different from openclaw, copy to target
|
|
274
277
|
if (options.target && options.target !== 'openclaw') {
|
|
@@ -13,9 +13,135 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @implements #1362
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
16
|
+
import { dirname } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { findPackageRoot } from '../../cli/find-package-root.js';
|
|
19
|
+
import { readAiwgConfigCandidate, validateWorkerConfigCandidate, resolveParallelism, PROVIDER_PARALLELISM_DEFAULTS } from '../../config/aiwg-config.js';
|
|
17
20
|
export const PARALLELISM_BLOCK_START = '<!-- AIWG-PARALLELISM-CAP:START -->';
|
|
18
21
|
export const PARALLELISM_BLOCK_END = '<!-- AIWG-PARALLELISM-CAP:END -->';
|
|
22
|
+
/**
|
|
23
|
+
* One-line note rendered when the bundled model defaults cannot be loaded.
|
|
24
|
+
* The orchestration health block is omitted; the rest of the section renders
|
|
25
|
+
* unchanged so context regeneration never throws for a missing home install.
|
|
26
|
+
*/
|
|
27
|
+
export const PARALLELISM_HEALTH_UNRESOLVED_NOTE = 'Health values could not be resolved from the bundled defaults, so the orchestration health block is omitted.';
|
|
28
|
+
/**
|
|
29
|
+
* Locate this package's bundled AIWG defaults (`models-v2.json` under
|
|
30
|
+
* `agentic/...`) from the running code, whether it runs from a source
|
|
31
|
+
* checkout or an installed package. Uses the canonical package-root walk
|
|
32
|
+
* shared with the rest of src/ (`findPackageRoot`), never a hand-rolled
|
|
33
|
+
* relative path and never the user's home install.
|
|
34
|
+
*/
|
|
35
|
+
function resolveBundledAiwgHome() {
|
|
36
|
+
try {
|
|
37
|
+
return findPackageRoot(dirname(fileURLToPath(import.meta.url))) ?? undefined;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the AIWG home to pass to `ConfigLoader` for this generator's own
|
|
45
|
+
* read: an explicit, non-empty `AIWG_HOME` always wins; otherwise fall back
|
|
46
|
+
* to this package's own bundled defaults so regeneration does not depend on
|
|
47
|
+
* what (if anything) is installed at the user's home. This function only
|
|
48
|
+
* ever READS `process.env.AIWG_HOME` — it never sets or deletes it, so
|
|
49
|
+
* concurrent callers and any spawned children never observe a value this
|
|
50
|
+
* generator did not itself receive from the real environment.
|
|
51
|
+
*/
|
|
52
|
+
function resolveAiwgHome() {
|
|
53
|
+
return process.env.AIWG_HOME || resolveBundledAiwgHome();
|
|
54
|
+
}
|
|
55
|
+
function isBundledDefaultsFailure(error) {
|
|
56
|
+
return error instanceof Error && error.message.includes('Failed to load AIWG default config');
|
|
57
|
+
}
|
|
58
|
+
function oneLine(value, maxLen) {
|
|
59
|
+
return value.replace(/[\r\n|]+/g, ' ').trim().slice(0, maxLen);
|
|
60
|
+
}
|
|
61
|
+
/** At most this many group summaries render; the rest collapse to "+N more". */
|
|
62
|
+
export const MAX_SHOWN_GROUPS = 8;
|
|
63
|
+
/**
|
|
64
|
+
* Config-level member exclusions for one group, mirroring the static gates
|
|
65
|
+
* in `resolveWorkerPlan()` (`src/models/resolver.ts`): a member or its CLI
|
|
66
|
+
* installation flagged `enabled: false` is statically excluded. This is
|
|
67
|
+
* NOT the live eligibility verdict — `resolveWorkerPlan()` resolves
|
|
68
|
+
* eligibility per (group, member, effort) tuple against the current
|
|
69
|
+
* catalog and request, so a member excluded nowhere here can still be
|
|
70
|
+
* excluded for a specific role/task/effort, and vice versa is never true
|
|
71
|
+
* (a statically excluded member is always excluded live too). Only the
|
|
72
|
+
* per-member reasons render; the group itself is never labeled
|
|
73
|
+
* "eligible"/"not eligible" — see the request-scoped resolve guidance
|
|
74
|
+
* below the group list.
|
|
75
|
+
*
|
|
76
|
+
* A `requiredRoles` entry with no enabled covering member is a config
|
|
77
|
+
* error `validateParallelismConfig()` (and `resolveParallelism()` /
|
|
78
|
+
* `ConfigLoader.load()`, both called earlier in `buildParallelismSection()`)
|
|
79
|
+
* already rejects outright — a config reaching this function can never
|
|
80
|
+
* have one, so that case is not re-rendered here.
|
|
81
|
+
*/
|
|
82
|
+
function excludedMembers(parallelism, group) {
|
|
83
|
+
const excluded = [];
|
|
84
|
+
for (const member of group.members) {
|
|
85
|
+
if (member.enabled === false)
|
|
86
|
+
excluded.push(`${member.id} (member disabled)`);
|
|
87
|
+
else if (parallelism?.clis?.[member.cli]?.enabled === false)
|
|
88
|
+
excluded.push(`${member.id} (installation ${member.cli} disabled)`);
|
|
89
|
+
}
|
|
90
|
+
return excluded;
|
|
91
|
+
}
|
|
92
|
+
function groupSummaries(entries, parallelism) {
|
|
93
|
+
return entries.map(([id, group]) => {
|
|
94
|
+
const roles = [...new Set(group.members.flatMap(m => m.roles ?? []))].join('/');
|
|
95
|
+
const clis = [...new Set(group.members.map(m => m.cli))].join('/');
|
|
96
|
+
const parts = [`${oneLine(id, 64)} (members: ${group.members.length}`];
|
|
97
|
+
if (clis)
|
|
98
|
+
parts.push(`clis: ${oneLine(clis, 96)}`);
|
|
99
|
+
if (roles)
|
|
100
|
+
parts.push(`roles: ${oneLine(roles, 96)}`);
|
|
101
|
+
if (group.maxParallel !== undefined)
|
|
102
|
+
parts.push(`maxParallel: ${group.maxParallel}`);
|
|
103
|
+
if (group.requiredRoles?.length)
|
|
104
|
+
parts.push(`requiredRoles: ${group.requiredRoles.join('/')}`);
|
|
105
|
+
parts.push('configured');
|
|
106
|
+
const excluded = excludedMembers(parallelism, group);
|
|
107
|
+
if (excluded.length)
|
|
108
|
+
parts.push(`excluded members: ${oneLine(excluded.join(', '), 160)}`);
|
|
109
|
+
return parts.join('; ') + ')';
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* One configured health window with its origin. The origin mirrors
|
|
114
|
+
* `resolveWorkerEffectiveHealth()` (`src/serve/worker-invocation.ts`): a
|
|
115
|
+
* field the project config leaves unset resolves from the built-in defaults.
|
|
116
|
+
*/
|
|
117
|
+
function healthValue(health, field, fallback) {
|
|
118
|
+
const value = health?.[field] ?? fallback;
|
|
119
|
+
return { value, origin: health?.[field] === undefined ? 'default' : 'project config' };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Plain-language dispatch state line (#3050 item 5). Agents must not assume
|
|
123
|
+
* other worker CLIs are unavailable just because dispatch is off — they
|
|
124
|
+
* should detect them and offer the opt-in, not silently work around it.
|
|
125
|
+
*/
|
|
126
|
+
function workerDispatchStateLine(parallelism, setup) {
|
|
127
|
+
if (parallelism?.worker_dispatch === true) {
|
|
128
|
+
return '**Direct worker dispatch**: on (`parallelism.worker_dispatch`).';
|
|
129
|
+
}
|
|
130
|
+
// A recorded decline (#3050 review item 3) means a person already said no
|
|
131
|
+
// to this offer — render the plain off state without repeating the
|
|
132
|
+
// detect-and-offer instruction, so agents stop re-litigating a settled
|
|
133
|
+
// decision every session. The decline is a setup/onboarding preference,
|
|
134
|
+
// not worker policy, so it lives in `setup`, not in the frozen
|
|
135
|
+
// `parallelism` contract fragment (B-OG-29).
|
|
136
|
+
if (setup?.worker_dispatch_offer === 'declined') {
|
|
137
|
+
return '**Direct worker dispatch**: off (`parallelism.worker_dispatch`); a prior offer was declined '
|
|
138
|
+
+ '(`setup.worker_dispatch_offer`). Do not re-offer it. It can still be turned on with '
|
|
139
|
+
+ '`aiwg config set --project parallelism.worker_dispatch true` if the user asks for it directly.';
|
|
140
|
+
}
|
|
141
|
+
return '**Direct worker dispatch**: off (`parallelism.worker_dispatch`). Do not assume other worker CLIs '
|
|
142
|
+
+ 'are unavailable — detect them yourself and offer `aiwg config set --project parallelism.worker_dispatch true` '
|
|
143
|
+
+ 'to the user if it would help.';
|
|
144
|
+
}
|
|
19
145
|
function delegationSupport(provider) {
|
|
20
146
|
switch (provider) {
|
|
21
147
|
case 'antigravity':
|
|
@@ -45,12 +171,34 @@ function delegationSupport(provider) {
|
|
|
45
171
|
* - `AIWG_HIDE_PARALLELISM_IN_CONTEXT=1` is set
|
|
46
172
|
* - No `.aiwg/aiwg.config` exists at the project path
|
|
47
173
|
*/
|
|
48
|
-
export async function buildParallelismSection(projectPath) {
|
|
174
|
+
export async function buildParallelismSection(projectPath, deps = {}) {
|
|
49
175
|
if (process.env.AIWG_HIDE_PARALLELISM_IN_CONTEXT === '1')
|
|
50
176
|
return '';
|
|
51
|
-
|
|
177
|
+
// Step 1: the project config, independently validated except for the
|
|
178
|
+
// worker-model defaults (never throws solely because bundled defaults are
|
|
179
|
+
// unreachable — any error here is a genuine config problem and stays fatal).
|
|
180
|
+
const cfg = deps.readConfig
|
|
181
|
+
? await deps.readConfig(projectPath)
|
|
182
|
+
: await readAiwgConfigCandidate(projectPath);
|
|
52
183
|
if (!cfg)
|
|
53
184
|
return '';
|
|
185
|
+
// Step 2: worker-model defaults validation, attempted only when dispatch
|
|
186
|
+
// is enabled. A bundled-defaults failure degrades (health/group details
|
|
187
|
+
// are omitted below with a one-line note); every other error stays fatal.
|
|
188
|
+
let healthUnresolved = false;
|
|
189
|
+
if (cfg.parallelism?.worker_dispatch === true) {
|
|
190
|
+
try {
|
|
191
|
+
if (deps.validateWorkerConfig)
|
|
192
|
+
await deps.validateWorkerConfig(projectPath, cfg);
|
|
193
|
+
else
|
|
194
|
+
await validateWorkerConfigCandidate(projectPath, cfg, { aiwgHome: resolveAiwgHome() });
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (!isBundledDefaultsFailure(error))
|
|
198
|
+
throw error;
|
|
199
|
+
healthUnresolved = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
54
202
|
const primary = cfg.providers[0];
|
|
55
203
|
const resolved = resolveParallelism(cfg.parallelism, primary);
|
|
56
204
|
const providerDefaults = primary && PROVIDER_PARALLELISM_DEFAULTS[primary];
|
|
@@ -98,6 +246,47 @@ export async function buildParallelismSection(projectPath) {
|
|
|
98
246
|
lines.push('');
|
|
99
247
|
lines.push(delegationSupport(primary));
|
|
100
248
|
lines.push('');
|
|
249
|
+
lines.push(workerDispatchStateLine(cfg.parallelism, cfg.setup));
|
|
250
|
+
lines.push('');
|
|
251
|
+
// Group dispatch disabled (worker_dispatch unset/false): the resolver reports
|
|
252
|
+
// `disabled`, so no group block renders — the native-delegation rubric above
|
|
253
|
+
// applies unchanged and the output stays byte-identical to the pre-dispatch
|
|
254
|
+
// rendering.
|
|
255
|
+
if (cfg.parallelism?.worker_dispatch === true && healthUnresolved) {
|
|
256
|
+
lines.push(PARALLELISM_HEALTH_UNRESOLVED_NOTE);
|
|
257
|
+
lines.push('');
|
|
258
|
+
}
|
|
259
|
+
else if (cfg.parallelism?.worker_dispatch === true) {
|
|
260
|
+
const health = cfg.parallelism?.health;
|
|
261
|
+
const check = healthValue(health, 'checkSeconds', 15);
|
|
262
|
+
const startup = healthValue(health, 'startupSeconds', 60);
|
|
263
|
+
const quiet = healthValue(health, 'quietSeconds', 90);
|
|
264
|
+
const stall = healthValue(health, 'stallSeconds', 300);
|
|
265
|
+
const hard = healthValue(health, 'hardTimeoutSeconds', 1800);
|
|
266
|
+
lines.push('### Orchestration groups (group dispatch enabled)');
|
|
267
|
+
lines.push('');
|
|
268
|
+
const entries = Object.entries(cfg.parallelism?.groups ?? {});
|
|
269
|
+
if (entries.length > 0) {
|
|
270
|
+
// Slice before summarizing so at most MAX_SHOWN_GROUPS summaries are built.
|
|
271
|
+
const shown = groupSummaries(entries.slice(0, MAX_SHOWN_GROUPS), cfg.parallelism).join(' | ');
|
|
272
|
+
const hidden = entries.length - MAX_SHOWN_GROUPS;
|
|
273
|
+
lines.push(`Configured groups: ${shown}${hidden > 0 ? ` (+${hidden} more)` : ''}`);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
lines.push('Configured groups: none — group dispatch is enabled but no groups are configured; the native delegation rubric above applies unchanged.');
|
|
277
|
+
}
|
|
278
|
+
lines.push('');
|
|
279
|
+
lines.push('A group above shown as configured, with excluded members or an uncovered required role listed, is a static config read — not a live eligibility verdict. Eligibility is resolved per (group, member, effort) tuple against the current catalog and request; see `aiwg models resolve` below to check a specific role/task/group.');
|
|
280
|
+
lines.push('');
|
|
281
|
+
lines.push('Tier 0 direct handoff is the default: use your own shell or a qualified MCP worker tool. No Mission/MC session, scheduler, daemon, or activation marker is needed. Self-count active native + CLI + MCP workers, take the minimum of the project, group, CLI, model, and context caps, and launch each writing task once.');
|
|
282
|
+
lines.push('');
|
|
283
|
+
lines.push('Resolve each bounded brief with `aiwg models resolve --role <role> --task <class> --group <group> --brief <file> --workdir <tree> --final-output <file> --transport shell --format shell`. Copy the exact rendered command or the MCP server/tool/arguments byte-for-byte; never assemble vendor flags from memory. Re-resolve after any brief, profile, model, or workspace change. `--format shell` prints only the launch command: to see the resolved health windows for an attempt, use `--format json` (the default) and read `invocation.health` with its per-field `origins`.');
|
|
284
|
+
lines.push('');
|
|
285
|
+
lines.push(`Configured health windows (each followed by its origin — \`default\` or \`project config\`): check every ${check.value}s (${check.origin}); confirm startup within ${startup.value}s (${startup.origin}); quiet limit ${quiet.value}s (${quiet.origin}); stall limit ${stall.value}s (${stall.origin}); hard stop ${hard.value}s (${hard.origin}). Per-attempt windows scale quiet and stall by the single largest applicable role/effort/model factor, never their product (for example, reasoning at \`xhigh\` effort resolves 180s/600s from the 90s/300s defaults; an effective 1.25 scale gives 112.5s/375s). Only the hard deadline and its diagnostic/termination grace periods are capped by the remaining wall-clock and transport ceilings; startup, quiet, and stall keep their configured or scaled values, and \`truncatedWindows\` instead records when one of them would exceed that capped deadline (\`resolveWorkerEffectiveHealth()\` in \`src/serve/worker-invocation.ts\`). Resolved values win over these configured values.`);
|
|
286
|
+
lines.push('');
|
|
287
|
+
lines.push('Supervise by observer state, never by suspicion. `quiet` (no observed activity for the quiet window, reported with confidence `suspected`) is not a stop: read the status (`aiwg ralph-external --status --workers --runs-root <dir> --json` is read-only and never kills or retries), check owned-worktree progress, and keep waiting — the observer’s `shouldStop()` stays false. `stalled` (no progress for the stall window) opens diagnosis through the diagnostic grace period; only `hung` (still no progress after grace, with the process live and connected) authorizes a no-progress stop — and only a worker this conductor owns. The launcher also stops independently of progress state on `failed-to-start`, the hard wall-clock deadline (`HARD_DEADLINE`), the step-count limit (`STEP_LIMIT`), and a fatal observer-record failure (`tools/ralph-external/session-launcher.mjs`). Confirm the stop (a sent signal, lease expiry, or final-file presence is not proof — only a confirmed exit is), preserve partial work for independent verification, then re-resolve within remaining attempts and budgets. Observer loss (`OBSERVER_GAP`, `DISCONNECTED`, degraded or unobserved coverage, opaque MCP output) is unknown, never worker failure: keep polling owned run files and processes from the shell at the same cadence. Heartbeats and log bytes are not progress. On correlated failure across workers sharing one account, quota, or host, pause further launches there and report cause and scope.');
|
|
288
|
+
lines.push('');
|
|
289
|
+
}
|
|
101
290
|
lines.push(PARALLELISM_BLOCK_END);
|
|
102
291
|
lines.push('');
|
|
103
292
|
return lines.join('\n');
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* an operator runs the explicit migration workflow.
|
|
8
8
|
*/
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
|
-
import { promises as fs } from 'node:fs';
|
|
10
|
+
import { promises as fs, existsSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
11
12
|
import * as path from 'node:path';
|
|
12
13
|
import { buildAiwgMdContent } from './aiwg-md.js';
|
|
13
14
|
import { buildNormalizedAiwgMd } from './finalization.js';
|
|
@@ -592,11 +593,106 @@ function sensitiveEvidence(content) {
|
|
|
592
593
|
}
|
|
593
594
|
return findings;
|
|
594
595
|
}
|
|
596
|
+
// Directory names the context-graph walk always skips, regardless of where
|
|
597
|
+
// the walk root is: VCS/build output that is never hand-authored project
|
|
598
|
+
// content (#277, pre-existing) plus two reserved, self-identifying names
|
|
599
|
+
// that are never legitimately a real project directory.
|
|
600
|
+
const CONTEXT_WALK_ALWAYS_IGNORED_DIR_NAMES = new Set([
|
|
601
|
+
'.git', 'node_modules', 'dist', 'build', 'coverage', '.aiwg',
|
|
602
|
+
]);
|
|
603
|
+
// Cache/registry directory names that are only skipped when the walk root
|
|
604
|
+
// is the home directory itself (#277 fix-round). A project subtree that
|
|
605
|
+
// happens to be named `.cache`, `target`, `.gradle`, etc. is NOT generated
|
|
606
|
+
// data and must still be scanned — these names are only reliably "cache,
|
|
607
|
+
// not project" when they sit directly under $HOME. `Library/Caches` and
|
|
608
|
+
// `.local/share` are two-segment (parent + name) patterns, handled
|
|
609
|
+
// separately below.
|
|
610
|
+
const HOME_LEVEL_CACHE_DIR_NAMES = new Set([
|
|
611
|
+
'.cache', '.cargo', '.rustup', '.npm', '.nvm', '.pnpm-store',
|
|
612
|
+
'.gradle', '.m2', 'Trash',
|
|
613
|
+
]);
|
|
614
|
+
/** True when `dirPath` contains `markerFile` — a cheap, synchronous, read-only check. */
|
|
615
|
+
function hasMarkerFile(dirPath, markerFile) {
|
|
616
|
+
return existsSync(path.join(dirPath, markerFile));
|
|
617
|
+
}
|
|
618
|
+
/** A `target` directory is Cargo/build output only when it carries the
|
|
619
|
+
* standard `CACHEDIR.TAG` marker, or sits next to a `Cargo.toml` — not
|
|
620
|
+
* merely because it is named `target` (#277 fix-round). */
|
|
621
|
+
function isGeneratedTargetDir(dirPath) {
|
|
622
|
+
if (hasMarkerFile(dirPath, 'CACHEDIR.TAG'))
|
|
623
|
+
return true;
|
|
624
|
+
return existsSync(path.join(path.dirname(dirPath), 'Cargo.toml'));
|
|
625
|
+
}
|
|
626
|
+
/** A `venv`/`.venv` directory is a Python virtualenv only when it carries
|
|
627
|
+
* `pyvenv.cfg` — not merely because of its name (#277 fix-round). */
|
|
628
|
+
function isPythonVenvDir(dirPath) {
|
|
629
|
+
return hasMarkerFile(dirPath, 'pyvenv.cfg');
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Whether the context-graph walk should skip `entryName` (a directory
|
|
633
|
+
* directly under `directory`) without descending into it.
|
|
634
|
+
*
|
|
635
|
+
* `isHomeRoot` gates the home-level cache list and the `Library/Caches` /
|
|
636
|
+
* `.local/share` two-segment patterns — they apply only when the overall
|
|
637
|
+
* walk root is $HOME, never for an arbitrary project subtree that happens
|
|
638
|
+
* to share one of those names (#277 fix-round).
|
|
639
|
+
*/
|
|
640
|
+
export function shouldSkipContextWalkDir(directory, entryName, isHomeRoot) {
|
|
641
|
+
if (CONTEXT_WALK_ALWAYS_IGNORED_DIR_NAMES.has(entryName))
|
|
642
|
+
return true;
|
|
643
|
+
if (entryName.startsWith('.context-migration-'))
|
|
644
|
+
return true;
|
|
645
|
+
const childPath = path.join(directory, entryName);
|
|
646
|
+
if (entryName === 'target' && isGeneratedTargetDir(childPath))
|
|
647
|
+
return true;
|
|
648
|
+
if ((entryName === 'venv' || entryName === '.venv') && isPythonVenvDir(childPath))
|
|
649
|
+
return true;
|
|
650
|
+
// Codex's own plugin/temp scratch — a known, specific noise source named
|
|
651
|
+
// in the original report, not a broad `.tmp` exclusion (#277 fix-round).
|
|
652
|
+
if (entryName === '.tmp' && path.basename(directory) === '.codex')
|
|
653
|
+
return true;
|
|
654
|
+
if (isHomeRoot) {
|
|
655
|
+
if (HOME_LEVEL_CACHE_DIR_NAMES.has(entryName))
|
|
656
|
+
return true;
|
|
657
|
+
if (entryName === 'Caches' && path.basename(directory) === 'Library')
|
|
658
|
+
return true;
|
|
659
|
+
if (entryName === 'share' && path.basename(directory) === '.local')
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
/** Walk depth/entry bounds for the context-graph walk (#277). A hit sets
|
|
665
|
+
* `truncated: true` on the result instead of silently stopping. Overridable
|
|
666
|
+
* via environment for tests that need to exercise the bound without
|
|
667
|
+
* building a correspondingly large fixture tree. */
|
|
668
|
+
function readPositiveIntEnv(name, fallback) {
|
|
669
|
+
const raw = process.env[name];
|
|
670
|
+
if (!raw)
|
|
671
|
+
return fallback;
|
|
672
|
+
const parsed = Number.parseInt(raw, 10);
|
|
673
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
674
|
+
}
|
|
675
|
+
const CONTEXT_WALK_MAX_DEPTH = readPositiveIntEnv('AIWG_CONTEXT_WALK_MAX_DEPTH', 10);
|
|
676
|
+
const CONTEXT_WALK_MAX_DIRECTORIES = readPositiveIntEnv('AIWG_CONTEXT_WALK_MAX_DIRECTORIES', 20_000);
|
|
595
677
|
async function nestedInstructionFiles(projectPath) {
|
|
596
678
|
const found = [];
|
|
597
|
-
const ignored = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.aiwg']);
|
|
598
679
|
const artifactRoot = path.resolve(resolveProjectAiwgDir(projectPath));
|
|
599
|
-
|
|
680
|
+
// Running from $HOME is the exact shape of #277: bound the walk to the
|
|
681
|
+
// top level and apply the home-level cache exclusions instead of
|
|
682
|
+
// recursing into every cache/registry/venv underneath it. A markerless
|
|
683
|
+
// but otherwise ordinary (non-home) project root keeps the full bounded
|
|
684
|
+
// walk — "no project marker" is not evidence the directory is generated
|
|
685
|
+
// data (#277 fix-round).
|
|
686
|
+
const isHomeDirectory = path.resolve(projectPath) === path.resolve(homedir());
|
|
687
|
+
const maxDepth = isHomeDirectory ? 1 : CONTEXT_WALK_MAX_DEPTH;
|
|
688
|
+
let visitedDirectories = 0;
|
|
689
|
+
let truncated = false;
|
|
690
|
+
async function walk(directory, depth) {
|
|
691
|
+
if (depth > maxDepth || visitedDirectories >= CONTEXT_WALK_MAX_DIRECTORIES) {
|
|
692
|
+
truncated = true;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
visitedDirectories += 1;
|
|
600
696
|
let entries;
|
|
601
697
|
try {
|
|
602
698
|
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
@@ -605,12 +701,14 @@ async function nestedInstructionFiles(projectPath) {
|
|
|
605
701
|
return;
|
|
606
702
|
}
|
|
607
703
|
for (const entry of entries) {
|
|
704
|
+
// Dirent.isDirectory() is false for symlinks (it reflects the entry
|
|
705
|
+
// itself, not its target), so symlinked directories are never
|
|
706
|
+
// followed out of the root by this walk.
|
|
608
707
|
if (entry.isDirectory()) {
|
|
609
708
|
const child = path.join(directory, entry.name);
|
|
610
|
-
if (!
|
|
611
|
-
&& !entry.name.startsWith('.context-migration-')
|
|
709
|
+
if (!shouldSkipContextWalkDir(directory, entry.name, isHomeDirectory)
|
|
612
710
|
&& path.resolve(child) !== artifactRoot)
|
|
613
|
-
await walk(child);
|
|
711
|
+
await walk(child, depth + 1);
|
|
614
712
|
}
|
|
615
713
|
else if (['AGENTS.md', 'CLAUDE.md', 'WARP.md'].includes(entry.name)) {
|
|
616
714
|
const relative = path.relative(projectPath, path.join(directory, entry.name)).replace(/\\/g, '/');
|
|
@@ -621,8 +719,8 @@ async function nestedInstructionFiles(projectPath) {
|
|
|
621
719
|
}
|
|
622
720
|
}
|
|
623
721
|
}
|
|
624
|
-
await walk(projectPath);
|
|
625
|
-
return found.sort((a, b) => a.localeCompare(b));
|
|
722
|
+
await walk(projectPath, 1);
|
|
723
|
+
return { files: found.sort((a, b) => a.localeCompare(b)), truncated };
|
|
626
724
|
}
|
|
627
725
|
async function migratedProviderFiles(projectPath) {
|
|
628
726
|
const directory = projectAiwgPath(projectPath, 'context', 'providers');
|
|
@@ -638,10 +736,11 @@ async function migratedProviderFiles(projectPath) {
|
|
|
638
736
|
}
|
|
639
737
|
export async function auditWorkspaceContext(projectPath) {
|
|
640
738
|
const extractedProject = await extractExistingProjectContext(projectPath);
|
|
739
|
+
const nested = await nestedInstructionFiles(projectPath);
|
|
641
740
|
const candidates = new Set([
|
|
642
741
|
'WORKSPACE.md',
|
|
643
742
|
...LEGACY_ROOT_FILES,
|
|
644
|
-
...
|
|
743
|
+
...nested.files,
|
|
645
744
|
...await migratedProviderFiles(projectPath),
|
|
646
745
|
]);
|
|
647
746
|
const sources = [];
|
|
@@ -740,6 +839,7 @@ export async function auditWorkspaceContext(projectPath) {
|
|
|
740
839
|
overlaps,
|
|
741
840
|
conflicts,
|
|
742
841
|
sensitiveFindings,
|
|
842
|
+
contextWalkTruncated: nested.truncated,
|
|
743
843
|
plan: {
|
|
744
844
|
routing,
|
|
745
845
|
scopeReview,
|
|
@@ -1039,6 +1139,13 @@ export async function workspaceLinkedFiles(projectPath) {
|
|
|
1039
1139
|
export async function diagnoseWorkspaceContext(projectPath) {
|
|
1040
1140
|
const diagnostics = [];
|
|
1041
1141
|
const audit = await auditWorkspaceContext(projectPath);
|
|
1142
|
+
if (audit.contextWalkTruncated) {
|
|
1143
|
+
diagnostics.push({
|
|
1144
|
+
severity: 'info',
|
|
1145
|
+
code: 'context-walk-truncated',
|
|
1146
|
+
message: 'Nested-instruction-file walk stopped early (depth/entry bound reached, or the root looks like $HOME or has no project markers); the context graph may be incomplete.',
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1042
1149
|
const workspacePath = path.join(projectPath, 'WORKSPACE.md');
|
|
1043
1150
|
const workspace = await readOptional(workspacePath);
|
|
1044
1151
|
if (!workspace) {
|
|
@@ -13,6 +13,7 @@ import { promises as fs } from 'node:fs';
|
|
|
13
13
|
import * as path from 'node:path';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import { substituteEnvVars } from './shim.js';
|
|
16
|
+
import { tomlString } from '../../mcp/toml-string.mjs';
|
|
16
17
|
/** Codex-native event names (subset; extend when verified against codex-rs). */
|
|
17
18
|
const CODEX_EVENT_MAP = {
|
|
18
19
|
PreToolUse: 'before_tool',
|
|
@@ -44,16 +45,16 @@ export function renderCodexHookToml(source) {
|
|
|
44
45
|
const subCommand = substituteEnvVars(source.command, 'codex');
|
|
45
46
|
const subArgs = (source.args || []).map((a) => substituteEnvVars(a, 'codex'));
|
|
46
47
|
lines.push(`[[hooks.${codexEvent}]]`);
|
|
47
|
-
lines.push(`# AIWG-managed hook: ${source.id} — ${source.description}`);
|
|
48
|
-
lines.push(`command =
|
|
48
|
+
lines.push(`# AIWG-managed hook: ${`${source.id} — ${source.description}`.replace(/[\x00-\x1f\x7f]/g, ' ')}`);
|
|
49
|
+
lines.push(`command = ${tomlString(subCommand)}`);
|
|
49
50
|
if (subArgs.length > 0) {
|
|
50
|
-
lines.push(`args = [${subArgs.map(
|
|
51
|
+
lines.push(`args = [${subArgs.map(tomlString).join(', ')}]`);
|
|
51
52
|
}
|
|
52
53
|
if (source.safetyCritical) {
|
|
53
54
|
lines.push(`# safety-critical: cannot be auto-removed without --allow-unsafe-shadow`);
|
|
54
55
|
}
|
|
55
56
|
lines.push(`_aiwg_managed = true`);
|
|
56
|
-
lines.push(`_aiwg_id =
|
|
57
|
+
lines.push(`_aiwg_id = ${tomlString(source.id)}`);
|
|
57
58
|
lines.push('');
|
|
58
59
|
}
|
|
59
60
|
return lines.join('\n');
|
|
@@ -95,7 +96,7 @@ export async function translateForCodex(source, options) {
|
|
|
95
96
|
result.skipReason = 'degrade-on declared codex';
|
|
96
97
|
return result;
|
|
97
98
|
}
|
|
98
|
-
const configPath = path.join(homedir(), '.codex', 'config.toml');
|
|
99
|
+
const configPath = path.join(process.env.CODEX_HOME || path.join(homedir(), '.codex'), 'config.toml');
|
|
99
100
|
const block = renderCodexHookToml(source);
|
|
100
101
|
if (block.trim().length === 0) {
|
|
101
102
|
result.skipped = true;
|
|
@@ -99,7 +99,7 @@ async function main() {
|
|
|
99
99
|
console.log(` node ${generatedServer.files.server}`);
|
|
100
100
|
console.log();
|
|
101
101
|
console.log(' To register with Claude Code:');
|
|
102
|
-
console.log(' Add to .
|
|
102
|
+
console.log(' Add to .mcp.json:');
|
|
103
103
|
console.log(' {');
|
|
104
104
|
console.log(' "mcpServers": {');
|
|
105
105
|
console.log(' "git": {');
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @architecture @.aiwg/architecture/toolsmith-implementation-spec.md
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { boundedExecFileSync, boundedExecSync } from '../../process/bounded-child-process.mjs';
|
|
11
11
|
import { readFile, writeFile, mkdir, access } from 'fs/promises';
|
|
12
12
|
import { existsSync, constants } from 'fs';
|
|
13
13
|
import { resolve, join, dirname } from 'path';
|
|
@@ -140,10 +140,10 @@ export class RuntimeDiscovery {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
} else if (os === 'darwin') {
|
|
143
|
-
const version =
|
|
143
|
+
const version = boundedExecFileSync('sw_vers', ['-productVersion'], 'quick', { encoding: 'utf-8' }).trim();
|
|
144
144
|
osVersion = `macOS ${version}`;
|
|
145
145
|
} else if (os === 'win32') {
|
|
146
|
-
osVersion =
|
|
146
|
+
osVersion = boundedExecSync('ver', 'quick', { encoding: 'utf-8' }).trim();
|
|
147
147
|
}
|
|
148
148
|
} catch (error) {
|
|
149
149
|
console.warn('Could not detect OS version:', error.message);
|
|
@@ -172,7 +172,7 @@ export class RuntimeDiscovery {
|
|
|
172
172
|
// Disk space
|
|
173
173
|
if (platform() === 'win32') {
|
|
174
174
|
// Windows: use wmic
|
|
175
|
-
const output =
|
|
175
|
+
const output = boundedExecFileSync('wmic', ['logicaldisk', 'get', 'size,freespace'], 'quick', { encoding: 'utf-8' });
|
|
176
176
|
const lines = output.trim().split('\n').slice(1);
|
|
177
177
|
let totalFree = 0;
|
|
178
178
|
for (const line of lines) {
|
|
@@ -184,7 +184,7 @@ export class RuntimeDiscovery {
|
|
|
184
184
|
diskFreeGb = Math.round((totalFree / (1024 ** 3)) * 10) / 10;
|
|
185
185
|
} else {
|
|
186
186
|
// Unix: use df
|
|
187
|
-
const output =
|
|
187
|
+
const output = boundedExecFileSync('df', ['-k', '.'], 'quick', { encoding: 'utf-8' });
|
|
188
188
|
const lines = output.trim().split('\n');
|
|
189
189
|
if (lines.length > 1) {
|
|
190
190
|
const parts = lines[1].trim().split(/\s+/);
|
|
@@ -365,7 +365,7 @@ export class RuntimeDiscovery {
|
|
|
365
365
|
async #findToolPath(toolName) {
|
|
366
366
|
try {
|
|
367
367
|
const cmd = platform() === 'win32' ? 'where' : 'which';
|
|
368
|
-
const path =
|
|
368
|
+
const path = boundedExecFileSync(cmd, [toolName], 'quick', { encoding: 'utf-8' })
|
|
369
369
|
.trim()
|
|
370
370
|
.split('\n')[0];
|
|
371
371
|
return path;
|
|
@@ -393,15 +393,11 @@ export class RuntimeDiscovery {
|
|
|
393
393
|
// Bare `version` (positional) is intentionally excluded: it caused
|
|
394
394
|
// BPF tools to attach tracepoints, input-leapc to connect to
|
|
395
395
|
// hostname "version", and cs2cs to open a file named "version".
|
|
396
|
-
const
|
|
397
|
-
`${toolPath} --version`,
|
|
398
|
-
`${toolPath} -v`,
|
|
399
|
-
`${toolPath} -V`
|
|
400
|
-
];
|
|
396
|
+
const versionArgs = ['--version', '-v', '-V'];
|
|
401
397
|
|
|
402
|
-
for (const
|
|
398
|
+
for (const arg of versionArgs) {
|
|
403
399
|
try {
|
|
404
|
-
const output =
|
|
400
|
+
const output = boundedExecFileSync(toolPath, [arg], 'quick', {
|
|
405
401
|
encoding: 'utf-8',
|
|
406
402
|
timeout: 3000,
|
|
407
403
|
killSignal: 'SIGKILL',
|
|
@@ -503,7 +499,7 @@ export class RuntimeDiscovery {
|
|
|
503
499
|
if (platform() === 'win32') return undefined;
|
|
504
500
|
|
|
505
501
|
try {
|
|
506
|
-
const manPath =
|
|
502
|
+
const manPath = boundedExecFileSync('man', ['-w', toolName], 'quick', {
|
|
507
503
|
encoding: 'utf-8',
|
|
508
504
|
stdio: ['ignore', 'pipe', 'ignore']
|
|
509
505
|
}).trim();
|