lazycodex-ai 5.0.0-beta.49 → 5.0.0-beta.50

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 (108) hide show
  1. package/dist/cli/index.js +4682 -14969
  2. package/dist/cli-node/index.js +4727 -15014
  3. package/package.json +1 -1
  4. package/packages/lsp-daemon/dist/cli.js +181 -181
  5. package/packages/lsp-daemon/dist/client.js +232 -232
  6. package/packages/lsp-daemon/dist/index.js +132 -132
  7. package/packages/lsp-tools-mcp/dist/cli.js +103 -101
  8. package/packages/lsp-tools-mcp/dist/lsp/manager.js +14 -14
  9. package/packages/lsp-tools-mcp/dist/mcp.js +129 -129
  10. package/packages/lsp-tools-mcp/dist/tools.js +47 -47
  11. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  12. package/packages/omo-codex/plugin/README.md +4 -0
  13. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +151 -102
  14. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  15. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  16. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  17. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  19. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  20. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/AGENTS.md +2 -2
  21. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/directive.md +2 -0
  22. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/cli.js +35 -24
  23. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/codex-hook.js +31 -21
  24. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/types.d.ts +3 -0
  25. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  26. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  27. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/src/codex-hook.ts +31 -23
  28. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/src/types.ts +3 -0
  29. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/cli.test.ts +1 -1
  30. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/codex-hook.test.ts +133 -7
  31. package/packages/omo-codex/plugin/components/lcx/skills/lcx-doctor/SKILL.md +3 -1
  32. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  33. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +298 -298
  34. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  35. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  36. package/packages/omo-codex/plugin/components/rules/README.md +2 -0
  37. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-6.md +123 -0
  38. package/packages/omo-codex/plugin/components/rules/dist/cli.js +38 -30
  39. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  40. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  41. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +6 -0
  42. package/packages/omo-codex/plugin/components/rules/test/hephaestus-model-variant.test.ts +38 -2
  43. package/packages/omo-codex/plugin/components/rules/test/post-compact-budget.test.ts +24 -0
  44. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  45. package/packages/omo-codex/plugin/components/teammode/package.json +2 -2
  46. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +66 -357
  47. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +66 -358
  48. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  49. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  50. package/packages/omo-codex/plugin/components/ultrawork/README.md +2 -0
  51. package/packages/omo-codex/plugin/components/ultrawork/agents/explorer.toml +1 -1
  52. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-clone-fidelity-reviewer.toml +1 -1
  53. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-code-reviewer.toml +1 -1
  54. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +1 -1
  55. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-qa-executor.toml +1 -1
  56. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-high.toml +1 -1
  57. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-low.toml +1 -1
  58. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-medium.toml +1 -1
  59. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +1 -1
  60. package/packages/omo-codex/plugin/components/ultrawork/agents/metis.toml +1 -1
  61. package/packages/omo-codex/plugin/components/ultrawork/agents/momus.toml +1 -1
  62. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +1 -1
  63. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  64. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  65. package/packages/omo-codex/plugin/components/ulw-execute-continuation/dist/cli.js +3 -3
  66. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  67. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  68. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +63 -63
  69. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  70. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  71. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  72. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  73. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  74. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  76. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  77. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  78. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  79. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  80. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  84. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  85. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  86. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  87. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  88. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  89. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  90. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  91. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  92. package/packages/omo-codex/plugin/model-catalog.json +16 -7
  93. package/packages/omo-codex/plugin/package-lock.json +16 -16
  94. package/packages/omo-codex/plugin/package.json +1 -1
  95. package/packages/omo-codex/plugin/scripts/migrate-codex-config/catalog.mjs +16 -7
  96. package/packages/omo-codex/plugin/scripts/migrate-codex-config/multi-agent-v2-guard.mjs +8 -4
  97. package/packages/omo-codex/plugin/scripts/migrate-codex-config/subagent-limit-guard.mjs +18 -90
  98. package/packages/omo-codex/plugin/scripts/migrate-codex-config/toml-section-editor.mjs +1 -1
  99. package/packages/omo-codex/plugin/shared/package.json +1 -1
  100. package/packages/omo-codex/plugin/skills/lcx-doctor/SKILL.md +3 -1
  101. package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +12 -12
  102. package/packages/omo-codex/plugin/test/aggregate-model-catalog.test.mjs +14 -4
  103. package/packages/omo-codex/plugin/test/auto-update.test.mjs +4 -4
  104. package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +25 -1
  105. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +35 -35
  106. package/packages/omo-codex/plugin/test/multi-agent-v2-regression.test.mjs +1 -1
  107. package/packages/omo-codex/plugin/test/subagent-limit-migration.test.mjs +107 -15
  108. package/packages/omo-codex/scripts/install-dist/install-local.mjs +318 -569
