mtech-ai-local-runner 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,12 @@ The package stores safe runner configuration locally and keeps OAuth refresh cre
10
10
 
11
11
  Public npm installation requires no npm account or publishing OTP. Setup requires Gateway OAuth login, and local Claude Code must already be authenticated (`claude auth status`).
12
12
 
13
+ ## 0.1.6
14
+
15
+ - Require a final JSON report with actual findings, nonempty evidence and no pending work before reporting completion. Incomplete or missing reports fail without a successful investigation artifact.
16
+ - Explicitly restrict available built-in tools and ignore child messages as executor completion evidence.
17
+ - Preserve employee-local duration/count policy and Gateway model selection. Structured assertions remain client-reported and require separate semantic review.
18
+
13
19
  ## 0.1.5
14
20
 
15
21
  - Authenticate local MCP relay callers with a random per-job bearer capability.
@@ -29,6 +35,6 @@ The relay binds only to loopback. Local capabilities are stored in private mode-
29
35
  Existing employees can upgrade their installed daemon with:
30
36
 
31
37
  ```bash
32
- npx mtech-ai-local-runner@0.1.5 setup
33
- npx mtech-ai-local-runner@0.1.5 doctor
38
+ npx mtech-ai-local-runner@0.1.6 setup
39
+ npx mtech-ai-local-runner@0.1.6 doctor
34
40
  ```
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { isTerminalEventType } from "../../protocol.js";
4
4
  import { parseClaudeLine } from "./stream-parser.js";
5
5
  import { compilePrompt } from "./prompt-compiler.js";
6
+ import { completionEvent } from "./completion-report.js";
6
7
  import { buildHookPolicy, encodeHookPolicy } from "../../safety/hook-policy-builder.js";
7
8
  import { HOOK_POLICY_ENV_VAR } from "../../safety/hook-cli.js";
