-
Notifications
You must be signed in to change notification settings - Fork 0
v0.2: crash-safe execution journal, beginExecute/confirm lifecycle, reconcile callback #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,167 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { closeSync, existsSync, openSync, readFileSync, fsyncSync, mkdirSync, writeSync } from "node:fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { dirname } from "node:path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { AuditStatus } from "./audit.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { PlanMeta } from "./planStore.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * The lifecycle statuses the core records in the journal. Subset of | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * AuditStatus — only the transitions the core itself emits. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export type JournalStatus = | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | "previewed" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | "awaiting_approval" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | "approved" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | "executing" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | "executed" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | "rejected" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | "failed"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * One immutable journal line. Every appends records a single plan transition; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * replaying all lines rebuilds the in-memory state on restart. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface JournalEntry { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Schema version for forward compatibility. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| version: 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Epoch milliseconds when the entry was appended. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ts: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** The plan token this entry describes. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| token: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** The lifecycle status after this transition. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| status: JournalStatus; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Plan expiry in epoch milliseconds — replay enforces TTL on restore. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expiresAt: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** sha256 fingerprint of the payload at create time. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fingerprint: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** The PlanMeta as recorded at create time. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| meta: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| tool: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| reason: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| callerId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| previewCount: number | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| dataDigest: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| extra: Readonly<Record<string, unknown>>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Boolean flags restored on replay. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| flags: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| requiresApproval: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| approved: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rejected: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rejectionReason: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| used: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| executing: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * A host-supplied journal. The core appends one line per transition; on | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * startup the host replays the journal to restore state. The journal is the | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * crash-safety mechanism — without it, an `executing` plan is silently | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * forgotten on restart. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface Journal { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Append one entry. Must fsync before returning. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| append(entry: JournalEntry): void; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Read all entries in order. Called once on startup. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| replay(): JournalEntry[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Release any underlying file handle. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| close(): void; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Default: no persistence. Used when the host does not configure a journal. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export const NoopJournal: Journal = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| append(): void {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| replay(): JournalEntry[] { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| close(): void {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Append-only JSONL journal with fsync per line. Each entry is one JSON object | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * terminated by a newline; partial writes from a crash mid-line are skipped on | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * replay (the line won't parse). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export class FileJournal implements Journal { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private fd: number | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor(private path: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const dir = dirname(path); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (dir && !existsSync(dir)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| mkdirSync(dir, { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.fd = openSync(path, "a"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+89
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Fsync the parent directory after creating the journal file. The constructor creates the directory and the file. Fsync the parent directory once when the journal file is created. 🛡️ Proposed fix constructor(private path: string) {
const dir = dirname(path);
if (dir && !existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
+ const isNew = !existsSync(path);
this.fd = openSync(path, "a");
+ if (isNew && dir) {
+ // Persist the new directory entry, not just the file data.
+ const dirFd = openSync(dir, "r");
+ try {
+ fsyncSync(dirFd);
+ } finally {
+ closeSync(dirFd);
+ }
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| append(entry: JournalEntry): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.fd === null) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Do not drop entries silently after After - if (this.fd === null) return;
+ if (this.fd === null) {
+ throw new Error("FileJournal is closed; cannot append.");
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| const line = JSON.stringify(entry) + "\n"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| writeSync(this.fd, line); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fsyncSync(this.fd); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+97
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: In Node.js, the Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
fd -i 'journal|package|lock' . | head -80
printf '%s\n' '--- journal structure and append usages ---'
ast-grep outline src/journal.ts --view expanded
rg -n -C 5 'append\(|writeSync|fsyncSync|openSync|PlanStore' src package.json 2>/dev/null | head -240
printf '%s\n' '--- package metadata ---'
if [ -f package.json ]; then cat package.json; fiRepository: jpka/safe-write-mcp-core Length of output: 14197 🌐 Web query:
💡 Result: The fs.writeSync method in Node.js returns the number of bytes written to the file [1]. If a write operation cannot write the entire amount of data requested, fs.writeSync will return the number of bytes that were successfully written rather than throwing an error [1]. Because fs.writeSync may perform partial writes, it is the responsibility of the developer to inspect the returned value and handle any remaining data if a partial write occurs. If you need to ensure all data is written, you should implement a loop that tracks the remaining buffer and calls fs.writeSync repeatedly until the entire length has been processed. Citations: 🌐 Web query:
💡 Result: In Node.js, fs.writeSync is a synchronous wrapper around the underlying libuv file system operation. At the libuv layer, the implementation of write operations (specifically uvfs_write_all) is designed to handle potential partial writes [1]. Key points regarding partial writes and fs.writeSync include: 1. Internal Retries: The libuv function uvfs_write_all contains a loop that handles partial writes by continuing to write the remaining data until the entire buffer is processed or an error occurs [1]. This logic is critical because standard system calls like write(2) or pwrite(2) may write fewer bytes than requested, especially when dealing with non-blocking file descriptors or specific device types [1][2]. 2. Synchronous Nature: Because fs.writeSync is synchronous, it blocks the main event loop while these internal loops complete. If a partial write occurs, the operation will continue to attempt to finish the write within the same synchronous call until the total requested length is reached or an error (such as an I/O error) stops the process [1]. 3. Historical Context: While early implementations and some asynchronous APIs faced issues where they did not adequately handle partial writes—leading to bugs where only part of the data was written—these have been addressed in modern Node.js versions [3][4][5][2]. For example, improvements have been made to both fs.write and fs.createWriteStream to ensure they do not incorrectly assume a single call will complete an entire buffer write [4][5][2]. 4. Reliability: When using fs.writeSync, you can generally rely on the fact that it will either write the entire buffer provided or throw an exception if an error prevents it from finishing. It does not return a partial count of bytes written unless an error terminates the operation, as the libuv implementation abstracts the retry logic away from the Node.js user [1]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Node tooling available ---'
node --version 2>/dev/null || true
printf '%s\n' '--- journal implementation ---'
cat -n src/journal.ts | sed -n '80,132p'
printf '%s\n' '--- PlanStore state update around journal calls ---'
cat -n src/planStore.ts | sed -n '238,270p;360,390p;515,545p;625,655p'Repository: jpka/safe-write-mcp-core Length of output: 7375 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Node 18 documentation excerpt ---'
curl -L --fail --silent https://nodejs.org/docs/latest-v18.x/api/fs.html |
sed 's/<[^>]*>/ /g' |
tr -s ' ' |
grep -o -i -E '.{0,220}fs\.writeSync.{0,500}' |
head -5
printf '%s\n' '--- Node core implementation references ---'
curl -L --fail --silent https://raw.githubusercontent.com/nodejs/node/v18.20.8/lib/fs.js |
grep -n -A35 -B8 'function writeSync'
printf '%s\n' '--- libuv synchronous write implementation references ---'
curl -L --fail --silent https://raw.githubusercontent.com/libuv/libuv/v1.44.2/src/unix/fs.c |
grep -n -A55 -B8 'uv__fs_write'Repository: jpka/safe-write-mcp-core Length of output: 7933 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
import re
import urllib.request
sources = {
"node-v18.0.0": "https://raw.githubusercontent.com/nodejs/node/v18.0.0/lib/fs.js",
"node-v18.20.8": "https://raw.githubusercontent.com/nodejs/node/v18.20.8/lib/fs.js",
"libuv-v1.44.2": "https://raw.githubusercontent.com/libuv/libuv/v1.44.2/src/unix/fs.c",
"libuv-v1.48.0": "https://raw.githubusercontent.com/libuv/libuv/v1.48.0/src/unix/fs.c",
}
text = {}
for name, url in sources.items():
with urllib.request.urlopen(url) as response:
text[name] = response.read().decode()
for name in ("node-v18.0.0", "node-v18.20.8"):
match = re.search(r"function writeSync\(.*?^}", text[name], re.MULTILINE | re.DOTALL)
assert match and "binding.writeString" in match.group(0)
print(f"{name}: writeSync delegates string writes to binding.writeString")
for name in ("libuv-v1.44.2", "libuv-v1.48.0"):
assert "static ssize_t uv__fs_write_all" in text[name]
write_case = re.search(r"X\(WRITE,\s*uv__fs_write_all\(req\)\)", text[name])
loop = re.search(
r"static ssize_t uv__fs_write_all.*?while \(nbufs > 0\).*?"
r"if \(result <= 0\).*?req->nbufs = uv__fs_buf_offset",
text[name], re.DOTALL,
)
assert write_case and loop
print(f"{name}: synchronous UV_FS_WRITE uses the partial-write loop")
PYRepository: jpka/safe-write-mcp-core Length of output: 154 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Node 18.0 fs.writeSync delegation ---'
curl --max-time 30 -L --fail --silent https://raw.githubusercontent.com/nodejs/node/v18.0.0/lib/fs.js |
grep -n -A4 -B4 'binding.writeString' | head -20
printf '%s\n' '--- Node 18 bundled libuv write path ---'
curl --max-time 30 -L --fail --silent https://raw.githubusercontent.com/nodejs/node/v18.0.0/deps/uv/src/unix/fs.c |
grep -n -A38 -B4 'static ssize_t uv__fs_write_all' | head -55Repository: jpka/safe-write-mcp-core Length of output: 1859 🌐 Web query:
💡 Result: In Node.js, fs.writeSync returns the number of bytes successfully written to the file descriptor [1][2]. The behavior involving Citations:
Handle partial writes in Node.js retries positive partial writes internally, but 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| replay(): JournalEntry[] { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!existsSync(this.path)) return []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const content = readFileSync(this.path, "utf-8"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!content.trim()) return []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const entries: JournalEntry[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const line of content.split("\n")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!line.trim()) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const entry = JSON.parse(line) as JournalEntry; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entry.version === 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| entries.push(entry); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Malformed line (crash mid-write) — skip it. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return entries; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| close(): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.fd !== null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| closeSync(this.fd); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.fd = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Builds a JournalEntry from the current state of a TokenEntry. Called on | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * every state transition that the journal must record. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export function makeJournalEntry( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| token: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| status: JournalStatus, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expiresAt: number, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fingerprint: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| meta: PlanMeta, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| flags: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| requiresApproval: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| approved: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rejected: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rejectionReason: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| used: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| executing: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): JournalEntry { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| version: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ts: Date.now(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| token, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| status, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expiresAt, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fingerprint, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| meta: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| tool: meta.tool, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| reason: meta.reason, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| callerId: meta.callerId, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| previewCount: meta.previewCount, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| dataDigest: meta.dataDigest, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| extra: { ...meta.extra }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| flags: { ...flags }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the grammar in the doc comment.
"Every appends records a single plan transition" is not grammatical.
📝 Committable suggestion
🤖 Prompt for AI Agents