auditor-lambda 0.2.8 → 0.2.9

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.
Files changed (98) hide show
  1. package/README.md +6 -0
  2. package/audit-code-wrapper-lib.mjs +1 -1
  3. package/dist/adapters/eslint.js +9 -5
  4. package/dist/cli.d.ts +42 -1
  5. package/dist/cli.js +114 -64
  6. package/dist/extractors/bucketing.d.ts +4 -0
  7. package/dist/extractors/bucketing.js +6 -2
  8. package/dist/extractors/disposition.d.ts +4 -0
  9. package/dist/extractors/disposition.js +6 -2
  10. package/dist/extractors/fileInventory.js +24 -28
  11. package/dist/extractors/flows.d.ts +5 -0
  12. package/dist/extractors/flows.js +18 -38
  13. package/dist/extractors/pathPatterns.d.ts +10 -3
  14. package/dist/extractors/pathPatterns.js +109 -61
  15. package/dist/extractors/surfaces.d.ts +4 -0
  16. package/dist/extractors/surfaces.js +11 -11
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +2 -1
  19. package/dist/io/artifacts.d.ts +55 -40
  20. package/dist/io/artifacts.js +73 -110
  21. package/dist/io/json.js +52 -21
  22. package/dist/io/runArtifacts.d.ts +1 -1
  23. package/dist/io/runArtifacts.js +26 -3
  24. package/dist/orchestrator/advance.js +83 -62
  25. package/dist/orchestrator/flowCoverage.js +11 -5
  26. package/dist/orchestrator/flowPlanning.d.ts +7 -2
  27. package/dist/orchestrator/flowPlanning.js +46 -21
  28. package/dist/orchestrator/flowRequeue.js +28 -8
  29. package/dist/orchestrator/internalExecutors.js +12 -8
  30. package/dist/orchestrator/planning.js +25 -3
  31. package/dist/orchestrator/requeue.js +11 -1
  32. package/dist/orchestrator/taskBuilder.d.ts +4 -2
  33. package/dist/orchestrator/taskBuilder.js +153 -52
  34. package/dist/orchestrator/unitBuilder.d.ts +3 -1
  35. package/dist/orchestrator/unitBuilder.js +24 -16
  36. package/dist/prompts/renderWorkerPrompt.d.ts +1 -1
  37. package/dist/prompts/renderWorkerPrompt.js +16 -8
  38. package/dist/providers/claudeCodeProvider.d.ts +4 -1
  39. package/dist/providers/claudeCodeProvider.js +8 -5
  40. package/dist/providers/localSubprocessProvider.d.ts +4 -0
  41. package/dist/providers/localSubprocessProvider.js +7 -2
  42. package/dist/providers/spawnLoggedCommand.d.ts +9 -1
  43. package/dist/providers/spawnLoggedCommand.js +77 -29
  44. package/dist/reporting/synthesis.d.ts +2 -0
  45. package/dist/reporting/synthesis.js +12 -9
  46. package/dist/supervisor/operatorHandoff.js +48 -18
  47. package/dist/supervisor/runLedger.d.ts +1 -1
  48. package/dist/supervisor/runLedger.js +112 -5
  49. package/dist/supervisor/sessionConfig.js +10 -10
  50. package/dist/types/externalAnalyzer.d.ts +3 -0
  51. package/dist/types/flowCoverage.d.ts +5 -1
  52. package/dist/types/flowCoverage.js +5 -1
  53. package/dist/types/flows.d.ts +5 -1
  54. package/dist/types/flows.js +1 -1
  55. package/dist/types/runLedger.d.ts +5 -1
  56. package/dist/types/runLedger.js +6 -1
  57. package/dist/types/runtimeValidation.d.ts +12 -3
  58. package/dist/types/runtimeValidation.js +16 -1
  59. package/dist/types/sessionConfig.d.ts +15 -2
  60. package/dist/types/sessionConfig.js +15 -1
  61. package/dist/types/surfaces.d.ts +4 -1
  62. package/dist/types/surfaces.js +1 -1
  63. package/dist/types/workerSession.d.ts +9 -0
  64. package/dist/types/workerSession.js +5 -1
  65. package/dist/validation/artifacts.d.ts +1 -1
  66. package/dist/validation/artifacts.js +33 -20
  67. package/dist/validation/auditResults.d.ts +2 -2
  68. package/dist/validation/auditResults.js +7 -15
  69. package/dist/validation/basic.d.ts +9 -1
  70. package/dist/validation/basic.js +40 -3
  71. package/dist/validation/sessionConfig.d.ts +4 -2
  72. package/dist/validation/sessionConfig.js +62 -15
  73. package/docs/agent-integrations.md +29 -9
  74. package/docs/next-steps.md +21 -4
  75. package/docs/packaging.md +14 -0
  76. package/docs/product-direction.md +22 -0
  77. package/docs/production-launch-bar.md +2 -0
  78. package/docs/releasing.md +17 -0
  79. package/docs/remediation-baseline.md +75 -0
  80. package/docs/run-flow.md +23 -11
  81. package/docs/session-config.md +50 -5
  82. package/docs/supervisor.md +7 -0
  83. package/docs/workflow-refactor-brief.md +177 -0
  84. package/package.json +1 -1
  85. package/schemas/audit_result.schema.json +4 -1
  86. package/schemas/audit_task.schema.json +3 -1
  87. package/schemas/coverage_matrix.schema.json +3 -3
  88. package/schemas/critical_flows.schema.json +6 -2
  89. package/schemas/file_disposition.schema.json +2 -2
  90. package/schemas/finding.schema.json +9 -4
  91. package/schemas/flow_coverage.schema.json +2 -2
  92. package/schemas/repo_manifest.schema.json +4 -4
  93. package/schemas/risk_register.schema.json +2 -2
  94. package/schemas/runtime_validation_report.schema.json +2 -2
  95. package/schemas/runtime_validation_tasks.schema.json +8 -2
  96. package/schemas/surface_manifest.schema.json +6 -3
  97. package/schemas/unit_manifest.schema.json +3 -2
  98. package/skills/audit-code/SKILL.md +5 -0
