@praneeth_54/agentdoctor 2.0.1 → 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 (91) hide show
  1. package/CHANGELOG.md +69 -1
  2. package/README.md +65 -14
  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/cli/commands/agent.d.ts +27 -0
  68. package/dist/cli/commands/agent.js +153 -0
  69. package/dist/cli/commands/chat.d.ts +14 -0
  70. package/dist/cli/commands/chat.js +155 -0
  71. package/dist/cli/commands/learn.d.ts +12 -0
  72. package/dist/cli/commands/learn.js +107 -0
  73. package/dist/cli/program.js +92 -0
  74. package/dist/constants.d.ts +1 -1
  75. package/dist/constants.js +1 -1
  76. package/dist/dashboard/server.d.ts +6 -0
  77. package/dist/dashboard/server.js +89 -1
  78. package/dist/enforcement/runner.d.ts +5 -0
  79. package/dist/enforcement/runner.js +51 -4
  80. package/dist/index.d.ts +13 -0
  81. package/dist/index.js +8 -0
  82. package/dist/languages/go.js +7 -2
  83. package/dist/languages/php.d.ts +1 -0
  84. package/dist/languages/php.js +11 -2
  85. package/dist/languages/python.d.ts +6 -2
  86. package/dist/languages/python.js +39 -9
  87. package/dist/mcp/agent/registry.d.ts +13 -0
  88. package/dist/mcp/agent/registry.js +234 -0
  89. package/dist/mcp/agentdoctor/server.js +8 -1
  90. package/dist/security/paths.js +43 -15
  91. package/package.json +1 -1
