mtech-ai-local-runner 0.1.21 → 0.1.23

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,33 @@ 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.23: separate progress from the final report
14
+
15
+ Codex receives native-message instructions: optional progress is plain text and
16
+ the structured completed/blocked report is emitted once at the end. The Runner
17
+ derives lifecycle events; Codex is no longer asked to author normalized progress
18
+ or checkpoint events. Claude's existing instructions are unchanged.
19
+
20
+ Strict report uniqueness and final-message validation remain in force. Fake
21
+ tests reproduce the observed multiple-report failure, but do not prove why the
22
+ historical model emitted three reports or guarantee future model compliance.
23
+ Publication and foreground installation do not rerun Codex or update the daemon.
24
+
25
+ ## 0.1.22: completion report classification and terminal diagnostics
26
+
27
+ Report validation distinguishes missing messages, zero valid reports, multiple
28
+ valid reports, an invalid final message and an incomplete turn. Runtime events
29
+ and terminal Job results retain only fixed codes and consistent message/report
30
+ counts, never raw provider output. Structured blocked reports use the same
31
+ bounded single-fence envelope rules as completed reports and remain blocked.
32
+
33
+ Exactly one valid final report and successful turn completion remain required;
34
+ multiple reports are never picked or merged. Existing evidence, scope, identity,
35
+ cancellation and unlimited admitted employee work remain unchanged. Earlier
36
+ 0.1.21 report failure cannot be reclassified without its missing original
37
+ counts. Publication and foreground installation do not update the daemon or
38
+ rerun Codex. See [completion report validation](https://bitbucket.org/mtechbal/api-cloud-service-ai-gateway/src/development/docs/ai-ops-gateway/codex-completion-report-validation.md).
39
+
13
40
  ## 0.1.21: exclude personal history from isolated job startup
14
41
 
15
42
  The job-only Codex wrapper denies reads of the original account's `sessions`
@@ -1,6 +1,6 @@
1
1
  import { compilePrompt as compileBrief } from "../prompt-compiler.js";
2
2
  export function compileCodexPrompt(taskPackage, label) {
3
- return `${compileBrief(taskPackage, { workItemLabel: label })}\n\nUse only the ai_local job tools for repository reads, edits and approved tests. Native filesystem, commands, web, images and delegation are unavailable or denied. A denied tool is a scope boundary, never a reason to try another path. If required work cannot proceed, return the structured blocked report.`;
3
+ return `${compileBrief(taskPackage, { workItemLabel: label, progressReporting: "native_messages" })}\n\nUse only the ai_local job tools for repository reads, edits and approved tests. Native filesystem, commands, web, images and delegation are unavailable or denied. A denied tool is a scope boundary, never a reason to try another path. If required work cannot proceed, return the structured blocked report.`;
4
4
  }
5
5
  export const CODEX_REPORT_SCHEMA = {
6
6
  type: "object", additionalProperties: false, required: ["status", "report", "evidence", "pending_work"],
@@ -1,19 +1,13 @@
1
- import { completionEvent } from "../completion-report.js";
1
+ import { completionEnvelope, completionEvent } from "../completion-report.js";
2
2
  function record(value) {
3
3
  return value !== null && typeof value === "object" && !Array.isArray(value)
4
4
  ? value : undefined;
5
5
  }
6
6
  function finalReport(text) {
7
7
  const complete = completionEvent(text);
8
- if (complete.eventType === "completed" || typeof text !== "string" || text.length > 16_000)
8
+ if (complete.eventType === "completed")
9
9
  return complete;
10
- let value;
11
- try {
12
- value = record(JSON.parse(text));
13
- }
14
- catch {
15
- return complete;
16
- }
10
+ const value = completionEnvelope(text);
17
11
  const bounded = (v, size) => typeof v === "string" && v.trim().length > 0 && v.length <= size;
18
12
  const entries = (v, min) => Array.isArray(v) && v.length >= min && v.length <= 20 && v.every(item => bounded(item, 500));
19
13
  if (!value || Object.keys(value).some(key => !["status", "report", "evidence", "pending_work"].includes(key))
@@ -26,6 +20,7 @@ function finalReport(text) {
26
20
  export class CodexStreamParser {
27
21
  stopped = false;
28
22
  report;
23
+ messages = 0;
29
24
  candidates = 0;
30
25
  parse(line) {
31
26
  if (this.stopped)
@@ -53,15 +48,21 @@ export class CodexStreamParser {
53
48
  return this.finish({ eventType: "scope_conflict", summary: "Codex emitted a native tool operation outside the job bridge" });
54
49
  }
55
50
  if (event.type === "item.completed" && item.type === "agent_message") {
56
- this.report = item.text;
57
- if (["completed", "blocked"].includes(finalReport(item.text).eventType))
51
+ this.messages += 1;
52
+ this.report = finalReport(item.text);
53
+ if (["completed", "blocked"].includes(this.report.eventType))
58
54
  this.candidates += 1;
59
55
  }
60
56
  }
61
57
  if (event.type !== "turn.completed")
62
58
  return [];
63
- const terminal = this.candidates === 1 ? finalReport(this.report)
64
- : { eventType: "failed", summary: "Codex did not return one unambiguous completion report" };
59
+ // Keep final-message validity and uniqueness independent. Never select an
60
+ // earlier report over an invalid final message, or pick between two reports.
61
+ const terminal = this.candidates === 0
62
+ ? this.reportFailure(this.messages === 0 ? "CODEX_COMPLETION_REPORT_MISSING" : "CODEX_COMPLETION_REPORT_INVALID")
63
+ : this.candidates > 1 ? this.reportFailure("CODEX_COMPLETION_REPORT_MULTIPLE")
64
+ : this.report && ["completed", "blocked"].includes(this.report.eventType) ? this.report
65
+ : this.reportFailure("CODEX_COMPLETION_REPORT_NOT_FINAL");
65
66
  const usage = record(event.usage);
66
67
  const observed = {};
67
68
  for (const field of ["input_tokens", "cached_input_tokens", "output_tokens"]) {
@@ -75,7 +76,20 @@ export class CodexStreamParser {
75
76
  return this.finish(terminal);
76
77
  }
77
78
  end() {
78
- return this.stopped ? [] : this.finish({ eventType: "failed", summary: "Codex ended without a validated completion report" });
79
+ return this.stopped ? [] : this.finish(this.reportFailure("CODEX_TURN_INCOMPLETE"));
80
+ }
81
+ reportFailure(code) {
82
+ const summaries = {
83
+ CODEX_COMPLETION_REPORT_MISSING: "Codex returned no completed agent message",
84
+ CODEX_COMPLETION_REPORT_INVALID: "Codex returned no valid completion report",
85
+ CODEX_COMPLETION_REPORT_MULTIPLE: "Codex returned multiple valid completion reports",
86
+ CODEX_COMPLETION_REPORT_NOT_FINAL: "Codex's final message was not the unique valid completion report",
87
+ CODEX_TURN_INCOMPLETE: "Codex ended before successful turn completion",
88
+ };
89
+ return { eventType: "failed", summary: summaries[code], payload: { completion_report: {
90
+ version: "codex-completion-report-v1", code, agent_message_count: this.messages,
91
+ valid_report_count: this.candidates, invalid_message_count: this.messages - this.candidates,
92
+ } } };
79
93
  }
80
94
  finish(event) {
81
95
  this.stopped = true;
@@ -2,6 +2,24 @@ import { MAX_EVENT_SUMMARY_CHARS } from "../protocol.js";
2
2
  const MAX_EVIDENCE_ITEMS = 20;
3
3
  const MAX_EVIDENCE_CHARS = 500;
4
4
  const MAX_REPORT_ENVELOPE_CHARS = 16_000;
5
+ const REPORT_FAILURE_CODES = ["CODEX_COMPLETION_REPORT_MISSING", "CODEX_COMPLETION_REPORT_INVALID", "CODEX_COMPLETION_REPORT_MULTIPLE", "CODEX_COMPLETION_REPORT_NOT_FINAL", "CODEX_TURN_INCOMPLETE"];
6
+ /** Diagnostic metadata only, never completion authority. Copy no provider text. */
7
+ export function completionReportDiagnostic(value) {
8
+ if (!value || typeof value !== "object" || Array.isArray(value))
9
+ return undefined;
10
+ const d = value, code = d.code;
11
+ const messages = d.agent_message_count, valid = d.valid_report_count, invalid = d.invalid_message_count;
12
+ if (d.version !== "codex-completion-report-v1" || typeof code !== "string" || !REPORT_FAILURE_CODES.includes(code)
13
+ || typeof messages !== "number" || !Number.isSafeInteger(messages) || messages < 0
14
+ || typeof valid !== "number" || !Number.isSafeInteger(valid) || valid < 0 || valid > messages
15
+ || typeof invalid !== "number" || !Number.isSafeInteger(invalid) || invalid !== messages - valid
16
+ || code === "CODEX_COMPLETION_REPORT_MISSING" && messages !== 0
17
+ || code === "CODEX_COMPLETION_REPORT_INVALID" && (messages === 0 || valid !== 0)
18
+ || code === "CODEX_COMPLETION_REPORT_MULTIPLE" && valid < 2
19
+ || code === "CODEX_COMPLETION_REPORT_NOT_FINAL" && (valid !== 1 || invalid === 0))
20
+ return undefined;
21
+ return { version: "codex-completion-report-v1", code, agent_message_count: messages, valid_report_count: valid, invalid_message_count: invalid };
22
+ }
5
23
  export const COMPLETION_REPORT_NOTICE = "Do the assigned work yourself using the available tools; do not delegate or start background work. " +
6
24
  "Do not report completed while waiting for results, tests, or another agent. " +
7
25
  "Your final response must be one JSON object, without Markdown fences: " +
@@ -15,40 +33,43 @@ export const COMPLETION_REPORT_NOTICE = "Do the assigned work yourself using the
15
33
  function boundedText(value, max) {
16
34
  return typeof value === "string" && value.trim().length > 0 && value.length <= max;
17
35
  }
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
- };
36
+ /** Decode bounded framing only. Callers must still validate status and evidence. */
37
+ export function completionEnvelope(result) {
28
38
  if (typeof result !== "string" || result.length > MAX_REPORT_ENVELOPE_CHARS)
29
- return failed;
39
+ return undefined;
30
40
  let report;
31
41
  try {
32
42
  report = JSON.parse(result);
33
43
  }
34
44
  catch {
35
- // Claude may wrap the final report in one Markdown fence after a short
36
- // explanation. Accept framing only; retain every completion/evidence check.
37
45
  if ((result.match(/```/g) ?? []).length !== 2)
38
- return failed;
46
+ return undefined;
39
47
  const framed = result.trim().match(/^(?:[\s\S]*?\n)?```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n```$/);
40
48
  if (!framed)
41
- return failed;
49
+ return undefined;
42
50
  try {
43
51
  report = JSON.parse(framed[1]);
44
52
  }
45
53
  catch {
46
- return failed;
54
+ return undefined;
47
55
  }
48
56
  }
49
- if (!report || typeof report !== "object" || Array.isArray(report))
57
+ return report && typeof report === "object" && !Array.isArray(report)
58
+ ? report : undefined;
59
+ }
60
+ /** Validate the full final result before truncation; never publish malformed/interim output. */
61
+ export function completionEvent(result, costUsd) {
62
+ const usage = typeof costUsd === "number" && Number.isFinite(costUsd) && costUsd >= 0
63
+ ? { cost_usd: costUsd }
64
+ : {};
65
+ const failed = {
66
+ eventType: "failed",
67
+ summary: "completion report is missing, incomplete or invalid",
68
+ payload: usage,
69
+ };
70
+ const value = completionEnvelope(result);
71
+ if (!value)
50
72
  return failed;
51
- const value = report;
52
73
  if (Object.keys(value).some((key) => !["status", "report", "evidence", "pending_work"].includes(key))
53
74
  || value.status !== "completed"
54
75
  || !boundedText(value.report, MAX_EVENT_SUMMARY_CHARS)
@@ -4,6 +4,11 @@ const STOP_BEFORE_PUBLICATION_RULE = "Stop before publication. Never run git com
4
4
  const EVENT_SCHEMA_NOTICE = "Report progress only through the normalized event lifecycle (started, progress, file_changed, question, " +
5
5
  "test_started, test_result, budget_warning, checkpoint_ready, blocked, completed, failed). " +
6
6
  "Checkpoint your work with checkpoint_ready before stopping whenever it is safe to do so.";
7
+ const NATIVE_PROGRESS_NOTICE = "The Runner derives lifecycle events from the native CLI stream; do not author normalized lifecycle or checkpoint events. " +
8
+ "If progress commentary is needed, use brief plain text, not the completion-report schema. " +
9
+ "Do not emit report-shaped JSON in commentary or progress messages. " +
10
+ "Emit the structured completion report exactly once, as your final response after all available work and checks. " +
11
+ "Do not repeat, preview, revise or re-emit a completed or blocked report. If blocked, stop work and emit that one final blocked report.";
7
12
  function requiredArray(value, label) {
8
13
  if (!Array.isArray(value)) {
9
14
  throw new Error(`task package is missing ${label}`);
@@ -53,7 +58,9 @@ export function compilePrompt(taskPackage, options) {
53
58
  `## Required tests\n${requiredTests.map((item) => `- ${item}`).join("\n")}`,
54
59
  `## Prohibited actions\n${prohibitedActions.map((item) => `- ${item}`).join("\n")}`,
55
60
  `## Stop-before-publication rule\n${STOP_BEFORE_PUBLICATION_RULE}`,
56
- `## Event and checkpoint obligations\n${EVENT_SCHEMA_NOTICE}`,
61
+ options.progressReporting === "native_messages"
62
+ ? `## Native progress and terminal reporting\n${NATIVE_PROGRESS_NOTICE}`
63
+ : `## Event and checkpoint obligations\n${EVENT_SCHEMA_NOTICE}`,
57
64
  `## Final completion report\n${COMPLETION_REPORT_NOTICE}`,
58
65
  ];
59
66
  return sections.join("\n\n");
@@ -2,6 +2,7 @@ import { executionError, executionFailure } from '../safety/execution-failure.js
2
2
  import { requireSecurityRequirements } from '../safety/security-requirements.js';
3
3
  import { GatewayToolError } from "../gateway-client.js";
4
4
  import { createHash } from "node:crypto";
5
+ import { completionReportDiagnostic } from "../adapters/completion-report.js";
5
6
  import { PROTOCOL_VERSION, isTerminalEventType, validateEvent, MAX_EVENT_SUMMARY_CHARS } from "../protocol.js";
6
7
  function canonical(value) {
7
8
  if (Array.isArray(value))
@@ -268,7 +269,8 @@ export class ExecutionCoordinator {
268
269
  clearInterval(heartbeatTimer);
269
270
  }
270
271
  const status = terminal.eventType === "completed" ? "completed" : terminal.eventType === "cancelled" ? "cancelled" : terminal.eventType === "blocked" || terminal.eventType === "scope_conflict" ? "blocked" : "failed";
271
- await this.deps.gateway.complete({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, status, result: { binding_id: binding.bindingId, event_type: terminal.eventType, summary: terminal.summary, ...(failureDiagnostic ? { execution_failure: { ...failureDiagnostic } } : {}), ...(adapter.securityReceipt?.() ? { security_receipt: adapter.securityReceipt() } : {}) } });
272
+ const reportDiagnostic = status === "failed" ? completionReportDiagnostic(terminal.payload?.completion_report) : undefined;
273
+ await this.deps.gateway.complete({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, status, result: { binding_id: binding.bindingId, event_type: terminal.eventType, summary: terminal.summary, ...(failureDiagnostic ? { execution_failure: { ...failureDiagnostic } } : {}), ...(reportDiagnostic ? { completion_report: reportDiagnostic } : {}), ...(adapter.securityReceipt?.() ? { security_receipt: adapter.securityReceipt() } : {}) } });
272
274
  await this.deps.state.clear();
273
275
  }
274
276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {