@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,66 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: deterministic digest, diagnostic, and logical-identity helpers shared by fleet lint modules.
|
|
3
|
+
// read_when:
|
|
4
|
+
// - changing fleet report hashing, diagnostic ordering, or logical repository identity.
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { basename, dirname } from "node:path";
|
|
9
|
+
import type { FleetLintDiagnostic } from "./fleet-lint-types.ts";
|
|
10
|
+
|
|
11
|
+
export function fleetSha256(value: string | Buffer): string {
|
|
12
|
+
return createHash("sha256").update(value).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function stableFleetValue(value: unknown): unknown {
|
|
16
|
+
if (Array.isArray(value)) return value.map(stableFleetValue);
|
|
17
|
+
if (typeof value !== "object" || value === null) return value;
|
|
18
|
+
return Object.fromEntries(
|
|
19
|
+
Object.entries(value as Record<string, unknown>)
|
|
20
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
21
|
+
.map(([key, entry]) => [key, stableFleetValue(entry)]),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function addFleetDiagnostic(
|
|
26
|
+
target: FleetLintDiagnostic[],
|
|
27
|
+
repo: string,
|
|
28
|
+
code: string,
|
|
29
|
+
severity: FleetLintDiagnostic["severity"],
|
|
30
|
+
message: string,
|
|
31
|
+
path?: string,
|
|
32
|
+
hint?: string,
|
|
33
|
+
): void {
|
|
34
|
+
target.push({
|
|
35
|
+
code,
|
|
36
|
+
severity,
|
|
37
|
+
repo,
|
|
38
|
+
...(path ? { path } : {}),
|
|
39
|
+
message,
|
|
40
|
+
...(hint ? { hint } : {}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function sortFleetDiagnostics(values: FleetLintDiagnostic[]): FleetLintDiagnostic[] {
|
|
45
|
+
const compare = (left: string, right: string) => (left < right ? -1 : left > right ? 1 : 0);
|
|
46
|
+
return values.sort((a, b) =>
|
|
47
|
+
compare(
|
|
48
|
+
[a.repo, a.code, a.path ?? "", a.message].join("\0"),
|
|
49
|
+
[b.repo, b.code, b.path ?? "", b.message].join("\0"),
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function logicalFleetPath(root: string): string {
|
|
55
|
+
const parent = basename(dirname(root)) || "root";
|
|
56
|
+
const leaf = basename(root) || "unknown";
|
|
57
|
+
return `${parent}/${leaf}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function logicalFleetRepo(root: string): string {
|
|
61
|
+
return logicalFleetPath(root);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function logicalFleetRoot(root: string): string {
|
|
65
|
+
return logicalFleetPath(root);
|
|
66
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: aggregate, immutable-observation fleet lint orchestration and deterministic report digest.
|
|
3
|
+
// read_when:
|
|
4
|
+
// - changing agent_registry lint, fleet aggregation, profile baselines, collisions, or report identity.
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import { basename, dirname, relative } from "node:path";
|
|
8
|
+
import { EC_PROFILE_SCHEMA, type EcProfileSource, loadEcProfiles } from "./ec-profiles.ts";
|
|
9
|
+
import { captureFleetGitSnapshot, type FleetGitSnapshot } from "./fleet-git-snapshot.ts";
|
|
10
|
+
import { normalizeFleetRole } from "./fleet-lint-provenance.ts";
|
|
11
|
+
import { invalidFleetRepositoryResult, lintFleetRepository } from "./fleet-lint-repository.ts";
|
|
12
|
+
import {
|
|
13
|
+
AGENT_FLEET_LINT_SCHEMA,
|
|
14
|
+
type AgentFleetLintReport,
|
|
15
|
+
type FleetLintCollision,
|
|
16
|
+
type FleetLintDiagnostic,
|
|
17
|
+
type FleetLintRepositoryResult,
|
|
18
|
+
} from "./fleet-lint-types.ts";
|
|
19
|
+
import {
|
|
20
|
+
addFleetDiagnostic,
|
|
21
|
+
fleetSha256,
|
|
22
|
+
logicalFleetRoot,
|
|
23
|
+
sortFleetDiagnostics,
|
|
24
|
+
stableFleetValue,
|
|
25
|
+
} from "./fleet-lint-utils.ts";
|
|
26
|
+
import { AGENT_REGISTRY_ROOTS_ENV, defaultRegistryRoots, expandTildePath } from "./registry.ts";
|
|
27
|
+
import { discoverAgentRepositories, RegistryDiscoveryError } from "./registry-discovery.ts";
|
|
28
|
+
|
|
29
|
+
const DEFAULT_STALE_AFTER_DAYS = 90;
|
|
30
|
+
const DEFAULT_MAX_REPOSITORIES = 5_000;
|
|
31
|
+
const RFC3339_TIMESTAMP =
|
|
32
|
+
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/u;
|
|
33
|
+
|
|
34
|
+
export interface FleetLintOptions {
|
|
35
|
+
roots?: string[];
|
|
36
|
+
ec?: EcProfileSource;
|
|
37
|
+
observedAt?: string;
|
|
38
|
+
staleAfterDays?: number;
|
|
39
|
+
maxRepositories?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class FleetLintInfrastructureError extends Error {
|
|
43
|
+
readonly code: string;
|
|
44
|
+
|
|
45
|
+
constructor(code: string, message: string) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "FleetLintInfrastructureError";
|
|
48
|
+
this.code = code;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function defaultFleetLintRoots(): string[] {
|
|
53
|
+
return process.env[AGENT_REGISTRY_ROOTS_ENV]?.trim()
|
|
54
|
+
? defaultRegistryRoots()
|
|
55
|
+
: [expandTildePath("~/ai-society/agents/agent-*")];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function digestValue(value: unknown): string {
|
|
59
|
+
return fleetSha256(JSON.stringify(stableFleetValue(value)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function canonicalObservedAt(value: string): { text: string; date: Date } {
|
|
63
|
+
const match = RFC3339_TIMESTAMP.exec(value);
|
|
64
|
+
if (!match) {
|
|
65
|
+
throw new FleetLintInfrastructureError(
|
|
66
|
+
"fleet.options_invalid",
|
|
67
|
+
"observedAt must be one RFC3339 timestamp",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText, zoneHour, zoneMinute] =
|
|
71
|
+
match;
|
|
72
|
+
const year = Number(yearText);
|
|
73
|
+
const month = Number(monthText);
|
|
74
|
+
const day = Number(dayText);
|
|
75
|
+
const hour = Number(hourText);
|
|
76
|
+
const minute = Number(minuteText);
|
|
77
|
+
const second = Number(secondText);
|
|
78
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
79
|
+
const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
80
|
+
const validFields =
|
|
81
|
+
month >= 1 &&
|
|
82
|
+
month <= 12 &&
|
|
83
|
+
day >= 1 &&
|
|
84
|
+
day <= (daysInMonth[month - 1] ?? 0) &&
|
|
85
|
+
hour <= 23 &&
|
|
86
|
+
minute <= 59 &&
|
|
87
|
+
second <= 59 &&
|
|
88
|
+
(zoneHour === undefined || Number(zoneHour) <= 23) &&
|
|
89
|
+
(zoneMinute === undefined || Number(zoneMinute) <= 59);
|
|
90
|
+
const date = new Date(value);
|
|
91
|
+
if (!validFields || !Number.isFinite(date.getTime())) {
|
|
92
|
+
throw new FleetLintInfrastructureError(
|
|
93
|
+
"fleet.options_invalid",
|
|
94
|
+
"observedAt must be one valid RFC3339 timestamp",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return { text: date.toISOString(), date };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function logicalProfileSourcePath(ec: EcProfileSource): string {
|
|
101
|
+
const repository = basename(dirname(ec.skillsRoot)) || "root";
|
|
102
|
+
const filename = basename(ec.path) || "profiles.json";
|
|
103
|
+
return `${repository}/skills/${filename}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function captureProfileSource(ec: EcProfileSource, diagnostics: FleetLintDiagnostic[]) {
|
|
107
|
+
const repo = "engineering-core/skills-profiles";
|
|
108
|
+
try {
|
|
109
|
+
const snapshot = await captureFleetGitSnapshot(dirname(ec.skillsRoot));
|
|
110
|
+
const profilePath = relative(snapshot.root, ec.path).split("\\").join("/");
|
|
111
|
+
const file = await snapshot.readFile(profilePath, 2 * 1024 * 1024);
|
|
112
|
+
if (!file) throw new Error(`committed profile source is missing: ${profilePath}`);
|
|
113
|
+
const status =
|
|
114
|
+
snapshot.status === "clean_observed" && file.sha256 === ec.rawSha256 ? "bound" : "dirty";
|
|
115
|
+
if (status !== "bound") {
|
|
116
|
+
addFleetDiagnostic(
|
|
117
|
+
diagnostics,
|
|
118
|
+
repo,
|
|
119
|
+
status === "dirty" ? "profile.source_dirty" : "profile.source_unstable",
|
|
120
|
+
"error",
|
|
121
|
+
"engineering-core profile bytes are not one clean stable committed observation",
|
|
122
|
+
profilePath,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
snapshot,
|
|
127
|
+
profile: {
|
|
128
|
+
path: logicalProfileSourcePath(ec),
|
|
129
|
+
schema: ec.schema,
|
|
130
|
+
rawSha256: ec.rawSha256,
|
|
131
|
+
commit: snapshot.commit,
|
|
132
|
+
blobOid: file.blobOid,
|
|
133
|
+
committedSha256: file.sha256,
|
|
134
|
+
status,
|
|
135
|
+
} satisfies AgentFleetLintReport["profileSource"],
|
|
136
|
+
};
|
|
137
|
+
} catch {
|
|
138
|
+
addFleetDiagnostic(
|
|
139
|
+
diagnostics,
|
|
140
|
+
repo,
|
|
141
|
+
"profile.source_invalid",
|
|
142
|
+
"error",
|
|
143
|
+
"engineering-core profile bytes could not be bound to one committed Git snapshot",
|
|
144
|
+
logicalProfileSourcePath(ec),
|
|
145
|
+
);
|
|
146
|
+
return {
|
|
147
|
+
snapshot: undefined as FleetGitSnapshot | undefined,
|
|
148
|
+
profile: {
|
|
149
|
+
path: logicalProfileSourcePath(ec),
|
|
150
|
+
schema: ec.schema,
|
|
151
|
+
rawSha256: ec.rawSha256,
|
|
152
|
+
status: "invalid",
|
|
153
|
+
} satisfies AgentFleetLintReport["profileSource"],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function addCollisions(repositories: FleetLintRepositoryResult[]): FleetLintCollision[] {
|
|
159
|
+
const collisions: FleetLintCollision[] = [];
|
|
160
|
+
for (const [kind, values] of [
|
|
161
|
+
["name", repositories.map((entry) => [entry.manifest.name, entry] as const)],
|
|
162
|
+
["role", repositories.map((entry) => [entry.manifest.role, entry] as const)],
|
|
163
|
+
] as const) {
|
|
164
|
+
const index = new Map<string, FleetLintRepositoryResult[]>();
|
|
165
|
+
for (const [value, repository] of values) {
|
|
166
|
+
if (!value) continue;
|
|
167
|
+
const normalized = kind === "role" ? normalizeFleetRole(value) : value;
|
|
168
|
+
index.set(normalized, [...(index.get(normalized) ?? []), repository]);
|
|
169
|
+
}
|
|
170
|
+
for (const [normalizedValue, matches] of index) {
|
|
171
|
+
if (matches.length < 2) continue;
|
|
172
|
+
const repos = matches.map((entry) => entry.repo).sort();
|
|
173
|
+
collisions.push({ kind, normalizedValue, repositories: repos });
|
|
174
|
+
for (const entry of matches) {
|
|
175
|
+
addFleetDiagnostic(
|
|
176
|
+
entry.diagnostics,
|
|
177
|
+
entry.repo,
|
|
178
|
+
`${kind}.exact_collision`,
|
|
179
|
+
"error",
|
|
180
|
+
`${kind} collides exactly after normalization across: ${repos.join(", ")}`,
|
|
181
|
+
"agent.json",
|
|
182
|
+
"semantic differentiation remains an owner review; lint proves only this exact collision",
|
|
183
|
+
);
|
|
184
|
+
sortFleetDiagnostics(entry.diagnostics);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return collisions.sort((a, b) => {
|
|
189
|
+
const left = `${a.kind}\0${a.normalizedValue}`;
|
|
190
|
+
const right = `${b.kind}\0${b.normalizedValue}`;
|
|
191
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function lintAgentFleet(
|
|
196
|
+
options: FleetLintOptions = {},
|
|
197
|
+
): Promise<AgentFleetLintReport> {
|
|
198
|
+
const observation = canonicalObservedAt(options.observedAt ?? new Date().toISOString());
|
|
199
|
+
const observedAt = observation.text;
|
|
200
|
+
const observedDate = observation.date;
|
|
201
|
+
const staleAfterDays = options.staleAfterDays ?? DEFAULT_STALE_AFTER_DAYS;
|
|
202
|
+
if (!Number.isSafeInteger(staleAfterDays) || staleAfterDays <= 0) {
|
|
203
|
+
throw new FleetLintInfrastructureError(
|
|
204
|
+
"fleet.options_invalid",
|
|
205
|
+
"staleAfterDays must be a positive safe integer",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const roots = options.roots ?? defaultFleetLintRoots();
|
|
209
|
+
let ec: EcProfileSource;
|
|
210
|
+
try {
|
|
211
|
+
ec = options.ec ?? (await loadEcProfiles());
|
|
212
|
+
} catch {
|
|
213
|
+
throw new FleetLintInfrastructureError(
|
|
214
|
+
"profile.source_load_failed",
|
|
215
|
+
"engineering-core skill profile source could not be loaded",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const diagnostics: FleetLintDiagnostic[] = [];
|
|
219
|
+
if (ec.schema !== EC_PROFILE_SCHEMA) {
|
|
220
|
+
addFleetDiagnostic(
|
|
221
|
+
diagnostics,
|
|
222
|
+
"engineering-core/skills-profiles",
|
|
223
|
+
"profile.legacy_schema",
|
|
224
|
+
"error",
|
|
225
|
+
`fleet lint requires ${EC_PROFILE_SCHEMA}; legacy compatibility is runtime-only`,
|
|
226
|
+
logicalProfileSourcePath(ec),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const profileSource = await captureProfileSource(ec, diagnostics);
|
|
230
|
+
let discovered: Awaited<ReturnType<typeof discoverAgentRepositories>>;
|
|
231
|
+
try {
|
|
232
|
+
discovered = await discoverAgentRepositories(
|
|
233
|
+
roots,
|
|
234
|
+
true,
|
|
235
|
+
options.maxRepositories ?? DEFAULT_MAX_REPOSITORIES,
|
|
236
|
+
);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error instanceof RegistryDiscoveryError) {
|
|
239
|
+
throw new FleetLintInfrastructureError(error.code, error.message);
|
|
240
|
+
}
|
|
241
|
+
throw new FleetLintInfrastructureError(
|
|
242
|
+
"fleet.discovery_failed",
|
|
243
|
+
"fleet repository discovery failed unexpectedly",
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (discovered.omittedCount > 0) {
|
|
247
|
+
addFleetDiagnostic(
|
|
248
|
+
diagnostics,
|
|
249
|
+
"fleet",
|
|
250
|
+
"fleet.repository_limit_exceeded",
|
|
251
|
+
"error",
|
|
252
|
+
`${discovered.omittedCount} candidate repositories were omitted by the bound`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const repositories: FleetLintRepositoryResult[] = [];
|
|
257
|
+
for (const failure of discovered.failures) {
|
|
258
|
+
const repo = logicalFleetRoot(failure.root);
|
|
259
|
+
const repositoryDiagnostics: FleetLintDiagnostic[] = [];
|
|
260
|
+
addFleetDiagnostic(
|
|
261
|
+
repositoryDiagnostics,
|
|
262
|
+
repo,
|
|
263
|
+
failure.code,
|
|
264
|
+
"error",
|
|
265
|
+
"candidate repository could not be resolved after bounded discovery",
|
|
266
|
+
undefined,
|
|
267
|
+
"later fleet candidates were still inspected",
|
|
268
|
+
);
|
|
269
|
+
repositories.push(invalidFleetRepositoryResult(failure.root, false, repositoryDiagnostics));
|
|
270
|
+
}
|
|
271
|
+
for (const candidate of discovered.repositories) {
|
|
272
|
+
try {
|
|
273
|
+
repositories.push(
|
|
274
|
+
await lintFleetRepository({
|
|
275
|
+
root: candidate.root,
|
|
276
|
+
manifestPresent: candidate.manifestPresent,
|
|
277
|
+
ec,
|
|
278
|
+
ecSnapshot: profileSource.snapshot,
|
|
279
|
+
observedAt: observedDate,
|
|
280
|
+
staleAfterDays,
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
} catch {
|
|
284
|
+
const repositoryDiagnostics: FleetLintDiagnostic[] = [];
|
|
285
|
+
const repo = logicalFleetRoot(candidate.root);
|
|
286
|
+
addFleetDiagnostic(
|
|
287
|
+
repositoryDiagnostics,
|
|
288
|
+
repo,
|
|
289
|
+
"repository.lint_failed",
|
|
290
|
+
"error",
|
|
291
|
+
"repository lint failed unexpectedly inside its aggregate isolation boundary",
|
|
292
|
+
undefined,
|
|
293
|
+
"one repository failed bounded capture; later fleet candidates were still inspected",
|
|
294
|
+
);
|
|
295
|
+
repositories.push(
|
|
296
|
+
invalidFleetRepositoryResult(
|
|
297
|
+
candidate.root,
|
|
298
|
+
candidate.manifestPresent,
|
|
299
|
+
repositoryDiagnostics,
|
|
300
|
+
),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (profileSource.snapshot) {
|
|
305
|
+
try {
|
|
306
|
+
const finalProfileState = await profileSource.snapshot.finish();
|
|
307
|
+
if (!finalProfileState.stable) {
|
|
308
|
+
profileSource.profile.status = "invalid";
|
|
309
|
+
addFleetDiagnostic(
|
|
310
|
+
diagnostics,
|
|
311
|
+
"engineering-core/skills-profiles",
|
|
312
|
+
"profile.source_concurrent_change",
|
|
313
|
+
"error",
|
|
314
|
+
"engineering-core profile HEAD or worktree status changed during fleet capture",
|
|
315
|
+
logicalProfileSourcePath(ec),
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
profileSource.profile.status = "invalid";
|
|
320
|
+
addFleetDiagnostic(
|
|
321
|
+
diagnostics,
|
|
322
|
+
"engineering-core/skills-profiles",
|
|
323
|
+
"profile.source_finalize_failed",
|
|
324
|
+
"error",
|
|
325
|
+
"engineering-core profile endpoint stability could not be verified after fleet capture",
|
|
326
|
+
logicalProfileSourcePath(ec),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
repositories.sort((a, b) => (a.repo < b.repo ? -1 : a.repo > b.repo ? 1 : 0));
|
|
331
|
+
const collisions = addCollisions(repositories);
|
|
332
|
+
const allDiagnostics = sortFleetDiagnostics([
|
|
333
|
+
...diagnostics,
|
|
334
|
+
...repositories.flatMap((entry) => entry.diagnostics),
|
|
335
|
+
]);
|
|
336
|
+
const count = (severity: FleetLintDiagnostic["severity"]) =>
|
|
337
|
+
allDiagnostics.filter((entry) => entry.severity === severity).length;
|
|
338
|
+
const state: Omit<AgentFleetLintReport, "reportSha256" | "stateSha256" | "observedAt"> = {
|
|
339
|
+
schema: AGENT_FLEET_LINT_SCHEMA,
|
|
340
|
+
kind: "immutable_observation",
|
|
341
|
+
authorityEffect: "none",
|
|
342
|
+
roots: roots.map(logicalFleetRoot),
|
|
343
|
+
profileSource: profileSource.profile,
|
|
344
|
+
policy: {
|
|
345
|
+
staleAfterDays,
|
|
346
|
+
lifecycleAuthority: "advisory_signal_only",
|
|
347
|
+
dispatchPosture: "fleet_phase_0_disabled",
|
|
348
|
+
},
|
|
349
|
+
repositories,
|
|
350
|
+
collisions,
|
|
351
|
+
diagnostics: allDiagnostics,
|
|
352
|
+
summary: {
|
|
353
|
+
status: count("error") > 0 ? "unhealthy" : "healthy",
|
|
354
|
+
candidateRepositories:
|
|
355
|
+
discovered.repositories.length + discovered.failures.length + discovered.omittedCount,
|
|
356
|
+
includedRepositories: repositories.length,
|
|
357
|
+
omittedRepositories: discovered.omittedCount,
|
|
358
|
+
manifests: repositories.filter((entry) => entry.manifest.present).length,
|
|
359
|
+
errors: count("error"),
|
|
360
|
+
warnings: count("warning"),
|
|
361
|
+
infos: count("info"),
|
|
362
|
+
recentActivitySignals: repositories.filter(
|
|
363
|
+
(entry) => entry.lifecycle.signal === "recent_activity",
|
|
364
|
+
).length,
|
|
365
|
+
staleCandidateSignals: repositories.filter(
|
|
366
|
+
(entry) => entry.lifecycle.signal === "stale_candidate",
|
|
367
|
+
).length,
|
|
368
|
+
unknownLifecycleSignals: repositories.filter((entry) => entry.lifecycle.signal === "unknown")
|
|
369
|
+
.length,
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
const stateSha256 = digestValue(state);
|
|
373
|
+
const complete = { ...state, observedAt, stateSha256 };
|
|
374
|
+
return { ...complete, reportSha256: digestValue(complete) };
|
|
375
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: trusted byte-for-byte reconstruction of the ratified tpl-agent-repo v2 system-prompt compiler contract.
|
|
3
|
+
// read_when:
|
|
4
|
+
// - changing fleet prompt freshness, canonical persona inputs, or compiler provenance.
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
export const FLEET_PERSONA_FILES = [
|
|
10
|
+
"README.md",
|
|
11
|
+
"identity.md",
|
|
12
|
+
"reason.md",
|
|
13
|
+
"main_task.md",
|
|
14
|
+
"dream_goal.md",
|
|
15
|
+
"behavior_rules.md",
|
|
16
|
+
] as const;
|
|
17
|
+
export const FLEET_PERSONA_DIR = "docs/person";
|
|
18
|
+
export const FLEET_COMPILED_PROMPT_PATH = "docs/person/system-prompt.md";
|
|
19
|
+
export const FLEET_PROMPT_COMPILER_CONTRACT = "ai-society.agent-prompt-compiler/1";
|
|
20
|
+
|
|
21
|
+
export interface FleetPromptCompileResult {
|
|
22
|
+
expected: Buffer;
|
|
23
|
+
expectedSha256: string;
|
|
24
|
+
inputSha256: string;
|
|
25
|
+
inputPaths: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class FleetPromptCompilerError extends Error {
|
|
29
|
+
constructor(message: string) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "FleetPromptCompilerError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sha256(value: string | Buffer): string {
|
|
36
|
+
return createHash("sha256").update(value).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function decodeUtf8(bytes: Buffer, path: string): string {
|
|
40
|
+
try {
|
|
41
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
42
|
+
} catch {
|
|
43
|
+
throw new FleetPromptCompilerError(`canonical prompt input is not UTF-8: ${path}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compareCodePoints(left: string, right: string): number {
|
|
48
|
+
const leftPoints = [...left].map((value) => value.codePointAt(0) ?? -1);
|
|
49
|
+
const rightPoints = [...right].map((value) => value.codePointAt(0) ?? -1);
|
|
50
|
+
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index += 1) {
|
|
51
|
+
if (leftPoints[index] !== rightPoints[index]) return leftPoints[index] - rightPoints[index];
|
|
52
|
+
}
|
|
53
|
+
return leftPoints.length - rightPoints.length;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasUnpairedSurrogate(value: string): boolean {
|
|
57
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
58
|
+
const code = value.charCodeAt(index);
|
|
59
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
60
|
+
const next = value.charCodeAt(index + 1);
|
|
61
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return true;
|
|
62
|
+
index += 1;
|
|
63
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertPortableJson(value: unknown, path = "$manifest"): void {
|
|
71
|
+
if (typeof value === "string" && hasUnpairedSurrogate(value)) {
|
|
72
|
+
throw new FleetPromptCompilerError(
|
|
73
|
+
`unpaired surrogate cannot be encoded by the Python compiler: ${path}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === "number") {
|
|
77
|
+
throw new FleetPromptCompilerError(
|
|
78
|
+
`numeric additive manifest value cannot be proven byte-identical to the Python compiler: ${path}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
for (const [index, entry] of value.entries()) {
|
|
83
|
+
assertPortableJson(entry, `${path}[${index}]`);
|
|
84
|
+
}
|
|
85
|
+
} else if (typeof value === "object" && value !== null) {
|
|
86
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
87
|
+
if (hasUnpairedSurrogate(key)) {
|
|
88
|
+
throw new FleetPromptCompilerError(
|
|
89
|
+
`unpaired surrogate cannot be encoded by the Python compiler: ${path} key`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
assertPortableJson(entry, `${path}.${key}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sortJson(value: unknown): unknown {
|
|
98
|
+
if (Array.isArray(value)) return value.map(sortJson);
|
|
99
|
+
if (typeof value !== "object" || value === null) return value;
|
|
100
|
+
return Object.fromEntries(
|
|
101
|
+
Object.entries(value as Record<string, unknown>)
|
|
102
|
+
.sort(([left], [right]) => compareCodePoints(left, right))
|
|
103
|
+
.map(([key, entry]) => [key, sortJson(entry)]),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function canonicalInputDigest(entries: Array<{ path: string; sha256: string }>): string {
|
|
108
|
+
return sha256(JSON.stringify(entries.sort((a, b) => compareCodePoints(a.path, b.path))));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function compileFleetSystemPrompt(params: {
|
|
112
|
+
manifestBytes: Buffer;
|
|
113
|
+
readFile(path: string): Promise<Buffer | undefined>;
|
|
114
|
+
}): Promise<FleetPromptCompileResult> {
|
|
115
|
+
const manifestText = decodeUtf8(params.manifestBytes, "agent.json");
|
|
116
|
+
let manifest: unknown;
|
|
117
|
+
try {
|
|
118
|
+
manifest = JSON.parse(manifestText);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new FleetPromptCompilerError(
|
|
121
|
+
`agent.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
|
|
125
|
+
throw new FleetPromptCompilerError("agent.json root must be an object");
|
|
126
|
+
}
|
|
127
|
+
assertPortableJson(manifest);
|
|
128
|
+
|
|
129
|
+
const parts: string[] = [
|
|
130
|
+
"<!-- compiled: do not edit -->\n",
|
|
131
|
+
"# Agent system prompt\n\n",
|
|
132
|
+
"## Manifest\n\n",
|
|
133
|
+
"```json\n",
|
|
134
|
+
JSON.stringify(sortJson(manifest), null, 2),
|
|
135
|
+
"\n```\n",
|
|
136
|
+
];
|
|
137
|
+
const inputs = [{ path: "agent.json", sha256: sha256(params.manifestBytes) }];
|
|
138
|
+
const inputPaths = ["agent.json"];
|
|
139
|
+
for (const name of FLEET_PERSONA_FILES) {
|
|
140
|
+
const path = `${FLEET_PERSONA_DIR}/${name}`;
|
|
141
|
+
const bytes = await params.readFile(path);
|
|
142
|
+
if (!bytes) throw new FleetPromptCompilerError(`missing canonical prompt input: ${path}`);
|
|
143
|
+
const text = decodeUtf8(bytes, path).replace(/\r\n?/gu, "\n").replace(/\n+$/u, "");
|
|
144
|
+
parts.push(`\n## Persona source: ${name}\n\n`, text, "\n");
|
|
145
|
+
inputs.push({ path, sha256: sha256(bytes) });
|
|
146
|
+
inputPaths.push(path);
|
|
147
|
+
}
|
|
148
|
+
const expected = Buffer.from(parts.join(""), "utf8");
|
|
149
|
+
return {
|
|
150
|
+
expected,
|
|
151
|
+
expectedSha256: sha256(expected),
|
|
152
|
+
inputSha256: canonicalInputDigest(inputs),
|
|
153
|
+
inputPaths,
|
|
154
|
+
};
|
|
155
|
+
}
|