agentic-scorecard 0.1.0 → 0.2.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/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { appendFile, mkdir, readFile as readFile3, writeFile } from "fs/promises";
5
- import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
5
+ import { dirname as dirname2, join as join2, resolve as resolve4 } from "path";
6
6
  import { Command } from "commander";
7
7
  import { stringify } from "yaml";
8
8
 
@@ -29,31 +29,49 @@ var dimensionIds = [
29
29
  ];
30
30
  var DimensionIdSchema = z.enum(dimensionIds);
31
31
  var LevelSchema = z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3), z.literal(4)]);
32
+ var EvidenceScopeSchema = z.enum(["repository", "platform", "organization", "outcome"]);
33
+ var AssessmentScopeSchema = z.enum(["tracked", "workspace"]);
32
34
  var PathAnySchema = z.object({
33
35
  type: z.literal("path_any"),
34
- patterns: z.array(z.string().min(1)).min(1)
36
+ scope: z.literal("repository").default("repository"),
37
+ patterns: z.array(z.string().min(1)).min(1),
38
+ min_bytes: z.number().int().positive().default(1)
35
39
  });
36
40
  var PathAllSchema = z.object({
37
41
  type: z.literal("path_all"),
38
- patterns: z.array(z.string().min(1)).min(1)
42
+ scope: z.literal("repository").default("repository"),
43
+ patterns: z.array(z.string().min(1)).min(1),
44
+ min_bytes: z.number().int().positive().default(1)
39
45
  });
40
46
  var ContentAnySchema = z.object({
41
47
  type: z.literal("content_any"),
48
+ scope: z.literal("repository").default("repository"),
42
49
  files: z.array(z.string().min(1)).min(1),
43
50
  needles: z.array(z.string().min(1)).min(1)
44
51
  });
45
52
  var ContentAllSchema = z.object({
46
53
  type: z.literal("content_all"),
54
+ scope: z.literal("repository").default("repository"),
47
55
  files: z.array(z.string().min(1)).min(1),
48
56
  needles: z.array(z.string().min(1)).min(1)
49
57
  });
58
+ var ContentTermsSchema = z.object({
59
+ type: z.literal("content_terms"),
60
+ scope: z.literal("repository").default("repository"),
61
+ files: z.array(z.string().min(1)).min(1),
62
+ terms: z.array(z.string().min(1)).min(1),
63
+ min_terms: z.number().int().positive(),
64
+ required_any_terms: z.array(z.string().min(1)).min(1).optional()
65
+ });
50
66
  var MaxBytesSchema = z.object({
51
67
  type: z.literal("max_bytes"),
68
+ scope: z.literal("repository").default("repository"),
52
69
  patterns: z.array(z.string().min(1)).min(1),
53
70
  max_bytes: z.number().int().positive()
54
71
  });
55
72
  var ManualSchema = z.object({
56
73
  type: z.literal("manual"),
74
+ scope: z.enum(["platform", "organization", "outcome"]).default("organization"),
57
75
  prompt: z.string().min(1)
58
76
  });