@@ -0,0 +1,155 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { ChatService, CHAT_PROVIDER_NONE_MESSAGE, formatChatResponseForCli, } from "../../agent/chat/index.js";
4
+ import { MockModelProvider, createModelProvider, loadAiConfig } from "../../ai/index.js";
5
+ import { EXIT_CODES } from "../../types/index.js";
6
+ import { resolveRepoRoot } from "../../utils/path.js";
7
+ import { colors } from "../../utils/colors.js";
8
+ function printBanner(options) {
9
+ const lines = [
10
+ "",
11
+ colors.bold("AgentDoctor Project Chat"),
12
+ "",
13
+ `Project:`,
14
+ ` ${options.name}`,
15
+ ` ${options.root}`,
16
+ "",
17
+ `Detected:`,
18
+ ` ${(options.languages.length ? options.languages : ["(none)"]).join(", ")}`,
19
+ ` frameworks: ${(options.frameworks.length ? options.frameworks : ["(none)"]).join(", ")}`,
20
+ "",
21
+ `AI:`,
22
+ ` ${options.providerLabel}`,
23
+ "",
24
+ "Ask questions about your project.",
25
+ "",
26
+ "Commands:",
27
+ " /help Show help",
28
+ " /project Project summary",
29
+ " /context Context used for last answer",
30
+ " /clear Clear conversation memory",
31
+ " /exit Leave chat",
32
+ "",
33
+ "Milestone 2 does not edit files or run commands.",
34
+ "",
35
+ ];
36
+ process.stdout.write(`${lines.join("\n")}\n`);
37
+ }
38
+ function printHelp() {
39
+ process.stdout.write([
40
+ "",
41
+ "Ask natural-language questions about this repository.",
42
+ "Answers use project evidence (Brain/graph/context) when available.",
43
+ "",
44
+ "Truth labels: VERIFIED | INFERRED | UNKNOWN | EXTERNAL",
45
+ "",
46
+ "Examples:",
47
+ " Explain my project.",
48
+ " How does authentication work?",
49
+ " Explain this like I'm a beginner.",
50
+ "",
51
+ ].join("\n"));
52
+ }
53
+ export async function runAskCommand(options) {
54
+ const root = resolveRepoRoot(options.root ?? process.cwd());
55
+ const provider = options.useMock ? new MockModelProvider() : createModelProvider(loadAiConfig());
56
+ const chat = new ChatService({
57
+ root,
58
+ provider,
59
+ persistAudit: !options.useMock,
60
+ });
61
+ try {
62
+ const response = await chat.ask(options.question);
63
+ if (options.json) {
64
+ process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
65
+ }
66
+ else {
67
+ process.stdout.write(formatChatResponseForCli(response));
68
+ }
69
+ if (response.status === "provider-none")
70
+ return EXIT_CODES.USAGE_ERROR;
71
+ if (response.status === "provider-error" || response.status === "failed") {
72
+ return EXIT_CODES.INTERNAL_ERROR;
73
+ }
74
+ return EXIT_CODES.SUCCESS;
75
+ }
76
+ finally {
77
+ await chat.end();
78
+ }
79
+ }
80
+ export async function runChatCommand(options) {
81
+ const root = resolveRepoRoot(options.root ?? process.cwd());
82
+ const provider = options.useMock ? new MockModelProvider() : createModelProvider(loadAiConfig());
83
+ const chat = new ChatService({
84
+ root,
85
+ provider,
86
+ persistAudit: !options.useMock,
87
+ });
88
+ const started = await chat.start();
89
+ printBanner({
90
+ root: started.summary.root,
91
+ name: started.summary.name,
92
+ providerLabel: started.providerLabel,
93
+ languages: started.summary.languages,
94
+ frameworks: started.summary.frameworks,
95
+ });
96
+ if (provider.id === "none" && !options.useMock) {
97
+ process.stdout.write(`${CHAT_PROVIDER_NONE_MESSAGE}\n`);
98
+ await chat.end();
99
+ return EXIT_CODES.USAGE_ERROR;
100
+ }
101
+ const scripted = options.scriptedInputs ? [...options.scriptedInputs] : undefined;
102
+ const rl = scripted === undefined
103
+ ? readline.createInterface({ input, output, terminal: Boolean(output.isTTY) })
104
+ : undefined;
105
+ const readLine = async () => {
106
+ if (scripted) {
107
+ if (scripted.length === 0)
108
+ return null;
109
+ return scripted.shift() ?? null;
110
+ }
111
+ const answer = await rl.question("You:\n> ");
112
+ return answer;
113
+ };
114
+ try {
115
+ while (true) {
116
+ const raw = await readLine();
117
+ if (raw === null)
118
+ break;
119
+ const line = raw.trim();
120
+ if (!line)
121
+ continue;
122
+ if (line === "/exit" || line === "exit" || line === "quit") {
123
+ process.stdout.write("Goodbye.\n");
124
+ break;
125
+ }
126
+ if (line === "/help" || line === "help") {
127
+ printHelp();
128
+ continue;
129
+ }
130
+ if (line === "/project") {
131
+ process.stdout.write(await chat.formatProject());
132
+ continue;
133
+ }
134
+ if (line === "/context") {
135
+ process.stdout.write(chat.formatContext());
136
+ continue;
137
+ }
138
+ if (line === "/clear") {
139
+ chat.clearMemory();
140
+ process.stdout.write("Conversation memory cleared.\n");
141
+ continue;
142
+ }
143
+ const response = await chat.ask(line);
144
+ process.stdout.write(chat.formatResponse(response));
145
+ if (response.status === "provider-error") {
146
+ // stay in loop; user may fix provider elsewhere
147
+ }
148
+ }
149
+ return EXIT_CODES.SUCCESS;
150
+ }
151
+ finally {
152
+ rl?.close();
153
+ await chat.end();
154
+ }
155
+ }
@@ -0,0 +1,12 @@
1
+ import { type ExitCode } from "../../types/index.js";
2
+ export declare function runLearnCommand(options: {
3
+ root?: string;
4
+ mode?: string;
5
+ viva?: boolean;
6
+ docs?: boolean;
7
+ build?: string;
8
+ approve?: boolean;
9
+ applyOpsJson?: string;
10
+ json?: boolean;
11
+ useMock?: boolean;
12
+ }): Promise<ExitCode>;
@@ -0,0 +1,107 @@
1
+ import { MockModelProvider, createModelProvider, loadAiConfig } from "../../ai/index.js";
2
+ import { StudentService } from "../../agent/student.js";
3
+ import { parseAgentMode, defaultStudentMode } from "../../agent/modes.js";
4
+ import { EXIT_CODES } from "../../types/index.js";
5
+ import { resolveRepoRoot } from "../../utils/path.js";
6
+ export async function runLearnCommand(options) {
7
+ const root = resolveRepoRoot(options.root ?? process.cwd());
8
+ const mode = parseAgentMode(options.mode) ?? defaultStudentMode();
9
+ const provider = options.useMock ? new MockModelProvider() : createModelProvider(loadAiConfig());
10
+ const resolvedMode = options.build
11
+ ? mode === "LEARN"
12
+ ? "BUILD_WITH_ME"
13
+ : mode
14
+ : options.viva || options.docs
15
+ ? "LEARN"
16
+ : mode;
17
+ const student = new StudentService({
18
+ root,
19
+ provider,
20
+ mode: resolvedMode,
21
+ });
22
+ try {
23
+ if (options.build) {
24
+ let toolCalls;
25
+ if (options.applyOpsJson) {
26
+ try {
27
+ const parsed = JSON.parse(options.applyOpsJson);
28
+ if (!Array.isArray(parsed))
29
+ throw new Error("expected array");
30
+ toolCalls = parsed.map((item) => {
31
+ const row = item;
32
+ if (!row.name || typeof row.name !== "string")
33
+ throw new Error("ops need name");
34
+ return {
35
+ name: row.name,
36
+ arguments: row.arguments ?? {},
37
+ };
38
+ });
39
+ }
40
+ catch (error) {
41
+ process.stderr.write(`Invalid --apply-ops JSON: ${error instanceof Error ? error.message : String(error)}\n`);
42
+ return EXIT_CODES.USAGE_ERROR;
43
+ }
44
+ }
45
+ const result = await student.buildFeature({
46
+ goal: options.build,
47
+ approvedByHuman: options.approve === true,
48
+ ...(toolCalls ? { toolCalls } : {}),
49
+ });
50
+ if (options.json) {
51
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
52
+ }
53
+ else {
54
+ process.stdout.write(result.explanation);
55
+ if (result.status === "awaiting-approval") {
56
+ process.stdout.write("\nRe-run with --approve to execute the coding loop.\n");
57
+ }
58
+ else if (result.coding) {
59
+ process.stdout.write(result.evidenceText);
60
+ }
61
+ else {
62
+ process.stdout.write(`${result.evidenceText}\n`);
63
+ }
64
+ }
65
+ if (result.status === "awaiting-approval" || result.status === "mode_forbidden") {
66
+ return EXIT_CODES.USAGE_ERROR;
67
+ }
68
+ if (result.status === "failed" || result.status === "limit") {
69
+ return EXIT_CODES.INTERNAL_ERROR;
70
+ }
71
+ return EXIT_CODES.SUCCESS;
72
+ }
73
+ if (options.viva) {
74
+ const questions = await student.generateVivaQuestions();
75
+ if (options.json)
76
+ process.stdout.write(`${JSON.stringify({ mode, questions }, null, 2)}\n`);
77
+ else {
78
+ process.stdout.write("\nViva questions (grounded in detected stack):\n\n");
79
+ for (const [i, q] of questions.entries())
80
+ process.stdout.write(` ${i + 1}. ${q}\n`);
81
+ process.stdout.write("\n");
82
+ }
83
+ return EXIT_CODES.SUCCESS;
84
+ }
85
+ if (options.docs) {
86
+ const sections = await student.generateProjectDocumentation();
87
+ if (options.json)
88
+ process.stdout.write(`${JSON.stringify({ mode, sections }, null, 2)}\n`);
89
+ else {
90
+ for (const s of sections) {
91
+ process.stdout.write(`\n[${s.truth}] ${s.title}\n${s.body}\n`);
92
+ }
93
+ process.stdout.write("\n");
94
+ }
95
+ return EXIT_CODES.SUCCESS;
96
+ }
97
+ const explained = await student.explainProject();
98
+ if (options.json)
99
+ process.stdout.write(`${JSON.stringify({ mode, ...explained }, null, 2)}\n`);
100
+ else
101
+ process.stdout.write(explained.text);
102
+ return EXIT_CODES.SUCCESS;
103
+ }
104
+ finally {
105
+ await student.end();
106
+ }
107
+ }
@@ -17,6 +17,9 @@ import { runChangeAnalyzeCommand, runChangeDiffCommand, runChangeExplainCommand,
17
17
  import { runGraphSurfaceCommand, runPolicyCheckCommand, runPolicyEnforceCommand, runPolicyExplainCommand, runRunCommand, runRunExplainCommand, } from "./commands/policy-graph-run.js";
18
18
  import { runArchitectureCommand } from "./commands/architecture.js";
19
19
  import { runWorkspaceCommand } from "./commands/workspace.js";
20
+ import { runAskCommand, runChatCommand } from "./commands/chat.js";
21
+ import { runAgentCommand, runPlanCommand } from "./commands/agent.js";
22
+ import { runLearnCommand } from "./commands/learn.js";
20
23
  import { collectOpsHealth } from "../ops/health.js";
21
24
  import { listSessions, loadSession } from "../platform/sessions/store.js";
22
25
  import { exportReports } from "../platform/reports/export.js";
@@ -1235,6 +1238,95 @@ export function createProgram() {
1235
1238
  json: Boolean(options.json),
1236
1239
  });
