@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
|
@@ -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];
|
|
@@ -298,6 +298,9 @@ async function handleRuntimeInfo(args, cwd = process.cwd()) {
|
|
|
298
298
|
console.log(`\nAIWG Installation:`);
|
|
299
299
|
console.log(` Canonical: ${installation.identity?.method ?? 'unrecorded'} at ${installation.identity?.root ?? '(unrecorded)'}`);
|
|
300
300
|
console.log(` Actual: ${installation.actualMethod} at ${installation.actualRoot}`);
|
|
301
|
+
if (installation.launcher) {
|
|
302
|
+
console.log(` Launcher: ${installation.launcher.method} at ${installation.launcher.root} (edge redirect)`);
|
|
303
|
+
}
|
|
301
304
|
console.log(` Run mode: ${installation.identity?.runMode ?? '(unrecorded)'}`);
|
|
302
305
|
console.log(` State: ${installation.state}`);
|
|
303
306
|
// Scheduler backend detection
|
|
@@ -16,6 +16,8 @@ import { sandboxRegistry, normalizeSandboxEvent, } from '../../serve/sandbox-reg
|
|
|
16
16
|
import { routeTask } from '../../serve/agent-router.js';
|
|
17
17
|
import { routeDispatch } from '../../serve/dispatch-router.js';
|
|
18
18
|
import { observeA2ATerminalState } from '../../serve/a2a-terminal-observer.js';
|
|
19
|
+
import { respondToA2AMission } from '../../serve/mission-hitl.js';
|
|
20
|
+
import { A2A_HITL_PROMPT_V1 } from '../../a2a/client.js';
|
|
19
21
|
import { executorRegistry, validateRegisterPayload, validateDispatchPayload, validateEventEnvelope, } from '../../serve/executor-registry.js';
|
|
20
22
|
import { handleWebhook, IdempotencyCache, PushSecretRegistry, } from '../../a2a/webhook.js';
|
|
21
23
|
import { AiwgError, EXIT_CODES } from '../errors.js';
|
|
@@ -807,6 +809,7 @@ export async function startServer(opts) {
|
|
|
807
809
|
const a2aProtocolPolicy = configuredA2AProtocolPolicy;
|
|
808
810
|
try {
|
|
809
811
|
const result = await routeDispatch(executor, payload, {
|
|
812
|
+
optionalExtensions: [A2A_HITL_PROMPT_V1],
|
|
810
813
|
a2aProtocolPolicy,
|
|
811
814
|
allowA2AProtocolFallback: configuredA2AProtocolFallback,
|
|
812
815
|
allowLegacyExecutorFallback: configuredLegacyExecutorFallback,
|
|
@@ -872,7 +875,14 @@ export async function startServer(opts) {
|
|
|
872
875
|
...(a2aFallbackReason ? { fallbackReason: a2aFallbackReason } : {}),
|
|
873
876
|
});
|
|
874
877
|
}
|
|
875
|
-
executorRegistry.
|
|
878
|
+
const existingMission = executorRegistry.getMission(missionId);
|
|
879
|
+
// An idempotent replay must not discard in-flight/accepted approvals.
|
|
880
|
+
if (!(dispatchPath === 'v2' && a2aTask && a2aInstanceId
|
|
881
|
+
&& existingMission?.executorId === executor.executorId
|
|
882
|
+
&& existingMission.a2a?.taskId === a2aTask.id
|
|
883
|
+
&& existingMission.a2a.instanceId === a2aInstanceId)) {
|
|
884
|
+
executorRegistry.assignMission(missionId, executor.executorId);
|
|
885
|
+
}
|
|
876
886
|
if (dispatchPath === 'v2' && a2aTask && a2aInstanceId) {
|
|
877
887
|
void observeA2ATerminalState(executorRegistry, executor, missionId, a2aInstanceId, a2aTask, {
|
|
878
888
|
onError: (err) => {
|
|
@@ -929,7 +939,7 @@ export async function startServer(opts) {
|
|
|
929
939
|
error: mission.error,
|
|
930
940
|
});
|
|
931
941
|
});
|
|
932
|
-
// POST /api/v1/missions/:id/hitl_response →
|
|
942
|
+
// POST /api/v1/missions/:id/hitl_response → owning task's negotiated transport
|
|
933
943
|
app.post('/api/v1/missions/:id/hitl_response', async (c) => {
|
|
934
944
|
const missionId = c.req.param('id');
|
|
935
945
|
const mission = executorRegistry.getMission(missionId);
|
|
@@ -944,9 +954,17 @@ export async function startServer(opts) {
|
|
|
944
954
|
}
|
|
945
955
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
946
956
|
const payload = body;
|
|
947
|
-
if (!payload
|
|
957
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)
|
|
958
|
+
|| typeof payload.hitl_id !== 'string' || !payload.hitl_id || !Object.hasOwn(payload, 'response')) {
|
|
948
959
|
return c.json({ error: 'hitl_id and response are required' }, 400);
|
|
949
960
|
}
|
|
961
|
+
if (mission.a2a) {
|
|
962
|
+
const result = await respondToA2AMission(executorRegistry, missionId, payload.hitl_id, payload.response);
|
|
963
|
+
return c.json(result.body, result.status);
|
|
964
|
+
}
|
|
965
|
+
if (typeof payload.response !== 'string' || !payload.response) {
|
|
966
|
+
return c.json({ error: 'Legacy HITL response must be a non-empty string' }, 400);
|
|
967
|
+
}
|
|
950
968
|
// Push hitl_responded event to the executor over WS
|
|
951
969
|
const envelope = {
|
|
952
970
|
event: 'mission.hitl_responded',
|
|
@@ -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);
|
|
@@ -581,6 +581,7 @@ async function mirrorStandardCommandSkills(opts) {
|
|
|
581
581
|
projectPath: opts.target,
|
|
582
582
|
dryRun: opts.dryRun,
|
|
583
583
|
verbose: opts.verbose,
|
|
584
|
+
deployVersion: (await getVersionInfo()).version,
|
|
584
585
|
nameFilter: shouldMirrorStandardCommandSkill,
|
|
585
586
|
});
|
|
586
587
|
count += result.translated.length;
|
|
@@ -693,8 +694,10 @@ const SESSION_RELOAD_NOTICE = {
|
|
|
693
694
|
rationale: 'Claude Code reads .claude/agents/ at session start. A running session retains its old registry until reloaded.',
|
|
694
695
|
},
|
|
695
696
|
codex: {
|
|
696
|
-
|
|
697
|
-
|
|
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.',
|
|
698
701
|
},
|
|
699
702
|
copilot: {
|
|
700
703
|
action: 'Reload the VS Code window (`Developer: Reload Window`) so Copilot picks up the new .github/agents/ entries.',
|
|
@@ -735,7 +738,8 @@ function printSessionReloadNotice(provider) {
|
|
|
735
738
|
if (!notice)
|
|
736
739
|
return;
|
|
737
740
|
const defaultSymptom = 'Until reloaded, the Agent/Task tool will report "Agent type not found" for the newly deployed agents.';
|
|
738
|
-
|
|
741
|
+
const required = notice.required !== false;
|
|
742
|
+
ui.section(required ? 'Session reload required:' : 'Session reload (only if something is missing):', [
|
|
739
743
|
notice.action,
|
|
740
744
|
`Why: ${notice.rationale}`,
|
|
741
745
|
notice.symptom ?? defaultSymptom,
|
|
@@ -1053,6 +1057,43 @@ async function countBundleDeployedArtifacts(bundlePath, target, provider) {
|
|
|
1053
1057
|
};
|
|
1054
1058
|
}
|
|
1055
1059
|
const SKILL_SUPPORT_REFERENCE = /(?:^|[\s`('"\[])((?:templates|references|scripts|assets)\/[A-Za-z0-9._@/+\-]+)(?=$|[\s`)'"\],:;])/gm;
|
|
1060
|
+
/** Copy one support file, preserving its executable bit. */
|
|
1061
|
+
async function copySkillSupportFile(source, destination) {
|
|
1062
|
+
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
1063
|
+
await fs.copyFile(source, destination);
|
|
1064
|
+
await fs.chmod(destination, (await fs.stat(source)).mode & 0o777);
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Materialize a directory-valued support-asset reference (#2503).
|
|
1068
|
+
*
|
|
1069
|
+
* Applies the same rules the single-file path applies, per entry: symlinks are
|
|
1070
|
+
* refused rather than followed (a link inside a bundle can point anywhere), and
|
|
1071
|
+
* file modes are preserved so script packs stay executable. Empty directories
|
|
1072
|
+
* are still created — a reference to an empty pack is odd but not an error.
|
|
1073
|
+
*/
|
|
1074
|
+
async function copySkillSupportTree(source, destination, sourceSkillMd, reference, deployFile) {
|
|
1075
|
+
await fs.mkdir(destination, { recursive: true });
|
|
1076
|
+
const label = reference.replace(/\/+$/, '');
|
|
1077
|
+
const entries = await fs.readdir(source, { withFileTypes: true });
|
|
1078
|
+
for (const entry of entries) {
|
|
1079
|
+
const from = path.join(source, entry.name);
|
|
1080
|
+
const to = path.join(destination, entry.name);
|
|
1081
|
+
if (entry.isSymbolicLink()) {
|
|
1082
|
+
throw new Error(`unsafe skill support asset '${label}/${entry.name}' referenced by ${sourceSkillMd}: symbolic links are not deployed`);
|
|
1083
|
+
}
|
|
1084
|
+
if (entry.isDirectory()) {
|
|
1085
|
+
await copySkillSupportTree(from, to, sourceSkillMd, `${label}/${entry.name}`, deployFile);
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
if (!entry.isFile())
|
|
1089
|
+
continue;
|
|
1090
|
+
if (deployFile) {
|
|
1091
|
+
deployFile(from, to);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
await copySkillSupportFile(from, to);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1056
1097
|
/**
|
|
1057
1098
|
* Skill-relative support files may live beside the skill or at the bundle root
|
|
1058
1099
|
* (plugin payloads commonly share report templates). Materialize
|
|
@@ -1103,11 +1144,18 @@ async function reconcileDeployedSkillAssets(bundlePath, target, provider, option
|
|
|
1103
1144
|
}
|
|
1104
1145
|
const candidates = [path.join(sourceSkillDir, normalized), path.join(bundlePath, normalized)];
|
|
1105
1146
|
let source;
|
|
1147
|
+
let sourceIsDirectory = false;
|
|
1106
1148
|
for (const candidate of candidates) {
|
|
1107
1149
|
try {
|
|
1108
1150
|
const stat = await fs.lstat(candidate);
|
|
1109
|
-
if (stat.
|
|
1151
|
+
if (stat.isSymbolicLink())
|
|
1152
|
+
continue;
|
|
1153
|
+
// A reference may name a whole support directory (a templates pack,
|
|
1154
|
+
// a references folder). Rejecting those as "missing" aborted the
|
|
1155
|
+
// bundle deploy over a path that was present all along (#2503).
|
|
1156
|
+
if (stat.isFile() || stat.isDirectory()) {
|
|
1110
1157
|
source = candidate;
|
|
1158
|
+
sourceIsDirectory = stat.isDirectory();
|
|
1111
1159
|
break;
|
|
1112
1160
|
}
|
|
1113
1161
|
}
|
|
@@ -1119,6 +1167,9 @@ async function reconcileDeployedSkillAssets(bundlePath, target, provider, option
|
|
|
1119
1167
|
}
|
|
1120
1168
|
continue;
|
|
1121
1169
|
}
|
|
1170
|
+
if (sourceIsDirectory && declaredEntrypoints.has(relative)) {
|
|
1171
|
+
throw new Error(`skill entrypoint '${relative}' in ${sourceSkillMd} resolves to a directory; an entrypoint must be a file`);
|
|
1172
|
+
}
|
|
1122
1173
|
let deployedSkillRoot;
|
|
1123
1174
|
for (const root of deployRoots) {
|
|
1124
1175
|
// The deployer may select the bulk or kernel tier; use the tier that
|
|
@@ -1131,18 +1182,36 @@ async function reconcileDeployedSkillAssets(bundlePath, target, provider, option
|
|
|
1131
1182
|
if (!deployedSkillRoot)
|
|
1132
1183
|
throw new Error(`deployed skill '${skillName}' not found while reconciling support assets`);
|
|
1133
1184
|
const destination = path.join(deployedSkillRoot, skillName, ...normalized.split('/'));
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1185
|
+
const deployFile = provider === 'omp'
|
|
1186
|
+
? await (async () => {
|
|
1187
|
+
const adapter = await import(pathToFileURL(path.join(await getFrameworkRoot(), 'tools/agents/providers/omp.mjs')).href);
|
|
1188
|
+
return (from, to) => adapter.deploySkillSupportAsset(from, to, { quiet: true });
|
|
1189
|
+
})()
|
|
1190
|
+
: null;
|
|
1191
|
+
if (sourceIsDirectory) {
|
|
1192
|
+
await copySkillSupportTree(source, destination, sourceSkillMd, relative, deployFile);
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (deployFile) {
|
|
1196
|
+
deployFile(source, destination);
|
|
1137
1197
|
continue;
|
|
1138
1198
|
}
|
|
1139
|
-
await
|
|
1140
|
-
await fs.copyFile(source, destination);
|
|
1141
|
-
const mode = (await fs.stat(source)).mode & 0o777;
|
|
1142
|
-
await fs.chmod(destination, mode);
|
|
1199
|
+
await copySkillSupportFile(source, destination);
|
|
1143
1200
|
}
|
|
1144
1201
|
}
|
|
1145
1202
|
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Managed-marker version for a project-local bundle's deployed artifacts (#2502).
|
|
1205
|
+
*
|
|
1206
|
+
* The deployer otherwise derives this from a `package.json` in the `--source`
|
|
1207
|
+
* tree; project-local bundles carry a `manifest.json` instead, so every
|
|
1208
|
+
* artifact was stamped `vunknown`. Falls back to `unknown` only when the
|
|
1209
|
+
* manifest itself omits a version.
|
|
1210
|
+
*/
|
|
1211
|
+
function projectLocalDeployVersion(bundle) {
|
|
1212
|
+
const version = bundle.manifest.version;
|
|
1213
|
+
return typeof version === 'string' && version.length > 0 ? version : 'unknown';
|
|
1214
|
+
}
|
|
1146
1215
|
/**
|
|
1147
1216
|
* Deploy a single project-local bundle to one provider via deploy-agents.mjs.
|
|
1148
1217
|
* Runs the same script and flags used for upstream addons, with the bundle
|
|
@@ -1187,6 +1256,12 @@ async function deployOneProjectLocalBundle(opts) {
|
|
|
1187
1256
|
// never reach <provider>/.aiwg/skills/, leaving them invisible to
|
|
1188
1257
|
// both the platform and the index.
|
|
1189
1258
|
'--copy-all',
|
|
1259
|
+
// Provenance for the managed marker (#2502). Without this the deployer
|
|
1260
|
+
// stamps `bundled`/`unknown`, and `aiwg refresh`'s stale-artifact prune —
|
|
1261
|
+
// whose desired set is the packaged framework corpus — deletes every
|
|
1262
|
+
// project-local agent in the same run that re-deployed it.
|
|
1263
|
+
'--deploy-source', 'project-local',
|
|
1264
|
+
'--deploy-version', projectLocalDeployVersion(bundle),
|
|
1190
1265
|
...modelArgs,
|
|
1191
1266
|
];
|
|
1192
1267
|
if (dryRun)
|
|
@@ -1197,7 +1272,9 @@ async function deployOneProjectLocalBundle(opts) {
|
|
|
1197
1272
|
args.push('--quiet');
|
|
1198
1273
|
// Project-local bundles are addon-shaped — never trigger the legacy commands
|
|
1199
1274
|
// migration prompt (which is only relevant for full-framework deploys).
|
|
1200
|
-
|
|
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');
|
|
1201
1278
|
const captureOpts = quiet && !verbose ? { capture: true } : {};
|
|
1202
1279
|
// Inject AIWG_ROOT so the deploy subprocess can resolve the upstream AIWG
|
|
1203
1280
|
// install root. The bundle's `--source` is its project-local path, so
|
|
@@ -2122,6 +2199,35 @@ async function mirrorProjectLocalBundleToUserScope(opts) {
|
|
|
2122
2199
|
* Deploys framework agents, commands, and skills to the current project,
|
|
2123
2200
|
* then registers them in the extension registry for discovery.
|
|
2124
2201
|
*/
|
|
2202
|
+
const USE_HELP = `Usage: aiwg use <bundle> [options]
|
|
2203
|
+
|
|
2204
|
+
Deploy an AIWG framework, addon, or extension into the current project.
|
|
2205
|
+
|
|
2206
|
+
Bundles:
|
|
2207
|
+
all Kernel surface only (kernel skills, rules, behaviors)
|
|
2208
|
+
sdlc | research | ops | forensics | marketing | media-curator | ...
|
|
2209
|
+
Full framework surface (agents, commands, skills, rules)
|
|
2210
|
+
<addon> | <extension> Any installed addon or extension name
|
|
2211
|
+
|
|
2212
|
+
Options:
|
|
2213
|
+
--provider <name> Target provider (default: .aiwg/aiwg.config providers)
|
|
2214
|
+
--target <dir> Deploy into <dir> instead of the current directory
|
|
2215
|
+
--scope project|user Deploy to the project (default) or the user scope
|
|
2216
|
+
--force Re-write every artifact, replacing files AIWG does
|
|
2217
|
+
not currently manage. Use this to reclaim a
|
|
2218
|
+
directory left behind by an older AIWG install.
|
|
2219
|
+
--copy-all Mirror standard-tier skills into the project instead
|
|
2220
|
+
of relying on index-driven discovery
|
|
2221
|
+
--dry-run Preview the deployment without writing files
|
|
2222
|
+
--verbose, -v Show per-artifact deploy decisions
|
|
2223
|
+
--json Emit the machine-readable deployment result
|
|
2224
|
+
--no-project-local Skip project-local bundles under .aiwg/
|
|
2225
|
+
--no-context-files Skip WORKSPACE.md / AIWG.md / AGENTS.md emission
|
|
2226
|
+
-h, --help Show this help without deploying
|
|
2227
|
+
|
|
2228
|
+
Deployment counts report what the run wrote or already manages. Files AIWG does
|
|
2229
|
+
not own are listed separately as unmanaged and are never counted as deployed.
|
|
2230
|
+
`;
|
|
2125
2231
|
export class UseHandler {
|
|
2126
2232
|
id = 'use';
|
|
2127
2233
|
name = 'Use Framework';
|
|
@@ -2129,6 +2235,9 @@ export class UseHandler {
|
|
|
2129
2235
|
category = 'framework';
|
|
2130
2236
|
aliases = [];
|
|
2131
2237
|
orchestrationDepth = 0;
|
|
2238
|
+
async help() {
|
|
2239
|
+
return { exitCode: 0, message: USE_HELP, rawOutput: true };
|
|
2240
|
+
}
|
|
2132
2241
|
async execute(ctx) {
|
|
2133
2242
|
const requestedBundle = firstUsePositional(ctx.args)
|
|
2134
2243
|
?? (ctx.args[0] === '--profile' ? 'all' : undefined);
|
|
@@ -3400,6 +3509,7 @@ export class UseHandler {
|
|
|
3400
3509
|
projectPath: target,
|
|
3401
3510
|
dryRun,
|
|
3402
3511
|
verbose,
|
|
3512
|
+
deployVersion: (await getVersionInfo()).version,
|
|
3403
3513
|
});
|
|
3404
3514
|
if (verbose && translationResult.translated.length > 0) {
|
|
3405
3515
|
ui.success(`Translated ${translationResult.translated.length} skills → commands (${provider})`);
|
|
@@ -3450,6 +3560,7 @@ export class UseHandler {
|
|
|
3450
3560
|
projectPath: target,
|
|
3451
3561
|
dryRun,
|
|
3452
3562
|
verbose,
|
|
3563
|
+
deployVersion: (await getVersionInfo()).version,
|
|
3453
3564
|
nameFilter: shouldMirrorKernelCommandSkill,
|
|
3454
3565
|
});
|
|
3455
3566
|
if (verbose && kernel.translated.length > 0) {
|
|
@@ -548,18 +548,34 @@ export const doctorHandler = {
|
|
|
548
548
|
namespace: 'aiwg',
|
|
549
549
|
skillsBaseDir: skillsDir,
|
|
550
550
|
});
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
551
|
+
// Two distinct causes share this scan, and conflating them mislabels
|
|
552
|
+
// one as the other: an `error` is a name that shadows a Claude
|
|
553
|
+
// built-in; a `warn` is a deployed skill this namespace does not own
|
|
554
|
+
// (#2504). Report each under its own heading with its own remedy.
|
|
555
|
+
const builtinCollisions = collisions.filter(r => r.severity === 'error');
|
|
556
|
+
const unownedCollisions = collisions.filter(r => r.severity === 'warn');
|
|
557
|
+
if (builtinCollisions.length > 0 || unownedCollisions.length > 0) {
|
|
555
558
|
console.log('\n── Skill collision scan ──');
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
559
|
+
if (builtinCollisions.length > 0) {
|
|
560
|
+
console.log('');
|
|
561
|
+
console.log('⚠ Stale skills detected (names collide with Claude built-ins):');
|
|
562
|
+
for (const r of builtinCollisions) {
|
|
563
|
+
console.log(` ✗ ${r.skillName}: ${r.reason}`);
|
|
564
|
+
}
|
|
565
|
+
console.log('');
|
|
566
|
+
console.log(' Fix: run `aiwg use <framework>` to redeploy and auto-clean stale skill directories.');
|
|
567
|
+
}
|
|
568
|
+
if (unownedCollisions.length > 0) {
|
|
569
|
+
console.log('');
|
|
570
|
+
console.log("⚠ Deployed skills not owned by namespace 'aiwg' (a redeploy would overwrite them):");
|
|
571
|
+
for (const r of unownedCollisions) {
|
|
572
|
+
console.log(` ✗ ${r.skillName}: ${r.reason}`);
|
|
573
|
+
}
|
|
574
|
+
console.log('');
|
|
575
|
+
console.log(" Fix: if the skill is yours, move it out of the AIWG-managed skills directory");
|
|
576
|
+
console.log(" or give it its own namespace. If AIWG generated it, re-run `aiwg use` to");
|
|
577
|
+
console.log(' restore the ownership marker.');
|
|
560
578
|
}
|
|
561
|
-
console.log('');
|
|
562
|
-
console.log(' Fix: run `aiwg use <framework>` to redeploy and auto-clean stale skill directories.');
|
|
563
579
|
}
|
|
564
580
|
}
|
|
565
581
|
}
|