@aiwg/cli 2026.9.5 → 2026.9.7
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/cli/handlers/help.js +7 -1
- package/dist/src/cli/handlers/installation.js +4 -0
- package/dist/src/cli/handlers/mc.js +13 -20
- package/dist/src/cli/handlers/ralph.js +14 -4
- package/dist/src/cli/handlers/refresh.js +298 -30
- package/dist/src/cli/handlers/runtime-info.js +3 -0
- package/dist/src/cli/handlers/serve.js +21 -3
- package/dist/src/cli/handlers/use.js +114 -8
- package/dist/src/cli/handlers/utilities.js +26 -10
- package/dist/src/cli/services/deployment-verification.js +117 -1
- package/dist/src/cli/watch-service.js +47 -4
- package/dist/src/config/project-artifacts-health.mjs +15 -2
- package/dist/src/cost/fleet-report.js +19 -5
- 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/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 +81 -5
- package/dist/src/testing/fixtures/test-data-factory.js +3 -3
- package/dist/src/writing/pattern-library.js +29 -6
- package/package.json +3 -1
- package/tools/agents/deploy-agents.mjs +87 -5
- package/tools/agents/providers/base.mjs +61 -2
|
@@ -8,12 +8,24 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @issue #1374
|
|
10
10
|
*/
|
|
11
|
-
import { A2AClient } from '../a2a/client.js';
|
|
11
|
+
import { A2AClient, A2A_HITL_PROMPT_V1 } from '../a2a/client.js';
|
|
12
12
|
import { isTerminalTaskState, } from '../a2a/types.js';
|
|
13
13
|
import { extractGraphMetadata } from '../flow/graph-metadata.js';
|
|
14
|
+
import { extractHitlEnvelope } from '../a2a/hitl.js';
|
|
14
15
|
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
15
16
|
const DEFAULT_MAX_POLLS = 300;
|
|
16
17
|
export async function observeA2ATerminalState(registry, executor, missionId, a2aInstanceId, initialTask, opts = {}) {
|
|
18
|
+
const mission = registry.getMission(missionId);
|
|
19
|
+
if (mission && !(mission.a2a?.taskId === initialTask.id
|
|
20
|
+
&& mission.a2a.instanceId === a2aInstanceId)) {
|
|
21
|
+
mission.a2a = {
|
|
22
|
+
instanceId: a2aInstanceId, taskId: initialTask.id,
|
|
23
|
+
...(initialTask.contextId ? { contextId: initialTask.contextId } : {}),
|
|
24
|
+
protocolVersion: opts.protocolVersion ?? '0.3',
|
|
25
|
+
...(opts.selectedInterface ? { selectedInterface: opts.selectedInterface } : {}),
|
|
26
|
+
acceptedPrompts: new Set(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
17
29
|
try {
|
|
18
30
|
const clientOpts = {
|
|
19
31
|
baseUrl: executor.transportEndpoints.rest,
|
|
@@ -21,6 +33,7 @@ export async function observeA2ATerminalState(registry, executor, missionId, a2a
|
|
|
21
33
|
instanceId: a2aInstanceId,
|
|
22
34
|
protocolVersion: opts.protocolVersion ?? '0.3',
|
|
23
35
|
protocolPolicy: opts.protocolVersion ?? '0.3',
|
|
36
|
+
optionalExtensions: [A2A_HITL_PROMPT_V1],
|
|
24
37
|
};
|
|
25
38
|
if (opts.selectedInterface)
|
|
26
39
|
clientOpts.selectedInterface = opts.selectedInterface;
|
|
@@ -38,6 +51,9 @@ export async function observeA2ATerminalState(registry, executor, missionId, a2a
|
|
|
38
51
|
break;
|
|
39
52
|
await sleep(opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
40
53
|
task = await client.getTask(task.id);
|
|
54
|
+
if (task.id !== initialTask.id || task.contextId !== initialTask.contextId) {
|
|
55
|
+
throw new Error('A2A observer task binding changed');
|
|
56
|
+
}
|
|
41
57
|
emitNonTerminalProgress(registry, executor.executorId, missionId, task);
|
|
42
58
|
}
|
|
43
59
|
registry.failMission(missionId, `A2A task ${initialTask.id} did not reach a terminal state before observer timeout`);
|
|
@@ -58,10 +74,12 @@ function emitNonTerminalProgress(registry, executorId, missionId, task) {
|
|
|
58
74
|
return;
|
|
59
75
|
}
|
|
60
76
|
if (state === 'input-required') {
|
|
77
|
+
const prompt = extractHitlEnvelope(task);
|
|
61
78
|
registry.handleEvent(makeEnvelope('mission.hitl_required', executorId, missionId, task, {
|
|
62
79
|
state: 'hitl-required',
|
|
63
80
|
a2a_task_id: task.id,
|
|
64
81
|
summary: taskStatusSummary(task),
|
|
82
|
+
...(prompt?.ok ? { hitl_id: prompt.envelope.prompt_id, hitl_prompt: prompt.envelope } : {}),
|
|
65
83
|
}));
|
|
66
84
|
}
|
|
67
85
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** A2A approval routing for the public mission API. */
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import Ajv from 'ajv';
|
|
4
|
+
import { A2AClient, A2A_HITL_PROMPT_V1 } from '../a2a/client.js';
|
|
5
|
+
import { buildHitlResponseMessage, extractHitlEnvelope } from '../a2a/hitl.js';
|
|
6
|
+
export async function respondToA2AMission(registry, missionId, promptId, response, opts = {}) {
|
|
7
|
+
const mission = registry.getMission(missionId);
|
|
8
|
+
const binding = mission?.a2a;
|
|
9
|
+
const executor = mission && registry.getRegistration(mission.executorId);
|
|
10
|
+
if (!mission || !binding || !executor)
|
|
11
|
+
return reply(404, 'mission_binding_not_found');
|
|
12
|
+
if (binding.responding || binding.acceptedPrompts.has(promptId))
|
|
13
|
+
return reply(409, 'approval_already_submitted');
|
|
14
|
+
if (['done', 'failed', 'aborted'].includes(mission.state))
|
|
15
|
+
return reply(409, 'mission_terminal');
|
|
16
|
+
binding.responding = true;
|
|
17
|
+
try {
|
|
18
|
+
const client = new A2AClient({
|
|
19
|
+
baseUrl: executor.transportEndpoints.rest,
|
|
20
|
+
bearer: executor.token,
|
|
21
|
+
instanceId: binding.instanceId,
|
|
22
|
+
protocolVersion: binding.protocolVersion,
|
|
23
|
+
protocolPolicy: binding.protocolVersion,
|
|
24
|
+
optionalExtensions: [A2A_HITL_PROMPT_V1],
|
|
25
|
+
...(binding.selectedInterface ? { selectedInterface: binding.selectedInterface } : {}),
|
|
26
|
+
...(opts.fetch ? { fetch: opts.fetch } : {}),
|
|
27
|
+
});
|
|
28
|
+
// Re-read the owning task: cached mission events can outlive a prompt.
|
|
29
|
+
const task = await client.getTask(binding.taskId);
|
|
30
|
+
if (task.id !== binding.taskId || task.contextId !== binding.contextId)
|
|
31
|
+
return reply(409, 'task_binding_mismatch');
|
|
32
|
+
const extracted = extractHitlEnvelope(task);
|
|
33
|
+
if (!extracted?.ok)
|
|
34
|
+
return reply(409, 'no_valid_pending_approval');
|
|
35
|
+
const prompt = extracted.envelope;
|
|
36
|
+
if (prompt.prompt_id !== promptId)
|
|
37
|
+
return reply(409, 'approval_prompt_mismatch');
|
|
38
|
+
if (prompt.deadline && Date.parse(prompt.deadline) <= Date.now())
|
|
39
|
+
return reply(410, 'approval_expired');
|
|
40
|
+
// The local mission API has no authenticated operator identity. Never
|
|
41
|
+
// accept a caller-supplied name as authority for a restricted prompt.
|
|
42
|
+
if (prompt.allowed_responders && !prompt.allowed_responders.includes('any'))
|
|
43
|
+
return reply(403, 'authenticated_responder_required');
|
|
44
|
+
try {
|
|
45
|
+
const validate = new Ajv({ allErrors: true, strict: true }).compile(prompt.response_schema);
|
|
46
|
+
if ('$async' in validate && validate.$async === true)
|
|
47
|
+
return reply(422, 'approval_schema_unsupported');
|
|
48
|
+
if (!validate(response))
|
|
49
|
+
return reply(422, 'approval_response_invalid');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return reply(422, 'approval_schema_unsupported');
|
|
53
|
+
}
|
|
54
|
+
if (registry.getMission(missionId) !== mission || mission.a2a !== binding
|
|
55
|
+
|| ['done', 'failed', 'aborted'].includes(mission.state)) {
|
|
56
|
+
return reply(409, 'mission_changed_during_approval');
|
|
57
|
+
}
|
|
58
|
+
const digest = createHash('sha256').update(JSON.stringify(response)).digest('hex');
|
|
59
|
+
const attempts = binding.attempts ??= new Map();
|
|
60
|
+
const previous = attempts.get(promptId);
|
|
61
|
+
if (previous && previous.digest !== digest)
|
|
62
|
+
return reply(409, 'approval_retry_payload_changed');
|
|
63
|
+
const attempt = previous ?? { messageId: randomUUID(), digest };
|
|
64
|
+
attempts.set(promptId, attempt);
|
|
65
|
+
const result = await client.sendMessage(buildHitlResponseMessage({
|
|
66
|
+
promptId, response, messageId: attempt.messageId, taskId: binding.taskId,
|
|
67
|
+
...(binding.contextId ? { contextId: binding.contextId } : {}),
|
|
68
|
+
}));
|
|
69
|
+
if (result.task.id !== binding.taskId || result.task.contextId !== binding.contextId)
|
|
70
|
+
return reply(502, 'approval_result_binding_mismatch');
|
|
71
|
+
binding.acceptedPrompts.add(promptId);
|
|
72
|
+
// Audit correlation only; approval payloads can contain sensitive data.
|
|
73
|
+
registry.handleEvent({
|
|
74
|
+
event: 'mission.progress', executor_id: mission.executorId, mission_id: missionId,
|
|
75
|
+
ts: new Date().toISOString(),
|
|
76
|
+
data: { action: 'hitl_response_accepted', hitl_id: promptId, a2a_task_id: binding.taskId },
|
|
77
|
+
});
|
|
78
|
+
return { status: 200, body: { ok: true } };
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Preserve the binding for a retry; the next attempt rechecks executor state.
|
|
82
|
+
return reply(502, 'approval_forward_failed');
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
binding.responding = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function reply(status, error) {
|
|
89
|
+
return { status, body: { error } };
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=mission-hitl.js.map
|
|
@@ -49,9 +49,12 @@ export async function acquireImportLease(databasePath, runId, options = {}) {
|
|
|
49
49
|
heartbeatAt: observed,
|
|
50
50
|
};
|
|
51
51
|
await writeOwner(ownerPath, owner);
|
|
52
|
+
let heartbeatWrite = Promise.resolve();
|
|
52
53
|
const timer = setInterval(() => {
|
|
53
54
|
owner.heartbeatAt = now().toISOString();
|
|
54
|
-
|
|
55
|
+
heartbeatWrite = heartbeatWrite
|
|
56
|
+
.then(() => writeOwner(ownerPath, owner))
|
|
57
|
+
.catch(() => undefined);
|
|
55
58
|
}, heartbeatMs);
|
|
56
59
|
timer.unref();
|
|
57
60
|
let released = false;
|
|
@@ -63,6 +66,7 @@ export async function acquireImportLease(databasePath, runId, options = {}) {
|
|
|
63
66
|
return;
|
|
64
67
|
released = true;
|
|
65
68
|
clearInterval(timer);
|
|
69
|
+
await heartbeatWrite;
|
|
66
70
|
const current = await readOwner(ownerPath);
|
|
67
71
|
if (current?.runId === owner.runId) {
|
|
68
72
|
await rm(lockPath, { recursive: true, force: true });
|
|
@@ -142,7 +142,16 @@ async function firstReadmePurpose(projectPath) {
|
|
|
142
142
|
const content = await readOptional(path.join(projectPath, source));
|
|
143
143
|
if (!content || isGeneratedRootContext(source, content))
|
|
144
144
|
continue;
|
|
145
|
-
|
|
145
|
+
// Strip HTML before block-splitting. The per-line filter below drops lines
|
|
146
|
+
// that *start* with `<`, which misses the continuation lines of a tag that
|
|
147
|
+
// wraps — a hero `<a ...><img alt="..." width="1000"></a>` then yields its
|
|
148
|
+
// own attribute text as the project purpose. Removing tags outright (dotall,
|
|
149
|
+
// so multi-line tags are covered) leaves only prose for the filter to weigh.
|
|
150
|
+
const prose = content
|
|
151
|
+
.replace(/\r\n/g, '\n')
|
|
152
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
153
|
+
.replace(/<[^<>]*>/g, ' ');
|
|
154
|
+
const blocks = prose.split(/\n\s*\n/);
|
|
146
155
|
for (const block of blocks) {
|
|
147
156
|
const lines = block.split('\n').filter((line) => {
|
|
148
157
|
const trimmed = line.trim();
|
|
@@ -329,6 +338,18 @@ function workspaceLinks(projectPath, providerFiles = []) {
|
|
|
329
338
|
links.add('[Project-local quickref](.aiwg/quickref.json) (when configured)');
|
|
330
339
|
return [...links];
|
|
331
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Sentences that carry the rule-authority invariant (#2512).
|
|
343
|
+
*
|
|
344
|
+
* Diagnostics compare against these rather than the whole managed block: the
|
|
345
|
+
* block also contains a project-specific link list, so a full-text comparison
|
|
346
|
+
* would report drift on every workspace with an extra provider file. These are
|
|
347
|
+
* the parts whose absence actually changes agent behaviour.
|
|
348
|
+
*/
|
|
349
|
+
export const WORKSPACE_PRECEDENCE_SIGNATURE = 'AIWG rules deployed to this project bind over any provider, harness, or session';
|
|
350
|
+
export const BOOTSTRAP_AUTHORITY_SIGNATURE = 'AIWG rules deployed to this project are binding';
|
|
351
|
+
/** Precedence ordering superseded by #2512; its presence means a stale block. */
|
|
352
|
+
export const WORKSPACE_PRECEDENCE_SUPERSEDED = 'Provider, system, and organization instructions retain their native authority.';
|
|
332
353
|
export function buildWorkspaceManagedBlock(projectPath, providerFiles = []) {
|
|
333
354
|
const links = workspaceLinks(projectPath, providerFiles);
|
|
334
355
|
return [
|
|
@@ -342,10 +363,19 @@ export function buildWorkspaceManagedBlock(projectPath, providerFiles = []) {
|
|
|
342
363
|
'',
|
|
343
364
|
'### Precedence',
|
|
344
365
|
'',
|
|
345
|
-
'1.
|
|
346
|
-
'
|
|
347
|
-
'
|
|
348
|
-
'
|
|
366
|
+
'1. Platform capability and safety constraints are absolute: what a harness can do, what it is',
|
|
367
|
+
' permitted to do, and its refusal boundaries. Nothing here overrides those.',
|
|
368
|
+
'2. AIWG rules deployed to this project bind over any provider, harness, or session *directive*',
|
|
369
|
+
' on a subject an AIWG rule covers — including a directive that claims to supersede earlier',
|
|
370
|
+
' guidance. A harness decides how a tool is invoked; it does not set project policy.',
|
|
371
|
+
'3. Root WORKSPACE.md supplies shared project/operator context.',
|
|
372
|
+
'4. AIWG.md supplies generated framework/discovery context.',
|
|
373
|
+
'5. Narrower linked files and provider-native subtree instructions govern their declared scope,',
|
|
374
|
+
' within the ceiling set above.',
|
|
375
|
+
'',
|
|
376
|
+
'The distinction in 1 vs 2 is capability versus preference. "This tool is unavailable" is a',
|
|
377
|
+
'constraint. "Format commits this way" is a directive, and an AIWG rule on commit content wins.',
|
|
378
|
+
'When a directive and an AIWG rule conflict, follow the rule and say plainly that you did.',
|
|
349
379
|
'',
|
|
350
380
|
'### Ownership',
|
|
351
381
|
'',
|
|
@@ -430,9 +460,24 @@ export function buildProviderBootstrapBlock(provider) {
|
|
|
430
460
|
'This provider has no verified project-local automatic context loader.',
|
|
431
461
|
'If the harness exposes file-reading tools, read WORKSPACE.md first and AIWG.md second.',
|
|
432
462
|
'',
|
|
463
|
+
'AIWG rules deployed to this project are binding. They outrank provider, harness, and session',
|
|
464
|
+
'directives on any subject they cover, including directives that claim to supersede earlier',
|
|
465
|
+
'guidance. Platform capability and safety constraints remain absolute.',
|
|
466
|
+
'',
|
|
433
467
|
PROVIDER_BOOTSTRAP_END,
|
|
434
468
|
].join('\n');
|
|
435
469
|
}
|
|
470
|
+
// The bootstrap file is the first thing the harness reads, so rule authority
|
|
471
|
+
// is asserted here rather than left to be inferred once WORKSPACE.md loads.
|
|
472
|
+
// A session directive that arrives later cannot claim precedence it was never
|
|
473
|
+
// granted (#2512).
|
|
474
|
+
const authority = [
|
|
475
|
+
'',
|
|
476
|
+
'AIWG rules deployed to this project are binding. They outrank provider, harness, and session',
|
|
477
|
+
'directives on any subject they cover, including directives that claim to supersede earlier',
|
|
478
|
+
'guidance. Platform capability and safety constraints remain absolute; see WORKSPACE.md',
|
|
479
|
+
'"Precedence" for the capability-versus-directive distinction.',
|
|
480
|
+
];
|
|
436
481
|
const loading = contract.loadMode === 'native-include'
|
|
437
482
|
? [
|
|
438
483
|
'Load the canonical project context first, then the generated AIWG framework context:',
|
|
@@ -457,6 +502,7 @@ export function buildProviderBootstrapBlock(provider) {
|
|
|
457
502
|
'# Provider workspace bootstrap',
|
|
458
503
|
'',
|
|
459
504
|
...loading,
|
|
505
|
+
...authority,
|
|
460
506
|
'',
|
|
461
507
|
PROVIDER_BOOTSTRAP_END,
|
|
462
508
|
].join('\n');
|
|
@@ -971,6 +1017,26 @@ export async function diagnoseWorkspaceContext(projectPath) {
|
|
|
971
1017
|
for (const finding of audit.sensitiveFindings) {
|
|
972
1018
|
diagnostics.push({ severity: 'error', code: 'possible-secret', message: 'Possible credential value found in context; remove it.', path: finding.path });
|
|
973
1019
|
}
|
|
1020
|
+
// #2512 — "points at WORKSPACE.md" is not the same as "carries current policy".
|
|
1021
|
+
// Without this, a workspace generated before the precedence correction reads
|
|
1022
|
+
// as healthy while still telling agents that harness directives outrank AIWG
|
|
1023
|
+
// rules, and nothing ever prompts the regenerate that would fix it.
|
|
1024
|
+
if (workspace.includes(WORKSPACE_PRECEDENCE_SUPERSEDED)) {
|
|
1025
|
+
diagnostics.push({
|
|
1026
|
+
severity: 'warning',
|
|
1027
|
+
code: 'precedence-superseded',
|
|
1028
|
+
message: 'WORKSPACE.md carries the superseded precedence that ranks provider and harness instructions above AIWG rules. Run `aiwg regenerate`.',
|
|
1029
|
+
path: 'WORKSPACE.md',
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
else if (!workspace.includes(WORKSPACE_PRECEDENCE_SIGNATURE)) {
|
|
1033
|
+
diagnostics.push({
|
|
1034
|
+
severity: 'warning',
|
|
1035
|
+
code: 'precedence-missing',
|
|
1036
|
+
message: 'WORKSPACE.md does not state that AIWG rules bind over provider, harness, and session directives. Run `aiwg regenerate`.',
|
|
1037
|
+
path: 'WORKSPACE.md',
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
974
1040
|
const providers = await configuredProviders(projectPath);
|
|
975
1041
|
for (const provider of providers) {
|
|
976
1042
|
const definition = getProviderDefinition(provider);
|
|
@@ -981,6 +1047,16 @@ export async function diagnoseWorkspaceContext(projectPath) {
|
|
|
981
1047
|
if (targetContent?.includes(WORKSPACE_SIGNATURE) && !targetContent.includes('WORKSPACE.md')) {
|
|
982
1048
|
diagnostics.push({ severity: 'error', code: 'bootstrap-drift', message: `${target} is AIWG-managed but no longer points to WORKSPACE.md first.`, path: target });
|
|
983
1049
|
}
|
|
1050
|
+
// The bootstrap file is read before WORKSPACE.md, so a directive arriving
|
|
1051
|
+
// mid-session wins unless authority is asserted here too.
|
|
1052
|
+
if (targetContent?.includes(PROVIDER_BOOTSTRAP_START) && !targetContent.includes(BOOTSTRAP_AUTHORITY_SIGNATURE)) {
|
|
1053
|
+
diagnostics.push({
|
|
1054
|
+
severity: 'warning',
|
|
1055
|
+
code: 'authority-missing',
|
|
1056
|
+
message: `${target} does not assert that AIWG rules are binding. Run \`aiwg regenerate\`.`,
|
|
1057
|
+
path: target,
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
984
1060
|
}
|
|
985
1061
|
if (definition.context.configRegistration) {
|
|
986
1062
|
const registration = definition.context.configRegistration;
|
|
@@ -294,11 +294,11 @@ export class TestDataFactory {
|
|
|
294
294
|
const constraints = field.constraints || {};
|
|
295
295
|
switch (field.type) {
|
|
296
296
|
case 'string':
|
|
297
|
-
return this.generateString(constraints.minLength
|
|
297
|
+
return this.generateString(constraints.minLength ?? 1, constraints.maxLength ?? 50, constraints.pattern);
|
|
298
298
|
case 'number':
|
|
299
|
-
return this.generateNumber(constraints.min
|
|
299
|
+
return this.generateNumber(constraints.min ?? 0, constraints.max ?? 1000);
|
|
300
300
|
case 'integer':
|
|
301
|
-
return this.generateInteger(constraints.min
|
|
301
|
+
return this.generateInteger(constraints.min ?? 0, constraints.max ?? 1000);
|
|
302
302
|
case 'boolean':
|
|
303
303
|
return this.generateBoolean();
|
|
304
304
|
case 'date':
|
|
@@ -286,11 +286,18 @@ export class PatternLibrary {
|
|
|
286
286
|
* Export patterns in various formats
|
|
287
287
|
*/
|
|
288
288
|
exportPatterns(format) {
|
|
289
|
+
// RegExp objects otherwise serialize as {}, losing executable behavior.
|
|
290
|
+
const serialized = this.patterns.map(pattern => ({
|
|
291
|
+
...pattern,
|
|
292
|
+
pattern: pattern.pattern instanceof RegExp
|
|
293
|
+
? { source: pattern.pattern.source, flags: pattern.pattern.flags }
|
|
294
|
+
: pattern.pattern
|
|
295
|
+
}));
|
|
289
296
|
switch (format) {
|
|
290
297
|
case 'json':
|
|
291
|
-
return JSON.stringify(
|
|
298
|
+
return JSON.stringify(serialized, null, 2);
|
|
292
299
|
case 'yaml':
|
|
293
|
-
return yaml.stringify(
|
|
300
|
+
return yaml.stringify(serialized);
|
|
294
301
|
case 'markdown':
|
|
295
302
|
return this.exportAsMarkdown();
|
|
296
303
|
default:
|
|
@@ -311,11 +318,27 @@ export class PatternLibrary {
|
|
|
311
318
|
else {
|
|
312
319
|
throw new Error(`Unsupported import format: ${format}`);
|
|
313
320
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
321
|
+
if (!Array.isArray(patterns)) {
|
|
322
|
+
throw new Error('Imported patterns must be an array');
|
|
323
|
+
}
|
|
324
|
+
// Compile the complete input before changing any library index.
|
|
325
|
+
const compiled = patterns.map(pattern => {
|
|
326
|
+
const value = pattern?.pattern;
|
|
327
|
+
let regex;
|
|
328
|
+
if (typeof value === 'string') {
|
|
329
|
+
regex = this.createRegExpFromPattern(value);
|
|
330
|
+
}
|
|
331
|
+
else if (value && typeof value === 'object' &&
|
|
332
|
+
'source' in value && typeof value.source === 'string' &&
|
|
333
|
+
'flags' in value && typeof value.flags === 'string') {
|
|
334
|
+
regex = new RegExp(value.source, value.flags);
|
|
318
335
|
}
|
|
336
|
+
else {
|
|
337
|
+
throw new Error('Invalid pattern: expected a phrase string or regex source/flags');
|
|
338
|
+
}
|
|
339
|
+
return { ...pattern, pattern: regex };
|
|
340
|
+
});
|
|
341
|
+
for (const pattern of compiled) {
|
|
319
342
|
// Skip duplicates
|
|
320
343
|
if (!this.patternsById.has(pattern.id)) {
|
|
321
344
|
this.addPattern(pattern);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwg/cli",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.7",
|
|
4
4
|
"description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -80,7 +80,9 @@
|
|
|
80
80
|
"js-yaml": "^4.3.0",
|
|
81
81
|
"listr2": "^8.2.5",
|
|
82
82
|
"ora": "^5.4.1",
|
|
83
|
+
"saxes": "^6.0.0",
|
|
83
84
|
"semver": "^7.8.5",
|
|
85
|
+
"toml-eslint-parser": "0.10.0",
|
|
84
86
|
"yaml": "^2.9.0",
|
|
85
87
|
"zod": "^3.25.0"
|
|
86
88
|
},
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
* --as-agents-md Aggregate to single AGENTS.md (OpenAI/Codex)
|
|
30
30
|
* --create-agents-md Create/update AGENTS.md template
|
|
31
31
|
* --skip-commands-migration Skip deleting the commands directory (warns about duplicate TUI entries) (Factory/Codex/OpenCode/Cursor)
|
|
32
|
+
* --deploy-source <name> Managed-marker source for deployed artifacts (default: bundled)
|
|
33
|
+
* --deploy-version <version> Managed-marker version for deployed artifacts (default: source package.json)
|
|
32
34
|
*
|
|
33
35
|
* Modes:
|
|
34
36
|
* general - Deploy only writing-quality addon agents and commands (alias: writing)
|
|
@@ -73,9 +75,12 @@ import os from 'os';
|
|
|
73
75
|
import readline from 'readline';
|
|
74
76
|
import { fileURLToPath } from 'url';
|
|
75
77
|
import {
|
|
78
|
+
addManagedMarker,
|
|
76
79
|
collectBehaviorDirs,
|
|
77
80
|
collectFrameworkArtifacts,
|
|
78
81
|
computeAllArtifactBasenames,
|
|
82
|
+
computeAllSkillNames,
|
|
83
|
+
contentHash,
|
|
79
84
|
deployEmulatedBehaviors,
|
|
80
85
|
getAddonSkillDirs,
|
|
81
86
|
listSkillDirs,
|
|
@@ -85,6 +90,7 @@ import {
|
|
|
85
90
|
parseFrontmatter,
|
|
86
91
|
pruneStaleAiwgFiles,
|
|
87
92
|
resolveAiwgRoot,
|
|
93
|
+
updateSidecarManifest,
|
|
88
94
|
} from './providers/base.mjs';
|
|
89
95
|
const modelCatalog = loadRuntimeModelCatalog(staticModelCatalog);
|
|
90
96
|
|
|
@@ -293,6 +299,7 @@ function mirrorSkillsAsCommands(provider, target, srcRoot, opts) {
|
|
|
293
299
|
if (!opts.dryRun) fs.mkdirSync(targetDir, { recursive: true });
|
|
294
300
|
|
|
295
301
|
const ext = commandFileExtensionForProvider(provider);
|
|
302
|
+
const deployedEntries = [];
|
|
296
303
|
let count = 0;
|
|
297
304
|
|
|
298
305
|
for (const skillDir of skillDirs) {
|
|
@@ -301,8 +308,15 @@ function mirrorSkillsAsCommands(provider, target, srcRoot, opts) {
|
|
|
301
308
|
if (typeof provider.transformCommand === 'function') {
|
|
302
309
|
content = provider.transformCommand(path.join(skillDir, `${skillName}.md`), content, opts);
|
|
303
310
|
}
|
|
304
|
-
|
|
305
|
-
|
|
311
|
+
// #2507: mirrored wrappers used to be written with no ownership signal, so
|
|
312
|
+
// AIWG could neither count them as deployed nor prune them when the source
|
|
313
|
+
// skill went away — a later run reported its own 46 wrappers as unmanaged
|
|
314
|
+
// artifacts the operator should delete. They carry the same managed marker
|
|
315
|
+
// and sidecar entry as any other deployed command now.
|
|
316
|
+
content = addManagedMarker(content, opts.deployVersion || 'unknown', opts.deploySource || 'bundled');
|
|
317
|
+
|
|
318
|
+
const filename = `${skillName}${ext}`;
|
|
319
|
+
const dest = path.join(targetDir, filename);
|
|
306
320
|
if (opts.dryRun) {
|
|
307
321
|
if (opts.verbose) console.log(`[dry-run] mirror skill command ${skillName} -> ${dest}`);
|
|
308
322
|
const raw = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
|
|
@@ -315,12 +329,22 @@ function mirrorSkillsAsCommands(provider, target, srcRoot, opts) {
|
|
|
315
329
|
} else {
|
|
316
330
|
fs.writeFileSync(dest, content, 'utf8');
|
|
317
331
|
}
|
|
332
|
+
deployedEntries.push({ filename, hash: contentHash(content), kind: 'skill-command' });
|
|
318
333
|
count++;
|
|
319
334
|
}
|
|
320
335
|
|
|
336
|
+
if (deployedEntries.length > 0) {
|
|
337
|
+
updateSidecarManifest(targetDir, deployedEntries, {
|
|
338
|
+
dryRun: opts.dryRun,
|
|
339
|
+
version: opts.deployVersion || 'unknown',
|
|
340
|
+
source: opts.deploySource || 'bundled',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
321
344
|
return count;
|
|
322
345
|
}
|
|
323
346
|
|
|
347
|
+
|
|
324
348
|
// ============================================================================
|
|
325
349
|
// Stale-Artifact Prune (agents / commands / rules) — #1627
|
|
326
350
|
// ============================================================================
|
|
@@ -346,6 +370,29 @@ function mirrorSkillsAsCommands(provider, target, srcRoot, opts) {
|
|
|
346
370
|
* @param {object} opts deploy opts (dryRun/verbose/quiet + deploy flags)
|
|
347
371
|
* @param {string|null} explicitSource the raw `--source` value (null when unset)
|
|
348
372
|
*/
|
|
373
|
+
/** Bundles whose deploy is itself the kernel-only bulk install. */
|
|
374
|
+
const BULK_INSTALL_BUNDLES = new Set(['all']);
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* True when the kernel-only bulk install is the only thing this project has
|
|
378
|
+
* deployed, so clearing leftover flat artifacts is a migration and not a
|
|
379
|
+
* deletion of another bundle's surface (#2508).
|
|
380
|
+
*
|
|
381
|
+
* A project with no readable `.aiwg/aiwg.config` has no recorded owner, so the
|
|
382
|
+
* pre-#152 migration cleanup still applies.
|
|
383
|
+
*/
|
|
384
|
+
export function bulkInstallOwnsFlatArtifacts(target) {
|
|
385
|
+
let installed;
|
|
386
|
+
try {
|
|
387
|
+
const raw = realFs.readFileSync(path.join(target, '.aiwg', 'aiwg.config'), 'utf8');
|
|
388
|
+
installed = JSON.parse(raw)?.installed;
|
|
389
|
+
} catch {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
if (!installed || typeof installed !== 'object') return true;
|
|
393
|
+
return !Object.keys(installed).some(name => !BULK_INSTALL_BUNDLES.has(name));
|
|
394
|
+
}
|
|
395
|
+
|
|
349
396
|
function pruneStaleAiwgArtifacts(provider, target, srcRoot, opts, explicitSource) {
|
|
350
397
|
if (opts.skillsOnly && !opts.kernelOnly) return; // skills run their own prune in the provider
|
|
351
398
|
|
|
@@ -363,6 +410,23 @@ function pruneStaleAiwgArtifacts(provider, target, srcRoot, opts, explicitSource
|
|
|
363
410
|
}
|
|
364
411
|
|
|
365
412
|
if (opts.kernelOnly) {
|
|
413
|
+
// A kernel-only run deploys skills and nothing else (deployCommands /
|
|
414
|
+
// deployRules / deployBehaviors are all forced false above), so it has no
|
|
415
|
+
// basis for judging any flat artifact stale. The empty desired set below
|
|
416
|
+
// exists for one narrow migration: clearing agents/commands/rules left by
|
|
417
|
+
// the pre-#152 bulk default, when the bulk install is the only thing this
|
|
418
|
+
// project ever deployed.
|
|
419
|
+
//
|
|
420
|
+
// #2508: applying it unconditionally deleted every artifact a sibling
|
|
421
|
+
// bundle owned — `aiwg use sdlc` followed by `aiwg use all` took
|
|
422
|
+
// .claude/agents from 139 to 0. When another bundle is installed, it owns
|
|
423
|
+
// this surface deliberately and the migration assumption does not hold.
|
|
424
|
+
if (!bulkInstallOwnsFlatArtifacts(target)) {
|
|
425
|
+
if (opts.verbose) {
|
|
426
|
+
console.log('skip kernel-only flat prune: another installed bundle owns agents/commands/rules');
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
366
430
|
for (const type of ['agents', 'commands', 'rules']) {
|
|
367
431
|
const relPath = provider.paths?.[type];
|
|
368
432
|
if (!relPath || relPath.endsWith('.md')) continue;
|
|
@@ -393,6 +457,10 @@ function pruneStaleAiwgArtifacts(provider, target, srcRoot, opts, explicitSource
|
|
|
393
457
|
const removed = pruneStaleAiwgFiles(destDir, desired, {
|
|
394
458
|
dryRun: opts.dryRun,
|
|
395
459
|
verbose: opts.verbose,
|
|
460
|
+
// Retire wrappers whose source skill no longer ships (#2511). Only the
|
|
461
|
+
// command directory holds them; the kernel-only branch returns earlier,
|
|
462
|
+
// so a bulk install still cannot touch wrappers it did not write.
|
|
463
|
+
skillCommandStems: type === 'commands' ? computeAllSkillNames(srcRoot) : null,
|
|
396
464
|
});
|
|
397
465
|
if (removed.length > 0 && !opts.quiet) {
|
|
398
466
|
console.log(` Pruned: ${removed.length} stale AIWG ${type} file${removed.length === 1 ? '' : 's'}`);
|
|
@@ -436,7 +504,13 @@ function parseArgs() {
|
|
|
436
504
|
quiet: false, // Suppress all non-error output (for embedding in use.ts)
|
|
437
505
|
asPlugin: false, // Generate .factory-plugin/ bundle (Factory provider only)
|
|
438
506
|
deployBehaviors: false, // Deploy behaviors in addition to agents
|
|
439
|
-
skipCommandsMigration: false
|
|
507
|
+
skipCommandsMigration: false, // Skip commands → skills migration (warns about duplicates)
|
|
508
|
+
// Managed-marker provenance (#2502). Deployers that are not shipping the
|
|
509
|
+
// bundled framework corpus (project-local bundles, in particular) must
|
|
510
|
+
// override these so `aiwg refresh` does not mistake their artifacts for
|
|
511
|
+
// stale copies of packaged ones.
|
|
512
|
+
deploySource: null, // Managed-marker source; defaults to 'bundled'
|
|
513
|
+
deployVersion: null // Managed-marker version; defaults to srcRoot package.json
|
|
440
514
|
};
|
|
441
515
|
for (let i = 0; i < args.length; i++) {
|
|
442
516
|
const a = args[i];
|
|
@@ -471,6 +545,8 @@ function parseArgs() {
|
|
|
471
545
|
else if (a === '--as-plugin') cfg.asPlugin = true;
|
|
472
546
|
else if (a === '--skip-commands-migration') cfg.skipCommandsMigration = true;
|
|
473
547
|
else if (a === '--copy-all' || a === '--copy-standard-skills') cfg.copyStandardSkills = true;
|
|
548
|
+
else if (a === '--deploy-source' && args[i + 1]) cfg.deploySource = String(args[++i]);
|
|
549
|
+
else if (a === '--deploy-version' && args[i + 1]) cfg.deployVersion = String(args[++i]);
|
|
474
550
|
else if (a === '--help' || a === '-h') {
|
|
475
551
|
printHelp();
|
|
476
552
|
process.exit(0);
|
|
@@ -513,6 +589,12 @@ Options:
|
|
|
513
589
|
--as-agents-md Aggregate to single AGENTS.md (Codex)
|
|
514
590
|
--create-agents-md Create/update AGENTS.md template
|
|
515
591
|
--skip-commands-migration Skip deleting the commands directory before skills deployment
|
|
592
|
+
--deploy-source <name> Managed-marker source stamped into deployed artifacts.
|
|
593
|
+
Defaults to 'bundled'. Deploys that do not ship the packaged
|
|
594
|
+
framework corpus (e.g. project-local bundles) MUST override
|
|
595
|
+
this so refresh's stale-artifact prune skips them (#2502).
|
|
596
|
+
--deploy-version <version> Managed-marker version stamped into deployed artifacts.
|
|
597
|
+
Defaults to the --source tree's package.json version.
|
|
516
598
|
--copy-all Copy ALL skills per-project (legacy mirror at <provider>/.aiwg/skills/).
|
|
517
599
|
For aiwg use all, this also restores the legacy full agent,
|
|
518
600
|
command, and expanded-rule copy. Default bulk deployment is
|
|
@@ -933,8 +1015,8 @@ export async function main() {
|
|
|
933
1015
|
// Replaces the legacy AIWG_COPY_STANDARD_SKILLS env var (removed rc.30).
|
|
934
1016
|
// Default (#1217) is no-copy + index-driven discovery.
|
|
935
1017
|
copyStandardSkills: cfg.copyStandardSkills === true,
|
|
936
|
-
deployVersion: getDeployVersion(srcRoot),
|
|
937
|
-
deploySource: 'bundled',
|
|
1018
|
+
deployVersion: cfg.deployVersion || getDeployVersion(srcRoot),
|
|
1019
|
+
deploySource: cfg.deploySource || 'bundled',
|
|
938
1020
|
};
|
|
939
1021
|
|
|
940
1022
|
// Commands → Skills migration: prompt then delete the commands directory
|