Skip to content

Commit 1e7c689

Browse files
committed
feat(coding-agent): adapt upstream edit and GitHub search improvements
1 parent 9bc72f3 commit 1e7c689

9 files changed

Lines changed: 455 additions & 2 deletions

File tree

.oh/skills/oh-ship/SKILL.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ Reviewed upstream commit: `<sha>`
9494

9595
If the selected list is empty, explicitly say `Selected: none` and explain why every high-level upstream theme was excluded. A shallow log/stat dump is not a review.
9696

97+
Do not exclude an entire high-level theme merely because some commits are incompatible. If a theme contains potentially useful harness improvements, split it into: selected now, deferred for a focused follow-up batch, and excluded. Useful-but-risky candidates should become explicit follow-up phases rather than disappearing into `Excluded`.
98+
9799
### 3. Incorporate selected changes
98100

99101
Preferred methods, in order:
@@ -108,6 +110,8 @@ git cherry-pick -n <commit>
108110

109111
Do not use a full merge to pick up upstream work. If a change cannot be isolated with these methods, leave it upstream until it can be understood and adapted safely.
110112

113+
Use phased incorporation for large upstream themes. Prefer small batches such as: edit/hashline reliability, provider compatibility, browser/tool reliability, GitHub/release tooling, read/fetch ergonomics, async jobs, then high-authority tools such as SQLite. Each phase must preserve fork-specific contracts and avoid importing upstream's obsolete architecture.
114+
111115
### 4. Fork-specific removals remain removed
112116

113117
**The fork intentionally removes these upstream subsystems:**
@@ -128,6 +132,7 @@ These are replaced by the assembler pipeline (ADR 0003).
128132
| Upstream modifies code the fork deleted | Leave it upstream; the deletion stands |
129133
| Upstream adds a useful feature unrelated to removed subsystems | Incorporate the feature selectively |
130134
| Upstream mixes useful behavior with removed subsystem wiring | Adapt only the useful behavior without the removed wiring |
135+
| Upstream improves edit/tool reliability but changes wire/display contracts | Adapt the behavior while preserving fork contracts such as `LINE#ID` anchors and assembler-aware tool outputs |
131136
| Upstream adds tests for removed settings/events | Do not copy those tests unless rewritten for fork behavior |
132137
| `bun.lock` changes from selected dependency updates | Apply the dependency update, then run `bun install` |
133138
| `CHANGELOG.md` has upstream release notes | Preserve useful reference sections only when they help fork users |

packages/coding-agent/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- Adapted upstream GitHub code and commit search tools as standalone `gh_search_code` and `gh_search_commits` tools for source/release archaeology without adopting the broader upstream GitHub op-tool refactor.
8+
9+
### Changed
10+
11+
- Adapted upstream hashline duplicate-boundary handling to auto-absorb repeated multiline replacement boundaries while preserving the fork's structured `LINE#ID` edit contract.
12+
13+
514
## [0.8.1] - 2026-05-03
615

716
### Added

