diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bc2e6e3c64..c7bd0fe430 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -280,7 +280,6 @@ jobs:
marker-guard:
name: Marker Guard
runs-on: ubuntu-latest
- if: github.event_name == 'pull_request'
timeout-minutes: 5
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@@ -300,9 +299,20 @@ jobs:
run: bun install
working-directory: script/upstream
+ - name: Run marker parser tests
+ run: bun test
+ working-directory: script/upstream
+
- name: Check for missing altimate_change markers
run: |
- if [[ "${{ github.head_ref }}" == merge-upstream-* ]] || [[ "${{ github.head_ref }}" == upstream/merge-* ]]; then
+ if [[ "${{ github.event_name }}" == "push" ]]; then
+ if [[ "${{ github.event.before }}" == "0000000000000000000000000000000000000000" ]]; then
+ echo "Initial push (zero-SHA) — skipping marker check"
+ exit 0
+ fi
+ echo "Push to main — running marker check against pre-push state"
+ bun run script/upstream/analyze.ts --markers --base "${{ github.event.before }}" --strict
+ elif [[ "${{ github.head_ref }}" == merge-upstream-* ]] || [[ "${{ github.head_ref }}" == upstream/merge-* ]]; then
echo "Upstream merge PR detected — running marker check in non-strict mode"
bun run script/upstream/analyze.ts --markers --base ${{ github.event.pull_request.base.ref }}
else
diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx
index 54a0e47aa2..865553899f 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx
@@ -15,7 +15,9 @@ import { Installation } from "@/installation"
import { useKV } from "../context/kv"
import { useCommandDialog } from "../component/dialog-command"
import { useLocal } from "../context/local"
+// altimate_change start — upgrade indicator import
import { UpgradeIndicator } from "../component/upgrade-indicator"
+// altimate_change end
// TODO: what is the best way to do this?
let once = false
@@ -153,7 +155,9 @@ export function Home() {
+ {/* altimate_change start — upgrade indicator in home footer */}
{Installation.VERSION}} />
+ {/* altimate_change end */}
>
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
index 98cb87ebbe..c7225214c0 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
@@ -8,7 +8,9 @@ import { useRoute } from "../../context/route"
// altimate_change start - yolo mode visual indicator
import { Flag } from "@/flag/flag"
// altimate_change end
+// altimate_change start — upgrade indicator import
import { UpgradeIndicator } from "../../component/upgrade-indicator"
+// altimate_change end
export function Footer() {
const { theme } = useTheme()
@@ -96,7 +98,9 @@ export function Footer() {
/status
+ {/* altimate_change start — upgrade indicator in session footer */}
+ {/* altimate_change end */}
)
diff --git a/script/upstream/analyze.test.ts b/script/upstream/analyze.test.ts
new file mode 100644
index 0000000000..ae4ae72fce
--- /dev/null
+++ b/script/upstream/analyze.test.ts
@@ -0,0 +1,290 @@
+import { describe, test, expect } from "bun:test"
+import { parseDiffForMarkerWarnings } from "./analyze"
+
+// Helper to create a unified diff string from lines
+function makeDiff(hunks: string): string {
+ return `diff --git a/file.ts b/file.ts
+index abc1234..def5678 100644
+--- a/file.ts
++++ b/file.ts
+${hunks}`
+}
+
+describe("parseDiffForMarkerWarnings", () => {
+ test("returns no warnings for empty diff", () => {
+ expect(parseDiffForMarkerWarnings("file.ts", "")).toEqual([])
+ expect(parseDiffForMarkerWarnings("file.ts", " \n ")).toEqual([])
+ })
+
+ test("added code inside added markers — no warning", () => {
+ const diff = makeDiff(
+ `@@ -10,3 +10,5 @@
+ const existing = true
++// altimate_change start — new feature
++const custom = true
++// altimate_change end
+ const more = true`,
+ )
+ expect(parseDiffForMarkerWarnings("file.ts", diff)).toEqual([])
+ })
+
+ test("added code without markers — warning", () => {
+ const diff = makeDiff(
+ `@@ -10,3 +10,4 @@
+ const existing = true
++const unmarked = true
+ const more = true`,
+ )
+ const warnings = parseDiffForMarkerWarnings("file.ts", diff)
+ expect(warnings).toHaveLength(1)
+ expect(warnings[0].file).toBe("file.ts")
+ expect(warnings[0].context).toContain("unmarked")
+ })
+
+ test("REGRESSION: added code inside existing (context) markers — no warning", () => {
+ // This is the exact bug that caused the upgrade indicator leak.
+ // Markers are context lines (already committed), new code is added inside.
+ const diff = makeDiff(
+ `@@ -10,4 +10,5 @@
+ const existing = true
+ // altimate_change start — existing feature
++const newCodeInsideExistingBlock = true
+ // altimate_change end
+ const more = true`,
+ )
+ expect(parseDiffForMarkerWarnings("file.ts", diff)).toEqual([])
+ })
+
+ test("REGRESSION: context marker end followed by added code — warning", () => {
+ // New code added AFTER an existing marker block should be flagged.
+ const diff = makeDiff(
+ `@@ -10,4 +10,5 @@
+ // altimate_change start — block A
+ const blockA = true
+ // altimate_change end
++const outsideBlock = true
+ const existing = true`,
+ )
+ const warnings = parseDiffForMarkerWarnings("file.ts", diff)
+ expect(warnings).toHaveLength(1)
+ expect(warnings[0].context).toContain("outsideBlock")
+ })
+
+ test("REGRESSION: context marker start, added code, context marker end — no warning", () => {
+ // Entire marker block is pre-existing (context), only the inner code is new.
+ const diff = makeDiff(
+ `@@ -8,4 +8,5 @@
+ // altimate_change start - yolo mode visual indicator
+ import { Flag } from "@/flag/flag"
+ // altimate_change end
++import { UpgradeIndicator } from "../../component/upgrade-indicator"
+ const next = true`,
+ )
+ // The import line is skipped by the "import " heuristic, so no warning
+ expect(parseDiffForMarkerWarnings("file.ts", diff)).toEqual([])
+ })
+
+ test("REGRESSION: non-import code inside existing context markers — no warning", () => {
+ // Verifies the fix works independently of the import heuristic
+ const diff = makeDiff(
+ `@@ -10,4 +10,5 @@
+ const existing = true
+ // altimate_change start — custom feature
++const customCode = doSomething()
+ // altimate_change end
+ const more = true`,
+ )
+ expect(parseDiffForMarkerWarnings("file.ts", diff)).toEqual([])
+ })
+
+ test("REGRESSION: JSX comment markers on context lines — no warning", () => {
+ // JSX uses {/* altimate_change start ... */} syntax
+ const diff = makeDiff(
+ `@@ -95,4 +95,5 @@
+ {/* altimate_change start — upgrade indicator */}
++
+ {/* altimate_change end */}
+ `,
+ )
+ expect(parseDiffForMarkerWarnings("file.ts", diff)).toEqual([])
+ })
+
+ test("multiple hunks — marker state resets at hunk boundary", () => {
+ // Each hunk starts fresh, marker state should NOT carry across hunks
+ // since different parts of the file may have different marker context.
+ const diff = makeDiff(
+ `@@ -5,3 +5,4 @@
+ // altimate_change start — block 1
++const inBlock = true
+ // altimate_change end
+@@ -50,3 +51,4 @@
+ const existing = true
++const unmarkedInSecondHunk = true
+ const more = true`,
+ )
+ const warnings = parseDiffForMarkerWarnings("file.ts", diff)
+ expect(warnings).toHaveLength(1)
+ expect(warnings[0].context).toContain("unmarkedInSecondHunk")
+ })
+
+ test("marker state from hunk 1 does not leak into hunk 2", () => {
+ // If hunk 1 ends inside a marker block (start without end in context),
+ // hunk 2 should NOT inherit that state.
+ const diff = makeDiff(
+ `@@ -5,3 +5,4 @@
+ // altimate_change start — block 1
++const inBlock = true
+ const moreInBlock = true
+@@ -80,3 +81,4 @@
+ const unrelated = true
++const shouldBeWarned = true
+ const end = true`,
+ )
+ const warnings = parseDiffForMarkerWarnings("file.ts", diff)
+ expect(warnings).toHaveLength(1)
+ expect(warnings[0].context).toContain("shouldBeWarned")
+ })
+
+ test("import lines are skipped even without markers", () => {
+ const diff = makeDiff(
+ `@@ -1,3 +1,4 @@
+ import { existing } from "./existing"
++import { NewThing } from "./new-thing"
+ const x = 1`,
+ )
+ expect(parseDiffForMarkerWarnings("file.ts", diff)).toEqual([])
+ })
+
+ test("export lines are skipped even without markers", () => {
+ const diff = makeDiff(
+ `@@ -1,3 +1,4 @@
+ const x = 1
++export { x }
+ const y = 2`,
+ )
+ expect(parseDiffForMarkerWarnings("file.ts", diff)).toEqual([])
+ })
+
+ test("comment-only lines are skipped (not TODOs)", () => {
+ const diff = makeDiff(
+ `@@ -1,3 +1,4 @@
+ const x = 1
++// this is a harmless comment
+ const y = 2`,
+ )
+ expect(parseDiffForMarkerWarnings("file.ts", diff)).toEqual([])
+ })
+
+ test("TODO comments are NOT skipped", () => {
+ const diff = makeDiff(
+ `@@ -1,3 +1,4 @@
+ const x = 1
++// TODO: implement custom feature
+ const y = 2`,
+ )
+ const warnings = parseDiffForMarkerWarnings("file.ts", diff)
+ expect(warnings).toHaveLength(1)
+ })
+
+ test("empty added lines are skipped", () => {
+ const diff = makeDiff(
+ `@@ -1,3 +1,4 @@
+ const x = 1
++
+ const y = 2`,
+ )
+ expect(parseDiffForMarkerWarnings("file.ts", diff)).toEqual([])
+ })
+
+ test("deleted lines don't affect marker state or line numbers", () => {
+ const diff = makeDiff(
+ `@@ -5,5 +5,5 @@
+ // altimate_change start — feature
+-const oldCode = true
++const newCode = true
+ // altimate_change end
+ const next = true`,
+ )
+ expect(parseDiffForMarkerWarnings("file.ts", diff)).toEqual([])
+ })
+
+ test("line number in warning matches diff hunk position", () => {
+ const diff = makeDiff(
+ `@@ -42,3 +42,4 @@
+ const existing = true
++const unmarked = true
+ const more = true`,
+ )
+ const warnings = parseDiffForMarkerWarnings("file.ts", diff)
+ expect(warnings).toHaveLength(1)
+ expect(warnings[0].line).toBe(43)
+ })
+
+ test("context truncated to 80 chars in warning", () => {
+ const longLine = "x".repeat(120)
+ const diff = makeDiff(
+ `@@ -1,3 +1,4 @@
+ const a = 1
++const ${longLine} = true
+ const b = 2`,
+ )
+ const warnings = parseDiffForMarkerWarnings("file.ts", diff)
+ expect(warnings).toHaveLength(1)
+ expect(warnings[0].context.length).toBeLessThanOrEqual(80)
+ })
+
+ test("only first unmarked line is reported per file", () => {
+ const diff = makeDiff(
+ `@@ -1,3 +1,5 @@
+ const a = 1
++const first = true
++const second = true
+ const b = 2`,
+ )
+ const warnings = parseDiffForMarkerWarnings("file.ts", diff)
+ expect(warnings).toHaveLength(1)
+ expect(warnings[0].context).toContain("first")
+ })
+
+ test("real-world scenario: upgrade indicator in footer.tsx", () => {
+ // Simulates the exact diff that leaked: UpgradeIndicator added to
+ // session footer without markers, adjacent to existing yolo marker block.
+ const diff = makeDiff(
+ `@@ -8,4 +8,6 @@
+ // altimate_change start - yolo mode visual indicator
+ import { Flag } from "@/flag/flag"
+ // altimate_change end
++// altimate_change start — upgrade indicator import
++import { UpgradeIndicator } from "../../component/upgrade-indicator"
++// altimate_change end
+
+@@ -96,4 +98,6 @@
+
++ {/* altimate_change start — upgrade indicator in session footer */}
++
++ {/* altimate_change end */}
+ `,
+ )
+ expect(parseDiffForMarkerWarnings("footer.tsx", diff)).toEqual([])
+ })
+
+ test("real-world scenario: unmarked upgrade indicator would be caught", () => {
+ // Same scenario but WITHOUT markers — should flag
+ const diff = makeDiff(
+ `@@ -8,4 +8,5 @@
+ // altimate_change start - yolo mode visual indicator
+ import { Flag } from "@/flag/flag"
+ // altimate_change end
++import { UpgradeIndicator } from "../../component/upgrade-indicator"
+
+@@ -96,4 +97,5 @@
+
++
+ `,
+ )
+ const warnings = parseDiffForMarkerWarnings("footer.tsx", diff)
+ // import is skipped by heuristic, but is flagged
+ expect(warnings).toHaveLength(1)
+ expect(warnings[0].context).toContain("UpgradeIndicator")
+ })
+})
diff --git a/script/upstream/analyze.ts b/script/upstream/analyze.ts
index f205a9989e..efbc435a38 100644
--- a/script/upstream/analyze.ts
+++ b/script/upstream/analyze.ts
@@ -606,22 +606,9 @@ function isUpstreamShared(file: string, config: MergeConfig): boolean {
return upstreamFiles.has(file)
}
-function checkFileForMarkers(file: string, base?: string): MarkerWarning[] {
- const { execSync } = require("child_process")
- const root = repoRoot()
+// altimate_change start — exported for unit testing
+export function parseDiffForMarkerWarnings(file: string, diffOutput: string): MarkerWarning[] {
const warnings: MarkerWarning[] = []
-
- const diffCmd = base
- ? `git diff -U5 ${base}...HEAD -- "${file}"`
- : `git diff -U5 HEAD -- "${file}"`
-
- let diffOutput: string
- try {
- diffOutput = execSync(diffCmd, { cwd: root, encoding: "utf-8" })
- } catch {
- return warnings
- }
-
if (!diffOutput.trim()) return warnings
const lines = diffOutput.split("\n")
@@ -635,10 +622,27 @@ function checkFileForMarkers(file: string, base?: string): MarkerWarning[] {
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)/)
if (hunkMatch) {
currentLine = parseInt(hunkMatch[1]) - 1
+ // Reset marker state at hunk boundaries — hunks are discontinuous
+ // regions of the file, so marker blocks from one region don't apply
+ // to another.
+ inMarkerBlock = false
continue
}
- if (line.startsWith("+") && !line.startsWith("+++")) {
+ // altimate_change start — fix: track marker state from context lines, not just added lines
+ if (line.startsWith("-") && !line.startsWith("---")) {
+ // deleted line, don't increment
+ continue
+ }
+
+ // Both added (+) and context ( ) lines appear in the final file,
+ // so both must update marker state. Without this, existing markers
+ // on context lines are invisible and new code next to them looks
+ // unmarked — causing false positives.
+ const isAdded = line.startsWith("+") && !line.startsWith("+++")
+ const isContext = !line.startsWith("+") && !line.startsWith("-") && !line.startsWith("\\")
+
+ if (isAdded || isContext) {
currentLine++
const content = line.slice(1).trim()
@@ -646,6 +650,9 @@ function checkFileForMarkers(file: string, base?: string): MarkerWarning[] {
if (content.includes("altimate_change end")) { inMarkerBlock = false; continue }
if (content.includes("altimate_change")) continue
+ // Only flag added lines as violations — context lines are pre-existing
+ if (!isAdded) continue
+
if (!content) continue
if (content.startsWith("//") && !content.includes("TODO")) continue
if (content.startsWith("import ")) continue
@@ -658,11 +665,8 @@ function checkFileForMarkers(file: string, base?: string): MarkerWarning[] {
newCodeContext = content
}
}
- } else if (line.startsWith("-")) {
- // deleted line, don't increment
- } else {
- currentLine++
}
+ // altimate_change end
}
if (hasNewCode) {
@@ -676,6 +680,25 @@ function checkFileForMarkers(file: string, base?: string): MarkerWarning[] {
return warnings
}
+// altimate_change end
+
+function checkFileForMarkers(file: string, base?: string): MarkerWarning[] {
+ const { execSync } = require("child_process")
+ const root = repoRoot()
+
+ const diffCmd = base
+ ? `git diff -U5 ${base}...HEAD -- "${file}"`
+ : `git diff -U5 HEAD -- "${file}"`
+
+ let diffOutput: string
+ try {
+ diffOutput = execSync(diffCmd, { cwd: root, encoding: "utf-8" })
+ } catch {
+ return []
+ }
+
+ return parseDiffForMarkerWarnings(file, diffOutput)
+}
function runMarkerCheck(config: MergeConfig, base?: string, strict?: boolean): number {
const { execSync } = require("child_process")
@@ -804,7 +827,11 @@ async function main(): Promise {
}
}
-main().catch((e) => {
- logger.error(`Analysis failed: ${e.message || e}`)
- process.exit(2)
-})
+// altimate_change start — guard CLI execution when imported as a module (e.g., by tests)
+if (import.meta.main) {
+ main().catch((e) => {
+ logger.error(`Analysis failed: ${e.message || e}`)
+ process.exit(2)
+ })
+}
+// altimate_change end