@@ -1,3 +1,4 @@
1
+ /** One normalized result imported from an external analyzer such as eslint or tsc. */
1
2
  export interface ExternalAnalyzerResultItem {
2
3
  id: string;
3
4
  category: string;
@@ -7,8 +8,10 @@ export interface ExternalAnalyzerResultItem {
7
8
  line_end?: number;
8
9
  summary: string;
9
10
  rule?: string;
11
+ /** Preserves the analyzer-native payload when consumers need original detail. */
10
12
  raw?: unknown;
11
13
  }
14
+ /** Imported analyzer output captured at a single generation time. */
12
15
  export interface ExternalAnalyzerResults {
13
16
  tool: string;
14
17
  generated_at?: string;
@@ -1,11 +1,15 @@
1
+ export declare const FLOW_COVERAGE_STATUSES: readonly ["pending", "partial", "complete"];
2
+ export type FlowCoverageStatus = (typeof FLOW_COVERAGE_STATUSES)[number];
3
+ /** Coverage for one critical flow across the lenses the audit expects to see. */
1
4
  export interface FlowCoverageRecord {
2
5
  flow_id: string;
3
6
  paths: string[];
4
7
  required_lenses: string[];
5
8
  completed_lenses: string[];
6
- status: "pending" | "partial" | "complete";
9
+ status: FlowCoverageStatus;
7
10
  notes?: string[];
8
11
  }
12
+ /** Aggregated flow coverage written beside the critical flow manifest. */
9
13
  export interface FlowCoverageManifest {
10
14
  flows: FlowCoverageRecord[];
11
15
  }
@@ -1 +1,5 @@
1
- export {};
1
+ export const FLOW_COVERAGE_STATUSES = [
2
+ "pending",
3
+ "partial",
4
+ "complete",
5
+ ];
@@ -1,12 +1,16 @@
1
+ export declare const FLOW_CONFIDENCE_LEVELS: readonly ["high", "low"];
2
+ export type FlowConfidenceLevel = (typeof FLOW_CONFIDENCE_LEVELS)[number];
3
+ /** A critical user or system flow that must be covered by the audit. */
1
4
  export interface CriticalFlow {
2
5
  id: string;
3
6
  name: string;
4
7
  entrypoints: string[];
5
8
  paths: string[];
6
9
  concerns: string[];
7
- confidence?: "high" | "low";
10
+ confidence?: FlowConfidenceLevel;
8
11
  notes?: string[];
9
12
  }
13
+ /** The set of critical flows inferred from intake artifacts. */
10
14
  export interface CriticalFlowManifest {
11
15
  flows: CriticalFlow[];
12
16
  fallback_required?: boolean;
@@ -1 +1 @@
1
- export {};
1
+ export const FLOW_CONFIDENCE_LEVELS = ["high", "low"];
@@ -1,13 +1,17 @@
1
+ export declare const RUN_LEDGER_STATUSES: readonly ["completed", "blocked", "failed", "no_progress"];
2
+ export type RunLedgerStatus = (typeof RUN_LEDGER_STATUSES)[number];
3
+ /** One persisted supervisor run entry, including the terminal worker outcome. */
1
4
  export interface RunLedgerEntry {
2
5
  run_id: string;
3
6
  provider: string;
4
7
  obligation_id: string | null;
5
8
  selected_executor: string | null;
6
- status: "completed" | "blocked" | "failed" | "no_progress";
9
+ status: RunLedgerStatus;
7
10
  started_at: string;
8
11
  ended_at: string;
9
12
  result_path: string;
10
13
  }
14
+ /** Append-only ledger used to explain how the audit advanced over time. */
11
15
  export interface RunLedger {
12
16
  runs: RunLedgerEntry[];
13
17
  }
@@ -1 +1,6 @@
1
- export {};
1
+ export const RUN_LEDGER_STATUSES = [
2
+ "completed",
3
+ "blocked",
4
+ "failed",
5
+ "no_progress",
6
+ ];
@@ -1,24 +1,33 @@
1
- export type RuntimeValidationKind = "unit-risk-check" | "critical-flow-check";
1
+ export declare const RUNTIME_VALIDATION_KINDS: readonly ["unit-risk-check", "critical-flow-check"];
2
+ export type RuntimeValidationKind = (typeof RUNTIME_VALIDATION_KINDS)[number];
3
+ export declare const RUNTIME_VALIDATION_PRIORITIES: readonly ["high", "medium", "low"];
4
+ export type RuntimeValidationPriority = (typeof RUNTIME_VALIDATION_PRIORITIES)[number];
5
+ export declare const RUNTIME_VALIDATION_STATUSES: readonly ["pending", "confirmed", "not_confirmed", "inconclusive", "not_required"];
6
+ export type RuntimeValidationStatus = (typeof RUNTIME_VALIDATION_STATUSES)[number];
7
+ /** A deterministic runtime check queued after static review highlights risk. */
2
8
  export interface RuntimeValidationTask {
3
9
  id: string;
4
10
  kind: RuntimeValidationKind;
5
11
  target_paths: string[];
6
12
  reason: string;
7
- priority: "high" | "medium" | "low";
13
+ priority: RuntimeValidationPriority;
8
14
  command?: string[];
9
15
  suggested_checks?: string[];
10
16
  source_artifacts?: string[];
11
17
  }
18
+ /** Planner output for the runtime validation stage. */
12
19
  export interface RuntimeValidationTaskManifest {
13
20
  tasks: RuntimeValidationTask[];
14
21
  }
22
+ /** Result recorded after a runtime validation task runs or is intentionally skipped. */
15
23
  export interface RuntimeValidationResult {
16
24
  task_id: string;
17
- status: "pending" | "confirmed" | "not_confirmed" | "inconclusive" | "not_required";
25
+ status: RuntimeValidationStatus;
18
26
  summary: string;
19
27
  evidence?: string[];
20
28
  notes?: string[];
21
29
  }
30
+ /** Persisted runtime validation outcomes keyed by generated task id. */
22
31
  export interface RuntimeValidationReport {
23
32
  results: RuntimeValidationResult[];
24
33
  }
@@ -1 +1,16 @@
1
- export {};
1
+ export const RUNTIME_VALIDATION_KINDS = [
2
+ "unit-risk-check",
3
+ "critical-flow-check",
4
+ ];
5
+ export const RUNTIME_VALIDATION_PRIORITIES = [
6
+ "high",
7
+ "medium",
8
+ "low",
9
+ ];
10
+ export const RUNTIME_VALIDATION_STATUSES = [
11
+ "pending",
12
+ "confirmed",
13
+ "not_confirmed",
14
+ "inconclusive",
15
+ "not_required",
16
+ ];
@@ -1,5 +1,8 @@
1
- export type ProviderName = "auto" | "local-subprocess" | "subprocess-template" | "claude-code" | "opencode" | "vscode-task";
1
+ export declare const PROVIDER_NAMES: readonly ["auto", "local-subprocess", "subprocess-template", "claude-code", "opencode", "vscode-task"];
2
+ export type ProviderName = (typeof PROVIDER_NAMES)[number];
2
3
  export type ResolvedProviderName = Exclude<ProviderName, "auto">;
4
+ export declare const SESSION_UI_MODES: readonly ["visible", "headless"];
5
+ export type SessionUiMode = (typeof SESSION_UI_MODES)[number];
3
6
  export interface SubprocessTemplateConfig {
4
7
  command_template: string[];
5
8
  env?: Record<string, string>;
@@ -16,10 +19,20 @@ export interface VSCodeTaskConfig {
16
19
  command_template: string[];
17
20
  env?: Record<string, string>;
18
21
  }
22
+ export declare const PROVIDER_SECTION_KEYS: {
23
+ readonly "subprocess-template": "subprocess_template";
24
+ readonly "claude-code": "claude_code";
25
+ readonly opencode: "opencode";
26
+ readonly "vscode-task": "vscode_task";
27
+ };
28
+ /**
29
+ * Provider names use CLI-friendly hyphenation, while nested provider config
30
+ * sections stay snake_case because they serialize directly into JSON files.
31
+ */
19
32
  export interface SessionConfig {
20
33
  provider?: ProviderName;
21
34
  timeout_ms?: number;
22
- ui_mode?: "visible" | "headless";
35
+ ui_mode?: SessionUiMode;
23
36
  subprocess_template?: SubprocessTemplateConfig;
24
37
  claude_code?: ClaudeCodeConfig;
25
38
  opencode?: OpenCodeConfig;
@@ -1 +1,15 @@
1
- export {};
1
+ export const PROVIDER_NAMES = [
2
+ "auto",
3
+ "local-subprocess",
4
+ "subprocess-template",
5
+ "claude-code",
6
+ "opencode",
7
+ "vscode-task",
8
+ ];
9
+ export const SESSION_UI_MODES = ["visible", "headless"];
10
+ export const PROVIDER_SECTION_KEYS = {
11
+ "subprocess-template": "subprocess_template",
12
+ "claude-code": "claude_code",
13
+ opencode: "opencode",
14
+ "vscode-task": "vscode_task",
15
+ };
@@ -1,4 +1,6 @@
1
- export type SurfaceKind = "interface" | "background";
1
+ export declare const SURFACE_KINDS: readonly ["interface", "background"];
2
+ export type SurfaceKind = (typeof SURFACE_KINDS)[number];
3
+ /** Discovered execution surfaces that define where the product can be reached. */
2
4
  export interface SurfaceRecord {
3
5
  id: string;
4
6
  kind: SurfaceKind;
@@ -7,6 +9,7 @@ export interface SurfaceRecord {
7
9
  methods?: string[];
8
10
  notes?: string[];
9
11
  }
12
+ /** Intake output that summarizes externally reachable product surfaces. */
10
13
  export interface SurfaceManifest {
11
14
  surfaces: SurfaceRecord[];
12
15
  }
@@ -1 +1 @@
1
- export {};
1
+ export const SURFACE_KINDS = ["interface", "background"];
@@ -1,3 +1,9 @@
1
+ export declare const WORKER_COMMAND_MODES: readonly ["run", "deferred"];
2
+ export type WorkerCommandMode = (typeof WORKER_COMMAND_MODES)[number];
3
+ /**
4
+ * Worker tasks serialize directly to task.json, so their persisted field names
5
+ * intentionally stay snake_case for consistency across providers and bridges.
6
+ */
1
7
  export interface WorkerTask {
2
8
  contract_version: "audit-code-worker/v1alpha1";
3
9
  run_id: string;
@@ -11,7 +17,10 @@ export interface WorkerTask {
11
17
  pending_audit_tasks_path?: string;
12
18
  runtime_updates_path?: string;
13
19
  external_analyzer_results_path?: string;
20
+ worker_command_mode?: WorkerCommandMode;
21
+ /** @deprecated Prefer worker_command_mode: "deferred" for new task files. */
14
22
  skip_worker_command?: boolean;
15
23
  timeout_ms?: number;
16
24
  max_retries?: number;
17
25
  }
26
+ export declare function usesDeferredWorkerCommand(task: Pick<WorkerTask, "worker_command_mode" | "skip_worker_command">): boolean;
@@ -1 +1,5 @@
1
- export {};
1
+ export const WORKER_COMMAND_MODES = ["run", "deferred"];
2
+ export function usesDeferredWorkerCommand(task) {
3
+ return (task.worker_command_mode === "deferred" ||
4
+ task.skip_worker_command === true);
5
+ }
@@ -1,3 +1,3 @@
1
1
  import type { ArtifactBundle } from "../io/artifacts.js";
2
- import type { ValidationIssue } from "./basic.js";
2
+ import { type ValidationIssue } from "./basic.js";
3
3
  export declare function validateArtifactBundle(bundle: ArtifactBundle): ValidationIssue[];
@@ -1,6 +1,9 @@
1
- import { requireKeys } from "./basic.js";
1
+ import { pushValidationIssue, requireKeys, } from "./basic.js";
2
2
  function pushIssue(issues, path, message) {
3
- issues.push({ path, message });
3
+ pushValidationIssue(issues, path, message);
4
+ }
5
+ function asArray(value) {
6
+ return Array.isArray(value) ? value : [];
4
7
  }
5
8
  export function validateArtifactBundle(bundle) {
6
9
  const issues = [];
@@ -37,14 +40,24 @@ export function validateArtifactBundle(bundle) {
37
40
  if (bundle.external_analyzer_results) {
38
41
  issues.push(...requireKeys(bundle.external_analyzer_results, "external_analyzer_results", ["tool", "results"]));
39
42
  }
40
- const repoPaths = new Set(bundle.repo_manifest?.files.map((file) => file.path) ?? []);
41
- const dispositionMap = new Map(bundle.file_disposition?.files.map((item) => [item.path, item.status]) ??
42
- []);
43
- const unitIds = new Set(bundle.unit_manifest?.units.map((unit) => unit.unit_id) ?? []);
44
- const flowIds = new Set(bundle.critical_flows?.flows.map((flow) => flow.id) ?? []);
45
- const runtimeTaskIds = new Set(bundle.runtime_validation_tasks?.tasks.map((task) => task.id) ?? []);
43
+ const repoManifestFiles = asArray(bundle.repo_manifest?.files);
44
+ const fileDispositionEntries = asArray(bundle.file_disposition?.files);
45
+ const unitManifestUnits = asArray(bundle.unit_manifest?.units);
46
+ const criticalFlows = asArray(bundle.critical_flows?.flows);
47
+ const flowCoverageEntries = asArray(bundle.flow_coverage?.flows);
48
+ const riskRegisterItems = asArray(bundle.risk_register?.items);
49
+ const surfaceEntries = asArray(bundle.surface_manifest?.surfaces);
50
+ const runtimeValidationTasks = asArray(bundle.runtime_validation_tasks?.tasks);
51
+ const runtimeValidationResults = asArray(bundle.runtime_validation_report?.results);
52
+ const externalAnalyzerResults = asArray(bundle.external_analyzer_results?.results);
53
+ const coverageFiles = asArray(bundle.coverage_matrix?.files);
54
+ const repoPaths = new Set(repoManifestFiles.map((file) => file.path));
55
+ const dispositionMap = new Map(fileDispositionEntries.map((item) => [item.path, item.status]));
56
+ const unitIds = new Set(unitManifestUnits.map((unit) => unit.unit_id));
57
+ const flowIds = new Set(criticalFlows.map((flow) => flow.id));
58
+ const runtimeTaskIds = new Set(runtimeValidationTasks.map((task) => task.id));
46
59
  if (bundle.repo_manifest && bundle.coverage_matrix) {
47
- const coveragePaths = new Set(bundle.coverage_matrix.files.map((file) => file.path));
60
+ const coveragePaths = new Set(coverageFiles.map((file) => file.path));
48
61
  for (const path of repoPaths) {
49
62
  if (!coveragePaths.has(path)) {
50
63
  pushIssue(issues, "coverage_matrix", `Missing coverage entry for ${path}`);
@@ -52,7 +65,7 @@ export function validateArtifactBundle(bundle) {
52
65
  }
53
66
  }
54
67
  if (bundle.repo_manifest && bundle.file_disposition) {
55
- const dispositionPaths = new Set(bundle.file_disposition.files.map((file) => file.path));
68
+ const dispositionPaths = new Set(fileDispositionEntries.map((file) => file.path));
56
69
  for (const path of repoPaths) {
57
70
  if (!dispositionPaths.has(path)) {
58
71
  pushIssue(issues, "file_disposition", `Missing disposition entry for ${path}`);
@@ -60,7 +73,7 @@ export function validateArtifactBundle(bundle) {
60
73
  }
61
74
  }
62
75
  if (bundle.unit_manifest) {
63
- for (const unit of bundle.unit_manifest.units) {
76
+ for (const unit of unitManifestUnits) {
64
77
  if (unit.files.length === 0) {
65
78
  pushIssue(issues, `unit_manifest:${unit.unit_id}`, "Unit has no files");
66
79
  }
@@ -79,7 +92,7 @@ export function validateArtifactBundle(bundle) {
79
92
  }
80
93
  }
81
94
  if (bundle.coverage_matrix && bundle.unit_manifest) {
82
- for (const file of bundle.coverage_matrix.files) {
95
+ for (const file of coverageFiles) {
83
96
  if (!repoPaths.has(file.path)) {
84
97
  pushIssue(issues, "coverage_matrix", `Coverage contains unknown file ${file.path}`);
85
98
  }
@@ -103,7 +116,7 @@ export function validateArtifactBundle(bundle) {
103
116
  }
104
117
  }
105
118
  if (bundle.critical_flows) {
106
- for (const flow of bundle.critical_flows.flows) {
119
+ for (const flow of criticalFlows) {
107
120
  if (flow.paths.length === 0) {
108
121
  pushIssue(issues, `critical_flows:${flow.id}`, "Flow has no paths");
109
122
  }
@@ -122,7 +135,7 @@ export function validateArtifactBundle(bundle) {
122
135
  }
123
136
  }
124
137
  if (bundle.flow_coverage && bundle.critical_flows) {
125
- for (const flow of bundle.flow_coverage.flows) {
138
+ for (const flow of flowCoverageEntries) {
126
139
  if (!flowIds.has(flow.flow_id)) {
127
140
  pushIssue(issues, `flow_coverage:${flow.flow_id}`, `Flow coverage references unknown flow ${flow.flow_id}`);
128
141
  }
@@ -143,15 +156,15 @@ export function validateArtifactBundle(bundle) {
143
156
  }
144
157
  }
145
158
  if (bundle.risk_register && bundle.unit_manifest) {
146
- const riskUnitIds = new Set(bundle.risk_register.items.map((item) => item.unit_id));
147
- for (const unit of bundle.unit_manifest.units) {
159
+ const riskUnitIds = new Set(riskRegisterItems.map((item) => item.unit_id));
160
+ for (const unit of unitManifestUnits) {
148
161
  if (!riskUnitIds.has(unit.unit_id)) {
149
162
  pushIssue(issues, "risk_register", `Missing risk entry for unit ${unit.unit_id}`);
150
163
  }
151
164
  }
152
165
  }
153
166
  if (bundle.surface_manifest) {
154
- for (const surface of bundle.surface_manifest.surfaces) {
167
+ for (const surface of surfaceEntries) {
155
168
  if (!repoPaths.has(surface.entrypoint)) {
156
169
  pushIssue(issues, `surface_manifest:${surface.id}`, `Surface references unknown entrypoint ${surface.entrypoint}`);
157
170
  }
@@ -162,7 +175,7 @@ export function validateArtifactBundle(bundle) {
162
175
  }
163
176
  }
164
177
  if (bundle.runtime_validation_tasks) {
165
- for (const task of bundle.runtime_validation_tasks.tasks) {
178
+ for (const task of runtimeValidationTasks) {
166
179
  if (task.target_paths.length === 0) {
167
180
  pushIssue(issues, `runtime_validation_tasks:${task.id}`, "Runtime validation task has no target paths");
168
181
  }
@@ -174,14 +187,14 @@ export function validateArtifactBundle(bundle) {
174
187
  }
175
188
  }
176
189
  if (bundle.runtime_validation_report) {
177
- for (const result of bundle.runtime_validation_report.results) {
190
+ for (const result of runtimeValidationResults) {
178
191
  if (!runtimeTaskIds.has(result.task_id)) {
179
192
  pushIssue(issues, `runtime_validation_report:${result.task_id}`, `Runtime validation result references unknown task ${result.task_id}`);
180
193
  }
181
194
  }
182
195
  }
183
196
  if (bundle.external_analyzer_results) {
184
- for (const item of bundle.external_analyzer_results.results) {
197
+ for (const item of externalAnalyzerResults) {
185
198
  if (!repoPaths.has(item.path) && bundle.repo_manifest) {
186
199
  pushIssue(issues, `external_analyzer_results:${item.id}`, `External analyzer result references unknown path ${item.path}`);
187
200
  }
@@ -1,11 +1,11 @@
1
1
  import type { AuditTask } from "../types.js";
2
+ import { type ValidationIssue } from "./basic.js";
2
3
  export type IssueSeverity = "error" | "warning";
3
- export interface AuditResultIssue {
4
+ export interface AuditResultIssue extends ValidationIssue {
4
5
  result_index: number;
5
6
  task_id: string;
6
7
  severity: IssueSeverity;
7
8
  field: string;
8
- message: string;
9
9
  }
10
10
  export interface ValidateAuditResultOptions {
11
11
  lineIndex?: Record<string, number>;
@@ -1,3 +1,4 @@
1
+ import { describeValue, formatValidationIssues, isRecord, } from "./basic.js";
1
2
  const REQUIRED_FINDING_FIELDS = [
2
3
  "id",
3
4
  "title",
@@ -24,21 +25,10 @@ const VALID_LENSES = new Set([
24
25
  function pushIssue(issues, params) {
25
26
  issues.push({
26
27
  ...params,
28
+ path: params.path ?? params.field,
27
29
  severity: params.severity ?? "error",
28
30
  });
29
31
  }
30
- function describeValue(value) {
31
- if (Array.isArray(value)) {
32
- return "array";
33
- }
34
- if (value === null) {
35
- return "null";
36
- }
37
- return typeof value;
38
- }
39
- function isRecord(value) {
40
- return typeof value === "object" && value !== null && !Array.isArray(value);
41
- }
42
32
  function isNonEmptyString(value) {
43
33
  return typeof value === "string" && value.trim().length > 0;
44
34
  }
@@ -404,7 +394,9 @@ export function validateAuditResults(results, tasks, options = {}) {
404
394
  return issues;
405
395
  }
406
396
  export function formatAuditResultIssues(issues) {
407
- return issues
408
- .map((issue) => ` [${issue.severity}] ${issue.task_id} / ${issue.field}: ${issue.message}`)
409
- .join("\n");
397
+ return formatValidationIssues(issues.map((issue) => ({
398
+ path: `${issue.task_id} / ${issue.field}`,
399
+ message: issue.message,
400
+ severity: issue.severity,
401
+ })));
410
402
  }
@@ -1,5 +1,13 @@
1
+ export type ValidationSeverity = "error" | "warning";
1
2
  export interface ValidationIssue {
2
3
  path: string;
3
4
  message: string;
5
+ severity: ValidationSeverity;
4
6
  }
5
- export declare function requireKeys(record: Record<string, unknown>, path: string, keys: string[]): ValidationIssue[];
7
+ export declare function describeValue(value: unknown): string;
8
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
9
+ export declare function createValidationIssue(path: string, message: string, severity?: ValidationSeverity): ValidationIssue;
10
+ export declare function pushValidationIssue(issues: ValidationIssue[], path: string, message: string, severity?: ValidationSeverity): void;
11
+ export declare function prefixValidationIssues(prefix: string, issues: ValidationIssue[]): ValidationIssue[];
12
+ export declare function formatValidationIssues(issues: ValidationIssue[]): string;
13
+ export declare function requireKeys(value: unknown, path: string, keys: readonly string[]): ValidationIssue[];
@@ -1,8 +1,45 @@
1
- export function requireKeys(record, path, keys) {
1
+ export function describeValue(value) {
2
+ if (Array.isArray(value)) {
3
+ return "array";
4
+ }
5
+ if (value === null) {
6
+ return "null";
7
+ }
8
+ return typeof value;
9
+ }
10
+ export function isRecord(value) {
11
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12
+ }
13
+ export function createValidationIssue(path, message, severity = "error") {
14
+ return { path, message, severity };
15
+ }
16
+ export function pushValidationIssue(issues, path, message, severity = "error") {
17
+ issues.push(createValidationIssue(path, message, severity));
18
+ }
19
+ export function prefixValidationIssues(prefix, issues) {
20
+ return issues.map((issue) => ({
21
+ ...issue,
22
+ path: issue.path.length === 0
23
+ ? prefix
24
+ : issue.path === prefix || issue.path.startsWith(`${prefix}.`)
25
+ ? issue.path
26
+ : `${prefix}.${issue.path}`,
27
+ }));
28
+ }
29
+ export function formatValidationIssues(issues) {
30
+ return issues
31
+ .map((issue) => ` [${issue.severity}] ${issue.path}: ${issue.message}`)
32
+ .join("\n");
33
+ }
34
+ export function requireKeys(value, path, keys) {
2
35
  const issues = [];
36
+ if (!isRecord(value)) {
37
+ pushValidationIssue(issues, path, `Expected an object, got ${describeValue(value)}.`);
38
+ return issues;
39
+ }
3
40
  for (const key of keys) {
4
- if (!(key in record)) {
5
- issues.push({ path, message: `Missing required key: ${key}` });
41
+ if (!(key in value)) {
42
+ pushValidationIssue(issues, path, `Missing required key: ${key}`);
6
43
  }
7
44
  }
8
45
  return issues;
@@ -1,6 +1,8 @@
1
- import type { SessionConfig } from "../types/sessionConfig.js";
2
- import type { ValidationIssue } from "./basic.js";
1
+ import { type SessionConfig } from "../types/sessionConfig.js";
2
+ import { type ValidationIssue } from "./basic.js";
3
3
  export declare function validateSessionConfig(value: unknown): ValidationIssue[];
4
4
  export declare function validateConfiguredProviderEnvironment(sessionConfig: SessionConfig, options?: {
5
5
  commandExists?: (command: string) => boolean;
6
+ pathExists?: (commandPath: string) => boolean;
6
7
  }): ValidationIssue[];
8
+ export { formatValidationIssues } from "./basic.js";
@@ -1,18 +1,11 @@
1
1
  import { spawnSync } from "node:child_process";
2
- const VALID_PROVIDERS = new Set([
3
- "auto",
4
- "local-subprocess",
5
- "subprocess-template",
6
- "claude-code",
7
- "opencode",
8
- "vscode-task",
9
- ]);
10
- const VALID_UI_MODES = new Set(["headless", "visible"]);
2
+ import { accessSync, constants } from "node:fs";
3
+ import { PROVIDER_NAMES, SESSION_UI_MODES, } from "../types/sessionConfig.js";
4
+ import { isRecord, pushValidationIssue, } from "./basic.js";
5
+ const VALID_PROVIDERS = new Set(PROVIDER_NAMES);
6
+ const VALID_UI_MODES = new Set(SESSION_UI_MODES);
11
7
  function pushIssue(issues, path, message) {
12
- issues.push({ path, message });
13
- }
14
- function isRecord(value) {
15
- return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ pushValidationIssue(issues, path, message);
16
9
  }
17
10
  function validateStringArray(value, path, label, issues, options = {}) {
18
11
  if (!Array.isArray(value)) {
@@ -74,6 +67,9 @@ function validateAgentProviderSection(value, path, issues) {
74
67
  if (typeof value.command !== "string" || value.command.trim().length === 0) {
75
68
  pushIssue(issues, `${path}.command`, "command must be a non-empty string when provided.");
76
69
  }
70
+ else if (!isSupportedConfiguredCommand(value.command)) {
71
+ pushIssue(issues, `${path}.command`, "command must be a bare executable name or direct executable path. Put CLI flags in extra_args.");
72
+ }
77
73
  }
78
74
  if (value.extra_args !== undefined) {
79
75
  validateStringArray(value.extra_args, `${path}.extra_args`, "extra_args", issues, { allowEmptyArray: true });
@@ -84,6 +80,43 @@ function commandExists(command) {
84
80
  const result = spawnSync(lookupCommand, [command], { stdio: "ignore" });
85
81
  return result.status === 0;
86
82
  }
83
+ function configuredPathExists(commandPath) {
84
+ try {
85
+ accessSync(commandPath, constants.F_OK);
86
+ return true;
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ }
92
+ function startsWithPathPrefix(command) {
93
+ return (command.startsWith(".") ||
94
+ command.startsWith("/") ||
95
+ command.startsWith("\\\\") ||
96
+ /^[A-Za-z]:[\\/]/.test(command));
97
+ }
98
+ function containsForbiddenCommandSyntax(command) {
99
+ return /[\r\n"'`|&;<>]/.test(command);
100
+ }
101
+ function isBareExecutableName(command) {
102
+ return (command.length > 0 &&
103
+ !/\s/.test(command) &&
104
+ !containsForbiddenCommandSyntax(command) &&
105
+ !/[\\/]/.test(command) &&
106
+ !/^[A-Za-z]:/.test(command));
107
+ }
108
+ function isDirectExecutablePath(command) {
109
+ return (command.length > 0 &&
110
+ !containsForbiddenCommandSyntax(command) &&
111
+ startsWithPathPrefix(command));
112
+ }
113
+ function isSupportedConfiguredCommand(command) {
114
+ const trimmed = command.trim();
115
+ if (trimmed.length === 0 || trimmed !== command) {
116
+ return false;
117
+ }
118
+ return isBareExecutableName(trimmed) || isDirectExecutablePath(trimmed);
119
+ }
87
120
  export function validateSessionConfig(value) {
88
121
  const issues = [];
89
122
  if (value === undefined) {
@@ -122,18 +155,32 @@ export function validateSessionConfig(value) {
122
155
  export function validateConfiguredProviderEnvironment(sessionConfig, options = {}) {
123
156
  const issues = [];
124
157
  const lookupCommand = options.commandExists ?? commandExists;
158
+ const lookupPath = options.pathExists ?? configuredPathExists;
125
159
  const provider = sessionConfig.provider ?? "local-subprocess";
126
160
  if (provider === "claude-code") {
127
161
  const command = sessionConfig.claude_code?.command ?? "claude";
128
- if (!lookupCommand(command)) {
162
+ if (isBareExecutableName(command) && !lookupCommand(command)) {
129
163
  pushIssue(issues, "claude_code.command", `Configured claude-code executable was not found on PATH: ${command}.`);
130
164
  }
165
+ else if (isDirectExecutablePath(command) && !lookupPath(command)) {
166
+ pushIssue(issues, "claude_code.command", `Configured claude-code executable path does not exist: ${command}.`);
167
+ }
168
+ else if (!isSupportedConfiguredCommand(command)) {
169
+ pushIssue(issues, "claude_code.command", "Configured claude-code command must be a bare executable name or direct path. Put CLI flags in extra_args.");
170
+ }
131
171
  }
132
172
  if (provider === "opencode") {
133
173
  const command = sessionConfig.opencode?.command ?? "opencode";
134
- if (!lookupCommand(command)) {
174
+ if (isBareExecutableName(command) && !lookupCommand(command)) {
135
175
  pushIssue(issues, "opencode.command", `Configured opencode executable was not found on PATH: ${command}.`);
136
176
  }
177
+ else if (isDirectExecutablePath(command) && !lookupPath(command)) {
178
+ pushIssue(issues, "opencode.command", `Configured opencode executable path does not exist: ${command}.`);
179
+ }
180
+ else if (!isSupportedConfiguredCommand(command)) {
181
+ pushIssue(issues, "opencode.command", "Configured opencode command must be a bare executable name or direct path. Put CLI flags in extra_args.");
182
+ }
137
183
  }
138
184
  return issues;
139
185
  }
186
+ export { formatValidationIssues } from "./basic.js";