packages/coding-agent/src/patch/hashline.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,128 @@ function maybeWarnSuspiciousUnicodeEscapePlaceholder(edits: HashlineEdit[], warn
454454
);
455455
}
456456
}
457+
458+
type ReplacementHashlineEdit =
459+
| { op: "replace_line"; pos: Anchor; lines: string[] }
460+
| { op: "replace_range"; pos: Anchor; end: Anchor; lines: string[] };
461+
462+
interface ReplacementBounds {
463+
startLine: number;
464+
endLine: number;
465+
}
466+
467+
function replacementBounds(edit: HashlineEdit): ReplacementBounds | undefined {
468+
switch (edit.op) {
469+
case "replace_line":
470+
return { startLine: edit.pos.line, endLine: edit.pos.line };
471+
case "replace_range":
472+
return { startLine: edit.pos.line, endLine: edit.end.line };
473+
default:
474+
return undefined;
475+
}
476+
}
477+
478+
function isReplacementEdit(edit: HashlineEdit): edit is ReplacementHashlineEdit {
479+
return edit.op === "replace_line" || edit.op === "replace_range";
480+
}
481+
482+
function collectProtectedAnchorLines(edits: HashlineEdit[]): Set<number> {
483+
const protectedLines = new Set<number>();
484+
for (const edit of edits) {
485+
switch (edit.op) {
486+
case "replace_line":
487+
case "append_at":
488+
case "prepend_at":
489+
protectedLines.add(edit.pos.line);
490+
break;
491+
case "replace_range":
492+
protectedLines.add(edit.pos.line);
493+
protectedLines.add(edit.end.line);
494+
break;
495+
case "append_file":
496+
case "prepend_file":
497+
break;
498+
}
499+
}
500+
return protectedLines;
501+
}
502+
503+
function hasProtectedAnchorInRange(protectedLines: Set<number>, startLine: number, endLine: number): boolean {
504+
for (let line = startLine; line <= endLine; line++) {
505+
if (protectedLines.has(line)) return true;
506+
}
507+
return false;
508+
}
509+
510+
function countDuplicatePrefix(
511+
insertedLines: string[],
512+
startLine: number,
513+
originalFileLines: string[],
514+
protectedLines: Set<number>,
515+
): number {
516+
const maxCount = Math.min(insertedLines.length - 1, startLine - 1);
517+
for (let count = maxCount; count >= 2; count--) {
518+
const sourceStartLine = startLine - count;
519+
const sourceEndLine = startLine - 1;
520+
if (hasProtectedAnchorInRange(protectedLines, sourceStartLine, sourceEndLine)) continue;
521+
const sourceLines = originalFileLines.slice(sourceStartLine - 1, sourceEndLine);
522+
if (sourceLines.every((line, index) => line === insertedLines[index])) return count;
523+
}
524+
return 0;
525+
}
526+
527+
function countDuplicateSuffix(
528+
insertedLines: string[],
529+
endLine: number,
530+
originalFileLines: string[],
531+
protectedLines: Set<number>,
532+
): number {
533+
const maxCount = Math.min(insertedLines.length - 1, originalFileLines.length - endLine);
534+
for (let count = maxCount; count >= 2; count--) {
535+
const sourceStartLine = endLine + 1;
536+
const sourceEndLine = endLine + count;
537+
if (hasProtectedAnchorInRange(protectedLines, sourceStartLine, sourceEndLine)) continue;
538+
const sourceLines = originalFileLines.slice(sourceStartLine - 1, sourceEndLine);
539+
const insertedStart = insertedLines.length - count;
540+
if (sourceLines.every((line, index) => line === insertedLines[insertedStart + index])) return count;
541+
}
542+
return 0;
543+
}
544+
545+
function absorbDuplicateReplacementBoundaries(
546+
edits: HashlineEdit[],
547+
originalFileLines: string[],
548+
warnings: string[],
549+
): void {
550+
const protectedLines = collectProtectedAnchorLines(edits);
551+
for (const edit of edits) {
552+
if (!isReplacementEdit(edit)) continue;
553+
const bounds = replacementBounds(edit);
554+
if (!bounds || edit.lines.length < 3) continue;
555+
556+
const prefixCount = countDuplicatePrefix(edit.lines, bounds.startLine, originalFileLines, protectedLines);
557+
if (prefixCount >= 2) {
558+
edit.lines = edit.lines.slice(prefixCount);
559+
const tag = formatLineTag(
560+
bounds.startLine - prefixCount,
561+
originalFileLines[bounds.startLine - prefixCount - 1],
562+
);
563+
warnings.push(
564+
`Auto-absorbed ${prefixCount} duplicate line(s) above replacement at ${tag}; replacement content repeated unchanged boundary lines immediately before the target.`,
565+
);
566+
}
567+
568+
if (edit.lines.length < 3) continue;
569+
const suffixCount = countDuplicateSuffix(edit.lines, bounds.endLine, originalFileLines, protectedLines);
570+
if (suffixCount >= 2) {
571+
edit.lines = edit.lines.slice(0, -suffixCount);
572+
const tag = formatLineTag(bounds.endLine + 1, originalFileLines[bounds.endLine]);
573+
warnings.push(
574+
`Auto-absorbed ${suffixCount} duplicate line(s) below replacement at ${tag}; replacement content repeated unchanged boundary lines immediately after the target.`,
575+
);
576+
}
577+
}
578+
}
457579
// ═══════════════════════════════════════════════════════════════════════════
458580
// Edit Application
459581
// ═══════════════════════════════════════════════════════════════════════════
@@ -539,6 +661,7 @@ export function applyHashlineEdits(
539661
}
540662
maybeAutocorrectEscapedTabIndentation(edits, warnings);
541663
maybeWarnSuspiciousUnicodeEscapePlaceholder(edits, warnings);
664+
absorbDuplicateReplacementBoundaries(edits, originalFileLines, warnings);
542665

543666
// Warn when a replace_range/replace_line's last inserted line duplicates the next surviving line.
544667
// This catches the common boundary-overreach pattern where the agent includes a closing delimiter
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Searches GitHub code through the local GitHub CLI.
2+
3+
<instruction>
4+
- `query` supports normal GitHub code search syntax
5+
- Use `repo` to scope results to a single repository when relevant
6+
- Prefer small limits and refine the query instead of pulling large result sets
7+
- Use this for upstream curation, API discovery, and locating implementation examples across GitHub
8+
</instruction>
9+
10+
<output>
11+
Returns a concise list of matching files with repository, commit SHA, URL, and the first matched fragment when available.
12+
</output>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Searches GitHub commits through the local GitHub CLI.
2+
3+
<instruction>
4+
- `query` supports normal GitHub commit search syntax
5+
- Use `repo` to scope results to a single repository when relevant
6+
- Prefer small limits and refine the query instead of pulling large result sets
7+
- Use this for upstream curation, release archaeology, and finding fixes by topic
8+
</instruction>
9+
10+
<output>
11+
Returns a concise list of matching commits with repository, short SHA, subject, author, date, and URL.
12+
</output>

0 commit comments

Comments
 (0)