8
9
  const ALLOWED_ENV_KEYS = ["PATH", "HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "TMPDIR", "NODE_ENV"];
@@ -108,6 +109,7 @@ export class ClaudeCodeAdapter {
108
109
  "--settings", JSON.stringify(settings),
109
110
  "--setting-sources", "user",
110
111
  "--disable-slash-commands",
112
+ "--tools", builtinToolSurface.join(","),
111
113
  "--allowedTools", toolSurface.join(","),
112
114
  ];
113
115
  if (ctx.resumeSessionId) {
@@ -136,9 +138,25 @@ export class ClaudeCodeAdapter {
136
138
  });
137
139
  let sawTerminal = false;
138
140
  for await (const line of handle.stdout) {
139
- const parsed = parseClaudeLine(line, this.options.worktreeRoot);
141
+ let message;
142
+ try {
143
+ const value = JSON.parse(line);
144
+ if (!value || typeof value !== "object" || Array.isArray(value))
145
+ continue;
146
+ message = value;
147
+ }
148
+ catch {
149
+ continue;
150
+ }
151
+ // A nested agent message is not evidence that the executor has finished.
152
+ if (message.parent_tool_use_id != null)
153
+ continue;
154
+ let parsed = parseClaudeLine(line, this.options.worktreeRoot);
140
155
  if (!parsed)
141
156
  continue;
157
+ if (parsed.eventType === "completed") {
158
+ parsed = completionEvent(message.result, parsed.payload?.cost_usd);
159
+ }
142
160
  if (isTerminalEventType(parsed.eventType))
143
161
  sawTerminal = true;
144
162
  yield parsed;
@@ -0,0 +1,52 @@
1
+ import { MAX_EVENT_SUMMARY_CHARS } from "../../protocol.js";
2
+ const MAX_EVIDENCE_ITEMS = 20;
3
+ const MAX_EVIDENCE_CHARS = 500;
4
+ const MAX_REPORT_ENVELOPE_CHARS = 16_000;
5
+ export const COMPLETION_REPORT_NOTICE = "Do the assigned work yourself using the available tools; do not delegate or start background work. " +
6
+ "Do not report completed while waiting for results, tests, or another agent. " +
7
+ "Your final response must be one JSON object, without Markdown fences: " +
8
+ '{"status":"completed","report":"actual findings and results","evidence":["file path and observed finding, or command and actual result"],"pending_work":[]}. ' +
9
+ `The report must be nonempty and at most ${MAX_EVENT_SUMMARY_CHARS} characters. ` +
10
+ `Include 1-${MAX_EVIDENCE_ITEMS} nonempty evidence strings, each at most ${MAX_EVIDENCE_CHARS} characters. ` +
11
+ "Address the assigned objective and each applicable acceptance criterion with observed evidence, not a plan or promise. " +
12
+ "Only attest to executor work you observed; Gateway records transport, completion, and review receipts separately. " +
13
+ 'If the task is unfinished, use "status":"blocked" and list the remaining work in "pending_work". ' +
14
+ "An incomplete or missing report is a failed execution, never a successful investigation artifact.";
15
+ function boundedText(value, max) {
16
+ return typeof value === "string" && value.trim().length > 0 && value.length <= max;
17
+ }
18
+ /** Validate the full final result before truncation; never publish malformed/interim output. */
19
+ export function completionEvent(result, costUsd) {
20
+ const usage = typeof costUsd === "number" && Number.isFinite(costUsd) && costUsd >= 0
21
+ ? { cost_usd: costUsd }
22
+ : {};
23
+ const failed = {
24
+ eventType: "failed",
25
+ summary: "completion report is missing, incomplete or invalid",
26
+ payload: usage,
27
+ };
28
+ if (typeof result !== "string" || result.length > MAX_REPORT_ENVELOPE_CHARS)
29
+ return failed;
30
+ let report;
31
+ try {
32
+ report = JSON.parse(result);
33
+ }
34
+ catch {
35
+ return failed;
36
+ }
37
+ if (!report || typeof report !== "object" || Array.isArray(report))
38
+ return failed;
39
+ const value = report;
40
+ if (Object.keys(value).some((key) => !["status", "report", "evidence", "pending_work"].includes(key))
41
+ || value.status !== "completed"
42
+ || !boundedText(value.report, MAX_EVENT_SUMMARY_CHARS)
43
+ || !Array.isArray(value.pending_work) || value.pending_work.length !== 0
44
+ || !Array.isArray(value.evidence) || value.evidence.length < 1 || value.evidence.length > MAX_EVIDENCE_ITEMS
45
+ || !value.evidence.every((item) => boundedText(item, MAX_EVIDENCE_CHARS)))
46
+ return failed;
47
+ return {
48
+ eventType: "completed",
49
+ summary: value.report.trim(),
50
+ payload: { ...usage, investigation_summary: value.report.trim(), completion_evidence: value.evidence },
51
+ };
52
+ }
@@ -1,3 +1,4 @@
1
+ import { COMPLETION_REPORT_NOTICE } from "./completion-report.js";
1
2
  const STOP_BEFORE_PUBLICATION_RULE = "Stop before publication. Never run git commit, git push, create a pull request, merge, " +
2
3
  "deploy, access production, send an external message, or write data outside this worktree.";
3
4
  const EVENT_SCHEMA_NOTICE = "Report progress only through the normalized event lifecycle (started, progress, file_changed, question, " +
@@ -53,6 +54,7 @@ export function compilePrompt(taskPackage, options) {
53
54
  `## Prohibited actions\n${prohibitedActions.map((item) => `- ${item}`).join("\n")}`,
54
55
  `## Stop-before-publication rule\n${STOP_BEFORE_PUBLICATION_RULE}`,
55
56
  `## Event and checkpoint obligations\n${EVENT_SCHEMA_NOTICE}`,
57
+ `## Final completion report\n${COMPLETION_REPORT_NOTICE}`,
56
58
  ];
57
59
  return sections.join("\n\n");
58
60
  }
package/dist/src/cli.js CHANGED
@@ -182,7 +182,7 @@ export async function main(argv, env, io = {}) {
182
182
  const authenticated = await runFile(binary, ["auth", "status"], { encoding: "utf8" }).then(() => true).catch(() => false);
183
183
  const registered = await gateway.registerLocalRunner({
184
184
  deviceFingerprint: createHash("sha256").update(`${process.getuid?.() ?? 0}|${runnerDisplayName()}|${config.gatewayMcpUrl}`).digest("hex"),
185
- displayName: runnerDisplayName(), packageVersion: "0.1.5",
185
+ displayName: runnerDisplayName(), packageVersion: "0.1.6",
186
186
  capabilities: [{ adapter: "claude_code_cli", version: "1", capabilities: ["repository_read", "repository_edit", "shell_test", "git_diff"], authenticated }],
187
187
  });
188
188
  await localStore.save({ schemaVersion: 1, gatewayMcpUrl: config.gatewayMcpUrl, runnerId: registered.runner.id, workspaceRoot: config.workspaceRoot, concurrency: 1 });
@@ -72,7 +72,7 @@ export async function buildDaemon(input) {
72
72
  const poller = new JobPoller({
73
73
  heartbeat: () => gateway.heartbeatLocalRunner({
74
74
  deviceFingerprint: createHash("sha256").update(`${process.getuid?.() ?? 0}|${runnerDisplayName()}|${input.gatewayMcpUrl}`).digest("hex"),
75
- displayName: runnerDisplayName(), packageVersion: "0.1.5",
75
+ displayName: runnerDisplayName(), packageVersion: "0.1.6",
76
76
  capabilities: probes.map((probe) => ({ adapter: probe.id, version: probe.version,
77
77
  capabilities: probe.capabilities, authenticated: probe.available && probe.authenticated })),
78
78
  }).then(() => undefined),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {