@kodrunhq/opencode-autopilot 1.14.1 → 1.15.1

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.
@@ -1,7 +1,10 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
1
3
  import type { Config } from "@opencode-ai/plugin";
2
4
  import { tool } from "@opencode-ai/plugin";
3
5
  import { runHealthChecks } from "../health/runner";
4
6
  import type { HealthResult } from "../health/types";
7
+ import { getProjectArtifactDir } from "../utils/paths";
5
8
 
6
9
  /**
7
10
  * A single check in the doctor report, with an optional fix suggestion.
@@ -13,6 +16,35 @@ interface DoctorCheck {
13
16
  readonly fixSuggestion: string | null;
14
17
  }
15
18
 
19
+ interface ContractHealth {
20
+ readonly legacyTasksFallbackSeen: boolean;
21
+ readonly legacyResultParserSeen: boolean;
22
+ }
23
+
24
+ async function detectContractHealth(projectRoot?: string): Promise<ContractHealth> {
25
+ if (!projectRoot) {
26
+ return {
27
+ legacyTasksFallbackSeen: false,
28
+ legacyResultParserSeen: false,
29
+ };
30
+ }
31
+
32
+ try {
33
+ const artifactDir = getProjectArtifactDir(projectRoot);
34
+ const logPath = join(artifactDir, "orchestration.jsonl");
35
+ const content = await readFile(logPath, "utf-8");
36
+ return {
37
+ legacyTasksFallbackSeen: content.includes("PLAN fallback: parsed legacy tasks.md"),
38
+ legacyResultParserSeen: content.includes("Legacy result parser path used"),
39
+ };
40
+ } catch {
41
+ return {
42
+ legacyTasksFallbackSeen: false,
43
+ legacyResultParserSeen: false,
44
+ };
45
+ }
46
+ }
47
+
16
48
  /**
17
49
  * Options for doctorCore — all optional for testability.
18
50
  */
@@ -92,6 +124,7 @@ export async function doctorCore(options?: DoctorOptions): Promise<string> {
92
124
  });
93
125
 
94
126
  const allChecks = [...healthChecks, hookCheck];
127
+ const contractHealth = await detectContractHealth(options?.projectRoot);
95
128
  const allPassed = report.allPassed && hookCheck.status === "pass";
96
129
  const displayText = buildDisplayText(allChecks, report.duration);
97
130
 
@@ -99,6 +132,7 @@ export async function doctorCore(options?: DoctorOptions): Promise<string> {
99
132
  action: "doctor",
100
133
  checks: allChecks,
101
134
  allPassed,
135
+ contractHealth,
102
136
  displayText,
103
137
  duration: report.duration,
104
138
  });
@@ -1,3 +1,5 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
1
3
  import { tool } from "@opencode-ai/plugin";
2
4
  import { loadState } from "../orchestrator/state";
3
5
  import { getProjectArtifactDir } from "../utils/paths";
@@ -21,6 +23,36 @@ function getSuggestedAction(failedPhase: string, recoverable: boolean): "resume"
21
23
  return "resume";
22
24
  }
23
25
 
26
+ async function readRecentContractEvents(artifactDir: string): Promise<readonly string[]> {
27
+ try {
28
+ const raw = await readFile(join(artifactDir, "orchestration.jsonl"), "utf-8");
29
+ const lines = raw
30
+ .split("\n")
31
+ .map((line) => line.trim())
32
+ .filter(Boolean)
33
+ .slice(-120);
34
+ const codes = new Set<string>();
35
+ for (const line of lines) {
36
+ for (const code of [
37
+ "E_INVALID_RESULT",
38
+ "E_STALE_RESULT",
39
+ "E_PHASE_MISMATCH",
40
+ "E_UNKNOWN_DISPATCH",
41
+ "E_DUPLICATE_RESULT",
42
+ "E_BUILD_TASK_ID_REQUIRED",
43
+ "E_BUILD_UNKNOWN_TASK",
44
+ ]) {
45
+ if (line.includes(code)) {
46
+ codes.add(code);
47
+ }
48
+ }
49
+ }
50
+ return Object.freeze([...codes].sort());
51
+ } catch {
52
+ return Object.freeze([]);
53
+ }
54
+ }
55
+
24
56
  export async function forensicsCore(
25
57
  _args: Record<string, never>,
26
58
  projectRoot: string,
@@ -66,6 +98,7 @@ export async function forensicsCore(
66
98
  const recoverable = isRecoverable(failureContext.failedPhase);
67
99
  const suggestedAction = getSuggestedAction(failureContext.failedPhase, recoverable);
68
100
  const phasesCompleted = state.phases.filter((p) => p.status === "DONE").map((p) => p.name);
101
+ const deterministicErrorCodes = await readRecentContractEvents(artifactDir);
69
102
 
70
103
  return JSON.stringify({
71
104
  failedPhase: failureContext.failedPhase,
@@ -75,6 +108,7 @@ export async function forensicsCore(
75
108
  recoverable,
76
109
  suggestedAction,
77
110
  phasesCompleted,
111
+ deterministicErrorCodes,
78
112
  });
79
113
  }
80
114