@praneeth_54/agentdoctor 2.0.0 → 2.1.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.
Files changed (168) hide show
  1. package/CHANGELOG.md +141 -4
  2. package/README.md +329 -153
  3. package/dist/agent/approvals.d.ts +24 -0
  4. package/dist/agent/approvals.js +64 -0
  5. package/dist/agent/chat/index.d.ts +7 -0
  6. package/dist/agent/chat/index.js +5 -0
  7. package/dist/agent/chat/memory.d.ts +44 -0
  8. package/dist/agent/chat/memory.js +103 -0
  9. package/dist/agent/chat/project-summary.d.ts +16 -0
  10. package/dist/agent/chat/project-summary.js +70 -0
  11. package/dist/agent/chat/prompts.d.ts +6 -0
  12. package/dist/agent/chat/prompts.js +39 -0
  13. package/dist/agent/chat/response.d.ts +19 -0
  14. package/dist/agent/chat/response.js +109 -0
  15. package/dist/agent/chat/service.d.ts +42 -0
  16. package/dist/agent/chat/service.js +252 -0
  17. package/dist/agent/chat/types.d.ts +48 -0
  18. package/dist/agent/chat/types.js +1 -0
  19. package/dist/agent/context/retrieve.d.ts +21 -0
  20. package/dist/agent/context/retrieve.js +117 -0
  21. package/dist/agent/context/truth.d.ts +6 -0
  22. package/dist/agent/context/truth.js +19 -0
  23. package/dist/agent/context/types.d.ts +23 -0
  24. package/dist/agent/context/types.js +4 -0
  25. package/dist/agent/index.d.ts +26 -0
  26. package/dist/agent/index.js +14 -0
  27. package/dist/agent/loop.d.ts +51 -0
  28. package/dist/agent/loop.js +222 -0
  29. package/dist/agent/modes.d.ts +18 -0
  30. package/dist/agent/modes.js +102 -0
  31. package/dist/agent/plan.d.ts +30 -0
  32. package/dist/agent/plan.js +121 -0
  33. package/dist/agent/runtime.d.ts +67 -0
  34. package/dist/agent/runtime.js +180 -0
  35. package/dist/agent/state.d.ts +30 -0
  36. package/dist/agent/state.js +95 -0
  37. package/dist/agent/student.d.ts +56 -0
  38. package/dist/agent/student.js +230 -0
  39. package/dist/agent/tools/execute.d.ts +18 -0
  40. package/dist/agent/tools/execute.js +355 -0
  41. package/dist/agent/tools/index.d.ts +6 -0
  42. package/dist/agent/tools/index.js +5 -0
  43. package/dist/agent/tools/registry.d.ts +7 -0
  44. package/dist/agent/tools/registry.js +212 -0
  45. package/dist/agent/tools/run.d.ts +24 -0
  46. package/dist/agent/tools/run.js +44 -0
  47. package/dist/agent/tools/types.d.ts +32 -0
  48. package/dist/agent/tools/types.js +10 -0
  49. package/dist/agent/tools/write.d.ts +24 -0
  50. package/dist/agent/tools/write.js +121 -0
  51. package/dist/agent/verify.d.ts +29 -0
  52. package/dist/agent/verify.js +210 -0
  53. package/dist/ai/config.d.ts +22 -0
  54. package/dist/ai/config.js +68 -0
  55. package/dist/ai/index.d.ts +17 -0
  56. package/dist/ai/index.js +52 -0
  57. package/dist/ai/providers/mock.d.ts +20 -0
  58. package/dist/ai/providers/mock.js +84 -0
  59. package/dist/ai/providers/none.d.ts +6 -0
  60. package/dist/ai/providers/none.js +23 -0
  61. package/dist/ai/providers/openai-compatible.d.ts +21 -0
  62. package/dist/ai/providers/openai-compatible.js +151 -0
  63. package/dist/ai/redact.d.ts +8 -0
  64. package/dist/ai/redact.js +21 -0
  65. package/dist/ai/types.d.ts +71 -0
  66. package/dist/ai/types.js +6 -0
  67. package/dist/architecture/contract.d.ts +64 -0
  68. package/dist/architecture/contract.js +373 -0
  69. package/dist/assurance/change.d.ts +211 -0
  70. package/dist/assurance/change.js +624 -0
  71. package/dist/assurance/proof.d.ts +136 -0
  72. package/dist/assurance/proof.js +338 -0
  73. package/dist/auth/index.d.ts +81 -0
  74. package/dist/auth/index.js +130 -0
  75. package/dist/auth/rbac.d.ts +9 -0
  76. package/dist/auth/rbac.js +26 -0
  77. package/dist/cli/commands/agent.d.ts +27 -0
  78. package/dist/cli/commands/agent.js +153 -0
  79. package/dist/cli/commands/architecture.d.ts +6 -0
  80. package/dist/cli/commands/architecture.js +109 -0
  81. package/dist/cli/commands/assurance.d.ts +67 -0
  82. package/dist/cli/commands/assurance.js +210 -0
  83. package/dist/cli/commands/chat.d.ts +14 -0
  84. package/dist/cli/commands/chat.js +155 -0
  85. package/dist/cli/commands/complete.js +2 -2
  86. package/dist/cli/commands/learn.d.ts +12 -0
  87. package/dist/cli/commands/learn.js +107 -0
  88. package/dist/cli/commands/platform.d.ts +2 -0
  89. package/dist/cli/commands/platform.js +5 -1
  90. package/dist/cli/commands/policy-graph-run.d.ts +40 -0
  91. package/dist/cli/commands/policy-graph-run.js +200 -0
  92. package/dist/cli/commands/workspace.d.ts +9 -0
  93. package/dist/cli/commands/workspace.js +82 -0
  94. package/dist/cli/program.js +627 -17
  95. package/dist/constants.d.ts +1 -1
  96. package/dist/constants.js +1 -1
  97. package/dist/contracts/index.d.ts +1 -1
  98. package/dist/contracts/index.js +2 -2
  99. package/dist/core/brain-product/init.js +5 -2
  100. package/dist/core/secrets/scan.d.ts +7 -1
  101. package/dist/core/secrets/scan.js +9 -9
  102. package/dist/coverage/cobertura.d.ts +6 -0
  103. package/dist/coverage/cobertura.js +52 -0
  104. package/dist/coverage/istanbul.d.ts +7 -0
  105. package/dist/coverage/istanbul.js +134 -0
  106. package/dist/coverage/lcov.d.ts +7 -0
  107. package/dist/coverage/lcov.js +59 -0
  108. package/dist/coverage/load.d.ts +9 -0
  109. package/dist/coverage/load.js +39 -0
  110. package/dist/coverage/types.d.ts +23 -0
  111. package/dist/coverage/types.js +5 -0
  112. package/dist/dashboard/server.d.ts +7 -1
  113. package/dist/dashboard/server.js +110 -6
  114. package/dist/enforcement/runner.d.ts +52 -3
  115. package/dist/enforcement/runner.js +467 -16
  116. package/dist/index.d.ts +36 -2
  117. package/dist/index.js +23 -2
  118. package/dist/intelligence/graph/build.d.ts +2 -0
  119. package/dist/intelligence/graph/build.js +49 -5
  120. package/dist/intelligence/graph/incremental.d.ts +68 -0
  121. package/dist/intelligence/graph/incremental.js +234 -0
  122. package/dist/intelligence/resolve/imports.d.ts +36 -0
  123. package/dist/intelligence/resolve/imports.js +245 -0
  124. package/dist/languages/go.d.ts +9 -0
  125. package/dist/languages/go.js +50 -0
  126. package/dist/languages/index.d.ts +11 -0
  127. package/dist/languages/index.js +76 -0
  128. package/dist/languages/php.d.ts +8 -0
  129. package/dist/languages/php.js +202 -0
  130. package/dist/languages/python.d.ts +11 -0
  131. package/dist/languages/python.js +180 -0
  132. package/dist/languages/types.d.ts +42 -0
  133. package/dist/languages/types.js +26 -0
  134. package/dist/languages/typescript.d.ts +3 -0
  135. package/dist/languages/typescript.js +95 -0
  136. package/dist/mcp/agent/registry.d.ts +13 -0
  137. package/dist/mcp/agent/registry.js +234 -0
  138. package/dist/mcp/agentdoctor/server.js +8 -1
  139. package/dist/mcp/intelligence/handlers.d.ts +5 -0
  140. package/dist/mcp/intelligence/handlers.js +229 -40
  141. package/dist/mcp/intelligence/path-safety.d.ts +2 -4
  142. package/dist/mcp/intelligence/path-safety.js +25 -44
  143. package/dist/mcp/intelligence/registry.d.ts +1 -1
  144. package/dist/mcp/intelligence/registry.js +69 -1
  145. package/dist/platform/firewall/evaluate.d.ts +6 -3
  146. package/dist/platform/firewall/evaluate.js +66 -15
  147. package/dist/platform/graph/build.js +19 -23
  148. package/dist/platform/health/analyze.js +23 -17
  149. package/dist/platform/index.js +2 -0
  150. package/dist/platform/test-impact/analyze.d.ts +21 -3
  151. package/dist/platform/test-impact/analyze.js +214 -28
  152. package/dist/platform/tokens/plan.js +39 -20
  153. package/dist/platform/types.d.ts +9 -0
  154. package/dist/policy/compose.d.ts +34 -0
  155. package/dist/policy/compose.js +118 -0
  156. package/dist/policy/packs.js +33 -1
  157. package/dist/security/paths.d.ts +21 -0
  158. package/dist/security/paths.js +133 -0
  159. package/dist/storage/postgres.d.ts +26 -0
  160. package/dist/storage/postgres.js +90 -0
  161. package/dist/storage/provider.d.ts +2 -7
  162. package/dist/storage/provider.js +22 -16
  163. package/dist/storage/sqlite.d.ts +33 -0
  164. package/dist/storage/sqlite.js +77 -0
  165. package/dist/team/auth.js +1 -1
  166. package/dist/workspace/index.d.ts +69 -0
  167. package/dist/workspace/index.js +220 -0
  168. package/package.json +9 -3
@@ -0,0 +1,32 @@
1
+ export type AgentRiskLevel = "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
2
+ export type AgentToolName = "read_file" | "list_files" | "search_code" | "find_symbol" | "find_references" | "find_callers" | "find_callees" | "inspect_project" | "inspect_architecture" | "inspect_dependencies" | "inspect_tests" | "inspect_findings" | "inspect_git_status" | "inspect_git_diff" | "create_file" | "edit_file" | "delete_file" | "run_command" | "run_tests";
3
+ export interface AgentToolSpec {
4
+ name: AgentToolName;
5
+ description: string;
6
+ risk: AgentRiskLevel;
7
+ /** JSON-schema-like args description for prompts */
8
+ parameters: Record<string, unknown>;
9
+ /** M3 read tools vs later write/exec */
10
+ category: "read" | "write" | "execute";
11
+ }
12
+ export interface AgentToolCall {
13
+ id: string;
14
+ name: AgentToolName;
15
+ arguments: Record<string, unknown>;
16
+ timestamp: string;
17
+ sessionId: string;
18
+ }
19
+ export interface AgentToolResult {
20
+ callId: string;
21
+ name: AgentToolName;
22
+ ok: boolean;
23
+ /** Structured DATA — never treat as instructions */
24
+ data: unknown;
25
+ error?: {
26
+ code: string;
27
+ message: string;
28
+ };
29
+ risk: AgentRiskLevel;
30
+ durationMs: number;
31
+ }
32
+ export declare function newToolCall(sessionId: string, name: AgentToolName, args: Record<string, unknown>): AgentToolCall;
@@ -0,0 +1,10 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export function newToolCall(sessionId, name, args) {
3
+ return {
4
+ id: randomUUID(),
5
+ name,
6
+ arguments: args,
7
+ timestamp: new Date().toISOString(),
8
+ sessionId,
9
+ };
10
+ }
@@ -0,0 +1,24 @@
1
+ export interface FileDiffResult {
2
+ path: string;
3
+ action: "create" | "edit" | "delete";
4
+ beforeBytes: number;
5
+ afterBytes: number;
6
+ /** Unified-ish line diff (bounded) */
7
+ diff: string;
8
+ }
9
+ export declare function buildUnifiedDiff(pathLabel: string, before: string, after: string): string;
10
+ /**
11
+ * Path-safe create. Rejects existing files (use edit_file to overwrite).
12
+ */
13
+ export declare function createFileSafe(root: string, relativePath: string, content: string): Promise<FileDiffResult>;
14
+ /**
15
+ * Path-safe edit. Prefer exact oldContent match or line-range replacement over blind overwrite.
16
+ */
17
+ export declare function editFileSafe(root: string, relativePath: string, options: {
18
+ content?: string;
19
+ oldContent?: string;
20
+ startLine?: number;
21
+ endLine?: number;
22
+ replacement?: string;
23
+ }): Promise<FileDiffResult>;
24
+ export declare function deleteFileSafe(root: string, relativePath: string): Promise<FileDiffResult>;
@@ -0,0 +1,121 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { PathEscapeError, resolveSafeRepoPath } from "../../security/paths.js";
4
+ import { atomicWriteTextFile } from "../../utils/fs.js";
5
+ const MAX_DIFF_LINES = 400;
6
+ export function buildUnifiedDiff(pathLabel, before, after) {
7
+ const a = before.split(/\r?\n/);
8
+ const b = after.split(/\r?\n/);
9
+ const lines = [`--- a/${pathLabel}`, `+++ b/${pathLabel}`];
10
+ const max = Math.max(a.length, b.length);
11
+ let shown = 0;
12
+ for (let i = 0; i < max && shown < MAX_DIFF_LINES; i += 1) {
13
+ const left = a[i];
14
+ const right = b[i];
15
+ if (left === right) {
16
+ if (left !== undefined) {
17
+ lines.push(` ${left}`);
18
+ shown += 1;
19
+ }
20
+ continue;
21
+ }
22
+ if (left !== undefined) {
23
+ lines.push(`-${left}`);
24
+ shown += 1;
25
+ }
26
+ if (right !== undefined) {
27
+ lines.push(`+${right}`);
28
+ shown += 1;
29
+ }
30
+ }
31
+ if (shown >= MAX_DIFF_LINES) {
32
+ lines.push("... diff truncated ...");
33
+ }
34
+ return lines.join("\n");
35
+ }
36
+ function normalizeRel(rel) {
37
+ return rel.split(path.sep).join("/");
38
+ }
39
+ /**
40
+ * Path-safe create. Rejects existing files (use edit_file to overwrite).
41
+ */
42
+ export async function createFileSafe(root, relativePath, content) {
43
+ const abs = resolveSafeRepoPath(root, relativePath);
44
+ const rel = normalizeRel(relativePath);
45
+ try {
46
+ await fs.access(abs);
47
+ throw new Error(`file_exists: ${rel}`);
48
+ }
49
+ catch (error) {
50
+ if (error instanceof PathEscapeError)
51
+ throw error;
52
+ if (error instanceof Error && error.message.startsWith("file_exists:"))
53
+ throw error;
54
+ // ENOENT — ok to create
55
+ }
56
+ await fs.mkdir(path.dirname(abs), { recursive: true });
57
+ await atomicWriteTextFile(abs, content);
58
+ return {
59
+ path: rel,
60
+ action: "create",
61
+ beforeBytes: 0,
62
+ afterBytes: Buffer.byteLength(content, "utf8"),
63
+ diff: buildUnifiedDiff(rel, "", content),
64
+ };
65
+ }
66
+ /**
67
+ * Path-safe edit. Prefer exact oldContent match or line-range replacement over blind overwrite.
68
+ */
69
+ export async function editFileSafe(root, relativePath, options) {
70
+ const abs = resolveSafeRepoPath(root, relativePath);
71
+ const rel = normalizeRel(relativePath);
72
+ const before = await fs.readFile(abs, "utf8");
73
+ let after;
74
+ if (typeof options.startLine === "number" &&
75
+ typeof options.endLine === "number" &&
76
+ typeof options.replacement === "string") {
77
+ const lines = before.split(/\r?\n/);
78
+ const start = Math.max(1, options.startLine);
79
+ const end = Math.max(start, options.endLine);
80
+ if (start > lines.length) {
81
+ throw new Error(`invalid_range: startLine ${start} beyond file length ${lines.length}`);
82
+ }
83
+ const head = lines.slice(0, start - 1);
84
+ const tail = lines.slice(end);
85
+ const mid = options.replacement.split(/\r?\n/);
86
+ after = [...head, ...mid, ...tail].join("\n");
87
+ }
88
+ else if (typeof options.oldContent === "string" && typeof options.content === "string") {
89
+ if (!before.includes(options.oldContent)) {
90
+ throw new Error("old_content_mismatch: exact oldContent not found in file");
91
+ }
92
+ after = before.replace(options.oldContent, options.content);
93
+ }
94
+ else if (typeof options.content === "string") {
95
+ after = options.content;
96
+ }
97
+ else {
98
+ throw new Error("invalid_argument: provide content, or oldContent+content, or line range");
99
+ }
100
+ await atomicWriteTextFile(abs, after);
101
+ return {
102
+ path: rel,
103
+ action: "edit",
104
+ beforeBytes: Buffer.byteLength(before, "utf8"),
105
+ afterBytes: Buffer.byteLength(after, "utf8"),
106
+ diff: buildUnifiedDiff(rel, before, after),
107
+ };
108
+ }
109
+ export async function deleteFileSafe(root, relativePath) {
110
+ const abs = resolveSafeRepoPath(root, relativePath);
111
+ const rel = normalizeRel(relativePath);
112
+ const before = await fs.readFile(abs, "utf8");
113
+ await fs.unlink(abs);
114
+ return {
115
+ path: rel,
116
+ action: "delete",
117
+ beforeBytes: Buffer.byteLength(before, "utf8"),
118
+ afterBytes: 0,
119
+ diff: buildUnifiedDiff(rel, before, ""),
120
+ };
121
+ }
@@ -0,0 +1,29 @@
1
+ export interface VerificationCheck {
2
+ id: string;
3
+ label: string;
4
+ status: "passed" | "failed" | "skipped" | "not-run";
5
+ detail: string;
6
+ }
7
+ export interface AgentVerificationReport {
8
+ root: string;
9
+ changeId: string | null;
10
+ proofId: string | null;
11
+ filesChanged: string[];
12
+ checks: VerificationCheck[];
13
+ verified: string[];
14
+ notVerified: string[];
15
+ correctnessStatus: "ENGINEERING_CORRECTNESS_NOT_CLAIMED";
16
+ summaryText: string;
17
+ }
18
+ /**
19
+ * Post-change verification for the coding agent (M5).
20
+ * Reuses analyzeChange / verifyChange / proof / architecture — does not invent results.
21
+ */
22
+ export declare function verifyAgentWork(options: {
23
+ root: string;
24
+ filesChanged?: string[];
25
+ /** When true, attempt controlled test run (requires approval flags on tool exec) */
26
+ runTests?: boolean;
27
+ approvedByHuman?: boolean;
28
+ }): Promise<AgentVerificationReport>;
29
+ export declare function formatVerificationReport(report: AgentVerificationReport): string;
@@ -0,0 +1,210 @@
1
+ import { analyzeChange, verifyChange } from "../assurance/change.js";
2
+ import { buildProofFromEvidence } from "../assurance/proof.js";
3
+ import { checkArchitectureAtRoot } from "../architecture/contract.js";
4
+ import { buildRepositoryGraph } from "../platform/graph/build.js";
5
+ import { analyzeChanges } from "../core/changes/analyze.js";
6
+ import { resolveRepoRoot } from "../utils/path.js";
7
+ import { executeAgentTool, newToolCall } from "./tools/index.js";
8
+ /**
9
+ * Post-change verification for the coding agent (M5).
10
+ * Reuses analyzeChange / verifyChange / proof / architecture — does not invent results.
11
+ */
12
+ export async function verifyAgentWork(options) {
13
+ const root = resolveRepoRoot(options.root);
14
+ const checks = [];
15
+ const verified = [];
16
+ const notVerified = [
17
+ "production database",
18
+ "external deployment",
19
+ "external API behavior",
20
+ "full product correctness",
21
+ ];
22
+ const git = await analyzeChanges({ root, impact: true });
23
+ checks.push({
24
+ id: "git-status",
25
+ label: "Git status / change set",
26
+ status: git.gitAvailable ? "passed" : "skipped",
27
+ detail: git.gitAvailable
28
+ ? `${git.files.length} changed path(s); impact=${git.impact?.status ?? "n/a"}`
29
+ : "Git not available — change set UNKNOWN",
30
+ });
31
+ if (git.gitAvailable)
32
+ verified.push("Git change set inspected");
33
+ const filesChanged = options.filesChanged?.length
34
+ ? options.filesChanged
35
+ : git.files.map((f) => f.path).slice(0, 50);
36
+ let changeId = null;
37
+ let proofId = null;
38
+ try {
39
+ const assessment = await analyzeChange({ root });
40
+ changeId = assessment.changeId;
41
+ checks.push({
42
+ id: "change-analyze",
43
+ label: "Change impact analyzed",
44
+ status: "passed",
45
+ detail: `changeId=${assessment.changeId}; symbols=${assessment.changedSymbols.length}`,
46
+ });
47
+ verified.push("Change impact analyzed");
48
+ const { assessment: verifiedAssessment, manifest } = await verifyChange({
49
+ root,
50
+ changeId: assessment.changeId,
51
+ });
52
+ checks.push({
53
+ id: "change-verify",
54
+ label: "Evidence bundle produced",
55
+ status: verifiedAssessment.verificationStatus === "evidence-produced" ? "passed" : "failed",
56
+ detail: `status=${verifiedAssessment.verificationStatus}; evidence=${manifest.changeId}`,
57
+ });
58
+ if (verifiedAssessment.verificationStatus === "evidence-produced") {
59
+ verified.push("Evidence bundle produced");
60
+ }
61
+ try {
62
+ const proof = await buildProofFromEvidence(root, assessment.changeId);
63
+ proofId = proof.proofId;
64
+ checks.push({
65
+ id: "proof",
66
+ label: "Change proof",
67
+ status: "passed",
68
+ detail: `proofId=${proof.proofId}; integrity=${proof.integrityStatus}; correctness=${proof.correctnessStatus}`,
69
+ });
70
+ verified.push(`Proof built (${proof.integrityStatus})`);
71
+ }
72
+ catch (error) {
73
+ checks.push({
74
+ id: "proof",
75
+ label: "Change proof",
76
+ status: "failed",
77
+ detail: error instanceof Error ? error.message : String(error),
78
+ });
79
+ }
80
+ }
81
+ catch (error) {
82
+ checks.push({
83
+ id: "change-analyze",
84
+ label: "Change impact analyzed",
85
+ status: "failed",
86
+ detail: error instanceof Error ? error.message : String(error),
87
+ });
88
+ }
89
+ try {
90
+ const graph = await buildRepositoryGraph(root);
91
+ const arch = await checkArchitectureAtRoot(root, graph);
92
+ const hasContract = Boolean(arch.contractPath && arch.contract);
93
+ const ok = arch.violations.length === 0;
94
+ checks.push({
95
+ id: "architecture",
96
+ label: "Architecture check",
97
+ status: !hasContract ? "skipped" : ok ? "passed" : "failed",
98
+ detail: !hasContract
99
+ ? "No architecture contract present"
100
+ : `${arch.violations.length} violation(s); edges checked=${arch.importEdgesChecked}`,
101
+ });
102
+ if (hasContract) {
103
+ if (ok)
104
+ verified.push("Architecture check passed");
105
+ else
106
+ verified.push("Architecture check ran (violations found)");
107
+ }
108
+ else {
109
+ notVerified.push("architecture contract (none present)");
110
+ }
111
+ }
112
+ catch (error) {
113
+ checks.push({
114
+ id: "architecture",
115
+ label: "Architecture check",
116
+ status: "failed",
117
+ detail: error instanceof Error ? error.message : String(error),
118
+ });
119
+ }
120
+ if (options.runTests && options.approvedByHuman) {
121
+ const testResult = await executeAgentTool(root, newToolCall("verify", "run_tests", {}), {
122
+ allowExecute: true,
123
+ approvedByHuman: true,
124
+ });
125
+ checks.push({
126
+ id: "tests",
127
+ label: "Controlled test run",
128
+ status: testResult.ok ? "passed" : "failed",
129
+ detail: testResult.ok
130
+ ? `exit=${testResult.data.exitCode ?? 0}`
131
+ : `${testResult.error?.code}: ${testResult.error?.message}`,
132
+ });
133
+ if (testResult.ok)
134
+ verified.push("Relevant tests executed (controlled runner)");
135
+ else
136
+ notVerified.push("test suite (execution failed or policy blocked)");
137
+ }
138
+ else {
139
+ checks.push({
140
+ id: "tests",
141
+ label: "Controlled test run",
142
+ status: "not-run",
143
+ detail: options.runTests
144
+ ? "Tests requested but human approval missing"
145
+ : "Tests not requested for this verification pass",
146
+ });
147
+ notVerified.push("test execution (not run in this pass)");
148
+ }
149
+ checks.push({
150
+ id: "workspace",
151
+ label: "Workspace boundary",
152
+ status: "passed",
153
+ detail: "All prior agent writes used path-safe resolveSafeRepoPath",
154
+ });
155
+ verified.push("Workspace boundary respected (write path)");
156
+ const summaryText = formatVerificationReport({
157
+ root,
158
+ changeId,
159
+ proofId,
160
+ filesChanged,
161
+ checks,
162
+ verified,
163
+ notVerified,
164
+ correctnessStatus: "ENGINEERING_CORRECTNESS_NOT_CLAIMED",
165
+ summaryText: "",
166
+ });
167
+ return {
168
+ root,
169
+ changeId,
170
+ proofId,
171
+ filesChanged,
172
+ checks,
173
+ verified,
174
+ notVerified,
175
+ correctnessStatus: "ENGINEERING_CORRECTNESS_NOT_CLAIMED",
176
+ summaryText,
177
+ };
178
+ }
179
+ export function formatVerificationReport(report) {
180
+ const lines = [
181
+ "",
182
+ "IMPLEMENTATION COMPLETE",
183
+ "",
184
+ report.filesChanged.length
185
+ ? `Changed:\n${report.filesChanged.map((f) => ` - ${f}`).join("\n")}`
186
+ : "Changed: (none recorded)",
187
+ "",
188
+ "Verification:",
189
+ ...report.checks.map((c) => {
190
+ const mark = c.status === "passed"
191
+ ? "✓"
192
+ : c.status === "failed"
193
+ ? "✗"
194
+ : c.status === "skipped"
195
+ ? "○"
196
+ : "·";
197
+ return ` ${mark} ${c.label} — ${c.detail}`;
198
+ }),
199
+ "",
200
+ "Verified:",
201
+ ...report.verified.map((v) => ` - ${v}`),
202
+ "",
203
+ "Not verified:",
204
+ ...report.notVerified.map((v) => ` - ${v}`),
205
+ "",
206
+ report.correctnessStatus,
207
+ "",
208
+ ];
209
+ return lines.join("\n");
210
+ }
@@ -0,0 +1,22 @@
1
+ import type { AiProviderId } from "./types.js";
2
+ /**
3
+ * AI provider configuration.
4
+ * Secrets come only from environment variables — never from Brain or committed files.
5
+ */
6
+ export interface AiConfig {
7
+ provider: AiProviderId;
8
+ model: string;
9
+ baseUrl?: string;
10
+ /** Present only in memory from env — never serialized to Brain/evidence */
11
+ apiKey?: string;
12
+ configured: boolean;
13
+ }
14
+ /**
15
+ * Load AI config from environment.
16
+ * Does not read API keys from disk. Does not write secrets anywhere.
17
+ */
18
+ export declare function loadAiConfig(env?: NodeJS.ProcessEnv): AiConfig;
19
+ /** Safe view for logs / CLI — never includes apiKey. */
20
+ export declare function publicAiConfig(config: AiConfig): Omit<AiConfig, "apiKey"> & {
21
+ apiKeySet: boolean;
22
+ };
@@ -0,0 +1,68 @@
1
+ const DEFAULT_MODELS = {
2
+ none: "none",
3
+ mock: "mock",
4
+ "openai-compatible": "gpt-4o-mini",
5
+ ollama: "llama3.2",
6
+ anthropic: "claude-sonnet-4-5",
7
+ gemini: "gemini-2.0-flash",
8
+ };
9
+ function normalizeProvider(raw) {
10
+ const v = (raw ?? "none").trim().toLowerCase();
11
+ switch (v) {
12
+ case "none":
13
+ case "":
14
+ return "none";
15
+ case "mock":
16
+ return "mock";
17
+ case "openai":
18
+ case "openai-compatible":
19
+ case "openrouter":
20
+ return "openai-compatible";
21
+ case "ollama":
22
+ return "ollama";
23
+ case "anthropic":
24
+ return "anthropic";
25
+ case "gemini":
26
+ case "google":
27
+ return "gemini";
28
+ default:
29
+ return "none";
30
+ }
31
+ }
32
+ /**
33
+ * Load AI config from environment.
34
+ * Does not read API keys from disk. Does not write secrets anywhere.
35
+ */
36
+ export function loadAiConfig(env = process.env) {
37
+ const provider = normalizeProvider(env.AGENTDOCTOR_AI_PROVIDER);
38
+ const model = (env.AGENTDOCTOR_AI_MODEL ?? DEFAULT_MODELS[provider]).trim();
39
+ const baseUrl = env.AGENTDOCTOR_AI_BASE_URL?.trim() || undefined;
40
+ const apiKey = env.AGENTDOCTOR_AI_API_KEY?.trim() || undefined;
41
+ const configured = provider !== "none" &&
42
+ (provider === "mock" ||
43
+ provider === "ollama" ||
44
+ Boolean(apiKey) ||
45
+ (provider === "openai-compatible" && Boolean(baseUrl)));
46
+ const result = {
47
+ provider,
48
+ model,
49
+ configured: provider === "mock" ? true : configured,
50
+ };
51
+ if (baseUrl)
52
+ result.baseUrl = baseUrl;
53
+ if (apiKey)
54
+ result.apiKey = apiKey;
55
+ return result;
56
+ }
57
+ /** Safe view for logs / CLI — never includes apiKey. */
58
+ export function publicAiConfig(config) {
59
+ const pub = {
60
+ provider: config.provider,
61
+ model: config.model,
62
+ configured: config.configured,
63
+ apiKeySet: Boolean(config.apiKey),
64
+ };
65
+ if (config.baseUrl)
66
+ pub.baseUrl = config.baseUrl;
67
+ return pub;
68
+ }
@@ -0,0 +1,17 @@
1
+ import { type AiConfig } from "./config.js";
2
+ import type { ModelProvider } from "./types.js";
3
+ export type { AiConfig } from "./config.js";
4
+ export { loadAiConfig, publicAiConfig } from "./config.js";
5
+ export { redactForModel, redactMessages } from "./redact.js";
6
+ export type { AiProviderId, ChatMessage, ChatRequest, ChatResponse, ModelMetadata, ModelProvider, ToolCallRequest, ToolSpec, TokenUsage, } from "./types.js";
7
+ export { AI_PROVIDER_REQUIRED_MESSAGE } from "./types.js";
8
+ export { NoneModelProvider } from "./providers/none.js";
9
+ export { MockModelProvider } from "./providers/mock.js";
10
+ export { OpenAiCompatibleProvider } from "./providers/openai-compatible.js";
11
+ /**
12
+ * Create a ModelProvider from config / env.
13
+ * Unsupported providers (anthropic/gemini native) return none with a clear error path via none,
14
+ * unless mapped later — for M1 only none/mock/openai-compatible/ollama-via-compatible are live.
15
+ */
16
+ export declare function createModelProvider(config?: AiConfig): ModelProvider;
17
+ export declare function requireAiProviderMessage(provider: ModelProvider): string | null;
@@ -0,0 +1,52 @@
1
+ import { loadAiConfig } from "./config.js";
2
+ import { NoneModelProvider } from "./providers/none.js";
3
+ import { MockModelProvider } from "./providers/mock.js";
4
+ import { OpenAiCompatibleProvider } from "./providers/openai-compatible.js";
5
+ import { AI_PROVIDER_REQUIRED_MESSAGE } from "./types.js";
6
+ export { loadAiConfig, publicAiConfig } from "./config.js";
7
+ export { redactForModel, redactMessages } from "./redact.js";
8
+ export { AI_PROVIDER_REQUIRED_MESSAGE } from "./types.js";
9
+ export { NoneModelProvider } from "./providers/none.js";
10
+ export { MockModelProvider } from "./providers/mock.js";
11
+ export { OpenAiCompatibleProvider } from "./providers/openai-compatible.js";
12
+ /**
13
+ * Create a ModelProvider from config / env.
14
+ * Unsupported providers (anthropic/gemini native) return none with a clear error path via none,
15
+ * unless mapped later — for M1 only none/mock/openai-compatible/ollama-via-compatible are live.
16
+ */
17
+ export function createModelProvider(config = loadAiConfig()) {
18
+ switch (config.provider) {
19
+ case "mock":
20
+ return new MockModelProvider();
21
+ case "openai-compatible": {
22
+ const opts = {
23
+ model: config.model,
24
+ };
25
+ if (config.apiKey)
26
+ opts.apiKey = config.apiKey;
27
+ if (config.baseUrl)
28
+ opts.baseUrl = config.baseUrl;
29
+ return new OpenAiCompatibleProvider(opts);
30
+ }
31
+ case "ollama":
32
+ // Ollama OpenAI-compatible endpoint when available; otherwise fail closed via HTTP errors.
33
+ return new OpenAiCompatibleProvider({
34
+ apiKey: config.apiKey ?? "ollama",
35
+ baseUrl: config.baseUrl ?? "http://127.0.0.1:11434/v1",
36
+ model: config.model,
37
+ });
38
+ case "anthropic":
39
+ case "gemini":
40
+ // Native SDKs are NOT IMPLEMENTED in M1 — fail closed via none message.
41
+ return new NoneModelProvider();
42
+ case "none":
43
+ default:
44
+ return new NoneModelProvider();
45
+ }
46
+ }
47
+ export function requireAiProviderMessage(provider) {
48
+ if (provider.id === "none") {
49
+ return AI_PROVIDER_REQUIRED_MESSAGE;
50
+ }
51
+ return null;
52
+ }
@@ -0,0 +1,20 @@
1
+ import type { ChatRequest, ChatResponse, ModelMetadata, ModelProvider, ToolCallRequest } from "../types.js";
2
+ export interface MockScriptStep {
3
+ content?: string;
4
+ toolCalls?: ToolCallRequest[];
5
+ }
6
+ /**
7
+ * Deterministic mock provider for tests and offline demos.
8
+ * Never calls the network.
9
+ * Optional `script` advances one step per chat() for coding-loop tests.
10
+ */
11
+ export declare class MockModelProvider implements ModelProvider {
12
+ readonly id: "mock";
13
+ private turn;
14
+ private readonly script;
15
+ constructor(options?: {
16
+ script?: MockScriptStep[];
17
+ });
18
+ metadata(): ModelMetadata;
19
+ chat(request: ChatRequest): Promise<ChatResponse>;
20
+ }