@aiwg/cli 2026.9.6 → 2026.9.9
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/dist/src/artifacts/index-builder.js +43 -1
- package/dist/src/artifacts/query-engine.js +7 -0
- package/dist/src/cli/handlers/help.js +7 -1
- package/dist/src/cli/handlers/installation.js +106 -2
- package/dist/src/cli/handlers/mc.js +100 -37
- package/dist/src/cli/handlers/ralph.js +14 -4
- package/dist/src/cli/handlers/refresh.js +359 -31
- package/dist/src/cli/handlers/repo-access.js +155 -4
- package/dist/src/cli/handlers/runtime-info.js +3 -0
- package/dist/src/cli/handlers/serve.js +21 -3
- package/dist/src/cli/handlers/setup.js +5 -5
- package/dist/src/cli/handlers/steward.js +30 -1
- package/dist/src/cli/handlers/use.js +123 -12
- package/dist/src/cli/handlers/utilities.js +26 -10
- package/dist/src/cli/handlers/version.js +40 -14
- package/dist/src/cli/handlers/workspace-context.js +8 -0
- package/dist/src/cli/services/deployment-verification.js +156 -7
- package/dist/src/cli/watch-service.js +47 -4
- package/dist/src/config/aiwg-config.js +95 -3
- package/dist/src/config/cli.js +16 -1
- package/dist/src/config/gitignore.js +5 -0
- package/dist/src/config/project-artifacts-health.mjs +15 -2
- package/dist/src/cost/fleet-report.js +19 -5
- package/dist/src/extensions/claude-hooks-installer.js +22 -6
- package/dist/src/extensions/project-local-doctor.js +40 -2
- package/dist/src/extensions/project-quickref.js +4 -0
- package/dist/src/installation/manager.mjs +38 -3
- package/dist/src/lint/runner.js +138 -0
- package/dist/src/mcp/helpers.mjs +56 -22
- package/dist/src/mcp/registry.js +32 -22
- package/dist/src/mcp/registry.mjs +31 -26
- package/dist/src/mcp/toml-editor.mjs +117 -0
- package/dist/src/mcp/tools/orchestration.mjs +7 -7
- package/dist/src/mcp/tools/subsystems.mjs +7 -7
- package/dist/src/memory/context-pack.js +5 -1
- package/dist/src/plugin/skill-command-translator.js +70 -1
- package/dist/src/serve/a2a-terminal-observer.js +19 -1
- package/dist/src/serve/mission-hitl.js +91 -0
- package/dist/src/sessions/import-lease.js +5 -1
- package/dist/src/smiths/context-pipeline/workspace-context.js +132 -6
- package/dist/src/testing/fixtures/test-data-factory.js +3 -3
- package/dist/src/writing/pattern-library.js +29 -6
- package/package.json +2 -1
- package/tools/agents/deploy-agents.mjs +91 -5
- package/tools/agents/providers/base.mjs +162 -6
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
* @issue #173, #174, #482, #557, #694
|
|
14
14
|
*/
|
|
15
15
|
import { promises as fs } from 'fs';
|
|
16
|
+
import { execFile } from 'node:child_process';
|
|
16
17
|
import path from 'path';
|
|
18
|
+
import { promisify } from 'node:util';
|
|
19
|
+
const execFileAsync = promisify(execFile);
|
|
17
20
|
import { createScriptRunner } from './script-runner.js';
|
|
18
21
|
import { createUseHandler } from './use.js';
|
|
19
22
|
import { getFrameworkRoot } from '../../channel/manager.mjs';
|
|
@@ -21,6 +24,7 @@ import { refreshAllPackages } from '../../packages/registry.js';
|
|
|
21
24
|
import { resolveActiveProvider } from '../provider-resolution.js';
|
|
22
25
|
import { readAiwgConfig, writeAiwgConfig, hashManifest, getProviderParallelismDefaults, } from '../../config/aiwg-config.js';
|
|
23
26
|
import { discoverProjectLocalBundles } from '../../extensions/project-local-discovery.js';
|
|
27
|
+
import { getProviderArtifactPathStrings } from '../../providers/provider-definitions.js';
|
|
24
28
|
import { collectPackagedAgentInventory, normalizeAgentArtifactName, parseManagedArtifactMarker, } from '../../agents/packaged-agent-inventory.js';
|
|
25
29
|
import * as ui from '../ui.js';
|
|
26
30
|
const PROVIDER_AGENT_DIRS = {
|
|
@@ -36,6 +40,48 @@ const PROVIDER_AGENT_DIRS = {
|
|
|
36
40
|
export async function currentBundledAgentBasenames(frameworkRoot) {
|
|
37
41
|
return new Set((await collectPackagedAgentInventory(frameworkRoot)).keys());
|
|
38
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Basenames of every rule the current package can deploy.
|
|
45
|
+
*
|
|
46
|
+
* Symmetric to {@link currentBundledAgentBasenames}. A rule absent from this set
|
|
47
|
+
* is residue from a deploy model that no longer writes it (#2540).
|
|
48
|
+
*
|
|
49
|
+
* Every group that can ship a `rules/` directory must be listed here: the set is
|
|
50
|
+
* the prune's definition of "still shipped", so a missed group makes live rules
|
|
51
|
+
* look orphaned and deletes them. `extensions` ships 22 rules and was the group
|
|
52
|
+
* this nearly lost.
|
|
53
|
+
*/
|
|
54
|
+
export const BUNDLED_RULE_SOURCE_GROUPS = ['frameworks', 'addons', 'plugins', 'extensions'];
|
|
55
|
+
export async function currentBundledRuleBasenames(frameworkRoot) {
|
|
56
|
+
const names = new Set();
|
|
57
|
+
const codeRoot = path.join(frameworkRoot, 'agentic', 'code');
|
|
58
|
+
for (const group of BUNDLED_RULE_SOURCE_GROUPS) {
|
|
59
|
+
let units;
|
|
60
|
+
try {
|
|
61
|
+
units = await fs.readdir(path.join(codeRoot, group), { withFileTypes: true });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for (const unit of units) {
|
|
67
|
+
if (!unit.isDirectory())
|
|
68
|
+
continue;
|
|
69
|
+
const rulesDir = path.join(codeRoot, group, unit.name, 'rules');
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await fs.readdir(rulesDir, { withFileTypes: true });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (entry.isFile() && entry.name.endsWith('.md'))
|
|
79
|
+
names.add(entry.name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return names;
|
|
84
|
+
}
|
|
39
85
|
async function readFrameworkVersion(frameworkRoot) {
|
|
40
86
|
try {
|
|
41
87
|
const pkg = JSON.parse(await fs.readFile(path.join(frameworkRoot, 'package.json'), 'utf8'));
|
|
@@ -69,12 +115,33 @@ function isOlderManagedVersion(deployedVersion, currentVersion) {
|
|
|
69
115
|
return true;
|
|
70
116
|
return deployed.prerelease.localeCompare(current.prerelease, undefined, { numeric: true }) < 0;
|
|
71
117
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
118
|
+
const PRUNABLE_ARTIFACT_KINDS = ['agents', 'commands', 'rules'];
|
|
119
|
+
/**
|
|
120
|
+
* Resolve the on-disk artifact directories for a provider.
|
|
121
|
+
*
|
|
122
|
+
* `PROVIDER_AGENT_DIRS` remains the enumeration of providers AIWG can prune;
|
|
123
|
+
* the command/rule directories come from the provider definitions so the
|
|
124
|
+
* unit-prune path never drifts from what `aiwg use` actually wrote.
|
|
125
|
+
*/
|
|
126
|
+
function providerArtifactDirs(provider) {
|
|
127
|
+
const agents = PROVIDER_AGENT_DIRS[provider] ?? null;
|
|
128
|
+
const declared = getProviderArtifactPathStrings(provider);
|
|
129
|
+
const usable = (value) => typeof value === 'string' && value.length > 0 && !path.isAbsolute(value) ? value : null;
|
|
130
|
+
return {
|
|
131
|
+
agents,
|
|
132
|
+
commands: usable(declared?.commands),
|
|
133
|
+
rules: usable(declared?.rules),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Collect every AIWG-managed (`source: bundled`) artifact in one provider tree. */
|
|
137
|
+
async function collectManagedProviderArtifacts(projectRoot, provider, kinds) {
|
|
138
|
+
const dirs = providerArtifactDirs(provider);
|
|
139
|
+
const hits = [];
|
|
140
|
+
for (const kind of kinds) {
|
|
141
|
+
const relDir = dirs[kind];
|
|
142
|
+
if (!relDir)
|
|
143
|
+
continue;
|
|
144
|
+
const dir = path.join(projectRoot, relDir);
|
|
78
145
|
let entries;
|
|
79
146
|
try {
|
|
80
147
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -85,6 +152,9 @@ export async function pruneStaleManagedAgentFiles(options) {
|
|
|
85
152
|
for (const entry of entries) {
|
|
86
153
|
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
87
154
|
continue;
|
|
155
|
+
// Generated indexes are rewritten on every deploy, never orphaned.
|
|
156
|
+
if (entry.name === 'RULES-INDEX.md' || entry.name === 'RULES-ONDEMAND.md')
|
|
157
|
+
continue;
|
|
88
158
|
const file = path.join(dir, entry.name);
|
|
89
159
|
let content;
|
|
90
160
|
try {
|
|
@@ -96,32 +166,198 @@ export async function pruneStaleManagedAgentFiles(options) {
|
|
|
96
166
|
const marker = parseManagedArtifactMarker(content);
|
|
97
167
|
if (marker?.source !== 'bundled')
|
|
98
168
|
continue;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
169
|
+
hits.push({
|
|
170
|
+
kind,
|
|
171
|
+
absolutePath: file,
|
|
172
|
+
relativePath: path.relative(projectRoot, file),
|
|
173
|
+
version: marker.version,
|
|
174
|
+
artifactName: normalizeAgentArtifactName(entry.name),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return hits;
|
|
179
|
+
}
|
|
180
|
+
export async function detectStaleProviderTrees(options) {
|
|
181
|
+
const currentVersion = options.currentVersion ?? await readFrameworkVersion(options.frameworkRoot);
|
|
182
|
+
if (currentVersion === null)
|
|
183
|
+
return [];
|
|
184
|
+
const trees = [];
|
|
185
|
+
for (const provider of Object.keys(PROVIDER_AGENT_DIRS)) {
|
|
186
|
+
if (provider === options.provider)
|
|
187
|
+
continue;
|
|
188
|
+
const hits = (await collectManagedProviderArtifacts(options.projectRoot, provider, PRUNABLE_ARTIFACT_KINDS))
|
|
189
|
+
.filter((hit) => isOlderManagedVersion(hit.version, currentVersion));
|
|
190
|
+
if (hits.length === 0)
|
|
191
|
+
continue;
|
|
192
|
+
const counts = { agents: 0, commands: 0, rules: 0 };
|
|
193
|
+
for (const hit of hits)
|
|
194
|
+
counts[hit.kind] += 1;
|
|
195
|
+
const oldest = hits
|
|
196
|
+
.map((hit) => hit.version)
|
|
197
|
+
.reduce((a, b) => (isOlderManagedVersion(a, b) ? a : b));
|
|
198
|
+
trees.push({ provider, version: oldest, counts, total: hits.length });
|
|
199
|
+
}
|
|
200
|
+
trees.sort((a, b) => a.provider.localeCompare(b.provider));
|
|
201
|
+
return trees;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Split project-relative paths into the ones git tracks and the ones it does not (#2509).
|
|
205
|
+
*
|
|
206
|
+
* Deleting an ignored, regenerable artifact and deleting a committed file are
|
|
207
|
+
* not the same act. A project that is not a git repo — or a machine without
|
|
208
|
+
* git — reports everything as untracked, so the caller behaves as before.
|
|
209
|
+
*/
|
|
210
|
+
export async function partitionTrackedPaths(projectRoot, relativePaths) {
|
|
211
|
+
if (relativePaths.length === 0)
|
|
212
|
+
return { tracked: [], untracked: [] };
|
|
213
|
+
let trackedSet;
|
|
214
|
+
try {
|
|
215
|
+
const { stdout } = await execFileAsync('git', ['ls-files', '-z', '--', ...relativePaths], { cwd: projectRoot, maxBuffer: 32 * 1024 * 1024 });
|
|
216
|
+
trackedSet = new Set(stdout.split('\0').filter(Boolean));
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return { tracked: [], untracked: [...relativePaths] };
|
|
220
|
+
}
|
|
221
|
+
const tracked = [];
|
|
222
|
+
const untracked = [];
|
|
223
|
+
for (const relativePath of relativePaths) {
|
|
224
|
+
// git reports POSIX separators regardless of platform.
|
|
225
|
+
(trackedSet.has(relativePath.split(path.sep).join('/')) ? tracked : untracked).push(relativePath);
|
|
226
|
+
}
|
|
227
|
+
return { tracked, untracked };
|
|
228
|
+
}
|
|
229
|
+
export async function pruneStaleManagedAgentFiles(options) {
|
|
230
|
+
const desired = await currentBundledAgentBasenames(options.frameworkRoot);
|
|
231
|
+
const desiredRules = await currentBundledRuleBasenames(options.frameworkRoot);
|
|
232
|
+
const currentVersion = options.currentVersion ?? await readFrameworkVersion(options.frameworkRoot);
|
|
233
|
+
const crossProvider = options.crossProvider ?? 'skip';
|
|
234
|
+
const removals = [];
|
|
235
|
+
const trackedSkipped = [];
|
|
236
|
+
const record = (list, provider, relativePath) => {
|
|
237
|
+
let entry = list.find((item) => item.provider === provider);
|
|
238
|
+
if (!entry) {
|
|
239
|
+
entry = { provider, paths: [] };
|
|
240
|
+
list.push(entry);
|
|
241
|
+
}
|
|
242
|
+
entry.paths.push(relativePath);
|
|
243
|
+
};
|
|
244
|
+
for (const provider of Object.keys(PROVIDER_AGENT_DIRS)) {
|
|
245
|
+
const isTargetProvider = provider === options.provider;
|
|
246
|
+
// Non-target trees are pruned as a unit or not at all; the refreshed
|
|
247
|
+
// provider only drops agents whose source no longer ships them.
|
|
248
|
+
if (!isTargetProvider && crossProvider === 'skip')
|
|
249
|
+
continue;
|
|
250
|
+
// Rules were excluded from the target-provider pass, so a project carried
|
|
251
|
+
// every rule any past version ever deployed. On long-lived projects that is
|
|
252
|
+
// the bulk of the startup-context budget (#2540).
|
|
253
|
+
const kinds = isTargetProvider ? ['agents', 'rules'] : PRUNABLE_ARTIFACT_KINDS;
|
|
254
|
+
const hits = await collectManagedProviderArtifacts(options.projectRoot, provider, kinds);
|
|
255
|
+
const eligible = hits.filter((hit) => {
|
|
256
|
+
if (isTargetProvider) {
|
|
257
|
+
// Addons have independent manifest versions. Comparing their managed
|
|
258
|
+
// marker to the top-level package version makes a successful refresh
|
|
259
|
+
// delete freshly restored addon agents, so the active provider removes
|
|
260
|
+
// only artifacts absent from current sources.
|
|
261
|
+
if (hit.kind === 'rules')
|
|
262
|
+
return !desiredRules.has(path.basename(hit.relativePath));
|
|
263
|
+
return !desired.has(hit.artifactName);
|
|
264
|
+
}
|
|
265
|
+
return currentVersion !== null && isOlderManagedVersion(hit.version, currentVersion);
|
|
266
|
+
});
|
|
267
|
+
if (eligible.length === 0)
|
|
268
|
+
continue;
|
|
269
|
+
// Only the cross-provider unit prune defers to VCS state.
|
|
270
|
+
let protectedPaths = new Set();
|
|
271
|
+
if (!isTargetProvider && !options.allowTrackedDeletes) {
|
|
272
|
+
const { tracked } = await partitionTrackedPaths(options.projectRoot, eligible.map((hit) => hit.relativePath));
|
|
273
|
+
protectedPaths = new Set(tracked);
|
|
274
|
+
}
|
|
275
|
+
for (const hit of eligible) {
|
|
276
|
+
if (protectedPaths.has(hit.relativePath)) {
|
|
277
|
+
record(trackedSkipped, provider, hit.relativePath);
|
|
110
278
|
continue;
|
|
111
|
-
|
|
279
|
+
}
|
|
112
280
|
if (!options.dryRun)
|
|
113
|
-
await fs.rm(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
281
|
+
await fs.rm(hit.absolutePath, { force: true });
|
|
282
|
+
record(removals, provider, hit.relativePath);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const list of [removals, trackedSkipped]) {
|
|
286
|
+
for (const entry of list)
|
|
287
|
+
entry.paths.sort((a, b) => a.localeCompare(b));
|
|
288
|
+
}
|
|
289
|
+
return { removals, trackedSkipped };
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Reconcile `installed.deployedTo` with the artifacts a prune pass left behind (#2506).
|
|
293
|
+
*
|
|
294
|
+
* Recorded counts are written by `aiwg use` before the prune runs, so a run
|
|
295
|
+
* that deletes artifacts otherwise leaves the config permanently claiming
|
|
296
|
+
* files that are gone. Two cases:
|
|
297
|
+
*
|
|
298
|
+
* - **Unit-pruned provider tree** (a non-refreshed provider removed wholesale):
|
|
299
|
+
* every managed agent/command/rule for that provider is gone, so those
|
|
300
|
+
* counts drop to zero across all installed bundles. Skills are not part of
|
|
301
|
+
* the prune pass and keep their recorded values.
|
|
302
|
+
* - **Refreshed provider** (agent orphans only): subtract the removed count
|
|
303
|
+
* from the recorded agent totals, largest contributor first, clamped at 0.
|
|
304
|
+
*/
|
|
305
|
+
async function reconcileDeployedToAfterPrune(projectRoot, removals, refreshedProvider) {
|
|
306
|
+
if (removals.length === 0)
|
|
307
|
+
return;
|
|
308
|
+
const config = await readAiwgConfig(projectRoot);
|
|
309
|
+
if (!config)
|
|
310
|
+
return;
|
|
311
|
+
let changed = false;
|
|
312
|
+
for (const removal of removals) {
|
|
313
|
+
const unitPruned = removal.provider !== refreshedProvider;
|
|
314
|
+
const entries = Object.values(config.installed)
|
|
315
|
+
.filter((entry) => entry.deployedTo?.[removal.provider]);
|
|
316
|
+
if (entries.length === 0)
|
|
317
|
+
continue;
|
|
318
|
+
if (unitPruned) {
|
|
319
|
+
for (const entry of entries) {
|
|
320
|
+
const counts = entry.deployedTo[removal.provider];
|
|
321
|
+
if (counts.agents === 0 && counts.commands === 0 && counts.rules === 0)
|
|
322
|
+
continue;
|
|
323
|
+
counts.agents = 0;
|
|
324
|
+
counts.commands = 0;
|
|
325
|
+
counts.rules = 0;
|
|
326
|
+
changed = true;
|
|
118
327
|
}
|
|
119
|
-
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
let outstanding = removal.paths.length;
|
|
331
|
+
const byAgentsDesc = entries
|
|
332
|
+
.slice()
|
|
333
|
+
.sort((a, b) => b.deployedTo[removal.provider].agents - a.deployedTo[removal.provider].agents);
|
|
334
|
+
for (const entry of byAgentsDesc) {
|
|
335
|
+
if (outstanding <= 0)
|
|
336
|
+
break;
|
|
337
|
+
const counts = entry.deployedTo[removal.provider];
|
|
338
|
+
const deduct = Math.min(counts.agents, outstanding);
|
|
339
|
+
if (deduct <= 0)
|
|
340
|
+
continue;
|
|
341
|
+
counts.agents -= deduct;
|
|
342
|
+
outstanding -= deduct;
|
|
343
|
+
changed = true;
|
|
120
344
|
}
|
|
121
345
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
346
|
+
if (changed)
|
|
347
|
+
await writeAiwgConfig(projectRoot, config);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Whether this run may delete git-tracked artifacts during a cross-provider
|
|
351
|
+
* prune (#2514).
|
|
352
|
+
*
|
|
353
|
+
* Deliberately not derived from `--force`. `--force` governs what gets
|
|
354
|
+
* *written* — it replaces artifacts AIWG does not currently manage. Deleting
|
|
355
|
+
* files someone committed, in a provider tree this run was not asked to touch,
|
|
356
|
+
* is a different decision and gets its own switch, so habitual `--force` use
|
|
357
|
+
* can never authorise it.
|
|
358
|
+
*/
|
|
359
|
+
export function allowsTrackedDeletes(args) {
|
|
360
|
+
return args.includes('--prune-tracked');
|
|
125
361
|
}
|
|
126
362
|
/**
|
|
127
363
|
* Parse --flag value pairs from args
|
|
@@ -161,6 +397,17 @@ Options:
|
|
|
161
397
|
--skip-update Skip the installation update
|
|
162
398
|
--packages-only Refresh remote packages only
|
|
163
399
|
--provider <name> Override provider auto-detection
|
|
400
|
+
--prune-other-providers Remove stale AIWG-managed trees belonging to
|
|
401
|
+
providers this run did not refresh. Off by default:
|
|
402
|
+
a provider-scoped refresh never mutates another
|
|
403
|
+
provider's deployed surface. Git-tracked files are
|
|
404
|
+
always left in place unless --prune-tracked is given.
|
|
405
|
+
--prune-tracked Allow --prune-other-providers to delete git-tracked
|
|
406
|
+
files. Deleting committed files is a separate
|
|
407
|
+
decision from --force, which only governs writes.
|
|
408
|
+
--force Re-write every deployed artifact, replacing files
|
|
409
|
+
AIWG does not currently manage. Never authorises
|
|
410
|
+
deleting tracked files.
|
|
164
411
|
--channel <name> Select the update channel (stable or main)
|
|
165
412
|
--frameworks <list> Re-deploy a comma-separated installed subset
|
|
166
413
|
--model <name> Override all deployed agent model tiers
|
|
@@ -192,6 +439,14 @@ export const refreshHandler = {
|
|
|
192
439
|
const quiet = hasFlag(ctx.args, '--quiet');
|
|
193
440
|
const skipUpdate = hasFlag(ctx.args, '--skip-update');
|
|
194
441
|
const packagesOnly = hasFlag(ctx.args, '--packages-only');
|
|
442
|
+
// #2506: cross-provider pruning is opt-in. A provider-scoped refresh must
|
|
443
|
+
// not silently delete another provider's deployed surface.
|
|
444
|
+
const pruneOtherProviders = hasFlag(ctx.args, '--prune-other-providers');
|
|
445
|
+
const forceDeploy = hasFlag(ctx.args, '--force');
|
|
446
|
+
// #2514: deleting committed files in a tree this run was not asked to touch
|
|
447
|
+
// is its own decision, not a consequence of asking for a forceful re-write.
|
|
448
|
+
// `--force` governs what gets written; this governs what gets destroyed.
|
|
449
|
+
const pruneTracked = allowsTrackedDeletes(ctx.args);
|
|
195
450
|
const provider = parseFlag(ctx.args, '--provider');
|
|
196
451
|
const channel = parseFlag(ctx.args, '--channel');
|
|
197
452
|
const frameworksArg = parseFlag(ctx.args, '--frameworks');
|
|
@@ -313,6 +568,9 @@ export const refreshHandler = {
|
|
|
313
568
|
'--target', ctx.cwd,
|
|
314
569
|
'--yes',
|
|
315
570
|
'--json',
|
|
571
|
+
// #2507: give operators a reclaim path for artifacts an older
|
|
572
|
+
// AIWG left behind without a managed marker.
|
|
573
|
+
...(forceDeploy ? ['--force'] : []),
|
|
316
574
|
...modelDeployArgs,
|
|
317
575
|
],
|
|
318
576
|
rawArgs: ['use', fw],
|
|
@@ -364,32 +622,93 @@ export const refreshHandler = {
|
|
|
364
622
|
catch {
|
|
365
623
|
// Non-fatal — refresh continues
|
|
366
624
|
}
|
|
367
|
-
// Step 4.5: Stale deployment check (#621, #1460, #1799)
|
|
625
|
+
// Step 4.5: Stale deployment check (#621, #1460, #1799, #2506)
|
|
368
626
|
if (!quiet)
|
|
369
627
|
ui.info('Checking for stale deployments...');
|
|
628
|
+
// A modifier with nothing to modify is almost always a mistyped intent.
|
|
629
|
+
if (pruneTracked && !pruneOtherProviders && !quiet) {
|
|
630
|
+
ui.warn('--prune-tracked has no effect without --prune-other-providers; nothing was removed.');
|
|
631
|
+
}
|
|
370
632
|
let staleAgentRemovals = [];
|
|
371
|
-
|
|
633
|
+
let trackedSkipped = [];
|
|
634
|
+
// A dry run still reports what a real run would remove. Skipping the pass
|
|
635
|
+
// entirely meant `refresh --dry-run` printed "Checking for stale deployments..."
|
|
636
|
+
// and nothing else, so orphaned artifacts were invisible until they had
|
|
637
|
+
// pushed the project over its context budget (#2540).
|
|
638
|
+
if (deploymentFailures.length === 0) {
|
|
372
639
|
try {
|
|
373
|
-
|
|
640
|
+
const pruneResult = await pruneStaleManagedAgentFiles({
|
|
374
641
|
projectRoot: ctx.cwd,
|
|
375
642
|
frameworkRoot,
|
|
376
643
|
provider: detectedProvider,
|
|
644
|
+
crossProvider: pruneOtherProviders ? 'prune' : 'skip',
|
|
645
|
+
allowTrackedDeletes: pruneTracked,
|
|
646
|
+
dryRun,
|
|
377
647
|
});
|
|
648
|
+
staleAgentRemovals = pruneResult.removals;
|
|
649
|
+
trackedSkipped = pruneResult.trackedSkipped;
|
|
650
|
+
if (trackedSkipped.length > 0 && !quiet) {
|
|
651
|
+
const total = trackedSkipped.reduce((sum, item) => sum + item.paths.length, 0);
|
|
652
|
+
ui.warn(`Left ${total} git-tracked AIWG-managed file${total === 1 ? '' : 's'} in place ` +
|
|
653
|
+
`across ${trackedSkipped.length} provider${trackedSkipped.length === 1 ? '' : 's'}`);
|
|
654
|
+
for (const skipped of trackedSkipped) {
|
|
655
|
+
const shown = skipped.paths.slice(0, 3).join(', ');
|
|
656
|
+
const remainder = skipped.paths.length - 3;
|
|
657
|
+
ui.dim(` ${skipped.provider}: ${skipped.paths.length} (${shown}${remainder > 0 ? `, ...and ${remainder} more` : ''})`);
|
|
658
|
+
}
|
|
659
|
+
ui.dim(" These are committed files. Add --prune-tracked to remove them as well.");
|
|
660
|
+
}
|
|
378
661
|
if (staleAgentRemovals.length > 0 && !quiet) {
|
|
379
662
|
const total = staleAgentRemovals.reduce((sum, item) => sum + item.paths.length, 0);
|
|
380
|
-
ui.
|
|
663
|
+
ui.warn(`${dryRun ? 'Would remove' : 'Removed'} ${total} stale AIWG-managed file${total === 1 ? '' : 's'} ` +
|
|
381
664
|
`across ${staleAgentRemovals.length} provider${staleAgentRemovals.length === 1 ? '' : 's'}`);
|
|
382
665
|
for (const removal of staleAgentRemovals) {
|
|
383
666
|
const shown = removal.paths.slice(0, 3).join(', ');
|
|
384
667
|
const remainder = removal.paths.length - 3;
|
|
385
668
|
ui.dim(` ${removal.provider}: ${removal.paths.length} (${shown}${remainder > 0 ? `, ...and ${remainder} more` : ''})`);
|
|
386
669
|
}
|
|
670
|
+
ui.dim(dryRun
|
|
671
|
+
? ' Re-run without --dry-run to remove them.'
|
|
672
|
+
: ' Review `git status` before committing — deployed artifacts may be tracked.');
|
|
673
|
+
}
|
|
674
|
+
// #2506: keep the recorded deployment state consistent with what the
|
|
675
|
+
// prune actually left on disk, so a later run does not trust counts
|
|
676
|
+
// for artifacts that no longer exist. A dry run deleted nothing, so the
|
|
677
|
+
// recorded state is already accurate.
|
|
678
|
+
if (!dryRun) {
|
|
679
|
+
await reconcileDeployedToAfterPrune(ctx.cwd, staleAgentRemovals, pruneOtherProviders ? detectedProvider : null);
|
|
387
680
|
}
|
|
388
681
|
}
|
|
389
682
|
catch {
|
|
390
683
|
if (!quiet)
|
|
391
684
|
ui.dim(' Agent orphan cleanup skipped (non-critical)');
|
|
392
685
|
}
|
|
686
|
+
// #2506: a provider-scoped refresh reports other providers' stale trees
|
|
687
|
+
// instead of mutating them. Silent cross-provider deletion destroyed
|
|
688
|
+
// git-tracked artifacts and left half-deployed surfaces behind.
|
|
689
|
+
if (!pruneOtherProviders) {
|
|
690
|
+
try {
|
|
691
|
+
const staleTrees = await detectStaleProviderTrees({
|
|
692
|
+
projectRoot: ctx.cwd,
|
|
693
|
+
frameworkRoot,
|
|
694
|
+
provider: detectedProvider,
|
|
695
|
+
});
|
|
696
|
+
for (const tree of staleTrees) {
|
|
697
|
+
const breakdown = ['agents', 'commands', 'rules']
|
|
698
|
+
.filter((kind) => tree.counts[kind] > 0)
|
|
699
|
+
.map((kind) => `${tree.counts[kind]} ${kind}`)
|
|
700
|
+
.join(', ');
|
|
701
|
+
ui.warn(`Stale ${tree.provider} deployment: ${tree.total} AIWG-managed file(s) from v${tree.version} ` +
|
|
702
|
+
`(${breakdown}) — this run refreshed ${detectedProvider ?? 'the active provider'} only`);
|
|
703
|
+
ui.dim(` Refresh it with 'aiwg refresh --provider ${tree.provider}', ` +
|
|
704
|
+
`or remove it with 'aiwg refresh --prune-other-providers'`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
if (!quiet)
|
|
709
|
+
ui.dim(' Stale provider tree check skipped (non-critical)');
|
|
710
|
+
}
|
|
711
|
+
}
|
|
393
712
|
try {
|
|
394
713
|
const { getFrameworkRoot } = await import('../../channel/manager.mjs');
|
|
395
714
|
const { join } = await import('path');
|
|
@@ -420,6 +739,15 @@ export const refreshHandler = {
|
|
|
420
739
|
ui.warn(`Stale deployment: ${name} — run 'aiwg use ${name}' to redeploy`);
|
|
421
740
|
}
|
|
422
741
|
}
|
|
742
|
+
else if (staleAgentRemovals.length > 0) {
|
|
743
|
+
// #2506: a run that deleted artifacts is not an "up to date" run.
|
|
744
|
+
const removed = staleAgentRemovals.reduce((sum, item) => sum + item.paths.length, 0);
|
|
745
|
+
if (!quiet) {
|
|
746
|
+
ui.warn(dryRun
|
|
747
|
+
? `Deployments current, but ${removed} stale artifact(s) would be removed — review the list above`
|
|
748
|
+
: `Deployments current, but ${removed} stale artifact(s) were removed this run — review the list above`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
423
751
|
else {
|
|
424
752
|
if (!quiet)
|
|
425
753
|
ui.success('All deployments up to date');
|