oh-my-opencode 5.0.0-beta.67 → 5.0.0-beta.68

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 (74) hide show
  1. package/dist/cli/index.js +14 -14
  2. package/dist/cli-node/index.js +14 -14
  3. package/dist/index.js +13 -13
  4. package/package.json +13 -13
  5. package/packages/lsp-core/src/lsp/client-diagnostics-concurrency.integration.test.ts +9 -1
  6. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +43 -8
  7. package/packages/lsp-core/src/lsp/controlled-clock-test-support.ts +29 -0
  8. package/packages/lsp-core/src/lsp/workspace-document-state.ts +5 -0
  9. package/packages/lsp-daemon/dist/cli.js +2 -0
  10. package/packages/lsp-daemon/dist/client.js +2 -0
  11. package/packages/lsp-daemon/dist/index.js +2 -0
  12. package/packages/lsp-tools-mcp/dist/cli.js +2 -0
  13. package/packages/lsp-tools-mcp/dist/lsp/manager.js +2 -0
  14. package/packages/lsp-tools-mcp/dist/mcp.js +2 -0
  15. package/packages/lsp-tools-mcp/dist/tools.js +2 -0
  16. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  17. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  18. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  19. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  20. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  21. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  22. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  23. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  24. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  25. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  26. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +2 -0
  27. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  28. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  29. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  30. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  31. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  32. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  33. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  34. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  35. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  36. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  37. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  38. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  39. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +1 -0
  40. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +26 -2
  41. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +6 -1
  42. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +2 -1
  43. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +2 -1
  44. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +30 -1
  45. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  46. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +6 -1
  48. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +9 -1
  49. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +30 -2
  50. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-legacy-projection.test.ts +228 -0
  51. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  52. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  53. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  54. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  55. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  56. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  57. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  58. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  59. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  60. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  61. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  62. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  63. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  68. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  69. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  70. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  71. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  72. package/packages/omo-codex/plugin/package-lock.json +12 -12
  73. package/packages/omo-codex/plugin/package.json +1 -1
  74. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-lsp",
3
- "version": "5.0.0-beta.67",
3
+ "version": "5.0.0-beta.68",
4
4
  "description": "Codex plugin that exposes Language Server Protocol tools and post-edit diagnostics.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook session-start",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.67) Loading Project Rules"
10
+ "statusMessage": "(OmO 5.0.0-beta.68) Loading Project Rules"
11
11
  }
12
12
  ]
13
13
  }
@@ -19,7 +19,7 @@
19
19
  "type": "command",
20
20
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit",
21
21
  "timeout": 10,
22
- "statusMessage": "(OmO 5.0.0-beta.67) Loading Project Rules"
22
+ "statusMessage": "(OmO 5.0.0-beta.68) Loading Project Rules"
23
23
  }
24
24
  ]
25
25
  }
@@ -32,7 +32,7 @@
32
32
  "type": "command",
33
33
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use",
34
34
  "timeout": 10,
35
- "statusMessage": "(OmO 5.0.0-beta.67) Matching Project Rules"
35
+ "statusMessage": "(OmO 5.0.0-beta.68) Matching Project Rules"
36
36
  }
37
37
  ]
38
38
  }
@@ -45,7 +45,7 @@
45
45
  "type": "command",
46
46
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-compact",
47
47
  "timeout": 10,
48
- "statusMessage": "(OmO 5.0.0-beta.67) Resetting Project Rule Cache"
48
+ "statusMessage": "(OmO 5.0.0-beta.68) Resetting Project Rule Cache"
49
49
  }
50
50
  ]
51
51
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-rules",
3
- "version": "5.0.0-beta.67",
3
+ "version": "5.0.0-beta.68",
4
4
  "description": "Codex plugin that injects project rule files into model context through lifecycle hooks.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use",
10
10
  "timeout": 10,
11
- "statusMessage": "(OmO 5.0.0-beta.67) Checking Thread Title Hygiene"
11
+ "statusMessage": "(OmO 5.0.0-beta.68) Checking Thread Title Hygiene"
12
12
  }
13
13
  ]
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisyphuslabs/codex-teammode",
3
- "version": "5.0.0-beta.67",
3
+ "version": "5.0.0-beta.68",
4
4
  "description": "Codex team-mode hook component that keeps background thread titles descriptive after create_thread.",
5
5
  "type": "module",
6
6
  "private": true,
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook session-start",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.67) Recording Session Telemetry"
10
+ "statusMessage": "(OmO 5.0.0-beta.68) Recording Session Telemetry"
11
11
  }
12
12
  ]
13
13
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-telemetry",
3
- "version": "5.0.0-beta.67",
3
+ "version": "5.0.0-beta.68",
4
4
  "description": "Codex plugin component that emits omo-codex anonymous daily-active telemetry on SessionStart.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.67) Checking Ultrawork Trigger"
10
+ "statusMessage": "(OmO 5.0.0-beta.68) Checking Ultrawork Trigger"
11
11
  }
12
12
  ]
13
13
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-ultrawork",
3
- "version": "5.0.0-beta.67",
3
+ "version": "5.0.0-beta.68",
4
4
  "description": "Codex plugin that injects the ultrawork orchestration directive and ships LazyCodex planning, review, QA, and gate agent roles.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/ulw-execute-continuation/dist/cli.js\" hook stop",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.67) Checking Ulw-Execute Continuation"
10
+ "statusMessage": "(OmO 5.0.0-beta.68) Checking Ulw-Execute Continuation"
11
11
  }
12
12
  ]
13
13
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-ulw-execute-continuation",
3
- "version": "5.0.0-beta.67",
3
+ "version": "5.0.0-beta.68",
4
4
  "description": "Codex Stop hook continuation injector for omo-codex ulw-execute plans.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -2,6 +2,7 @@
2
2
 
3
3
  ## [0.1.0] - unreleased
4
4
 
5
+ - **Fix:** a `goals.json` written outside the commit log (the removed `omo_agent_toolkit` tool path rewrote it wholesale, never stamping `revision`) no longer loses to an older `revisions/` snapshot when it holds goals that snapshot lacks; the cache wins, is stamped with the snapshot's revision, and the next publish folds it into revision N+1 instead of colliding on an existing record. Raw `ledger.jsonl` lines appended after a published revision are attributed to that revision rather than to the pre-store past a later `ledgerResetRevision` discards. Locked reads and every commit now refuse to rewrite `goals.json` while the reconciled ledger names a `goal_added` goal the projection lacks (`ULW_LOOP_PROJECTION_TRUNCATED`, `details.missingGoalIds`) instead of silently truncating the plan (code-yeongyu/oh-my-openagent#8328).
5
6
  - Status nextActions and missing-plan recovery now follow the calling surface: omo-senpi teaches the eval SDK methods and current attempt directory without asking for a driver snapshot; LazyCodex retains its existing CLI guidance byte-for-byte.
6
7
 
7
8
  - Async `.state.lock` holders now keep their file descriptor open and refresh a 30-second lease through it. An expired lease can be reclaimed even while its process remains alive; sync hooks and older lease-less records retain dead-pid-only ownership. Windows filesystems that refuse unlinking an open lock fail closed. Interrupted kernels can recover after lease expiry without deleting a live owner's lock.
@@ -456,7 +456,28 @@ function reconcilePlan(dir) {
456
456
  }
457
457
  }
458
458
  const latest = readNewestRecord(dir);
459
- return latest !== undefined && latest.revision >= (cached?.revision ?? 0) ? latest.plan : cached;
459
+ if (latest === undefined)
460
+ return cached;
461
+ if (cached === undefined)
462
+ return latest.plan;
463
+ if ((cached.revision ?? 0) > latest.revision)
464
+ return cached;
465
+ if (cached.revision === undefined || cached.revision === latest.revision) {
466
+ const published = new Set(latest.plan.goals.map((goal) => goal.id));
467
+ if (Array.isArray(cached.goals) && cached.goals.some((goal) => !published.has(goal.id)))
468
+ return { ...cached, revision: latest.revision };
469
+ }
470
+ return latest.plan;
471
+ }
472
+ function assertProjectionComplete(plan, ledger) {
473
+ const present = new Set(plan.goals.map((goal) => goal.id));
474
+ const missing = new Set;
475
+ for (const entry of ledger)
476
+ if (entry.kind === "goal_added" && entry.goalId !== undefined && !present.has(entry.goalId))
477
+ missing.add(entry.goalId);
478
+ if (missing.size === 0)
479
+ return;
480
+ throw new UlwLoopError(`Refusing to rewrite goals.json: the ledger records goals the plan projection lacks (${[...missing].join(", ")}). Restore those goals into the newest revisions/ record (keeping plan.revision equal to its file number) before mutating this run.`, "ULW_LOOP_PROJECTION_TRUNCATED", { details: { missingGoalIds: [...missing] } });
460
481
  }