59
77
  var EvidenceCheckSchema = z.discriminatedUnion("type", [
@@ -61,6 +79,7 @@ var EvidenceCheckSchema = z.discriminatedUnion("type", [
61
79
  PathAllSchema,
62
80
  ContentAnySchema,
63
81
  ContentAllSchema,
82
+ ContentTermsSchema,
64
83
  MaxBytesSchema,
65
84
  ManualSchema
66
85
  ]);
@@ -73,13 +92,21 @@ var RawControlSchema = z.object({
73
92
  evidence: z.array(EvidenceCheckSchema).min(1),
74
93
  remediation: z.string().min(1),
75
94
  references: z.array(z.string().min(1)).default([]),
76
- allow_attestation: z.boolean().default(true),
77
- allow_not_applicable: z.boolean().default(false)
95
+ allow_attestation: z.boolean().default(false),
96
+ allow_not_applicable: z.boolean().default(false),
97
+ allow_agent_evidence: z.boolean().default(false)
98
+ });
99
+ var LegacyRawControlSchema = RawControlSchema.extend({
100
+ allow_attestation: z.boolean().default(true)
78
101
  });
79
102
  var ControlFileSchema = z.object({
80
103
  dimension: DimensionIdSchema,
81
104
  controls: z.array(RawControlSchema).min(1)
82
105
  });
106
+ var LegacyControlFileSchema = z.object({
107
+ dimension: DimensionIdSchema,
108
+ controls: z.array(LegacyRawControlSchema).min(1)
109
+ });
83
110
  var DimensionSchema = z.object({
84
111
  id: DimensionIdSchema,
85
112
  title: z.string().min(1),
@@ -116,16 +143,53 @@ var AttestationSchema = z.object({
116
143
  evidence: z.string().min(1),
117
144
  owner: z.string().min(1),
118
145
  reviewed_at: z.string().date(),
146
+ expires_at: z.string().date()
147
+ });
148
+ var LegacyAttestationSchema = AttestationSchema.extend({
119
149
  expires_at: z.string().date().nullable().default(null)
120
150
  });
121
151
  var AttestationFileSchema = z.object({
122
152
  benchmark_version: z.string().min(1),
123
153
  attestations: z.record(z.string(), AttestationSchema).default({})
124
154
  });
155
+ var LegacyAttestationFileSchema = z.object({
156
+ benchmark_version: z.string().min(1),
157
+ attestations: z.record(z.string(), LegacyAttestationSchema).default({})
158
+ });
159
+ var AgentEvidenceClaimSchema = z.object({
160
+ status: z.enum(["met", "not_met", "unknown"]),
161
+ scope: z.enum(["platform", "organization", "outcome"]),
162
+ summary: z.string().min(1),
163
+ references: z.array(z.string().min(1)).min(1),
164
+ collected_at: z.string().datetime(),
165
+ expires_at: z.string().datetime(),
166
+ error: z.string().min(1).nullable().default(null)
167
+ }).strict().superRefine((claim, context) => {
168
+ if (claim.error && claim.status !== "unknown") {
169
+ context.addIssue({
170
+ code: z.ZodIssueCode.custom,
171
+ message: "A claim with an error must have unknown status",
172
+ path: ["status"]
173
+ });
174
+ }
175
+ });
176
+ var AgentEvidenceFileSchema = z.object({
177
+ schema_version: z.literal("0.2.0"),
178
+ benchmark_version: z.literal("0.2.0"),
179
+ target: z.object({
180
+ repository: z.string().min(1),
181
+ git_head: z.string().min(1).nullable()
182
+ }).strict(),
183
+ collector: z.object({
184
+ name: z.string().min(1),
185
+ version: z.string().min(1)
186
+ }).strict(),
187
+ claims: z.record(z.string().regex(/^ADRB-[A-Z]{3}-\d{3}$/), AgentEvidenceClaimSchema)
188
+ }).strict();
125
189
 
126
190
  // src/load.ts
127
191
  var packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
128
- var defaultBenchmarkRoot = join(packageRoot, "benchmark", "v0.1");
192
+ var defaultBenchmarkRoot = join(packageRoot, "benchmark", "v0.2");
129
193
  async function readYaml(path) {
130
194
  return parse(await readFile(path, "utf8"));
131
195
  }
@@ -134,7 +198,7 @@ async function loadBenchmark(root = defaultBenchmarkRoot) {
134
198
  const controlPaths = await fg("controls/*.yaml", { cwd: root, absolute: true, onlyFiles: true });
135
199
  const controls = [];
136
200
  for (const path of controlPaths.sort()) {
137
- const file = ControlFileSchema.parse(await readYaml(path));
201
+ const file = benchmark.version === "0.1.0" ? LegacyControlFileSchema.parse(await readYaml(path)) : ControlFileSchema.parse(await readYaml(path));
138
202
  controls.push(...file.controls.map((control) => ({ ...control, dimension: file.dimension })));
139
203
  }
140
204
  validateCatalog(benchmark, controls);
@@ -142,7 +206,8 @@ async function loadBenchmark(root = defaultBenchmarkRoot) {
142
206
  }
143
207
  async function loadAttestations(path, benchmarkVersion) {
144
208
  try {
145
- const file = AttestationFileSchema.parse(await readYaml(path));
209
+ const rawFile = await readYaml(path);
210
+ const file = benchmarkVersion === "0.1.0" ? LegacyAttestationFileSchema.parse(rawFile) : AttestationFileSchema.parse(rawFile);
146
211
  if (file.benchmark_version !== benchmarkVersion) {
147
212
  throw new Error(
148
213
  `Attestation benchmark version ${file.benchmark_version} does not match ${benchmarkVersion}`
@@ -154,11 +219,33 @@ async function loadAttestations(path, benchmarkVersion) {
154
219
  throw error;
155
220
  }
156
221
  }
222
+ async function loadAgentEvidence(path) {
223
+ try {
224
+ return AgentEvidenceFileSchema.parse(await readYaml(path));
225
+ } catch (error) {
226
+ if (error.code === "ENOENT") return null;
227
+ throw error;
228
+ }
229
+ }
157
230
  function validateCatalog(benchmark, controls) {
158
231
  const ids = /* @__PURE__ */ new Set();
159
232
  for (const control of controls) {
160
233
  if (ids.has(control.id)) throw new Error(`Duplicate control id: ${control.id}`);
161
234
  ids.add(control.id);
235
+ if (benchmark.version === "0.2.0" && control.evidence.some(({ type }) => type === "content_any" || type === "content_all")) {
236
+ throw new Error(`${control.id} uses a legacy broad content collector in benchmark v0.2.0`);
237
+ }
238
+ if (control.allow_agent_evidence && !control.evidence.some(({ type }) => type === "manual")) {
239
+ throw new Error(`${control.id} allows agent evidence without an external evidence check`);
240
+ }
241
+ if (benchmark.version === "0.2.0" && control.allow_attestation && !control.evidence.some(({ type }) => type === "manual")) {
242
+ throw new Error(`${control.id} allows attestation for repository-detected evidence`);
243
+ }
244
+ for (const check of control.evidence) {
245
+ if (check.type === "content_terms" && check.min_terms > check.terms.length) {
246
+ throw new Error(`${control.id} requires more content terms than it defines`);
247
+ }
248
+ }
162
249
  }
163
250
  const benchmarkDimensions = benchmark.dimensions.map(({ id }) => id);
164
251
  if (benchmarkDimensions.join(",") !== dimensionIds.join(",")) {
@@ -181,18 +268,88 @@ var statusIcon = {
181
268
  unknown: "UNKNOWN",
182
269
  not_applicable: "N/A"
183
270
  };
271
+ function controlScope(control) {
272
+ return control.evidence.find(({ scope }) => scope !== "repository")?.scope ?? "repository";
273
+ }
274
+ function safeText(value) {
275
+ return value.replace(/\s+/g, " ").replace(/([\\`*_[\]<>|])/g, "\\$1");
276
+ }
277
+ function sentence(value) {
278
+ const safe = safeText(value);
279
+ return /[.!?]$/.test(safe) ? safe : `${safe}.`;
280
+ }
281
+ function references(values) {
282
+ return values.map((value) => `\`${value.replaceAll("`", "'").replace(/\s+/g, " ")}\``).join(", ");
283
+ }
284
+ function evidenceLines(control) {
285
+ const lines = control.evidence.flatMap((evidence) => {
286
+ const evidenceReferences = evidence.references.length > 0 ? ` References: ${references(evidence.references)}.` : "";
287
+ return `- **${evidence.scope}/${evidence.type}:** ${evidence.status} \u2014 ${sentence(evidence.summary)}${evidenceReferences}`;
288
+ });
289
+ if (control.agent_evidence) {
290
+ lines.push(
291
+ `- **Agent-collected:** ${control.agent_evidence.status} \u2014 ${sentence(control.agent_evidence.summary)} References: ${references(control.agent_evidence.references)}.`
292
+ );
293
+ }
294
+ if (control.attestation) {
295
+ lines.push(
296
+ `- **Human-attested:** ${control.attestation.status} \u2014 ${references([control.attestation.evidence])} (owner: ${safeText(control.attestation.owner)}; reviewed: ${control.attestation.reviewed_at}).`
297
+ );
298
+ }
299
+ if (control.agent_evidence && control.attestation && control.agent_evidence.status !== "unknown" && control.attestation.status !== "unknown" && control.agent_evidence.status !== control.attestation.status) {
300
+ lines.push(
301
+ "- **Conflict:** agent-collected and human-attested statuses disagree; fail closed."
302
+ );
303
+ }
304
+ return lines;
305
+ }
306
+ function appendControlDetails(lines, heading, controls) {
307
+ lines.push("", `## ${heading}`, "");
308
+ if (controls.length === 0) {
309
+ lines.push("None.");
310
+ return;
311
+ }
312
+ for (const control of controls) {
313
+ lines.push(
314
+ `### ${statusIcon[control.status]} ${control.id} \u2014 ${control.title}`,
315
+ "",
316
+ `**Risk:** ${control.risk}`,
317
+ "",
318
+ `**Improve:** ${control.remediation}`,
319
+ "",
320
+ `Evidence confidence: ${control.confidence}.`,
321
+ "",
322
+ ...evidenceLines(control),
323
+ ""
324
+ );
325
+ }
326
+ }
184
327
  function toMarkdown(report) {
185
328
  const target = report.profiles.find(({ id }) => id === report.target.profile);
329
+ const established = report.controls.filter(
330
+ ({ status }) => status === "met" || status === "not_applicable"
331
+ );
332
+ const unresolved = report.controls.filter(
333
+ ({ status }) => status === "not_met" || status === "unknown"
334
+ );
335
+ const repositoryGaps = unresolved.filter((control) => controlScope(control) === "repository");
336
+ const externalControls = unresolved.filter(
337
+ (control) => ["platform", "organization"].includes(controlScope(control))
338
+ );
339
+ const outcomeControls = unresolved.filter((control) => controlScope(control) === "outcome");
186
340
  const lines = [
187
341
  "# Agentic Development Readiness Assessment",
188
342
  "",
189
343
  `- Benchmark: ${report.benchmark.id} v${report.benchmark.version}`,
190
344
  `- Repository: \`${report.target.repository}\``,
345
+ `- Assessment scope: **${report.target.scope}**`,
346
+ `- Git commit: ${report.target.git_head ? `\`${report.target.git_head}\`` : "unavailable"}`,
347
+ `- Working tree dirty: ${report.target.working_tree_dirty === null ? "unknown" : String(report.target.working_tree_dirty)}`,
191
348
  `- Assessed: ${report.assessed_at}`,
192
349
  `- Score: **${report.score.total}/${report.score.maximum} (${report.score.percentage}%)**`,
193
350
  `- Highest readiness profile: **${report.readiness.highest_profile ?? "none"}**`,
194
351
  `- Target \`${report.target.profile}\`: **${report.readiness.target_passed ? "PASS" : "FAIL"}**`,
195
- `- Evidence: ${report.evidence_summary.verified} verified, ${report.evidence_summary.attested} attested, ${report.evidence_summary.unmet_or_unknown} unmet/unknown`,
352
+ `- Evidence: ${report.evidence_summary.repository_detected} repository-detected, ${report.evidence_summary.agent_collected} agent-collected, ${report.evidence_summary.attested} human-attested, ${report.evidence_summary.unmet} unmet, ${report.evidence_summary.unknown} unknown`,
196
353
  "",
197
354
  "## Dimensions",
198
355
  "",
@@ -200,7 +357,9 @@ function toMarkdown(report) {
200
357
  "| --- | ---: | ---: |",
201
358
  ...report.dimensions.map(
202
359
  ({ title, score, controls_met: met, controls_total: total }) => `| ${title} | ${score}/4 | ${met}/${total} |`
203
- )
360
+ ),
361
+ "",
362
+ "> A dimension earns only consecutive levels. Controls met above the first gap remain visible but do not increase its score."
204
363
  ];
205
364
  if (target && !target.passed) {
206
365
  lines.push("", "## Target-profile blockers", "");
@@ -210,27 +369,22 @@ function toMarkdown(report) {
210
369
  );
211
370
  }
212
371
  }
213
- lines.push("", "## Controls requiring action", "");
214
- const actionControls = report.controls.filter(
215
- ({ status }) => status === "not_met" || status === "unknown"
216
- );
217
- if (actionControls.length === 0) {
372
+ lines.push("", "## Established controls", "");
373
+ if (established.length === 0) {
218
374
  lines.push("None.");
219
375
  } else {
220
- for (const control of actionControls) {
376
+ lines.push("| Control | Level | Evidence scope | Confidence |", "| --- | ---: | --- | --- |");
377
+ for (const control of established) {
221
378
  lines.push(
222
- `### ${statusIcon[control.status]} ${control.id} \u2014 ${control.title}`,
223
- "",
224
- `**Risk:** ${control.risk}`,
225
- "",
226
- `**Improve:** ${control.remediation}`,
227
- "",
228
- `Evidence confidence: ${control.confidence}.`,
229
- ""
379
+ `| ${control.id} \u2014 ${control.title.replaceAll("|", "\\|")} | ${control.level} | ${controlScope(control)} | ${control.confidence} |`
230
380
  );
231
381
  }
232
382
  }
383
+ appendControlDetails(lines, "Repository evidence gaps", repositoryGaps);
384
+ appendControlDetails(lines, "External controls not established", externalControls);
385
+ appendControlDetails(lines, "Outcome evidence not established", outcomeControls);
233
386
  lines.push(
387
+ "",
234
388
  "## Limitations",
235
389
  "",
236
390
  ...report.limitations.map((limitation) => `- ${limitation}`),
@@ -239,143 +393,332 @@ function toMarkdown(report) {
239
393
  return lines.join("\n");
240
394
  }
241
395
 
396
+ // src/repository.ts
397
+ import { execFile } from "child_process";
398
+ import { realpath } from "fs/promises";
399
+ import { isAbsolute, relative, resolve as resolve2, sep } from "path";
400
+ import { promisify } from "util";
401
+ var execFileAsync = promisify(execFile);
402
+ var generatedEvidenceIgnores = [
403
+ "**/.git/**",
404
+ "**/node_modules/**",
405
+ "**/dist/**",
406
+ "**/coverage/**",
407
+ "**/.agentic/reports/**",
408
+ "**/.agentic/report*.json",
409
+ "**/.agentic/agentic-readiness*.json",
410
+ "**/.agentic/agentic-readiness*.md",
411
+ "**/.agentic/attestations.*",
412
+ "**/.agentic/agent-evidence.*",
413
+ "**/.agentic/evidence-request.*"
414
+ ];
415
+ function relativePathWithin(root, path) {
416
+ const candidate = relative(root, path);
417
+ if (candidate === "") return candidate;
418
+ if (candidate === ".." || candidate.startsWith(`..${sep}`) || isAbsolute(candidate)) {
419
+ return null;
420
+ }
421
+ return candidate;
422
+ }
423
+ function normalizeExcludedPath(path, requestedRoot, canonicalRoot) {
424
+ const absolute = isAbsolute(path) ? resolve2(path) : resolve2(requestedRoot, path);
425
+ return relativePathWithin(requestedRoot, absolute) ?? relativePathWithin(canonicalRoot, absolute);
426
+ }
427
+ function sanitizeRemote(remote) {
428
+ if (!remote) return null;
429
+ try {
430
+ const url = new URL(remote);
431
+ if (url.protocol === "http:" || url.protocol === "https:") {
432
+ url.username = "";
433
+ url.password = "";
434
+ } else if (url.password) {
435
+ url.password = "";
436
+ }
437
+ return url.toString();
438
+ } catch {
439
+ return remote;
440
+ }
441
+ }
442
+ async function git(repo, args) {
443
+ try {
444
+ const { stdout } = await execFileAsync("git", ["-C", repo, ...args], {
445
+ encoding: "utf8",
446
+ maxBuffer: 1e7
447
+ });
448
+ return stdout;
449
+ } catch {
450
+ return null;
451
+ }
452
+ }
453
+ async function createRepositoryContext(repository, scope, excludedPaths = []) {
454
+ const requestedRoot = resolve2(repository);
455
+ const root = await realpath(requestedRoot);
456
+ const [headOutput, remoteOutput, statusOutput] = await Promise.all([
457
+ git(root, ["rev-parse", "HEAD"]),
458
+ git(root, ["config", "--get", "remote.origin.url"]),
459
+ git(root, ["status", "--porcelain"])
460
+ ]);
461
+ let includedPaths = null;
462
+ if (scope === "tracked") {
463
+ const tracked = await git(root, ["ls-files", "-z", "--cached"]);
464
+ if (tracked === null) {
465
+ throw new Error(
466
+ "Tracked assessment requires a Git worktree. Use --scope workspace for a provisional filesystem assessment."
467
+ );
468
+ }
469
+ includedPaths = new Set(tracked.split("\0").filter((path) => path.length > 0));
470
+ }
471
+ return {
472
+ metadata: {
473
+ root,
474
+ scope,
475
+ git_head: headOutput?.trim() || null,
476
+ git_remote: sanitizeRemote(remoteOutput?.trim() || null),
477
+ working_tree_dirty: statusOutput === null ? null : statusOutput.length > 0
478
+ },
479
+ includedPaths,
480
+ excludedPaths: new Set(
481
+ excludedPaths.flatMap((path) => {
482
+ const normalized = normalizeExcludedPath(path, requestedRoot, root);
483
+ return normalized === null ? [] : [normalized];
484
+ })
485
+ )
486
+ };
487
+ }
488
+ function repositoryEvidenceTarget(metadata) {
489
+ return {
490
+ repository: metadata.git_remote ?? metadata.root,
491
+ git_head: metadata.git_head
492
+ };
493
+ }
494
+
242
495
  // src/evidence.ts
243
- import { lstat, readFile as readFile2, realpath, stat } from "fs/promises";
244
- import { relative, resolve as resolve2, sep } from "path";
496
+ import { lstat, readFile as readFile2, realpath as realpath2, stat } from "fs/promises";
497
+ import { resolve as resolve3, sep as sep2 } from "path";
245
498
  import fg2 from "fast-glob";
246
- var ignored = ["**/.git/**", "**/node_modules/**", "**/dist/**", "**/coverage/**"];
247
499
  var maxContentFileBytes = 512e3;
248
500
  var maxContentFiles = 250;
249
501
  var maxContentTotalBytes = 5e6;
250
- async function matches(repo, patterns) {
251
- return (await fg2(patterns, {
252
- cwd: repo,
502
+ async function matches(context, patterns) {
503
+ const found = await fg2(patterns, {
504
+ cwd: context.metadata.root,
253
505
  dot: true,
254
- onlyFiles: false,
506
+ onlyFiles: true,
255
507
  unique: true,
256
508
  followSymbolicLinks: false,
257
- ignore: ignored
258
- })).sort();
509
+ ignore: generatedEvidenceIgnores
510
+ });
511
+ return found.filter((path) => context.includedPaths === null || context.includedPaths.has(path)).filter((path) => !context.excludedPaths.has(path)).sort();
259
512
  }
260
- async function evaluatePathAny(repo, check) {
261
- const found = await matches(repo, check.patterns);
513
+ async function safeFileSize(root, path) {
514
+ try {
515
+ const requestedPath = resolve3(root, path);
516
+ if ((await lstat(requestedPath)).isSymbolicLink()) return null;
517
+ const canonicalPath = await realpath2(requestedPath);
518
+ if (canonicalPath !== root && !canonicalPath.startsWith(`${root}${sep2}`)) return null;
519
+ const metadata = await stat(canonicalPath);
520
+ return metadata.isFile() ? metadata.size : null;
521
+ } catch {
522
+ return null;
523
+ }
524
+ }
525
+ async function nonEmptyMatches(context, patterns, minBytes) {
526
+ const found = await matches(context, patterns);
527
+ const qualifying = await Promise.all(
528
+ found.map(async (path) => ({ path, size: await safeFileSize(context.metadata.root, path) }))
529
+ );
530
+ return qualifying.filter(({ size }) => size !== null && size >= minBytes).map(({ path }) => path);
531
+ }
532
+ async function evaluatePathAny(context, check) {
533
+ const found = await nonEmptyMatches(context, check.patterns, check.min_bytes);
262
534
  return result(
263
535
  check.type,
536
+ check.scope,
264
537
  found.length > 0 ? "met" : "not_met",
265
- `${found.length} matching path(s)`,
538
+ `${found.length} safe, non-empty matching file(s)`,
266
539
  found
267
540
  );
268
541
  }
269
- async function evaluatePathAll(repo, check) {
270
- const groups = await Promise.all(check.patterns.map(async (pattern) => matches(repo, [pattern])));
542
+ async function evaluatePathAll(context, check) {
543
+ const groups = await Promise.all(
544
+ check.patterns.map(async (pattern) => nonEmptyMatches(context, [pattern], check.min_bytes))
545
+ );
271
546
  const missing = check.patterns.filter((_, index) => groups[index]?.length === 0);
272
547
  const found = [...new Set(groups.flat())].sort();
273
548
  return result(
274
549
  check.type,
550
+ check.scope,
275
551
  missing.length === 0 ? "met" : "not_met",
276
- missing.length === 0 ? "Every required path pattern matched" : `Missing patterns: ${missing.join(", ")}`,
552
+ missing.length === 0 ? "Every required pattern matched a safe, non-empty file" : `Missing non-empty patterns: ${missing.join(", ")}`,
277
553
  found
278
554
  );
279
555
  }
280
- async function readSearchableFiles(repo, patterns) {
281
- const root = await realpath(repo);
282
- const paths = (await matches(repo, patterns)).slice(0, maxContentFiles);
556
+ async function readSearchableFiles(context, patterns) {
557
+ const root = context.metadata.root;
558
+ const paths = (await matches(context, patterns)).slice(0, maxContentFiles);
283
559
  const files = [];
284
560
  let totalBytes = 0;
285
561
  for (const path of paths) {
286
562
  try {
287
- const requestedPath = resolve2(root, path);
563
+ const requestedPath = resolve3(root, path);
288
564
  if ((await lstat(requestedPath)).isSymbolicLink()) continue;
289
- const canonicalPath = await realpath(requestedPath);
290
- if (canonicalPath !== root && !canonicalPath.startsWith(`${root}${sep}`)) continue;
565
+ const canonicalPath = await realpath2(requestedPath);
566
+ if (canonicalPath !== root && !canonicalPath.startsWith(`${root}${sep2}`)) continue;
291
567
  const metadata = await stat(canonicalPath);
292
- if (!metadata.isFile() || metadata.size > maxContentFileBytes || totalBytes + metadata.size > maxContentTotalBytes) {
568
+ if (!metadata.isFile() || metadata.size === 0 || metadata.size > maxContentFileBytes || totalBytes + metadata.size > maxContentTotalBytes) {
293
569
  continue;
294
570
  }
571
+ const text = (await readFile2(canonicalPath, "utf8")).toLowerCase();
572
+ if (isGeneratedAssessment(text)) continue;
295
573
  totalBytes += metadata.size;
296
- files.push({ path, text: (await readFile2(canonicalPath, "utf8")).toLowerCase() });
574
+ files.push({ path, text });
297
575
  } catch {
298
576
  }
299
577
  }
300
578
  return files;
301
579
  }
302
- async function evaluateContent(repo, check) {
303
- const files = await readSearchableFiles(repo, check.files);
580
+ function isGeneratedAssessment(text) {
581
+ const normalized = text.trimStart();
582
+ if (normalized.startsWith("# agentic development readiness assessment")) return true;
583
+ if (!normalized.startsWith("{")) return false;
584
+ try {
585
+ const candidate = JSON.parse(normalized);
586
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return false;
587
+ const report = candidate;
588
+ const benchmark = report.benchmark;
589
+ return Boolean(benchmark) && typeof benchmark === "object" && !Array.isArray(benchmark) && benchmark.id === "agentic-development-readiness" && typeof report.assessed_at === "string" && Array.isArray(report.controls);
590
+ } catch {
591
+ return false;
592
+ }
593
+ }
594
+ async function evaluateLegacyContent(context, check) {
595
+ const files = await readSearchableFiles(context, check.files);
596
+ const matchingFiles = files.filter(
597
+ ({ text }) => check.needles.some((needle) => text.includes(needle.toLowerCase()))
598
+ );
304
599
  const matchedNeedles = check.needles.filter(
305
600
  (needle) => files.some(({ text }) => text.includes(needle.toLowerCase()))
306
601
  );
307
602
  const passed = check.type === "content_any" ? matchedNeedles.length > 0 : matchedNeedles.length === check.needles.length;
308
603
  return result(
309
604
  check.type,
605
+ check.scope,
310
606
  passed ? "met" : "not_met",
311
- `Matched ${matchedNeedles.length}/${check.needles.length} required term(s) across ${files.length} file(s)`,
312
- files.map(({ path }) => path)
607
+ `Matched ${matchedNeedles.length}/${check.needles.length} term(s) across ${files.length} candidate file(s)`,
608
+ matchingFiles.map(({ path }) => path)
313
609
  );
314
610
  }
315
- async function evaluateMaxBytes(repo, check) {
316
- const paths = await matches(repo, check.patterns);
317
- const root = await realpath(repo);
611
+ async function evaluateContentTerms(context, check) {
612
+ const files = await readSearchableFiles(context, check.files);
613
+ const matchesByFile = files.map(({ path, text }) => ({
614
+ path,
615
+ matched: check.terms.filter((term) => containsTerm(text, term)).length,
616
+ requiredMatched: check.required_any_terms?.filter((term) => containsTerm(text, term)).length ?? 0
617
+ }));
618
+ const qualifying = matchesByFile.filter(
619
+ ({ matched, requiredMatched }) => matched >= check.min_terms && (check.required_any_terms === void 0 || requiredMatched > 0)
620
+ );
621
+ const strongest = matchesByFile.reduce((maximum, file) => Math.max(maximum, file.matched), 0);
622
+ const strongestRequired = matchesByFile.reduce(
623
+ (maximum, file) => Math.max(maximum, file.requiredMatched),
624
+ 0
625
+ );
626
+ const requiredSummary = check.required_any_terms ? `; strongest required match ${strongestRequired}/${check.required_any_terms.length}` : "";
627
+ return result(
628
+ check.type,
629
+ check.scope,
630
+ qualifying.length > 0 ? "met" : "not_met",
631
+ `${qualifying.length} qualifying file(s); strongest co-located match ${strongest}/${check.terms.length} term(s)${requiredSummary} across ${files.length} candidate file(s); threshold ${check.min_terms}`,
632
+ qualifying.map(({ path }) => path)
633
+ );
634
+ }
635
+ function containsTerm(text, term) {
636
+ const pattern = term.trim().split(/\s+/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\s_-]+");
637
+ return new RegExp(`(^|[^a-z0-9])${pattern}(?=$|[^a-z0-9])`, "i").test(text);
638
+ }
639
+ async function evaluateMaxBytes(context, check) {
640
+ const paths = await matches(context, check.patterns);
318
641
  let total = 0;
319
- let inspected = 0;
642
+ const inspected = [];
320
643
  for (const path of paths) {
321
- const requestedPath = resolve2(root, path);
322
- if ((await lstat(requestedPath)).isSymbolicLink()) continue;
323
- const canonicalPath = await realpath(requestedPath);
324
- if (canonicalPath !== root && !canonicalPath.startsWith(`${root}${sep}`)) continue;
325
- total += (await stat(canonicalPath)).size;
326
- inspected += 1;
327
- }
328
- const passed = inspected > 0 && total <= check.max_bytes;
644
+ const size = await safeFileSize(context.metadata.root, path);
645
+ if (size === null) continue;
646
+ total += size;
647
+ inspected.push(path);
648
+ }
649
+ const passed = inspected.length > 0 && total <= check.max_bytes;
329
650
  return result(
330
651
  check.type,
652
+ check.scope,
331
653
  passed ? "met" : "not_met",
332
- `${total} byte(s) across ${inspected} safe matching file(s); maximum ${check.max_bytes}`,
333
- paths
654
+ `${total} byte(s) across ${inspected.length} safe matching file(s); maximum ${check.max_bytes}`,
655
+ inspected
334
656
  );
335
657
  }
336
- function result(type, status, summary, references) {
337
- return { type, status, summary, references };
658
+ function result(type, scope, status, summary, references2) {
659
+ return { type, scope, status, summary, references: references2 };
338
660
  }
339
- async function evaluateCheck(repo, check) {
661
+ async function evaluateCheck(context, check) {
340
662
  switch (check.type) {
341
663
  case "path_any":
342
- return evaluatePathAny(repo, check);
664
+ return evaluatePathAny(context, check);
343
665
  case "path_all":
344
- return evaluatePathAll(repo, check);
666
+ return evaluatePathAll(context, check);
345
667
  case "content_any":
346
668
  case "content_all":
347
- return evaluateContent(repo, check);
669
+ return evaluateLegacyContent(context, check);
670
+ case "content_terms":
671
+ return evaluateContentTerms(context, check);
348
672
  case "max_bytes":
349
- return evaluateMaxBytes(repo, check);
673
+ return evaluateMaxBytes(context, check);
350
674
  case "manual":
351
- return result("manual", "unknown", check.prompt, []);
675
+ return result("manual", check.scope, "unknown", check.prompt, []);
352
676
  }
353
677
  }
354
678
  function activeAttestation(control, attestations, now) {
679
+ if (!control.allow_attestation) return null;
355
680
  const attestation = attestations?.attestations[control.id];
356
681
  if (!attestation) return null;
357
682
  if (attestation.expires_at && new Date(attestation.expires_at) < now) return null;
358
683
  if (attestation.status === "not_applicable" && !control.allow_not_applicable) return null;
359
684
  return attestation;
360
685
  }
361
- async function evaluateControl(repo, control, attestations, now = /* @__PURE__ */ new Date()) {
686
+ function activeAgentEvidence(control, claim, now) {
687
+ if (!claim || !control.allow_agent_evidence) return null;
688
+ if (new Date(claim.expires_at) < now) return null;
689
+ return claim;
690
+ }
691
+ async function evaluateControl(context, control, attestations, agentClaim, now = /* @__PURE__ */ new Date()) {
362
692
  const evidence = await Promise.all(
363
- control.evidence.map(async (check) => evaluateCheck(repo, check))
693
+ control.evidence.map(async (check) => evaluateCheck(context, check))
364
694
  );
365
695
  const attestation = activeAttestation(control, attestations, now);
696
+ const agentEvidence = activeAgentEvidence(control, agentClaim, now);
366
697
  const checksPassed = evidence.every(({ status: status2 }) => status2 === "met");
367
698
  const hasManualCheck = control.evidence.some(({ type }) => type === "manual");
368
699
  let status = checksPassed ? "met" : "not_met";
369
- let confidence = checksPassed && !hasManualCheck ? "verified" : "none";
370
- if (attestation?.status === "not_applicable") {
700
+ let confidence = checksPassed && !hasManualCheck ? "repository-detected" : "none";
701
+ const attestationStatus = attestation?.status === "unknown" ? null : attestation?.status ?? null;
702
+ const hasExternalConflict = agentEvidence !== null && agentEvidence.status !== "unknown" && attestationStatus !== null && agentEvidence.status !== attestationStatus;
703
+ if (checksPassed && !hasManualCheck) {
704
+ } else if (hasExternalConflict) {
705
+ status = "unknown";
706
+ confidence = "none";
707
+ } else if (agentEvidence && agentEvidence.status !== "unknown") {
708
+ status = agentEvidence.status;
709
+ confidence = "agent-collected";
710
+ } else if (attestation?.status === "not_applicable") {
371
711
  status = "not_applicable";
372
712
  confidence = "attested";
373
- } else if (attestation?.status === "met" && control.allow_attestation) {
713
+ } else if (attestation?.status === "met") {
374
714
  status = "met";
375
- confidence = checksPassed && !hasManualCheck ? "verified" : "attested";
715
+ confidence = "attested";
376
716
  } else if (attestation?.status === "not_met" || attestation?.status === "unknown") {
377
717
  status = attestation.status;
378
718
  confidence = "attested";
719
+ } else if (agentEvidence?.status === "unknown") {
720
+ status = "unknown";
721
+ confidence = "agent-collected";
379
722
  } else if (evidence.some(({ status: checkStatus }) => checkStatus === "unknown")) {
380
723
  status = "unknown";
381
724
  }
@@ -389,6 +732,7 @@ async function evaluateControl(repo, control, attestations, now = /* @__PURE__ *
389
732
  status,
390
733
  confidence,
391
734
  evidence,
735
+ agent_evidence: agentEvidence,
392
736
  attestation,
393
737
  remediation: control.remediation
394
738
  };
@@ -429,15 +773,27 @@ function assessProfiles(benchmark, dimensions, controls) {
429
773
  return { id: profile.id, title: profile.title, passed: blockers.length === 0, blockers };
430
774
  });
431
775
  }
432
- async function assess(repo, benchmark, catalog, profileId, attestations, now = /* @__PURE__ */ new Date()) {
776
+ async function assess(repo, benchmark, catalog, profileId, options = {}) {
433
777
  const profile = benchmark.readiness_profiles.find(({ id }) => id === profileId);
434
778
  if (!profile) {
435
779
  throw new Error(
436
780
  `Unknown profile ${profileId}. Choose: ${benchmark.readiness_profiles.map(({ id }) => id).join(", ")}`
437
781
  );
438
782
  }
783
+ const scope = options.scope ?? "tracked";
784
+ const now = options.now ?? /* @__PURE__ */ new Date();
785
+ const context = await createRepositoryContext(repo, scope, options.excludedPaths);
786
+ validateAgentEvidence(benchmark, catalog, context, options.agentEvidence ?? null, now);
439
787
  const controls = await Promise.all(
440
- catalog.map(async (control) => evaluateControl(repo, control, attestations, now))
788
+ catalog.map(
789
+ async (control) => evaluateControl(
790
+ context,
791
+ control,
792
+ options.attestations ?? null,
793
+ options.agentEvidence?.claims[control.id] ?? null,
794
+ now
795
+ )
796
+ )
441
797
  );
442
798
  const dimensions = benchmark.dimensions.map(({ id, title }) => {
443
799
  const dimensionControls = controls.filter((control) => control.dimension === id);
@@ -454,37 +810,90 @@ async function assess(repo, benchmark, catalog, profileId, attestations, now = /
454
810
  const highestProfile = [...profiles].reverse().find(({ passed }) => passed)?.id ?? null;
455
811
  const targetPassed = profiles.find(({ id }) => id === profileId)?.passed ?? false;
456
812
  return {
457
- schema_version: "0.1.0",
813
+ schema_version: "0.2.0",
458
814
  benchmark: { id: benchmark.id, version: benchmark.version },
459
- target: { repository: repo, profile: profileId },
815
+ target: {
816
+ repository: repo,
817
+ profile: profileId,
818
+ scope,
819
+ git_head: context.metadata.git_head,
820
+ git_remote: context.metadata.git_remote,
821
+ working_tree_dirty: context.metadata.working_tree_dirty
822
+ },
460
823
  assessed_at: now.toISOString(),
461
824
  score: { total, maximum: 40, percentage: Math.round(total / 40 * 100) },
462
825
  evidence_summary: {
463
- verified: controls.filter(
464
- ({ confidence, status }) => confidence === "verified" && status === "met"
826
+ repository_detected: controls.filter(
827
+ ({ confidence, status }) => confidence === "repository-detected" && status === "met"
828
+ ).length,
829
+ agent_collected: controls.filter(
830
+ ({ confidence, status }) => confidence === "agent-collected" && status === "met"
465
831
  ).length,
466
832
  attested: controls.filter(
467
833
  ({ confidence, status }) => confidence === "attested" && status === "met"
468
834
  ).length,
469
- unmet_or_unknown: controls.filter(
470
- ({ status }) => status === "not_met" || status === "unknown"
471
- ).length
835
+ unmet: controls.filter(({ status }) => status === "not_met").length,
836
+ unknown: controls.filter(({ status }) => status === "unknown").length
472
837
  },
473
838
  dimensions,
474
839
  controls,
475
840
  profiles,
476
841
  readiness: { highest_profile: highestProfile, target_passed: targetPassed },
477
842
  limitations: [
478
- "Repository evidence proves that an artifact exists, not that people consistently follow it.",
479
- "Self-attestations are reported separately and are not tool-verified evidence.",
843
+ scope === "tracked" ? "Tracked mode considers only Git-tracked paths, using current working-tree contents; uncommitted edits to tracked files can affect the result." : "Workspace mode includes untracked local files and is provisional; do not compare it directly with tracked-mode reports.",
844
+ "Repository-detected evidence proves a qualifying artifact match, not consistent practice or external enforcement.",
845
+ "Agent-collected evidence and human attestations are reported separately and are not independently verified.",
480
846
  "This assessment does not grant production access, deployment authority, or certification."
481
847
  ]
482
848
  };
483
849
  }
850
+ function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
851
+ if (!evidence) return;
852
+ if (evidence.benchmark_version !== benchmark.version) {
853
+ throw new Error(
854
+ `Agent evidence benchmark ${evidence.benchmark_version} does not match ${benchmark.version}`
855
+ );
856
+ }
857
+ const expectedTarget = repositoryEvidenceTarget(context.metadata);
858
+ if (evidence.target.repository !== expectedTarget.repository) {
859
+ throw new Error(
860
+ `Agent evidence target ${evidence.target.repository} does not match ${expectedTarget.repository}`
861
+ );
862
+ }
863
+ if (evidence.target.git_head !== expectedTarget.git_head) {
864
+ throw new Error(
865
+ `Agent evidence commit ${evidence.target.git_head ?? "unavailable"} does not match ${expectedTarget.git_head ?? "an unavailable Git commit"}`
866
+ );
867
+ }
868
+ const controls = new Map(catalog.map((control) => [control.id, control]));
869
+ if (Object.keys(evidence.claims).length > 0 && (evidence.collector.name.startsWith("TODO") || evidence.collector.version.startsWith("TODO"))) {
870
+ throw new Error("Agent evidence with claims must identify the collector name and version");
871
+ }
872
+ for (const [controlId, claim] of Object.entries(evidence.claims)) {
873
+ const control = controls.get(controlId);
874
+ if (!control) throw new Error(`Agent evidence references unknown control ${controlId}`);
875
+ if (!control.allow_agent_evidence) {
876
+ throw new Error(`${controlId} does not permit agent-collected evidence`);
877
+ }
878
+ const allowedScopes = control.evidence.filter(({ type }) => type === "manual").map(({ scope: evidenceScope }) => evidenceScope);
879
+ if (!allowedScopes.includes(claim.scope)) {
880
+ throw new Error(`${controlId} does not accept ${claim.scope} evidence`);
881
+ }
882
+ if (new Date(claim.collected_at) > new Date(claim.expires_at)) {
883
+ throw new Error(`${controlId} expires before it was collected`);
884
+ }
885
+ if (new Date(claim.collected_at) > now) {
886
+ throw new Error(`${controlId} has a future collection timestamp`);
887
+ }
888
+ if (claim.summary.startsWith("TODO") || claim.references.some((reference) => reference.startsWith("TODO"))) {
889
+ throw new Error(`${controlId} contains unresolved TODO evidence`);
890
+ }
891
+ }
892
+ }
484
893
 
485
894
  // src/cli.ts
486
895
  var program = new Command();
487
- program.name("agentic-scorecard").description("Evidence-backed readiness assessment for agentic software development harnesses").version("0.1.0");
896
+ program.name("agentic-scorecard").description("Evidence-backed readiness assessment for agentic software development harnesses").version("0.2.0");
488
897
  program.command("validate").description("Validate the bundled benchmark catalog").action(async () => {
489
898
  const { benchmark, controls } = await loadBenchmark();
490
899
  process.stdout.write(
@@ -510,7 +919,7 @@ ${control.evidence.map((check) => `- ${stringify(check).trim().replaceAll("\n",
510
919
  );
511
920
  });
512
921
  program.command("init").argument("[repository]", "repository to initialize", ".").option("--force", "replace an existing attestation file", false).description("Create a manual-attestation template").action(async (repository, options) => {
513
- const repo = resolve3(repository);
922
+ const repo = resolve4(repository);
514
923
  const path = join2(repo, ".agentic", "attestations.yaml");
515
924
  if (!options.force) {
516
925
  try {
@@ -521,6 +930,9 @@ program.command("init").argument("[repository]", "repository to initialize", "."
521
930
  }
522
931
  }
523
932
  const { benchmark, controls } = await loadBenchmark();
933
+ const reviewedAt = /* @__PURE__ */ new Date();
934
+ const expiresAt = new Date(reviewedAt);
935
+ expiresAt.setDate(expiresAt.getDate() + 90);
524
936
  const attestations = Object.fromEntries(
525
937
  controls.filter((control) => control.evidence.some(({ type }) => type === "manual")).map((control) => {
526
938
  const manualCheck = control.evidence.find(
@@ -532,8 +944,8 @@ program.command("init").argument("[repository]", "repository to initialize", "."
532
944
  status: "unknown",
533
945
  evidence: `TODO: ${manualCheck?.prompt ?? control.outcome}`,
534
946
  owner: "TODO",
535
- reviewed_at: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
536
- expires_at: null
947
+ reviewed_at: reviewedAt.toISOString().slice(0, 10),
948
+ expires_at: expiresAt.toISOString().slice(0, 10)
537
949
  }
538
950
  ];
539
951
  })
@@ -541,30 +953,123 @@ program.command("init").argument("[repository]", "repository to initialize", "."
541
953
  await mkdir(dirname2(path), { recursive: true });
542
954
  await writeFile(
543
955
  path,
544
- `# Claims are visible as attested, never tool-verified. Link durable evidence; do not paste secrets.
956
+ `# Claims are visibly human-attested. Link durable evidence; do not paste secrets.
545
957
  ${stringify({ benchmark_version: benchmark.version, attestations })}`,
546
958
  "utf8"
547
959
  );
548
960
  process.stdout.write(`Created ${path}
549
961
  `);
550
962
  });
551
- program.command("assess").argument("[repository]", "repository to assess", ".").option("--profile <profile>", "target autonomy profile", "pr-creation").option("--format <format>", "json or markdown", "markdown").option("--output <path>", "write the report to a file").option("--attestations <path>", "manual attestation file").option("--enforce", "exit non-zero when the target profile fails", false).option("--github-output", "append summary values to $GITHUB_OUTPUT", false).description("Assess a repository using local, read-only evidence collection").action(
963
+ program.command("init-evidence").argument("[repository]", "repository to prepare external evidence for", ".").option("--output <path>", "agent evidence bundle path").option("--request-output <path>", "human-readable evidence request path").option("--force", "replace an existing agent evidence bundle", false).description("Create a target-bound template for agent-collected external evidence").action(
964
+ async (repository, options) => {
965
+ const repo = resolve4(repository);
966
+ const path = resolve4(options.output ?? join2(repo, ".agentic", "agent-evidence.yaml"));
967
+ const requestPath = resolve4(
968
+ options.requestOutput ?? join2(repo, ".agentic", "evidence-request.md")
969
+ );
970
+ if (path === requestPath) {
971
+ throw new Error("Agent evidence bundle and request paths must be different");
972
+ }
973
+ if (!options.force) {
974
+ for (const candidate of [path, requestPath]) {
975
+ try {
976
+ await readFile3(candidate, "utf8");
977
+ throw new Error(`${candidate} already exists; use --force to replace it`);
978
+ } catch (error) {
979
+ if (error.code !== "ENOENT") throw error;
980
+ }
981
+ }
982
+ }
983
+ const { benchmark, controls } = await loadBenchmark();
984
+ const context = await createRepositoryContext(repo, "tracked").catch((error) => {
985
+ if (error instanceof Error && error.message.startsWith("Tracked assessment requires a Git worktree")) {
986
+ throw new Error(
987
+ "init-evidence requires a Git worktree with a commit so the bundle can be target-bound."
988
+ );
989
+ }
990
+ throw error;
991
+ });
992
+ if (!context.metadata.git_head) {
993
+ throw new Error(
994
+ "init-evidence requires a Git commit so the bundle can be target-bound. Commit the assessed state and try again."
995
+ );
996
+ }
997
+ const eligibleControls = controls.filter(({ allow_agent_evidence: allowed }) => allowed);
998
+ const bundle = {
999
+ schema_version: "0.2.0",
1000
+ benchmark_version: benchmark.version,
1001
+ target: repositoryEvidenceTarget(context.metadata),
1002
+ collector: { name: "TODO: agent or adapter name", version: "TODO" },
1003
+ claims: {}
1004
+ };
1005
+ await mkdir(dirname2(path), { recursive: true });
1006
+ await writeFile(
1007
+ path,
1008
+ `# Use authorized read-only tools. Do not paste secrets or raw sensitive content.
1009
+ ${stringify(bundle)}`,
1010
+ "utf8"
1011
+ );
1012
+ await mkdir(dirname2(requestPath), { recursive: true });
1013
+ await writeFile(
1014
+ requestPath,
1015
+ [
1016
+ "# ADRB v0.2 external evidence request",
1017
+ "",
1018
+ `- Repository: ${bundle.target.repository}`,
1019
+ `- Git commit: ${bundle.target.git_head ?? "unavailable"}`,
1020
+ `- Benchmark: ${benchmark.version}`,
1021
+ "",
1022
+ "Obtain authorization before accessing connected systems. Use read-only, least-privileged tools. Add only attempted claims to the bundle; errors remain `unknown`. Never paste secrets or raw sensitive content.",
1023
+ "",
1024
+ ...eligibleControls.flatMap((control) => {
1025
+ const manualCheck = control.evidence.find(
1026
+ (check) => check.type === "manual"
1027
+ );
1028
+ return [
1029
+ `## ${control.id} \u2014 ${control.title}`,
1030
+ "",
1031
+ `- Scope: ${manualCheck?.scope ?? "organization"}`,
1032
+ `- Request: ${manualCheck?.prompt ?? control.outcome}`,
1033
+ `- Risk: ${control.risk}`,
1034
+ ""
1035
+ ];
1036
+ })
1037
+ ].join("\n"),
1038
+ "utf8"
1039
+ );
1040
+ process.stdout.write(`Created ${path}
1041
+ Created ${requestPath}
1042
+ `);
1043
+ }
1044
+ );
1045
+ program.command("assess").argument("[repository]", "repository to assess", ".").option("--profile <profile>", "target autonomy profile", "pr-creation").option("--format <format>", "json or markdown", "markdown").option("--output <path>", "write the report to a file").option("--attestations <path>", "manual attestation file").option("--agent-evidence <path>", "agent-collected external evidence bundle").option("--scope <scope>", "tracked or workspace", "tracked").option("--enforce", "exit non-zero when the target profile fails", false).option("--github-output", "append summary values to $GITHUB_OUTPUT", false).description("Assess a repository using local, read-only evidence collection").action(
552
1046
  async (repository, options) => {
553
1047
  if (!["json", "markdown"].includes(options.format)) {
554
1048
  throw new Error("--format must be json or markdown");
555
1049
  }
556
- const repo = resolve3(repository);
1050
+ if (!["tracked", "workspace"].includes(options.scope)) {
1051
+ throw new Error("--scope must be tracked or workspace");
1052
+ }
1053
+ const repo = resolve4(repository);
557
1054
  const { benchmark, controls } = await loadBenchmark();
558
- const attestationPath = resolve3(
1055
+ const attestationPath = resolve4(
559
1056
  options.attestations ?? join2(repo, ".agentic", "attestations.yaml")
560
1057
  );
561
1058
  const attestations = await loadAttestations(attestationPath, benchmark.version);
562
- const report = await assess(repo, benchmark, controls, options.profile, attestations);
1059
+ const agentEvidencePath = resolve4(
1060
+ options.agentEvidence ?? join2(repo, ".agentic", "agent-evidence.yaml")
1061
+ );
1062
+ const agentEvidence = await loadAgentEvidence(agentEvidencePath);
1063
+ const reportPath = options.output ? resolve4(options.output) : null;
1064
+ const report = await assess(repo, benchmark, controls, options.profile, {
1065
+ scope: options.scope,
1066
+ attestations,
1067
+ agentEvidence,
1068
+ excludedPaths: [attestationPath, agentEvidencePath, ...reportPath ? [reportPath] : []]
1069
+ });
563
1070
  const output = options.format === "json" ? `${JSON.stringify(report, null, 2)}
564
1071
  ` : toMarkdown(report);
565
- let reportPath = null;
566
- if (options.output) {
567
- reportPath = resolve3(options.output);
1072
+ if (reportPath) {
568
1073
  await mkdir(dirname2(reportPath), { recursive: true });
569
1074
  await writeFile(reportPath, output, "utf8");
570
1075
  process.stdout.write(`Wrote ${reportPath}