@pellux/goodvibes-agent 1.0.31 → 1.0.34
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/CHANGELOG.md +59 -2
- package/README.md +72 -58
- package/dist/package/main.js +7572 -2430
- package/docs/README.md +25 -21
- package/docs/channels-remote-and-api.md +6 -2
- package/docs/connected-host.md +27 -6
- package/docs/getting-started.md +87 -66
- package/docs/knowledge-artifacts-and-multimodal.md +16 -4
- package/docs/project-planning.md +2 -2
- package/docs/providers-and-routing.md +3 -2
- package/docs/release-and-publishing.md +15 -11
- package/docs/tools-and-commands.md +150 -128
- package/docs/voice-and-live-tts.md +1 -1
- package/package.json +8 -3
- package/release/live-verification/live-verification.json +148 -0
- package/release/live-verification/live-verification.md +187 -0
- package/release/performance-snapshot.json +57 -0
- package/release/release-notes.md +19 -0
- package/release/release-readiness.json +581 -0
- package/src/agent/harness-control.ts +42 -3
- package/src/cli/agent-knowledge-command.ts +5 -5
- package/src/cli/agent-knowledge-format.ts +11 -0
- package/src/cli/agent-knowledge-runtime.ts +92 -13
- package/src/cli/bundle-command.ts +5 -4
- package/src/cli/entrypoint.ts +5 -2
- package/src/cli/external-runtime.ts +2 -15
- package/src/cli/management.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/knowledge.ts +2 -2
- package/src/runtime/bootstrap.ts +10 -18
- package/src/runtime/connected-host-auth.ts +16 -0
- package/src/tools/agent-analysis-registry-policy.ts +2 -9
- package/src/tools/agent-channel-send-tool.ts +3 -9
- package/src/tools/agent-context-policy.ts +1 -5
- package/src/tools/agent-find-policy.ts +1 -4
- package/src/tools/agent-harness-channel-metadata.ts +177 -0
- package/src/tools/agent-harness-cli-metadata.ts +4 -1
- package/src/tools/agent-harness-command-catalog.ts +10 -3
- package/src/tools/agent-harness-connected-host-status.ts +9 -3
- package/src/tools/agent-harness-delegation-posture.ts +216 -0
- package/src/tools/agent-harness-keybinding-metadata.ts +57 -22
- package/src/tools/agent-harness-mcp-metadata.ts +248 -0
- package/src/tools/agent-harness-media-posture.ts +282 -0
- package/src/tools/agent-harness-metadata.ts +44 -9
- package/src/tools/agent-harness-model-routing.ts +501 -0
- package/src/tools/agent-harness-model-tool-catalog.ts +7 -2
- package/src/tools/agent-harness-notification-metadata.ts +217 -0
- package/src/tools/agent-harness-operator-methods.ts +285 -0
- package/src/tools/agent-harness-pairing-posture.ts +265 -0
- package/src/tools/agent-harness-panel-metadata.ts +26 -12
- package/src/tools/agent-harness-provider-account-metadata.ts +205 -0
- package/src/tools/agent-harness-release-evidence.ts +364 -0
- package/src/tools/agent-harness-release-readiness.ts +298 -0
- package/src/tools/agent-harness-security-posture.ts +648 -0
- package/src/tools/agent-harness-service-posture.ts +207 -0
- package/src/tools/agent-harness-session-metadata.ts +284 -0
- package/src/tools/agent-harness-setup-posture.ts +295 -0
- package/src/tools/agent-harness-tool-schema.ts +104 -27
- package/src/tools/agent-harness-tool.ts +251 -235
- package/src/tools/agent-harness-ui-surface-metadata.ts +20 -12
- package/src/tools/agent-harness-workspace-actions.ts +260 -0
- package/src/tools/agent-knowledge-ingest-tool.ts +4 -10
- package/src/tools/agent-knowledge-tool.ts +120 -25
- package/src/tools/agent-local-registry-tool.ts +3 -7
- package/src/tools/agent-media-generate-tool.ts +2 -8
- package/src/tools/agent-notify-tool.ts +3 -8
- package/src/tools/agent-operator-action-tool.ts +4 -10
- package/src/tools/agent-operator-briefing-tool.ts +1 -6
- package/src/tools/agent-read-policy.ts +1 -4
- package/src/tools/agent-reminder-schedule-tool.ts +4 -9
- package/src/tools/agent-tool-policy-guard.ts +15 -51
- package/src/tools/agent-web-search-policy.ts +1 -4
- package/src/tools/agent-work-plan-tool.ts +1 -6
- package/src/version.ts +2 -2
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const RELEASE_EVIDENCE_ARTIFACTS = [
|
|
5
|
+
{
|
|
6
|
+
id: 'release-notes',
|
|
7
|
+
path: 'release/release-notes.md',
|
|
8
|
+
kind: 'markdown',
|
|
9
|
+
description: 'Product-facing release notes for the current release evidence bundle.',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: 'performance-snapshot',
|
|
13
|
+
path: 'release/performance-snapshot.json',
|
|
14
|
+
kind: 'json',
|
|
15
|
+
description: 'Recorded performance snapshot used by the release performance gate.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'release-readiness',
|
|
19
|
+
path: 'release/release-readiness.json',
|
|
20
|
+
kind: 'json',
|
|
21
|
+
description: 'Release capability inventory with owner, evidence, and quality dimensions.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'live-verification-json',
|
|
25
|
+
path: 'release/live-verification/live-verification.json',
|
|
26
|
+
kind: 'json',
|
|
27
|
+
description: 'Machine-readable strict live verification report.',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'live-verification-markdown',
|
|
31
|
+
path: 'release/live-verification/live-verification.md',
|
|
32
|
+
kind: 'markdown',
|
|
33
|
+
description: 'Human-readable strict live verification report.',
|
|
34
|
+
},
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
type ReleaseEvidenceArtifactId = typeof RELEASE_EVIDENCE_ARTIFACTS[number]['id'];
|
|
38
|
+
|
|
39
|
+
interface ReleaseEvidenceArtifact {
|
|
40
|
+
readonly id: ReleaseEvidenceArtifactId;
|
|
41
|
+
readonly path: string;
|
|
42
|
+
readonly kind: 'json' | 'markdown';
|
|
43
|
+
readonly description: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ReleaseEvidenceArgs {
|
|
47
|
+
readonly query?: unknown;
|
|
48
|
+
readonly target?: unknown;
|
|
49
|
+
readonly artifactId?: unknown;
|
|
50
|
+
readonly limit?: unknown;
|
|
51
|
+
readonly includeParameters?: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ReleaseEvidenceLookup {
|
|
55
|
+
readonly source: 'artifactId' | 'target' | 'query';
|
|
56
|
+
readonly input: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type ReleaseEvidenceArtifactLoadResult =
|
|
60
|
+
| {
|
|
61
|
+
readonly status: 'available';
|
|
62
|
+
readonly artifact: ReleaseEvidenceArtifact;
|
|
63
|
+
readonly absolutePath: string;
|
|
64
|
+
readonly source: string;
|
|
65
|
+
readonly parsed?: unknown;
|
|
66
|
+
readonly sizeBytes: number;
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
readonly status: 'missing' | 'invalid';
|
|
70
|
+
readonly artifact: ReleaseEvidenceArtifact;
|
|
71
|
+
readonly absolutePath: string;
|
|
72
|
+
readonly reason: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type ReleaseEvidenceArtifactResolution =
|
|
76
|
+
| {
|
|
77
|
+
readonly status: 'found';
|
|
78
|
+
readonly lookup: ReleaseEvidenceLookup & { readonly resolvedBy: 'id' | 'case-insensitive-id' | 'path' | 'search' };
|
|
79
|
+
readonly artifact: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
| {
|
|
82
|
+
readonly status: 'ambiguous';
|
|
83
|
+
readonly input: string;
|
|
84
|
+
readonly candidates: readonly Record<string, unknown>[];
|
|
85
|
+
}
|
|
86
|
+
| {
|
|
87
|
+
readonly status: 'not_found' | 'missing_lookup';
|
|
88
|
+
readonly input?: string;
|
|
89
|
+
readonly total?: number;
|
|
90
|
+
readonly usage?: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function packageRoot(): string {
|
|
94
|
+
return join(import.meta.dir, '..', '..');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readString(value: unknown): string {
|
|
98
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readLimit(value: unknown, fallback: number): number {
|
|
102
|
+
const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
|
|
103
|
+
if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
|
|
104
|
+
return Math.max(1, Math.min(100, Math.trunc(parsed)));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
108
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
112
|
+
return isRecord(value) ? value : {};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalized(value: string): string {
|
|
116
|
+
return value.toLowerCase();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function artifactSearchText(artifact: ReleaseEvidenceArtifact): string {
|
|
120
|
+
return `${artifact.id}\n${artifact.path}\n${artifact.kind}\n${artifact.description}`.toLowerCase();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function evidenceLookupFromArgs(args: ReleaseEvidenceArgs): ReleaseEvidenceLookup | null {
|
|
124
|
+
const artifactId = readString(args.artifactId);
|
|
125
|
+
if (artifactId) return { source: 'artifactId', input: artifactId };
|
|
126
|
+
const target = readString(args.target);
|
|
127
|
+
if (target) return { source: 'target', input: target };
|
|
128
|
+
const query = readString(args.query);
|
|
129
|
+
return query ? { source: 'query', input: query } : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function loadArtifact(artifact: ReleaseEvidenceArtifact): ReleaseEvidenceArtifactLoadResult {
|
|
133
|
+
const absolutePath = join(packageRoot(), artifact.path);
|
|
134
|
+
if (!existsSync(absolutePath)) {
|
|
135
|
+
return {
|
|
136
|
+
status: 'missing',
|
|
137
|
+
artifact,
|
|
138
|
+
absolutePath,
|
|
139
|
+
reason: `${artifact.path} is not present in this Agent installation.`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const source = readFileSync(absolutePath, 'utf-8');
|
|
144
|
+
const sizeBytes = statSync(absolutePath).size;
|
|
145
|
+
if (artifact.kind === 'json') {
|
|
146
|
+
return {
|
|
147
|
+
status: 'available',
|
|
148
|
+
artifact,
|
|
149
|
+
absolutePath,
|
|
150
|
+
source,
|
|
151
|
+
parsed: JSON.parse(source) as unknown,
|
|
152
|
+
sizeBytes,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return { status: 'available', artifact, absolutePath, source, sizeBytes };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
158
|
+
return {
|
|
159
|
+
status: 'invalid',
|
|
160
|
+
artifact,
|
|
161
|
+
absolutePath,
|
|
162
|
+
reason: `${artifact.path} could not be read: ${message}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function firstHeading(source: string): string | undefined {
|
|
168
|
+
return source.split(/\r?\n/).find((line) => line.startsWith('# '))?.replace(/^#\s+/, '').trim() || undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function countMarkdownBullets(source: string): number {
|
|
172
|
+
return source.split(/\r?\n/).filter((line) => /^-\s+/.test(line)).length;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function countBy(items: readonly Record<string, unknown>[], key: string): Record<string, number> {
|
|
176
|
+
const counts: Record<string, number> = {};
|
|
177
|
+
for (const item of items) {
|
|
178
|
+
const value = readString(item[key]) || '<missing>';
|
|
179
|
+
counts[value] = (counts[value] ?? 0) + 1;
|
|
180
|
+
}
|
|
181
|
+
return Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function summarizeJsonArtifact(artifactId: ReleaseEvidenceArtifactId, parsed: unknown): Record<string, unknown> {
|
|
185
|
+
const root = asRecord(parsed);
|
|
186
|
+
if (artifactId === 'performance-snapshot') {
|
|
187
|
+
const surfacePerf = asRecord(root.surfacePerf);
|
|
188
|
+
const extraMetrics = asRecord(root.extraMetrics);
|
|
189
|
+
return {
|
|
190
|
+
budgetStatus: surfacePerf.budgetStatus,
|
|
191
|
+
targetBudgetMs: surfacePerf.targetBudgetMs,
|
|
192
|
+
overBudgetCount: surfacePerf.overBudgetCount,
|
|
193
|
+
extraMetrics: Object.keys(extraMetrics).length,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (artifactId === 'release-readiness') {
|
|
197
|
+
const items = Array.isArray(root.items) ? root.items.filter(isRecord) : [];
|
|
198
|
+
return {
|
|
199
|
+
gate: root.gate,
|
|
200
|
+
checkedAt: root.checkedAt,
|
|
201
|
+
items: items.length,
|
|
202
|
+
statuses: countBy(items, 'status'),
|
|
203
|
+
owners: countBy(items, 'owner'),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (artifactId === 'live-verification-json') {
|
|
207
|
+
return {
|
|
208
|
+
generatedAt: root.generatedAt,
|
|
209
|
+
strict: root.strict,
|
|
210
|
+
ok: root.ok,
|
|
211
|
+
counts: root.counts,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function summarizeMarkdownArtifact(artifactId: ReleaseEvidenceArtifactId, source: string): Record<string, unknown> {
|
|
218
|
+
if (artifactId === 'release-notes') {
|
|
219
|
+
return {
|
|
220
|
+
title: firstHeading(source),
|
|
221
|
+
bullets: countMarkdownBullets(source),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (artifactId === 'live-verification-markdown') {
|
|
225
|
+
const generated = source.match(/^Generated:\s*(.+)$/m)?.[1]?.trim();
|
|
226
|
+
const result = source.match(/^Result:\s*(.+)$/m)?.[1]?.trim();
|
|
227
|
+
return {
|
|
228
|
+
title: firstHeading(source),
|
|
229
|
+
generatedAt: generated,
|
|
230
|
+
result,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { title: firstHeading(source) };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function summarizeLoadedArtifact(
|
|
237
|
+
loaded: ReleaseEvidenceArtifactLoadResult,
|
|
238
|
+
options: { readonly includeSource?: boolean } = {},
|
|
239
|
+
): Record<string, unknown> {
|
|
240
|
+
const base = {
|
|
241
|
+
id: loaded.artifact.id,
|
|
242
|
+
path: loaded.artifact.path,
|
|
243
|
+
kind: loaded.artifact.kind,
|
|
244
|
+
description: loaded.artifact.description,
|
|
245
|
+
status: loaded.status,
|
|
246
|
+
};
|
|
247
|
+
if (loaded.status !== 'available') {
|
|
248
|
+
return { ...base, reason: loaded.reason };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const summary = loaded.artifact.kind === 'json'
|
|
252
|
+
? summarizeJsonArtifact(loaded.artifact.id, loaded.parsed)
|
|
253
|
+
: summarizeMarkdownArtifact(loaded.artifact.id, loaded.source);
|
|
254
|
+
return {
|
|
255
|
+
...base,
|
|
256
|
+
sizeBytes: loaded.sizeBytes,
|
|
257
|
+
summary,
|
|
258
|
+
...(options.includeSource
|
|
259
|
+
? loaded.artifact.kind === 'json'
|
|
260
|
+
? { data: loaded.parsed }
|
|
261
|
+
: { content: loaded.source }
|
|
262
|
+
: {}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function releaseEvidenceCandidates(artifacts: readonly ReleaseEvidenceArtifact[]): readonly Record<string, unknown>[] {
|
|
267
|
+
return artifacts.slice(0, 10).map((artifact) => ({
|
|
268
|
+
id: artifact.id,
|
|
269
|
+
path: artifact.path,
|
|
270
|
+
kind: artifact.kind,
|
|
271
|
+
description: artifact.description,
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function releaseEvidenceBundleStatus(): Record<string, unknown> {
|
|
276
|
+
const loaded = RELEASE_EVIDENCE_ARTIFACTS.map(loadArtifact);
|
|
277
|
+
const available = loaded.filter((artifact) => artifact.status === 'available').length;
|
|
278
|
+
return {
|
|
279
|
+
status: available === RELEASE_EVIDENCE_ARTIFACTS.length ? 'available' : available > 0 ? 'degraded' : 'unavailable',
|
|
280
|
+
artifacts: RELEASE_EVIDENCE_ARTIFACTS.length,
|
|
281
|
+
available,
|
|
282
|
+
missingOrInvalid: RELEASE_EVIDENCE_ARTIFACTS.length - available,
|
|
283
|
+
paths: RELEASE_EVIDENCE_ARTIFACTS.map((artifact) => artifact.path),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function releaseEvidenceSummary(args: ReleaseEvidenceArgs): Record<string, unknown> {
|
|
288
|
+
const query = readString(args.query || args.target);
|
|
289
|
+
const filtered = query
|
|
290
|
+
? RELEASE_EVIDENCE_ARTIFACTS.filter((artifact) => artifactSearchText(artifact).includes(normalized(query)))
|
|
291
|
+
: RELEASE_EVIDENCE_ARTIFACTS;
|
|
292
|
+
const limit = readLimit(args.limit, 100);
|
|
293
|
+
const loaded = filtered.slice(0, limit).map(loadArtifact);
|
|
294
|
+
const status = releaseEvidenceBundleStatus();
|
|
295
|
+
return {
|
|
296
|
+
mode: 'release_evidence',
|
|
297
|
+
...status,
|
|
298
|
+
filtered: filtered.length,
|
|
299
|
+
returned: loaded.length,
|
|
300
|
+
artifactsList: loaded.map((artifact) => summarizeLoadedArtifact(artifact, { includeSource: args.includeParameters === true })),
|
|
301
|
+
artifactLookup: 'Use mode:"release_evidence_artifact" with artifactId, target, or query to inspect one release evidence artifact.',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function describeHarnessReleaseEvidenceArtifact(args: ReleaseEvidenceArgs): ReleaseEvidenceArtifactResolution {
|
|
306
|
+
const lookup = evidenceLookupFromArgs(args);
|
|
307
|
+
if (!lookup) {
|
|
308
|
+
return {
|
|
309
|
+
status: 'missing_lookup',
|
|
310
|
+
total: RELEASE_EVIDENCE_ARTIFACTS.length,
|
|
311
|
+
usage: 'Provide artifactId, target, or query for mode:"release_evidence_artifact".',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const exactId = RELEASE_EVIDENCE_ARTIFACTS.find((artifact) => artifact.id === lookup.input);
|
|
316
|
+
if (exactId) {
|
|
317
|
+
return {
|
|
318
|
+
status: 'found',
|
|
319
|
+
lookup: { ...lookup, resolvedBy: 'id' },
|
|
320
|
+
artifact: summarizeLoadedArtifact(loadArtifact(exactId), { includeSource: true }),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const input = normalized(lookup.input);
|
|
325
|
+
const insensitiveId = RELEASE_EVIDENCE_ARTIFACTS.find((artifact) => artifact.id.toLowerCase() === input);
|
|
326
|
+
if (insensitiveId) {
|
|
327
|
+
return {
|
|
328
|
+
status: 'found',
|
|
329
|
+
lookup: { ...lookup, resolvedBy: 'case-insensitive-id' },
|
|
330
|
+
artifact: summarizeLoadedArtifact(loadArtifact(insensitiveId), { includeSource: true }),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const path = RELEASE_EVIDENCE_ARTIFACTS.find((artifact) => artifact.path.toLowerCase() === input);
|
|
335
|
+
if (path) {
|
|
336
|
+
return {
|
|
337
|
+
status: 'found',
|
|
338
|
+
lookup: { ...lookup, resolvedBy: 'path' },
|
|
339
|
+
artifact: summarizeLoadedArtifact(loadArtifact(path), { includeSource: true }),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const searched = RELEASE_EVIDENCE_ARTIFACTS.filter((artifact) => artifactSearchText(artifact).includes(input));
|
|
344
|
+
if (searched.length === 1) {
|
|
345
|
+
return {
|
|
346
|
+
status: 'found',
|
|
347
|
+
lookup: { ...lookup, resolvedBy: 'search' },
|
|
348
|
+
artifact: summarizeLoadedArtifact(loadArtifact(searched[0]!), { includeSource: true }),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (searched.length > 1) {
|
|
352
|
+
return {
|
|
353
|
+
status: 'ambiguous',
|
|
354
|
+
input: lookup.input,
|
|
355
|
+
candidates: releaseEvidenceCandidates(searched),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
status: 'not_found',
|
|
361
|
+
input: lookup.input,
|
|
362
|
+
total: RELEASE_EVIDENCE_ARTIFACTS.length,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const RELEASE_READINESS_RELATIVE_PATH = 'release/release-readiness.json';
|
|
5
|
+
const RELEASE_READINESS_PATH = join(import.meta.dir, '..', '..', RELEASE_READINESS_RELATIVE_PATH);
|
|
6
|
+
const QUALITY_DIMENSIONS = [
|
|
7
|
+
'capabilityCoverage',
|
|
8
|
+
'userAccess',
|
|
9
|
+
'modelAccess',
|
|
10
|
+
'safetyBoundary',
|
|
11
|
+
'releaseEvidence',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
interface ReleaseReadinessArgs {
|
|
15
|
+
readonly query?: unknown;
|
|
16
|
+
readonly target?: unknown;
|
|
17
|
+
readonly itemId?: unknown;
|
|
18
|
+
readonly limit?: unknown;
|
|
19
|
+
readonly includeParameters?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ReleaseReadinessLookup {
|
|
23
|
+
readonly source: 'itemId' | 'target' | 'query';
|
|
24
|
+
readonly input: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ReleaseReadinessLoadResult =
|
|
28
|
+
| { readonly status: 'available'; readonly root: Record<string, unknown>; readonly source: string }
|
|
29
|
+
| { readonly status: 'unavailable'; readonly reason: string };
|
|
30
|
+
|
|
31
|
+
export type ReleaseReadinessItemResolution =
|
|
32
|
+
| {
|
|
33
|
+
readonly status: 'found';
|
|
34
|
+
readonly path: string;
|
|
35
|
+
readonly lookup: ReleaseReadinessLookup & { readonly resolvedBy: 'id' | 'case-insensitive-id' | 'capability' | 'search' };
|
|
36
|
+
readonly item: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
readonly status: 'ambiguous';
|
|
40
|
+
readonly path: string;
|
|
41
|
+
readonly input: string;
|
|
42
|
+
readonly candidates: readonly Record<string, unknown>[];
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
readonly status: 'not_found' | 'missing_lookup' | 'unavailable';
|
|
46
|
+
readonly path: string;
|
|
47
|
+
readonly input?: string;
|
|
48
|
+
readonly reason?: string;
|
|
49
|
+
readonly total?: number;
|
|
50
|
+
readonly usage?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
54
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readString(value: unknown): string {
|
|
58
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readLimit(value: unknown, fallback: number): number {
|
|
62
|
+
const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
|
|
63
|
+
if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
|
|
64
|
+
return Math.max(1, Math.min(500, Math.trunc(parsed)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function loadReleaseReadiness(): ReleaseReadinessLoadResult {
|
|
68
|
+
if (!existsSync(RELEASE_READINESS_PATH)) {
|
|
69
|
+
return { status: 'unavailable', reason: `${RELEASE_READINESS_RELATIVE_PATH} is not present in this Agent installation.` };
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const source = readFileSync(RELEASE_READINESS_PATH, 'utf-8');
|
|
73
|
+
const parsed = JSON.parse(source) as unknown;
|
|
74
|
+
if (!isRecord(parsed)) return { status: 'unavailable', reason: `${RELEASE_READINESS_RELATIVE_PATH} must contain a JSON object.` };
|
|
75
|
+
return { status: 'available', root: parsed, source };
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
78
|
+
return { status: 'unavailable', reason: `${RELEASE_READINESS_RELATIVE_PATH} could not be read: ${message}` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function releaseReadinessItems(root: Record<string, unknown>): readonly Record<string, unknown>[] {
|
|
83
|
+
return Array.isArray(root.items) ? root.items.filter(isRecord) : [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function releaseReadinessSources(root: Record<string, unknown>): readonly Record<string, unknown>[] {
|
|
87
|
+
return Array.isArray(root.sources) ? root.sources.filter(isRecord) : [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalized(value: string): string {
|
|
91
|
+
return value.toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function itemSearchText(item: Record<string, unknown>): string {
|
|
95
|
+
const fields: string[] = [];
|
|
96
|
+
for (const key of ['id', 'capability', 'owner', 'status', 'evidence', 'action'] as const) {
|
|
97
|
+
const value = item[key];
|
|
98
|
+
if (typeof value === 'string') fields.push(value);
|
|
99
|
+
}
|
|
100
|
+
if (isRecord(item.quality)) {
|
|
101
|
+
fields.push(...Object.values(item.quality).filter((value): value is string => typeof value === 'string'));
|
|
102
|
+
}
|
|
103
|
+
return fields.join('\n').toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function countBy(items: readonly Record<string, unknown>[], key: string): Record<string, number> {
|
|
107
|
+
const counts: Record<string, number> = {};
|
|
108
|
+
for (const item of items) {
|
|
109
|
+
const value = readString(item[key]) || '<missing>';
|
|
110
|
+
counts[value] = (counts[value] ?? 0) + 1;
|
|
111
|
+
}
|
|
112
|
+
return Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function qualityDimensionCount(item: Record<string, unknown>): number {
|
|
116
|
+
const quality = item.quality;
|
|
117
|
+
if (!isRecord(quality)) return 0;
|
|
118
|
+
return QUALITY_DIMENSIONS.filter((dimension) => readString(quality[dimension])).length;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function summarizeSource(source: Record<string, unknown>): Record<string, unknown> {
|
|
122
|
+
return {
|
|
123
|
+
id: source.id,
|
|
124
|
+
kind: source.kind,
|
|
125
|
+
observedAt: source.observedAt,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function summarizeItem(item: Record<string, unknown>, options: { readonly includeQuality?: boolean } = {}): Record<string, unknown> {
|
|
130
|
+
return {
|
|
131
|
+
id: item.id,
|
|
132
|
+
capability: item.capability,
|
|
133
|
+
owner: item.owner,
|
|
134
|
+
status: item.status,
|
|
135
|
+
evidence: item.evidence,
|
|
136
|
+
action: item.action,
|
|
137
|
+
...(options.includeQuality ? { quality: item.quality } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readinessLookupFromArgs(args: ReleaseReadinessArgs): ReleaseReadinessLookup | null {
|
|
142
|
+
const itemId = readString(args.itemId);
|
|
143
|
+
if (itemId) return { source: 'itemId', input: itemId };
|
|
144
|
+
const target = readString(args.target);
|
|
145
|
+
if (target) return { source: 'target', input: target };
|
|
146
|
+
const query = readString(args.query);
|
|
147
|
+
return query ? { source: 'query', input: query } : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function releaseReadinessCandidates(items: readonly Record<string, unknown>[]): readonly Record<string, unknown>[] {
|
|
151
|
+
return items.slice(0, 10).map((item) => ({
|
|
152
|
+
id: item.id,
|
|
153
|
+
capability: item.capability,
|
|
154
|
+
owner: item.owner,
|
|
155
|
+
status: item.status,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function releaseReadinessInventoryStatus(): Record<string, unknown> {
|
|
160
|
+
const loaded = loadReleaseReadiness();
|
|
161
|
+
if (loaded.status !== 'available') {
|
|
162
|
+
return {
|
|
163
|
+
status: loaded.status,
|
|
164
|
+
path: RELEASE_READINESS_RELATIVE_PATH,
|
|
165
|
+
reason: loaded.reason,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const items = releaseReadinessItems(loaded.root);
|
|
169
|
+
return {
|
|
170
|
+
status: 'available',
|
|
171
|
+
path: RELEASE_READINESS_RELATIVE_PATH,
|
|
172
|
+
gate: loaded.root.gate,
|
|
173
|
+
checkedAt: loaded.root.checkedAt,
|
|
174
|
+
items: items.length,
|
|
175
|
+
qualityDimensions: QUALITY_DIMENSIONS,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function releaseReadinessSummary(args: ReleaseReadinessArgs): Record<string, unknown> {
|
|
180
|
+
const loaded = loadReleaseReadiness();
|
|
181
|
+
if (loaded.status !== 'available') {
|
|
182
|
+
return {
|
|
183
|
+
mode: 'release_readiness',
|
|
184
|
+
status: loaded.status,
|
|
185
|
+
path: RELEASE_READINESS_RELATIVE_PATH,
|
|
186
|
+
reason: loaded.reason,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const items = releaseReadinessItems(loaded.root);
|
|
191
|
+
const query = readString(args.query || args.target);
|
|
192
|
+
const filtered = query ? items.filter((item) => itemSearchText(item).includes(normalized(query))) : items;
|
|
193
|
+
const limit = readLimit(args.limit, 100);
|
|
194
|
+
const includeQuality = args.includeParameters === true;
|
|
195
|
+
const sources = releaseReadinessSources(loaded.root);
|
|
196
|
+
const completeQualityDimensions = items.reduce((sum, item) => sum + qualityDimensionCount(item), 0);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
mode: 'release_readiness',
|
|
200
|
+
status: 'available',
|
|
201
|
+
path: RELEASE_READINESS_RELATIVE_PATH,
|
|
202
|
+
gate: loaded.root.gate,
|
|
203
|
+
schemaVersion: loaded.root.schemaVersion,
|
|
204
|
+
checkedAt: loaded.root.checkedAt,
|
|
205
|
+
policy: loaded.root.policy,
|
|
206
|
+
totals: {
|
|
207
|
+
items: items.length,
|
|
208
|
+
filtered: filtered.length,
|
|
209
|
+
returned: Math.min(filtered.length, limit),
|
|
210
|
+
statuses: countBy(items, 'status'),
|
|
211
|
+
owners: countBy(items, 'owner'),
|
|
212
|
+
requiredQualityDimensions: QUALITY_DIMENSIONS,
|
|
213
|
+
completeQualityDimensions,
|
|
214
|
+
expectedQualityDimensions: items.length * QUALITY_DIMENSIONS.length,
|
|
215
|
+
},
|
|
216
|
+
sources: includeQuality ? sources : sources.map(summarizeSource),
|
|
217
|
+
items: filtered.slice(0, limit).map((item) => summarizeItem(item, { includeQuality })),
|
|
218
|
+
itemLookup: 'Use mode:"release_readiness_item" with itemId, target, or query to inspect one readiness item.',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function describeHarnessReleaseReadinessItem(args: ReleaseReadinessArgs): ReleaseReadinessItemResolution {
|
|
223
|
+
const loaded = loadReleaseReadiness();
|
|
224
|
+
if (loaded.status !== 'available') {
|
|
225
|
+
return {
|
|
226
|
+
status: 'unavailable',
|
|
227
|
+
path: RELEASE_READINESS_RELATIVE_PATH,
|
|
228
|
+
reason: loaded.reason,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const items = releaseReadinessItems(loaded.root);
|
|
233
|
+
const lookup = readinessLookupFromArgs(args);
|
|
234
|
+
if (!lookup) {
|
|
235
|
+
return {
|
|
236
|
+
status: 'missing_lookup',
|
|
237
|
+
path: RELEASE_READINESS_RELATIVE_PATH,
|
|
238
|
+
total: items.length,
|
|
239
|
+
usage: 'Provide itemId, target, or query for mode:"release_readiness_item".',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const exactId = items.find((item) => item.id === lookup.input);
|
|
244
|
+
if (exactId) {
|
|
245
|
+
return {
|
|
246
|
+
status: 'found',
|
|
247
|
+
path: RELEASE_READINESS_RELATIVE_PATH,
|
|
248
|
+
lookup: { ...lookup, resolvedBy: 'id' },
|
|
249
|
+
item: summarizeItem(exactId, { includeQuality: true }),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const input = normalized(lookup.input);
|
|
254
|
+
const insensitiveId = items.find((item) => readString(item.id).toLowerCase() === input);
|
|
255
|
+
if (insensitiveId) {
|
|
256
|
+
return {
|
|
257
|
+
status: 'found',
|
|
258
|
+
path: RELEASE_READINESS_RELATIVE_PATH,
|
|
259
|
+
lookup: { ...lookup, resolvedBy: 'case-insensitive-id' },
|
|
260
|
+
item: summarizeItem(insensitiveId, { includeQuality: true }),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const capability = items.find((item) => readString(item.capability).toLowerCase() === input);
|
|
265
|
+
if (capability) {
|
|
266
|
+
return {
|
|
267
|
+
status: 'found',
|
|
268
|
+
path: RELEASE_READINESS_RELATIVE_PATH,
|
|
269
|
+
lookup: { ...lookup, resolvedBy: 'capability' },
|
|
270
|
+
item: summarizeItem(capability, { includeQuality: true }),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const searched = items.filter((item) => itemSearchText(item).includes(input));
|
|
275
|
+
if (searched.length === 1) {
|
|
276
|
+
return {
|
|
277
|
+
status: 'found',
|
|
278
|
+
path: RELEASE_READINESS_RELATIVE_PATH,
|
|
279
|
+
lookup: { ...lookup, resolvedBy: 'search' },
|
|
280
|
+
item: summarizeItem(searched[0]!, { includeQuality: true }),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (searched.length > 1) {
|
|
284
|
+
return {
|
|
285
|
+
status: 'ambiguous',
|
|
286
|
+
path: RELEASE_READINESS_RELATIVE_PATH,
|
|
287
|
+
input: lookup.input,
|
|
288
|
+
candidates: releaseReadinessCandidates(searched),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
status: 'not_found',
|
|
294
|
+
path: RELEASE_READINESS_RELATIVE_PATH,
|
|
295
|
+
input: lookup.input,
|
|
296
|
+
total: items.length,
|
|
297
|
+
};
|
|
298
|
+
}
|