@tryinget/pi-agent-registry 0.3.0
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/LICENSE +78 -0
- package/README.md +238 -0
- package/docs/engineering.local.md +90 -0
- package/docs/project/2026-08-27-agent-registry.md +287 -0
- package/docs/project/foundation.md +31 -0
- package/docs/project/vision.md +18 -0
- package/examples/.gitkeep +0 -0
- package/extensions/pi-agent-registry.ts +378 -0
- package/package.json +105 -0
- package/policy/engineering-lane.json +34 -0
- package/policy/security-policy.json +10 -0
- package/prompts/implementation-planning.md +20 -0
- package/prompts/security-review.md +20 -0
- package/scripts/fleet-lint.mjs +82 -0
- package/src/.gitkeep +0 -0
- package/src/agent-skill-resolver.ts +50 -0
- package/src/asc-execution-surface.ts +64 -0
- package/src/dispatch-authorization.ts +237 -0
- package/src/dispatch-contract.ts +89 -0
- package/src/dispatch-receipt.ts +326 -0
- package/src/dispatch-request.ts +135 -0
- package/src/dispatch.ts +498 -0
- package/src/ec-profiles.ts +392 -0
- package/src/fleet-git-snapshot.ts +323 -0
- package/src/fleet-lint-provenance.ts +356 -0
- package/src/fleet-lint-repository.ts +450 -0
- package/src/fleet-lint-skills.ts +131 -0
- package/src/fleet-lint-types.ts +113 -0
- package/src/fleet-lint-utils.ts +66 -0
- package/src/fleet-lint.ts +375 -0
- package/src/fleet-prompt-compiler.ts +155 -0
- package/src/manifest.ts +678 -0
- package/src/registry-discovery.ts +225 -0
- package/src/registry.ts +280 -0
- package/src/sessions-dir.ts +30 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: per-repository immutable manifest/profile/prompt/provenance/lifecycle diagnostics for aggregate fleet lint.
|
|
3
|
+
// read_when:
|
|
4
|
+
// - changing one-agent lint semantics, committed prompt freshness, profile references, or revision currentness.
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import { basename } from "node:path";
|
|
8
|
+
import type { EcProfileSource } from "./ec-profiles.ts";
|
|
9
|
+
import {
|
|
10
|
+
type CapturedGitFile,
|
|
11
|
+
captureFleetGitSnapshot,
|
|
12
|
+
type FleetGitSnapshot,
|
|
13
|
+
} from "./fleet-git-snapshot.ts";
|
|
14
|
+
import { inspectTemplateProvenance } from "./fleet-lint-provenance.ts";
|
|
15
|
+
import { checkFleetSkills } from "./fleet-lint-skills.ts";
|
|
16
|
+
import type { FleetLintDiagnostic, FleetLintRepositoryResult } from "./fleet-lint-types.ts";
|
|
17
|
+
import {
|
|
18
|
+
addFleetDiagnostic,
|
|
19
|
+
fleetSha256,
|
|
20
|
+
logicalFleetRepo,
|
|
21
|
+
sortFleetDiagnostics,
|
|
22
|
+
stableFleetValue,
|
|
23
|
+
} from "./fleet-lint-utils.ts";
|
|
24
|
+
import { compileFleetSystemPrompt, FLEET_COMPILED_PROMPT_PATH } from "./fleet-prompt-compiler.ts";
|
|
25
|
+
import {
|
|
26
|
+
AGENT_CREATION_TASK_PATTERN,
|
|
27
|
+
AGENT_MANIFEST_TOP_LEVEL_KEYS,
|
|
28
|
+
type AgentManifest,
|
|
29
|
+
validateAgentManifest,
|
|
30
|
+
} from "./manifest.ts";
|
|
31
|
+
|
|
32
|
+
const MAX_MANIFEST_BYTES = 64 * 1024;
|
|
33
|
+
const MAX_PROMPT_INPUT_BYTES = 512 * 1024;
|
|
34
|
+
|
|
35
|
+
function containsPhysicalPath(value: string): boolean {
|
|
36
|
+
return value.includes("/") || value.includes("\\") || value.includes("~");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function lifecycleSignal(
|
|
40
|
+
latestActivityAt: string | undefined,
|
|
41
|
+
observedAt: Date,
|
|
42
|
+
staleAfterDays: number,
|
|
43
|
+
): FleetLintRepositoryResult["lifecycle"] {
|
|
44
|
+
if (!latestActivityAt) return { signal: "unknown", authorityEffect: "none" };
|
|
45
|
+
const parsed = Date.parse(latestActivityAt);
|
|
46
|
+
if (!Number.isFinite(parsed)) return { signal: "unknown", authorityEffect: "none" };
|
|
47
|
+
const stale = observedAt.getTime() - parsed > staleAfterDays * 24 * 60 * 60 * 1000;
|
|
48
|
+
return {
|
|
49
|
+
signal: stale ? "stale_candidate" : "recent_activity",
|
|
50
|
+
latestActivityAt,
|
|
51
|
+
authorityEffect: "none",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function finalizeRepositorySnapshot(params: {
|
|
56
|
+
snapshot: FleetGitSnapshot;
|
|
57
|
+
revision: FleetLintRepositoryResult["revision"];
|
|
58
|
+
diagnostics: FleetLintDiagnostic[];
|
|
59
|
+
repo: string;
|
|
60
|
+
}): Promise<void> {
|
|
61
|
+
try {
|
|
62
|
+
const finished = await params.snapshot.finish();
|
|
63
|
+
if (!finished.stable) {
|
|
64
|
+
params.revision.status = "concurrent_change";
|
|
65
|
+
addFleetDiagnostic(
|
|
66
|
+
params.diagnostics,
|
|
67
|
+
params.repo,
|
|
68
|
+
"revision.concurrent_change",
|
|
69
|
+
"error",
|
|
70
|
+
"repository HEAD or worktree status changed during fleet capture",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
params.revision.status = "invalid";
|
|
75
|
+
addFleetDiagnostic(
|
|
76
|
+
params.diagnostics,
|
|
77
|
+
params.repo,
|
|
78
|
+
"revision.finalize_failed",
|
|
79
|
+
"error",
|
|
80
|
+
"repository endpoint stability could not be verified after bounded capture",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function invalidFleetRepositoryResult(
|
|
86
|
+
root: string,
|
|
87
|
+
manifestPresent: boolean,
|
|
88
|
+
diagnostics: FleetLintDiagnostic[],
|
|
89
|
+
): FleetLintRepositoryResult {
|
|
90
|
+
return {
|
|
91
|
+
repo: logicalFleetRepo(root),
|
|
92
|
+
repoName: basename(root),
|
|
93
|
+
revision: { status: "invalid" },
|
|
94
|
+
manifest: { present: manifestPresent },
|
|
95
|
+
prompt: { status: "unverifiable", compilerContract: "ai-society.agent-prompt-compiler/1" },
|
|
96
|
+
template: { mode: "unknown", provenanceStatus: "unbound" },
|
|
97
|
+
lifecycle: { signal: "unknown", authorityEffect: "none" },
|
|
98
|
+
diagnostics,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function lintFleetRepository(params: {
|
|
103
|
+
root: string;
|
|
104
|
+
manifestPresent: boolean;
|
|
105
|
+
ec: EcProfileSource;
|
|
106
|
+
ecSnapshot?: FleetGitSnapshot;
|
|
107
|
+
observedAt: Date;
|
|
108
|
+
staleAfterDays: number;
|
|
109
|
+
}): Promise<FleetLintRepositoryResult> {
|
|
110
|
+
const repo = logicalFleetRepo(params.root);
|
|
111
|
+
const diagnostics: FleetLintDiagnostic[] = [];
|
|
112
|
+
let snapshot: FleetGitSnapshot;
|
|
113
|
+
try {
|
|
114
|
+
snapshot = await captureFleetGitSnapshot(params.root);
|
|
115
|
+
} catch {
|
|
116
|
+
addFleetDiagnostic(
|
|
117
|
+
diagnostics,
|
|
118
|
+
repo,
|
|
119
|
+
"revision.invalid",
|
|
120
|
+
"error",
|
|
121
|
+
"repository Git snapshot could not be captured",
|
|
122
|
+
);
|
|
123
|
+
return invalidFleetRepositoryResult(params.root, params.manifestPresent, diagnostics);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const revision: FleetLintRepositoryResult["revision"] = {
|
|
127
|
+
commit: snapshot.commit,
|
|
128
|
+
treeOid: snapshot.treeOid,
|
|
129
|
+
status: snapshot.status,
|
|
130
|
+
statusSha256: snapshot.statusSha256,
|
|
131
|
+
};
|
|
132
|
+
if (snapshot.status === "dirty") {
|
|
133
|
+
addFleetDiagnostic(
|
|
134
|
+
diagnostics,
|
|
135
|
+
repo,
|
|
136
|
+
"revision.worktree_dirty",
|
|
137
|
+
"error",
|
|
138
|
+
"runtime worktree differs from the immutable committed snapshot",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const lifecycle = lifecycleSignal(
|
|
142
|
+
snapshot.latestActivityAt,
|
|
143
|
+
params.observedAt,
|
|
144
|
+
params.staleAfterDays,
|
|
145
|
+
);
|
|
146
|
+
if (lifecycle.signal === "stale_candidate") {
|
|
147
|
+
addFleetDiagnostic(
|
|
148
|
+
diagnostics,
|
|
149
|
+
repo,
|
|
150
|
+
"lifecycle.stale_candidate",
|
|
151
|
+
"warning",
|
|
152
|
+
`no committed diary or learning activity observed within ${params.staleAfterDays} days`,
|
|
153
|
+
);
|
|
154
|
+
} else if (lifecycle.signal === "unknown") {
|
|
155
|
+
addFleetDiagnostic(
|
|
156
|
+
diagnostics,
|
|
157
|
+
repo,
|
|
158
|
+
"lifecycle.activity_unknown",
|
|
159
|
+
"warning",
|
|
160
|
+
"no committed diary or learning activity signal was found; lifecycle remains owner-dispositioned",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let provenance: Awaited<ReturnType<typeof inspectTemplateProvenance>>;
|
|
165
|
+
try {
|
|
166
|
+
provenance = await inspectTemplateProvenance({ snapshot, repoName: repo });
|
|
167
|
+
diagnostics.push(...provenance.diagnostics);
|
|
168
|
+
} catch {
|
|
169
|
+
provenance = {
|
|
170
|
+
template: { mode: "unknown", provenanceStatus: "invalid" },
|
|
171
|
+
diagnostics: [],
|
|
172
|
+
};
|
|
173
|
+
addFleetDiagnostic(
|
|
174
|
+
diagnostics,
|
|
175
|
+
repo,
|
|
176
|
+
"template.capture_failed",
|
|
177
|
+
"error",
|
|
178
|
+
"template provenance capture failed unexpectedly",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let manifestFile: CapturedGitFile | undefined;
|
|
183
|
+
let manifestCaptureFailed = false;
|
|
184
|
+
try {
|
|
185
|
+
manifestFile = await snapshot.readFile("agent.json", MAX_MANIFEST_BYTES);
|
|
186
|
+
} catch {
|
|
187
|
+
manifestCaptureFailed = true;
|
|
188
|
+
addFleetDiagnostic(
|
|
189
|
+
diagnostics,
|
|
190
|
+
repo,
|
|
191
|
+
"manifest.capture_failed",
|
|
192
|
+
"error",
|
|
193
|
+
"committed agent.json bytes could not be captured within the lint bound",
|
|
194
|
+
"agent.json",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (!manifestFile) {
|
|
198
|
+
if (!manifestCaptureFailed) {
|
|
199
|
+
addFleetDiagnostic(
|
|
200
|
+
diagnostics,
|
|
201
|
+
repo,
|
|
202
|
+
params.manifestPresent ? "manifest.committed_blob_invalid" : "fleet.manifest_missing",
|
|
203
|
+
"error",
|
|
204
|
+
params.manifestPresent
|
|
205
|
+
? "root agent.json is not one committed non-symlink regular file"
|
|
206
|
+
: "canonical agent repository has no committed root agent.json",
|
|
207
|
+
"agent.json",
|
|
208
|
+
"backfill additively under one exact owner-authorized AK task; preserve persona bytes",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
await finalizeRepositorySnapshot({ snapshot, revision, diagnostics, repo });
|
|
212
|
+
revision.snapshotSha256 = fleetSha256(
|
|
213
|
+
JSON.stringify(
|
|
214
|
+
stableFleetValue({
|
|
215
|
+
commit: snapshot.commit,
|
|
216
|
+
tree: snapshot.treeOid,
|
|
217
|
+
status: snapshot.statusSha256,
|
|
218
|
+
}),
|
|
219
|
+
),
|
|
220
|
+
);
|
|
221
|
+
return {
|
|
222
|
+
repo,
|
|
223
|
+
repoName: basename(params.root),
|
|
224
|
+
revision,
|
|
225
|
+
manifest: { present: params.manifestPresent },
|
|
226
|
+
prompt: { status: "unverifiable", compilerContract: "ai-society.agent-prompt-compiler/1" },
|
|
227
|
+
template: provenance.template,
|
|
228
|
+
lifecycle,
|
|
229
|
+
diagnostics: sortFleetDiagnostics(diagnostics),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let rawManifest: Record<string, unknown> | undefined;
|
|
234
|
+
let manifest: AgentManifest | undefined;
|
|
235
|
+
try {
|
|
236
|
+
const parsed = JSON.parse(
|
|
237
|
+
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(manifestFile.bytes),
|
|
238
|
+
);
|
|
239
|
+
rawManifest =
|
|
240
|
+
typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
241
|
+
? (parsed as Record<string, unknown>)
|
|
242
|
+
: undefined;
|
|
243
|
+
manifest = validateAgentManifest(parsed, snapshot.root, `${repo}/agent.json`);
|
|
244
|
+
} catch {
|
|
245
|
+
addFleetDiagnostic(
|
|
246
|
+
diagnostics,
|
|
247
|
+
repo,
|
|
248
|
+
"manifest.invalid",
|
|
249
|
+
"error",
|
|
250
|
+
"committed agent.json does not satisfy strict UTF-8 and runtime schema requirements",
|
|
251
|
+
"agent.json",
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (rawManifest) {
|
|
256
|
+
for (const key of Object.keys(rawManifest)
|
|
257
|
+
.filter((key) => !AGENT_MANIFEST_TOP_LEVEL_KEYS.has(key))
|
|
258
|
+
.sort()) {
|
|
259
|
+
addFleetDiagnostic(
|
|
260
|
+
diagnostics,
|
|
261
|
+
repo,
|
|
262
|
+
"manifest.additive_field_ignored",
|
|
263
|
+
"warning",
|
|
264
|
+
`schema-1 additive field is ignored by runtime normalization (key sha256=${fleetSha256(key)})`,
|
|
265
|
+
"agent.json",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const nestedFields: Array<[string, ReadonlySet<string>]> = [
|
|
270
|
+
["skills", new Set(["profile", "extra"])],
|
|
271
|
+
["defaults", new Set(["model", "thinking"])],
|
|
272
|
+
["scope", new Set(["repos", "forbidden", "note"])],
|
|
273
|
+
];
|
|
274
|
+
for (const [section, known] of nestedFields) {
|
|
275
|
+
const value = rawManifest?.[section];
|
|
276
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
277
|
+
for (const key of Object.keys(value as Record<string, unknown>)
|
|
278
|
+
.filter((key) => !known.has(key))
|
|
279
|
+
.sort()) {
|
|
280
|
+
addFleetDiagnostic(
|
|
281
|
+
diagnostics,
|
|
282
|
+
repo,
|
|
283
|
+
"manifest.additive_field_ignored",
|
|
284
|
+
"warning",
|
|
285
|
+
`schema-1 additive field is ignored by runtime normalization (key sha256=${fleetSha256(`${section}.${key}`)})`,
|
|
286
|
+
"agent.json",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
let fleetRole: string | undefined;
|
|
291
|
+
if (manifest) {
|
|
292
|
+
if (manifest.name !== basename(params.root)) {
|
|
293
|
+
addFleetDiagnostic(
|
|
294
|
+
diagnostics,
|
|
295
|
+
repo,
|
|
296
|
+
"manifest.name_repo_mismatch",
|
|
297
|
+
"error",
|
|
298
|
+
`manifest name ${manifest.name} does not match repository ${basename(params.root)}`,
|
|
299
|
+
"agent.json",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (!manifest.role) {
|
|
303
|
+
addFleetDiagnostic(
|
|
304
|
+
diagnostics,
|
|
305
|
+
repo,
|
|
306
|
+
"manifest.role_missing",
|
|
307
|
+
"error",
|
|
308
|
+
"manifest has no canonical role binding",
|
|
309
|
+
"agent.json",
|
|
310
|
+
);
|
|
311
|
+
} else if (containsPhysicalPath(manifest.role)) {
|
|
312
|
+
addFleetDiagnostic(
|
|
313
|
+
diagnostics,
|
|
314
|
+
repo,
|
|
315
|
+
"manifest.role_not_reportable",
|
|
316
|
+
"error",
|
|
317
|
+
"manifest role contains a physical-path-shaped value and was omitted from the report",
|
|
318
|
+
"agent.json",
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
fleetRole = manifest.role;
|
|
322
|
+
}
|
|
323
|
+
if (!manifest.creation_task) {
|
|
324
|
+
addFleetDiagnostic(
|
|
325
|
+
diagnostics,
|
|
326
|
+
repo,
|
|
327
|
+
"manifest.creation_task_missing",
|
|
328
|
+
"error",
|
|
329
|
+
"manifest has no exact AK creation-task provenance",
|
|
330
|
+
"agent.json",
|
|
331
|
+
);
|
|
332
|
+
} else if (!AGENT_CREATION_TASK_PATTERN.test(manifest.creation_task)) {
|
|
333
|
+
addFleetDiagnostic(
|
|
334
|
+
diagnostics,
|
|
335
|
+
repo,
|
|
336
|
+
"manifest.creation_task_invalid",
|
|
337
|
+
"error",
|
|
338
|
+
"creation_task must match AK-<positive integer>",
|
|
339
|
+
"agent.json",
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const profile = manifest
|
|
345
|
+
? await checkFleetSkills({
|
|
346
|
+
manifest,
|
|
347
|
+
ec: params.ec,
|
|
348
|
+
ecSnapshot: params.ecSnapshot,
|
|
349
|
+
agentSnapshot: snapshot,
|
|
350
|
+
diagnostics,
|
|
351
|
+
repo,
|
|
352
|
+
})
|
|
353
|
+
: undefined;
|
|
354
|
+
let prompt: FleetLintRepositoryResult["prompt"] = {
|
|
355
|
+
status: "unverifiable",
|
|
356
|
+
compilerContract: "ai-society.agent-prompt-compiler/1",
|
|
357
|
+
};
|
|
358
|
+
if (manifest && manifest.system_prompt_file !== FLEET_COMPILED_PROMPT_PATH) {
|
|
359
|
+
addFleetDiagnostic(
|
|
360
|
+
diagnostics,
|
|
361
|
+
repo,
|
|
362
|
+
"manifest.system_prompt_file_noncanonical",
|
|
363
|
+
"error",
|
|
364
|
+
`v2 fleet lint requires ${FLEET_COMPILED_PROMPT_PATH}; runtime declares a noncanonical path`,
|
|
365
|
+
"agent.json",
|
|
366
|
+
);
|
|
367
|
+
} else if (rawManifest) {
|
|
368
|
+
try {
|
|
369
|
+
const compiled = await compileFleetSystemPrompt({
|
|
370
|
+
manifestBytes: manifestFile.bytes,
|
|
371
|
+
readFile: async (path) => (await snapshot.readFile(path, MAX_PROMPT_INPUT_BYTES))?.bytes,
|
|
372
|
+
});
|
|
373
|
+
const actual = await snapshot.readFile(FLEET_COMPILED_PROMPT_PATH, MAX_PROMPT_INPUT_BYTES);
|
|
374
|
+
prompt = {
|
|
375
|
+
status: !actual
|
|
376
|
+
? "missing"
|
|
377
|
+
: actual.sha256 === compiled.expectedSha256
|
|
378
|
+
? "current"
|
|
379
|
+
: "stale",
|
|
380
|
+
...(actual ? { actualSha256: actual.sha256 } : {}),
|
|
381
|
+
expectedSha256: compiled.expectedSha256,
|
|
382
|
+
inputSha256: compiled.inputSha256,
|
|
383
|
+
compilerContract: "ai-society.agent-prompt-compiler/1",
|
|
384
|
+
};
|
|
385
|
+
if (!actual) {
|
|
386
|
+
addFleetDiagnostic(
|
|
387
|
+
diagnostics,
|
|
388
|
+
repo,
|
|
389
|
+
"prompt.compiled_missing",
|
|
390
|
+
"error",
|
|
391
|
+
"compiled system prompt is missing",
|
|
392
|
+
FLEET_COMPILED_PROMPT_PATH,
|
|
393
|
+
);
|
|
394
|
+
} else if (prompt.status === "stale") {
|
|
395
|
+
addFleetDiagnostic(
|
|
396
|
+
diagnostics,
|
|
397
|
+
repo,
|
|
398
|
+
"prompt.compiled_stale",
|
|
399
|
+
"error",
|
|
400
|
+
"compiled system prompt does not match canonical manifest/persona inputs",
|
|
401
|
+
FLEET_COMPILED_PROMPT_PATH,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
addFleetDiagnostic(
|
|
406
|
+
diagnostics,
|
|
407
|
+
repo,
|
|
408
|
+
"prompt.freshness_unproven",
|
|
409
|
+
"error",
|
|
410
|
+
"compiled prompt freshness could not be proven from bounded canonical inputs",
|
|
411
|
+
FLEET_COMPILED_PROMPT_PATH,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
await finalizeRepositorySnapshot({ snapshot, revision, diagnostics, repo });
|
|
417
|
+
revision.snapshotSha256 = fleetSha256(
|
|
418
|
+
JSON.stringify(
|
|
419
|
+
stableFleetValue({
|
|
420
|
+
commit: snapshot.commit,
|
|
421
|
+
treeOid: snapshot.treeOid,
|
|
422
|
+
statusSha256: snapshot.statusSha256,
|
|
423
|
+
manifestSha256: manifestFile.sha256,
|
|
424
|
+
profileSha256: params.ec.rawSha256,
|
|
425
|
+
promptActual: prompt.actualSha256 ?? null,
|
|
426
|
+
promptExpected: prompt.expectedSha256 ?? null,
|
|
427
|
+
templateAnswers: provenance.template.answersSha256 ?? null,
|
|
428
|
+
templateOwnership: provenance.template.ownershipSha256 ?? null,
|
|
429
|
+
}),
|
|
430
|
+
),
|
|
431
|
+
);
|
|
432
|
+
return {
|
|
433
|
+
repo,
|
|
434
|
+
repoName: basename(params.root),
|
|
435
|
+
revision,
|
|
436
|
+
manifest: {
|
|
437
|
+
present: true,
|
|
438
|
+
...(manifest ? { schema: manifest.schema, name: manifest.name } : {}),
|
|
439
|
+
...(fleetRole ? { role: fleetRole } : {}),
|
|
440
|
+
...(manifest?.creation_task ? { creationTask: manifest.creation_task } : {}),
|
|
441
|
+
blobOid: manifestFile.blobOid,
|
|
442
|
+
sha256: manifestFile.sha256,
|
|
443
|
+
},
|
|
444
|
+
...(profile ? { profile } : {}),
|
|
445
|
+
prompt,
|
|
446
|
+
template: provenance.template,
|
|
447
|
+
lifecycle,
|
|
448
|
+
diagnostics: sortFleetDiagnostics(diagnostics),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: immutable engineering-core profile and extra-skill binding diagnostics for fleet lint.
|
|
3
|
+
// read_when:
|
|
4
|
+
// - changing fleet profile conformance, profile-member capture, or committed extra-skill binding.
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import type { EcProfileSource } from "./ec-profiles.ts";
|
|
8
|
+
import type { CapturedGitFile, FleetGitSnapshot } from "./fleet-git-snapshot.ts";
|
|
9
|
+
import type { FleetLintDiagnostic, FleetLintRepositoryResult } from "./fleet-lint-types.ts";
|
|
10
|
+
import { addFleetDiagnostic } from "./fleet-lint-utils.ts";
|
|
11
|
+
import type { AgentManifest } from "./manifest.ts";
|
|
12
|
+
|
|
13
|
+
const MAX_SKILL_INPUT_BYTES = 512 * 1024;
|
|
14
|
+
|
|
15
|
+
export async function checkFleetSkills(params: {
|
|
16
|
+
manifest: AgentManifest;
|
|
17
|
+
ec: EcProfileSource;
|
|
18
|
+
ecSnapshot?: FleetGitSnapshot;
|
|
19
|
+
agentSnapshot: FleetGitSnapshot;
|
|
20
|
+
diagnostics: FleetLintDiagnostic[];
|
|
21
|
+
repo: string;
|
|
22
|
+
}): Promise<FleetLintRepositoryResult["profile"]> {
|
|
23
|
+
const requested = params.manifest.skills?.profile;
|
|
24
|
+
let resolved: string | undefined;
|
|
25
|
+
let status: NonNullable<FleetLintRepositoryResult["profile"]>["status"] = "none";
|
|
26
|
+
let members: string[] = [];
|
|
27
|
+
if (!requested) {
|
|
28
|
+
addFleetDiagnostic(
|
|
29
|
+
params.diagnostics,
|
|
30
|
+
params.repo,
|
|
31
|
+
"profile.missing",
|
|
32
|
+
"error",
|
|
33
|
+
"fleet lint requires one non-empty engineering-core skills.profile",
|
|
34
|
+
"agent.json",
|
|
35
|
+
"runtime may inspect a profile-less legacy manifest; current fleet conformance may not",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (requested) {
|
|
39
|
+
resolved = params.ec.deprecatedAliases.get(requested) ?? requested;
|
|
40
|
+
members = params.ec.profiles.get(resolved) ?? [];
|
|
41
|
+
if (params.ec.profiles.has(requested)) status = "canonical";
|
|
42
|
+
else if (params.ec.deprecatedAliases.has(requested)) {
|
|
43
|
+
status = "deprecated_alias";
|
|
44
|
+
addFleetDiagnostic(
|
|
45
|
+
params.diagnostics,
|
|
46
|
+
params.repo,
|
|
47
|
+
"profile.deprecated_alias",
|
|
48
|
+
"warning",
|
|
49
|
+
`profile ${requested} is a deprecated alias for ${resolved}`,
|
|
50
|
+
"agent.json",
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
status = "unknown";
|
|
54
|
+
addFleetDiagnostic(
|
|
55
|
+
params.diagnostics,
|
|
56
|
+
params.repo,
|
|
57
|
+
"profile.unknown",
|
|
58
|
+
"error",
|
|
59
|
+
"manifest references an unknown engineering-core profile",
|
|
60
|
+
"agent.json",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const skill of members) {
|
|
66
|
+
const path = `skills/${skill}/SKILL.md`;
|
|
67
|
+
let captured: CapturedGitFile | undefined;
|
|
68
|
+
try {
|
|
69
|
+
captured = params.ecSnapshot
|
|
70
|
+
? await params.ecSnapshot.readFile(path, MAX_SKILL_INPUT_BYTES)
|
|
71
|
+
: undefined;
|
|
72
|
+
} catch {
|
|
73
|
+
addFleetDiagnostic(
|
|
74
|
+
params.diagnostics,
|
|
75
|
+
params.repo,
|
|
76
|
+
"profile.member_capture_failed",
|
|
77
|
+
"error",
|
|
78
|
+
"a committed profile member could not be captured within the lint bound",
|
|
79
|
+
path,
|
|
80
|
+
);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!captured) {
|
|
84
|
+
addFleetDiagnostic(
|
|
85
|
+
params.diagnostics,
|
|
86
|
+
params.repo,
|
|
87
|
+
"profile.member_missing",
|
|
88
|
+
"error",
|
|
89
|
+
`profile ${requested} references missing committed skill ${skill}`,
|
|
90
|
+
path,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const skill of params.manifest.skills?.extra ?? []) {
|
|
95
|
+
const agentPath = `.pi/skills/${skill}/SKILL.md`;
|
|
96
|
+
const ecPath = `skills/${skill}/SKILL.md`;
|
|
97
|
+
let bound: CapturedGitFile | undefined;
|
|
98
|
+
try {
|
|
99
|
+
bound =
|
|
100
|
+
(await params.agentSnapshot.readFile(agentPath, MAX_SKILL_INPUT_BYTES)) ??
|
|
101
|
+
(params.ecSnapshot
|
|
102
|
+
? await params.ecSnapshot.readFile(ecPath, MAX_SKILL_INPUT_BYTES)
|
|
103
|
+
: undefined);
|
|
104
|
+
} catch {
|
|
105
|
+
addFleetDiagnostic(
|
|
106
|
+
params.diagnostics,
|
|
107
|
+
params.repo,
|
|
108
|
+
"skill.extra_capture_failed",
|
|
109
|
+
"error",
|
|
110
|
+
"a committed extra-skill source could not be captured within the lint bound",
|
|
111
|
+
"agent.json",
|
|
112
|
+
);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!bound) {
|
|
116
|
+
addFleetDiagnostic(
|
|
117
|
+
params.diagnostics,
|
|
118
|
+
params.repo,
|
|
119
|
+
"skill.extra_revision_unbound",
|
|
120
|
+
"error",
|
|
121
|
+
`extra skill ${skill} is not bound in the committed agent or engineering-core snapshot`,
|
|
122
|
+
"agent.json",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
...(requested && status !== "unknown" ? { requested } : {}),
|
|
128
|
+
...(resolved && status !== "unknown" ? { resolved } : {}),
|
|
129
|
+
status,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: versioned non-authorizing fleet-lint report and stable diagnostic vocabulary.
|
|
3
|
+
// read_when:
|
|
4
|
+
// - changing fleet lint machine output, severity semantics, immutable observations, or lifecycle claims.
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
export const AGENT_FLEET_LINT_SCHEMA = "ai-society.agent-fleet-lint/1";
|
|
8
|
+
export type FleetLintSeverity = "error" | "warning" | "info";
|
|
9
|
+
|
|
10
|
+
export interface FleetLintDiagnostic {
|
|
11
|
+
code: string;
|
|
12
|
+
severity: FleetLintSeverity;
|
|
13
|
+
repo: string;
|
|
14
|
+
path?: string;
|
|
15
|
+
message: string;
|
|
16
|
+
hint?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FleetLintRepositoryRevision {
|
|
20
|
+
commit?: string;
|
|
21
|
+
treeOid?: string;
|
|
22
|
+
status: "clean_observed" | "dirty" | "invalid" | "concurrent_change";
|
|
23
|
+
statusSha256?: string;
|
|
24
|
+
snapshotSha256?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FleetLintRepositoryResult {
|
|
28
|
+
repo: string;
|
|
29
|
+
repoName: string;
|
|
30
|
+
revision: FleetLintRepositoryRevision;
|
|
31
|
+
manifest: {
|
|
32
|
+
present: boolean;
|
|
33
|
+
schema?: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
role?: string;
|
|
36
|
+
creationTask?: string;
|
|
37
|
+
blobOid?: string;
|
|
38
|
+
sha256?: string;
|
|
39
|
+
};
|
|
40
|
+
profile?: {
|
|
41
|
+
requested?: string;
|
|
42
|
+
resolved?: string;
|
|
43
|
+
status: "canonical" | "deprecated_alias" | "unknown" | "none";
|
|
44
|
+
};
|
|
45
|
+
prompt: {
|
|
46
|
+
status: "current" | "stale" | "missing" | "unverifiable";
|
|
47
|
+
actualSha256?: string;
|
|
48
|
+
expectedSha256?: string;
|
|
49
|
+
inputSha256?: string;
|
|
50
|
+
compilerContract: string;
|
|
51
|
+
};
|
|
52
|
+
template: {
|
|
53
|
+
mode: "managed_v2" | "legacy" | "unknown";
|
|
54
|
+
provenanceStatus: "verified_local_source" | "unbound" | "invalid";
|
|
55
|
+
sourcePath?: string;
|
|
56
|
+
sourceRevision?: string;
|
|
57
|
+
sourceTreeOid?: string;
|
|
58
|
+
answersSha256?: string;
|
|
59
|
+
ownershipSha256?: string;
|
|
60
|
+
};
|
|
61
|
+
lifecycle: {
|
|
62
|
+
signal: "recent_activity" | "stale_candidate" | "unknown";
|
|
63
|
+
latestActivityAt?: string;
|
|
64
|
+
authorityEffect: "none";
|
|
65
|
+
};
|
|
66
|
+
diagnostics: FleetLintDiagnostic[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface FleetLintCollision {
|
|
70
|
+
kind: "name" | "role";
|
|
71
|
+
normalizedValue: string;
|
|
72
|
+
repositories: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface AgentFleetLintReport {
|
|
76
|
+
schema: typeof AGENT_FLEET_LINT_SCHEMA;
|
|
77
|
+
kind: "immutable_observation";
|
|
78
|
+
authorityEffect: "none";
|
|
79
|
+
observedAt: string;
|
|
80
|
+
roots: string[];
|
|
81
|
+
profileSource: {
|
|
82
|
+
path: string;
|
|
83
|
+
schema: string;
|
|
84
|
+
rawSha256: string;
|
|
85
|
+
commit?: string;
|
|
86
|
+
blobOid?: string;
|
|
87
|
+
committedSha256?: string;
|
|
88
|
+
status: "bound" | "dirty" | "invalid";
|
|
89
|
+
};
|
|
90
|
+
policy: {
|
|
91
|
+
staleAfterDays: number;
|
|
92
|
+
lifecycleAuthority: "advisory_signal_only";
|
|
93
|
+
dispatchPosture: "fleet_phase_0_disabled";
|
|
94
|
+
};
|
|
95
|
+
repositories: FleetLintRepositoryResult[];
|
|
96
|
+
collisions: FleetLintCollision[];
|
|
97
|
+
diagnostics: FleetLintDiagnostic[];
|
|
98
|
+
summary: {
|
|
99
|
+
status: "healthy" | "unhealthy";
|
|
100
|
+
candidateRepositories: number;
|
|
101
|
+
includedRepositories: number;
|
|
102
|
+
omittedRepositories: number;
|
|
103
|
+
manifests: number;
|
|
104
|
+
errors: number;
|
|
105
|
+
warnings: number;
|
|
106
|
+
infos: number;
|
|
107
|
+
recentActivitySignals: number;
|
|
108
|
+
staleCandidateSignals: number;
|
|
109
|
+
unknownLifecycleSignals: number;
|
|
110
|
+
};
|
|
111
|
+
reportSha256: string;
|
|
112
|
+
stateSha256: string;
|
|
113
|
+
}
|