1237
1240
  });
1241
+ program
1242
+ .command("chat")
1243
+ .description("Project Chat (2.1): evidence-backed conversation about this repository (requires AI provider)")
1244
+ .argument("[path]", "Repository path (default: current directory)")
1245
+ .action(async (pathArg) => {
1246
+ process.exitCode = await runChatCommand({
1247
+ root: resolveTargetArgument(pathArg),
1248
+ });
1249
+ });
1250
+ program
1251
+ .command("ask")
1252
+ .description("One-shot Project Chat question (same engine as chat)")
1253
+ .argument("<question>", "Question about the project")
1254
+ .argument("[path]", "Repository path (default: current directory)")
1255
+ .option("--json", "Emit JSON response", false)
1256
+ .action(async (question, pathArg, options) => {
1257
+ process.exitCode = await runAskCommand({
1258
+ question,
1259
+ root: resolveTargetArgument(pathArg),
1260
+ json: Boolean(options.json),
1261
+ });
1262
+ });
1263
+ program
1264
+ .command("plan")
1265
+ .description("Project Agent plan (2.1 M3): understand goal, list likely files, request approval — no file edits")
1266
+ .argument("<goal>", "What you want to build or change")
1267
+ .argument("[path]", "Repository path (default: current directory)")
1268
+ .option("--json", "Emit JSON plan", false)
1269
+ .option("--approve", "Record human approval (still no writes in M3)", false)
1270
+ .action(async (goal, pathArg, options) => {
1271
+ process.exitCode = await runPlanCommand({
1272
+ goal,
1273
+ root: resolveTargetArgument(pathArg),
1274
+ json: Boolean(options.json),
1275
+ approve: Boolean(options.approve),
1276
+ });
1277
+ });
1278
+ program
1279
+ .command("agent")
1280
+ .description("Project Agent (2.1): list tools, run tools, plan, or apply approved file/command changes")
1281
+ .argument("[path]", "Repository path (default: current directory)")
1282
+ .option("--list-tools", "List available agent tools", false)
1283
+ .option("--tool <name>", "Run a single tool by name")
1284
+ .option("--goal <text>", "Build a change plan for this goal")
1285
+ .option("--approve", "Record explicit human approval", false)
1286
+ .option("--apply", "Apply approved changes (requires --approve)", false)
1287
+ .option("--apply-ops <json>", "JSON array of {name, arguments} tool ops for --apply")
1288
+ .option("--skip-verify", "Skip post-change verification after apply", false)
1289
+ .option("--run-tests", "Run controlled tests during verification", false)
1290
+ .option("--json", "Emit JSON", false)
1291
+ .action(async (pathArg, options) => {
1292
+ process.exitCode = await runAgentCommand({
1293
+ root: resolveTargetArgument(pathArg),
1294
+ listTools: Boolean(options.listTools),
1295
+ ...(options.tool !== undefined ? { tool: options.tool } : {}),
1296
+ ...(options.goal !== undefined ? { goal: options.goal } : {}),
1297
+ approve: Boolean(options.approve),
1298
+ apply: Boolean(options.apply),
1299
+ ...(options.applyOps !== undefined ? { applyOpsJson: options.applyOps } : {}),
1300
+ verify: !options.skipVerify,
1301
+ runTests: Boolean(options.runTests),
1302
+ json: Boolean(options.json),
1303
+ });
1304
+ });
1305
+ program
1306
+ .command("learn")
1307
+ .description("Student Learn mode (2.1 M6): explain project, viva questions, grounded documentation")
1308
+ .argument("[path]", "Repository path (default: current directory)")
1309
+ .option("--mode <mode>", "LEARN | BUILD_WITH_ME | BUILD_FOR_ME | DEVELOPER", "LEARN")
1310
+ .option("--viva", "Generate viva questions from detected stack", false)
1311
+ .option("--docs", "Generate project documentation sections", false)
1312
+ .option("--build <goal>", "BUILD_WITH_ME / BUILD_FOR_ME: explain, plan, then coding loop")
1313
+ .option("--approve", "Human approval required before BUILD writes", false)
1314
+ .option("--apply-ops <json>", "Deterministic tool ops JSON array for --build --approve")
1315
+ .option("--json", "Emit JSON", false)
1316
+ .option("--mock", "Use MockModelProvider", false)
1317
+ .action(async (pathArg, options) => {
1318
+ process.exitCode = await runLearnCommand({
1319
+ root: resolveTargetArgument(pathArg),
1320
+ ...(options.mode !== undefined ? { mode: options.mode } : {}),
1321
+ viva: Boolean(options.viva),
1322
+ docs: Boolean(options.docs),
1323
+ ...(options.build !== undefined ? { build: options.build } : {}),
1324
+ approve: Boolean(options.approve),
1325
+ ...(options.applyOps !== undefined ? { applyOpsJson: options.applyOps } : {}),
1326
+ json: Boolean(options.json),
1327
+ useMock: Boolean(options.mock),
1328
+ });
1329
+ });
1238
1330
  const platform = program