461
482
  function planExists(repoRoot, scope) {
462
483
  const dir = ulwLoopDir(repoRoot, scope);
@@ -467,12 +488,14 @@ function planExists(repoRoot, scope) {
467
488
  function readLedgerAt(dir) {
468
489
  const entries = new Map;
469
490
  const lines = (readOptional(join3(dir, "ledger.jsonl")) ?? "").split(/\r?\n/);
491
+ let reached = 0;
470
492
  for (const [index, line] of lines.entries()) {
471
493
  if (line.trim().length === 0)
472
494
  continue;
473
495
  try {
474
496
  const entry = JSON.parse(line);
475
- entry.revision ??= 0;
497
+ entry.revision = entry.revision || reached;
498
+ reached = Math.max(reached, entry.revision);
476
499
  entry.id ??= `legacy-${index + 1}`;
477
500
  entries.set(entry.id, entry);
478
501
  } catch (error) {
@@ -959,6 +982,7 @@ function viewContents(dir) {
959
982
  if (plan === undefined || readRecords(dir).length === 0)
960
983
  return [];
961
984
  const ledger = readLedgerAt(dir);
985
+ assertProjectionComplete(plan, ledger);
962
986
  const views = [
963
987
  ["goals.json", `${JSON.stringify(plan, null, 2)}
964
988
  `],
@@ -4,12 +4,17 @@ import { readOptional, readRecords, reconcilePlan } from "./plan-log.js";
4
4
  export function readLedgerAt(dir) {
5
5
  const entries = new Map();
6
6
  const lines = (readOptional(join(dir, "ledger.jsonl")) ?? "").split(/\r?\n/);
7
+ // A materialized view is sorted by revision, so a line that carries no revision (or revision 0)
8
+ // AFTER a published revision was appended out-of-band (the removed tool path) and belongs to the
9
+ // newest revision before it - not to the pre-store past a later ledgerResetRevision would discard.
10
+ let reached = 0;
7
11
  for (const [index, line] of lines.entries()) {
8
12
  if (line.trim().length === 0)
9
13
  continue;
10
14
  try {
11
15
  const entry = JSON.parse(line);
12
- entry.revision ??= 0;
16
+ entry.revision = entry.revision || reached;
17
+ reached = Math.max(reached, entry.revision);
13
18
  entry.id ??= `legacy-${index + 1}`;
14
19
  entries.set(entry.id, entry);
15
20
  }
@@ -6,7 +6,7 @@ import { join } from "node:path";
6
6
  import { readLedgerAt } from "./ledger.js";
7
7
  import { ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
8
8
  import { assertStateLockOwned, migrationEntries } from "./plan-io.js";
9
- import { hasCode, readOptional, readRecords, reconcilePlan } from "./plan-log.js";
9
+ import { assertProjectionComplete, hasCode, readOptional, readRecords, reconcilePlan, } from "./plan-log.js";
10
10
  import { UlwLoopError } from "./types.js";
11
11
  const hooks = new AsyncLocalStorage();
12
12
  export function withCommitHooks(value, fn) {
@@ -72,6 +72,7 @@ function viewContents(dir) {
72
72
  if (plan === undefined || readRecords(dir).length === 0)
73
73
  return [];
74
74
  const ledger = readLedgerAt(dir);
75
+ assertProjectionComplete(plan, ledger);
75
76
  const views = [
76
77
  ["goals.json", `${JSON.stringify(plan, null, 2)}\n`],
77
78
  ["ledger.jsonl", ledger.length === 0 ? "" : `${ledger.map((entry) => JSON.stringify(entry)).join("\n")}\n`],
@@ -1,5 +1,5 @@
1
1
  import { type UlwLoopScope } from "./paths.js";
2
- import type { UlwLoopLedgerEntry, UlwLoopPlan } from "./types.js";
2
+ import { type UlwLoopLedgerEntry, type UlwLoopPlan } from "./types.js";
3
3
  export interface PlanCommitRecord {
4
4
  readonly version: 1;
5
5
  readonly revision: number;
@@ -12,4 +12,5 @@ export declare function logNames(dir: string): string[];
12
12
  export declare function readRecords(dir: string): PlanCommitRecord[];
13
13
  export declare function readNewestRecord(dir: string): PlanCommitRecord | undefined;
14
14
  export declare function reconcilePlan(dir: string): UlwLoopPlan | undefined;
15
+ export declare function assertProjectionComplete(plan: UlwLoopPlan, ledger: readonly UlwLoopLedgerEntry[]): void;
15
16
  export declare function planExists(repoRoot: string, scope?: UlwLoopScope): boolean;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { ulwLoopDir } from "./paths.js";
4
+ import { UlwLoopError } from "./types.js";
4
5
  export function hasCode(error, code) {
5
6
  return error instanceof Error && "code" in error && error.code === code;
6
7
  }
@@ -78,7 +79,35 @@ export function reconcilePlan(dir) {
78
79
  }
79
80
  }
80
81
  const latest = readNewestRecord(dir);
81
- return latest !== undefined && latest.revision >= (cached?.revision ?? 0) ? latest.plan : cached;
82
+ if (latest === undefined)
83
+ return cached;
84
+ if (cached === undefined)
85
+ return latest.plan;
86
+ if ((cached.revision ?? 0) > latest.revision)
87
+ return cached;
88
+ // A goals.json written outside the commit log (the removed tool path rewrote it wholesale, never
89
+ // stamping `revision`) can hold goals no snapshot has. Serving the snapshot would drop them, so the
90
+ // cache wins and is stamped with the snapshot's revision: the next publish derives N+1 from it and
91
+ // folds the whole plan into a real record instead of colliding with an existing revision file.
92
+ // A cache that names an OLDER revision is a lagging view of a force-recreate and never wins.
93
+ if (cached.revision === undefined || cached.revision === latest.revision) {
94
+ const published = new Set(latest.plan.goals.map((goal) => goal.id));
95
+ if (Array.isArray(cached.goals) && cached.goals.some((goal) => !published.has(goal.id)))
96
+ return { ...cached, revision: latest.revision };
97
+ }
98
+ return latest.plan;
99
+ }
100
+ // Goals are never removed from a plan, so every `goal_added` the reconciled ledger still carries must
101
+ // have a goal in the projection. A projection that lacks one is truncated and must not become goals.json.
102
+ export function assertProjectionComplete(plan, ledger) {
103
+ const present = new Set(plan.goals.map((goal) => goal.id));
104
+ const missing = new Set();
105
+ for (const entry of ledger)
106
+ if (entry.kind === "goal_added" && entry.goalId !== undefined && !present.has(entry.goalId))
107
+ missing.add(entry.goalId);
108
+ if (missing.size === 0)
109
+ return;
110
+ throw new UlwLoopError(`Refusing to rewrite goals.json: the ledger records goals the plan projection lacks (${[...missing].join(", ")}). Restore those goals into the newest revisions/ record (keeping plan.revision equal to its file number) before mutating this run.`, "ULW_LOOP_PROJECTION_TRUNCATED", { details: { missingGoalIds: [...missing] } });
82
111
  }
83
112
  export function planExists(repoRoot, scope) {
84
113
  const dir = ulwLoopDir(repoRoot, scope);
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit --with-ultrawork",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.67) Checking Ulw-Loop Steering"
10
+ "statusMessage": "(OmO 5.0.0-beta.68) Checking Ulw-Loop Steering"
11
11
  }
12
12
  ]
13
13
  }
@@ -20,7 +20,7 @@
20
20
  "type": "command",
21
21
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use-spawn",
22
22
  "timeout": 5,
23
- "statusMessage": "(OmO 5.0.0-beta.67) Recording Spawn Admission"
23
+ "statusMessage": "(OmO 5.0.0-beta.68) Recording Spawn Admission"
24
24
  }
25
25
  ]
26
26
  }
@@ -33,7 +33,7 @@
33
33
  "type": "command",
34
34
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
35
35
  "timeout": 5,
36
- "statusMessage": "(OmO 5.0.0-beta.67) Enforcing Unlimited Ulw-Loop Budget"
36
+ "statusMessage": "(OmO 5.0.0-beta.68) Enforcing Unlimited Ulw-Loop Budget"
37
37
  }
38
38
  ]
39
39
  },
@@ -44,7 +44,7 @@
44
44
  "type": "command",
45
45
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
46
46
  "timeout": 5,
47
- "statusMessage": "(OmO 5.0.0-beta.67) Guarding Ulw-Loop Spawns"
47
+ "statusMessage": "(OmO 5.0.0-beta.68) Guarding Ulw-Loop Spawns"
48
48
  }
49
49
  ]
50
50
  }
@@ -56,7 +56,7 @@
56
56
  "type": "command",
57
57
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
58
58
  "timeout": 10,
59
- "statusMessage": "(OmO 5.0.0-beta.67) Checking Ulw-Loop Resume"
59
+ "statusMessage": "(OmO 5.0.0-beta.68) Checking Ulw-Loop Resume"
60
60
  }
