@aiwg/cli 2026.9.7 → 2026.9.10
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/bin/aiwg.mjs +55 -10
- package/dist/src/artifacts/cli.js +20 -8
- package/dist/src/artifacts/corpus-views/taxonomies.js +1 -1
- 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 +6 -5
- package/dist/src/cli/handlers/init.js +7 -2
- package/dist/src/cli/handlers/installation.js +102 -2
- package/dist/src/cli/handlers/mc.js +87 -17
- package/dist/src/cli/handlers/ralph.js +8 -8
- 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 +39 -2
- package/dist/src/cli/handlers/use.js +13 -4
- package/dist/src/cli/handlers/version.js +49 -14
- package/dist/src/cli/handlers/workspace-context.js +8 -0
- package/dist/src/cli/services/deployment-verification.js +71 -10
- package/dist/src/cli/watch-service.js +43 -3
- 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/dataset/fortemi-capability.d.ts +70 -0
- package/dist/src/dataset/fortemi-capability.js +211 -0
- package/dist/src/dataset/fortemi-dataset-execution.d.ts +9 -0
- package/dist/src/dataset/fortemi-dataset-execution.js +49 -8
- package/dist/src/dataset/fortemi-run-receipt.d.ts +1 -0
- package/dist/src/dataset/fortemi-run-receipt.js +2 -19
- package/dist/src/dataset/fortemi-schema-path.d.ts +7 -0
- package/dist/src/dataset/fortemi-schema-path.js +24 -0
- package/dist/src/dataset/index.d.ts +1 -0
- package/dist/src/dataset/index.js +1 -0
- package/dist/src/extensions/claude-hooks-installer.js +22 -6
- package/dist/src/extensions/project-local-doctor.js +15 -1
- package/dist/src/lint/cli.js +5 -1
- package/dist/src/lint/reporters.js +17 -0
- package/dist/src/lint/runner.js +297 -4
- package/dist/src/policy/authorization.js +1 -1
- package/dist/src/smiths/context-pipeline/workspace-context.js +138 -49
- package/package.json +1 -1
- package/schemas/dataset/fortemi-capability-validation/1.0.1/authority.json +61 -0
- package/schemas/dataset/fortemi-capability-validation/1.0.1/capability.schema.json +469 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +2 -1
- package/tools/agents/deploy-agents.mjs +8 -0
- package/tools/agents/providers/base.mjs +261 -16
- package/tools/agents/providers/claude.mjs +61 -4
- package/tools/agents/providers/codex.mjs +9 -0
- package/tools/skills/deploy-skills-codex.mjs +121 -7
|
@@ -3,44 +3,56 @@ import path from 'node:path';
|
|
|
3
3
|
import { loadGraphIndexFile } from '../../artifacts/index-reader.js';
|
|
4
4
|
import { readAiwgConfig } from '../../config/aiwg-config.js';
|
|
5
5
|
import { readUserRegistry } from '../../config/user-registry.js';
|
|
6
|
+
import { inspectInstallation } from '../../installation/manager.mjs';
|
|
7
|
+
import { getPackageRoot } from '../../channel/manager.mjs';
|
|
6
8
|
import { getProviderDefinition, normalizeProviderDefinitionId, resolveProviderPathValue, } from '../../providers/provider-definitions.js';
|
|
7
9
|
import { diagnoseIntegratedProviderTransformationReceipt } from '../../providers/transformation-receipt-integration.js';
|
|
8
10
|
import { diagnoseWorkspaceContext, providerContextContract, } from '../../smiths/context-pipeline/workspace-context.js';
|
|
9
11
|
import { USER_SCOPE_PATHS } from '../scope-resolver.js';
|
|
10
12
|
const RESTART_NOTICES = {
|
|
11
13
|
claude: {
|
|
14
|
+
policy: 'restart-required',
|
|
12
15
|
action: 'Restart Claude Code so the running session reloads deployed agents and skills.',
|
|
13
16
|
reason: 'Claude Code reads its agent and skill registries when a session starts.',
|
|
14
17
|
},
|
|
15
18
|
codex: {
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
policy: 'live-refresh',
|
|
20
|
+
action: 'Reopen Codex in this workspace if a deployed skill or agent does not appear.',
|
|
21
|
+
reason: 'Codex refreshes project skills between turns — a running session exposed newly deployed skills on the next turn without a restart (#2309).',
|
|
22
|
+
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
23
|
},
|
|
19
24
|
copilot: {
|
|
25
|
+
policy: 'restart-required',
|
|
20
26
|
action: 'Reload the VS Code window so Copilot reloads workspace agents and instructions.',
|
|
21
27
|
reason: 'Copilot caches workspace agent definitions until the VS Code window reloads.',
|
|
22
28
|
},
|
|
23
29
|
cursor: {
|
|
30
|
+
policy: 'restart-required',
|
|
24
31
|
action: 'Reload the Cursor workspace so it reloads agents and rules.',
|
|
25
32
|
reason: 'Cursor reads workspace agents and rules when the workspace opens.',
|
|
26
33
|
},
|
|
27
34
|
factory: {
|
|
35
|
+
policy: 'restart-required',
|
|
28
36
|
action: 'Restart the Factory droid runtime so it reloads deployed droids.',
|
|
29
37
|
reason: 'Factory loads its droid registry when the runtime starts.',
|
|
30
38
|
},
|
|
31
39
|
opencode: {
|
|
40
|
+
policy: 'restart-required',
|
|
32
41
|
action: 'Restart the OpenCode session so it reloads deployed agents.',
|
|
33
42
|
reason: 'OpenCode scans its agent directory when the session starts.',
|
|
34
43
|
},
|
|
35
44
|
openclaw: {
|
|
45
|
+
policy: 'restart-required',
|
|
36
46
|
action: 'Restart OpenClaw so it reloads its home-directory registry.',
|
|
37
47
|
reason: 'OpenClaw loads its home-directory registry when the process starts.',
|
|
38
48
|
},
|
|
39
49
|
warp: {
|
|
50
|
+
policy: 'restart-required',
|
|
40
51
|
action: 'Open a fresh Warp tab so it reloads project context.',
|
|
41
52
|
reason: 'Warp reads project context when a tab starts.',
|
|
42
53
|
},
|
|
43
54
|
windsurf: {
|
|
55
|
+
policy: 'restart-required',
|
|
44
56
|
action: 'Reload Devin Desktop so it reparses project context.',
|
|
45
57
|
reason: 'Devin Desktop reads the Windsurf-compatible project context when the workspace opens.',
|
|
46
58
|
},
|
|
@@ -327,9 +339,16 @@ export async function verifyProviderDeployment(options) {
|
|
|
327
339
|
const findings = [];
|
|
328
340
|
const counts = emptyCounts();
|
|
329
341
|
const restartNotice = RESTART_NOTICES[normalized] ?? null;
|
|
330
|
-
|
|
342
|
+
// A provider with no notice gets no restart claim, which is what it got
|
|
343
|
+
// before this field existed. The label is the absence of a known restart
|
|
344
|
+
// requirement, not a positive claim that the client refreshes live.
|
|
345
|
+
const reloadPolicy = restartNotice?.policy ?? 'live-refresh';
|
|
346
|
+
const restartRequired = reloadPolicy === 'restart-required';
|
|
347
|
+
// Only a `restart-required` provider gets an imperative restart step. A
|
|
348
|
+
// `live-refresh` provider keeps its rationale and a conditional fallback (#2309).
|
|
349
|
+
const restartAction = restartRequired ? (restartNotice?.action ?? null) : null;
|
|
331
350
|
const restartReason = restartNotice?.reason ?? null;
|
|
332
|
-
const
|
|
351
|
+
const reloadFallback = restartRequired ? null : (restartNotice?.fallback ?? null);
|
|
333
352
|
if (!definition) {
|
|
334
353
|
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
354
|
}
|
|
@@ -362,12 +381,17 @@ export async function verifyProviderDeployment(options) {
|
|
|
362
381
|
counts[flatKind] = tally.deployed;
|
|
363
382
|
if (tally.unmanaged.length === 0)
|
|
364
383
|
continue;
|
|
365
|
-
|
|
366
|
-
|
|
384
|
+
// Name enough of the set that the operator can judge it, and make the
|
|
385
|
+
// overwrite previewable: `--force` replaces every listed file, so it is
|
|
386
|
+
// never the answer to an unrelated budget or listing warning (#2561).
|
|
387
|
+
const shown = tally.unmanaged.slice(0, 10).join(', ');
|
|
388
|
+
const remainder = tally.unmanaged.length - 10;
|
|
389
|
+
const bundle = options.requestedBundles[0] ?? 'all';
|
|
367
390
|
findings.push(finding(normalized, `unmanaged-artifacts:${flatKind}`, 'advisory', `${tally.unmanaged.length} unmanaged ${FLAT_ARTIFACT_NOUNS[flatKind]} file(s) left in place at ${artifactPaths[flatKind]}: `
|
|
368
391
|
+ `${shown}${remainder > 0 ? `, and ${remainder} more` : ''}. `
|
|
369
|
-
+ 'They are not managed by AIWG and were not counted as deployed.', `
|
|
370
|
-
+ `
|
|
392
|
+
+ 'They are not managed by AIWG and were not counted as deployed.', `Preview the overwrite set with aiwg use ${bundle} --provider ${normalized} --force --dry-run; `
|
|
393
|
+
+ `then re-run aiwg use ${bundle} --provider ${normalized} --force to replace exactly those ${tally.unmanaged.length} file(s), `
|
|
394
|
+
+ `or delete ${artifactPaths[flatKind]} so AIWG can reclaim the directory. Leave them in place if they are yours.`, { kind: flatKind, unmanaged: tally.unmanaged }));
|
|
371
395
|
}
|
|
372
396
|
const resolvedSkillsPath = resolveProviderPathValue(artifactPaths.skills, deploymentRoot);
|
|
373
397
|
const kernelPath = options.scope === 'user'
|
|
@@ -492,6 +516,8 @@ export async function verifyProviderDeployment(options) {
|
|
|
492
516
|
restartRequired,
|
|
493
517
|
restartAction,
|
|
494
518
|
restartReason,
|
|
519
|
+
reloadPolicy,
|
|
520
|
+
reloadFallback,
|
|
495
521
|
counts,
|
|
496
522
|
phases,
|
|
497
523
|
findings,
|
|
@@ -501,7 +527,9 @@ export function buildDryRunUseResult(options) {
|
|
|
501
527
|
const providers = options.providers.map((provider) => {
|
|
502
528
|
const normalized = normalizeProviderDefinitionId(provider) ?? provider;
|
|
503
529
|
const restartNotice = RESTART_NOTICES[normalized] ?? null;
|
|
504
|
-
const
|
|
530
|
+
const reloadPolicy = restartNotice?.policy ?? 'live-refresh';
|
|
531
|
+
const restartRequired = reloadPolicy === 'restart-required';
|
|
532
|
+
const restartAction = restartRequired ? (restartNotice?.action ?? null) : null;
|
|
505
533
|
const phases = [
|
|
506
534
|
phase('resolve', 'planned', true, `Would resolve ${options.projectRoot}, ${normalized}, ${options.scope} scope.`),
|
|
507
535
|
phase('deploy', 'planned', true, 'Would deploy the requested managed artifact surface.'),
|
|
@@ -514,9 +542,11 @@ export function buildDryRunUseResult(options) {
|
|
|
514
542
|
provider: normalized,
|
|
515
543
|
scope: options.scope,
|
|
516
544
|
outcome: 'planned',
|
|
517
|
-
restartRequired
|
|
545
|
+
restartRequired,
|
|
518
546
|
restartAction,
|
|
519
547
|
restartReason: restartNotice?.reason ?? null,
|
|
548
|
+
reloadPolicy,
|
|
549
|
+
reloadFallback: restartRequired ? null : (restartNotice?.fallback ?? null),
|
|
520
550
|
counts: emptyCounts(),
|
|
521
551
|
phases,
|
|
522
552
|
findings: [],
|
|
@@ -613,6 +643,8 @@ export async function verifyConfiguredDeployments(projectRoot, filters = {}, fra
|
|
|
613
643
|
restartRequired: false,
|
|
614
644
|
restartAction: null,
|
|
615
645
|
restartReason: null,
|
|
646
|
+
reloadPolicy: 'live-refresh',
|
|
647
|
+
reloadFallback: null,
|
|
616
648
|
counts: emptyCounts(),
|
|
617
649
|
phases: [phase('verify', 'failed', true, 'No installed provider deployment could be resolved.')],
|
|
618
650
|
findings: [finding(fallback, 'deployment-not-configured', 'blocking', 'No installed provider deployment could be resolved.', 'Run aiwg use all --provider <provider>.')],
|
|
@@ -620,6 +652,26 @@ export async function verifyConfiguredDeployments(projectRoot, filters = {}, fra
|
|
|
620
652
|
}
|
|
621
653
|
return aggregateUseDeploymentResult({ projectRoot, frameworkRoot, scope: filters.scope ?? 'project', requestedBundles: bundles, providers: results });
|
|
622
654
|
}
|
|
655
|
+
/**
|
|
656
|
+
* Installation identity as the probe sees it. Read-only commands keep running
|
|
657
|
+
* under recorded/actual drift (#2559); the probe names that drift so an agent
|
|
658
|
+
* reading only the JSON does not treat a blocked installation as healthy.
|
|
659
|
+
*/
|
|
660
|
+
function probeInstallationIdentity() {
|
|
661
|
+
try {
|
|
662
|
+
const status = inspectInstallation({ actualRoot: getPackageRoot(), createIfMissing: false });
|
|
663
|
+
return {
|
|
664
|
+
state: status.state,
|
|
665
|
+
canonical: status.identity ? { method: status.identity.method, root: status.identity.root } : null,
|
|
666
|
+
actual: { method: status.actualMethod, root: status.actualRoot },
|
|
667
|
+
drift: status.drift,
|
|
668
|
+
mutations_blocked: status.state !== 'aligned' && status.state !== 'unrecorded',
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
return { state: 'unknown', drift: [error instanceof Error ? error.message : String(error)], mutations_blocked: true };
|
|
673
|
+
}
|
|
674
|
+
}
|
|
623
675
|
export async function buildDeploymentStatusProbe(projectRoot, frameworkRoot = process.env.AIWG_ROOT || projectRoot, filters = {}) {
|
|
624
676
|
const result = await verifyConfiguredDeployments(projectRoot, filters, frameworkRoot);
|
|
625
677
|
const notConfigured = result.requestedBundles.length === 0
|
|
@@ -631,6 +683,7 @@ export async function buildDeploymentStatusProbe(projectRoot, frameworkRoot = pr
|
|
|
631
683
|
generated_at: result.generatedAt,
|
|
632
684
|
project_root: result.projectRoot,
|
|
633
685
|
engaged,
|
|
686
|
+
installation: probeInstallationIdentity(),
|
|
634
687
|
status: notConfigured ? 'not-configured' : result.outcome === 'failed' ? 'needs-repair' : result.outcome,
|
|
635
688
|
checks: {
|
|
636
689
|
workspace_exists: await exists(path.join(projectRoot, '.aiwg')),
|
|
@@ -756,6 +809,14 @@ export function renderUseDeploymentResult(result, options = {}) {
|
|
|
756
809
|
lines.push(...wrapParagraph(`Framework index built: ${result.discovery.builtAt}`, width));
|
|
757
810
|
}
|
|
758
811
|
}
|
|
812
|
+
const reloadFallbacks = result.providers
|
|
813
|
+
.filter((provider) => !provider.restartRequired && provider.reloadFallback)
|
|
814
|
+
.map((provider) => provider.reloadFallback);
|
|
815
|
+
if (reloadFallbacks.length > 0) {
|
|
816
|
+
lines.push('', 'If something is missing');
|
|
817
|
+
for (const note of reloadFallbacks)
|
|
818
|
+
lines.push(...wrapParagraph(note, width));
|
|
819
|
+
}
|
|
759
820
|
const restartActions = result.providers
|
|
760
821
|
.filter((provider) => provider.restartRequired && provider.restartAction)
|
|
761
822
|
.map((provider) => provider.restartAction);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import chokidar from 'chokidar';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
+
const MAX_RECORDED_ERRORS = 20;
|
|
9
10
|
/**
|
|
10
11
|
* File watching service with debouncing
|
|
11
12
|
*/
|
|
@@ -16,6 +17,9 @@ export class WatchService {
|
|
|
16
17
|
debounceMs = 500;
|
|
17
18
|
stats;
|
|
18
19
|
isRunning = false;
|
|
20
|
+
readyAt;
|
|
21
|
+
missingTargets = [];
|
|
22
|
+
recordedErrors = [];
|
|
19
23
|
constructor() {
|
|
20
24
|
this.stats = {
|
|
21
25
|
filesWatched: 0,
|
|
@@ -48,13 +52,20 @@ export class WatchService {
|
|
|
48
52
|
this.watcher.on('unlink', (path) => this.handleEvent('unlink', path));
|
|
49
53
|
this.watcher.on('error', (error) => {
|
|
50
54
|
this.stats.errors++;
|
|
55
|
+
this.recordError(error);
|
|
51
56
|
console.error('Watch error:', error);
|
|
52
57
|
});
|
|
53
58
|
this.isRunning = true;
|
|
59
|
+
this.readyAt = undefined;
|
|
60
|
+
this.missingTargets = [];
|
|
61
|
+
this.recordedErrors = [];
|
|
54
62
|
// Wait for ready
|
|
55
63
|
await new Promise((resolve) => {
|
|
56
64
|
if (this.watcher) {
|
|
57
|
-
this.watcher.on('ready', () =>
|
|
65
|
+
this.watcher.on('ready', () => {
|
|
66
|
+
this.readyAt = new Date();
|
|
67
|
+
resolve();
|
|
68
|
+
});
|
|
58
69
|
}
|
|
59
70
|
else {
|
|
60
71
|
resolve();
|
|
@@ -67,6 +78,7 @@ export class WatchService {
|
|
|
67
78
|
// Resolving `start()` only once every target appears in `getWatched()`
|
|
68
79
|
// makes readiness mean armed.
|
|
69
80
|
await this.waitUntilArmed(patterns);
|
|
81
|
+
this.missingTargets = this.unarmedTargets(patterns);
|
|
70
82
|
const watched = this.watcher?.getWatched() ?? {};
|
|
71
83
|
this.stats.filesWatched = Object.values(watched).reduce((sum, files) => sum + files.length, 0);
|
|
72
84
|
}
|
|
@@ -90,8 +102,12 @@ export class WatchService {
|
|
|
90
102
|
}
|
|
91
103
|
/** True when chokidar reports a watch covering every requested target. */
|
|
92
104
|
allTargetsArmed(patterns) {
|
|
105
|
+
return this.unarmedTargets(patterns).length === 0;
|
|
106
|
+
}
|
|
107
|
+
/** Requested targets that `getWatched()` does not yet cover. */
|
|
108
|
+
unarmedTargets(patterns) {
|
|
93
109
|
if (!this.watcher) {
|
|
94
|
-
return
|
|
110
|
+
return [];
|
|
95
111
|
}
|
|
96
112
|
const watched = this.watcher.getWatched();
|
|
97
113
|
// chokidar keys `getWatched()` with the spelling it was given, so resolve
|
|
@@ -106,7 +122,16 @@ export class WatchService {
|
|
|
106
122
|
}
|
|
107
123
|
// A directory target is keyed directly; a file target is listed under its
|
|
108
124
|
// parent. Either spelling resolves into the same set.
|
|
109
|
-
return patterns
|
|
125
|
+
return patterns
|
|
126
|
+
.map((pattern) => path.resolve(pattern))
|
|
127
|
+
.filter((target) => !armed.has(target));
|
|
128
|
+
}
|
|
129
|
+
recordError(error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
this.recordedErrors.push(message);
|
|
132
|
+
if (this.recordedErrors.length > MAX_RECORDED_ERRORS) {
|
|
133
|
+
this.recordedErrors.splice(0, this.recordedErrors.length - MAX_RECORDED_ERRORS);
|
|
134
|
+
}
|
|
110
135
|
}
|
|
111
136
|
/**
|
|
112
137
|
* Stop watching
|
|
@@ -157,6 +182,21 @@ export class WatchService {
|
|
|
157
182
|
getStats() {
|
|
158
183
|
return { ...this.stats };
|
|
159
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* What the watcher has observed about its own lifecycle. Intended for the
|
|
187
|
+
* failure path: a caller that waited for an event that never came reports
|
|
188
|
+
* this instead of a bare "no events" (#2553).
|
|
189
|
+
*/
|
|
190
|
+
getDiagnostics() {
|
|
191
|
+
return {
|
|
192
|
+
ready: this.readyAt !== undefined,
|
|
193
|
+
readyAt: this.readyAt,
|
|
194
|
+
armed: this.missingTargets.length === 0,
|
|
195
|
+
missingTargets: [...this.missingTargets],
|
|
196
|
+
errors: [...this.recordedErrors],
|
|
197
|
+
watched: this.watcher ? this.watcher.getWatched() : {},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
160
200
|
/**
|
|
161
201
|
* Check if service is running
|
|
162
202
|
*/
|
|
@@ -35,6 +35,92 @@ export const WORKSPACE_REPO_ACTIONS = [
|
|
|
35
35
|
'service-action',
|
|
36
36
|
'destructive',
|
|
37
37
|
];
|
|
38
|
+
export const PROJECT_CLASSIFICATIONS = ['private', 'sanitized', 'public'];
|
|
39
|
+
/** Normalize the string-or-object `project` field to the object form. */
|
|
40
|
+
/**
|
|
41
|
+
* Validate the `project` block. Accepts the bare-string form unconditionally so
|
|
42
|
+
* existing configs keep loading (#2535).
|
|
43
|
+
*/
|
|
44
|
+
export function validateProjectConfig(project) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
if (project === undefined || typeof project === 'string')
|
|
47
|
+
return errors;
|
|
48
|
+
if (typeof project !== 'object' || project === null || Array.isArray(project)) {
|
|
49
|
+
errors.push('project: must be a string (name) or an object');
|
|
50
|
+
return errors;
|
|
51
|
+
}
|
|
52
|
+
const value = project;
|
|
53
|
+
if (value.classification !== undefined
|
|
54
|
+
&& !PROJECT_CLASSIFICATIONS.includes(value.classification)) {
|
|
55
|
+
errors.push(`project.classification: must be one of ${PROJECT_CLASSIFICATIONS.join(' | ')}`);
|
|
56
|
+
}
|
|
57
|
+
for (const key of ['name', 'description']) {
|
|
58
|
+
if (value[key] !== undefined && typeof value[key] !== 'string') {
|
|
59
|
+
errors.push(`project.${key}: must be a string`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (value.pii !== undefined && typeof value.pii !== 'boolean') {
|
|
63
|
+
errors.push('project.pii: must be a boolean');
|
|
64
|
+
}
|
|
65
|
+
if (value.handling !== undefined) {
|
|
66
|
+
if (typeof value.handling !== 'object' || value.handling === null || Array.isArray(value.handling)) {
|
|
67
|
+
errors.push('project.handling: must be an object');
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const handling = value.handling;
|
|
71
|
+
for (const key of ['excerptable', 'publishable', 'mirror']) {
|
|
72
|
+
if (handling[key] !== undefined && typeof handling[key] !== 'boolean') {
|
|
73
|
+
errors.push(`project.handling.${key}: must be a boolean`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return errors;
|
|
79
|
+
}
|
|
80
|
+
export function resolveProject(project) {
|
|
81
|
+
if (project === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
if (typeof project === 'string')
|
|
84
|
+
return { name: project };
|
|
85
|
+
return project;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Handling defaults derived from the classification when not stated explicitly.
|
|
89
|
+
* A `private` repo is closed by default; anything else stays permissive, so
|
|
90
|
+
* declaring a classification never silently tightens an existing project.
|
|
91
|
+
*/
|
|
92
|
+
export function resolveProjectHandling(project) {
|
|
93
|
+
const closed = project?.classification === 'private';
|
|
94
|
+
return {
|
|
95
|
+
excerptable: project?.handling?.excerptable ?? !closed,
|
|
96
|
+
publishable: project?.handling?.publishable ?? !closed,
|
|
97
|
+
mirror: project?.handling?.mirror ?? !closed,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Deprecated spellings mapped to their current value. */
|
|
101
|
+
export const FORCE_PUSH_POLICY_ALIASES = {
|
|
102
|
+
'main-only-blocked': 'own-branch-only',
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* The rename also narrowed the permission: `main-only-blocked` allowed force-push on
|
|
106
|
+
* *any* feature branch, `own-branch-only` restricts it to the agent's own. Callers
|
|
107
|
+
* surface this rather than migrating silently, because accepting the alias quietly
|
|
108
|
+
* would change what an agent is permitted to do.
|
|
109
|
+
*/
|
|
110
|
+
export const FORCE_PUSH_POLICY_ALIAS_NOTE = "'main-only-blocked' is a deprecated alias for 'own-branch-only'. The permission also narrowed: "
|
|
111
|
+
+ 'the old value allowed force-push on any feature branch, the new one only on the agent\'s own branch.';
|
|
112
|
+
/**
|
|
113
|
+
* Normalize a force-push policy, mapping deprecated spellings forward. Returns the
|
|
114
|
+
* canonical value and the alias it came from, if any.
|
|
115
|
+
*/
|
|
116
|
+
export function normalizeForcePushPolicy(value) {
|
|
117
|
+
if (value === undefined)
|
|
118
|
+
return { policy: undefined };
|
|
119
|
+
const alias = FORCE_PUSH_POLICY_ALIASES[value];
|
|
120
|
+
if (alias)
|
|
121
|
+
return { policy: alias, deprecatedFrom: value };
|
|
122
|
+
return { policy: value };
|
|
123
|
+
}
|
|
38
124
|
const DEFAULT_BRANCH_NAMING = {
|
|
39
125
|
prefix_by_type: {
|
|
40
126
|
feat: 'feat/{issue}-{slug}',
|
|
@@ -70,7 +156,7 @@ export function resolveDelivery(delivery) {
|
|
|
70
156
|
committer: delivery?.committer,
|
|
71
157
|
signing: delivery?.signing,
|
|
72
158
|
release_signing: delivery?.release_signing,
|
|
73
|
-
force_push_policy: delivery?.force_push_policy ?? 'never',
|
|
159
|
+
force_push_policy: normalizeForcePushPolicy(delivery?.force_push_policy).policy ?? 'never',
|
|
74
160
|
auto_close_issues: delivery?.auto_close_issues ?? true,
|
|
75
161
|
issue_comment_on_cycle: delivery?.issue_comment_on_cycle ?? true,
|
|
76
162
|
};
|
|
@@ -770,7 +856,10 @@ export async function readAiwgConfig(projectDir) {
|
|
|
770
856
|
if (authorizationErrors.length > 0) {
|
|
771
857
|
throw new Error(`Invalid .aiwg/aiwg.config:\n${authorizationErrors.map(item => item.message).join('\n')}`);
|
|
772
858
|
}
|
|
773
|
-
const threatAssessmentErrors =
|
|
859
|
+
const threatAssessmentErrors = [
|
|
860
|
+
...validateThreatAssessmentConfig(parsed.security?.threatAssessment),
|
|
861
|
+
...validateProjectConfig(parsed.project),
|
|
862
|
+
];
|
|
774
863
|
if (threatAssessmentErrors.length > 0) {
|
|
775
864
|
throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
|
|
776
865
|
}
|
|
@@ -788,7 +877,10 @@ export async function readAiwgConfig(projectDir) {
|
|
|
788
877
|
* sync so split-root health remains deterministic.
|
|
789
878
|
*/
|
|
790
879
|
export async function writeAiwgConfig(projectDir, config) {
|
|
791
|
-
const threatAssessmentErrors =
|
|
880
|
+
const threatAssessmentErrors = [
|
|
881
|
+
...validateThreatAssessmentConfig(config.security?.threatAssessment),
|
|
882
|
+
...validateProjectConfig(config.project),
|
|
883
|
+
];
|
|
792
884
|
if (threatAssessmentErrors.length > 0) {
|
|
793
885
|
throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
|
|
794
886
|
}
|
package/dist/src/config/cli.js
CHANGED
|
@@ -142,6 +142,14 @@ async function handleSet(config, args) {
|
|
|
142
142
|
// Set validates enum membership for known fields (delivery.mode,
|
|
143
143
|
// delivery.merge_style, delivery.force_push_policy, remotes.issue_provider)
|
|
144
144
|
// before writing.
|
|
145
|
+
/** Deprecated values accepted on `set` and normalized to their current spelling. */
|
|
146
|
+
const ENUM_ALIASES = {
|
|
147
|
+
'delivery.force_push_policy': { 'main-only-blocked': 'own-branch-only' },
|
|
148
|
+
};
|
|
149
|
+
/** Extra context printed alongside an alias normalization, where semantics also changed. */
|
|
150
|
+
const ENUM_ALIAS_NOTES = {
|
|
151
|
+
'delivery.force_push_policy': "The permission also narrowed: the old value allowed force-push on any feature branch, the new one only on the agent's own branch.",
|
|
152
|
+
};
|
|
145
153
|
const ENUM_RULES = {
|
|
146
154
|
'delivery.mode': ['direct', 'feature-branch', 'pr-required'],
|
|
147
155
|
'delivery.merge_style': ['rebase-merge', 'squash', 'merge', 'fast-forward-only'],
|
|
@@ -210,8 +218,15 @@ async function projectConfigGet(key, args) {
|
|
|
210
218
|
async function projectConfigSet(key, raw, args) {
|
|
211
219
|
const { readAiwgConfig, writeAiwgConfig, getProjectDir, emptyConfig, validateExternalLinks, } = await import('./aiwg-config.js');
|
|
212
220
|
const projectDir = getProjectDir(undefined, args);
|
|
213
|
-
// Validate enum fields before writing
|
|
221
|
+
// Validate enum fields before writing. Deprecated spellings normalize forward with a
|
|
222
|
+
// notice rather than failing, so a config written before a rename stays settable (#2532).
|
|
214
223
|
const allowed = ENUM_RULES[key];
|
|
224
|
+
const aliased = ENUM_ALIASES[key]?.[raw];
|
|
225
|
+
if (aliased) {
|
|
226
|
+
process.stderr.write(`Note: '${raw}' is a deprecated alias for '${aliased}'; writing '${aliased}'.\n`
|
|
227
|
+
+ (ENUM_ALIAS_NOTES[key] ? ` ${ENUM_ALIAS_NOTES[key]}\n` : ''));
|
|
228
|
+
raw = aliased;
|
|
229
|
+
}
|
|
215
230
|
if (allowed && !allowed.includes(raw)) {
|
|
216
231
|
throw new AiwgError({
|
|
217
232
|
code: 'ERR_INVALID_VALUE',
|
|
@@ -24,6 +24,9 @@ const GITIGNORE_PROBE_BASENAME = '.aiwg-ignore-probe';
|
|
|
24
24
|
*/
|
|
25
25
|
export const AIWG_RUNTIME_PATTERNS = [
|
|
26
26
|
'.aiwg/working/',
|
|
27
|
+
// Transactional preimages written before AIWG rewrites operator files.
|
|
28
|
+
// Recoverable local evidence, not project content (#2542).
|
|
29
|
+
'.aiwg/backups/',
|
|
27
30
|
// .aiwg/.index/ is the artifact graph index (JSON nodes/edges + checksum
|
|
28
31
|
// manifest). It is a pure build artifact, fully regenerable from corpus
|
|
29
32
|
// content via `aiwg index build --all`, so it is ignored rather than committed.
|
|
@@ -56,6 +59,8 @@ export const PROVIDER_CONVENTIONAL_PATTERNS = [
|
|
|
56
59
|
*/
|
|
57
60
|
export const CLAUDE_SESSION_PATTERNS = [
|
|
58
61
|
'.claude/settings.local.json',
|
|
62
|
+
// Timestamped backups AIWG writes before merging hook entries (#2542).
|
|
63
|
+
'.claude/settings.json.bak.*',
|
|
59
64
|
];
|
|
60
65
|
/** All recommended patterns combined */
|
|
61
66
|
export const ALL_RECOMMENDED_PATTERNS = [
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Independent AIWG implementation of the Fortemi dataset capability wire rules
|
|
3
|
+
* (Fortemi/fortemi-react#422 validation revision 1.0.1). AIWG pins the authority
|
|
4
|
+
* schema and vectors but never imports the producer's validator, so agreement is
|
|
5
|
+
* established by shared vectors rather than by shared code.
|
|
6
|
+
*/
|
|
7
|
+
export declare const FORTEMI_CAPABILITY_CONTRACT = "fortemi.dataset-execution-capabilities/v1";
|
|
8
|
+
export declare const FORTEMI_CAPABILITY_SCHEMA_MAJOR = 1;
|
|
9
|
+
export interface FortemiCapabilityDeclaration {
|
|
10
|
+
id: string;
|
|
11
|
+
version: string;
|
|
12
|
+
status: "supported" | "experimental" | "unsupported";
|
|
13
|
+
limits?: Record<string, number>;
|
|
14
|
+
evidence: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface FortemiCapabilityDescriptor {
|
|
17
|
+
contract: string;
|
|
18
|
+
schemaVersion: string;
|
|
19
|
+
runtime: {
|
|
20
|
+
id: string;
|
|
21
|
+
version: string;
|
|
22
|
+
plane: string;
|
|
23
|
+
dataClass: string;
|
|
24
|
+
maturity: string;
|
|
25
|
+
};
|
|
26
|
+
guarantees: Record<string, string>;
|
|
27
|
+
capabilities: FortemiCapabilityDeclaration[];
|
|
28
|
+
evidence: Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
kind: string;
|
|
31
|
+
uri: string;
|
|
32
|
+
digest?: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export interface FortemiCapabilityRequirement {
|
|
36
|
+
id: string;
|
|
37
|
+
minimumVersion?: string;
|
|
38
|
+
minimumLimits?: Record<string, number>;
|
|
39
|
+
fallback?: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface FortemiCapabilityRequest {
|
|
42
|
+
contract: string;
|
|
43
|
+
required: FortemiCapabilityRequirement[];
|
|
44
|
+
optional?: FortemiCapabilityRequirement[];
|
|
45
|
+
}
|
|
46
|
+
export interface FortemiCapabilityDegradation {
|
|
47
|
+
requested: string;
|
|
48
|
+
selected?: string;
|
|
49
|
+
reason: "unsupported" | "version-insufficient" | "limit-insufficient";
|
|
50
|
+
changedGuarantees: string[];
|
|
51
|
+
}
|
|
52
|
+
export interface FortemiCapabilityDecision {
|
|
53
|
+
accepted: boolean;
|
|
54
|
+
selected: string[];
|
|
55
|
+
degradations: FortemiCapabilityDegradation[];
|
|
56
|
+
diagnostics: string[];
|
|
57
|
+
}
|
|
58
|
+
/** SemVer 2.0.0 precedence; build metadata is ignored. `null` means unparseable. */
|
|
59
|
+
export declare function compareFortemiCapabilityVersions(left: unknown, right: unknown): number | null;
|
|
60
|
+
/** Structural and semantic validation of an advertised capability descriptor. */
|
|
61
|
+
export declare function validateFortemiCapabilityDescriptor(descriptor: unknown): string[];
|
|
62
|
+
/** Structural and semantic validation of a negotiation request AIWG is about to send. */
|
|
63
|
+
export declare function validateFortemiCapabilityRequest(request: unknown): string[];
|
|
64
|
+
/**
|
|
65
|
+
* Pure negotiation of a request against an advertised descriptor. Performs no
|
|
66
|
+
* I/O and mutates neither input, so a caller can compare the result with the
|
|
67
|
+
* decision a server reports for the same pair.
|
|
68
|
+
*/
|
|
69
|
+
export declare function negotiateFortemiCapabilities(descriptor: unknown, request: unknown): FortemiCapabilityDecision;
|
|
70
|
+
//# sourceMappingURL=fortemi-capability.d.ts.map
|