1239
1331
  .command("platform")
1240
1332
  .description("AgentDoctor 2.0 local platform (intelligence, Action Policy Evaluator, sessions, reports)");
@@ -1,5 +1,5 @@
1
1
  import type { AgentId } from "./types/index.js";
2
- export declare const PACKAGE_VERSION = "2.0.1";
2
+ export declare const PACKAGE_VERSION = "2.1.0";
3
3
  export declare const DEFAULT_MAX_FILE_SIZE_BYTES: number;
4
4
  /** Directories skipped during normal discovery (unless a rule needs them later). */
5
5
  export declare const DEFAULT_IGNORE_DIRECTORIES: Set<string>;
package/dist/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const PACKAGE_VERSION = "2.0.1";
1
+ export const PACKAGE_VERSION = "2.1.0";
2
2
  export const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024; // 2 MiB
3
3
  /** Directories skipped during normal discovery (unless a rule needs them later). */
4
4
  export const DEFAULT_IGNORE_DIRECTORIES = new Set([
@@ -1,3 +1,4 @@
1
+ import type { ModelProvider } from "../ai/types.js";
1
2
  export interface DashboardServerOptions {
2
3
  root: string;
3
4
  host?: string;
@@ -9,6 +10,11 @@ export interface DashboardServerOptions {
9
10
  * Query ?user= is a localDevIdentityHint only; role elevation requires AGENTDOCTOR_ALLOW_LOCAL_IDENTITY_HINT=1.
10
11
  */
11
12
  allowNonLoopback?: boolean;
13
+ /**
14
+ * Optional provider override (tests). Production uses loadAiConfig().
15
+ * When unset and provider resolves to none, /api/chat fails closed.
16
+ */
17
+ chatProvider?: ModelProvider;
12
18
  }
13
19
  export declare function isLoopbackHost(host: string): boolean;
14
20
  /**
@@ -21,6 +21,9 @@ import { CONTRACTS_VERSION } from "../contracts/index.js";
21
21
  import { collectOpsHealth } from "../ops/health.js";
22
22
  import { listPolicyPacks } from "../policy/packs.js";
23
23
  import { sanitizeForOutput } from "../utils/path.js";
24
+ import { createModelProvider, loadAiConfig } from "../ai/index.js";
25
+ import { ChatService } from "../agent/chat/service.js";
26
+ import { formatChatResponseForCli } from "../agent/chat/response.js";
24
27
  function safeJsonError(error) {
25
28
  const raw = error instanceof Error ? error.message : String(error);
26
29
  const { text } = redactSecrets(sanitizeForOutput(raw));
@@ -102,6 +105,7 @@ function htmlPage() {
102
105
  <a href="#brain">Brain</a>
103
106
  <a href="#graph">Graph</a>
104
107
  <a href="#knowledge">Knowledge</a>
108
+ <a href="#chat">Project Chat</a>
105
109
  <a href="#meta">Audit / baselines</a>
106
110
  </nav>
107
111
  <section id="overview">
@@ -128,6 +132,21 @@ function htmlPage() {
128
132
  <h2>Governed knowledge</h2>
129
133
  <pre id="knowledgeBody" class="muted">Loading…</pre>
130
134
  </section>
135
+ <section id="chat">
136
+ <h2>Project Chat</h2>
137
+ <p class="muted">Ask about this repository. Answers use evidence + truth labels. No file writes from this panel.</p>
138
+ <div style="display:grid;grid-template-columns:1.4fr 1fr;gap:1rem">
139
+ <div>
140
+ <textarea id="chatInput" rows="3" style="width:100%;background:#0d1520;color:#e8eef5;border:1px solid #243040;border-radius:8px;padding:0.6rem" placeholder="How does authentication work?"></textarea>
141
+ <button id="chatAsk" style="margin-top:0.5rem;background:var(--accent);color:#041018;border:0;border-radius:6px;padding:0.45rem 0.9rem;font-weight:600;cursor:pointer">Ask</button>
142
+ <pre id="chatBody" class="muted" style="margin-top:0.75rem">Ask a question…</pre>
143
+ </div>
144
+ <div>
145
+ <h3 style="margin:0 0 0.5rem;font-size:0.9rem">Evidence</h3>
146
+ <pre id="chatEvidence" class="muted">—</pre>
147
+ </div>
148
+ </div>
149
+ </section>
131
150
  <section id="meta">
132
151
  <h2>Safe Fix audit / baselines / sessions</h2>
133
152
  <pre id="metaBody" class="muted">Loading…</pre>
@@ -155,6 +174,28 @@ function htmlPage() {
155
174
  load().catch(err => {
156
175
  document.getElementById('status').textContent = String(err);
157
176
  });
177
+ document.getElementById('chatAsk').addEventListener('click', async () => {
178
+ const question = document.getElementById('chatInput').value.trim();
179
+ if (!question) return;
180
+ document.getElementById('chatBody').textContent = 'Thinking…';
181
+ try {
182
+ const res = await fetch('/api/chat', {
183
+ method: 'POST',
184
+ headers: { 'Content-Type': 'application/json' },
185
+ body: JSON.stringify({ question }),
186
+ });
187
+ const data = await res.json();
188
+ document.getElementById('chatBody').textContent = data.message || JSON.stringify(data, null, 2);
189
+ document.getElementById('chatEvidence').textContent = JSON.stringify({
190
+ truthClaims: data.truthClaims || [],
191
+ citations: data.citations || [],
192
+ limitations: data.limitations || [],
193
+ status: data.status,
194
+ }, null, 2);
195
+ } catch (err) {
196
+ document.getElementById('chatBody').textContent = String(err);
197
+ }
198
+ });
158
199
  </script>
159
200
  </body>
160
201
  </html>`;
@@ -186,8 +227,55 @@ export async function startDashboardServer(options) {
186
227
  return;
187
228
  }
188
229
  const url = new URL(rawUrl, `http://${host}:${port}`);
230
+ if (req.method === "POST" && url.pathname === "/api/chat") {
231
+ const chunks = [];
232
+ for await (const chunk of req) {
233
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
234
+ }
235
+ let body = {};
236
+ try {
237
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
238
+ }
239
+ catch {
240
+ sendJson(res, 400, { error: "invalid JSON body" });
241
+ return;
242
+ }
243
+ const question = typeof body.question === "string" ? body.question.trim() : "";
244
+ if (!question) {
245
+ sendJson(res, 400, { error: "question required" });
246
+ return;
247
+ }
248
+ const provider = options.chatProvider ?? createModelProvider(loadAiConfig());
249
+ if (provider.id === "none") {
250
+ sendJson(res, 503, {
251
+ error: "provider-none",
252
+ message: "AI chat is not configured. Set AGENTDOCTOR_AI_PROVIDER to mock or an OpenAI-compatible provider. Silent mock fallback is disabled.",
253
+ status: "provider-none",
254
+ });
255
+ return;
256
+ }
257
+ const chat = new ChatService({
258
+ root,
259
+ provider,
260
+ persistAudit: false,
261
+ });
262
+ try {
263
+ const response = await chat.ask(question);
264
+ sendJson(res, response.status === "ok" ? 200 : 502, {
265
+ ...response,
266
+ cliPreview: formatChatResponseForCli(response),
267
+ note: "Dashboard chat is ask-only; it does not edit files or run commands.",
268
+ });
269
+ }
270
+ finally {
271
+ await chat.end();
272
+ }
273
+ return;
274
+ }
189
275
  if (req.method !== "GET") {
190
- sendJson(res, 405, { error: "read-only dashboard; GET only" });
276
+ sendJson(res, 405, {
277
+ error: "dashboard is read-only except POST /api/chat (ask-only; no repo writes)",
278
+ });
191
279
  return;
192
280
  }
193
281
  if (pathnameLooksHostile(url.pathname)) {
@@ -1,4 +1,9 @@
1
1
  import type { PolicyDecisionContract } from "../contracts/index.js";
2
+ /**
3
+ * Map argv to a portable executable form for shell:false execution.
4
+ * Policy evaluation still uses the original human-facing argv/command string.
5
+ */
6
+ export declare function resolveExecutableArgv(argv: string[]): string[];
2
7
  export type ExecutionStatus = "executed" | "blocked-by-enforcement" | "not-executed" | "execution-failed";
3
8
  export interface EnforcementResult {
4
9
  decision: PolicyDecisionContract;
@@ -4,15 +4,59 @@ import { execFile, spawn } from "node:child_process";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { promisify } from "node:util";
7
- import { realpathSync } from "node:fs";
7
+ import { existsSync, realpathSync } from "node:fs";
8
8
  import { evaluateAgentAction, EVALUATE_ONLY } from "../platform/firewall/evaluate.js";
9
9
  import { resolveRepoRoot } from "../utils/path.js";
10
10
  import { PathEscapeError, assertInsideRepo } from "../security/paths.js";
11
11
  const execFileAsync = promisify(execFile);
12
+ /**
13
+ * Resolve npm/npx to `node <cli.js>` on Windows so execFile(shell:false) does not
14
+ * hit ENOENT on the `npm` name or fail to spawn `.cmd` shims without a shell.
15
+ */
16
+ function resolveNpmCliJs(cliFile) {
17
+ const candidates = [
18
+ path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", cliFile),
19
+ path.join(path.dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", cliFile),
20
+ ];
21
+ for (const candidate of candidates) {
22
+ if (existsSync(candidate))
23
+ return candidate;
24
+ }
25
+ return null;
26
+ }
27
+ /**
28
+ * Map argv to a portable executable form for shell:false execution.
29
+ * Policy evaluation still uses the original human-facing argv/command string.
30
+ */
31
+ export function resolveExecutableArgv(argv) {
32
+ if (argv.length === 0)
33
+ return argv;
34
+ const bin = argv[0];
35
+ if (process.platform === "win32" && (bin === "npm" || bin === "npx")) {
36
+ const cli = resolveNpmCliJs(bin === "npx" ? "npx-cli.js" : "npm-cli.js");
37
+ if (cli) {
38
+ return [process.execPath, cli, ...argv.slice(1)];
39
+ }
40
+ // Last resort: .cmd name (may still require shell on some hosts).
41
+ return [`${bin}.cmd`, ...argv.slice(1)];
42
+ }
43
+ return argv;
44
+ }
12
45
  const DEFAULT_TIMEOUT_MS = 30_000;
13
46
  const DEFAULT_MAX_BUFFER = 1024 * 1024;
14
47
  const OUTPUT_TRUNCATE = 32_768;
15
- const ENV_ALLOWLIST = new Set(["PATH", "NODE_ENV", "HOME", "LANG"]);
48
+ const ENV_ALLOWLIST = new Set([
49
+ "PATH",
50
+ "Path",
51
+ "NODE_ENV",
52
+ "HOME",
53
+ "USERPROFILE",
54
+ "LANG",
55
+ "SystemRoot",
56
+ "SYSTEMROOT",
57
+ "ComSpec",
58
+ "PATHEXT",
59
+ ]);
16
60
  /**
17
61
  * Parse a command string into argv without invoking a shell.
18
62
  * Supports simple double/single quotes; rejects unclosed quotes.
@@ -195,7 +239,8 @@ async function executeArgv(options) {
195
239
  });
196
240
  });
197
241
  }
198
- const [file, ...args] = options.argv;
242
+ const resolved = resolveExecutableArgv(options.argv);
243
+ const [file, ...args] = resolved;
199
244
  if (!file) {
200
245
  return {
201
246
  stdout: "",
@@ -206,6 +251,8 @@ async function executeArgv(options) {
206
251
  error: "empty command",
207
252
  };
208
253
  }
254
+ // Windows .cmd shims cannot be execFile'd with shell:false; use shell only then.
255
+ const needsWindowsCmdShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(file);
209
256
  try {
210
257
  const result = await execFileAsync(file, args, {
211
258
  cwd,
@@ -213,7 +260,7 @@ async function executeArgv(options) {
213
260
  timeout: options.timeoutMs,
214
261
  maxBuffer: options.maxBuffer,
215
262
  windowsHide: true,
216
- shell: false,
263
+ shell: needsWindowsCmdShell,
217
264
  });
218
265
  return {
219
266
  stdout: truncate(typeof result.stdout === "string" ? result.stdout : String(result.stdout)),
package/dist/index.d.ts CHANGED
@@ -50,3 +50,16 @@ export { runFix } from "./core/fix/run.js";
50
50
  export { evaluatePolicy, evaluateScanPolicy, evaluateVerifyPolicy, parseFailOnRules, parseSeverityGate, } from "./core/policy/evaluate.js";
51
51
  export type { PolicyOptions, PolicyViolation, PolicyViolationCode, PolicyInput, } from "./core/policy/evaluate.js";
52
52
  export type { FixPlan, FixAction, FixApplyResult } from "./core/fix/types.js";
53
+ /** AgentDoctor 2.1 AI provider layer (optional — core works without AI). */
54
+ export { createModelProvider, loadAiConfig, publicAiConfig, redactForModel, AI_PROVIDER_REQUIRED_MESSAGE, NoneModelProvider, MockModelProvider, OpenAiCompatibleProvider, } from "./ai/index.js";
55
+ export type { AiConfig, AiProviderId, ChatMessage, ChatRequest, ChatResponse, ModelMetadata, ModelProvider, ToolSpec, ToolCallRequest, TokenUsage, } from "./ai/index.js";
56
+ /** AgentDoctor 2.1 Project AI Agent core (M1 scaffolding). */
57
+ export { AgentState, AgentStateMachine, AgentRuntime, DEFAULT_AGENT_LIMITS, retrieveProjectContext, truthLabelHelp, minTruth, } from "./agent/index.js";
58
+ export type { AgentLimits, AgentAuditEvent, AgentRuntimeOptions, AgentTurnResult, AgentStateTransition, ContextBundle, ContextCitation, EvidenceType, TruthLabel, RetrieveContextOptions, } from "./agent/index.js";
59
+ export { ChatService, createChatService, ChatMemory, CHAT_PROVIDER_NONE_MESSAGE, buildChatTurnResponse, formatChatResponseForCli, summarizeProjectForChat, } from "./agent/chat/index.js";
60
+ export type { ChatTurnResponse, ChatServiceOptions, TruthClaim, ProjectChatSummary, } from "./agent/chat/index.js";
61
+ /** AgentDoctor 2.1 agent tools + plan (M3). */
62
+ export { listAgentToolSpecs, getToolSpec, riskForTool, executeAgentTool, isReadTool, newToolCall, evaluateApproval, formatApprovalPrompt, buildAgentPlan, formatAgentPlan, approvePlan, runCodingLoop, verifyAgentWork, formatVerificationReport, } from "./agent/index.js";
63
+ export type { AgentToolName, AgentToolSpec, AgentToolCall, AgentToolResult, AgentRiskLevel, ApprovalRequest, ApprovalResult, ApprovalDecision, AgentPlan, AgentPlanStep, CodingLoopOptions, CodingLoopResult, AgentVerificationReport, VerificationCheck, } from "./agent/index.js";
64
+ export { getModeProfile, defaultStudentMode, parseAgentMode, modeAllowsMutation, modeBlocksToolCategory, StudentService, } from "./agent/index.js";
65
+ export type { AgentMode, ModeProfile, StudentDocSection, BuildWithMeResult, } from "./agent/index.js";