61
61
  ]
62
62
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-ulw-loop",
3
- "version": "5.0.0-beta.67",
3
+ "version": "5.0.0-beta.68",
4
4
  "description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -6,11 +6,16 @@ import type { UlwLoopLedgerEntry } from "./types.js";
6
6
  export function readLedgerAt(dir: string): UlwLoopLedgerEntry[] {
7
7
  const entries = new Map<string, UlwLoopLedgerEntry>();
8
8
  const lines = (readOptional(join(dir, "ledger.jsonl")) ?? "").split(/\r?\n/);
9
+ // A materialized view is sorted by revision, so a line that carries no revision (or revision 0)
10
+ // AFTER a published revision was appended out-of-band (the removed tool path) and belongs to the
11
+ // newest revision before it - not to the pre-store past a later ledgerResetRevision would discard.
12
+ let reached = 0;
9
13
  for (const [index, line] of lines.entries()) {
10
14
  if (line.trim().length === 0) continue;
11
15
  try {
12
16
  const entry: UlwLoopLedgerEntry = JSON.parse(line);
13
- entry.revision ??= 0;
17
+ entry.revision = entry.revision || reached;
18
+ reached = Math.max(reached, entry.revision);
14
19
  entry.id ??= `legacy-${index + 1}`;
15
20
  entries.set(entry.id, entry);
16
21
  } catch (error) {
@@ -6,7 +6,14 @@ import { join } from "node:path";
6
6
  import { readLedgerAt } from "./ledger.js";
7
7
  import { type UlwLoopScope, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
8
8
  import { assertStateLockOwned, migrationEntries } from "./plan-io.js";
9
- import { hasCode, type PlanCommitRecord, readOptional, readRecords, reconcilePlan } from "./plan-log.js";
9
+ import {
10
+ assertProjectionComplete,
11
+ hasCode,
12
+ type PlanCommitRecord,
13
+ readOptional,
14
+ readRecords,
15
+ reconcilePlan,
16
+ } from "./plan-log.js";
10
17
  import { UlwLoopError, type UlwLoopLedgerEntry, type UlwLoopPlan } from "./types.js";
11
18
 
12
19
  interface CommitHooks {
@@ -91,6 +98,7 @@ function viewContents(dir: string): [string, string][] {
91
98
  const plan = reconcilePlan(dir);
92
99
  if (plan === undefined || readRecords(dir).length === 0) return [];
93
100
  const ledger = readLedgerAt(dir);
101
+ assertProjectionComplete(plan, ledger);
94
102
  const views: [string, string][] = [
95
103
  ["goals.json", `${JSON.stringify(plan, null, 2)}\n`],
96
104
  ["ledger.jsonl", ledger.length === 0 ? "" : `${ledger.map((entry) => JSON.stringify(entry)).join("\n")}\n`],
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { type UlwLoopScope, ulwLoopDir } from "./paths.js";
4
- import type { UlwLoopLedgerEntry, UlwLoopPlan } from "./types.js";
4
+ import { UlwLoopError, type UlwLoopLedgerEntry, type UlwLoopPlan } from "./types.js";
5
5
 
6
6
  export interface PlanCommitRecord {
7
7
  readonly version: 1;
@@ -76,7 +76,35 @@ export function reconcilePlan(dir: string): UlwLoopPlan | undefined {
76
76
  }
77
77
  }
78
78
  const latest = readNewestRecord(dir);
79
- return latest !== undefined && latest.revision >= (cached?.revision ?? 0) ? latest.plan : cached;
79
+ if (latest === undefined) return cached;
80
+ if (cached === undefined) return latest.plan;
81
+ if ((cached.revision ?? 0) > latest.revision) return cached;
82
+ // A goals.json written outside the commit log (the removed tool path rewrote it wholesale, never
83
+ // stamping `revision`) can hold goals no snapshot has. Serving the snapshot would drop them, so the
84
+ // cache wins and is stamped with the snapshot's revision: the next publish derives N+1 from it and
85
+ // folds the whole plan into a real record instead of colliding with an existing revision file.
86
+ // A cache that names an OLDER revision is a lagging view of a force-recreate and never wins.
87
+ if (cached.revision === undefined || cached.revision === latest.revision) {
88
+ const published = new Set(latest.plan.goals.map((goal) => goal.id));
89
+ if (Array.isArray(cached.goals) && cached.goals.some((goal) => !published.has(goal.id)))
90
+ return { ...cached, revision: latest.revision };
91
+ }
92
+ return latest.plan;
93
+ }
94
+ // Goals are never removed from a plan, so every `goal_added` the reconciled ledger still carries must
95
+ // have a goal in the projection. A projection that lacks one is truncated and must not become goals.json.
96
+ export function assertProjectionComplete(plan: UlwLoopPlan, ledger: readonly UlwLoopLedgerEntry[]): void {
97
+ const present = new Set(plan.goals.map((goal) => goal.id));
98
+ const missing = new Set<string>();
99
+ for (const entry of ledger)
100
+ if (entry.kind === "goal_added" && entry.goalId !== undefined && !present.has(entry.goalId))
101
+ missing.add(entry.goalId);
102
+ if (missing.size === 0) return;
103
+ throw new UlwLoopError(
104
+ `Refusing to rewrite goals.json: the ledger records goals the plan projection lacks (${[...missing].join(", ")}). Restore those goals into the newest revisions/ record (keeping plan.revision equal to its file number) before mutating this run.`,
105
+ "ULW_LOOP_PROJECTION_TRUNCATED",
106
+ { details: { missingGoalIds: [...missing] } },
107
+ );
80
108
  }
81
109
  export function planExists(repoRoot: string, scope?: UlwLoopScope): boolean {
82
110
  const dir = ulwLoopDir(repoRoot, scope);