@aiwg/cli 2026.9.7 → 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/installation.js +102 -2
- package/dist/src/cli/handlers/mc.js +87 -17
- package/dist/src/cli/handlers/refresh.js +67 -7
- package/dist/src/cli/handlers/repo-access.js +155 -4
- 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 +9 -4
- 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 +39 -6
- 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/extensions/claude-hooks-installer.js +22 -6
- package/dist/src/lint/runner.js +138 -0
- package/dist/src/smiths/context-pipeline/workspace-context.js +51 -1
- package/package.json +1 -1
- package/tools/agents/deploy-agents.mjs +4 -0
- package/tools/agents/providers/base.mjs +101 -4
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { checkRepoAccess, findRepoEntry, formatRepoAccessEntry, loadRepoAccessManifest, } from '../../policy/repo-access.js';
|
|
2
2
|
import { resolveWorkspace } from '../../config/workspace.js';
|
|
3
|
+
import { getProjectDir, readAiwgConfig, writeAiwgConfig, WORKSPACE_REPO_ACTIONS, } from '../../config/aiwg-config.js';
|
|
4
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
5
|
+
import * as nodePath from 'node:path';
|
|
3
6
|
function valueAfter(args, flag) {
|
|
4
7
|
const index = args.indexOf(flag);
|
|
5
8
|
if (index < 0)
|
|
6
9
|
return null;
|
|
7
10
|
return args[index + 1] ?? null;
|
|
8
11
|
}
|
|
9
|
-
function
|
|
10
|
-
|
|
12
|
+
function usage() {
|
|
13
|
+
return `
|
|
11
14
|
aiwg repo-access — repo authorization manifest preflight
|
|
12
15
|
|
|
13
16
|
Usage:
|
|
@@ -15,12 +18,149 @@ function printHelp() {
|
|
|
15
18
|
aiwg repo-access status
|
|
16
19
|
aiwg repo-access explain --path <repo-or-file>
|
|
17
20
|
aiwg repo-access check --path <repo-or-file> --action <read|write|commit|push|issue-comment|service-action|destructive>
|
|
21
|
+
aiwg repo-access add --path <p> --name <n> --allow <a,b,c> [--provider <gitea|github|gitlab>] [--notes "..."]
|
|
22
|
+
aiwg repo-access remove --name <n>
|
|
23
|
+
aiwg repo-access audit
|
|
18
24
|
|
|
19
25
|
Manifest:
|
|
20
26
|
.aiwg/aiwg.config workspace + repos blocks (preferred)
|
|
21
27
|
.aiwg/ops/security/repo-access.manifest.yaml
|
|
22
28
|
.aiwg/security/repo-access.manifest.yaml (fallback)
|
|
23
|
-
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
function printHelp() {
|
|
32
|
+
console.log(usage());
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The manifest is mandatory and default-deny, but until now it had no write path:
|
|
36
|
+
* registering a repo meant hand-editing JSON, and the rule's own recovery text
|
|
37
|
+
* ("ask for a manifest update") had no supported way to be carried out (#2531).
|
|
38
|
+
*/
|
|
39
|
+
async function addRepo(ctx, args) {
|
|
40
|
+
const repoPath = valueAfter(args, '--path');
|
|
41
|
+
const name = valueAfter(args, '--name');
|
|
42
|
+
const allowRaw = valueAfter(args, '--allow');
|
|
43
|
+
if (!repoPath)
|
|
44
|
+
return { exitCode: 2, message: 'repo-access add requires --path <repo-or-file>' };
|
|
45
|
+
if (!name)
|
|
46
|
+
return { exitCode: 2, message: 'repo-access add requires --name <name>' };
|
|
47
|
+
if (!allowRaw) {
|
|
48
|
+
return { exitCode: 2, message: `repo-access add requires --allow <${WORKSPACE_REPO_ACTIONS.join('|')}>` };
|
|
49
|
+
}
|
|
50
|
+
const allowed = allowRaw.split(',').map((item) => item.trim()).filter(Boolean);
|
|
51
|
+
const invalid = allowed.filter((item) => !WORKSPACE_REPO_ACTIONS.includes(item));
|
|
52
|
+
if (invalid.length > 0) {
|
|
53
|
+
return {
|
|
54
|
+
exitCode: 2,
|
|
55
|
+
message: `Unknown action(s): ${invalid.join(', ')}. Valid: ${WORKSPACE_REPO_ACTIONS.join(', ')}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const provider = valueAfter(args, '--provider') ?? undefined;
|
|
59
|
+
if (provider && !['gitea', 'github', 'gitlab'].includes(provider)) {
|
|
60
|
+
return { exitCode: 2, message: `Unknown provider: ${provider}. Valid: gitea, github, gitlab` };
|
|
61
|
+
}
|
|
62
|
+
const projectDir = getProjectDir(ctx, args);
|
|
63
|
+
const config = await readAiwgConfig(projectDir);
|
|
64
|
+
if (!config)
|
|
65
|
+
return { exitCode: 2, message: `No .aiwg/aiwg.config found in ${projectDir}` };
|
|
66
|
+
const repos = config.repos ? [...config.repos] : [];
|
|
67
|
+
const entry = {
|
|
68
|
+
name,
|
|
69
|
+
path: repoPath,
|
|
70
|
+
allowed: allowed,
|
|
71
|
+
...(provider ? { provider: provider } : {}),
|
|
72
|
+
...(valueAfter(args, '--notes') ? { notes: valueAfter(args, '--notes') } : {}),
|
|
73
|
+
};
|
|
74
|
+
const existingIndex = repos.findIndex((repo) => repo.name === name);
|
|
75
|
+
if (existingIndex >= 0) {
|
|
76
|
+
repos[existingIndex] = { ...repos[existingIndex], ...entry };
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
repos.push(entry);
|
|
80
|
+
}
|
|
81
|
+
await writeAiwgConfig(projectDir, { ...config, repos });
|
|
82
|
+
console.log(`${existingIndex >= 0 ? 'Updated' : 'Registered'} ${name}: ${repoPath} [${allowed.join(', ')}]`);
|
|
83
|
+
return { exitCode: 0 };
|
|
84
|
+
}
|
|
85
|
+
async function removeRepo(ctx, args) {
|
|
86
|
+
const name = valueAfter(args, '--name');
|
|
87
|
+
if (!name)
|
|
88
|
+
return { exitCode: 2, message: 'repo-access remove requires --name <name>' };
|
|
89
|
+
const projectDir = getProjectDir(ctx, args);
|
|
90
|
+
const config = await readAiwgConfig(projectDir);
|
|
91
|
+
if (!config)
|
|
92
|
+
return { exitCode: 2, message: `No .aiwg/aiwg.config found in ${projectDir}` };
|
|
93
|
+
const repos = config.repos ?? [];
|
|
94
|
+
const remaining = repos.filter((repo) => repo.name !== name);
|
|
95
|
+
if (remaining.length === repos.length) {
|
|
96
|
+
return { exitCode: 1, message: `No repo named '${name}' in the manifest.` };
|
|
97
|
+
}
|
|
98
|
+
// An empty `repos` array fails config validation, so drop the key entirely
|
|
99
|
+
// when the last entry goes rather than writing a config that cannot be read back.
|
|
100
|
+
const next = { ...config };
|
|
101
|
+
if (remaining.length > 0)
|
|
102
|
+
next.repos = remaining;
|
|
103
|
+
else
|
|
104
|
+
delete next.repos;
|
|
105
|
+
await writeAiwgConfig(projectDir, next);
|
|
106
|
+
console.log(`Removed ${name}. It now falls under the default-deny policy.`);
|
|
107
|
+
return { exitCode: 0 };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Report workspace subdirectories that are git repos but carry no manifest entry.
|
|
111
|
+
* Manifests drift out of sync with reality; on the reporting workspace two
|
|
112
|
+
* actively-used repos were both unlisted and therefore formally denied (#2531).
|
|
113
|
+
*/
|
|
114
|
+
async function auditRepos(ctx, args) {
|
|
115
|
+
// A missing manifest is the most important case to audit, not a reason to fail:
|
|
116
|
+
// every repo is then unlisted and formally denied.
|
|
117
|
+
let manifest = null;
|
|
118
|
+
try {
|
|
119
|
+
manifest = loadRepoAccessManifest(ctx.cwd);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
manifest = null;
|
|
123
|
+
}
|
|
124
|
+
const root = manifest?.workspaceProjectRoot ?? getProjectDir(ctx, args);
|
|
125
|
+
let children = [];
|
|
126
|
+
try {
|
|
127
|
+
children = readdirSync(root);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return { exitCode: 2, message: `Cannot read workspace root: ${root}` };
|
|
131
|
+
}
|
|
132
|
+
const unlisted = [];
|
|
133
|
+
for (const child of children.sort()) {
|
|
134
|
+
if (child.startsWith('.'))
|
|
135
|
+
continue;
|
|
136
|
+
const full = nodePath.join(root, child);
|
|
137
|
+
try {
|
|
138
|
+
if (!statSync(full).isDirectory())
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!existsSync(nodePath.join(full, '.git')))
|
|
145
|
+
continue;
|
|
146
|
+
if (manifest && findRepoEntry(manifest, full, ctx.cwd))
|
|
147
|
+
continue;
|
|
148
|
+
unlisted.push(child);
|
|
149
|
+
}
|
|
150
|
+
console.log(`Repo access manifest: ${manifest?.path ?? '(none — every repo is denied)'}`);
|
|
151
|
+
console.log(`Workspace root: ${root}`);
|
|
152
|
+
console.log(`Default policy: ${manifest?.defaultPolicy ?? 'deny'}`);
|
|
153
|
+
if (unlisted.length === 0) {
|
|
154
|
+
console.log('All git repositories under the workspace root are registered.');
|
|
155
|
+
return { exitCode: 0 };
|
|
156
|
+
}
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log(`Unlisted git repositories (${unlisted.length}) — denied by default:`);
|
|
159
|
+
for (const name of unlisted) {
|
|
160
|
+
console.log(` - ${name}`);
|
|
161
|
+
console.log(` aiwg repo-access add --path ./${name} --name ${name} --allow read`);
|
|
162
|
+
}
|
|
163
|
+
return { exitCode: 1 };
|
|
24
164
|
}
|
|
25
165
|
async function handleRepoAccess(ctx) {
|
|
26
166
|
const [subcommand = 'help', ...args] = ctx.args;
|
|
@@ -28,6 +168,14 @@ async function handleRepoAccess(ctx) {
|
|
|
28
168
|
printHelp();
|
|
29
169
|
return { exitCode: 0 };
|
|
30
170
|
}
|
|
171
|
+
// add/remove write .aiwg/aiwg.config directly and must work before any manifest
|
|
172
|
+
// exists — registering the first repo is exactly the bootstrap case (#2531).
|
|
173
|
+
if (subcommand === 'add')
|
|
174
|
+
return await addRepo(ctx, args);
|
|
175
|
+
if (subcommand === 'remove')
|
|
176
|
+
return await removeRepo(ctx, args);
|
|
177
|
+
if (subcommand === 'audit')
|
|
178
|
+
return await auditRepos(ctx, args);
|
|
31
179
|
try {
|
|
32
180
|
const manifest = loadRepoAccessManifest(ctx.cwd);
|
|
33
181
|
if (subcommand === 'list' || subcommand === 'status') {
|
|
@@ -87,7 +235,7 @@ async function handleRepoAccess(ctx) {
|
|
|
87
235
|
}
|
|
88
236
|
return { exitCode: decision.allowed ? 0 : 1 };
|
|
89
237
|
}
|
|
90
|
-
return { exitCode: 2, message: `Unknown repo-access subcommand: ${subcommand}` };
|
|
238
|
+
return { exitCode: 2, message: `Unknown repo-access subcommand: ${subcommand}\n${usage()}` };
|
|
91
239
|
}
|
|
92
240
|
catch (error) {
|
|
93
241
|
return {
|
|
@@ -103,6 +251,9 @@ export const repoAccessHandler = {
|
|
|
103
251
|
description: 'Validate and query repo access manifest permissions',
|
|
104
252
|
category: 'utility',
|
|
105
253
|
aliases: [],
|
|
254
|
+
async help() {
|
|
255
|
+
return { exitCode: 0, message: usage(), rawOutput: true };
|
|
256
|
+
},
|
|
106
257
|
execute: handleRepoAccess,
|
|
107
258
|
};
|
|
108
259
|
export const repoAccessHandlers = [repoAccessHandler];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'child_process';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
|
-
import { emptyConfig, getConfigPath, getProjectDir, readAiwgConfig, resolveRemoteProvider, VALID_PROVIDERS, writeAiwgConfig, } from '../../config/aiwg-config.js';
|
|
3
|
+
import { emptyConfig, FORCE_PUSH_POLICY_ALIAS_NOTE, normalizeForcePushPolicy as normalizeForcePushPolicyShared, getConfigPath, getProjectDir, readAiwgConfig, resolveRemoteProvider, VALID_PROVIDERS, writeAiwgConfig, } from '../../config/aiwg-config.js';
|
|
4
4
|
import { AiwgError, EXIT_CODES } from '../errors.js';
|
|
5
5
|
import { askChoice, askString, askYesNo, createPromptInterface } from '../prompt-utils.js';
|
|
6
6
|
import * as ui from '../ui.js';
|
|
@@ -165,11 +165,11 @@ function secondaryRemotes(remotes, primary, issueTracker, ci) {
|
|
|
165
165
|
}));
|
|
166
166
|
}
|
|
167
167
|
function normalizeForcePushPolicy(value, warnings) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
const { policy, deprecatedFrom } = normalizeForcePushPolicyShared(value);
|
|
169
|
+
if (deprecatedFrom) {
|
|
170
|
+
warnings.push(`delivery.force_push_policy=${deprecatedFrom} is a legacy alias; setup normalized it to ${policy}. ${FORCE_PUSH_POLICY_ALIAS_NOTE}`);
|
|
171
171
|
}
|
|
172
|
-
return
|
|
172
|
+
return policy;
|
|
173
173
|
}
|
|
174
174
|
function cloneConfig(config) {
|
|
175
175
|
return JSON.parse(JSON.stringify(config));
|
|
@@ -185,6 +185,18 @@ function printFullMatrix(matrix) {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
// ── Main execution ─────────────────────────────────────────────────────────────
|
|
188
|
+
function permissionsUsage() {
|
|
189
|
+
return `
|
|
190
|
+
aiwg steward permissions — authorization model audit and normalization
|
|
191
|
+
|
|
192
|
+
Usage:
|
|
193
|
+
aiwg steward permissions audit Find normalized-model errors and legacy grants
|
|
194
|
+
aiwg steward permissions migrate --dry-run Preview legacy permission normalization
|
|
195
|
+
aiwg steward permissions migrate --apply Back up and atomically normalize config
|
|
196
|
+
|
|
197
|
+
Reads .aiwg/aiwg.config authorization block. Migration backs up before writing.
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
188
200
|
async function handleSteward(args, ctx) {
|
|
189
201
|
const subcommand = args[0];
|
|
190
202
|
if (!subcommand || subcommand === '--help' || subcommand === 'help') {
|
|
@@ -221,6 +233,11 @@ async function handleSteward(args, ctx) {
|
|
|
221
233
|
}
|
|
222
234
|
if (subcommand === 'permissions') {
|
|
223
235
|
const operation = args[1];
|
|
236
|
+
// `<namespace> --help` must reach the same usage block bare invocation prints (#2533).
|
|
237
|
+
if (!operation || operation === 'help' || operation === '--help' || operation === '-h') {
|
|
238
|
+
console.log(permissionsUsage());
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
224
241
|
const projectDir = ctx ? getProjectDir(ctx, args) : process.cwd();
|
|
225
242
|
const config = await readAiwgConfig(projectDir);
|
|
226
243
|
if (!config)
|
|
@@ -273,7 +290,7 @@ async function handleSteward(args, ctx) {
|
|
|
273
290
|
}
|
|
274
291
|
throw new AiwgError({
|
|
275
292
|
code: 'ERR_USAGE_UNKNOWN_PERMISSION_OPERATION',
|
|
276
|
-
message: `Unknown permissions operation: ${operation
|
|
293
|
+
message: `Unknown permissions operation: ${operation}`,
|
|
277
294
|
hint: 'Use audit or migrate --dry-run|--apply.',
|
|
278
295
|
exitCode: EXIT_CODES.USAGE,
|
|
279
296
|
});
|
|
@@ -598,6 +615,18 @@ export const stewardHandler = {
|
|
|
598
615
|
description: 'Provider capability routing and permission normalization',
|
|
599
616
|
category: 'maintenance',
|
|
600
617
|
aliases: [],
|
|
618
|
+
// The router intercepts --help before execute(), so a handler without this
|
|
619
|
+
// property gets the generic "no detailed help" stub even when its own usage
|
|
620
|
+
// text exists. Route to the same block bare invocation prints, and keep
|
|
621
|
+
// sub-namespace help reachable, without executing anything (#2533).
|
|
622
|
+
async help(ctx) {
|
|
623
|
+
const positional = ctx.args.filter((arg) => !arg.startsWith('-'));
|
|
624
|
+
if (positional[0] === 'permissions') {
|
|
625
|
+
return { exitCode: 0, message: permissionsUsage(), rawOutput: true };
|
|
626
|
+
}
|
|
627
|
+
await handleSteward([], ctx);
|
|
628
|
+
return { exitCode: 0 };
|
|
629
|
+
},
|
|
601
630
|
async execute(ctx) {
|
|
602
631
|
try {
|
|
603
632
|
await handleSteward(ctx.args, ctx);
|
|
@@ -694,8 +694,10 @@ const SESSION_RELOAD_NOTICE = {
|
|
|
694
694
|
rationale: 'Claude Code reads .claude/agents/ at session start. A running session retains its old registry until reloaded.',
|
|
695
695
|
},
|
|
696
696
|
codex: {
|
|
697
|
-
|
|
698
|
-
|
|
697
|
+
required: false,
|
|
698
|
+
action: 'No restart needed for deployed skills — Codex exposes them on the next turn. Reopen Codex in this workspace only if a deployed skill or agent is still missing after that.',
|
|
699
|
+
rationale: 'A running Codex desktop session listed the newly deployed project skills on the very next user turn without any restart (#2309). Custom agent registry and MCP server changes were not observed to refresh live, so reopening remains the fallback for those.',
|
|
700
|
+
symptom: 'If a deployed skill or agent stays absent after the next turn, the registry did not rescan — reopen Codex in this workspace.',
|
|
699
701
|
},
|
|
700
702
|
copilot: {
|
|
701
703
|
action: 'Reload the VS Code window (`Developer: Reload Window`) so Copilot picks up the new .github/agents/ entries.',
|
|
@@ -736,7 +738,8 @@ function printSessionReloadNotice(provider) {
|
|
|
736
738
|
if (!notice)
|
|
737
739
|
return;
|
|
738
740
|
const defaultSymptom = 'Until reloaded, the Agent/Task tool will report "Agent type not found" for the newly deployed agents.';
|
|
739
|
-
|
|
741
|
+
const required = notice.required !== false;
|
|
742
|
+
ui.section(required ? 'Session reload required:' : 'Session reload (only if something is missing):', [
|
|
740
743
|
notice.action,
|
|
741
744
|
`Why: ${notice.rationale}`,
|
|
742
745
|
notice.symptom ?? defaultSymptom,
|
|
@@ -1269,7 +1272,9 @@ async function deployOneProjectLocalBundle(opts) {
|
|
|
1269
1272
|
args.push('--quiet');
|
|
1270
1273
|
// Project-local bundles are addon-shaped — never trigger the legacy commands
|
|
1271
1274
|
// migration prompt (which is only relevant for full-framework deploys).
|
|
1272
|
-
|
|
1275
|
+
// This is a structural opt-out, not the operator declining, so suppress the
|
|
1276
|
+
// duplicate-commands warning too: it fired once per bundle (#2541).
|
|
1277
|
+
args.push('--skip-commands-migration', '--no-commands-warning');
|
|
1273
1278
|
const captureOpts = quiet && !verbose ? { capture: true } : {};
|
|
1274
1279
|
// Inject AIWG_ROOT so the deploy subprocess can resolve the upstream AIWG
|
|
1275
1280
|
// install root. The bundle's `--source` is its project-local path, so
|
|
@@ -12,7 +12,7 @@ import { getVersionInfo } from '../../channel/manager.mjs';
|
|
|
12
12
|
import { getLoggerInfo } from '../log.js';
|
|
13
13
|
import * as ui from '../ui.js';
|
|
14
14
|
import { maybePrintCommunityFooter } from '../../community/footer.js';
|
|
15
|
-
import { existsSync, statSync, readdirSync } from 'fs';
|
|
15
|
+
import { existsSync, statSync, readdirSync, readFileSync } from 'fs';
|
|
16
16
|
import path from 'path';
|
|
17
17
|
/**
|
|
18
18
|
* Version command handler
|
|
@@ -30,6 +30,18 @@ export const versionHandler = {
|
|
|
30
30
|
return { exitCode: 0 };
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* Print the installation-drift notice when the canonical declaration and the
|
|
35
|
+
* running binary disagree. Keeps `aiwg version` honest about which one it is
|
|
36
|
+
* describing, and names the command that explains the rest (#2529).
|
|
37
|
+
*/
|
|
38
|
+
function printDrift(fp) {
|
|
39
|
+
if (!fp.drift)
|
|
40
|
+
return;
|
|
41
|
+
const method = fp.installation?.identity?.method ?? 'unrecorded';
|
|
42
|
+
const declared = fp.drift.canonicalVersion ? ` (${fp.drift.canonicalVersion})` : '';
|
|
43
|
+
ui.dim(` ! canonical install declares ${method} at ${fp.drift.canonicalRoot}${declared} — run \`aiwg installation show\``);
|
|
44
|
+
}
|
|
33
45
|
function collectFingerprint(versionInfo) {
|
|
34
46
|
const loggerInfo = getLoggerInfo();
|
|
35
47
|
const fp = {
|
|
@@ -62,6 +74,22 @@ function collectFingerprint(versionInfo) {
|
|
|
62
74
|
path: versionInfo.edgePath ?? versionInfo.packageRoot,
|
|
63
75
|
};
|
|
64
76
|
}
|
|
77
|
+
// `version` is the first thing anyone runs to answer "what am I on". If the
|
|
78
|
+
// canonical install declares a different root than the one executing, say so
|
|
79
|
+
// here rather than letting drift persist while every check looks correct.
|
|
80
|
+
const canonicalRoot = fp.installation?.identity?.root;
|
|
81
|
+
const actualRoot = fp.installation?.actualRoot;
|
|
82
|
+
if (canonicalRoot && actualRoot && path.resolve(canonicalRoot) !== path.resolve(actualRoot)) {
|
|
83
|
+
let canonicalVersion = null;
|
|
84
|
+
try {
|
|
85
|
+
const pkg = JSON.parse(readFileSync(path.join(canonicalRoot, 'package.json'), 'utf8'));
|
|
86
|
+
canonicalVersion = pkg.version ?? null;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Canonical root may not exist or be readable; report the drift regardless.
|
|
90
|
+
}
|
|
91
|
+
fp.drift = { canonicalRoot, canonicalVersion, actualRoot };
|
|
92
|
+
}
|
|
65
93
|
// Locale / timezone are useful for timezone-dependent bug reports.
|
|
66
94
|
try {
|
|
67
95
|
fp.locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
@@ -96,13 +124,12 @@ async function displayVersion(opts) {
|
|
|
96
124
|
ui.blank();
|
|
97
125
|
console.log(` ${ui.brandMark()} ${ui.bold('aiwg')} ${ui.bold(fp.version)} ${ui.channelLabel(fp.channel)}`);
|
|
98
126
|
if (!opts.verbose) {
|
|
99
|
-
if (fp.git)
|
|
127
|
+
if (fp.git)
|
|
100
128
|
ui.dim(` git: ${fp.git.sha} (${fp.git.branch})`);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
129
|
+
// Always the root that actually executed — not the declared edge checkout,
|
|
130
|
+
// which is what made drift invisible here (#2529).
|
|
131
|
+
ui.dim(` path: ${fp.packageRoot}`);
|
|
132
|
+
printDrift(fp);
|
|
106
133
|
maybePrintCommunityFooter();
|
|
107
134
|
ui.blank();
|
|
108
135
|
return;
|
|
@@ -110,15 +137,14 @@ async function displayVersion(opts) {
|
|
|
110
137
|
// --verbose: the full environment fingerprint.
|
|
111
138
|
if (fp.git) {
|
|
112
139
|
ui.dim(` git: ${fp.git.sha} (${fp.git.branch})`);
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
ui.dim(` path: ${fp.packageRoot}`);
|
|
140
|
+
if (fp.git.path !== fp.packageRoot)
|
|
141
|
+
ui.dim(` edge: ${fp.git.path}`);
|
|
117
142
|
}
|
|
143
|
+
ui.dim(` path: ${fp.packageRoot}`);
|
|
118
144
|
ui.dim(` channel: ${fp.channel}`);
|
|
119
|
-
ui.dim(` install: ${fp.installation
|
|
120
|
-
ui.dim(` canonical: ${fp.installation
|
|
121
|
-
ui.dim(` actual: ${fp.installation.
|
|
145
|
+
ui.dim(` install: ${fp.installation?.identity?.method ?? 'unrecorded'} (${fp.installation?.state ?? 'unknown'})`);
|
|
146
|
+
ui.dim(` canonical: ${fp.installation?.identity?.root ?? '(unrecorded)'}`);
|
|
147
|
+
ui.dim(` actual: ${fp.installation?.actualRoot ?? fp.packageRoot}`);
|
|
122
148
|
ui.dim(` node: ${fp.node}`);
|
|
123
149
|
ui.dim(` platform: ${fp.platform.os} ${fp.platform.arch} (${fp.platform.release})`);
|
|
124
150
|
ui.dim(` tty: stdin=${fp.tty.stdin} stdout=${fp.tty.stdout} stderr=${fp.tty.stderr}`);
|
|
@@ -55,6 +55,14 @@ export const workspaceContextHandler = {
|
|
|
55
55
|
console.log(JSON.stringify(result, null, 2));
|
|
56
56
|
else {
|
|
57
57
|
console.log(`${result.dryRun ? 'Migration dry run' : 'Migration applied'}: ${result.changed ? 'changes found' : 'already canonical'}`);
|
|
58
|
+
for (const entry of result.audit.plan.routing) {
|
|
59
|
+
console.log(` ${entry.source}: ${entry.operatorBytes.toLocaleString()} chars -> ${entry.destination} (${entry.scope})`);
|
|
60
|
+
}
|
|
61
|
+
for (const entry of result.audit.plan.scopeReview) {
|
|
62
|
+
console.log(` REVIEW ${entry.source} carries ${entry.operatorBytes.toLocaleString()} chars of operator content and is scoped to ${entry.scope} by filename.`);
|
|
63
|
+
console.log(' Content in .aiwg/context/providers/ is read by that provider only.');
|
|
64
|
+
console.log(" If this is project-neutral methodology, move it into WORKSPACE.md's Project Context section first.");
|
|
65
|
+
}
|
|
58
66
|
for (const file of result.written)
|
|
59
67
|
console.log(` ${result.dryRun ? 'would write' : 'wrote'} ${file}`);
|
|
60
68
|
if (result.transactionId)
|
|
@@ -9,38 +9,48 @@ import { diagnoseWorkspaceContext, providerContextContract, } from '../../smiths
|
|
|
9
9
|
import { USER_SCOPE_PATHS } from '../scope-resolver.js';
|
|
10
10
|
const RESTART_NOTICES = {
|
|
11
11
|
claude: {
|
|
12
|
+
policy: 'restart-required',
|
|
12
13
|
action: 'Restart Claude Code so the running session reloads deployed agents and skills.',
|
|
13
14
|
reason: 'Claude Code reads its agent and skill registries when a session starts.',
|
|
14
15
|
},
|
|
15
16
|
codex: {
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
policy: 'live-refresh',
|
|
18
|
+
action: 'Reopen Codex in this workspace if a deployed skill or agent does not appear.',
|
|
19
|
+
reason: 'Codex refreshes project skills between turns — a running session exposed newly deployed skills on the next turn without a restart (#2309).',
|
|
20
|
+
fallback: 'Codex picks up newly deployed skills on the next turn. Reopen Codex in this workspace only if a deployed skill or agent is still missing after that.',
|
|
18
21
|
},
|
|
19
22
|
copilot: {
|
|
23
|
+
policy: 'restart-required',
|
|
20
24
|
action: 'Reload the VS Code window so Copilot reloads workspace agents and instructions.',
|
|
21
25
|
reason: 'Copilot caches workspace agent definitions until the VS Code window reloads.',
|
|
22
26
|
},
|
|
23
27
|
cursor: {
|
|
28
|
+
policy: 'restart-required',
|
|
24
29
|
action: 'Reload the Cursor workspace so it reloads agents and rules.',
|
|
25
30
|
reason: 'Cursor reads workspace agents and rules when the workspace opens.',
|
|
26
31
|
},
|
|
27
32
|
factory: {
|
|
33
|
+
policy: 'restart-required',
|
|
28
34
|
action: 'Restart the Factory droid runtime so it reloads deployed droids.',
|
|
29
35
|
reason: 'Factory loads its droid registry when the runtime starts.',
|
|
30
36
|
},
|
|
31
37
|
opencode: {
|
|
38
|
+
policy: 'restart-required',
|
|
32
39
|
action: 'Restart the OpenCode session so it reloads deployed agents.',
|
|
33
40
|
reason: 'OpenCode scans its agent directory when the session starts.',
|
|
34
41
|
},
|
|
35
42
|
openclaw: {
|
|
43
|
+
policy: 'restart-required',
|
|
36
44
|
action: 'Restart OpenClaw so it reloads its home-directory registry.',
|
|
37
45
|
reason: 'OpenClaw loads its home-directory registry when the process starts.',
|
|
38
46
|
},
|
|
39
47
|
warp: {
|
|
48
|
+
policy: 'restart-required',
|
|
40
49
|
action: 'Open a fresh Warp tab so it reloads project context.',
|
|
41
50
|
reason: 'Warp reads project context when a tab starts.',
|
|
42
51
|
},
|
|
43
52
|
windsurf: {
|
|
53
|
+
policy: 'restart-required',
|
|
44
54
|
action: 'Reload Devin Desktop so it reparses project context.',
|
|
45
55
|
reason: 'Devin Desktop reads the Windsurf-compatible project context when the workspace opens.',
|
|
46
56
|
},
|
|
@@ -327,9 +337,16 @@ export async function verifyProviderDeployment(options) {
|
|
|
327
337
|
const findings = [];
|
|
328
338
|
const counts = emptyCounts();
|
|
329
339
|
const restartNotice = RESTART_NOTICES[normalized] ?? null;
|
|
330
|
-
|
|
340
|
+
// A provider with no notice gets no restart claim, which is what it got
|
|
341
|
+
// before this field existed. The label is the absence of a known restart
|
|
342
|
+
// requirement, not a positive claim that the client refreshes live.
|
|
343
|
+
const reloadPolicy = restartNotice?.policy ?? 'live-refresh';
|
|
344
|
+
const restartRequired = reloadPolicy === 'restart-required';
|
|
345
|
+
// Only a `restart-required` provider gets an imperative restart step. A
|
|
346
|
+
// `live-refresh` provider keeps its rationale and a conditional fallback (#2309).
|
|
347
|
+
const restartAction = restartRequired ? (restartNotice?.action ?? null) : null;
|
|
331
348
|
const restartReason = restartNotice?.reason ?? null;
|
|
332
|
-
const
|
|
349
|
+
const reloadFallback = restartRequired ? null : (restartNotice?.fallback ?? null);
|
|
333
350
|
if (!definition) {
|
|
334
351
|
findings.push(finding(normalized, 'provider-unknown', 'blocking', `No provider definition is available for '${options.provider}'.`, 'Choose a supported provider or repair the project-local provider adapter.'));
|
|
335
352
|
}
|
|
@@ -492,6 +509,8 @@ export async function verifyProviderDeployment(options) {
|
|
|
492
509
|
restartRequired,
|
|
493
510
|
restartAction,
|
|
494
511
|
restartReason,
|
|
512
|
+
reloadPolicy,
|
|
513
|
+
reloadFallback,
|
|
495
514
|
counts,
|
|
496
515
|
phases,
|
|
497
516
|
findings,
|
|
@@ -501,7 +520,9 @@ export function buildDryRunUseResult(options) {
|
|
|
501
520
|
const providers = options.providers.map((provider) => {
|
|
502
521
|
const normalized = normalizeProviderDefinitionId(provider) ?? provider;
|
|
503
522
|
const restartNotice = RESTART_NOTICES[normalized] ?? null;
|
|
504
|
-
const
|
|
523
|
+
const reloadPolicy = restartNotice?.policy ?? 'live-refresh';
|
|
524
|
+
const restartRequired = reloadPolicy === 'restart-required';
|
|
525
|
+
const restartAction = restartRequired ? (restartNotice?.action ?? null) : null;
|
|
505
526
|
const phases = [
|
|
506
527
|
phase('resolve', 'planned', true, `Would resolve ${options.projectRoot}, ${normalized}, ${options.scope} scope.`),
|
|
507
528
|
phase('deploy', 'planned', true, 'Would deploy the requested managed artifact surface.'),
|
|
@@ -514,9 +535,11 @@ export function buildDryRunUseResult(options) {
|
|
|
514
535
|
provider: normalized,
|
|
515
536
|
scope: options.scope,
|
|
516
537
|
outcome: 'planned',
|
|
517
|
-
restartRequired
|
|
538
|
+
restartRequired,
|
|
518
539
|
restartAction,
|
|
519
540
|
restartReason: restartNotice?.reason ?? null,
|
|
541
|
+
reloadPolicy,
|
|
542
|
+
reloadFallback: restartRequired ? null : (restartNotice?.fallback ?? null),
|
|
520
543
|
counts: emptyCounts(),
|
|
521
544
|
phases,
|
|
522
545
|
findings: [],
|
|
@@ -613,6 +636,8 @@ export async function verifyConfiguredDeployments(projectRoot, filters = {}, fra
|
|
|
613
636
|
restartRequired: false,
|
|
614
637
|
restartAction: null,
|
|
615
638
|
restartReason: null,
|
|
639
|
+
reloadPolicy: 'live-refresh',
|
|
640
|
+
reloadFallback: null,
|
|
616
641
|
counts: emptyCounts(),
|
|
617
642
|
phases: [phase('verify', 'failed', true, 'No installed provider deployment could be resolved.')],
|
|
618
643
|
findings: [finding(fallback, 'deployment-not-configured', 'blocking', 'No installed provider deployment could be resolved.', 'Run aiwg use all --provider <provider>.')],
|
|
@@ -756,6 +781,14 @@ export function renderUseDeploymentResult(result, options = {}) {
|
|
|
756
781
|
lines.push(...wrapParagraph(`Framework index built: ${result.discovery.builtAt}`, width));
|
|
757
782
|
}
|
|
758
783
|
}
|
|
784
|
+
const reloadFallbacks = result.providers
|
|
785
|
+
.filter((provider) => !provider.restartRequired && provider.reloadFallback)
|
|
786
|
+
.map((provider) => provider.reloadFallback);
|
|
787
|
+
if (reloadFallbacks.length > 0) {
|
|
788
|
+
lines.push('', 'If something is missing');
|
|
789
|
+
for (const note of reloadFallbacks)
|
|
790
|
+
lines.push(...wrapParagraph(note, width));
|
|
791
|
+
}
|
|
759
792
|
const restartActions = result.providers
|
|
760
793
|
.filter((provider) => provider.restartRequired && provider.restartAction)
|
|
761
794
|
.map((provider) => provider.restartAction);
|