@@ -74,7 +74,8 @@ function runSubagentStopHook(input, fs) {
74
74
  return "";
75
75
  if (transcriptHasContextPressureMarker(input.transcript_path, fs))
76
76
  return "";
77
- if (hasValidEvidenceReceipt(input, fs)) {
77
+ const receiptFailure = getEvidenceReceiptFailure(input, fs);
78
+ if (receiptFailure === null) {
78
79
  clearAttemptState(input.cwd, input.session_id, input.agent_id, fs);
79
80
  return "";
80
81
  }
@@ -87,7 +88,9 @@ function runSubagentStopHook(input, fs) {
87
88
  writeAttemptState(input.cwd, input.session_id, input.agent_id, { attempts }, fs);
88
89
  return JSON.stringify({
89
90
  decision: "block",
90
- reason: renderDirective(attempts, input.last_assistant_message)
91
+ reason: `Evidence receipt rejected: ${receiptFailure}.
92
+
93
+ ${renderDirective(attempts, input.last_assistant_message)}`
91
94
  });
92
95
  }
93
96
  var CONTEXT_PRESSURE_MARKERS = [
@@ -109,19 +112,19 @@ function transcriptHasContextPressureMarker(transcriptPath, fs) {
109
112
  throw error;
110
113
  }
111
114
  }
112
- function hasValidEvidenceReceipt(input, fs) {
115
+ function getEvidenceReceiptFailure(input, fs) {
113
116
  const receiptPath = extractEvidencePath(input.last_assistant_message);
114
117
  if (receiptPath === null)
115
- return false;
118
+ return "invalid";
116
119
  const evidenceRoot = resolve(input.cwd, ".omo", "evidence");
117
120
  const resolvedPath = isAbsolute(receiptPath) ? resolve(receiptPath) : resolve(input.cwd, receiptPath);
118
121
  if (!isPathInsideDirectory(resolvedPath, evidenceRoot))
119
- return false;
122
+ return "invalid";
120
123
  try {
121
- return isNonEmptyFileInsideEvidenceRoot(resolvedPath, evidenceRoot, input.cwd, fs);
124
+ return getEvidenceFileFailure(resolvedPath, evidenceRoot, input, fs);
122
125
  } catch (error) {
123
126
  if (error instanceof Error)
124
- return false;
127
+ return "invalid";
125
128
  throw error;
126
129
  }
127
130
  }
@@ -129,28 +132,36 @@ function isPathInsideDirectory(filePath, directoryPath) {
129
132
  const relativePath = relative(directoryPath, filePath);
130
133
  return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
131
134
  }
132
- function isNonEmptyFileInsideEvidenceRoot(filePath, evidenceRoot, cwd, fs) {
135
+ function getEvidenceFileFailure(filePath, evidenceRoot, input, fs) {
133
136
  if (!fs.existsSync(filePath))
134
- return false;
135
- const realCwd = realPath(cwd, fs);
137
+ return "invalid";
138
+ const realCwd = realPath(input.cwd, fs);
136
139
  const realEvidenceRoot = realPath(evidenceRoot, fs);
137
140
  const realFilePath = realPath(filePath, fs);
138
141
  if (!isPathInsideDirectory(realEvidenceRoot, realCwd))
139
- return false;
142
+ return "invalid";
140
143
  if (!isPathInsideDirectory(realFilePath, realEvidenceRoot))
141
- return false;
142
- return isNonEmptyFile(filePath, fs);
143
- }
144
- function isNonEmptyFile(filePath, fs) {
145
- if (!fs.existsSync(filePath))
146
- return false;
144
+ return "invalid";
147
145
  const linkStat = fs.lstatSync?.(filePath) ?? nodeLstatSync(filePath);
148
146
  if (linkStat.isSymbolicLink?.() === true)
149
- return false;
147
+ return "invalid";
150
148
  const stat = fs.statSync(filePath);
151
- if (stat.size <= 0)
152
- return false;
153
- return stat.isFile?.() ?? true;
149
+ if (stat.isFile?.() === false)
150
+ return "invalid";
151
+ if (stat.size <= 0 || fs.readFileSync(filePath, "utf8").trim().length < 40)
152
+ return "placeholder";
153
+ let transcriptStat;
154
+ try {
155
+ transcriptStat = fs.statSync(input.transcript_path);
156
+ } catch (error) {
157
+ if (error instanceof Error)
158
+ return null;
159
+ throw error;
160
+ }
161
+ const transcriptStart = transcriptStat.birthtimeMs ?? transcriptStat.ctimeMs;
162
+ if (transcriptStart !== undefined && (stat.mtimeMs === undefined || stat.mtimeMs < transcriptStart))
163
+ return "stale";
164
+ return null;
154
165
  }
155
166
  function realPath(path, fs) {
156
167
  return fs.realpathSync?.(path) ?? nodeRealpathSync(path);
@@ -211,13 +222,13 @@ function parseHookInput(raw) {
211
222
  }
212
223
  }
213
224
  function readStdin() {
214
- return new Promise((resolve2) => {
225
+ return new Promise((resolve) => {
215
226
  let data = "";
216
227
  processStdin.setEncoding("utf8");
217
228
  processStdin.on("data", (chunk) => {
218
229
  data += chunk;
219
230
  });
220
- processStdin.once("error", () => resolve2(data));
221
- processStdin.once("end", () => resolve2(data));
231
+ processStdin.once("error", () => resolve(data));
232
+ processStdin.once("end", () => resolve(data));
222
233
  });
223
234
  }
@@ -11,7 +11,8 @@ export function runSubagentStopHook(input, fs) {
11
11
  return "";
12
12
  if (transcriptHasContextPressureMarker(input.transcript_path, fs))
13
13
  return "";
14
- if (hasValidEvidenceReceipt(input, fs)) {
14
+ const receiptFailure = getEvidenceReceiptFailure(input, fs);
15
+ if (receiptFailure === null) {
15
16
  clearAttemptState(input.cwd, input.session_id, input.agent_id, fs);
16
17
  return "";
17
18
  }
@@ -24,7 +25,7 @@ export function runSubagentStopHook(input, fs) {
24
25
  writeAttemptState(input.cwd, input.session_id, input.agent_id, { attempts }, fs);
25
26
  return JSON.stringify({
26
27
  decision: "block",
27
- reason: renderDirective(attempts, input.last_assistant_message),
28
+ reason: `Evidence receipt rejected: ${receiptFailure}.\n\n${renderDirective(attempts, input.last_assistant_message)}`,
28
29
  });
29
30
  }
30
31
  const CONTEXT_PRESSURE_MARKERS = [
@@ -47,20 +48,20 @@ function transcriptHasContextPressureMarker(transcriptPath, fs) {
47
48
  throw error;
48
49
  }
49
50
  }
50
- function hasValidEvidenceReceipt(input, fs) {
51
+ function getEvidenceReceiptFailure(input, fs) {
51
52
  const receiptPath = extractEvidencePath(input.last_assistant_message);
52
53
  if (receiptPath === null)
53
- return false;
54
+ return "invalid";
54
55
  const evidenceRoot = resolve(input.cwd, ".omo", "evidence");
55
56
  const resolvedPath = isAbsolute(receiptPath) ? resolve(receiptPath) : resolve(input.cwd, receiptPath);
56
57
  if (!isPathInsideDirectory(resolvedPath, evidenceRoot))
57
- return false;
58
+ return "invalid";
58
59
  try {
59
- return isNonEmptyFileInsideEvidenceRoot(resolvedPath, evidenceRoot, input.cwd, fs);
60
+ return getEvidenceFileFailure(resolvedPath, evidenceRoot, input, fs);
60
61
  }
61
62
  catch (error) {
62
63
  if (error instanceof Error)
63
- return false;
64
+ return "invalid";
64
65
  throw error;
65
66
  }
66
67
  }
@@ -68,28 +69,37 @@ function isPathInsideDirectory(filePath, directoryPath) {
68
69
  const relativePath = relative(directoryPath, filePath);
69
70
  return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
70
71
  }
71
- function isNonEmptyFileInsideEvidenceRoot(filePath, evidenceRoot, cwd, fs) {
72
+ function getEvidenceFileFailure(filePath, evidenceRoot, input, fs) {
72
73
  if (!fs.existsSync(filePath))
73
- return false;
74
- const realCwd = realPath(cwd, fs);
74
+ return "invalid";
75
+ const realCwd = realPath(input.cwd, fs);
75
76
  const realEvidenceRoot = realPath(evidenceRoot, fs);
76
77
  const realFilePath = realPath(filePath, fs);
77
78
  if (!isPathInsideDirectory(realEvidenceRoot, realCwd))
78
- return false;
79
+ return "invalid";
79
80
  if (!isPathInsideDirectory(realFilePath, realEvidenceRoot))
80
- return false;
81
- return isNonEmptyFile(filePath, fs);
82
- }
83
- function isNonEmptyFile(filePath, fs) {
84
- if (!fs.existsSync(filePath))
85
- return false;
81
+ return "invalid";
86
82
  const linkStat = fs.lstatSync?.(filePath) ?? nodeLstatSync(filePath);
87
83
  if (linkStat.isSymbolicLink?.() === true)
88
- return false;
84
+ return "invalid";
89
85
  const stat = fs.statSync(filePath);
90
- if (stat.size <= 0)
91
- return false;
92
- return stat.isFile?.() ?? true;
86
+ if (stat.isFile?.() === false)
87
+ return "invalid";
88
+ if (stat.size <= 0 || fs.readFileSync(filePath, "utf8").trim().length < 40)
89
+ return "placeholder";
90
+ let transcriptStat;
91
+ try {
92
+ transcriptStat = fs.statSync(input.transcript_path);
93
+ }
94
+ catch (error) {
95
+ if (error instanceof Error)
96
+ return null;
97
+ throw error;
98
+ }
99
+ const transcriptStart = transcriptStat.birthtimeMs ?? transcriptStat.ctimeMs;
100
+ if (transcriptStart !== undefined && (stat.mtimeMs === undefined || stat.mtimeMs < transcriptStart))
101
+ return "stale";
102
+ return null;
93
103
  }
94
104
  function realPath(path, fs) {
95
105
  return fs.realpathSync?.(path) ?? nodeRealpathSync(path);
@@ -18,6 +18,9 @@ export type StopHookOutput = {
18
18
  };
19
19
  export type FileStat = {
20
20
  readonly size: number;
21
+ readonly mtimeMs?: number;
22
+ readonly birthtimeMs?: number;
23
+ readonly ctimeMs?: number;
21
24
  readonly isFile?: () => boolean;
22
25
  readonly isSymbolicLink?: () => boolean;
23
26
  };
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/components/lazycodex-executor-verify/dist/cli.js\" hook subagent-stop",
10
10
  "timeout": 10,
11
- "statusMessage": "(OmO 5.0.0-beta.49) Verifying LazyCodex Executor Evidence"
11
+ "statusMessage": "(OmO 5.0.0-beta.50) Verifying LazyCodex Executor Evidence"
12
12
  }
13
13
  ]
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-lazycodex-executor-verify",
3
- "version": "5.0.0-beta.49",
3
+ "version": "5.0.0-beta.50",
4
4
  "description": "Codex SubagentStop evidence verifier for LazyCodex executor completions.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -3,16 +3,19 @@ import { isAbsolute, relative, resolve } from "node:path";
3
3
 
4
4
  import { renderDirective } from "./directive.js";
5
5
  import { clearAttemptState, MAX_ATTEMPTS, readAttemptState, writeAttemptState } from "./state.js";
6
- import type { HookFileSystem, StopHookOutput, SubagentStopInput } from "./types.js";
6
+ import type { FileStat, HookFileSystem, StopHookOutput, SubagentStopInput } from "./types.js";
7
7
  import { SUBAGENT_STOP_EVENT } from "./types.js";
8
8
 
9
9
  const RECEIPT_ENFORCED_AGENTS = new Set(["lazycodex-worker-low", "lazycodex-worker-medium", "lazycodex-worker-high"]);
10
10
 
11
+ type EvidenceReceiptFailure = "invalid" | "placeholder" | "stale" | null;
12
+
11
13
  export function runSubagentStopHook(input: unknown, fs: HookFileSystem): string {
12
14
  if (!isSubagentStopInput(input)) return "";
13
15
  if (!RECEIPT_ENFORCED_AGENTS.has(input.agent_type)) return "";
14
16
  if (transcriptHasContextPressureMarker(input.transcript_path, fs)) return "";
15
- if (hasValidEvidenceReceipt(input, fs)) {
17
+ const receiptFailure = getEvidenceReceiptFailure(input, fs);
18
+ if (receiptFailure === null) {
16
19
  clearAttemptState(input.cwd, input.session_id, input.agent_id, fs);
17
20
  return "";
18
21
  }
@@ -25,7 +28,7 @@ export function runSubagentStopHook(input: unknown, fs: HookFileSystem): string
25
28
  writeAttemptState(input.cwd, input.session_id, input.agent_id, { attempts }, fs);
26
29
  return JSON.stringify({
27
30
  decision: "block",
28
- reason: renderDirective(attempts, input.last_assistant_message),
31
+ reason: `Evidence receipt rejected: ${receiptFailure}.\n\n${renderDirective(attempts, input.last_assistant_message)}`,
29
32
  } satisfies StopHookOutput);
30
33
  }
31
34
 
@@ -49,16 +52,16 @@ function transcriptHasContextPressureMarker(transcriptPath: string, fs: HookFile
49
52
  }
50
53
  }
51
54
 
52
- function hasValidEvidenceReceipt(input: SubagentStopInput, fs: HookFileSystem): boolean {
55
+ function getEvidenceReceiptFailure(input: SubagentStopInput, fs: HookFileSystem): EvidenceReceiptFailure {
53
56
  const receiptPath = extractEvidencePath(input.last_assistant_message);
54
- if (receiptPath === null) return false;
57
+ if (receiptPath === null) return "invalid";
55
58
  const evidenceRoot = resolve(input.cwd, ".omo", "evidence");
56
59
  const resolvedPath = isAbsolute(receiptPath) ? resolve(receiptPath) : resolve(input.cwd, receiptPath);
57
- if (!isPathInsideDirectory(resolvedPath, evidenceRoot)) return false;
60
+ if (!isPathInsideDirectory(resolvedPath, evidenceRoot)) return "invalid";
58
61
  try {
59
- return isNonEmptyFileInsideEvidenceRoot(resolvedPath, evidenceRoot, input.cwd, fs);
62
+ return getEvidenceFileFailure(resolvedPath, evidenceRoot, input, fs);
60
63
  } catch (error) {
61
- if (error instanceof Error) return false;
64
+ if (error instanceof Error) return "invalid";
62
65
  throw error;
63
66
  }
64
67
  }
@@ -68,28 +71,33 @@ function isPathInsideDirectory(filePath: string, directoryPath: string): boolean
68
71
  return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
69
72
  }
70
73
 
71
- function isNonEmptyFileInsideEvidenceRoot(
74
+ function getEvidenceFileFailure(
72
75
  filePath: string,
73
76
  evidenceRoot: string,
74
- cwd: string,
77
+ input: SubagentStopInput,
75
78
  fs: HookFileSystem,
76
- ): boolean {
77
- if (!fs.existsSync(filePath)) return false;
78
- const realCwd = realPath(cwd, fs);
79
+ ): EvidenceReceiptFailure {
80
+ if (!fs.existsSync(filePath)) return "invalid";
81
+ const realCwd = realPath(input.cwd, fs);
79
82
  const realEvidenceRoot = realPath(evidenceRoot, fs);
80
83
  const realFilePath = realPath(filePath, fs);
81
- if (!isPathInsideDirectory(realEvidenceRoot, realCwd)) return false;
82
- if (!isPathInsideDirectory(realFilePath, realEvidenceRoot)) return false;
83
- return isNonEmptyFile(filePath, fs);
84
- }
85
-
86
- function isNonEmptyFile(filePath: string, fs: HookFileSystem): boolean {
87
- if (!fs.existsSync(filePath)) return false;
84
+ if (!isPathInsideDirectory(realEvidenceRoot, realCwd)) return "invalid";
85
+ if (!isPathInsideDirectory(realFilePath, realEvidenceRoot)) return "invalid";
88
86
  const linkStat = fs.lstatSync?.(filePath) ?? nodeLstatSync(filePath);
89
- if (linkStat.isSymbolicLink?.() === true) return false;
87
+ if (linkStat.isSymbolicLink?.() === true) return "invalid";
90
88
  const stat = fs.statSync(filePath);
91
- if (stat.size <= 0) return false;
92
- return stat.isFile?.() ?? true;
89
+ if (stat.isFile?.() === false) return "invalid";
90
+ if (stat.size <= 0 || fs.readFileSync(filePath, "utf8").trim().length < 40) return "placeholder";
91
+ let transcriptStat: FileStat;
92
+ try {
93
+ transcriptStat = fs.statSync(input.transcript_path);
94
+ } catch (error) {
95
+ if (error instanceof Error) return null;
96
+ throw error;
97
+ }
98
+ const transcriptStart = transcriptStat.birthtimeMs ?? transcriptStat.ctimeMs;
99
+ if (transcriptStart !== undefined && (stat.mtimeMs === undefined || stat.mtimeMs < transcriptStart)) return "stale";
100
+ return null;
93
101
  }
94
102
 
95
103
  function realPath(path: string, fs: HookFileSystem): string {
@@ -21,6 +21,9 @@ export type StopHookOutput = {
21
21
 
22
22
  export type FileStat = {
23
23
  readonly size: number;
24
+ readonly mtimeMs?: number;
25
+ readonly birthtimeMs?: number;
26
+ readonly ctimeMs?: number;
24
27
  readonly isFile?: () => boolean;
25
28
  readonly isSymbolicLink?: () => boolean;
26
29
  };
@@ -59,7 +59,7 @@ describe("lazycodex executor verify CLI", () => {
59
59
  const cwd = createWorkspace();
60
60
  const artifactPath = join(cwd, ".omo", "evidence", "receipt.txt");
61
61
  mkdirSync(join(cwd, ".omo", "evidence"), { recursive: true });
62
- writeFileSync(artifactPath, "verified\n");
62
+ writeFileSync(artifactPath, "Command: verify\nExit code: 0\nOutput: all checks passed.\n");
63
63
  const payload = JSON.stringify(
64
64
  createPayload(cwd, { last_assistant_message: `EVIDENCE_RECORDED: ${artifactPath}` }),
65
65
  );
@@ -12,12 +12,12 @@ import {
12
12
  writeFileSync,
13
13
  } from "node:fs";
14
14
  import { tmpdir } from "node:os";
15
- import { join } from "node:path";
15
+ import { join, resolve } from "node:path";
16
16
  import { platform } from "node:process";
17
17
  import { afterEach, describe, expect, it } from "vitest";
18
18
 
19
19
  import { runSubagentStopHook } from "../src/codex-hook.js";
20
- import type { SubagentStopInput } from "../src/types.js";
20
+ import type { FileStat, HookFileSystem, SubagentStopInput } from "../src/types.js";
21
21
 
22
22
  const cleanupRoots: string[] = [];
23
23
 
@@ -71,7 +71,7 @@ describe("lazycodex executor SubagentStop verifier", () => {
71
71
  runSubagentStopHook(createInput(cwd), nodeFileSystem);
72
72
  const artifactPath = join(cwd, ".omo", "evidence", "receipt.txt");
73
73
  mkdirSync(join(cwd, ".omo", "evidence"), { recursive: true });
74
- writeFileSync(artifactPath, "verified\n");
74
+ writeFileSync(artifactPath, "Command: verify\nExit code: 0\nOutput: all checks passed.\n");
75
75
 
76
76
  // when
77
77
  const output = runSubagentStopHook(
@@ -140,7 +140,7 @@ describe("lazycodex executor SubagentStop verifier", () => {
140
140
  const outsideReceipt = join(outsideRoot, "receipt.txt");
141
141
  const linkPath = join(cwd, ".omo", "evidence", "outside-dir");
142
142
  mkdirSync(join(cwd, ".omo", "evidence"), { recursive: true });
143
- writeFileSync(outsideReceipt, "outside\n");
143
+ writeFileSync(outsideReceipt, "outside evidence root\n".repeat(3));
144
144
  symlinkSync(outsideRoot, linkPath, platform === "win32" ? "junction" : "dir");
145
145
 
146
146
  // when
@@ -186,7 +186,7 @@ describe("lazycodex executor SubagentStop verifier", () => {
186
186
  // given
187
187
  const cwd = createWorkspace();
188
188
  mkdirSync(join(cwd, ".omo"), { recursive: true });
189
- writeFileSync(join(cwd, ".omo", "outside.txt"), "outside\n");
189
+ writeFileSync(join(cwd, ".omo", "outside.txt"), "outside evidence root\n".repeat(3));
190
190
 
191
191
  // when
192
192
  const output = runSubagentStopHook(
@@ -251,6 +251,132 @@ describe("lazycodex executor SubagentStop verifier", () => {
251
251
  });
252
252
  });
253
253
 
254
+ describe("receipt content and freshness", () => {
255
+ it.each(["placeholder evidence\n", "x".repeat(39), ` ${"x".repeat(39)}\n`])(
256
+ "#given a short trimmed receipt %j #when the worker stops #then blocks as placeholder",
257
+ (content) => {
258
+ // given
259
+ const { input, fs } = createMemoryReceipt(content, { mtimeMs: 2_000 }, { birthtimeMs: 1_000 });
260
+
261
+ // when
262
+ const output = runSubagentStopHook(input, fs);
263
+
264
+ // then
265
+ expect(parseBlockOutput(output).reason).toContain("placeholder");
266
+ },
267
+ );
268
+
269
+ it("#given a 200-character receipt older than transcript birth #when the worker stops #then blocks as stale", () => {
270
+ // given
271
+ const { input, fs } = createMemoryReceipt("x".repeat(200), { mtimeMs: 999 }, { birthtimeMs: 1_000 });
272
+
273
+ // when
274
+ const output = runSubagentStopHook(input, fs);
275
+
276
+ // then
277
+ expect(parseBlockOutput(output).reason).toContain("stale");
278
+ });
279
+
280
+ it("#given a fresh 200-character receipt #when transcript birth precedes ctime #then passes using birthtime", () => {
281
+ // given
282
+ const { input, fs } = createMemoryReceipt(
283
+ "x".repeat(200),
284
+ { mtimeMs: 1_001 },
285
+ { birthtimeMs: 1_000, ctimeMs: 3_000 },
286
+ );
287
+
288
+ // when
289
+ const output = runSubagentStopHook(input, fs);
290
+
291
+ // then
292
+ expect(output).toBe("");
293
+ });
294
+
295
+ it("#given a 200-character receipt and unstatable transcript #when the worker stops #then skips freshness", () => {
296
+ // given
297
+ const { input, fs } = createMemoryReceipt("x".repeat(200), { mtimeMs: 0 });
298
+
299
+ // when
300
+ const output = runSubagentStopHook({ ...input, transcript_path: "/nonexistent" }, fs);
301
+
302
+ // then
303
+ expect(output).toBe("");
304
+ });
305
+
306
+ it("#given 40 trimmed characters with mtime equal to transcript birth #when the worker stops #then passes", () => {
307
+ // given
308
+ const { input, fs } = createMemoryReceipt(` ${"x".repeat(40)}\n`, { mtimeMs: 1_000 }, { birthtimeMs: 1_000 });
309
+
310
+ // when
311
+ const output = runSubagentStopHook(input, fs);
312
+
313
+ // then
314
+ expect(output).toBe("");
315
+ });
316
+
317
+ it.each([999, 1_000])(
318
+ "#given no transcript birthtime and receipt mtime %i #when the worker stops #then uses ctime",
319
+ (mtimeMs) => {
320
+ // given
321
+ const { input, fs } = createMemoryReceipt("x".repeat(200), { mtimeMs }, { ctimeMs: 1_000 });
322
+
323
+ // when
324
+ const output = runSubagentStopHook(input, fs);
325
+
326
+ // then
327
+ if (mtimeMs < 1_000) expect(parseBlockOutput(output).reason).toContain("stale");
328
+ else expect(output).toBe("");
329
+ },
330
+ );
331
+ });
332
+
333
+ function createMemoryReceipt(
334
+ content: string,
335
+ receiptStat: Partial<FileStat>,
336
+ transcriptStat?: Partial<FileStat>,
337
+ ): { readonly input: SubagentStopInput; readonly fs: HookFileSystem } {
338
+ const cwd = resolve("receipt-workspace");
339
+ const transcriptPath = join(cwd, "transcript.jsonl");
340
+ const files = new Map<string, { readonly content: string; readonly stat: Partial<FileStat> }>([
341
+ [join(cwd, ".omo", "evidence", "receipt.txt"), { content, stat: receiptStat }],
342
+ ]);
343
+ if (transcriptStat !== undefined) files.set(transcriptPath, { content: "", stat: transcriptStat });
344
+ const fileAt = (path: string) => {
345
+ const file = files.get(path);
346
+ if (file === undefined) throw new Error(`ENOENT: ${path}`);
347
+ return file;
348
+ };
349
+ const statAt = (path: string): FileStat => {
350
+ const file = fileAt(path);
351
+ return { size: file.content.length, isFile: () => true, isSymbolicLink: () => false, ...file.stat };
352
+ };
353
+ const fs: HookFileSystem = {
354
+ existsSync: (path) => files.has(path),
355
+ lstatSync: statAt,
356
+ mkdirSync: () => undefined,
357
+ readFileSync: (path) => fileAt(path).content,
358
+ realpathSync: (path) => resolve(path),
359
+ renameSync: (oldPath, newPath) => {
360
+ files.set(newPath, fileAt(oldPath));
361
+ files.delete(oldPath);
362
+ },
363
+ rmSync: (path) => {
364
+ files.delete(path);
365
+ },
366
+ statSync: statAt,
367
+ writeFileSync: (path, data) => {
368
+ files.set(path, { content: data, stat: {} });
369
+ },
370
+ };
371
+ return {
372
+ input: createInput(cwd, {
373
+ transcript_path: transcriptPath,
374
+ last_assistant_message: "done\nEVIDENCE_RECORDED: .omo/evidence/receipt.txt",
375
+ }),
376
+ fs,
377
+ };
378
+ }
379
+
254
380
  type BlockOutput = {
255
381
  readonly decision: "block";
256
382
  readonly reason: string;
@@ -278,7 +404,7 @@ function createWorkspaceWithParentOutsideReceipt(): { readonly cwd: string } {
278
404
  const root = createWorkspace();
279
405
  const cwd = join(root, "project");
280
406
  mkdirSync(cwd, { recursive: true });
281
- writeFileSync(join(root, "outside.txt"), "outside\n");
407
+ writeFileSync(join(root, "outside.txt"), "outside evidence root\n".repeat(3));
282
408
  return { cwd };
283
409
  }
284
410
 
@@ -291,7 +417,7 @@ function existingReceiptTargetOutsideEvidenceRoot(): string {
291
417
  if (existsSync("/etc/passwd") && statSync("/etc/passwd").size > 0) return "/etc/passwd";
292
418
  const root = createWorkspace();
293
419
  const receiptPath = join(root, "outside.txt");
294
- writeFileSync(receiptPath, "outside\n");
420
+ writeFileSync(receiptPath, "outside evidence root\n".repeat(3));
295
421
  return receiptPath;
296
422
  }
297
423
 
@@ -66,6 +66,7 @@ sync_latest_source openai/codex "$LAZYCODEX_SOURCE_ROOT/openai-codex-source"
66
66
  - Installed LazyCodex version: the `version` in the installed plugin manifest, discoverable with `find "${CODEX_HOME:-$HOME/.codex}/plugins" -path '*/.codex-plugin/plugin.json'`. Installed plugins live under `$CODEX_HOME/plugins/cache/<marketplace>/<name>/<version>/`.
67
67
  - Latest LazyCodex version from `$LAZYCODEX_SOURCE_ROOT/lazycodex-source` (release tags or the version stamped in the repo) and latest Codex release (`gh release view --repo openai/codex`).
68
68
  - OS, install method, and `lazycodex` / `lazycodex-ai` bin links resolving (`command -v`).
69
+ - Astra readiness. The LazyCodex catalog default is `gpt-6-astra`, which Codex only knows from codex-cli 0.153.1 onward. First release tag containing the `models.json` change: `rust-v0.153.1` (backport commit `5cc1c94b8e`). Reproduce on a full clone with tags: `git -C <codex-clone> log --format='%h %d' -S gpt-6-astra -- codex-rs/models-manager/models.json`, then `git -C <codex-clone> tag --contains 5cc1c94b8e | sort -V | head -1`; a shallow clone or one without tags returns nothing. Record the installed version from `codex --version`, the root `model` in `$CODEX_HOME/config.toml` (informational only, it doesn't change the verdict), and, when `$CODEX_HOME/models_cache.json` exists, whether it contains `gpt-6-astra` (`grep -c gpt-6-astra "${CODEX_HOME:-$HOME/.codex}/models_cache.json"`). Verdict: PASS when `codex --version` is 0.153.1 or newer and `models_cache.json` is either absent or lists `gpt-6-astra`. WARN when the version is older than 0.153.1, or the cache exists but lacks `gpt-6-astra`; remediation is upgrading Codex (`npm install -g @openai/codex@latest`, or the package manager that installed `codex`), then starting Codex once so `models_cache.json` refreshes.
69
70
  3. Check config and wiring against the latest installer, not against assumptions. Read what the current installer under `$LAZYCODEX_SOURCE_ROOT/lazycodex-source` writes (installer sources live in the omo-codex package, e.g. `scripts/install/`), then verify the local equivalents:
70
71
  - `$CODEX_HOME/config.toml` exists and parses; LazyCodex-managed entries match what the latest installer would write.
71
72
  - Plugin payload present and non-empty: read `.codex-plugin/plugin.json`; when that manifest declares a `hooks` array, validate every direct hook path declared by the manifest; require `hooks/hooks.json` only when the manifest declares it; do not require retired paths such as `components/workflow-selector` or `hooks/user-prompt-submit-selecting-lazycodex-workflow.json` unless the current manifest declares them.
@@ -96,6 +97,7 @@ sync_latest_source openai/codex "$LAZYCODEX_SOURCE_ROOT/openai-codex-source"
96
97
  | Check | Verdict | Evidence |
97
98
  | --- | --- | --- |
98
99
  | Versions current | PASS/WARN/FAIL | [command output or file:line] |
100
+ | Astra readiness | PASS/WARN | [`codex --version` vs the 0.153.1 floor; `gpt-6-astra` present/absent in `$CODEX_HOME/models_cache.json`; configured root `model`] |
99
101
  | config.toml integrity | PASS/WARN/FAIL | [evidence] |
100
102
  | Plugin payload wiring | PASS/WARN/FAIL | [evidence] |
101
103
  | Bin links / aliases | PASS/WARN/FAIL | [evidence] |
@@ -103,7 +105,7 @@ sync_latest_source openai/codex "$LAZYCODEX_SOURCE_ROOT/openai-codex-source"
103
105
  | Drift vs latest source | PASS/WARN/FAIL | [evidence, citing `$LAZYCODEX_SOURCE_ROOT/lazycodex-source` or `$LAZYCODEX_SOURCE_ROOT/openai-codex-source` paths] |
104
106
 
105
107
  ### Remediations
106
- 1. [Most important fix first: exact command or config edit, and what it resolves.]
108
+ 1. [Most important fix first: exact command or config edit, and what it resolves. When Astra readiness is WARN: upgrade Codex to 0.153.1 or newer and relaunch it once so `models_cache.json` picks up `gpt-6-astra`.]
107
109
 
108
110
  ### Known Issues Matched
109
111
  - [issue URL — or "none found"]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "5.0.0-beta.49",
4
- "inputDigest": "sha256:2558d595e0ff9f7b1fe251d3ce11a05dadf64b039021b5a34d5469762671b6de",
3
+ "version": "5.0.0-beta.50",
4
+ "inputDigest": "sha256:27672c2cd34eb3b44fc226b7e4e9137af0d0acedbb729fe92cdd663602ae6846",
5
5
  "outputs": [
6
6
  {
7
7
  "path": "cli.d.ts",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  {
11
11
  "path": "cli.js",
12
- "sha256": "bddbb2f82db0dffb0c2ec7b69778a63e77cac2c6a0f8bfb6d7a62980a49d14bb"
12
+ "sha256": "54d3621c04697495054be69fd6328d7b9044034801b295868b23bd92d344a816"
13
13
  },
14
14
  {
15
15
  "path": "codex-hook-cli.d.ts",