@aiwg/cli 2026.8.18 → 2026.8.20
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/THIRD_PARTY_NOTICES.md +12 -0
- package/dist/src/api/index.d.ts +2 -0
- package/dist/src/api/index.js +2 -0
- package/dist/src/artifacts/backend-runtime.js +26 -0
- package/dist/src/artifacts/backends/sqlite-backend.js +204 -28
- package/dist/src/artifacts/dep-graph.js +27 -5
- package/dist/src/artifacts/graph-backend.js +2 -2
- package/dist/src/artifacts/graph-query.js +21 -9
- package/dist/src/artifacts/index-builder.js +78 -12
- package/dist/src/artifacts/index-files.js +26 -5
- package/dist/src/artifacts/index-status.js +4 -1
- package/dist/src/artifacts/query-engine.js +67 -67
- package/dist/src/artifacts/stats.js +9 -2
- package/dist/src/artifacts/types.js +14 -2
- package/dist/src/cli/handlers/help.js +2 -0
- package/dist/src/cli/handlers/index.js +6 -2
- package/dist/src/cli/handlers/installation.js +1 -1
- package/dist/src/cli/handlers/mission.js +27 -0
- package/dist/src/cli/handlers/refresh.js +6 -4
- package/dist/src/cli/handlers/runtime-info.js +29 -0
- package/dist/src/cli/handlers/steward.js +12 -0
- package/dist/src/cli/handlers/uhp.js +88 -0
- package/dist/src/cli/handlers/use.js +19 -4
- package/dist/src/config/aiwg-config.js +10 -0
- package/dist/src/extensions/commands/definitions.js +38 -0
- package/dist/src/installation/manager-command.mjs +31 -0
- package/dist/src/installation/manager.mjs +21 -0
- package/dist/src/mission-protocol/codecs.js +265 -0
- package/dist/src/mission-protocol/index.js +3 -0
- package/dist/src/mission-protocol/types.js +2 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +8 -5
- package/dist/src/smiths/context-pipeline/line-endings.js +12 -0
- package/dist/src/smiths/context-pipeline/managed-hook.js +8 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +3 -1
- package/dist/src/storage/backend-contract.js +64 -0
- package/dist/src/storage/index.js +2 -0
- package/dist/src/storage/migration-protocol.js +378 -0
- package/dist/src/uhp/client.js +374 -0
- package/dist/src/uhp/config.js +130 -0
- package/dist/src/uhp/errors.js +63 -0
- package/dist/src/uhp/index.js +7 -0
- package/dist/src/uhp/mission.js +111 -0
- package/dist/src/uhp/sse.js +76 -0
- package/dist/src/uhp/types.js +2 -0
- package/dist/src/update/service.mjs +3 -1
- package/package.json +1 -1
- package/tools/agents/deploy-agents.mjs +28 -8
- package/tools/agents/providers/base.mjs +5 -3
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { MISSION_API_VERSION, } from './types.js';
|
|
2
|
+
const TERMINAL = new Set(['completed', 'failed', 'incomplete', 'cancelled']);
|
|
3
|
+
function object(value, label = 'Mission input') {
|
|
4
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
5
|
+
throw new Error(`${label} must be an object.`);
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function string(value) {
|
|
9
|
+
return typeof value === 'string' && value.length ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
function requiredId(record) {
|
|
12
|
+
const metadata = record.metadata && typeof record.metadata === 'object' ? record.metadata : {};
|
|
13
|
+
const id = string(record.missionId) ?? string(record.mission_id) ?? string(record.id)
|
|
14
|
+
?? string(record.taskId) ?? string(record.task_id) ?? string(record.responseId) ?? string(metadata.id);
|
|
15
|
+
if (!id)
|
|
16
|
+
throw new Error('Mission source is missing a stable identifier.');
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
19
|
+
export function normalizeMissionState(nativeState) {
|
|
20
|
+
const state = String(nativeState ?? 'unknown').toLowerCase().replace(/_/g, '-');
|
|
21
|
+
if (['done', 'completed', 'succeeded', 'success'].includes(state))
|
|
22
|
+
return 'completed';
|
|
23
|
+
if (['aborted', 'cancelled', 'canceled'].includes(state))
|
|
24
|
+
return 'cancelled';
|
|
25
|
+
if (['failed', 'error', 'rejected', 'timed-out'].includes(state))
|
|
26
|
+
return 'failed';
|
|
27
|
+
if (['incomplete', 'budget-exhausted', 'max-iterations', 'max-tokens'].includes(state))
|
|
28
|
+
return 'incomplete';
|
|
29
|
+
if (['running', 'in-progress', 'working', 'started', 'assigned', 'admitted', 'starting'].includes(state))
|
|
30
|
+
return 'running';
|
|
31
|
+
if (['pending', 'submitted', 'queued', 'runnable', 'scheduled'].includes(state))
|
|
32
|
+
return 'pending';
|
|
33
|
+
if (['blocked', 'blocked-hitl', 'input-required', 'auth-required', 'paused', 'suspended'].includes(state))
|
|
34
|
+
return 'blocked';
|
|
35
|
+
if (['operator-review', 'operator-review-required', 'manual-review'].includes(state))
|
|
36
|
+
return 'operator-review';
|
|
37
|
+
if (['unknown', 'disconnected', 'detached', 'unreachable'].includes(state))
|
|
38
|
+
return 'unknown';
|
|
39
|
+
return 'unknown';
|
|
40
|
+
}
|
|
41
|
+
function knownKeysFor(source) {
|
|
42
|
+
const common = ['id', 'missionId', 'mission_id', 'taskId', 'task_id', 'responseId', 'title', 'goal', 'objective', 'prompt', 'completion', 'completionCriterion', 'completion_criterion', 'status', 'state', 'nativeState', 'createdAt', 'created_at', 'updatedAt', 'updated_at', 'metadata', 'spec', 'provenance', 'extensions', 'artifacts', 'output', 'partialOutput', 'error', 'checkpoint', 'cycles', 'activityLog', 'totalCost', 'runtimesUsed', 'apiVersion', 'api_version', 'schemaVersion', 'schema_version', 'kind', 'session_id', 'previous_response_id'];
|
|
43
|
+
if (source === 'uhp-2026-08-11')
|
|
44
|
+
common.push('object', 'model', 'incomplete_details', 'store', 'usage');
|
|
45
|
+
return new Set(common);
|
|
46
|
+
}
|
|
47
|
+
function extensions(record, source) {
|
|
48
|
+
const explicit = record.extensions && typeof record.extensions === 'object' ? structuredClone(record.extensions) : {};
|
|
49
|
+
if (record.metadata && typeof record.metadata === 'object')
|
|
50
|
+
explicit.metadata = structuredClone(record.metadata);
|
|
51
|
+
if (record.status && typeof record.status === 'object')
|
|
52
|
+
explicit.nativeStatus = structuredClone(record.status);
|
|
53
|
+
for (const [key, value] of Object.entries(record))
|
|
54
|
+
if (!knownKeysFor(source).has(key))
|
|
55
|
+
explicit[key] = structuredClone(value);
|
|
56
|
+
return explicit;
|
|
57
|
+
}
|
|
58
|
+
function artifacts(record) {
|
|
59
|
+
const values = Array.isArray(record.artifacts) ? record.artifacts : [];
|
|
60
|
+
return values.flatMap((item, index) => {
|
|
61
|
+
if (!item || typeof item !== 'object')
|
|
62
|
+
return [];
|
|
63
|
+
const artifact = item;
|
|
64
|
+
const id = string(artifact.id) ?? string(artifact.fileId) ?? string(artifact.file_id) ?? string(artifact.uri) ?? `artifact-${index}`;
|
|
65
|
+
return [{
|
|
66
|
+
id,
|
|
67
|
+
kind: string(artifact.kind) ?? 'other',
|
|
68
|
+
...(string(artifact.uri) ? { uri: string(artifact.uri) } : {}),
|
|
69
|
+
...(string(artifact.sha256) ? { sha256: string(artifact.sha256) } : {}),
|
|
70
|
+
...(string(artifact.mediaType) ?? string(artifact.media_type) ? { mediaType: string(artifact.mediaType) ?? string(artifact.media_type) } : {}),
|
|
71
|
+
extensions: structuredClone(artifact),
|
|
72
|
+
}];
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function uhpOutputArtifacts(record) {
|
|
76
|
+
const found = new Map();
|
|
77
|
+
function visit(value) {
|
|
78
|
+
if (Array.isArray(value)) {
|
|
79
|
+
value.forEach(visit);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!value || typeof value !== 'object')
|
|
83
|
+
return;
|
|
84
|
+
const item = value;
|
|
85
|
+
const id = string(item.file_id);
|
|
86
|
+
if (id)
|
|
87
|
+
found.set(id, { id, kind: 'uhp-file', ...(string(item.media_type) ? { mediaType: string(item.media_type) } : {}), extensions: { 'uhp.file': structuredClone(item) } });
|
|
88
|
+
Object.values(item).forEach(visit);
|
|
89
|
+
}
|
|
90
|
+
visit(record.output);
|
|
91
|
+
return [...found.values()];
|
|
92
|
+
}
|
|
93
|
+
function nativeStateFor(source, record, statusRecord) {
|
|
94
|
+
const explicit = string(statusRecord.state) ?? string(record.nativeState) ?? string(record.status)
|
|
95
|
+
?? string(record.state) ?? string(record.observed_state) ?? string(record.observedState) ?? string(record.nodeState);
|
|
96
|
+
if (explicit)
|
|
97
|
+
return explicit;
|
|
98
|
+
if (source === 'mission-ledger' && record.checkpoint && typeof record.checkpoint === 'object') {
|
|
99
|
+
const checkpoint = record.checkpoint;
|
|
100
|
+
if (Array.isArray(checkpoint.pending) && checkpoint.pending.length)
|
|
101
|
+
return 'running';
|
|
102
|
+
if (Array.isArray(checkpoint.failed) && checkpoint.failed.length)
|
|
103
|
+
return 'failed';
|
|
104
|
+
if (Array.isArray(checkpoint.completed) && checkpoint.completed.length)
|
|
105
|
+
return 'completed';
|
|
106
|
+
}
|
|
107
|
+
return 'unknown';
|
|
108
|
+
}
|
|
109
|
+
function sourceVersion(source, record) {
|
|
110
|
+
if (source === 'canonical')
|
|
111
|
+
return string(record.apiVersion) ?? 'unknown';
|
|
112
|
+
if (source === 'uhp-2026-08-11')
|
|
113
|
+
return '2026-08-11';
|
|
114
|
+
if (source === 'executor-v1')
|
|
115
|
+
return 'executor.aiwg.io/v1';
|
|
116
|
+
if (source === 'fleet-workload-v1')
|
|
117
|
+
return 'fleet-workload/v1';
|
|
118
|
+
if (source === 'graph-flow-v1')
|
|
119
|
+
return 'graph.flow.aiwg.io/v1';
|
|
120
|
+
if (source === 'activity-v1')
|
|
121
|
+
return 'activity.aiwg.io/v1';
|
|
122
|
+
return string(record.apiVersion) ?? string(record.api_version) ?? string(record.schemaVersion) ?? string(record.schema_version) ?? 'unversioned';
|
|
123
|
+
}
|
|
124
|
+
function assertSupported(version) {
|
|
125
|
+
const supported = new Set(['unversioned', MISSION_API_VERSION, '2026-08-11', 'executor.aiwg.io/v1', 'fleet-workload/v1', 'graph.flow.aiwg.io/v1', 'activity.aiwg.io/v1']);
|
|
126
|
+
if (supported.has(version))
|
|
127
|
+
return;
|
|
128
|
+
const major = version.match(/(?:^|\/)v(\d+)(?:$|[.-])/)?.[1];
|
|
129
|
+
if (major && major !== '1')
|
|
130
|
+
throw new Error(`Unsupported Mission source major version '${version}'; supported major is v1.`);
|
|
131
|
+
throw new Error(`Unsupported Mission source version '${version}'.`);
|
|
132
|
+
}
|
|
133
|
+
export function validateCanonicalMission(value) {
|
|
134
|
+
const record = object(value, 'Canonical Mission');
|
|
135
|
+
if (record.apiVersion !== MISSION_API_VERSION)
|
|
136
|
+
throw new Error(`Unsupported canonical Mission version '${String(record.apiVersion)}'.`);
|
|
137
|
+
if (record.kind !== 'Mission')
|
|
138
|
+
throw new Error("Canonical Mission kind must be 'Mission'.");
|
|
139
|
+
const metadata = object(record.metadata, 'Mission metadata');
|
|
140
|
+
const spec = object(record.spec, 'Mission spec');
|
|
141
|
+
const status = object(record.status, 'Mission status');
|
|
142
|
+
const provenance = object(record.provenance, 'Mission provenance');
|
|
143
|
+
if (!string(metadata.id) || !string(spec.objective) || !string(provenance.sourceContract) || !string(provenance.sourceVersion))
|
|
144
|
+
throw new Error('Canonical Mission is missing required identity, objective, or provenance.');
|
|
145
|
+
const state = normalizeMissionState(status.state);
|
|
146
|
+
if (status.state !== state)
|
|
147
|
+
throw new Error(`Canonical Mission state '${String(status.state)}' is not normalized.`);
|
|
148
|
+
if (status.terminal !== TERMINAL.has(state))
|
|
149
|
+
throw new Error(`Canonical Mission terminal flag contradicts state '${state}'.`);
|
|
150
|
+
if (!Array.isArray(status.artifacts))
|
|
151
|
+
throw new Error('Canonical Mission artifacts must be an array.');
|
|
152
|
+
return structuredClone(value);
|
|
153
|
+
}
|
|
154
|
+
export function decodeMission(input, source) {
|
|
155
|
+
const record = object(input);
|
|
156
|
+
if (source === 'canonical') {
|
|
157
|
+
const value = validateCanonicalMission(record);
|
|
158
|
+
return { value, sourceVersion: MISSION_API_VERSION, warnings: [], preservedExtensions: structuredClone(value.extensions ?? {}), lossReport: [] };
|
|
159
|
+
}
|
|
160
|
+
const version = sourceVersion(source, record);
|
|
161
|
+
assertSupported(version);
|
|
162
|
+
const statusRecord = record.status && typeof record.status === 'object' ? record.status : {};
|
|
163
|
+
const nativeState = nativeStateFor(source, record, statusRecord);
|
|
164
|
+
const state = normalizeMissionState(nativeState);
|
|
165
|
+
const objective = string(record.goal) ?? string(record.objective) ?? string(record.title) ?? string(record.prompt) ?? '(legacy mission objective unavailable)';
|
|
166
|
+
const completionCriterion = string(record.completionCriterion) ?? string(record.completion_criterion) ?? string(record.completion);
|
|
167
|
+
const preserved = extensions(record, source);
|
|
168
|
+
const warnings = [];
|
|
169
|
+
const lossReport = [];
|
|
170
|
+
if (objective.startsWith('(legacy'))
|
|
171
|
+
warnings.push('Source did not contain a mission objective; placeholder retained.');
|
|
172
|
+
if (state === 'unknown' && nativeState !== 'unknown')
|
|
173
|
+
warnings.push(`Unknown native state '${nativeState}' preserved beside normalized unknown state.`);
|
|
174
|
+
const sourceArtifacts = artifacts(record);
|
|
175
|
+
if (source === 'uhp-2026-08-11') {
|
|
176
|
+
const ids = new Set(sourceArtifacts.map(artifact => artifact.id));
|
|
177
|
+
for (const artifact of uhpOutputArtifacts(record))
|
|
178
|
+
if (!ids.has(artifact.id))
|
|
179
|
+
sourceArtifacts.push(artifact);
|
|
180
|
+
}
|
|
181
|
+
const budgets = record.budgets && typeof record.budgets === 'object' ? record.budgets : undefined;
|
|
182
|
+
const lineage = Array.isArray(record.lineage) ? record.lineage.flatMap(item => {
|
|
183
|
+
if (!item || typeof item !== 'object')
|
|
184
|
+
return [];
|
|
185
|
+
const entry = item;
|
|
186
|
+
return string(entry.relation) && string(entry.id) ? [{ relation: string(entry.relation), id: string(entry.id) }] : [];
|
|
187
|
+
}) : undefined;
|
|
188
|
+
const value = {
|
|
189
|
+
apiVersion: MISSION_API_VERSION,
|
|
190
|
+
kind: 'Mission',
|
|
191
|
+
metadata: {
|
|
192
|
+
id: requiredId(record),
|
|
193
|
+
...(string(record.createdAt) ?? string(record.created_at) ? { createdAt: string(record.createdAt) ?? string(record.created_at) } : {}),
|
|
194
|
+
...(string(record.updatedAt) ?? string(record.updated_at) ? { updatedAt: string(record.updatedAt) ?? string(record.updated_at) } : {}),
|
|
195
|
+
...(string(record.previous_response_id) ? { previousId: string(record.previous_response_id) } : {}),
|
|
196
|
+
...(lineage?.length ? { lineage } : {}),
|
|
197
|
+
},
|
|
198
|
+
spec: { objective, ...(completionCriterion ? { completionCriterion } : {}), ...(budgets ? { budgets: structuredClone(budgets) } : {}) },
|
|
199
|
+
status: {
|
|
200
|
+
state,
|
|
201
|
+
terminal: TERMINAL.has(state),
|
|
202
|
+
nativeState,
|
|
203
|
+
artifacts: sourceArtifacts,
|
|
204
|
+
...(record.output !== undefined || record.partialOutput !== undefined || record.partial_output !== undefined ? { partialOutput: structuredClone(record.partialOutput ?? record.partial_output ?? record.output) } : {}),
|
|
205
|
+
},
|
|
206
|
+
provenance: { sourceContract: source, sourceVersion: version, ...(source === 'a2a' || source.startsWith('uhp') ? { transport: source.split('-')[0] } : {}), sourceId: requiredId(record) },
|
|
207
|
+
...(Object.keys(preserved).length ? { extensions: { [`aiwg.source.${source}`]: preserved } } : {}),
|
|
208
|
+
};
|
|
209
|
+
return { value, sourceVersion: version, warnings, preservedExtensions: preserved, lossReport };
|
|
210
|
+
}
|
|
211
|
+
function projectionLosses(value, target) {
|
|
212
|
+
const losses = [];
|
|
213
|
+
if (value.status.verification?.length)
|
|
214
|
+
losses.push({ path: '/status/verification', reason: `${target} has no native verification ledger`, severity: 'warning' });
|
|
215
|
+
if (value.metadata.lineage?.length && !['mission-ledger', 'fleet-workload-v1', 'canonical'].includes(target))
|
|
216
|
+
losses.push({ path: '/metadata/lineage', reason: `${target} cannot represent full lineage`, severity: 'required' });
|
|
217
|
+
if (value.spec.budgets && !['fleet-workload-v1', 'canonical'].includes(target))
|
|
218
|
+
losses.push({ path: '/spec/budgets', reason: `${target} cannot represent every canonical budget`, severity: 'warning' });
|
|
219
|
+
return losses;
|
|
220
|
+
}
|
|
221
|
+
export function encodeMission(value, target, options = {}) {
|
|
222
|
+
const mission = validateCanonicalMission(value);
|
|
223
|
+
if (target === 'canonical')
|
|
224
|
+
return { value: mission, targetVersion: MISSION_API_VERSION, warnings: [], lossReport: [] };
|
|
225
|
+
const lossReport = projectionLosses(mission, target);
|
|
226
|
+
const required = lossReport.filter(loss => loss.severity === 'required');
|
|
227
|
+
if (required.length && !options.allowLoss)
|
|
228
|
+
throw new Error(`Projection to ${target} would silently lose required semantics: ${required.map(loss => loss.path).join(', ')}.`);
|
|
229
|
+
const base = {
|
|
230
|
+
missionId: mission.metadata.id,
|
|
231
|
+
goal: mission.spec.objective,
|
|
232
|
+
completionCriterion: mission.spec.completionCriterion,
|
|
233
|
+
status: mission.status.nativeState ?? mission.status.state,
|
|
234
|
+
artifacts: structuredClone(mission.status.artifacts),
|
|
235
|
+
partialOutput: structuredClone(mission.status.partialOutput),
|
|
236
|
+
lineage: structuredClone(mission.metadata.lineage ?? []),
|
|
237
|
+
extensions: {
|
|
238
|
+
'aiwg.mission.canonical': { apiVersion: mission.apiVersion, state: mission.status.state, lossReport },
|
|
239
|
+
...(mission.extensions ?? {}),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
let projected = base;
|
|
243
|
+
if (target === 'mission-plan')
|
|
244
|
+
projected = { missionId: base.missionId, goal: base.goal, completionCriterion: base.completionCriterion ?? '', cycles: [], extensions: base.extensions };
|
|
245
|
+
else if (target === 'mission-ledger')
|
|
246
|
+
projected = { ...base, activityLog: [], cycles: [], totalCost: 0, checkpoint: { completed: [], pending: [], failed: [] }, runtimesUsed: [] };
|
|
247
|
+
else if (target === 'uhp-2026-08-11')
|
|
248
|
+
projected = { id: mission.provenance.sourceId ?? mission.metadata.id, object: 'response', created_at: 0, status: base.status, model: '', output: mission.status.partialOutput ?? [], metadata: base.extensions };
|
|
249
|
+
else if (target === 'a2a')
|
|
250
|
+
projected = { id: mission.provenance.sourceId ?? mission.metadata.id, status: { state: base.status }, artifacts: base.artifacts, partialOutput: base.partialOutput, metadata: base.extensions };
|
|
251
|
+
else if (target === 'graph-flow-v1')
|
|
252
|
+
projected = { schemaVersion: 'graph.flow.aiwg.io/v1', runId: mission.metadata.id, nodeState: mission.status.state, metadata: base.extensions };
|
|
253
|
+
else if (target === 'cockpit')
|
|
254
|
+
projected = { id: mission.metadata.id, session_id: mission.metadata.id, source: mission.provenance.transport ?? mission.provenance.sourceContract, title: mission.spec.objective, completion: mission.spec.completionCriterion, status: mission.status.state, artifacts: base.artifacts, partialOutput: base.partialOutput, extensions: base.extensions };
|
|
255
|
+
else if (target === 'activity-v1')
|
|
256
|
+
projected = { event: 'mission.projected', missionId: mission.metadata.id, objective: mission.spec.objective, state: mission.status.state, metadata: base.extensions };
|
|
257
|
+
else if (target === 'executor-v1')
|
|
258
|
+
projected = { mission_id: mission.metadata.id, objective: mission.spec.objective, completion_criterion: mission.spec.completionCriterion, state: base.status, artifacts: base.artifacts, partial_output: base.partialOutput, metadata: base.extensions };
|
|
259
|
+
else if (target === 'fleet-workload-v1')
|
|
260
|
+
projected = { mission_id: mission.metadata.id, objective: mission.spec.objective, completion_criterion: mission.spec.completionCriterion, observed_state: base.status, budgets: mission.spec.budgets, artifacts: base.artifacts, partial_output: base.partialOutput, lineage: base.lineage, metadata: base.extensions };
|
|
261
|
+
else if (target === 'mission-control-session')
|
|
262
|
+
projected = { id: mission.metadata.id, objective: mission.spec.objective, status: mission.status.state, completion: mission.spec.completionCriterion, artifacts: base.artifacts, partialOutput: base.partialOutput, extensions: base.extensions };
|
|
263
|
+
return { value: projected, targetVersion: target, warnings: lossReport.map(loss => `${loss.path}: ${loss.reason}`), lossReport };
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=codecs.js.map
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import * as fs from 'fs/promises';
|
|
23
23
|
import * as path from 'path';
|
|
24
24
|
import { buildProviderBootstrapBlock, PROVIDER_BOOTSTRAP_START, PROVIDER_BOOTSTRAP_END, } from './workspace-context.js';
|
|
25
|
+
import { dominantLineEnding, withLineEnding } from './line-endings.js';
|
|
25
26
|
export const CLAUDE_HOOK_START = '<!-- AIWG:claude-md-hook:start -->';
|
|
26
27
|
export const CLAUDE_HOOK_END = '<!-- AIWG:claude-md-hook:end -->';
|
|
27
28
|
function buildClaudeArtifactOutputPolicy(policy = {}) {
|
|
@@ -77,7 +78,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
77
78
|
catch {
|
|
78
79
|
// Missing/legacy/temporarily malformed config receives the safe default.
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
+
let block = buildClaudeHookBlock(policy);
|
|
81
82
|
// Case 1: CLAUDE.md does not exist — create a minimal one with just the block.
|
|
82
83
|
let existing;
|
|
83
84
|
try {
|
|
@@ -92,6 +93,8 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
92
93
|
}
|
|
93
94
|
throw err;
|
|
94
95
|
}
|
|
96
|
+
const lineEnding = dominantLineEnding(existing);
|
|
97
|
+
block = withLineEnding(block, lineEnding);
|
|
95
98
|
const startIdx = existing.indexOf(CLAUDE_HOOK_START);
|
|
96
99
|
const endIdx = existing.indexOf(CLAUDE_HOOK_END);
|
|
97
100
|
// Case 2: marker block does not exist — append the block to end of file.
|
|
@@ -117,8 +120,8 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
117
120
|
return result;
|
|
118
121
|
}
|
|
119
122
|
// Ensure the file ends with a single newline before appending.
|
|
120
|
-
const trimmed = existing.replace(
|
|
121
|
-
const updated = `${trimmed}
|
|
123
|
+
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
124
|
+
const updated = `${trimmed}${lineEnding}${block}${lineEnding}`;
|
|
122
125
|
await fs.writeFile(claudeMdPath, updated, 'utf8');
|
|
123
126
|
result.action = 'inserted';
|
|
124
127
|
return result;
|
|
@@ -135,8 +138,8 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
135
138
|
await fs.writeFile(backupPath, existing, 'utf8');
|
|
136
139
|
result.backupPath = backupPath;
|
|
137
140
|
}
|
|
138
|
-
const trimmed = existing.replace(
|
|
139
|
-
const updated = `${trimmed}
|
|
141
|
+
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
142
|
+
const updated = `${trimmed}${lineEnding}${block}${lineEnding}`;
|
|
140
143
|
await fs.writeFile(claudeMdPath, updated, 'utf8');
|
|
141
144
|
result.action = 'inserted';
|
|
142
145
|
return result;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Select the majority line ending, preferring LF for ties and new files. */
|
|
2
|
+
export function dominantLineEnding(content) {
|
|
3
|
+
const crlfCount = content.match(/\r\n/g)?.length ?? 0;
|
|
4
|
+
const newlineCount = content.match(/\n/g)?.length ?? 0;
|
|
5
|
+
const bareLfCount = newlineCount - crlfCount;
|
|
6
|
+
return crlfCount > bareLfCount ? '\r\n' : '\n';
|
|
7
|
+
}
|
|
8
|
+
/** Render generated text using the line-ending convention of existing content. */
|
|
9
|
+
export function withLineEnding(content, lineEnding) {
|
|
10
|
+
return content.replace(/\r?\n/g, lineEnding);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=line-endings.js.map
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import * as fs from 'fs/promises';
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import { buildProviderBootstrapBlock } from './workspace-context.js';
|
|
17
|
+
import { dominantLineEnding, withLineEnding } from './line-endings.js';
|
|
17
18
|
export const CONTEXT_HOOK_START = '<!-- AIWG:context-hook:start -->';
|
|
18
19
|
export const CONTEXT_HOOK_END = '<!-- AIWG:context-hook:end -->';
|
|
19
20
|
/** The managed block — loads canonical workspace context before framework context. */
|
|
@@ -42,7 +43,7 @@ export function hasContextHook(content) {
|
|
|
42
43
|
*/
|
|
43
44
|
export async function ensureManagedHook(filePath, opts = {}) {
|
|
44
45
|
const base = path.basename(filePath);
|
|
45
|
-
|
|
46
|
+
let block = buildContextHookBlock(opts.provider);
|
|
46
47
|
const result = { path: filePath, action: 'skipped', warnings: [] };
|
|
47
48
|
let existing;
|
|
48
49
|
try {
|
|
@@ -56,6 +57,8 @@ export async function ensureManagedHook(filePath, opts = {}) {
|
|
|
56
57
|
}
|
|
57
58
|
throw err;
|
|
58
59
|
}
|
|
60
|
+
const lineEnding = dominantLineEnding(existing);
|
|
61
|
+
block = withLineEnding(block, lineEnding);
|
|
59
62
|
// Already has both bare includes (operator wired them by hand) — nothing to do.
|
|
60
63
|
if (!existing.includes(CONTEXT_HOOK_START) && /^[ \t]*@WORKSPACE\.md[ \t]*$/m.test(existing) && /^[ \t]*@AIWG\.md[ \t]*$/m.test(existing)) {
|
|
61
64
|
result.action = 'unchanged';
|
|
@@ -65,16 +68,16 @@ export async function ensureManagedHook(filePath, opts = {}) {
|
|
|
65
68
|
const e = existing.indexOf(CONTEXT_HOOK_END);
|
|
66
69
|
// No managed block — append it to the end, preserving everything above.
|
|
67
70
|
if (s === -1 && e === -1) {
|
|
68
|
-
const trimmed = existing.replace(
|
|
69
|
-
await fs.writeFile(filePath, `${trimmed}
|
|
71
|
+
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
72
|
+
await fs.writeFile(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
|
|
70
73
|
result.action = 'inserted';
|
|
71
74
|
return result;
|
|
72
75
|
}
|
|
73
76
|
// Malformed (one marker only) — repair only with --force to avoid clobbering.
|
|
74
77
|
if (s === -1 || e === -1) {
|
|
75
78
|
if (opts.force) {
|
|
76
|
-
const trimmed = existing.replace(
|
|
77
|
-
await fs.writeFile(filePath, `${trimmed}
|
|
79
|
+
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
80
|
+
await fs.writeFile(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
|
|
78
81
|
result.action = 'inserted';
|
|
79
82
|
return result;
|
|
80
83
|
}
|
|
@@ -14,6 +14,7 @@ import { buildNormalizedAiwgMd } from './finalization.js';
|
|
|
14
14
|
import { getProviderDefinition, listProviderDefinitions, } from '../../providers/provider-definitions.js';
|
|
15
15
|
import { readAiwgConfig } from '../../config/aiwg-config.js';
|
|
16
16
|
import { projectAiwgPath, projectControlPath, resolveProjectAiwgDir, } from '../../config/project-artifacts.js';
|
|
17
|
+
import { dominantLineEnding, withLineEnding } from './line-endings.js';
|
|
17
18
|
export const WORKSPACE_MANAGED_START = '<!-- AIWG:workspace-context:start -->';
|
|
18
19
|
export const WORKSPACE_MANAGED_END = '<!-- AIWG:workspace-context:end -->';
|
|
19
20
|
export const WORKSPACE_OPERATOR_START = '<!-- AIWG:workspace-operator:start -->';
|
|
@@ -90,7 +91,8 @@ function replaceBlock(content, start, end, block) {
|
|
|
90
91
|
return null;
|
|
91
92
|
if (startIndex < 0 || endIndex < startIndex)
|
|
92
93
|
throw new Error(`Malformed managed block: ${start} / ${end}`);
|
|
93
|
-
|
|
94
|
+
const renderedBlock = withLineEnding(block, dominantLineEnding(content));
|
|
95
|
+
return content.slice(0, startIndex) + renderedBlock + content.slice(endIndex + end.length);
|
|
94
96
|
}
|
|
95
97
|
function stripGeneratedBlocks(content) {
|
|
96
98
|
let stripped = content;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned capability contract for scalable storage and index backends.
|
|
3
|
+
*
|
|
4
|
+
* This contract describes semantics; it does not make optional capabilities
|
|
5
|
+
* available. Consumers must negotiate before using anything beyond the
|
|
6
|
+
* required baseline.
|
|
7
|
+
*
|
|
8
|
+
* @issue #2193
|
|
9
|
+
*/
|
|
10
|
+
export const STORAGE_BACKEND_CONTRACT = 'aiwg.storage-backend/v1';
|
|
11
|
+
export class StorageCapabilityError extends Error {
|
|
12
|
+
code = 'AIWG_STORAGE_CAPABILITY_NEGOTIATION_FAILED';
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'StorageCapabilityError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Fail-closed negotiation. Unknown major contracts and missing capabilities never degrade silently. */
|
|
19
|
+
export function negotiateStorageCapabilities(descriptor, request) {
|
|
20
|
+
if (request.contract !== STORAGE_BACKEND_CONTRACT) {
|
|
21
|
+
throw new StorageCapabilityError(`unsupported storage contract "${request.contract}"; backend provides ${STORAGE_BACKEND_CONTRACT}`);
|
|
22
|
+
}
|
|
23
|
+
if (request.acceptedSchemaVersions?.length &&
|
|
24
|
+
!request.acceptedSchemaVersions.includes(descriptor.schemaVersion)) {
|
|
25
|
+
throw new StorageCapabilityError(`backend schema ${descriptor.schemaVersion} is not in the accepted schema set`);
|
|
26
|
+
}
|
|
27
|
+
const available = new Set(descriptor.capabilities);
|
|
28
|
+
const missing = [...new Set(request.required)].filter(capability => !available.has(capability));
|
|
29
|
+
if (missing.length) {
|
|
30
|
+
throw new StorageCapabilityError(`backend ${descriptor.backend} lacks required capabilities: ${missing.sort().join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
contract: STORAGE_BACKEND_CONTRACT,
|
|
34
|
+
backend: descriptor.backend,
|
|
35
|
+
schemaVersion: descriptor.schemaVersion,
|
|
36
|
+
capabilities: [...descriptor.capabilities].sort(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const BASELINE = ['read', 'subsystem-isolation'];
|
|
40
|
+
export const STORAGE_BACKEND_MATRIX = {
|
|
41
|
+
'json-filesystem': descriptor('json-filesystem', 'supported', BASELINE, 'filesystem', 'single-host', 'none', 'regenerable-index'),
|
|
42
|
+
graphology: descriptor('graphology', 'supported', BASELINE, 'process', 'local-process', 'none', 'regenerable-index'),
|
|
43
|
+
sqlite: descriptor('sqlite', 'supported', [...BASELINE, 'atomic-batch', 'consistent-snapshot', 'tombstones', 'idempotency-keys', 'filtered-query', 'cursor-pagination', 'backup', 'restore'], 'wal', 'single-host', 'serializable', 'regenerable-index'),
|
|
44
|
+
'fortemi-core-static': descriptor('fortemi-core-static', 'supported', ['read', 'filtered-query', 'recursive-traversal', 'set-operations'], 'filesystem', 'single-host', 'snapshot', 'static-cache'),
|
|
45
|
+
'fortemi-server': descriptor('fortemi-server', 'alpha', [...BASELINE, 'filtered-query', 'health', 'tls', 'tenant-isolation'], 'replicated', 'remote-service', 'none', 'remote-persistence'),
|
|
46
|
+
'postgres-direct': descriptor('postgres-direct', 'advanced', [], 'replicated', 'remote-service', 'none', 'canonical'),
|
|
47
|
+
'postgres-postgrest': descriptor('postgres-postgrest', 'advanced', [], 'replicated', 'remote-service', 'none', 'canonical'),
|
|
48
|
+
mysql: descriptor('mysql', 'deferred', [], 'replicated', 'remote-service', 'none', 'canonical'),
|
|
49
|
+
};
|
|
50
|
+
function descriptor(backend, maturity, capabilities, durability, availability, isolation, dataClass) {
|
|
51
|
+
return {
|
|
52
|
+
contract: STORAGE_BACKEND_CONTRACT,
|
|
53
|
+
backend,
|
|
54
|
+
implementationVersion: '1.0.0',
|
|
55
|
+
schemaVersion: '1',
|
|
56
|
+
maturity,
|
|
57
|
+
capabilities,
|
|
58
|
+
durability,
|
|
59
|
+
availability,
|
|
60
|
+
isolation,
|
|
61
|
+
dataClass,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=backend-contract.js.map
|
|
@@ -21,6 +21,8 @@ export { FilesystemAdapter } from './backends/fs.js';
|
|
|
21
21
|
export { ObsidianAdapter } from './backends/obsidian.js';
|
|
22
22
|
export { LogseqAdapter } from './backends/logseq.js';
|
|
23
23
|
export { FortemiAdapter } from './backends/fortemi.js';
|
|
24
|
+
export { STORAGE_BACKEND_CONTRACT, STORAGE_BACKEND_MATRIX, StorageCapabilityError, negotiateStorageCapabilities, } from './backend-contract.js';
|
|
25
|
+
export { STORAGE_MIGRATION_PROTOCOL, MigrationProtocolError, StorageMigrationCoordinator, approvalDigest, digestRecords, validateManifest, } from './migration-protocol.js';
|
|
24
26
|
let state = null;
|
|
25
27
|
/**
|
|
26
28
|
* Initialize or reset the registry for a given project root. Most
|