Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
4867a12
docs: add public community and release guides
Obed0101 Jun 13, 2026
5436921
ci: harden release supply chain gates
Obed0101 Jun 13, 2026
2e0ae10
docs: expand MendCode customization guide (#69)
Obed0101 Jun 13, 2026
ad9d4cd
docs: show mascot only in README header (#71)
Obed0101 Jun 13, 2026
1758ed5
docs: remove README mascot header (#74)
Obed0101 Jun 13, 2026
6b0e0b8
fix: show MendCode version on welcome
Obed0101 Jun 13, 2026
5c5e907
fix: align MendCode version and CLI name (#76)
Obed0101 Jun 13, 2026
60ca445
chore: sync main back to dev (#79)
Obed0101 Jun 13, 2026
b1da5d7
docs: add MendCode release flow skill (#81)
Obed0101 Jun 13, 2026
120c218
Add Agent View welcome date context (#80)
Obed0101 Jun 13, 2026
bfa5fd4
[codex] Backport OpenCode terminal and SDK fixes (#83)
Obed0101 Jun 13, 2026
f319f37
fix: prepare v0.1.4 compaction and prompt fixes (#85)
Obed0101 Jun 13, 2026
a745852
Backport OpenCode runtime updates for v0.1.5 (#88)
Obed0101 Jun 13, 2026
4923808
fix: release v0.1.6 memory injection (#91)
Obed0101 Jun 14, 2026
f58608f
feat: add usage insights dashboard (#95)
Obed0101 Jun 14, 2026
2d06971
fix: release v0.1.7 setup memory UX (#98)
Obed0101 Jun 14, 2026
3026de0
fix: complete v0.1.7 memory and docs (#100)
Obed0101 Jun 14, 2026
da84090
fix: release v0.1.8 stats cache and installer
Obed0101 Jun 14, 2026
14ce846
fix: simplify public cli and compaction resume (#107)
Obed0101 Jun 14, 2026
0096300
docs: add v0.1.9 changelog (#110)
Obed0101 Jun 14, 2026
37fa1cc
feat: prepare v0.1.10 tui and memory updates (#112)
Obed0101 Jun 15, 2026
cb0d14b
fix: prepare v0.1.11 tui and mflow updates (#115)
Obed0101 Jun 15, 2026
8f50f67
feat: prepare v0.1.12 rendering and installer updates (#118)
Obed0101 Jun 16, 2026
81543d6
chore: unblock v0.1.12 release dependencies (#122)
Obed0101 Jun 16, 2026
5a64c04
feat: prepare v0.1.13 release
Obed0101 Jun 16, 2026
20c6c6c
fix: update hono for v0.1.13 release
Obed0101 Jun 16, 2026
3cf5668
feat: prepare v0.1.14 package registry release (#132)
Obed0101 Jun 17, 2026
fb18a00
fix: update protobufjs for v0.1.14 release (#136)
Obed0101 Jun 17, 2026
741eedc
release: prepare v0.1.15
Obed0101 Jun 20, 2026
ff6b51c
fix: sync release dependency gates to dev
Obed0101 Jun 20, 2026
6ef1f04
fix: sync final OSV versions to dev
Obed0101 Jun 20, 2026
e8e9a86
fix: allow urgent OSV release versions through age gate (#145) (#146)
Obed0101 Jun 20, 2026
1bde962
Add Usage Insights screenshot (#147)
Obed0101 Jun 20, 2026
1eb370a
docs: sync v0.1.15 summary to dev (#149)
Obed0101 Jun 20, 2026
9b495a0
docs: expand memory center side agent guidance (#150)
Obed0101 Jun 20, 2026
e184095
fix: apply dream schedule proposals (#152)
Obed0101 Jun 20, 2026
e842d8f
docs: update website domain (#155)
Obed0101 Jun 20, 2026
96cb067
docs: remove personal release examples (#158)
Obed0101 Jun 20, 2026
eb8cab2
feat: add loop workflows and changes review
Obed0101 Jun 23, 2026
4c9a564
docs: add workflow screenshots (#163)
Obed0101 Jun 23, 2026
09a9f8a
fix: harden loop workflows (#165)
Obed0101 Jun 23, 2026
825d9b3
fix: keep loops cursor out of detail rows (#168)
Obed0101 Jun 24, 2026
d8a3920
fix: remove loops dashboard cursor sink (#171)
Obed0101 Jun 24, 2026
650323f
feat(loop): add goal budgets and optimize release ci
Obed0101 Jun 24, 2026
58eadd8
release: prepare v0.1.19 (#176)
Obed0101 Jun 26, 2026
9f49990
release: prepare v0.1.20 (#179)
Obed0101 Jul 27, 2026
69b7761
docs(readme): move branding to top and bottom (#181)
Obed0101 Jul 27, 2026
d30f242
fix: harden release dependencies and binary patches (#184)
Obed0101 Jul 28, 2026
0f99ed8
fix: update release dependency pins (#187)
Obed0101 Jul 28, 2026
47f40d6
docs: sync v0.1.20 changelog metadata (#189)
Obed0101 Jul 28, 2026
c7b27e7
feat: consolidate v0.1.20 TUI and runtime updates
Obed0101 Jul 28, 2026
6ace97a
fix: prevent hidden prompt leakage during questions (#194)
Obed0101 Jul 28, 2026
963afc6
docs: refresh README screenshots
Obed0101 Jul 29, 2026
21ba618
release: prepare v0.1.21
Obed0101 Jul 30, 2026
4e9f59f
fix(upgrade): show patch update notifications by default (#205)
Obed0101 Jul 30, 2026
d4fcff5
sync: carry v0.1.22 release fixes back to dev (#209)
Obed0101 Jul 30, 2026
38d47b5
release: prepare v0.1.23 session reliability update (#210)
Obed0101 Jul 31, 2026
582b0e6
fix(release): validate public help output across platforms (#213)
Obed0101 Jul 31, 2026
0e3abe3
fix(release): make help smoke test platform-independent (#215)
Obed0101 Jul 31, 2026
472e889
fix: harden runtime cleanup and session state (#218)
Obed0101 Aug 1, 2026
3743231
fix: repair compacted session cancellation state (#221)
Obed0101 Aug 1, 2026
5a9c608
fix(upgrade): make in-app updates transport-independent (#224)
Obed0101 Aug 2, 2026
e978f36
release: prepare v0.1.27 TUI reliability update (#226)
Obed0101 Aug 2, 2026
42386a5
feat: add durable workflows and harden background lifecycle (#230)
Obed0101 Aug 4, 2026
1d84441
fix: patch vulnerable release dependencies (#232)
Obed0101 Aug 4, 2026
df86414
fix(installer): skip setup prompt without TTY (#235)
Obed0101 Aug 4, 2026
4135338
fix(installer): make Windows downloads resilient
Obed0101 Aug 4, 2026
18ce846
release: prepare v0.1.29 Windows installer fixes
Obed0101 Aug 4, 2026
005bedf
release: prepare v0.1.30 runtime reliability update (#241)
Obed0101 Aug 5, 2026
0cd8ecf
fix(release): build Windows assets on native architectures (#244)
Obed0101 Aug 5, 2026
2cca2e4
fix(release): harden Windows baseline and long-running TUI state (#246)
Obed0101 Aug 5, 2026
1c371c3
fix(release): finalize v0.1.30 TUI recovery and notes (#249)
Obed0101 Aug 5, 2026
1f3ea62
fix(runtime): harden session and workflow recovery (#252)
Obed0101 Aug 8, 2026
50effea
release: integrate v0.1.30 runtime and TUI improvements (#254)
Obed0101 Aug 18, 2026
fa70a2f
chore(deps): bump dompurify to 3.4.13 (#258)
Obed0101 Aug 18, 2026
a40ca02
fix(deps): synchronize DOMPurify lockfile (#259) (#260)
Obed0101 Aug 18, 2026
7d07d19
fix(deps): patch release dependency vulnerabilities (#261) (#262)
Obed0101 Aug 18, 2026
08a736c
fix: stabilize Mermaid interaction and test isolation (#264)
Obed0101 Aug 18, 2026
5d65f77
release: prepare v0.1.31 Loop and Workflow verification (#266)
Obed0101 Aug 19, 2026
d050452
fix: harden long-running session controls (#269)
Obed0101 Aug 20, 2026
65ee075
fix: ship v0.1.33 reliability hotfix (#271)
Obed0101 Aug 20, 2026
ebe810c
fix: size short reasoning blocks (#274)
Obed0101 Aug 21, 2026
b21289f
fix: preserve v prefix in release names (#277)
Obed0101 Aug 21, 2026
2029d45
release: prepare v0.1.35 (#279)
Obed0101 Aug 22, 2026
d4b4cf1
release: prepare v0.1.36 (#281)
Obed0101 Aug 23, 2026
16e6bee
release: prepare v0.1.37
Obed0101 Aug 24, 2026
d90a1fe
release: promote v0.1.37 to main
Obed0101 Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Changelog

## 0.1.37 - 2026-08-23

MendCode v0.1.37 prevents orphaned macOS Loop services and makes Workflow completion recovery bounded and executable.

### Changed

- Run macOS Loop LaunchAgents as one-shot scheduler ticks instead of leaving a persistent Bun coordinator alive between wakeups.
- Accept bounded package-manager validation commands from relative project subdirectories, including `pnpm --dir app build`.

### Fixed

- Self-uninstall project Loop services after their final scheduled Loop disappears, including services already waking with an empty project scope.
- Remove macOS LaunchAgents by their loaded label so a service can safely uninstall itself while it is running.
- Reject unsupported Workflow completion commands before task execution instead of blocking after every task has finished.
- Time out a stalled structured completion auditor after 3 minutes, cancel its child session, and route it through the existing bounded retry or blocked state instead of auditing forever or overlapping the retry.
- Route completion audits through the explicit read-only Explore profile instead of silently inheriting the primary Build agent.
- Resume a Workflow whose work already finished but whose completion audit was blocked by creating a fresh audit generation without rerunning completed tasks.
- Keep explicit `workflows start|resume|retry-* --wait` CLI runs attached until a durable stop instead of disposing their runner immediately.

### Tests

- Add focused regressions for macOS one-shot scheduling and self-uninstall routing, package-manager validation safety, early Workflow plan validation, and stalled completion auditors.
- Run 96 focused public-launcher, Loop, Workflow, scheduler, persistence, and completion tests with isolated test state, plus a real isolated macOS LaunchAgent self-removal smoke.

## 0.1.36 - 2026-08-23

MendCode v0.1.36 makes Loop and Workflow supervision durable, observable, and consistent with the workspace users actually inspect.
Expand Down
12 changes: 6 additions & 6 deletions src/mendcode/packages/extensions/zed/extension.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
id = "mendcode"
name = "MendCode"
description = "The open source coding agent."
version = "0.1.36"
version = "0.1.37"
schema_version = 1
authors = ["MendCode"]
repository = "https://github.com/MendCode/MendCode"
Expand All @@ -11,26 +11,26 @@ name = "MendCode"
icon = "./icons/opencode.svg"

[agent_servers.mendcode.targets.darwin-aarch64]
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.36/mendcode-darwin-arm64.zip"
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.37/mendcode-darwin-arm64.zip"
cmd = "./mendcode"
args = ["acp"]

[agent_servers.mendcode.targets.darwin-x86_64]
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.36/mendcode-darwin-x64.zip"
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.37/mendcode-darwin-x64.zip"
cmd = "./mendcode"
args = ["acp"]

[agent_servers.mendcode.targets.linux-aarch64]
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.36/mendcode-linux-arm64.tar.gz"
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.37/mendcode-linux-arm64.tar.gz"
cmd = "./mendcode"
args = ["acp"]

[agent_servers.mendcode.targets.linux-x86_64]
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.36/mendcode-linux-x64.tar.gz"
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.37/mendcode-linux-x64.tar.gz"
cmd = "./mendcode"
args = ["acp"]

[agent_servers.mendcode.targets.windows-x86_64]
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.36/mendcode-windows-x64.zip"
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.37/mendcode-windows-x64.zip"
cmd = "./mendcode.exe"
args = ["acp"]
2 changes: 1 addition & 1 deletion src/mendcode/packages/opencode/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.1.36",
"version": "0.1.37",
"name": "mendcode",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion src/mendcode/packages/opencode/script/dev-rearm-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht
import { once } from "node:events"
import { probeRuntime } from "./dev-rearm"

const VERSION = "0.1.36"
const VERSION = "0.1.37"
const PROJECT = "/tmp/mendcode-dev-rearm-smoke-project"

function assert(condition: unknown, message: string): asserts condition {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -612,9 +612,294 @@ export function resolveWorkingStartedAt(input: {
sessionUpdated?: number
fallback?: number
}) {
return [input.stored, input.activeAssistantCreated, input.sessionUpdated, input.fallback]
.filter((item): item is number => typeof item === "number" && Number.isFinite(item) && item > 0)
.toSorted((a, b) => a - b)[0]
const valid = (item: number | undefined): item is number =>
typeof item === "number" && Number.isFinite(item) && item > 0
const activeTurn = [input.stored, input.activeAssistantCreated].filter(valid).toSorted((a, b) => a - b)[0]
if (activeTurn) return activeTurn
if (valid(input.sessionUpdated)) return input.sessionUpdated
if (valid(input.fallback)) return input.fallback
}

export function shouldClearWorkingStartedAt(input: {
statusType: string
hasActiveWorkingAssistant?: boolean
permissionPending?: boolean
interrupted?: boolean
terminalAssistant?: boolean
}) {
if (input.interrupted) return true
if (input.permissionPending) return false
if (input.terminalAssistant) return true
return input.statusType === "idle" && !input.hasActiveWorkingAssistant && !input.permissionPending
}

export function shouldKeepWorkingStatus(input: {
interrupted?: boolean
submitPreflightActive?: boolean
hasPendingPromptDelivery?: boolean
compactionActive?: boolean
hasActiveWorkingAssistant?: boolean
permissionPending?: boolean
terminalAssistant?: boolean
statusType: string
}) {
if (input.interrupted) return false
// A stale preflight flag can survive the terminal assistant event. Do not
// let that local flag resurrect Generating after a completed turn.
if (
input.terminalAssistant &&
!input.hasPendingPromptDelivery &&
!input.compactionActive &&
!input.permissionPending &&
!input.hasActiveWorkingAssistant
)
return false
return Boolean(
input.submitPreflightActive ||
input.hasPendingPromptDelivery ||
input.compactionActive ||
!shouldClearWorkingStartedAt({
statusType: input.statusType,
hasActiveWorkingAssistant: input.hasActiveWorkingAssistant,
permissionPending: input.permissionPending,
terminalAssistant: input.terminalAssistant,
}),
)
}

export function promptWorkingIndicatorVisible(input: {
hasSession: boolean
submitPreflightActive: boolean
configuredVisible: boolean
working: boolean
}) {
return (input.hasSession || input.submitPreflightActive) && input.configuredVisible && input.working
}

export function shouldEnableSessionInterrupt(input: {
statusType: string
hasActiveWorkingAssistant?: boolean
hasPendingPromptDelivery?: boolean
autocompleteVisible?: boolean
promptFocused?: boolean
compactionActive?: boolean
interruptRequested?: boolean
}) {
if (input.interruptRequested) return false
// An active turn owns Esc even when autocomplete/another prompt overlay has
// focus, so the first press can arm cancellation without closing the overlay.
if (
input.statusType !== "idle" ||
input.hasActiveWorkingAssistant ||
input.hasPendingPromptDelivery ||
input.compactionActive
)
return true
if (input.autocompleteVisible) return false
return false
}

export function hasInterruptibleSessionTurn(input: {
statusType: string
hasDraft?: boolean
hasActiveWorkingAssistant?: boolean
hasPendingPromptDelivery?: boolean
compactionActive?: boolean
}) {
return (
input.statusType !== "idle" ||
Boolean(input.hasActiveWorkingAssistant || input.hasPendingPromptDelivery || input.compactionActive)
)
}

export function sessionInterruptConfirmationAction(input: {
activeTargetMessageID?: string
armedTargetMessageID?: string
}): "ignore" | "arm" | "interrupt" {
if (!input.activeTargetMessageID) return "ignore"
return input.armedTargetMessageID === input.activeTargetMessageID ? "interrupt" : "arm"
}

export function sessionInterruptHint(input: { enabled: boolean; working: boolean; armed: boolean }) {
if (!input.enabled || !input.working || !input.armed) return
return "[esc again to interrupt]"
}

export function clipboardPasteAction(content: { mime?: string; data?: string } | undefined): "image" | "text" | "none" {
if (!content) return "none"
if (content.mime?.startsWith("image/")) return "image"
if (content.mime === "text/plain" && content.data) return "text"
return "none"
}

export function shouldAcceptPromptInterruptFocus(input: {
inputFocused: boolean
currentFocusedRenderable?: unknown
promptInput?: unknown
}) {
return input.inputFocused && input.currentFocusedRenderable === input.promptInput
}

export function shouldUseStoredPromptHistoryFallback(input: {
historyItems?: () => readonly PromptInfo[]
messageHistoryCount?: number
}) {
return input.historyItems === undefined
}

function stablePromptPartForHistory(part: PromptInfo["parts"][number]) {
if (part.type === "text" && part.source?.text) {
return {
...part,
source: {
...part.source,
text: {
value: part.source.text.value,
},
},
}
}
if (part.type === "file" && part.source?.text) {
return {
...part,
source: {
...part.source,
text: {
value: part.source.text.value,
},
},
}
}
if (part.type === "agent" && part.source) {
return {
...part,
source: {
value: part.source.value,
},
}
}
return part
}

export function promptHistoryMatchesCurrent(input: {
currentPrompt: PromptInfo
currentMode: "normal" | "shell"
historyIndex: number
historyPrompt?: PromptInfo
}) {
if (input.historyIndex === 0) return input.currentPrompt.input.length === 0 && input.currentPrompt.parts.length === 0
if (!input.historyPrompt) return false
if (input.currentPrompt.input !== input.historyPrompt.input) return false
if (input.currentMode !== (input.historyPrompt.mode ?? "normal")) return false
return (
JSON.stringify(input.currentPrompt.parts.map(stablePromptPartForHistory)) ===
JSON.stringify(input.historyPrompt.parts.map(stablePromptPartForHistory))
)
}

export function shouldPreferMessagePromptHistory(input: { direction?: 1 | -1; currentPromptMatchesHistory: boolean }) {
if (input.direction === undefined) return false
return input.currentPromptMatchesHistory
}

function trailingPromptPlaceholderStart(input: { text: string; parts?: readonly PromptInfo["parts"][number][] }) {
const trimmedEnd = input.text.trimEnd().length
return input.parts
?.flatMap((part) => {
if ((part.type === "text" || part.type === "file") && part.source?.text) return [part.source.text]
if (part.type === "agent" && part.source) return [part.source]
return []
})
.filter((source) => source.end >= trimmedEnd)
.map((source) => source.start)
.sort((a, b) => a - b)
.at(0)
}

export function shouldAttemptPromptHistoryNavigation(input: {
direction?: 1 | -1
cursorOffset: number
text: string
parts?: readonly PromptInfo["parts"][number][]
}) {
if (input.direction === undefined) return false
if (input.direction === -1) return input.cursorOffset === 0
if (input.cursorOffset === promptCursorEndOffset(input.text)) return true
const placeholderStart = trailingPromptPlaceholderStart(input)
return placeholderStart !== undefined && input.cursorOffset >= placeholderStart
}

export function shouldSnapPromptCursorToEnd(input: {
direction?: 1 | -1
cursorOffset: number
text: string
visualRow: number
totalVisualRows: number
scrollY: number
}) {
if (input.direction !== 1) return false
// OpenTUI reports visualRow relative to the viewport. Use the total wrapped
// row count instead of logical newline count so long lines do not look like
// the end of the prompt while ArrowDown is still scrolling through them.
const absoluteVisualRow = input.visualRow + input.scrollY
return absoluteVisualRow >= input.totalVisualRows - 1 && input.cursorOffset < promptCursorEndOffset(input.text)
}

export function shouldHandlePromptCursorArrow(input: Pick<ParsedKey, "name" | "ctrl" | "meta" | "shift" | "super">) {
if (input.ctrl || input.meta || input.shift || input.super) return false
return input.name === "left" || input.name === "right"
}

export function promptDraftHistoryAction(input: { undo: boolean; redo: boolean; canUndo: boolean; canRedo: boolean }) {
if (input.undo && input.canUndo) return "undo" as const
if (input.redo && input.canRedo) return "redo" as const

return undefined
}

export function promptCursorEndOffset(text: string) {
return text.length
}

export function promptCursorOffsetAfterArrow(input: {
cursorOffset: number
text: string
direction: "left" | "right"
atomicRanges?: readonly { start: number; end: number }[]
}) {
const textEnd = promptCursorEndOffset(input.text)
const cursorOffset = Math.max(0, Math.min(textEnd, input.cursorOffset))
const atomicRanges = (input.atomicRanges ?? [])
.filter((range) => Number.isFinite(range.start) && Number.isFinite(range.end) && range.end > range.start)
.map((range) => ({
start: Math.max(0, Math.min(textEnd, range.start)),
end: Math.max(0, Math.min(textEnd, range.end)),
}))
.filter((range) => range.end > range.start)

if (input.direction === "left") {
const atomicRange = atomicRanges.find((range) => cursorOffset > range.start && cursorOffset <= range.end)
if (atomicRange) return Math.max(0, atomicRange.start - 1)
return Math.max(0, cursorOffset - 1)
}

const atomicRange = atomicRanges.find((range) => cursorOffset >= range.start && cursorOffset < range.end)
if (atomicRange) return atomicRange.end
return Math.min(textEnd, cursorOffset + 1)
}

export function promptCursorOffsetFromMouse(input: { visualOffset: number; text: string }) {
return Math.max(0, Math.min(promptCursorEndOffset(input.text), Math.floor(input.visualOffset)))
}

function removeVisibleResolvingText(value: string) {
return value.replace(/(?:^|\s)(?:[^\w\s]\s*)*Resolving\s+\[\d+\/\d+\]\s*/giu, (match) =>
match.startsWith(" ") ? " " : "",
)
}

function cleanPromptInputText(value: string) {
return removeVisibleResolvingText(value)
}

export function shouldClearWorkingStartedAt(input: {
Expand Down
Loading
Loading