Compare commits
10 Commits
v3.1.11
...
omo-ultraw
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
682e11f126 | ||
|
|
616ee7805c | ||
|
|
aed5c33ae3 | ||
|
|
18601c5c70 | ||
|
|
a013e10d44 | ||
|
|
cc7732881a | ||
|
|
af91b5b662 | ||
|
|
862675c230 | ||
|
|
d7713ca8be | ||
|
|
3fb6edb269 |
1
.github/workflows/publish.yml
vendored
@@ -51,6 +51,7 @@ jobs:
|
||||
# Run them in separate processes to prevent cross-file contamination
|
||||
bun test src/plugin-handlers
|
||||
bun test src/hooks/atlas
|
||||
bun test src/hooks/compaction-context-injector
|
||||
bun test src/features/tmux-subagent
|
||||
|
||||
- name: Run remaining tests
|
||||
|
||||
2
.gitignore
vendored
@@ -33,4 +33,4 @@ yarn.lock
|
||||
test-injection/
|
||||
notepad.md
|
||||
oauth-success.html
|
||||
*.bun-build
|
||||
.188e87dbff6e7fd9-00000000.bun-build
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: Compare HEAD with the latest published npm version and list all unpublished changes
|
||||
model: anthropic/claude-haiku-4-5
|
||||
---
|
||||
|
||||
<command-instruction>
|
||||
@@ -81,68 +82,3 @@ None 또는 목록
|
||||
- **Recommendation**: patch|minor|major
|
||||
- **Reason**: 이유
|
||||
</output-format>
|
||||
|
||||
<oracle-safety-review>
|
||||
## Oracle 배포 안전성 검토 (사용자가 명시적으로 요청 시에만)
|
||||
|
||||
**트리거 키워드**: "배포 가능", "배포해도 될까", "안전한지", "리뷰", "검토", "oracle", "오라클"
|
||||
|
||||
사용자가 위 키워드 중 하나라도 포함하여 요청하면:
|
||||
|
||||
### 1. 사전 검증 실행
|
||||
```bash
|
||||
bun run typecheck
|
||||
bun test
|
||||
```
|
||||
- 실패 시 → Oracle 소환 없이 즉시 "❌ 배포 불가" 보고
|
||||
|
||||
### 2. Oracle 소환 프롬프트
|
||||
|
||||
다음 정보를 수집하여 Oracle에게 전달:
|
||||
|
||||
```
|
||||
## 배포 안전성 검토 요청
|
||||
|
||||
### 변경사항 요약
|
||||
{위에서 분석한 변경사항 테이블}
|
||||
|
||||
### 주요 diff (기능별로 정리)
|
||||
{각 feat/fix/refactor의 핵심 코드 변경 - 전체 diff가 아닌 핵심만}
|
||||
|
||||
### 검증 결과
|
||||
- Typecheck: ✅/❌
|
||||
- Tests: {pass}/{total} (✅/❌)
|
||||
|
||||
### 검토 요청사항
|
||||
1. **리그레션 위험**: 기존 기능에 영향을 줄 수 있는 변경이 있는가?
|
||||
2. **사이드이펙트**: 예상치 못한 부작용이 발생할 수 있는 부분은?
|
||||
3. **Breaking Changes**: 외부 사용자에게 영향을 주는 변경이 있는가?
|
||||
4. **Edge Cases**: 놓친 엣지 케이스가 있는가?
|
||||
5. **배포 권장 여부**: SAFE / CAUTION / UNSAFE
|
||||
|
||||
### 요청
|
||||
위 변경사항을 깊이 분석하고, 배포 안전성에 대해 판단해주세요.
|
||||
리스크가 있다면 구체적인 시나리오와 함께 설명해주세요.
|
||||
배포 후 모니터링해야 할 키워드가 있다면 제안해주세요.
|
||||
```
|
||||
|
||||
### 3. Oracle 응답 후 출력 포맷
|
||||
|
||||
## 🔍 Oracle 배포 안전성 검토 결과
|
||||
|
||||
### 판정: ✅ SAFE / ⚠️ CAUTION / ❌ UNSAFE
|
||||
|
||||
### 리스크 분석
|
||||
| 영역 | 리스크 레벨 | 설명 |
|
||||
|------|-------------|------|
|
||||
| ... | 🟢/🟡/🔴 | ... |
|
||||
|
||||
### 권장 사항
|
||||
- ...
|
||||
|
||||
### 배포 후 모니터링 키워드
|
||||
- ...
|
||||
|
||||
### 결론
|
||||
{Oracle의 최종 판단}
|
||||
</oracle-safety-review>
|
||||
|
||||
@@ -1,519 +0,0 @@
|
||||
---
|
||||
name: github-issue-triage
|
||||
description: "Triage GitHub issues with parallel analysis. 1 issue = 1 background agent. Exhaustive pagination. Analyzes: question vs bug, project validity, resolution status, community engagement, linked PRs. Triggers: 'triage issues', 'analyze issues', 'issue report'."
|
||||
---
|
||||
|
||||
# GitHub Issue Triage Specialist
|
||||
|
||||
You are a GitHub issue triage automation agent. Your job is to:
|
||||
1. Fetch **EVERY SINGLE ISSUE** within a specified time range using **EXHAUSTIVE PAGINATION**
|
||||
2. Launch ONE background agent PER issue for parallel analysis
|
||||
3. Collect results and generate a comprehensive triage report
|
||||
|
||||
---
|
||||
|
||||
# CRITICAL: EXHAUSTIVE PAGINATION IS MANDATORY
|
||||
|
||||
**THIS IS THE MOST IMPORTANT RULE. VIOLATION = COMPLETE FAILURE.**
|
||||
|
||||
## YOU MUST FETCH ALL ISSUES. PERIOD.
|
||||
|
||||
| WRONG | CORRECT |
|
||||
|----------|------------|
|
||||
| `gh issue list --limit 100` and stop | Paginate until ZERO results returned |
|
||||
| "I found 16 issues" (first page only) | "I found 61 issues after 5 pages" |
|
||||
| Assuming first page is enough | Using `--limit 500` and verifying count |
|
||||
| Stopping when you "feel" you have enough | Stopping ONLY when API returns empty |
|
||||
|
||||
### WHY THIS MATTERS
|
||||
|
||||
- GitHub API returns **max 100 issues per request** by default
|
||||
- A busy repo can have **50-100+ issues** in 48 hours
|
||||
- **MISSING ISSUES = MISSING CRITICAL BUGS = PRODUCTION OUTAGES**
|
||||
- The user asked for triage, not "sample triage"
|
||||
|
||||
### THE ONLY ACCEPTABLE APPROACH
|
||||
|
||||
```bash
|
||||
# ALWAYS use --limit 500 (maximum allowed)
|
||||
# ALWAYS check if more pages exist
|
||||
# ALWAYS continue until empty result
|
||||
|
||||
gh issue list --repo $REPO --state all --limit 500 --json number,title,state,createdAt,updatedAt,labels,author
|
||||
```
|
||||
|
||||
**If the result count equals your limit, THERE ARE MORE ISSUES. KEEP FETCHING.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1: Issue Collection (EXHAUSTIVE Pagination)
|
||||
|
||||
### 1.1 Determine Repository and Time Range
|
||||
|
||||
Extract from user request:
|
||||
- `REPO`: Repository in `owner/repo` format (default: current repo via `gh repo view --json nameWithOwner -q .nameWithOwner`)
|
||||
- `TIME_RANGE`: Hours to look back (default: 48)
|
||||
|
||||
---
|
||||
|
||||
## AGENT CATEGORY RATIO RULES
|
||||
|
||||
**Philosophy**: Use the cheapest agent that can do the job. Expensive agents = waste unless necessary.
|
||||
|
||||
### Default Ratio: `unspecified-low:8, quick:1, writing:1`
|
||||
|
||||
| Category | Ratio | Use For | Cost |
|
||||
|----------|-------|---------|------|
|
||||
| `unspecified-low` | 80% | Standard issue analysis - read issue, fetch comments, categorize | $ |
|
||||
| `quick` | 10% | Trivial issues - obvious duplicates, spam, clearly resolved | ¢ |
|
||||
| `writing` | 10% | Report generation, response drafting, summary synthesis | $$ |
|
||||
|
||||
### When to Override Default Ratio
|
||||
|
||||
| Scenario | Recommended Ratio | Reason |
|
||||
|----------|-------------------|--------|
|
||||
| Bug-heavy triage | `unspecified-low:7, quick:2, writing:1` | More simple duplicates |
|
||||
| Feature request triage | `unspecified-low:6, writing:3, quick:1` | More response drafting needed |
|
||||
| Security audit | `unspecified-high:5, unspecified-low:4, writing:1` | Deeper analysis required |
|
||||
| First-pass quick filter | `quick:8, unspecified-low:2` | Just categorize, don't analyze deeply |
|
||||
|
||||
### Agent Assignment Algorithm
|
||||
|
||||
```typescript
|
||||
function assignAgentCategory(issues: Issue[], ratio: Record<string, number>): Map<Issue, string> {
|
||||
const assignments = new Map<Issue, string>();
|
||||
const total = Object.values(ratio).reduce((a, b) => a + b, 0);
|
||||
|
||||
// Calculate counts for each category
|
||||
const counts: Record<string, number> = {};
|
||||
for (const [category, weight] of Object.entries(ratio)) {
|
||||
counts[category] = Math.floor(issues.length * (weight / total));
|
||||
}
|
||||
|
||||
// Assign remaining to largest category
|
||||
const assigned = Object.values(counts).reduce((a, b) => a + b, 0);
|
||||
const remaining = issues.length - assigned;
|
||||
const largestCategory = Object.entries(ratio).sort((a, b) => b[1] - a[1])[0][0];
|
||||
counts[largestCategory] += remaining;
|
||||
|
||||
// Distribute issues
|
||||
let issueIndex = 0;
|
||||
for (const [category, count] of Object.entries(counts)) {
|
||||
for (let i = 0; i < count && issueIndex < issues.length; i++) {
|
||||
assignments.set(issues[issueIndex++], category);
|
||||
}
|
||||
}
|
||||
|
||||
return assignments;
|
||||
}
|
||||
```
|
||||
|
||||
### Category Selection Heuristics
|
||||
|
||||
**Before launching agents, pre-classify issues for smarter category assignment:**
|
||||
|
||||
| Issue Signal | Assign To | Reason |
|
||||
|--------------|-----------|--------|
|
||||
| Has `duplicate` label | `quick` | Just confirm and close |
|
||||
| Has `wontfix` label | `quick` | Just confirm and close |
|
||||
| No comments, < 50 char body | `quick` | Likely spam or incomplete |
|
||||
| Has linked PR | `quick` | Already being addressed |
|
||||
| Has `bug` label + long body | `unspecified-low` | Needs proper analysis |
|
||||
| Has `feature` label | `unspecified-low` or `writing` | May need response |
|
||||
| User is maintainer | `quick` | They know what they're doing |
|
||||
| 5+ comments | `unspecified-low` | Complex discussion |
|
||||
| Needs response drafted | `writing` | Prose quality matters |
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Exhaustive Pagination Loop
|
||||
|
||||
# STOP. READ THIS BEFORE EXECUTING.
|
||||
|
||||
**YOU WILL FETCH EVERY. SINGLE. ISSUE. NO EXCEPTIONS.**
|
||||
|
||||
## THE GOLDEN RULE
|
||||
|
||||
```
|
||||
NEVER use --limit 100. ALWAYS use --limit 500.
|
||||
NEVER stop at first result. ALWAYS verify you got everything.
|
||||
NEVER assume "that's probably all". ALWAYS check if more exist.
|
||||
```
|
||||
|
||||
## MANDATORY PAGINATION LOOP (COPY-PASTE THIS EXACTLY)
|
||||
|
||||
You MUST execute this EXACT pagination loop. DO NOT simplify. DO NOT skip iterations.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# MANDATORY PAGINATION - Execute this EXACTLY as written
|
||||
|
||||
REPO="code-yeongyu/oh-my-opencode" # or use: gh repo view --json nameWithOwner -q .nameWithOwner
|
||||
TIME_RANGE=48 # hours
|
||||
CUTOFF_DATE=$(date -v-${TIME_RANGE}H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -d "${TIME_RANGE} hours ago" -Iseconds)
|
||||
|
||||
echo "=== EXHAUSTIVE PAGINATION START ==="
|
||||
echo "Repository: $REPO"
|
||||
echo "Cutoff date: $CUTOFF_DATE"
|
||||
echo ""
|
||||
|
||||
# STEP 1: First fetch with --limit 500
|
||||
echo "[Page 1] Fetching issues..."
|
||||
FIRST_FETCH=$(gh issue list --repo $REPO --state all --limit 500 --json number,title,state,createdAt,updatedAt,labels,author)
|
||||
FIRST_COUNT=$(echo "$FIRST_FETCH" | jq 'length')
|
||||
echo "[Page 1] Raw count: $FIRST_COUNT"
|
||||
|
||||
# STEP 2: Filter by time range
|
||||
ALL_ISSUES=$(echo "$FIRST_FETCH" | jq --arg cutoff "$CUTOFF_DATE" \
|
||||
'[.[] | select(.createdAt >= $cutoff or .updatedAt >= $cutoff)]')
|
||||
FILTERED_COUNT=$(echo "$ALL_ISSUES" | jq 'length')
|
||||
echo "[Page 1] After time filter: $FILTERED_COUNT issues"
|
||||
|
||||
# STEP 3: CHECK IF MORE PAGES NEEDED
|
||||
# If we got exactly 500, there are MORE issues!
|
||||
if [ "$FIRST_COUNT" -eq 500 ]; then
|
||||
echo ""
|
||||
echo "WARNING: Got exactly 500 results. MORE PAGES EXIST!"
|
||||
echo "Continuing pagination..."
|
||||
|
||||
PAGE=2
|
||||
LAST_ISSUE_NUMBER=$(echo "$FIRST_FETCH" | jq '.[- 1].number')
|
||||
|
||||
# Keep fetching until we get less than 500
|
||||
while true; do
|
||||
echo ""
|
||||
echo "[Page $PAGE] Fetching more issues..."
|
||||
|
||||
# Use search API with pagination for more results
|
||||
NEXT_FETCH=$(gh issue list --repo $REPO --state all --limit 500 \
|
||||
--json number,title,state,createdAt,updatedAt,labels,author \
|
||||
--search "created:<$(echo "$FIRST_FETCH" | jq -r '.[-1].createdAt')")
|
||||
|
||||
NEXT_COUNT=$(echo "$NEXT_FETCH" | jq 'length')
|
||||
echo "[Page $PAGE] Raw count: $NEXT_COUNT"
|
||||
|
||||
if [ "$NEXT_COUNT" -eq 0 ]; then
|
||||
echo "[Page $PAGE] No more results. Pagination complete."
|
||||
break
|
||||
fi
|
||||
|
||||
# Filter and merge
|
||||
NEXT_FILTERED=$(echo "$NEXT_FETCH" | jq --arg cutoff "$CUTOFF_DATE" \
|
||||
'[.[] | select(.createdAt >= $cutoff or .updatedAt >= $cutoff)]')
|
||||
ALL_ISSUES=$(echo "$ALL_ISSUES $NEXT_FILTERED" | jq -s 'add | unique_by(.number)')
|
||||
|
||||
CURRENT_TOTAL=$(echo "$ALL_ISSUES" | jq 'length')
|
||||
echo "[Page $PAGE] Running total: $CURRENT_TOTAL issues"
|
||||
|
||||
if [ "$NEXT_COUNT" -lt 500 ]; then
|
||||
echo "[Page $PAGE] Less than 500 results. Pagination complete."
|
||||
break
|
||||
fi
|
||||
|
||||
PAGE=$((PAGE + 1))
|
||||
|
||||
# Safety limit
|
||||
if [ $PAGE -gt 20 ]; then
|
||||
echo "SAFETY LIMIT: Stopped at page 20"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# STEP 4: FINAL COUNT
|
||||
FINAL_COUNT=$(echo "$ALL_ISSUES" | jq 'length')
|
||||
echo ""
|
||||
echo "=== EXHAUSTIVE PAGINATION COMPLETE ==="
|
||||
echo "Total issues found: $FINAL_COUNT"
|
||||
echo ""
|
||||
|
||||
# STEP 5: Verify we got everything
|
||||
if [ "$FINAL_COUNT" -lt 10 ]; then
|
||||
echo "WARNING: Only $FINAL_COUNT issues found. Double-check time range!"
|
||||
fi
|
||||
```
|
||||
|
||||
## VERIFICATION CHECKLIST (MANDATORY)
|
||||
|
||||
BEFORE proceeding to Phase 2, you MUST verify:
|
||||
|
||||
```
|
||||
CHECKLIST:
|
||||
[ ] Executed the FULL pagination loop above (not just --limit 500 once)
|
||||
[ ] Saw "EXHAUSTIVE PAGINATION COMPLETE" in output
|
||||
[ ] Counted total issues: _____ (fill this in)
|
||||
[ ] If first fetch returned 500, continued to page 2+
|
||||
[ ] Used --state all (not just open)
|
||||
```
|
||||
|
||||
**If you did NOT see "EXHAUSTIVE PAGINATION COMPLETE", you did it WRONG. Start over.**
|
||||
|
||||
## ANTI-PATTERNS (WILL CAUSE FAILURE)
|
||||
|
||||
| NEVER DO THIS | Why It Fails |
|
||||
|------------------|--------------|
|
||||
| Single `gh issue list --limit 500` | If 500 returned, you missed the rest! |
|
||||
| `--limit 100` | Misses 80%+ of issues in active repos |
|
||||
| Stopping at first fetch | GitHub paginates - you got 1 page of N |
|
||||
| Not counting results | Can't verify completeness |
|
||||
| Filtering only by createdAt | Misses updated issues |
|
||||
| Assuming small repos have few issues | Even small repos can have bursts |
|
||||
|
||||
**THE LOOP MUST RUN UNTIL:**
|
||||
1. Fetch returns 0 results, OR
|
||||
2. Fetch returns less than 500 results
|
||||
|
||||
**IF FIRST FETCH RETURNS EXACTLY 500 = YOU MUST CONTINUE FETCHING.**
|
||||
|
||||
### 1.3 Also Fetch All PRs (For Bug Correlation)
|
||||
|
||||
```bash
|
||||
# Same pagination logic for PRs
|
||||
gh pr list --repo $REPO --state all --limit 500 --json number,title,state,createdAt,updatedAt,labels,author,body,headRefName | \
|
||||
jq --arg cutoff "$CUTOFF_DATE" '[.[] | select(.createdAt >= $cutoff or .updatedAt >= $cutoff)]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2: Parallel Issue Analysis (1 Issue = 1 Agent)
|
||||
|
||||
### 2.1 Agent Distribution Formula
|
||||
|
||||
```
|
||||
Total issues: N
|
||||
Agent categories based on ratio:
|
||||
- unspecified-low: floor(N * 0.8)
|
||||
- quick: floor(N * 0.1)
|
||||
- writing: ceil(N * 0.1) # For report generation
|
||||
```
|
||||
|
||||
### 2.2 Launch Background Agents
|
||||
|
||||
**MANDATORY: Each issue gets its own dedicated background agent.**
|
||||
|
||||
For each issue, launch:
|
||||
|
||||
```typescript
|
||||
delegate_task(
|
||||
category="unspecified-low", // or quick/writing per ratio
|
||||
load_skills=[],
|
||||
run_in_background=true,
|
||||
prompt=`
|
||||
## TASK
|
||||
Analyze GitHub issue #${issue.number} for ${REPO}.
|
||||
|
||||
## ISSUE DATA
|
||||
- Number: #${issue.number}
|
||||
- Title: ${issue.title}
|
||||
- State: ${issue.state}
|
||||
- Author: ${issue.author.login}
|
||||
- Created: ${issue.createdAt}
|
||||
- Updated: ${issue.updatedAt}
|
||||
- Labels: ${issue.labels.map(l => l.name).join(', ')}
|
||||
|
||||
## ISSUE BODY
|
||||
${issue.body}
|
||||
|
||||
## FETCH COMMENTS
|
||||
Use: gh issue view ${issue.number} --repo ${REPO} --json comments
|
||||
|
||||
## ANALYSIS CHECKLIST
|
||||
1. **TYPE**: Is this a BUG, QUESTION, FEATURE request, or INVALID?
|
||||
2. **PROJECT_VALID**: Is this issue relevant to OUR project? (YES/NO/UNCLEAR)
|
||||
3. **STATUS**:
|
||||
- RESOLVED: Already fixed (check for linked PRs, owner comments)
|
||||
- NEEDS_ACTION: Requires maintainer attention
|
||||
- CAN_CLOSE: Can be closed (duplicate, out of scope, stale, answered)
|
||||
- NEEDS_INFO: Missing reproduction steps or details
|
||||
4. **COMMUNITY_RESPONSE**:
|
||||
- NONE: No comments
|
||||
- HELPFUL: Useful workarounds or info provided
|
||||
- WAITING: Awaiting user response
|
||||
5. **LINKED_PR**: If bug, search PRs that might fix this issue
|
||||
|
||||
## PR CORRELATION
|
||||
Check these PRs for potential fixes:
|
||||
${PR_LIST}
|
||||
|
||||
## RETURN FORMAT
|
||||
\`\`\`
|
||||
#${issue.number}: ${issue.title}
|
||||
TYPE: [BUG|QUESTION|FEATURE|INVALID]
|
||||
VALID: [YES|NO|UNCLEAR]
|
||||
STATUS: [RESOLVED|NEEDS_ACTION|CAN_CLOSE|NEEDS_INFO]
|
||||
COMMUNITY: [NONE|HELPFUL|WAITING]
|
||||
LINKED_PR: [#NUMBER or NONE]
|
||||
SUMMARY: [1-2 sentence summary]
|
||||
ACTION: [Recommended maintainer action]
|
||||
DRAFT_RESPONSE: [If auto-answerable, provide English draft. Otherwise "NEEDS_MANUAL_REVIEW"]
|
||||
\`\`\`
|
||||
`
|
||||
)
|
||||
```
|
||||
|
||||
### 2.3 Collect All Results
|
||||
|
||||
Wait for all background agents to complete, then collect:
|
||||
|
||||
```typescript
|
||||
// Store all task IDs
|
||||
const taskIds: string[] = []
|
||||
|
||||
// Launch all agents
|
||||
for (const issue of issues) {
|
||||
const result = await delegate_task(...)
|
||||
taskIds.push(result.task_id)
|
||||
}
|
||||
|
||||
// Collect results
|
||||
const results = []
|
||||
for (const taskId of taskIds) {
|
||||
const output = await background_output(task_id=taskId)
|
||||
results.push(output)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3: Report Generation
|
||||
|
||||
### 3.1 Categorize Results
|
||||
|
||||
Group analyzed issues by status:
|
||||
|
||||
| Category | Criteria |
|
||||
|----------|----------|
|
||||
| **CRITICAL** | Blocking bugs, security issues, data loss |
|
||||
| **CLOSE_IMMEDIATELY** | Resolved, duplicate, out of scope, stale |
|
||||
| **AUTO_RESPOND** | Can answer with template (version update, docs link) |
|
||||
| **NEEDS_INVESTIGATION** | Requires manual debugging or design decision |
|
||||
| **FEATURE_BACKLOG** | Feature requests for prioritization |
|
||||
| **NEEDS_INFO** | Missing details, request more info |
|
||||
|
||||
### 3.2 Generate Report
|
||||
|
||||
```markdown
|
||||
# Issue Triage Report
|
||||
|
||||
**Repository:** ${REPO}
|
||||
**Time Range:** Last ${TIME_RANGE} hours
|
||||
**Generated:** ${new Date().toISOString()}
|
||||
**Total Issues Analyzed:** ${issues.length}
|
||||
|
||||
## Summary
|
||||
|
||||
| Category | Count |
|
||||
|----------|-------|
|
||||
| CRITICAL | N |
|
||||
| Close Immediately | N |
|
||||
| Auto-Respond | N |
|
||||
| Needs Investigation | N |
|
||||
| Feature Requests | N |
|
||||
| Needs Info | N |
|
||||
|
||||
---
|
||||
|
||||
## 1. CRITICAL (Immediate Action Required)
|
||||
|
||||
[List issues with full details]
|
||||
|
||||
## 2. Close Immediately
|
||||
|
||||
[List with closing reason and template response]
|
||||
|
||||
## 3. Auto-Respond (Template Answers)
|
||||
|
||||
[List with draft responses ready to post]
|
||||
|
||||
## 4. Needs Investigation
|
||||
|
||||
[List with investigation notes]
|
||||
|
||||
## 5. Feature Backlog
|
||||
|
||||
[List for prioritization]
|
||||
|
||||
## 6. Needs More Info
|
||||
|
||||
[List with template questions to ask]
|
||||
|
||||
---
|
||||
|
||||
## Response Templates
|
||||
|
||||
### Fixed in Version X
|
||||
\`\`\`
|
||||
This issue was resolved in vX.Y.Z via PR #NNN.
|
||||
Please update: \`bunx oh-my-opencode@X.Y.Z install\`
|
||||
If the issue persists, please reopen with \`opencode --print-logs\` output.
|
||||
\`\`\`
|
||||
|
||||
### Needs More Info
|
||||
\`\`\`
|
||||
Thank you for reporting. To investigate, please provide:
|
||||
1. \`opencode --print-logs\` output
|
||||
2. Your configuration file
|
||||
3. Minimal reproduction steps
|
||||
Labeling as \`needs-info\`. Auto-closes in 7 days without response.
|
||||
\`\`\`
|
||||
|
||||
### Out of Scope
|
||||
\`\`\`
|
||||
Thank you for reaching out. This request falls outside the scope of this project.
|
||||
[Suggest alternative or explanation]
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ANTI-PATTERNS (BLOCKING VIOLATIONS)
|
||||
|
||||
## IF YOU DO ANY OF THESE, THE TRIAGE IS INVALID
|
||||
|
||||
| Violation | Why It's Wrong | Severity |
|
||||
|-----------|----------------|----------|
|
||||
| **Using `--limit 100`** | Misses 80%+ of issues in active repos | CRITICAL |
|
||||
| **Stopping at first fetch** | GitHub paginates - you only got page 1 | CRITICAL |
|
||||
| **Not counting results** | Can't verify completeness | CRITICAL |
|
||||
| Batching issues (7 per agent) | Loses detail, harder to track | HIGH |
|
||||
| Sequential agent calls | Slow, doesn't leverage parallelism | HIGH |
|
||||
| Skipping PR correlation | Misses linked fixes for bugs | MEDIUM |
|
||||
| Generic responses | Each issue needs specific analysis | MEDIUM |
|
||||
|
||||
## MANDATORY VERIFICATION BEFORE PHASE 2
|
||||
|
||||
```
|
||||
CHECKLIST:
|
||||
[ ] Used --limit 500 (not 100)
|
||||
[ ] Used --state all (not just open)
|
||||
[ ] Counted issues: _____ total
|
||||
[ ] Verified: if count < 500, all issues fetched
|
||||
[ ] If count = 500, fetched additional pages
|
||||
```
|
||||
|
||||
**DO NOT PROCEED TO PHASE 2 UNTIL ALL BOXES ARE CHECKED.**
|
||||
|
||||
---
|
||||
|
||||
## EXECUTION CHECKLIST
|
||||
|
||||
- [ ] Fetched ALL pages of issues (pagination complete)
|
||||
- [ ] Fetched ALL pages of PRs for correlation
|
||||
- [ ] Launched 1 agent per issue (not batched)
|
||||
- [ ] All agents ran in background (parallel)
|
||||
- [ ] Collected all results before generating report
|
||||
- [ ] Report includes draft responses where applicable
|
||||
- [ ] Critical issues flagged at top
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
When invoked, immediately:
|
||||
|
||||
1. `gh repo view --json nameWithOwner -q .nameWithOwner` (get current repo)
|
||||
2. Parse user's time range request (default: 48 hours)
|
||||
3. Exhaustive pagination for issues AND PRs
|
||||
4. Launch N background agents (1 per issue)
|
||||
5. Collect all results
|
||||
6. Generate categorized report with action items
|
||||
@@ -80,8 +80,7 @@
|
||||
"prometheus-md-only",
|
||||
"sisyphus-junior-notepad",
|
||||
"start-work",
|
||||
"atlas",
|
||||
"stop-continuation-guard"
|
||||
"atlas"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -782,7 +781,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpenCode-Builder": {
|
||||
"opencode-builder": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
|
||||
92
bun.lock
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "oh-my-opencode",
|
||||
@@ -28,13 +28,13 @@
|
||||
"typescript": "^5.7.3",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.1.10",
|
||||
"oh-my-opencode-darwin-x64": "3.1.10",
|
||||
"oh-my-opencode-linux-arm64": "3.1.10",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.1.10",
|
||||
"oh-my-opencode-linux-x64": "3.1.10",
|
||||
"oh-my-opencode-linux-x64-musl": "3.1.10",
|
||||
"oh-my-opencode-windows-x64": "3.1.10",
|
||||
"oh-my-opencode-darwin-arm64": "3.1.6",
|
||||
"oh-my-opencode-darwin-x64": "3.1.6",
|
||||
"oh-my-opencode-linux-arm64": "3.1.6",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.1.6",
|
||||
"oh-my-opencode-linux-x64": "3.1.6",
|
||||
"oh-my-opencode-linux-x64-musl": "3.1.6",
|
||||
"oh-my-opencode-windows-x64": "3.1.6",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -44,41 +44,41 @@
|
||||
"@code-yeongyu/comment-checker",
|
||||
],
|
||||
"packages": {
|
||||
"@ast-grep/cli": ["@ast-grep/cli@0.40.5", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.40.5", "@ast-grep/cli-darwin-x64": "0.40.5", "@ast-grep/cli-linux-arm64-gnu": "0.40.5", "@ast-grep/cli-linux-x64-gnu": "0.40.5", "@ast-grep/cli-win32-arm64-msvc": "0.40.5", "@ast-grep/cli-win32-ia32-msvc": "0.40.5", "@ast-grep/cli-win32-x64-msvc": "0.40.5" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-yVXL7Gz0WIHerQLf+MVaVSkhIhidtWReG5akNVr/JS9OVCVkSdz7gWm7H8jVv2M9OO1tauuG76K3UaRGBPu5lQ=="],
|
||||
"@ast-grep/cli": ["@ast-grep/cli@0.40.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.40.0", "@ast-grep/cli-darwin-x64": "0.40.0", "@ast-grep/cli-linux-arm64-gnu": "0.40.0", "@ast-grep/cli-linux-x64-gnu": "0.40.0", "@ast-grep/cli-win32-arm64-msvc": "0.40.0", "@ast-grep/cli-win32-ia32-msvc": "0.40.0", "@ast-grep/cli-win32-x64-msvc": "0.40.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-L8AkflsfI2ZP70yIdrwqvjR02ScCuRmM/qNGnJWUkOFck+e6gafNVJ4e4jjGQlEul+dNdBpx36+O2Op629t47A=="],
|
||||
|
||||
"@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.40.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-T9CzwJ1GqQhnANdsu6c7iT1akpvTVMK+AZrxnhIPv33Ze5hrXUUkqan+j4wUAukRJDqU7u94EhXLSLD+5tcJ8g=="],
|
||||
"@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UehY2MMUkdJbsriP7NKc6+uojrqPn7d1Cl0em+WAkee7Eij81VdyIjRsRxtZSLh440ZWQBHI3PALZ9RkOO8pKQ=="],
|
||||
|
||||
"@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.40.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-ez9b2zKvXU8f4ghhjlqYvbx6tWCKJTuVlNVqDDfjqwwhGeiTYfnzMlSVat4ElYRMd21gLtXZIMy055v2f21Ztg=="],
|
||||
"@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-RFDJ2ZxUbT0+grntNlOLJx7wa9/ciVCeaVtQpQy8WJJTvXvkY0etl8Qlh2TmO2x2yr+i0Z6aMJi4IG/Yx5ghTQ=="],
|
||||
|
||||
"@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-VXa2L1IEYD66AMb0GuG7VlMMbPmEGoJUySWDcwSZo/D9neiry3MJ41LQR5oTG2HyhIPBsf9umrXnmuRq66BviA=="],
|
||||
"@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4p55gnTQ1mMFCyqjtM7bH9SB9r16mkwXtUcJQGX1YgFG4WD+QG8rC4GwSuNNZcdlYaOQuTWrgUEQ9z5K06UXfg=="],
|
||||
|
||||
"@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-GQC5162eIOWXR2eQQ6Knzg7/8Trp5E1ODJkaErf0IubdQrZBGqj5AAcQPcWgPbbnmktjIp0H4NraPpOJ9eJ22A=="],
|
||||
"@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-u2MXFceuwvrO+OQ6zFGoJ6wbATXn46HWwW79j4UPrXYJzVl97jRyjJOIQTJOzTflsk02fjP98DQkfvbXt2dl3Q=="],
|
||||
|
||||
"@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.40.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-YiZdnQZsSlXQTMsZJop/Ux9MmUGfuRvC2x/UbFgrt5OBSYxND+yoiMc0WcA3WG+wU+tt4ZkB5HUea3r/IkOLYA=="],
|
||||
"@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-E/I1xpF/RQL2fo1CQsQfTxyDLnChsbZ+ERrQHKuF1FI4WrkaPOBibpqda60QgVmUcgOGZyZ/GRb3iKEVWPsQNQ=="],
|
||||
|
||||
"@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.40.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-MHkCxCITVTr8sY9CcVqNKbfUzMa3Hc6IilGXad0Clnw2vNmPfWqSky+hU/UTerr5YHWwWfAVURH7ANZgirtx0Q=="],
|
||||
"@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-9h12OQu1BR0GxHEtT+Z4QkJk3LLWLiKwjBkjXUGlASHYDPTyLcs85KwDLeFHs4BwarF8TDdF+KySvB9WPGl/nQ=="],
|
||||
|
||||
"@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.40.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/MJ5un7yxlClaaxou9eYl+Kr2xr/yTtYtTq5aLBWjPWA6dmmJ1nAJgx5zKHVuplFXFBrFDQk3paEgAETMTGcrA=="],
|
||||
"@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-n2+3WynEWFHhXg6KDgjwWQ0UEtIvqUITFbKEk5cDkUYrzYhg/A6kj0qauPwRbVMoJms49vtsNpLkzzqyunio5g=="],
|
||||
|
||||
"@ast-grep/napi": ["@ast-grep/napi@0.40.5", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.5", "@ast-grep/napi-darwin-x64": "0.40.5", "@ast-grep/napi-linux-arm64-gnu": "0.40.5", "@ast-grep/napi-linux-arm64-musl": "0.40.5", "@ast-grep/napi-linux-x64-gnu": "0.40.5", "@ast-grep/napi-linux-x64-musl": "0.40.5", "@ast-grep/napi-win32-arm64-msvc": "0.40.5", "@ast-grep/napi-win32-ia32-msvc": "0.40.5", "@ast-grep/napi-win32-x64-msvc": "0.40.5" } }, "sha512-hJA62OeBKUQT68DD2gDyhOqJxZxycqg8wLxbqjgqSzYttCMSDL9tiAQ9abgekBYNHudbJosm9sWOEbmCDfpX2A=="],
|
||||
"@ast-grep/napi": ["@ast-grep/napi@0.40.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.0", "@ast-grep/napi-darwin-x64": "0.40.0", "@ast-grep/napi-linux-arm64-gnu": "0.40.0", "@ast-grep/napi-linux-arm64-musl": "0.40.0", "@ast-grep/napi-linux-x64-gnu": "0.40.0", "@ast-grep/napi-linux-x64-musl": "0.40.0", "@ast-grep/napi-win32-arm64-msvc": "0.40.0", "@ast-grep/napi-win32-ia32-msvc": "0.40.0", "@ast-grep/napi-win32-x64-msvc": "0.40.0" } }, "sha512-tq6nO/8KwUF/mHuk1ECaAOSOlz2OB/PmygnvprJzyAHGRVzdcffblaOOWe90M9sGz5MAasXoF+PTcayQj9TKKA=="],
|
||||
|
||||
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2F072fGN0WTq7KI3okuEnkGJVEHLbi56Bw1H6NAMf7j2mJJeQWsRyGOMcyNnUXZDeNdvoMH0OB2a5wwUegY/nQ=="],
|
||||
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZMjl5yLhKjxdwbqEEdMizgQdWH2NrWsM6Px+JuGErgCDe6Aedq9yurEPV7veybGdLVJQhOah6htlSflXxjHnYA=="],
|
||||
|
||||
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.40.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-dJMidHZhhxuLBYNi6/FKI812jQ7wcFPSKkVPwviez2D+KvYagapUMAV/4dJ7FCORfguVk8Y0jpPAlYmWRT5nvA=="],
|
||||
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-f9Ol5oQKNRMBkvDtzBK1WiNn2/3eejF2Pn9xwTj7PhXuSFseedOspPYllxQo0gbwUlw/DJqGFTce/jarhR/rBw=="],
|
||||
|
||||
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-nBRCbyoS87uqkaw4Oyfe5VO+SRm2B+0g0T8ME69Qry9ShMf41a2bTdpcQx9e8scZPogq+CTwDHo3THyBV71l9w=="],
|
||||
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tO+VW5GDhT9jGkKOK+3b8+ohKjC98WTzn7wSskd/myyhK3oYL1WTKqCm07WSYBZOJvb3z+WaX+wOUrc4bvtyQ=="],
|
||||
|
||||
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-/qKsmds5FMoaEj6FdNzepbmLMtlFuBLdrAn9GIWCqOIcVcYvM1Nka8+mncfeXB/MFZKOrzQsQdPTWqrrQzXLrA=="],
|
||||
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-MS9qalLRjUnF2PCzuTKTvCMVSORYHxxe3Qa0+SSaVULsXRBmuy5C/b1FeWwMFnwNnC0uie3VDet31Zujwi8q6A=="],
|
||||
|
||||
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-DP4oDbq7f/1A2hRTFLhJfDFR6aI5mRWdEfKfHzRItmlKsR9WlcEl1qDJs/zX9R2EEtIDsSKRzuJNfJllY3/W8Q=="],
|
||||
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BeHZVMNXhM3WV3XE2yghO0fRxhMOt8BTN972p5piYEQUvKeSHmS8oeGcs6Ahgx5znBclqqqq37ZfioYANiTqJA=="],
|
||||
|
||||
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-BRZUvVBPUNpWPo6Ns8chXVzxHPY+k9gpsubGTHy92Q26ecZULd/dTkWWdnvfhRqttsSQ9Pe/XQdi5+hDQ6RYcg=="],
|
||||
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rG1YujF7O+lszX8fd5u6qkFTuv4FwHXjWvt1CCvCxXwQLSY96LaCW88oVKg7WoEYQh54y++Fk57F+Wh9Gv9nVQ=="],
|
||||
|
||||
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.40.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-y95zSEwc7vhxmcrcH0GnK4ZHEBQrmrszRBNQovzaciF9GUqEcCACNLoBesn4V47IaOp4fYgD2/EhGRTIBFb2Ug=="],
|
||||
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-9SqmnQqd4zTEUk6yx0TuW2ycZZs2+e569O/R0QnhSiQNpgwiJCYOe/yPS0BC9HkiaozQm6jjAcasWpFtz/dp+w=="],
|
||||
|
||||
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.40.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-K/u8De62iUnFCzVUs7FBdTZ2Jrgc5/DLHqjpup66KxZ7GIM9/HGME/O8aSoPkpcAeCD4TiTZ11C1i5p5H98hTg=="],
|
||||
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0JkdBZi5l9vZhGEO38A1way0LmLRDU5Vos6MXrLIOVkymmzDTDlCdY394J1LMmmsfwWcyJg6J7Yv2dw41MCxDQ=="],
|
||||
|
||||
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.5", "", { "os": "win32", "cpu": "x64" }, "sha512-dqm5zg/o4Nh4VOQPEpMS23ot8HVd22gG0eg01t4CFcZeuzyuSgBlOL3N7xLbz3iH2sVkk7keuBwAzOIpTqziNQ=="],
|
||||
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Hk2IwfPqMFGZt5SRxsoWmGLxBXxprow4LRp1eG6V8EEiJCNHxZ9ZiEaIc5bNvMDBjHVSnqZAXT22dROhrcSKQg=="],
|
||||
|
||||
"@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
|
||||
|
||||
@@ -86,17 +86,17 @@
|
||||
|
||||
"@code-yeongyu/comment-checker": ["@code-yeongyu/comment-checker@0.6.1", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "comment-checker": "bin/comment-checker" } }, "sha512-BBremX+Y5aW8sTzlhHrLsKParupYkPOVUYmq9STrlWvBvfAme6w5IWuZCLl6nHIQScRDdvGdrAjPycJC86EZFA=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="],
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="],
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.1.47", "", { "dependencies": { "@opencode-ai/sdk": "1.1.47", "zod": "4.1.8" } }, "sha512-gNMPz72altieDfLhUw3VAT1xbduKi3w3wZ57GLeS7qU9W474HdvdIiLBnt2Xq3U7Ko0/0tvK3nzCker6IIDqmQ=="],
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.1.19", "", { "dependencies": { "@opencode-ai/sdk": "1.1.19", "zod": "4.1.8" } }, "sha512-Q6qBEjHb/dJMEw4BUqQxEswTMxCCHUpFMMb6jR8HTTs8X/28XRkKt5pHNPA82GU65IlSoPRph+zd8LReBDN53Q=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.47", "", {}, "sha512-s3PBHwk1sP6Zt/lJxIWSBWZ1TnrI1nFxSP97LCODUytouAQgbygZ1oDH7O2sGMBEuGdA8B1nNSPla0aRSN3IpA=="],
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.19", "", {}, "sha512-XhZhFuvlLCqDpvNtUEjOsi/wvFj3YCXb1dySp+OONQRMuHlorNYnNa7P2A2ntKuhRdGT1Xt5na0nFzlUyNw+4A=="],
|
||||
|
||||
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||
|
||||
"@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@types/picomatch": ["@types/picomatch@3.0.2", "", {}, "sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA=="],
|
||||
|
||||
@@ -108,9 +108,9 @@
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
"body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
||||
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
@@ -184,11 +184,11 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
|
||||
"hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
"iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
@@ -226,19 +226,19 @@
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.1.10", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-6qsZQtrtBYZLufcXTTuUUMEG9PoG9Y98pX+HFVn2xHIEc6GpwR6i5xY8McFHmqPkC388tzybD556JhKqPX7Pnw=="],
|
||||
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.1.6", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-KK+ptnkBigvDYbRtF/B5izEC4IoXDS8mAnRHWFBSCINhzQR2No6AtEcwijd6vKBPR+/r71ofq/8mTsIeb1PEVQ=="],
|
||||
|
||||
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.1.10", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-I1tQQbcpSBvLGXTO652mBqlyIpwYhYuIlSJmrSM33YRGBiaUuhMASnHQsms+E0eC3U/TOyqomU/4KPnbWyxs4w=="],
|
||||
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.1.6", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-UkPI/RUi7INarFasBUZ4Rous6RUQXsU2nr0V8KFJp+70END43D/96dDUwX+zmPtpDhD+DfWkejuwzqfkZJ2ZDQ=="],
|
||||
|
||||
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.1.10", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-r6Rm5Ru/WwcBKKuPIP0RreI0gnf+MYRV0mmzPBVhMZdPWSC/eTT3GdyqFDZ4cCN76n5aea0sa5PPW7iPF+Uw6Q=="],
|
||||
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.1.6", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-gvmvgh7WtTtcHiCbG7z43DOYfY/jrf2S6TX/jBMX2/e1AGkcLKwz30NjGhZxeK5SyzxRVypgfZZK1IuriRgbdA=="],
|
||||
|
||||
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.1.10", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-UVo5OWO92DPIFhoEkw0tj8IcZyUKOG6NlFs1+tSExz7qrgkr0IloxpLslGMmdc895xxpljrr/FobYktLxyJbcg=="],
|
||||
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.1.6", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-j3R76pmQ4HGVGFJUMMCeF/1lO3Jg7xFdpcBUKCeFh42N1jMgn1aeyxkAaJYB9RwCF/p6+P8B6gVDLCEDu2mxjA=="],
|
||||
|
||||
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.1.10", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-3g99z2FweMzHSUYuzgU0E2H0kjVmtOhPZdavwVqcHQtLQ9NNhwfnIvj3yFBif+kGJphP9RDnByC1oA8Q26UrCg=="],
|
||||
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.1.6", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-VDdo0tHCOr5nm7ajd652u798nPNOLRSTcPOnVh6vIPddkZ+ujRke+enOKOw9Pd5e+4AkthqHBwFXNm2VFgnEKg=="],
|
||||
|
||||
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.1.10", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-2HS9Ju0Cr433lMFJtu/7bShApOJywp+zmVCduQUBWFi3xbX1nm5sJwWDhw1Wx+VcqHEuJl/SQzWPE4vaqkEQng=="],
|
||||
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.1.6", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-hBG/dhsr8PZelUlYsPBruSLnelB9ocB7H92I+S9svTpDVo67rAmXOoR04twKQ9TeCO4ShOa6hhMhbQnuI8fgNw=="],
|
||||
|
||||
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.1.10", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-QLncZJSlWmmcuXrAVKIH6a9Om1Ym6pkhG4hAxaD5K5aF1jw2QFsadjoT12VNq2WzQb+Pg5Y6IWvoow0ZR0aEvw=="],
|
||||
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.1.6", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-c8Awp03p2DsbS0G589nzveRCeJPgJRJ0vQrha4ChRmmo31Qc5OSmJ5xuMaF8L4nM+/trbTgAQMFMtCMLgtC8IQ=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
@@ -310,10 +310,8 @@
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
|
||||
"@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,7 +610,7 @@ Configure git-master skill behavior:
|
||||
When enabled (default), Sisyphus provides a powerful orchestrator with optional specialized agents:
|
||||
|
||||
- **Sisyphus**: Primary orchestrator agent (Claude Opus 4.5)
|
||||
- **OpenCode-Builder**: OpenCode's default build agent, renamed due to SDK limitations (disabled by default)
|
||||
- **opencode-builder**: OpenCode's default build agent, renamed due to SDK limitations (disabled by default)
|
||||
- **Prometheus (Planner)**: OpenCode's default plan agent with work-planner methodology (enabled by default)
|
||||
- **Metis (Plan Consultant)**: Pre-planning analysis agent that identifies hidden requirements and AI failure points
|
||||
|
||||
@@ -627,7 +627,7 @@ When enabled (default), Sisyphus provides a powerful orchestrator with optional
|
||||
}
|
||||
```
|
||||
|
||||
**Example: Enable OpenCode-Builder:**
|
||||
**Example: Enable opencode-builder:**
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -637,7 +637,7 @@ When enabled (default), Sisyphus provides a powerful orchestrator with optional
|
||||
}
|
||||
```
|
||||
|
||||
This enables OpenCode-Builder agent alongside Sisyphus. The default build agent is always demoted to subagent mode when Sisyphus is enabled.
|
||||
This enables opencode-builder agent alongside Sisyphus. The default build agent is always demoted to subagent mode when Sisyphus is enabled.
|
||||
|
||||
**Example: Disable all Sisyphus orchestration:**
|
||||
|
||||
@@ -658,7 +658,7 @@ You can also customize Sisyphus agents like other agents:
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"temperature": 0.3
|
||||
},
|
||||
"OpenCode-Builder": {
|
||||
"opencode-builder": {
|
||||
"model": "anthropic/claude-opus-4"
|
||||
},
|
||||
"Prometheus (Planner)": {
|
||||
@@ -674,7 +674,7 @@ You can also customize Sisyphus agents like other agents:
|
||||
| Option | Default | Description |
|
||||
| ------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `disabled` | `false` | When `true`, disables all Sisyphus orchestration and restores original build/plan as primary. |
|
||||
| `default_builder_enabled` | `false` | When `true`, enables OpenCode-Builder agent (same as OpenCode build, renamed due to SDK limitations). Disabled by default. |
|
||||
| `default_builder_enabled` | `false` | When `true`, enables opencode-builder agent (same as OpenCode build, renamed due to SDK limitations). Disabled by default. |
|
||||
| `planner_enabled` | `true` | When `true`, enables Prometheus (Planner) agent with work-planner methodology. Enabled by default. |
|
||||
| `replace_plan` | `true` | When `true`, demotes default plan agent to subagent mode. Set to `false` to keep both Prometheus (Planner) and default plan available. |
|
||||
|
||||
|
||||
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "3.1.11",
|
||||
"version": "3.1.10",
|
||||
"description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -74,13 +74,13 @@
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.1.11",
|
||||
"oh-my-opencode-darwin-x64": "3.1.11",
|
||||
"oh-my-opencode-linux-arm64": "3.1.11",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.1.11",
|
||||
"oh-my-opencode-linux-x64": "3.1.11",
|
||||
"oh-my-opencode-linux-x64-musl": "3.1.11",
|
||||
"oh-my-opencode-windows-x64": "3.1.11"
|
||||
"oh-my-opencode-darwin-arm64": "3.1.10",
|
||||
"oh-my-opencode-darwin-x64": "3.1.10",
|
||||
"oh-my-opencode-linux-arm64": "3.1.10",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.1.10",
|
||||
"oh-my-opencode-linux-x64": "3.1.10",
|
||||
"oh-my-opencode-linux-x64-musl": "3.1.10",
|
||||
"oh-my-opencode-windows-x64": "3.1.10"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@ast-grep/cli",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-arm64",
|
||||
"version": "3.1.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-arm64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-x64",
|
||||
"version": "3.1.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-arm64-musl",
|
||||
"version": "3.1.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64-musl)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-arm64",
|
||||
"version": "3.1.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64-musl",
|
||||
"version": "3.1.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64",
|
||||
"version": "3.1.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-windows-x64",
|
||||
"version": "3.1.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1031,38 +1031,6 @@
|
||||
"created_at": "2026-01-30T22:37:32Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1303
|
||||
},
|
||||
{
|
||||
"name": "taetaetae",
|
||||
"id": 10969354,
|
||||
"comment_id": 3828900888,
|
||||
"created_at": "2026-01-31T17:44:09Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1333
|
||||
},
|
||||
{
|
||||
"name": "taetaetae",
|
||||
"id": 10969354,
|
||||
"comment_id": 3828909557,
|
||||
"created_at": "2026-01-31T17:47:21Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1333
|
||||
},
|
||||
{
|
||||
"name": "dmealing",
|
||||
"id": 1153509,
|
||||
"comment_id": 3829284275,
|
||||
"created_at": "2026-01-31T20:23:51Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1296
|
||||
},
|
||||
{
|
||||
"name": "edxeth",
|
||||
"id": 105494645,
|
||||
"comment_id": 3829930814,
|
||||
"created_at": "2026-02-01T00:58:26Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1348
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export function categorizeTools(toolNames: string[]): AvailableTool[] {
|
||||
category = "search"
|
||||
} else if (name.startsWith("session_")) {
|
||||
category = "session"
|
||||
} else if (name === "slashcommand") {
|
||||
} else if (name === "slash_command") {
|
||||
category = "command"
|
||||
}
|
||||
return { name, category }
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentMode, AgentPromptMetadata } from "./types"
|
||||
import type { AgentMode } from "./types"
|
||||
import { isGptModel } from "./types"
|
||||
|
||||
const MODE: AgentMode = "primary"
|
||||
export const SISYPHUS_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "utility",
|
||||
cost: "EXPENSIVE",
|
||||
promptAlias: "Sisyphus",
|
||||
triggers: [],
|
||||
}
|
||||
import type { AvailableAgent, AvailableTool, AvailableSkill, AvailableCategory } from "./dynamic-agent-prompt-builder"
|
||||
import {
|
||||
buildKeyTriggersSection,
|
||||
|
||||
@@ -6,11 +6,11 @@ import { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle"
|
||||
import { createLibrarianAgent, LIBRARIAN_PROMPT_METADATA } from "./librarian"
|
||||
import { createExploreAgent, EXPLORE_PROMPT_METADATA } from "./explore"
|
||||
import { createMultimodalLookerAgent, MULTIMODAL_LOOKER_PROMPT_METADATA } from "./multimodal-looker"
|
||||
import { createMetisAgent, metisPromptMetadata } from "./metis"
|
||||
import { createAtlasAgent, atlasPromptMetadata } from "./atlas"
|
||||
import { createMomusAgent, momusPromptMetadata } from "./momus"
|
||||
import { createMetisAgent } from "./metis"
|
||||
import { createAtlasAgent } from "./atlas"
|
||||
import { createMomusAgent } from "./momus"
|
||||
import type { AvailableAgent, AvailableCategory, AvailableSkill } from "./dynamic-agent-prompt-builder"
|
||||
import { deepMerge, fetchAvailableModels, resolveModelPipeline, AGENT_MODEL_REQUIREMENTS, readConnectedProvidersCache, isModelAvailable } from "../shared"
|
||||
import { deepMerge, fetchAvailableModels, resolveModelWithFallback, AGENT_MODEL_REQUIREMENTS, findCaseInsensitive, includesCaseInsensitive, readConnectedProvidersCache, isModelAvailable } from "../shared"
|
||||
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants"
|
||||
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
|
||||
import { createBuiltinSkills } from "../features/builtin-skills"
|
||||
@@ -41,9 +41,6 @@ const agentMetadata: Partial<Record<BuiltinAgentName, AgentPromptMetadata>> = {
|
||||
librarian: LIBRARIAN_PROMPT_METADATA,
|
||||
explore: EXPLORE_PROMPT_METADATA,
|
||||
"multimodal-looker": MULTIMODAL_LOOKER_PROMPT_METADATA,
|
||||
metis: metisPromptMetadata,
|
||||
momus: momusPromptMetadata,
|
||||
atlas: atlasPromptMetadata,
|
||||
}
|
||||
|
||||
function isFactory(source: AgentSource): source is AgentFactory {
|
||||
@@ -150,45 +147,6 @@ function applyCategoryOverride(
|
||||
return result as AgentConfig
|
||||
}
|
||||
|
||||
function applyModelResolution(input: {
|
||||
uiSelectedModel?: string
|
||||
userModel?: string
|
||||
requirement?: { fallbackChain?: { providers: string[]; model: string; variant?: string }[] }
|
||||
availableModels: Set<string>
|
||||
systemDefaultModel?: string
|
||||
}) {
|
||||
const { uiSelectedModel, userModel, requirement, availableModels, systemDefaultModel } = input
|
||||
return resolveModelPipeline({
|
||||
intent: { uiSelectedModel, userModel },
|
||||
constraints: { availableModels },
|
||||
policy: { fallbackChain: requirement?.fallbackChain, systemDefaultModel },
|
||||
})
|
||||
}
|
||||
|
||||
function applyEnvironmentContext(config: AgentConfig, directory?: string): AgentConfig {
|
||||
if (!directory || !config.prompt) return config
|
||||
const envContext = createEnvContext()
|
||||
return { ...config, prompt: config.prompt + envContext }
|
||||
}
|
||||
|
||||
function applyOverrides(
|
||||
config: AgentConfig,
|
||||
override: AgentOverrideConfig | undefined,
|
||||
mergedCategories: Record<string, CategoryConfig>
|
||||
): AgentConfig {
|
||||
let result = config
|
||||
const overrideCategory = (override as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (overrideCategory) {
|
||||
result = applyCategoryOverride(result, overrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (override) {
|
||||
result = mergeAgentConfig(result, override)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function mergeAgentConfig(
|
||||
base: AgentConfig,
|
||||
override: AgentOverrideConfig
|
||||
@@ -265,10 +223,9 @@ export async function createBuiltinAgents(
|
||||
|
||||
if (agentName === "sisyphus") continue
|
||||
if (agentName === "atlas") continue
|
||||
if (disabledAgents.some((name) => name.toLowerCase() === agentName.toLowerCase())) continue
|
||||
if (includesCaseInsensitive(disabledAgents, agentName)) continue
|
||||
|
||||
const override = agentOverrides[agentName]
|
||||
?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
|
||||
const override = findCaseInsensitive(agentOverrides, agentName)
|
||||
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
|
||||
|
||||
// Check if agent requires a specific model
|
||||
@@ -280,10 +237,10 @@ export async function createBuiltinAgents(
|
||||
|
||||
const isPrimaryAgent = isFactory(source) && source.mode === "primary"
|
||||
|
||||
const resolution = applyModelResolution({
|
||||
const resolution = resolveModelWithFallback({
|
||||
uiSelectedModel: isPrimaryAgent ? uiSelectedModel : undefined,
|
||||
userModel: override?.model,
|
||||
requirement,
|
||||
fallbackChain: requirement?.fallbackChain,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
@@ -303,11 +260,15 @@ export async function createBuiltinAgents(
|
||||
config = applyCategoryOverride(config, overrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (agentName === "librarian") {
|
||||
config = applyEnvironmentContext(config, directory)
|
||||
if (agentName === "librarian" && directory && config.prompt) {
|
||||
const envContext = createEnvContext()
|
||||
config = { ...config, prompt: config.prompt + envContext }
|
||||
}
|
||||
|
||||
config = applyOverrides(config, override, mergedCategories)
|
||||
// Direct override properties take highest priority
|
||||
if (override) {
|
||||
config = mergeAgentConfig(config, override)
|
||||
}
|
||||
|
||||
result[name] = config
|
||||
|
||||
@@ -325,10 +286,10 @@ export async function createBuiltinAgents(
|
||||
const sisyphusOverride = agentOverrides["sisyphus"]
|
||||
const sisyphusRequirement = AGENT_MODEL_REQUIREMENTS["sisyphus"]
|
||||
|
||||
const sisyphusResolution = applyModelResolution({
|
||||
const sisyphusResolution = resolveModelWithFallback({
|
||||
uiSelectedModel,
|
||||
userModel: sisyphusOverride?.model,
|
||||
requirement: sisyphusRequirement,
|
||||
fallbackChain: sisyphusRequirement?.fallbackChain,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
@@ -348,8 +309,19 @@ export async function createBuiltinAgents(
|
||||
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusResolvedVariant }
|
||||
}
|
||||
|
||||
sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories)
|
||||
sisyphusConfig = applyEnvironmentContext(sisyphusConfig, directory)
|
||||
const sisOverrideCategory = (sisyphusOverride as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (sisOverrideCategory) {
|
||||
sisyphusConfig = applyCategoryOverride(sisyphusConfig, sisOverrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (directory && sisyphusConfig.prompt) {
|
||||
const envContext = createEnvContext()
|
||||
sisyphusConfig = { ...sisyphusConfig, prompt: sisyphusConfig.prompt + envContext }
|
||||
}
|
||||
|
||||
if (sisyphusOverride) {
|
||||
sisyphusConfig = mergeAgentConfig(sisyphusConfig, sisyphusOverride)
|
||||
}
|
||||
|
||||
result["sisyphus"] = sisyphusConfig
|
||||
}
|
||||
@@ -359,10 +331,10 @@ export async function createBuiltinAgents(
|
||||
const orchestratorOverride = agentOverrides["atlas"]
|
||||
const atlasRequirement = AGENT_MODEL_REQUIREMENTS["atlas"]
|
||||
|
||||
const atlasResolution = applyModelResolution({
|
||||
const atlasResolution = resolveModelWithFallback({
|
||||
// NOTE: Atlas does NOT use uiSelectedModel - respects its own fallbackChain (k2p5 primary)
|
||||
userModel: orchestratorOverride?.model,
|
||||
requirement: atlasRequirement,
|
||||
fallbackChain: atlasRequirement?.fallbackChain,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
@@ -381,7 +353,14 @@ export async function createBuiltinAgents(
|
||||
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
|
||||
}
|
||||
|
||||
orchestratorConfig = applyOverrides(orchestratorConfig, orchestratorOverride, mergedCategories)
|
||||
const atlasOverrideCategory = (orchestratorOverride as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (atlasOverrideCategory) {
|
||||
orchestratorConfig = applyCategoryOverride(orchestratorConfig, atlasOverrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (orchestratorOverride) {
|
||||
orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride)
|
||||
}
|
||||
|
||||
result["atlas"] = orchestratorConfig
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const OverridableAgentNameSchema = z.enum([
|
||||
"plan",
|
||||
"sisyphus",
|
||||
"sisyphus-junior",
|
||||
"OpenCode-Builder",
|
||||
"opencode-builder",
|
||||
"prometheus",
|
||||
"metis",
|
||||
"momus",
|
||||
@@ -88,7 +88,6 @@ export const HookNameSchema = z.enum([
|
||||
"sisyphus-junior-notepad",
|
||||
"start-work",
|
||||
"atlas",
|
||||
"stop-continuation-guard",
|
||||
])
|
||||
|
||||
export const BuiltinCommandNameSchema = z.enum([
|
||||
@@ -137,7 +136,7 @@ export const AgentOverridesSchema = z.object({
|
||||
plan: AgentOverrideConfigSchema.optional(),
|
||||
sisyphus: AgentOverrideConfigSchema.optional(),
|
||||
"sisyphus-junior": AgentOverrideConfigSchema.optional(),
|
||||
"OpenCode-Builder": AgentOverrideConfigSchema.optional(),
|
||||
"opencode-builder": AgentOverrideConfigSchema.optional(),
|
||||
prometheus: AgentOverrideConfigSchema.optional(),
|
||||
metis: AgentOverrideConfigSchema.optional(),
|
||||
momus: AgentOverrideConfigSchema.optional(),
|
||||
|
||||
@@ -2087,95 +2087,3 @@ describe("BackgroundManager.shutdown session abort", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("BackgroundManager.completionTimers - Memory Leak Fix", () => {
|
||||
function getCompletionTimers(manager: BackgroundManager): Map<string, ReturnType<typeof setTimeout>> {
|
||||
return (manager as unknown as { completionTimers: Map<string, ReturnType<typeof setTimeout>> }).completionTimers
|
||||
}
|
||||
|
||||
function setCompletionTimer(manager: BackgroundManager, taskId: string): void {
|
||||
const completionTimers = getCompletionTimers(manager)
|
||||
const timer = setTimeout(() => {
|
||||
completionTimers.delete(taskId)
|
||||
}, 5 * 60 * 1000)
|
||||
completionTimers.set(taskId, timer)
|
||||
}
|
||||
|
||||
test("should have completionTimers Map initialized", () => {
|
||||
// #given
|
||||
const manager = createBackgroundManager()
|
||||
|
||||
// #when
|
||||
const completionTimers = getCompletionTimers(manager)
|
||||
|
||||
// #then
|
||||
expect(completionTimers).toBeDefined()
|
||||
expect(completionTimers).toBeInstanceOf(Map)
|
||||
expect(completionTimers.size).toBe(0)
|
||||
|
||||
manager.shutdown()
|
||||
})
|
||||
|
||||
test("should clear all completion timers on shutdown", () => {
|
||||
// #given
|
||||
const manager = createBackgroundManager()
|
||||
setCompletionTimer(manager, "task-1")
|
||||
setCompletionTimer(manager, "task-2")
|
||||
|
||||
const completionTimers = getCompletionTimers(manager)
|
||||
expect(completionTimers.size).toBe(2)
|
||||
|
||||
// #when
|
||||
manager.shutdown()
|
||||
|
||||
// #then
|
||||
expect(completionTimers.size).toBe(0)
|
||||
})
|
||||
|
||||
test("should cancel timer when task is deleted via session.deleted", () => {
|
||||
// #given
|
||||
const manager = createBackgroundManager()
|
||||
const task: BackgroundTask = {
|
||||
id: "task-timer-4",
|
||||
sessionID: "session-timer-4",
|
||||
parentSessionID: "parent-session",
|
||||
parentMessageID: "msg-1",
|
||||
description: "Test task",
|
||||
prompt: "test",
|
||||
agent: "explore",
|
||||
status: "completed",
|
||||
startedAt: new Date(),
|
||||
}
|
||||
getTaskMap(manager).set(task.id, task)
|
||||
setCompletionTimer(manager, task.id)
|
||||
|
||||
const completionTimers = getCompletionTimers(manager)
|
||||
expect(completionTimers.size).toBe(1)
|
||||
|
||||
// #when
|
||||
manager.handleEvent({
|
||||
type: "session.deleted",
|
||||
properties: {
|
||||
info: { id: "session-timer-4" },
|
||||
},
|
||||
})
|
||||
|
||||
// #then
|
||||
expect(completionTimers.has(task.id)).toBe(false)
|
||||
|
||||
manager.shutdown()
|
||||
})
|
||||
|
||||
test("should not leak timers across multiple shutdown calls", () => {
|
||||
// #given
|
||||
const manager = createBackgroundManager()
|
||||
setCompletionTimer(manager, "task-1")
|
||||
|
||||
// #when
|
||||
manager.shutdown()
|
||||
manager.shutdown()
|
||||
|
||||
// #then
|
||||
const completionTimers = getCompletionTimers(manager)
|
||||
expect(completionTimers.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -83,7 +83,6 @@ export class BackgroundManager {
|
||||
|
||||
private queuesByKey: Map<string, QueueItem[]> = new Map()
|
||||
private processingKeys: Set<string> = new Set()
|
||||
private completionTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
||||
|
||||
constructor(
|
||||
ctx: PluginInput,
|
||||
@@ -709,11 +708,7 @@ export class BackgroundManager {
|
||||
this.concurrencyManager.release(task.concurrencyKey)
|
||||
task.concurrencyKey = undefined
|
||||
}
|
||||
const existingTimer = this.completionTimers.get(task.id)
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer)
|
||||
this.completionTimers.delete(task.id)
|
||||
}
|
||||
// Clean up pendingByParent to prevent stale entries
|
||||
this.cleanupPendingByParent(task)
|
||||
this.tasks.delete(task.id)
|
||||
this.clearNotificationsForTask(task.id)
|
||||
@@ -1078,15 +1073,14 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
||||
}
|
||||
|
||||
const taskId = task.id
|
||||
const timer = setTimeout(() => {
|
||||
this.completionTimers.delete(taskId)
|
||||
setTimeout(() => {
|
||||
// Guard: Only delete if task still exists (could have been deleted by session.deleted event)
|
||||
if (this.tasks.has(taskId)) {
|
||||
this.clearNotificationsForTask(taskId)
|
||||
this.tasks.delete(taskId)
|
||||
log("[background-agent] Removed completed task from memory:", taskId)
|
||||
}
|
||||
}, 5 * 60 * 1000)
|
||||
this.completionTimers.set(taskId, timer)
|
||||
}
|
||||
|
||||
private formatDuration(start: Date, end?: Date): string {
|
||||
@@ -1381,11 +1375,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
||||
}
|
||||
}
|
||||
|
||||
for (const timer of this.completionTimers.values()) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
this.completionTimers.clear()
|
||||
|
||||
// Then clear all state (cancels any remaining waiters)
|
||||
this.concurrencyManager.clear()
|
||||
this.tasks.clear()
|
||||
this.notifications.clear()
|
||||
@@ -1406,10 +1396,7 @@ function registerProcessSignal(
|
||||
const listener = () => {
|
||||
handler()
|
||||
if (exitAfter) {
|
||||
// Set exitCode and schedule exit after delay to allow other handlers to complete async cleanup
|
||||
// Use 6s delay to accommodate LSP cleanup (5s timeout + 1s SIGKILL wait)
|
||||
process.exitCode = 0
|
||||
setTimeout(() => process.exit(), 6000)
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
process.on(signal, listener)
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { CommandDefinition } from "../claude-code-command-loader"
|
||||
import type { BuiltinCommandName, BuiltinCommands } from "./types"
|
||||
import { INIT_DEEP_TEMPLATE } from "./templates/init-deep"
|
||||
import { RALPH_LOOP_TEMPLATE, CANCEL_RALPH_TEMPLATE } from "./templates/ralph-loop"
|
||||
import { STOP_CONTINUATION_TEMPLATE } from "./templates/stop-continuation"
|
||||
import { REFACTOR_TEMPLATE } from "./templates/refactor"
|
||||
import { START_WORK_TEMPLATE } from "./templates/start-work"
|
||||
|
||||
@@ -71,12 +70,6 @@ $ARGUMENTS
|
||||
</user-request>`,
|
||||
argumentHint: "[plan-name]",
|
||||
},
|
||||
"stop-continuation": {
|
||||
description: "(builtin) Stop all continuation mechanisms (ralph loop, todo continuation, boulder) for this session",
|
||||
template: `<command-instruction>
|
||||
${STOP_CONTINUATION_TEMPLATE}
|
||||
</command-instruction>`,
|
||||
},
|
||||
}
|
||||
|
||||
export function loadBuiltinCommands(
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { STOP_CONTINUATION_TEMPLATE } from "./stop-continuation"
|
||||
|
||||
describe("stop-continuation template", () => {
|
||||
test("should export a non-empty template string", () => {
|
||||
// #given - the stop-continuation template
|
||||
|
||||
// #when - we access the template
|
||||
|
||||
// #then - it should be a non-empty string
|
||||
expect(typeof STOP_CONTINUATION_TEMPLATE).toBe("string")
|
||||
expect(STOP_CONTINUATION_TEMPLATE.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test("should describe the stop-continuation behavior", () => {
|
||||
// #given - the stop-continuation template
|
||||
|
||||
// #when - we check the content
|
||||
|
||||
// #then - it should mention key behaviors
|
||||
expect(STOP_CONTINUATION_TEMPLATE).toContain("todo-continuation-enforcer")
|
||||
expect(STOP_CONTINUATION_TEMPLATE).toContain("Ralph Loop")
|
||||
expect(STOP_CONTINUATION_TEMPLATE).toContain("boulder state")
|
||||
})
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
export const STOP_CONTINUATION_TEMPLATE = `Stop all continuation mechanisms for the current session.
|
||||
|
||||
This command will:
|
||||
1. Stop the todo-continuation-enforcer from automatically continuing incomplete tasks
|
||||
2. Cancel any active Ralph Loop
|
||||
3. Clear the boulder state for the current project
|
||||
|
||||
After running this command:
|
||||
- The session will not auto-continue when idle
|
||||
- You can manually continue work when ready
|
||||
- The stop state is per-session and clears when the session ends
|
||||
|
||||
Use this when you need to pause automated continuation and take manual control.`
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CommandDefinition } from "../claude-code-command-loader"
|
||||
|
||||
export type BuiltinCommandName = "init-deep" | "ralph-loop" | "cancel-ralph" | "ulw-loop" | "refactor" | "start-work" | "stop-continuation"
|
||||
export type BuiltinCommandName = "init-deep" | "ralph-loop" | "cancel-ralph" | "ulw-loop" | "refactor" | "start-work"
|
||||
|
||||
export interface BuiltinCommandConfig {
|
||||
disabled_commands?: BuiltinCommandName[]
|
||||
|
||||
@@ -114,15 +114,23 @@ export class SkillMcpManager {
|
||||
this.pendingConnections.clear()
|
||||
}
|
||||
|
||||
// Note: Node's 'exit' event is synchronous-only, so we rely on signal handlers for async cleanup.
|
||||
// Signal handlers invoke the async cleanup function and ignore errors so they don't block or throw.
|
||||
// Don't call process.exit() here - let the background-agent manager handle the final process exit.
|
||||
// Use void + catch to trigger async cleanup without awaiting it in the signal handler.
|
||||
// Note: 'exit' event is synchronous-only in Node.js, so we use 'beforeExit' for async cleanup
|
||||
// However, 'beforeExit' is not emitted on explicit process.exit() calls
|
||||
// Signal handlers are made async to properly await cleanup
|
||||
|
||||
process.on("SIGINT", () => void cleanup().catch(() => {}))
|
||||
process.on("SIGTERM", () => void cleanup().catch(() => {}))
|
||||
process.on("SIGINT", async () => {
|
||||
await cleanup()
|
||||
process.exit(0)
|
||||
})
|
||||
process.on("SIGTERM", async () => {
|
||||
await cleanup()
|
||||
process.exit(0)
|
||||
})
|
||||
if (process.platform === "win32") {
|
||||
process.on("SIGBREAK", () => void cleanup().catch(() => {}))
|
||||
process.on("SIGBREAK", async () => {
|
||||
await cleanup()
|
||||
process.exit(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ export async function executeSlashCommand(parsed: ParsedSlashCommand, options?:
|
||||
if (!command) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Command "/${parsed.command}" not found. Use the slashcommand tool to list available commands.`,
|
||||
error: `Command "/${parsed.command}" not found. Use the slash_command tool to list available commands.`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
type PreCompactContext,
|
||||
} from "./pre-compact"
|
||||
import { cacheToolInput, getToolInput } from "./tool-input-cache"
|
||||
import { appendTranscriptEntry, getTranscriptPath } from "./transcript"
|
||||
import { recordToolUse, recordToolResult, getTranscriptPath, recordUserMessage } from "./transcript"
|
||||
import type { PluginConfig } from "./types"
|
||||
import { log, isHookDisabled } from "../../shared"
|
||||
import type { ContextCollector } from "../../features/context-injector"
|
||||
@@ -92,11 +92,7 @@ export function createClaudeCodeHooksHook(
|
||||
const textParts = output.parts.filter((p) => p.type === "text" && p.text)
|
||||
const prompt = textParts.map((p) => p.text ?? "").join("\n")
|
||||
|
||||
appendTranscriptEntry(input.sessionID, {
|
||||
type: "user",
|
||||
timestamp: new Date().toISOString(),
|
||||
content: prompt,
|
||||
})
|
||||
recordUserMessage(input.sessionID, prompt)
|
||||
|
||||
const messageParts: MessagePart[] = textParts.map((p) => ({
|
||||
type: p.type as "text",
|
||||
@@ -202,12 +198,7 @@ export function createClaudeCodeHooksHook(
|
||||
const claudeConfig = await loadClaudeHooksConfig()
|
||||
const extendedConfig = await loadPluginExtendedConfig()
|
||||
|
||||
appendTranscriptEntry(input.sessionID, {
|
||||
type: "tool_use",
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_name: input.tool,
|
||||
tool_input: output.args as Record<string, unknown>,
|
||||
})
|
||||
recordToolUse(input.sessionID, input.tool, output.args as Record<string, unknown>)
|
||||
|
||||
cacheToolInput(input.sessionID, input.tool, input.callID, output.args as Record<string, unknown>)
|
||||
|
||||
@@ -262,13 +253,7 @@ export function createClaudeCodeHooksHook(
|
||||
const metadata = output.metadata as Record<string, unknown> | undefined
|
||||
const hasMetadata = metadata && typeof metadata === "object" && Object.keys(metadata).length > 0
|
||||
const toolOutput = hasMetadata ? metadata : { output: output.output }
|
||||
appendTranscriptEntry(input.sessionID, {
|
||||
type: "tool_result",
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_name: input.tool,
|
||||
tool_input: cachedInput,
|
||||
tool_output: toolOutput,
|
||||
})
|
||||
recordToolResult(input.sessionID, input.tool, cachedInput, toolOutput)
|
||||
|
||||
if (!isHookDisabled(config, "PostToolUse")) {
|
||||
const postClient: PostToolUseClient = {
|
||||
|
||||
@@ -28,6 +28,56 @@ export function appendTranscriptEntry(
|
||||
appendFileSync(path, line)
|
||||
}
|
||||
|
||||
export function recordToolUse(
|
||||
sessionId: string,
|
||||
toolName: string,
|
||||
toolInput: Record<string, unknown>
|
||||
): void {
|
||||
appendTranscriptEntry(sessionId, {
|
||||
type: "tool_use",
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_name: toolName,
|
||||
tool_input: toolInput,
|
||||
})
|
||||
}
|
||||
|
||||
export function recordToolResult(
|
||||
sessionId: string,
|
||||
toolName: string,
|
||||
toolInput: Record<string, unknown>,
|
||||
toolOutput: Record<string, unknown>
|
||||
): void {
|
||||
appendTranscriptEntry(sessionId, {
|
||||
type: "tool_result",
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_name: toolName,
|
||||
tool_input: toolInput,
|
||||
tool_output: toolOutput,
|
||||
})
|
||||
}
|
||||
|
||||
export function recordUserMessage(
|
||||
sessionId: string,
|
||||
content: string
|
||||
): void {
|
||||
appendTranscriptEntry(sessionId, {
|
||||
type: "user",
|
||||
timestamp: new Date().toISOString(),
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
export function recordAssistantMessage(
|
||||
sessionId: string,
|
||||
content: string
|
||||
): void {
|
||||
appendTranscriptEntry(sessionId, {
|
||||
type: "assistant",
|
||||
timestamp: new Date().toISOString(),
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Claude Code Compatible Transcript Builder (PORT FROM DISABLED)
|
||||
// ============================================================================
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { existsSync, appendFileSync } from "fs"
|
||||
import { spawn } from "bun"
|
||||
import { existsSync, mkdirSync, chmodSync, unlinkSync, appendFileSync } from "fs"
|
||||
import { join } from "path"
|
||||
import { homedir, tmpdir } from "os"
|
||||
import { createRequire } from "module"
|
||||
import {
|
||||
cleanupArchive,
|
||||
downloadArchive,
|
||||
ensureCacheDir,
|
||||
ensureExecutable,
|
||||
extractTarGz,
|
||||
extractZipArchive,
|
||||
getCachedBinaryPath as getCachedBinaryPathShared,
|
||||
} from "../../shared/binary-downloader"
|
||||
import { extractZip } from "../../shared"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
const DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1"
|
||||
@@ -67,7 +60,8 @@ export function getBinaryName(): string {
|
||||
* Get the cached binary path if it exists.
|
||||
*/
|
||||
export function getCachedBinaryPath(): string | null {
|
||||
return getCachedBinaryPathShared(getCacheDir(), getBinaryName())
|
||||
const binaryPath = join(getCacheDir(), getBinaryName())
|
||||
return existsSync(binaryPath) ? binaryPath : null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,6 +78,27 @@ function getPackageVersion(): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tar.gz archive using system tar command.
|
||||
*/
|
||||
async function extractTarGz(archivePath: string, destDir: string): Promise<void> {
|
||||
debugLog("Extracting tar.gz:", archivePath, "to", destDir)
|
||||
|
||||
const proc = spawn(["tar", "-xzf", archivePath, "-C", destDir], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
const exitCode = await proc.exited
|
||||
|
||||
if (exitCode !== 0) {
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
throw new Error(`tar extraction failed (exit ${exitCode}): ${stderr}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Download the comment-checker binary from GitHub Releases.
|
||||
* Returns the path to the downloaded binary, or null on failure.
|
||||
@@ -117,26 +132,39 @@ export async function downloadCommentChecker(): Promise<string | null> {
|
||||
|
||||
try {
|
||||
// Ensure cache directory exists
|
||||
ensureCacheDir(cacheDir)
|
||||
if (!existsSync(cacheDir)) {
|
||||
mkdirSync(cacheDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Download with fetch() - Bun handles redirects automatically
|
||||
const response = await fetch(downloadUrl, { redirect: "follow" })
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const archivePath = join(cacheDir, assetName)
|
||||
await downloadArchive(downloadUrl, archivePath)
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
await Bun.write(archivePath, arrayBuffer)
|
||||
|
||||
debugLog(`Downloaded archive to: ${archivePath}`)
|
||||
|
||||
// Extract based on file type
|
||||
if (ext === "tar.gz") {
|
||||
debugLog("Extracting tar.gz:", archivePath, "to", cacheDir)
|
||||
await extractTarGz(archivePath, cacheDir)
|
||||
} else {
|
||||
await extractZipArchive(archivePath, cacheDir)
|
||||
await extractZip(archivePath, cacheDir)
|
||||
}
|
||||
|
||||
// Clean up archive
|
||||
cleanupArchive(archivePath)
|
||||
if (existsSync(archivePath)) {
|
||||
unlinkSync(archivePath)
|
||||
}
|
||||
|
||||
// Set execute permission on Unix
|
||||
ensureExecutable(binaryPath)
|
||||
if (process.platform !== "win32" && existsSync(binaryPath)) {
|
||||
chmodSync(binaryPath, 0o755)
|
||||
}
|
||||
|
||||
debugLog(`Successfully downloaded binary to: ${binaryPath}`)
|
||||
log(`[oh-my-opencode] comment-checker binary ready.`)
|
||||
|
||||
@@ -32,8 +32,8 @@ function cleanupOldPendingCalls(): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function createCommentCheckerHooks(config?: CommentCheckerConfig) {
|
||||
debugLog("createCommentCheckerHooks called", { config })
|
||||
export function createCommentCheckerHook(config?: CommentCheckerConfig) {
|
||||
debugLog("createCommentCheckerHook called", { config })
|
||||
|
||||
if (!cleanupIntervalStarted) {
|
||||
cleanupIntervalStarted = true
|
||||
|
||||
102
src/hooks/compaction-context-injector/index.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it, mock, beforeEach } from "bun:test"
|
||||
|
||||
// Mock dependencies before importing
|
||||
const mockInjectHookMessage = mock(() => true)
|
||||
mock.module("../../features/hook-message-injector", () => ({
|
||||
injectHookMessage: mockInjectHookMessage,
|
||||
}))
|
||||
|
||||
mock.module("../../shared/logger", () => ({
|
||||
log: () => {},
|
||||
}))
|
||||
|
||||
mock.module("../../shared/system-directive", () => ({
|
||||
createSystemDirective: (type: string) => `[DIRECTIVE:${type}]`,
|
||||
SystemDirectiveTypes: {
|
||||
TODO_CONTINUATION: "TODO CONTINUATION",
|
||||
RALPH_LOOP: "RALPH LOOP",
|
||||
BOULDER_CONTINUATION: "BOULDER CONTINUATION",
|
||||
DELEGATION_REQUIRED: "DELEGATION REQUIRED",
|
||||
SINGLE_TASK_ONLY: "SINGLE TASK ONLY",
|
||||
COMPACTION_CONTEXT: "COMPACTION CONTEXT",
|
||||
CONTEXT_WINDOW_MONITOR: "CONTEXT WINDOW MONITOR",
|
||||
PROMETHEUS_READ_ONLY: "PROMETHEUS READ-ONLY",
|
||||
},
|
||||
}))
|
||||
|
||||
import { createCompactionContextInjectorHook } from "./index"
|
||||
import type { SummarizeContext } from "./index"
|
||||
|
||||
describe("createCompactionContextInjector", () => {
|
||||
beforeEach(() => {
|
||||
mockInjectHookMessage.mockClear()
|
||||
})
|
||||
|
||||
describe("Agent Verification State preservation", () => {
|
||||
it("includes Agent Verification State section in compaction prompt", async () => {
|
||||
// given
|
||||
const injector = createCompactionContextInjectorHook()
|
||||
const context: SummarizeContext = {
|
||||
sessionID: "test-session",
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-sonnet-4-5",
|
||||
usageRatio: 0.85,
|
||||
directory: "/test/dir",
|
||||
}
|
||||
|
||||
// when
|
||||
await injector(context)
|
||||
|
||||
// then
|
||||
expect(mockInjectHookMessage).toHaveBeenCalledTimes(1)
|
||||
const calls = mockInjectHookMessage.mock.calls as unknown as [string, string, unknown][]
|
||||
const injectedPrompt = calls[0]?.[1] ?? ""
|
||||
expect(injectedPrompt).toContain("Agent Verification State")
|
||||
expect(injectedPrompt).toContain("Current Agent")
|
||||
expect(injectedPrompt).toContain("Verification Progress")
|
||||
})
|
||||
|
||||
it("includes Momus-specific context for reviewer agents", async () => {
|
||||
// given
|
||||
const injector = createCompactionContextInjectorHook()
|
||||
const context: SummarizeContext = {
|
||||
sessionID: "test-session",
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-sonnet-4-5",
|
||||
usageRatio: 0.9,
|
||||
directory: "/test/dir",
|
||||
}
|
||||
|
||||
// when
|
||||
await injector(context)
|
||||
|
||||
// then
|
||||
const calls = mockInjectHookMessage.mock.calls as unknown as [string, string, unknown][]
|
||||
const injectedPrompt = calls[0]?.[1] ?? ""
|
||||
expect(injectedPrompt).toContain("Previous Rejections")
|
||||
expect(injectedPrompt).toContain("Acceptance Status")
|
||||
expect(injectedPrompt).toContain("reviewer agents")
|
||||
})
|
||||
|
||||
it("preserves file verification progress in compaction prompt", async () => {
|
||||
// given
|
||||
const injector = createCompactionContextInjectorHook()
|
||||
const context: SummarizeContext = {
|
||||
sessionID: "test-session",
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-sonnet-4-5",
|
||||
usageRatio: 0.95,
|
||||
directory: "/test/dir",
|
||||
}
|
||||
|
||||
// when
|
||||
await injector(context)
|
||||
|
||||
// then
|
||||
const calls = mockInjectHookMessage.mock.calls as unknown as [string, string, unknown][]
|
||||
const injectedPrompt = calls[0]?.[1] ?? ""
|
||||
expect(injectedPrompt).toContain("Pending Verifications")
|
||||
expect(injectedPrompt).toContain("Files already verified")
|
||||
})
|
||||
})
|
||||
})
|
||||
76
src/hooks/compaction-context-injector/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { injectHookMessage } from "../../features/hook-message-injector"
|
||||
import { log } from "../../shared/logger"
|
||||
import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive"
|
||||
|
||||
export interface SummarizeContext {
|
||||
sessionID: string
|
||||
providerID: string
|
||||
modelID: string
|
||||
usageRatio: number
|
||||
directory: string
|
||||
}
|
||||
|
||||
const SUMMARIZE_CONTEXT_PROMPT = `${createSystemDirective(SystemDirectiveTypes.COMPACTION_CONTEXT)}
|
||||
|
||||
When summarizing this session, you MUST include the following sections in your summary:
|
||||
|
||||
## 1. User Requests (As-Is)
|
||||
- List all original user requests exactly as they were stated
|
||||
- Preserve the user's exact wording and intent
|
||||
|
||||
## 2. Final Goal
|
||||
- What the user ultimately wanted to achieve
|
||||
- The end result or deliverable expected
|
||||
|
||||
## 3. Work Completed
|
||||
- What has been done so far
|
||||
- Files created/modified
|
||||
- Features implemented
|
||||
- Problems solved
|
||||
|
||||
## 4. Remaining Tasks
|
||||
- What still needs to be done
|
||||
- Pending items from the original request
|
||||
- Follow-up tasks identified during the work
|
||||
|
||||
## 5. Active Working Context (For Seamless Continuation)
|
||||
- **Files**: Paths of files currently being edited or frequently referenced
|
||||
- **Code in Progress**: Key code snippets, function signatures, or data structures under active development
|
||||
- **External References**: Documentation URLs, library APIs, or external resources being consulted
|
||||
- **State & Variables**: Important variable names, configuration values, or runtime state relevant to ongoing work
|
||||
|
||||
## 6. MUST NOT Do (Critical Constraints)
|
||||
- Things that were explicitly forbidden
|
||||
- Approaches that failed and should not be retried
|
||||
- User's explicit restrictions or preferences
|
||||
- Anti-patterns identified during the session
|
||||
|
||||
## 7. Agent Verification State (Critical for Reviewers)
|
||||
- **Current Agent**: What agent is running (momus, oracle, etc.)
|
||||
- **Verification Progress**: Files already verified/validated
|
||||
- **Pending Verifications**: Files still needing verification
|
||||
- **Previous Rejections**: If reviewer agent, what was rejected and why
|
||||
- **Acceptance Status**: Current state of review process
|
||||
|
||||
This section is CRITICAL for reviewer agents (momus, oracle) to maintain continuity.
|
||||
|
||||
This context is critical for maintaining continuity after compaction.
|
||||
`
|
||||
|
||||
export function createCompactionContextInjectorHook() {
|
||||
return async (ctx: SummarizeContext): Promise<void> => {
|
||||
log("[compaction-context-injector] injecting context", { sessionID: ctx.sessionID })
|
||||
|
||||
const success = injectHookMessage(ctx.sessionID, SUMMARIZE_CONTEXT_PROMPT, {
|
||||
agent: "general",
|
||||
model: { providerID: ctx.providerID, modelID: ctx.modelID },
|
||||
path: { cwd: ctx.directory },
|
||||
})
|
||||
|
||||
if (success) {
|
||||
log("[compaction-context-injector] context injected", { sessionID: ctx.sessionID })
|
||||
} else {
|
||||
log("[compaction-context-injector] injection failed", { sessionID: ctx.sessionID })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,11 @@ interface ToolExecuteBeforeOutput {
|
||||
args: unknown;
|
||||
}
|
||||
|
||||
interface BatchToolCall {
|
||||
tool: string;
|
||||
parameters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface EventInput {
|
||||
event: {
|
||||
type: string;
|
||||
@@ -34,6 +39,7 @@ interface EventInput {
|
||||
|
||||
export function createDirectoryAgentsInjectorHook(ctx: PluginInput) {
|
||||
const sessionCaches = new Map<string, Set<string>>();
|
||||
const pendingBatchReads = new Map<string, string[]>();
|
||||
const truncator = createDynamicTruncator(ctx);
|
||||
|
||||
function getSessionCache(sessionID: string): Set<string> {
|
||||
@@ -104,6 +110,27 @@ export function createDirectoryAgentsInjectorHook(ctx: PluginInput) {
|
||||
saveInjectedPaths(sessionID, cache);
|
||||
}
|
||||
|
||||
const toolExecuteBefore = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteBeforeOutput,
|
||||
) => {
|
||||
if (input.tool.toLowerCase() !== "batch") return;
|
||||
|
||||
const args = output.args as { tool_calls?: BatchToolCall[] } | undefined;
|
||||
if (!args?.tool_calls) return;
|
||||
|
||||
const readFilePaths: string[] = [];
|
||||
for (const call of args.tool_calls) {
|
||||
if (call.tool.toLowerCase() === "read" && call.parameters?.filePath) {
|
||||
readFilePaths.push(call.parameters.filePath as string);
|
||||
}
|
||||
}
|
||||
|
||||
if (readFilePaths.length > 0) {
|
||||
pendingBatchReads.set(input.callID, readFilePaths);
|
||||
}
|
||||
};
|
||||
|
||||
const toolExecuteAfter = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteOutput,
|
||||
@@ -114,14 +141,16 @@ export function createDirectoryAgentsInjectorHook(ctx: PluginInput) {
|
||||
await processFilePathForInjection(output.title, input.sessionID, output);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const toolExecuteBefore = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteBeforeOutput,
|
||||
): Promise<void> => {
|
||||
void input;
|
||||
void output;
|
||||
if (toolName === "batch") {
|
||||
const filePaths = pendingBatchReads.get(input.callID);
|
||||
if (filePaths) {
|
||||
for (const filePath of filePaths) {
|
||||
await processFilePathForInjection(filePath, input.sessionID, output);
|
||||
}
|
||||
pendingBatchReads.delete(input.callID);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const eventHandler = async ({ event }: EventInput) => {
|
||||
|
||||
@@ -1,8 +1,48 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { AGENTS_INJECTOR_STORAGE } from "./constants";
|
||||
import { createInjectedPathsStorage } from "../../shared/session-injected-paths";
|
||||
import type { InjectedPathsData } from "./types";
|
||||
|
||||
export const {
|
||||
loadInjectedPaths,
|
||||
saveInjectedPaths,
|
||||
clearInjectedPaths,
|
||||
} = createInjectedPathsStorage(AGENTS_INJECTOR_STORAGE);
|
||||
function getStoragePath(sessionID: string): string {
|
||||
return join(AGENTS_INJECTOR_STORAGE, `${sessionID}.json`);
|
||||
}
|
||||
|
||||
export function loadInjectedPaths(sessionID: string): Set<string> {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (!existsSync(filePath)) return new Set();
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const data: InjectedPathsData = JSON.parse(content);
|
||||
return new Set(data.injectedPaths);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
export function saveInjectedPaths(sessionID: string, paths: Set<string>): void {
|
||||
if (!existsSync(AGENTS_INJECTOR_STORAGE)) {
|
||||
mkdirSync(AGENTS_INJECTOR_STORAGE, { recursive: true });
|
||||
}
|
||||
|
||||
const data: InjectedPathsData = {
|
||||
sessionID,
|
||||
injectedPaths: [...paths],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
writeFileSync(getStoragePath(sessionID), JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
export function clearInjectedPaths(sessionID: string): void {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (existsSync(filePath)) {
|
||||
unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
5
src/hooks/directory-agents-injector/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface InjectedPathsData {
|
||||
sessionID: string;
|
||||
injectedPaths: string[];
|
||||
updatedAt: number;
|
||||
}
|
||||
@@ -25,6 +25,11 @@ interface ToolExecuteBeforeOutput {
|
||||
args: unknown;
|
||||
}
|
||||
|
||||
interface BatchToolCall {
|
||||
tool: string;
|
||||
parameters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface EventInput {
|
||||
event: {
|
||||
type: string;
|
||||
@@ -34,6 +39,7 @@ interface EventInput {
|
||||
|
||||
export function createDirectoryReadmeInjectorHook(ctx: PluginInput) {
|
||||
const sessionCaches = new Map<string, Set<string>>();
|
||||
const pendingBatchReads = new Map<string, string[]>();
|
||||
const truncator = createDynamicTruncator(ctx);
|
||||
|
||||
function getSessionCache(sessionID: string): Set<string> {
|
||||
@@ -99,6 +105,27 @@ export function createDirectoryReadmeInjectorHook(ctx: PluginInput) {
|
||||
saveInjectedPaths(sessionID, cache);
|
||||
}
|
||||
|
||||
const toolExecuteBefore = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteBeforeOutput,
|
||||
) => {
|
||||
if (input.tool.toLowerCase() !== "batch") return;
|
||||
|
||||
const args = output.args as { tool_calls?: BatchToolCall[] } | undefined;
|
||||
if (!args?.tool_calls) return;
|
||||
|
||||
const readFilePaths: string[] = [];
|
||||
for (const call of args.tool_calls) {
|
||||
if (call.tool.toLowerCase() === "read" && call.parameters?.filePath) {
|
||||
readFilePaths.push(call.parameters.filePath as string);
|
||||
}
|
||||
}
|
||||
|
||||
if (readFilePaths.length > 0) {
|
||||
pendingBatchReads.set(input.callID, readFilePaths);
|
||||
}
|
||||
};
|
||||
|
||||
const toolExecuteAfter = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteOutput,
|
||||
@@ -109,14 +136,16 @@ export function createDirectoryReadmeInjectorHook(ctx: PluginInput) {
|
||||
await processFilePathForInjection(output.title, input.sessionID, output);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const toolExecuteBefore = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteBeforeOutput,
|
||||
): Promise<void> => {
|
||||
void input;
|
||||
void output;
|
||||
if (toolName === "batch") {
|
||||
const filePaths = pendingBatchReads.get(input.callID);
|
||||
if (filePaths) {
|
||||
for (const filePath of filePaths) {
|
||||
await processFilePathForInjection(filePath, input.sessionID, output);
|
||||
}
|
||||
pendingBatchReads.delete(input.callID);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const eventHandler = async ({ event }: EventInput) => {
|
||||
|
||||
@@ -1,8 +1,48 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { README_INJECTOR_STORAGE } from "./constants";
|
||||
import { createInjectedPathsStorage } from "../../shared/session-injected-paths";
|
||||
import type { InjectedPathsData } from "./types";
|
||||
|
||||
export const {
|
||||
loadInjectedPaths,
|
||||
saveInjectedPaths,
|
||||
clearInjectedPaths,
|
||||
} = createInjectedPathsStorage(README_INJECTOR_STORAGE);
|
||||
function getStoragePath(sessionID: string): string {
|
||||
return join(README_INJECTOR_STORAGE, `${sessionID}.json`);
|
||||
}
|
||||
|
||||
export function loadInjectedPaths(sessionID: string): Set<string> {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (!existsSync(filePath)) return new Set();
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const data: InjectedPathsData = JSON.parse(content);
|
||||
return new Set(data.injectedPaths);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
export function saveInjectedPaths(sessionID: string, paths: Set<string>): void {
|
||||
if (!existsSync(README_INJECTOR_STORAGE)) {
|
||||
mkdirSync(README_INJECTOR_STORAGE, { recursive: true });
|
||||
}
|
||||
|
||||
const data: InjectedPathsData = {
|
||||
sessionID,
|
||||
injectedPaths: [...paths],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
writeFileSync(getStoragePath(sessionID), JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
export function clearInjectedPaths(sessionID: string): void {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (existsSync(filePath)) {
|
||||
unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
5
src/hooks/directory-readme-injector/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface InjectedPathsData {
|
||||
sessionID: string;
|
||||
injectedPaths: string[];
|
||||
updatedAt: number;
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
export { createTodoContinuationEnforcer, type TodoContinuationEnforcer } from "./todo-continuation-enforcer";
|
||||
export { createTodoContinuationEnforcerHook as createTodoContinuationEnforcer, type TodoContinuationEnforcer } from "./todo-continuation-enforcer";
|
||||
export { createContextWindowMonitorHook } from "./context-window-monitor";
|
||||
export { createSessionNotification } from "./session-notification";
|
||||
export { createSessionNotificationHook as createSessionNotification } from "./session-notification";
|
||||
export { createSessionRecoveryHook, type SessionRecoveryHook, type SessionRecoveryOptions } from "./session-recovery";
|
||||
export { createCommentCheckerHooks } from "./comment-checker";
|
||||
export { createCommentCheckerHook as createCommentCheckerHooks } from "./comment-checker";
|
||||
export { createToolOutputTruncatorHook } from "./tool-output-truncator";
|
||||
export { createDirectoryAgentsInjectorHook } from "./directory-agents-injector";
|
||||
export { createDirectoryReadmeInjectorHook } from "./directory-readme-injector";
|
||||
export { createEmptyTaskResponseDetectorHook } from "./empty-task-response-detector";
|
||||
export { createAnthropicContextWindowLimitRecoveryHook, type AnthropicContextWindowLimitRecoveryOptions } from "./anthropic-context-window-limit-recovery";
|
||||
|
||||
export { createCompactionContextInjectorHook as createCompactionContextInjector } from "./compaction-context-injector";
|
||||
export { createThinkModeHook } from "./think-mode";
|
||||
export { createClaudeCodeHooksHook } from "./claude-code-hooks";
|
||||
export { createRulesInjectorHook } from "./rules-injector";
|
||||
@@ -33,4 +34,3 @@ export { createAtlasHook } from "./atlas";
|
||||
export { createDelegateTaskRetryHook } from "./delegate-task-retry";
|
||||
export { createQuestionLabelTruncatorHook } from "./question-label-truncator";
|
||||
export { createSubagentQuestionBlockerHook } from "./subagent-question-blocker";
|
||||
export { createStopContinuationGuardHook, type StopContinuationGuard } from "./stop-continuation-guard";
|
||||
|
||||
@@ -21,7 +21,6 @@ describe("keyword-detector message transform", () => {
|
||||
afterEach(() => {
|
||||
logSpy?.mockRestore()
|
||||
getMainSessionSpy?.mockRestore()
|
||||
_resetForTesting()
|
||||
})
|
||||
|
||||
function createMockPluginInput() {
|
||||
@@ -102,7 +101,7 @@ describe("keyword-detector session filtering", () => {
|
||||
let logSpy: ReturnType<typeof spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
_resetForTesting()
|
||||
setMainSession(undefined)
|
||||
logCalls = []
|
||||
logSpy = spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => {
|
||||
logCalls.push({ msg, data })
|
||||
@@ -111,7 +110,7 @@ describe("keyword-detector session filtering", () => {
|
||||
|
||||
afterEach(() => {
|
||||
logSpy?.mockRestore()
|
||||
_resetForTesting()
|
||||
setMainSession(undefined)
|
||||
})
|
||||
|
||||
function createMockPluginInput(options: { toastCalls?: string[] } = {}) {
|
||||
@@ -247,7 +246,7 @@ describe("keyword-detector word boundary", () => {
|
||||
let logSpy: ReturnType<typeof spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
_resetForTesting()
|
||||
setMainSession(undefined)
|
||||
logCalls = []
|
||||
logSpy = spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => {
|
||||
logCalls.push({ msg, data })
|
||||
@@ -256,7 +255,7 @@ describe("keyword-detector word boundary", () => {
|
||||
|
||||
afterEach(() => {
|
||||
logSpy?.mockRestore()
|
||||
_resetForTesting()
|
||||
setMainSession(undefined)
|
||||
})
|
||||
|
||||
function createMockPluginInput(options: { toastCalls?: string[] } = {}) {
|
||||
@@ -344,7 +343,7 @@ describe("keyword-detector system-reminder filtering", () => {
|
||||
let logSpy: ReturnType<typeof spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
_resetForTesting()
|
||||
setMainSession(undefined)
|
||||
logCalls = []
|
||||
logSpy = spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => {
|
||||
logCalls.push({ msg, data })
|
||||
@@ -353,7 +352,7 @@ describe("keyword-detector system-reminder filtering", () => {
|
||||
|
||||
afterEach(() => {
|
||||
logSpy?.mockRestore()
|
||||
_resetForTesting()
|
||||
setMainSession(undefined)
|
||||
})
|
||||
|
||||
function createMockPluginInput() {
|
||||
@@ -535,7 +534,7 @@ describe("keyword-detector agent-specific ultrawork messages", () => {
|
||||
let logSpy: ReturnType<typeof spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
_resetForTesting()
|
||||
setMainSession(undefined)
|
||||
logCalls = []
|
||||
logSpy = spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => {
|
||||
logCalls.push({ msg, data })
|
||||
@@ -544,7 +543,7 @@ describe("keyword-detector agent-specific ultrawork messages", () => {
|
||||
|
||||
afterEach(() => {
|
||||
logSpy?.mockRestore()
|
||||
_resetForTesting()
|
||||
setMainSession(undefined)
|
||||
})
|
||||
|
||||
function createMockPluginInput() {
|
||||
|
||||
@@ -17,7 +17,6 @@ export const PROJECT_RULE_SUBDIRS: [string, string][] = [
|
||||
[".github", "instructions"],
|
||||
[".cursor", "rules"],
|
||||
[".claude", "rules"],
|
||||
[".sisyphus", "rules"],
|
||||
];
|
||||
|
||||
export const PROJECT_RULE_FILES: string[] = [
|
||||
|
||||
@@ -33,6 +33,11 @@ interface ToolExecuteBeforeOutput {
|
||||
args: unknown;
|
||||
}
|
||||
|
||||
interface BatchToolCall {
|
||||
tool: string;
|
||||
parameters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface EventInput {
|
||||
event: {
|
||||
type: string;
|
||||
@@ -54,6 +59,7 @@ export function createRulesInjectorHook(ctx: PluginInput) {
|
||||
string,
|
||||
{ contentHashes: Set<string>; realPaths: Set<string> }
|
||||
>();
|
||||
const pendingBatchFiles = new Map<string, string[]>();
|
||||
const truncator = createDynamicTruncator(ctx);
|
||||
|
||||
function getSessionCache(sessionID: string): {
|
||||
@@ -137,6 +143,35 @@ export function createRulesInjectorHook(ctx: PluginInput) {
|
||||
saveInjectedRules(sessionID, cache);
|
||||
}
|
||||
|
||||
function extractFilePathFromToolCall(call: BatchToolCall): string | null {
|
||||
const params = call.parameters;
|
||||
return (params?.filePath ?? params?.file_path ?? params?.path) as string | null;
|
||||
}
|
||||
|
||||
const toolExecuteBefore = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteBeforeOutput
|
||||
) => {
|
||||
if (input.tool.toLowerCase() !== "batch") return;
|
||||
|
||||
const args = output.args as { tool_calls?: BatchToolCall[] } | undefined;
|
||||
if (!args?.tool_calls) return;
|
||||
|
||||
const filePaths: string[] = [];
|
||||
for (const call of args.tool_calls) {
|
||||
if (TRACKED_TOOLS.includes(call.tool.toLowerCase())) {
|
||||
const filePath = extractFilePathFromToolCall(call);
|
||||
if (filePath) {
|
||||
filePaths.push(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filePaths.length > 0) {
|
||||
pendingBatchFiles.set(input.callID, filePaths);
|
||||
}
|
||||
};
|
||||
|
||||
const toolExecuteAfter = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteOutput
|
||||
@@ -147,14 +182,16 @@ export function createRulesInjectorHook(ctx: PluginInput) {
|
||||
await processFilePathForInjection(output.title, input.sessionID, output);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const toolExecuteBefore = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteBeforeOutput
|
||||
): Promise<void> => {
|
||||
void input;
|
||||
void output;
|
||||
if (toolName === "batch") {
|
||||
const filePaths = pendingBatchFiles.get(input.callID);
|
||||
if (filePaths) {
|
||||
for (const filePath of filePaths) {
|
||||
await processFilePathForInjection(filePath, input.sessionID, output);
|
||||
}
|
||||
pendingBatchFiles.delete(input.callID);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const eventHandler = async ({ event }: EventInput) => {
|
||||
|
||||
@@ -2,6 +2,24 @@ import { spawn } from "bun"
|
||||
|
||||
type Platform = "darwin" | "linux" | "win32" | "unsupported"
|
||||
|
||||
let notifySendPath: string | null = null
|
||||
let notifySendPromise: Promise<string | null> | null = null
|
||||
|
||||
let osascriptPath: string | null = null
|
||||
let osascriptPromise: Promise<string | null> | null = null
|
||||
|
||||
let powershellPath: string | null = null
|
||||
let powershellPromise: Promise<string | null> | null = null
|
||||
|
||||
let afplayPath: string | null = null
|
||||
let afplayPromise: Promise<string | null> | null = null
|
||||
|
||||
let paplayPath: string | null = null
|
||||
let paplayPromise: Promise<string | null> | null = null
|
||||
|
||||
let aplayPath: string | null = null
|
||||
let aplayPromise: Promise<string | null> | null = null
|
||||
|
||||
async function findCommand(commandName: string): Promise<string | null> {
|
||||
const isWindows = process.platform === "win32"
|
||||
const cmd = isWindows ? "where" : "which"
|
||||
@@ -30,30 +48,83 @@ async function findCommand(commandName: string): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
function createCommandFinder(commandName: string): () => Promise<string | null> {
|
||||
let cachedPath: string | null = null
|
||||
let pending: Promise<string | null> | null = null
|
||||
export async function getNotifySendPath(): Promise<string | null> {
|
||||
if (notifySendPath !== null) return notifySendPath
|
||||
if (notifySendPromise) return notifySendPromise
|
||||
|
||||
return async () => {
|
||||
if (cachedPath !== null) return cachedPath
|
||||
if (pending) return pending
|
||||
notifySendPromise = (async () => {
|
||||
const path = await findCommand("notify-send")
|
||||
notifySendPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
pending = (async () => {
|
||||
const path = await findCommand(commandName)
|
||||
cachedPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
return pending
|
||||
}
|
||||
return notifySendPromise
|
||||
}
|
||||
|
||||
export const getNotifySendPath = createCommandFinder("notify-send")
|
||||
export const getOsascriptPath = createCommandFinder("osascript")
|
||||
export const getPowershellPath = createCommandFinder("powershell")
|
||||
export const getAfplayPath = createCommandFinder("afplay")
|
||||
export const getPaplayPath = createCommandFinder("paplay")
|
||||
export const getAplayPath = createCommandFinder("aplay")
|
||||
export async function getOsascriptPath(): Promise<string | null> {
|
||||
if (osascriptPath !== null) return osascriptPath
|
||||
if (osascriptPromise) return osascriptPromise
|
||||
|
||||
osascriptPromise = (async () => {
|
||||
const path = await findCommand("osascript")
|
||||
osascriptPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
return osascriptPromise
|
||||
}
|
||||
|
||||
export async function getPowershellPath(): Promise<string | null> {
|
||||
if (powershellPath !== null) return powershellPath
|
||||
if (powershellPromise) return powershellPromise
|
||||
|
||||
powershellPromise = (async () => {
|
||||
const path = await findCommand("powershell")
|
||||
powershellPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
return powershellPromise
|
||||
}
|
||||
|
||||
export async function getAfplayPath(): Promise<string | null> {
|
||||
if (afplayPath !== null) return afplayPath
|
||||
if (afplayPromise) return afplayPromise
|
||||
|
||||
afplayPromise = (async () => {
|
||||
const path = await findCommand("afplay")
|
||||
afplayPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
return afplayPromise
|
||||
}
|
||||
|
||||
export async function getPaplayPath(): Promise<string | null> {
|
||||
if (paplayPath !== null) return paplayPath
|
||||
if (paplayPromise) return paplayPromise
|
||||
|
||||
paplayPromise = (async () => {
|
||||
const path = await findCommand("paplay")
|
||||
paplayPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
return paplayPromise
|
||||
}
|
||||
|
||||
export async function getAplayPath(): Promise<string | null> {
|
||||
if (aplayPath !== null) return aplayPath
|
||||
if (aplayPromise) return aplayPromise
|
||||
|
||||
aplayPromise = (async () => {
|
||||
const path = await findCommand("aplay")
|
||||
aplayPath = path
|
||||
return path
|
||||
})()
|
||||
|
||||
return aplayPromise
|
||||
}
|
||||
|
||||
export function startBackgroundCheck(platform: Platform): void {
|
||||
if (platform === "darwin") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
|
||||
|
||||
import { createSessionNotification } from "./session-notification"
|
||||
import { createSessionNotificationHook } from "./session-notification"
|
||||
import { setMainSession, subagentSessions, _resetForTesting } from "../features/claude-code-session-state"
|
||||
import * as utils from "./session-notification-utils"
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("session-notification", () => {
|
||||
afterEach(() => {
|
||||
// #given - cleanup after each test
|
||||
subagentSessions.clear()
|
||||
_resetForTesting()
|
||||
setMainSession(undefined)
|
||||
})
|
||||
|
||||
test("should not trigger notification for subagent session", async () => {
|
||||
@@ -53,7 +53,7 @@ describe("session-notification", () => {
|
||||
const subagentSessionID = "subagent-123"
|
||||
subagentSessions.add(subagentSessionID)
|
||||
|
||||
const hook = createSessionNotification(createMockPluginInput(), {
|
||||
const hook = createSessionNotificationHook(createMockPluginInput(), {
|
||||
idleConfirmationDelay: 0,
|
||||
})
|
||||
|
||||
@@ -78,7 +78,7 @@ describe("session-notification", () => {
|
||||
const otherSessionID = "other-456"
|
||||
setMainSession(mainSessionID)
|
||||
|
||||
const hook = createSessionNotification(createMockPluginInput(), {
|
||||
const hook = createSessionNotificationHook(createMockPluginInput(), {
|
||||
idleConfirmationDelay: 0,
|
||||
})
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("session-notification", () => {
|
||||
const mainSessionID = "main-789"
|
||||
setMainSession(mainSessionID)
|
||||
|
||||
const hook = createSessionNotification(createMockPluginInput(), {
|
||||
const hook = createSessionNotificationHook(createMockPluginInput(), {
|
||||
idleConfirmationDelay: 10,
|
||||
skipIfIncompleteTodos: false,
|
||||
})
|
||||
@@ -129,7 +129,7 @@ describe("session-notification", () => {
|
||||
setMainSession(mainSessionID)
|
||||
subagentSessions.add(subagentSessionID)
|
||||
|
||||
const hook = createSessionNotification(createMockPluginInput(), {
|
||||
const hook = createSessionNotificationHook(createMockPluginInput(), {
|
||||
idleConfirmationDelay: 0,
|
||||
})
|
||||
|
||||
@@ -156,7 +156,7 @@ describe("session-notification", () => {
|
||||
setMainSession(mainSessionID)
|
||||
subagentSessions.add(subagentSessionID)
|
||||
|
||||
const hook = createSessionNotification(createMockPluginInput(), {
|
||||
const hook = createSessionNotificationHook(createMockPluginInput(), {
|
||||
idleConfirmationDelay: 0,
|
||||
})
|
||||
|
||||
@@ -188,7 +188,7 @@ describe("session-notification", () => {
|
||||
const mainSessionID = "main-cancel"
|
||||
setMainSession(mainSessionID)
|
||||
|
||||
const hook = createSessionNotification(createMockPluginInput(), {
|
||||
const hook = createSessionNotificationHook(createMockPluginInput(), {
|
||||
idleConfirmationDelay: 100, // Long delay
|
||||
skipIfIncompleteTodos: false,
|
||||
})
|
||||
@@ -218,7 +218,7 @@ describe("session-notification", () => {
|
||||
|
||||
test("should handle session.created event without notification", async () => {
|
||||
// #given - a new session is created
|
||||
const hook = createSessionNotification(createMockPluginInput(), {})
|
||||
const hook = createSessionNotificationHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session.created event fires
|
||||
await hook({
|
||||
@@ -239,7 +239,7 @@ describe("session-notification", () => {
|
||||
|
||||
test("should handle session.deleted event and cleanup state", async () => {
|
||||
// #given - a session exists
|
||||
const hook = createSessionNotification(createMockPluginInput(), {})
|
||||
const hook = createSessionNotificationHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session.deleted event fires
|
||||
await hook({
|
||||
@@ -263,7 +263,7 @@ describe("session-notification", () => {
|
||||
const mainSessionID = "main-message"
|
||||
setMainSession(mainSessionID)
|
||||
|
||||
const hook = createSessionNotification(createMockPluginInput(), {
|
||||
const hook = createSessionNotificationHook(createMockPluginInput(), {
|
||||
idleConfirmationDelay: 50,
|
||||
skipIfIncompleteTodos: false,
|
||||
})
|
||||
@@ -297,7 +297,7 @@ describe("session-notification", () => {
|
||||
const mainSessionID = "main-tool"
|
||||
setMainSession(mainSessionID)
|
||||
|
||||
const hook = createSessionNotification(createMockPluginInput(), {
|
||||
const hook = createSessionNotificationHook(createMockPluginInput(), {
|
||||
idleConfirmationDelay: 50,
|
||||
skipIfIncompleteTodos: false,
|
||||
})
|
||||
@@ -329,7 +329,7 @@ describe("session-notification", () => {
|
||||
const mainSessionID = "main-dup"
|
||||
setMainSession(mainSessionID)
|
||||
|
||||
const hook = createSessionNotification(createMockPluginInput(), {
|
||||
const hook = createSessionNotificationHook(createMockPluginInput(), {
|
||||
idleConfirmationDelay: 10,
|
||||
skipIfIncompleteTodos: false,
|
||||
})
|
||||
|
||||
@@ -139,7 +139,7 @@ async function hasIncompleteTodos(ctx: PluginInput, sessionID: string): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
export function createSessionNotification(
|
||||
export function createSessionNotificationHook(
|
||||
ctx: PluginInput,
|
||||
config: SessionNotificationConfig = {}
|
||||
) {
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createStopContinuationGuardHook } from "./index"
|
||||
|
||||
describe("stop-continuation-guard", () => {
|
||||
function createMockPluginInput() {
|
||||
return {
|
||||
client: {
|
||||
tui: {
|
||||
showToast: async () => ({}),
|
||||
},
|
||||
},
|
||||
directory: "/tmp/test",
|
||||
} as never
|
||||
}
|
||||
|
||||
test("should mark session as stopped", () => {
|
||||
// #given - a guard hook with no stopped sessions
|
||||
const guard = createStopContinuationGuardHook(createMockPluginInput())
|
||||
const sessionID = "test-session-1"
|
||||
|
||||
// #when - we stop continuation for the session
|
||||
guard.stop(sessionID)
|
||||
|
||||
// #then - session should be marked as stopped
|
||||
expect(guard.isStopped(sessionID)).toBe(true)
|
||||
})
|
||||
|
||||
test("should return false for non-stopped sessions", () => {
|
||||
// #given - a guard hook with no stopped sessions
|
||||
const guard = createStopContinuationGuardHook(createMockPluginInput())
|
||||
|
||||
// #when - we check a session that was never stopped
|
||||
|
||||
// #then - it should return false
|
||||
expect(guard.isStopped("non-existent-session")).toBe(false)
|
||||
})
|
||||
|
||||
test("should clear stopped state for a session", () => {
|
||||
// #given - a session that was stopped
|
||||
const guard = createStopContinuationGuardHook(createMockPluginInput())
|
||||
const sessionID = "test-session-2"
|
||||
guard.stop(sessionID)
|
||||
|
||||
// #when - we clear the session
|
||||
guard.clear(sessionID)
|
||||
|
||||
// #then - session should no longer be stopped
|
||||
expect(guard.isStopped(sessionID)).toBe(false)
|
||||
})
|
||||
|
||||
test("should handle multiple sessions independently", () => {
|
||||
// #given - multiple sessions with different stop states
|
||||
const guard = createStopContinuationGuardHook(createMockPluginInput())
|
||||
const session1 = "session-1"
|
||||
const session2 = "session-2"
|
||||
const session3 = "session-3"
|
||||
|
||||
// #when - we stop some sessions but not others
|
||||
guard.stop(session1)
|
||||
guard.stop(session2)
|
||||
|
||||
// #then - each session has its own state
|
||||
expect(guard.isStopped(session1)).toBe(true)
|
||||
expect(guard.isStopped(session2)).toBe(true)
|
||||
expect(guard.isStopped(session3)).toBe(false)
|
||||
})
|
||||
|
||||
test("should clear session on session.deleted event", async () => {
|
||||
// #given - a session that was stopped
|
||||
const guard = createStopContinuationGuardHook(createMockPluginInput())
|
||||
const sessionID = "test-session-3"
|
||||
guard.stop(sessionID)
|
||||
|
||||
// #when - session is deleted
|
||||
await guard.event({
|
||||
event: {
|
||||
type: "session.deleted",
|
||||
properties: { info: { id: sessionID } },
|
||||
},
|
||||
})
|
||||
|
||||
// #then - session should no longer be stopped (cleaned up)
|
||||
expect(guard.isStopped(sessionID)).toBe(false)
|
||||
})
|
||||
|
||||
test("should not affect other sessions on session.deleted", async () => {
|
||||
// #given - multiple stopped sessions
|
||||
const guard = createStopContinuationGuardHook(createMockPluginInput())
|
||||
const session1 = "session-keep"
|
||||
const session2 = "session-delete"
|
||||
guard.stop(session1)
|
||||
guard.stop(session2)
|
||||
|
||||
// #when - one session is deleted
|
||||
await guard.event({
|
||||
event: {
|
||||
type: "session.deleted",
|
||||
properties: { info: { id: session2 } },
|
||||
},
|
||||
})
|
||||
|
||||
// #then - other session should remain stopped
|
||||
expect(guard.isStopped(session1)).toBe(true)
|
||||
expect(guard.isStopped(session2)).toBe(false)
|
||||
})
|
||||
|
||||
test("should clear stopped state on new user message (chat.message)", async () => {
|
||||
// #given - a session that was stopped
|
||||
const guard = createStopContinuationGuardHook(createMockPluginInput())
|
||||
const sessionID = "test-session-4"
|
||||
guard.stop(sessionID)
|
||||
expect(guard.isStopped(sessionID)).toBe(true)
|
||||
|
||||
// #when - user sends a new message
|
||||
await guard["chat.message"]({ sessionID })
|
||||
|
||||
// #then - stop state should be cleared (one-time only)
|
||||
expect(guard.isStopped(sessionID)).toBe(false)
|
||||
})
|
||||
|
||||
test("should not affect non-stopped sessions on chat.message", async () => {
|
||||
// #given - a session that was never stopped
|
||||
const guard = createStopContinuationGuardHook(createMockPluginInput())
|
||||
const sessionID = "test-session-5"
|
||||
|
||||
// #when - user sends a message (session was never stopped)
|
||||
await guard["chat.message"]({ sessionID })
|
||||
|
||||
// #then - should not throw and session remains not stopped
|
||||
expect(guard.isStopped(sessionID)).toBe(false)
|
||||
})
|
||||
|
||||
test("should handle undefined sessionID in chat.message", async () => {
|
||||
// #given - a guard with a stopped session
|
||||
const guard = createStopContinuationGuardHook(createMockPluginInput())
|
||||
guard.stop("some-session")
|
||||
|
||||
// #when - chat.message is called without sessionID
|
||||
await guard["chat.message"]({ sessionID: undefined })
|
||||
|
||||
// #then - should not throw and stopped session remains stopped
|
||||
expect(guard.isStopped("some-session")).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,67 +0,0 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
const HOOK_NAME = "stop-continuation-guard"
|
||||
|
||||
export interface StopContinuationGuard {
|
||||
event: (input: { event: { type: string; properties?: unknown } }) => Promise<void>
|
||||
"chat.message": (input: { sessionID?: string }) => Promise<void>
|
||||
stop: (sessionID: string) => void
|
||||
isStopped: (sessionID: string) => boolean
|
||||
clear: (sessionID: string) => void
|
||||
}
|
||||
|
||||
export function createStopContinuationGuardHook(
|
||||
_ctx: PluginInput
|
||||
): StopContinuationGuard {
|
||||
const stoppedSessions = new Set<string>()
|
||||
|
||||
const stop = (sessionID: string): void => {
|
||||
stoppedSessions.add(sessionID)
|
||||
log(`[${HOOK_NAME}] Continuation stopped for session`, { sessionID })
|
||||
}
|
||||
|
||||
const isStopped = (sessionID: string): boolean => {
|
||||
return stoppedSessions.has(sessionID)
|
||||
}
|
||||
|
||||
const clear = (sessionID: string): void => {
|
||||
stoppedSessions.delete(sessionID)
|
||||
log(`[${HOOK_NAME}] Continuation guard cleared for session`, { sessionID })
|
||||
}
|
||||
|
||||
const event = async ({
|
||||
event,
|
||||
}: {
|
||||
event: { type: string; properties?: unknown }
|
||||
}): Promise<void> => {
|
||||
const props = event.properties as Record<string, unknown> | undefined
|
||||
|
||||
if (event.type === "session.deleted") {
|
||||
const sessionInfo = props?.info as { id?: string } | undefined
|
||||
if (sessionInfo?.id) {
|
||||
clear(sessionInfo.id)
|
||||
log(`[${HOOK_NAME}] Session deleted: cleaned up`, { sessionID: sessionInfo.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chatMessage = async ({
|
||||
sessionID,
|
||||
}: {
|
||||
sessionID?: string
|
||||
}): Promise<void> => {
|
||||
if (sessionID && stoppedSessions.has(sessionID)) {
|
||||
clear(sessionID)
|
||||
log(`[${HOOK_NAME}] Cleared stop state on new user message`, { sessionID })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
event,
|
||||
"chat.message": chatMessage,
|
||||
stop,
|
||||
isStopped,
|
||||
clear,
|
||||
}
|
||||
}
|
||||
@@ -458,71 +458,4 @@ describe("think-mode switcher", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Z.AI GLM-4.7 provider support", () => {
|
||||
describe("getThinkingConfig for zai-coding-plan", () => {
|
||||
it("should return thinking config for glm-4.7", () => {
|
||||
// #given zai-coding-plan provider with glm-4.7 model
|
||||
const config = getThinkingConfig("zai-coding-plan", "glm-4.7")
|
||||
|
||||
// #then should return zai-coding-plan thinking config
|
||||
expect(config).not.toBeNull()
|
||||
expect(config?.providerOptions).toBeDefined()
|
||||
const zaiOptions = (config?.providerOptions as Record<string, unknown>)?.[
|
||||
"zai-coding-plan"
|
||||
] as Record<string, unknown>
|
||||
expect(zaiOptions?.extra_body).toBeDefined()
|
||||
const extraBody = zaiOptions?.extra_body as Record<string, unknown>
|
||||
expect(extraBody?.thinking).toBeDefined()
|
||||
expect((extraBody?.thinking as Record<string, unknown>)?.type).toBe("enabled")
|
||||
expect((extraBody?.thinking as Record<string, unknown>)?.clear_thinking).toBe(false)
|
||||
})
|
||||
|
||||
it("should return thinking config for glm-4.6v (multimodal)", () => {
|
||||
// #given zai-coding-plan provider with glm-4.6v model
|
||||
const config = getThinkingConfig("zai-coding-plan", "glm-4.6v")
|
||||
|
||||
// #then should return zai-coding-plan thinking config
|
||||
expect(config).not.toBeNull()
|
||||
expect(config?.providerOptions).toBeDefined()
|
||||
})
|
||||
|
||||
it("should return null for non-GLM models on zai-coding-plan", () => {
|
||||
// #given zai-coding-plan provider with unknown model
|
||||
const config = getThinkingConfig("zai-coding-plan", "some-other-model")
|
||||
|
||||
// #then should return null
|
||||
expect(config).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("HIGH_VARIANT_MAP for GLM", () => {
|
||||
it("should NOT have high variant for glm-4.7 (thinking enabled by default)", () => {
|
||||
// #given glm-4.7 model
|
||||
const variant = getHighVariant("glm-4.7")
|
||||
|
||||
// #then should return null (no high variant needed)
|
||||
expect(variant).toBeNull()
|
||||
})
|
||||
|
||||
it("should NOT have high variant for glm-4.6v", () => {
|
||||
// #given glm-4.6v model
|
||||
const variant = getHighVariant("glm-4.6v")
|
||||
|
||||
// #then should return null
|
||||
expect(variant).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("THINKING_CONFIGS structure for zai-coding-plan", () => {
|
||||
it("should have correct structure for zai-coding-plan", () => {
|
||||
const config = THINKING_CONFIGS["zai-coding-plan"]
|
||||
expect(config.providerOptions).toBeDefined()
|
||||
const zaiOptions = (config.providerOptions as Record<string, unknown>)?.[
|
||||
"zai-coding-plan"
|
||||
] as Record<string, unknown>
|
||||
expect(zaiOptions?.extra_body).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -149,18 +149,6 @@ export const THINKING_CONFIGS = {
|
||||
openai: {
|
||||
reasoning_effort: "high",
|
||||
},
|
||||
"zai-coding-plan": {
|
||||
providerOptions: {
|
||||
"zai-coding-plan": {
|
||||
extra_body: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
clear_thinking: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, Record<string, unknown>>
|
||||
|
||||
const THINKING_CAPABLE_MODELS = {
|
||||
@@ -169,7 +157,6 @@ const THINKING_CAPABLE_MODELS = {
|
||||
google: ["gemini-2", "gemini-3"],
|
||||
"google-vertex": ["gemini-2", "gemini-3"],
|
||||
openai: ["gpt-5", "o1", "o3"],
|
||||
"zai-coding-plan": ["glm"],
|
||||
} as const satisfies Record<string, readonly string[]>
|
||||
|
||||
export function getHighVariant(modelID: string): string | null {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||
|
||||
import type { BackgroundManager } from "../features/background-agent"
|
||||
import { setMainSession, subagentSessions, _resetForTesting } from "../features/claude-code-session-state"
|
||||
import { createTodoContinuationEnforcer } from "./todo-continuation-enforcer"
|
||||
import { createTodoContinuationEnforcerHook } from "./todo-continuation-enforcer"
|
||||
|
||||
type TimerCallback = (...args: any[]) => void
|
||||
|
||||
@@ -191,7 +191,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-123"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {
|
||||
backgroundManager: createMockBackgroundManager(false),
|
||||
})
|
||||
|
||||
@@ -221,7 +221,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ id: "1", content: "Task 1", status: "completed", priority: "high" },
|
||||
]})
|
||||
|
||||
const hook = createTodoContinuationEnforcer(mockInput, {})
|
||||
const hook = createTodoContinuationEnforcerHook(mockInput, {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -239,7 +239,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-789"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {
|
||||
backgroundManager: createMockBackgroundManager(true),
|
||||
})
|
||||
|
||||
@@ -259,7 +259,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
setMainSession("main-session")
|
||||
const otherSession = "other-session"
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - non-main session goes idle
|
||||
await hook.handler({
|
||||
@@ -278,7 +278,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const bgTaskSession = "bg-task-session"
|
||||
subagentSessions.add(bgTaskSession)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - background task session goes idle
|
||||
await hook.handler({
|
||||
@@ -298,7 +298,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-cancel"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -324,7 +324,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-grace"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -350,7 +350,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-assistant"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -377,7 +377,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-tool"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -401,7 +401,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-recovery"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - mark as recovering
|
||||
hook.markRecovering(sessionID)
|
||||
@@ -422,7 +422,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-recovery-done"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - mark as recovering then complete
|
||||
hook.markRecovering(sessionID)
|
||||
@@ -444,7 +444,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-delete"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -469,7 +469,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
setMainSession(sessionID)
|
||||
|
||||
// #when - create hook with skipAgents option (should not throw)
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {
|
||||
skipAgents: ["Prometheus (Planner)", "custom-agent"],
|
||||
})
|
||||
|
||||
@@ -487,7 +487,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-toast"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -505,7 +505,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-no-throttle"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - first idle cycle completes
|
||||
await hook.handler({
|
||||
@@ -537,7 +537,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-noabort-error"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - non-abort error occurs (e.g., network error, API error)
|
||||
await hook.handler({
|
||||
@@ -581,7 +581,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ info: { id: "msg-2", role: "assistant", error: { name: "MessageAbortedError", data: { message: "The operation was aborted" } } } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -604,7 +604,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ info: { id: "msg-2", role: "assistant" } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -627,7 +627,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ info: { id: "msg-2", role: "user" } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -650,7 +650,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ info: { id: "msg-2", role: "assistant", error: { name: "AbortError" } } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -672,7 +672,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ info: { id: "msg-2", role: "assistant" } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - abort error event fires
|
||||
await hook.handler({
|
||||
@@ -702,7 +702,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ info: { id: "msg-2", role: "assistant" } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - AbortError event fires
|
||||
await hook.handler({
|
||||
@@ -732,7 +732,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ info: { id: "msg-2", role: "assistant" } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - abort error fires
|
||||
await hook.handler({
|
||||
@@ -764,7 +764,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ info: { id: "msg-2", role: "assistant" } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - abort error fires
|
||||
await hook.handler({
|
||||
@@ -803,7 +803,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ info: { id: "msg-2", role: "assistant" } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - abort error fires
|
||||
await hook.handler({
|
||||
@@ -841,7 +841,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ info: { id: "msg-2", role: "assistant" } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - abort error fires
|
||||
await hook.handler({
|
||||
@@ -879,7 +879,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ info: { id: "msg-2", role: "assistant" } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - abort error event fires (but API doesn't have it yet)
|
||||
await hook.handler({
|
||||
@@ -909,7 +909,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
{ info: { id: "msg-2", role: "assistant", error: { name: "MessageAbortedError" } } },
|
||||
]
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {})
|
||||
|
||||
// #when - session goes idle without prior session.error event
|
||||
await hook.handler({
|
||||
@@ -927,7 +927,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-model-preserve"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {
|
||||
const hook = createTodoContinuationEnforcerHook(createMockPluginInput(), {
|
||||
backgroundManager: createMockBackgroundManager(false),
|
||||
})
|
||||
|
||||
@@ -977,7 +977,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
directory: "/tmp/test",
|
||||
} as any
|
||||
|
||||
const hook = createTodoContinuationEnforcer(mockInput, {
|
||||
const hook = createTodoContinuationEnforcerHook(mockInput, {
|
||||
backgroundManager: createMockBackgroundManager(false),
|
||||
})
|
||||
|
||||
@@ -1029,7 +1029,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
directory: "/tmp/test",
|
||||
} as any
|
||||
|
||||
const hook = createTodoContinuationEnforcer(mockInput, {
|
||||
const hook = createTodoContinuationEnforcerHook(mockInput, {
|
||||
backgroundManager: createMockBackgroundManager(false),
|
||||
})
|
||||
|
||||
@@ -1073,7 +1073,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
directory: "/tmp/test",
|
||||
} as any
|
||||
|
||||
const hook = createTodoContinuationEnforcer(mockInput, {})
|
||||
const hook = createTodoContinuationEnforcerHook(mockInput, {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -1119,7 +1119,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
directory: "/tmp/test",
|
||||
} as any
|
||||
|
||||
const hook = createTodoContinuationEnforcer(mockInput, {})
|
||||
const hook = createTodoContinuationEnforcerHook(mockInput, {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
@@ -1164,7 +1164,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
directory: "/tmp/test",
|
||||
} as any
|
||||
|
||||
const hook = createTodoContinuationEnforcer(mockInput, {
|
||||
const hook = createTodoContinuationEnforcerHook(mockInput, {
|
||||
skipAgents: [],
|
||||
})
|
||||
|
||||
@@ -1178,68 +1178,4 @@ describe("todo-continuation-enforcer", () => {
|
||||
// #then - continuation injected (no agents to skip)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
})
|
||||
|
||||
test("should not inject when isContinuationStopped returns true", async () => {
|
||||
// #given - session with continuation stopped
|
||||
const sessionID = "main-stopped"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {
|
||||
isContinuationStopped: (id) => id === sessionID,
|
||||
})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await fakeTimers.advanceBy(3000)
|
||||
|
||||
// #then - no continuation injected (stopped flag is true)
|
||||
expect(promptCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should inject when isContinuationStopped returns false", async () => {
|
||||
// #given - session with continuation not stopped
|
||||
const sessionID = "main-not-stopped"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {
|
||||
isContinuationStopped: () => false,
|
||||
})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await fakeTimers.advanceBy(3000)
|
||||
|
||||
// #then - continuation injected (stopped flag is false)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
})
|
||||
|
||||
test("should cancel all countdowns via cancelAllCountdowns", async () => {
|
||||
// #given - multiple sessions with running countdowns
|
||||
const session1 = "main-cancel-all-1"
|
||||
const session2 = "main-cancel-all-2"
|
||||
setMainSession(session1)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
|
||||
// #when - first session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID: session1 } },
|
||||
})
|
||||
await fakeTimers.advanceBy(500)
|
||||
|
||||
// #when - cancel all countdowns
|
||||
hook.cancelAllCountdowns()
|
||||
|
||||
// #when - advance past countdown time
|
||||
await fakeTimers.advanceBy(3000)
|
||||
|
||||
// #then - no continuation injected (all countdowns cancelled)
|
||||
expect(promptCalls).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,14 +18,12 @@ const DEFAULT_SKIP_AGENTS = ["prometheus", "compaction"]
|
||||
export interface TodoContinuationEnforcerOptions {
|
||||
backgroundManager?: BackgroundManager
|
||||
skipAgents?: string[]
|
||||
isContinuationStopped?: (sessionID: string) => boolean
|
||||
}
|
||||
|
||||
export interface TodoContinuationEnforcer {
|
||||
handler: (input: { event: { type: string; properties?: unknown } }) => Promise<void>
|
||||
markRecovering: (sessionID: string) => void
|
||||
markRecoveryComplete: (sessionID: string) => void
|
||||
cancelAllCountdowns: () => void
|
||||
}
|
||||
|
||||
interface Todo {
|
||||
@@ -93,11 +91,11 @@ function isLastAssistantMessageAborted(messages: Array<{ info?: MessageInfo }>):
|
||||
return errorName === "MessageAbortedError" || errorName === "AbortError"
|
||||
}
|
||||
|
||||
export function createTodoContinuationEnforcer(
|
||||
export function createTodoContinuationEnforcerHook(
|
||||
ctx: PluginInput,
|
||||
options: TodoContinuationEnforcerOptions = {}
|
||||
): TodoContinuationEnforcer {
|
||||
const { backgroundManager, skipAgents = DEFAULT_SKIP_AGENTS, isContinuationStopped } = options
|
||||
const { backgroundManager, skipAgents = DEFAULT_SKIP_AGENTS } = options
|
||||
const sessions = new Map<string, SessionState>()
|
||||
|
||||
function getState(sessionID: string): SessionState {
|
||||
@@ -422,11 +420,6 @@ export function createTodoContinuationEnforcer(
|
||||
return
|
||||
}
|
||||
|
||||
if (isContinuationStopped?.(sessionID)) {
|
||||
log(`[${HOOK_NAME}] Skipped: continuation stopped for session`, { sessionID })
|
||||
return
|
||||
}
|
||||
|
||||
startCountdown(sessionID, incompleteCount, todos.length, resolvedInfo)
|
||||
return
|
||||
}
|
||||
@@ -492,17 +485,9 @@ export function createTodoContinuationEnforcer(
|
||||
}
|
||||
}
|
||||
|
||||
const cancelAllCountdowns = (): void => {
|
||||
for (const sessionID of sessions.keys()) {
|
||||
cancelCountdown(sessionID)
|
||||
}
|
||||
log(`[${HOOK_NAME}] All countdowns cancelled`)
|
||||
}
|
||||
|
||||
return {
|
||||
handler,
|
||||
markRecovering,
|
||||
markRecoveryComplete,
|
||||
cancelAllCountdowns,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { includesCaseInsensitive } from "./shared"
|
||||
|
||||
/**
|
||||
* Tests for conditional tool registration logic in index.ts
|
||||
*
|
||||
@@ -11,10 +13,8 @@ describe("look_at tool conditional registration", () => {
|
||||
// #when checking if agent is enabled
|
||||
// #then should return false (disabled)
|
||||
it("returns false when multimodal-looker is disabled (exact case)", () => {
|
||||
const disabledAgents: string[] = ["multimodal-looker"]
|
||||
const isEnabled = !disabledAgents.some(
|
||||
(agent) => agent.toLowerCase() === "multimodal-looker"
|
||||
)
|
||||
const disabledAgents = ["multimodal-looker"]
|
||||
const isEnabled = !includesCaseInsensitive(disabledAgents, "multimodal-looker")
|
||||
expect(isEnabled).toBe(false)
|
||||
})
|
||||
|
||||
@@ -22,10 +22,8 @@ describe("look_at tool conditional registration", () => {
|
||||
// #when checking if agent is enabled
|
||||
// #then should return false (case-insensitive match)
|
||||
it("returns false when multimodal-looker is disabled (case-insensitive)", () => {
|
||||
const disabledAgents: string[] = ["Multimodal-Looker"]
|
||||
const isEnabled = !disabledAgents.some(
|
||||
(agent) => agent.toLowerCase() === "multimodal-looker"
|
||||
)
|
||||
const disabledAgents = ["Multimodal-Looker"]
|
||||
const isEnabled = !includesCaseInsensitive(disabledAgents, "multimodal-looker")
|
||||
expect(isEnabled).toBe(false)
|
||||
})
|
||||
|
||||
@@ -33,10 +31,8 @@ describe("look_at tool conditional registration", () => {
|
||||
// #when checking if agent is enabled
|
||||
// #then should return true (enabled)
|
||||
it("returns true when multimodal-looker is not disabled", () => {
|
||||
const disabledAgents: string[] = ["oracle", "librarian"]
|
||||
const isEnabled = !disabledAgents.some(
|
||||
(agent) => agent.toLowerCase() === "multimodal-looker"
|
||||
)
|
||||
const disabledAgents = ["oracle", "librarian"]
|
||||
const isEnabled = !includesCaseInsensitive(disabledAgents, "multimodal-looker")
|
||||
expect(isEnabled).toBe(true)
|
||||
})
|
||||
|
||||
@@ -45,9 +41,7 @@ describe("look_at tool conditional registration", () => {
|
||||
// #then should return true (enabled by default)
|
||||
it("returns true when disabled_agents is empty", () => {
|
||||
const disabledAgents: string[] = []
|
||||
const isEnabled = !disabledAgents.some(
|
||||
(agent) => agent.toLowerCase() === "multimodal-looker"
|
||||
)
|
||||
const isEnabled = !includesCaseInsensitive(disabledAgents, "multimodal-looker")
|
||||
expect(isEnabled).toBe(true)
|
||||
})
|
||||
|
||||
@@ -55,11 +49,8 @@ describe("look_at tool conditional registration", () => {
|
||||
// #when checking if agent is enabled
|
||||
// #then should return true (enabled by default)
|
||||
it("returns true when disabled_agents is undefined (fallback to empty)", () => {
|
||||
const disabledAgents: string[] | undefined = undefined
|
||||
const list: string[] = disabledAgents ?? []
|
||||
const isEnabled = !list.some(
|
||||
(agent) => agent.toLowerCase() === "multimodal-looker"
|
||||
)
|
||||
const disabledAgents = undefined
|
||||
const isEnabled = !includesCaseInsensitive(disabledAgents ?? [], "multimodal-looker")
|
||||
expect(isEnabled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
71
src/index.ts
@@ -12,6 +12,8 @@ import {
|
||||
createThinkModeHook,
|
||||
createClaudeCodeHooksHook,
|
||||
createAnthropicContextWindowLimitRecoveryHook,
|
||||
|
||||
createCompactionContextInjector,
|
||||
createRulesInjectorHook,
|
||||
createBackgroundNotificationHook,
|
||||
createAutoUpdateCheckerHook,
|
||||
@@ -33,7 +35,6 @@ import {
|
||||
createSisyphusJuniorNotepadHook,
|
||||
createQuestionLabelTruncatorHook,
|
||||
createSubagentQuestionBlockerHook,
|
||||
createStopContinuationGuardHook,
|
||||
} from "./hooks";
|
||||
import {
|
||||
contextCollector,
|
||||
@@ -76,11 +77,10 @@ import { BackgroundManager } from "./features/background-agent";
|
||||
import { SkillMcpManager } from "./features/skill-mcp-manager";
|
||||
import { initTaskToastManager } from "./features/task-toast-manager";
|
||||
import { TmuxSessionManager } from "./features/tmux-subagent";
|
||||
import { clearBoulderState } from "./features/boulder-state";
|
||||
import { type HookName } from "./config";
|
||||
import { log, detectExternalNotificationPlugin, getNotificationConflictWarning, resetMessageCursor, hasConnectedProvidersCache, getOpenCodeVersion, isOpenCodeVersionAtLeast, OPENCODE_NATIVE_AGENTS_INJECTION_VERSION } from "./shared";
|
||||
import { log, detectExternalNotificationPlugin, getNotificationConflictWarning, resetMessageCursor, includesCaseInsensitive, hasConnectedProvidersCache, getOpenCodeVersion, isOpenCodeVersionAtLeast, OPENCODE_NATIVE_AGENTS_INJECTION_VERSION } from "./shared";
|
||||
import { loadPluginConfig } from "./plugin-config";
|
||||
import { createModelCacheState } from "./plugin-state";
|
||||
import { createModelCacheState, getModelLimit } from "./plugin-state";
|
||||
import { createConfigHandler } from "./plugin-handlers";
|
||||
|
||||
const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
@@ -174,6 +174,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
experimental: pluginConfig.experimental,
|
||||
})
|
||||
: null;
|
||||
const compactionContextInjector = isHookEnabled("compaction-context-injector")
|
||||
? createCompactionContextInjector()
|
||||
: undefined;
|
||||
const rulesInjector = isHookEnabled("rules-injector")
|
||||
? createRulesInjectorHook(ctx)
|
||||
: null;
|
||||
@@ -274,15 +277,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
|
||||
initTaskToastManager(ctx.client);
|
||||
|
||||
const stopContinuationGuard = isHookEnabled("stop-continuation-guard")
|
||||
? createStopContinuationGuardHook(ctx)
|
||||
: null;
|
||||
|
||||
const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer")
|
||||
? createTodoContinuationEnforcer(ctx, {
|
||||
backgroundManager,
|
||||
isContinuationStopped: stopContinuationGuard?.isStopped,
|
||||
})
|
||||
? createTodoContinuationEnforcer(ctx, { backgroundManager })
|
||||
: null;
|
||||
|
||||
if (sessionRecovery && todoContinuationEnforcer) {
|
||||
@@ -298,8 +294,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
|
||||
|
||||
const callOmoAgent = createCallOmoAgent(ctx, backgroundManager);
|
||||
const isMultimodalLookerEnabled = !(pluginConfig.disabled_agents ?? []).some(
|
||||
(agent) => agent.toLowerCase() === "multimodal-looker"
|
||||
const isMultimodalLookerEnabled = !includesCaseInsensitive(
|
||||
pluginConfig.disabled_agents ?? [],
|
||||
"multimodal-looker"
|
||||
);
|
||||
const lookAt = isMultimodalLookerEnabled ? createLookAt(ctx) : null;
|
||||
const browserProvider = pluginConfig.browser_automation_engine?.provider ?? "playwright";
|
||||
@@ -370,7 +367,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
});
|
||||
|
||||
const commands = discoverCommandsSync();
|
||||
const slashcommandTool = createSlashcommandTool({
|
||||
const slash_commandTool = createSlashcommandTool({
|
||||
commands,
|
||||
skills: mergedSkills,
|
||||
});
|
||||
@@ -394,7 +391,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
delegate_task: delegateTask,
|
||||
skill: skillTool,
|
||||
skill_mcp: skillMcpTool,
|
||||
slashcommand: slashcommandTool,
|
||||
slash_command: slash_commandTool,
|
||||
interactive_bash,
|
||||
},
|
||||
|
||||
@@ -423,7 +420,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
}
|
||||
}
|
||||
|
||||
await stopContinuationGuard?.["chat.message"]?.(input);
|
||||
await keywordDetector?.["chat.message"]?.(input, output);
|
||||
await claudeCodeHooks["chat.message"]?.(input, output);
|
||||
await autoSlashCommand?.["chat.message"]?.(input, output);
|
||||
@@ -525,7 +521,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
await categorySkillReminder?.event(input);
|
||||
await interactiveBashSession?.event(input);
|
||||
await ralphLoop?.event(input);
|
||||
await stopContinuationGuard?.event(input);
|
||||
await atlasHook?.handler(input);
|
||||
|
||||
const { event } = input;
|
||||
@@ -586,12 +581,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const recovered =
|
||||
await sessionRecovery.handleSessionRecovery(messageInfo);
|
||||
|
||||
if (
|
||||
recovered &&
|
||||
sessionID &&
|
||||
sessionID === getMainSessionID() &&
|
||||
!stopContinuationGuard?.isStopped(sessionID)
|
||||
) {
|
||||
if (recovered && sessionID && sessionID === getMainSessionID()) {
|
||||
await ctx.client.session
|
||||
.prompt({
|
||||
path: { id: sessionID },
|
||||
@@ -620,8 +610,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
if (input.tool === "task") {
|
||||
const args = output.args as Record<string, unknown>;
|
||||
const subagentType = args.subagent_type as string;
|
||||
const isExploreOrLibrarian = ["explore", "librarian"].some(
|
||||
(name) => name.toLowerCase() === (subagentType ?? "").toLowerCase()
|
||||
const isExploreOrLibrarian = includesCaseInsensitive(
|
||||
["explore", "librarian"],
|
||||
subagentType ?? ""
|
||||
);
|
||||
|
||||
args.tools = {
|
||||
@@ -631,7 +622,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (ralphLoop && input.tool === "slashcommand") {
|
||||
if (ralphLoop && input.tool === "slash_command") {
|
||||
const args = output.args as { command?: string } | undefined;
|
||||
const command = args?.command?.replace(/^\//, "").toLowerCase();
|
||||
const sessionID = input.sessionID || getMainSessionID();
|
||||
@@ -673,28 +664,14 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
);
|
||||
|
||||
ralphLoop.startLoop(sessionID, prompt, {
|
||||
ultrawork: true,
|
||||
maxIterations: maxIterMatch
|
||||
? parseInt(maxIterMatch[1], 10)
|
||||
: undefined,
|
||||
completionPromise: promiseMatch?.[1],
|
||||
});
|
||||
ultrawork: true,
|
||||
maxIterations: maxIterMatch
|
||||
? parseInt(maxIterMatch[1], 10)
|
||||
: undefined,
|
||||
completionPromise: promiseMatch?.[1],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (input.tool === "slashcommand") {
|
||||
const args = output.args as { command?: string } | undefined;
|
||||
const command = args?.command?.replace(/^\//, "").toLowerCase();
|
||||
const sessionID = input.sessionID || getMainSessionID();
|
||||
|
||||
if (command === "stop-continuation" && sessionID) {
|
||||
stopContinuationGuard?.stop(sessionID);
|
||||
todoContinuationEnforcer?.cancelAllCountdowns();
|
||||
ralphLoop?.cancelLoop(sessionID);
|
||||
clearBoulderState(ctx.directory);
|
||||
log("[stop-continuation] All continuation mechanisms stopped", { sessionID });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"tool.execute.after": async (input, output) => {
|
||||
|
||||
@@ -25,10 +25,11 @@ import { loadMcpConfigs } from "../features/claude-code-mcp-loader";
|
||||
import { loadAllPluginComponents } from "../features/claude-code-plugin-loader";
|
||||
import { createBuiltinMcps } from "../mcp";
|
||||
import type { OhMyOpenCodeConfig } from "../config";
|
||||
import { log, fetchAvailableModels, readConnectedProvidersCache, resolveModelPipeline } from "../shared";
|
||||
import { log, fetchAvailableModels, readConnectedProvidersCache } from "../shared";
|
||||
import { getOpenCodeConfigPaths } from "../shared/opencode-config-dir";
|
||||
import { migrateAgentConfig } from "../shared/permission-compat";
|
||||
import { AGENT_NAME_MAP } from "../shared/migration";
|
||||
import { resolveModelWithFallback } from "../shared/model-resolver";
|
||||
import { AGENT_MODEL_REQUIREMENTS } from "../shared/model-requirements";
|
||||
import { PROMETHEUS_SYSTEM_PROMPT, PROMETHEUS_PERMISSION } from "../agents/prometheus-prompt";
|
||||
import { DEFAULT_CATEGORIES } from "../tools/delegate-task/constants";
|
||||
@@ -207,13 +208,13 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
||||
buildConfigWithoutName as Record<string, unknown>
|
||||
);
|
||||
const openCodeBuilderOverride =
|
||||
pluginConfig.agents?.["OpenCode-Builder"];
|
||||
pluginConfig.agents?.["opencode-builder"];
|
||||
const openCodeBuilderBase = {
|
||||
...migratedBuildConfig,
|
||||
description: `${configAgent?.build?.description ?? "Build agent"} (OpenCode default)`,
|
||||
};
|
||||
|
||||
agentConfig["OpenCode-Builder"] = openCodeBuilderOverride
|
||||
agentConfig["opencode-builder"] = openCodeBuilderOverride
|
||||
? { ...openCodeBuilderBase, ...openCodeBuilderOverride }
|
||||
: openCodeBuilderBase;
|
||||
}
|
||||
@@ -258,16 +259,12 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
||||
connectedProviders: connectedProviders ?? undefined,
|
||||
});
|
||||
|
||||
const modelResolution = resolveModelPipeline({
|
||||
intent: {
|
||||
uiSelectedModel: currentModel,
|
||||
userModel: prometheusOverride?.model ?? categoryConfig?.model,
|
||||
},
|
||||
constraints: { availableModels },
|
||||
policy: {
|
||||
fallbackChain: prometheusRequirement?.fallbackChain,
|
||||
systemDefaultModel: undefined,
|
||||
},
|
||||
const modelResolution = resolveModelWithFallback({
|
||||
uiSelectedModel: currentModel,
|
||||
userModel: prometheusOverride?.model ?? categoryConfig?.model,
|
||||
fallbackChain: prometheusRequirement?.fallbackChain,
|
||||
availableModels,
|
||||
systemDefaultModel: undefined,
|
||||
});
|
||||
const resolvedModel = modelResolution?.model;
|
||||
const resolvedVariant = modelResolution?.variant;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* true = tool allowed, false = tool denied.
|
||||
*/
|
||||
|
||||
import { findCaseInsensitive } from "./case-insensitive"
|
||||
|
||||
const EXPLORATION_AGENT_DENYLIST: Record<string, boolean> = {
|
||||
write: false,
|
||||
edit: false,
|
||||
@@ -35,13 +37,10 @@ const AGENT_RESTRICTIONS: Record<string, Record<string, boolean>> = {
|
||||
}
|
||||
|
||||
export function getAgentToolRestrictions(agentName: string): Record<string, boolean> {
|
||||
return AGENT_RESTRICTIONS[agentName]
|
||||
?? Object.entries(AGENT_RESTRICTIONS).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
|
||||
?? {}
|
||||
return findCaseInsensitive(AGENT_RESTRICTIONS, agentName) ?? {}
|
||||
}
|
||||
|
||||
export function hasAgentToolRestrictions(agentName: string): boolean {
|
||||
const restrictions = AGENT_RESTRICTIONS[agentName]
|
||||
?? Object.entries(AGENT_RESTRICTIONS).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
|
||||
const restrictions = findCaseInsensitive(AGENT_RESTRICTIONS, agentName)
|
||||
return restrictions !== undefined && Object.keys(restrictions).length > 0
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OhMyOpenCodeConfig } from "../config"
|
||||
import { findCaseInsensitive } from "./case-insensitive"
|
||||
import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "./model-requirements"
|
||||
|
||||
export function resolveAgentVariant(
|
||||
@@ -12,10 +13,7 @@ export function resolveAgentVariant(
|
||||
const agentOverrides = config.agents as
|
||||
| Record<string, { variant?: string; category?: string }>
|
||||
| undefined
|
||||
const agentOverride = agentOverrides
|
||||
? agentOverrides[agentName]
|
||||
?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
|
||||
: undefined
|
||||
const agentOverride = agentOverrides ? findCaseInsensitive(agentOverrides, agentName) : undefined
|
||||
if (!agentOverride) {
|
||||
return undefined
|
||||
}
|
||||
@@ -45,10 +43,7 @@ export function resolveVariantForModel(
|
||||
const agentOverrides = config.agents as
|
||||
| Record<string, { category?: string }>
|
||||
| undefined
|
||||
const agentOverride = agentOverrides
|
||||
? agentOverrides[agentName]
|
||||
?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
|
||||
: undefined
|
||||
const agentOverride = agentOverrides ? findCaseInsensitive(agentOverrides, agentName) : undefined
|
||||
const categoryName = agentOverride?.category
|
||||
if (categoryName) {
|
||||
const categoryRequirement = CATEGORY_MODEL_REQUIREMENTS[categoryName]
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { chmodSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { spawn } from "bun";
|
||||
import { extractZip } from "./zip-extractor";
|
||||
|
||||
export function getCachedBinaryPath(cacheDir: string, binaryName: string): string | null {
|
||||
const binaryPath = path.join(cacheDir, binaryName);
|
||||
return existsSync(binaryPath) ? binaryPath : null;
|
||||
}
|
||||
|
||||
export function ensureCacheDir(cacheDir: string): void {
|
||||
if (!existsSync(cacheDir)) {
|
||||
mkdirSync(cacheDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadArchive(downloadUrl: string, archivePath: string): Promise<void> {
|
||||
const response = await fetch(downloadUrl, { redirect: "follow" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
await Bun.write(archivePath, arrayBuffer);
|
||||
}
|
||||
|
||||
export async function extractTarGz(
|
||||
archivePath: string,
|
||||
destDir: string,
|
||||
options?: { args?: string[]; cwd?: string }
|
||||
): Promise<void> {
|
||||
const args = options?.args ?? ["tar", "-xzf", archivePath, "-C", destDir];
|
||||
const proc = spawn(args, {
|
||||
cwd: options?.cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
const stderr = await new Response(proc.stderr).text();
|
||||
throw new Error(`tar extraction failed (exit ${exitCode}): ${stderr}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function extractZipArchive(archivePath: string, destDir: string): Promise<void> {
|
||||
await extractZip(archivePath, destDir);
|
||||
}
|
||||
|
||||
export function cleanupArchive(archivePath: string): void {
|
||||
if (existsSync(archivePath)) {
|
||||
unlinkSync(archivePath);
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureExecutable(binaryPath: string): void {
|
||||
if (process.platform !== "win32" && existsSync(binaryPath)) {
|
||||
chmodSync(binaryPath, 0o755);
|
||||
}
|
||||
}
|
||||
169
src/shared/case-insensitive.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import {
|
||||
findCaseInsensitive,
|
||||
includesCaseInsensitive,
|
||||
findByNameCaseInsensitive,
|
||||
equalsIgnoreCase,
|
||||
} from "./case-insensitive"
|
||||
|
||||
describe("findCaseInsensitive", () => {
|
||||
test("returns undefined for empty/undefined object", () => {
|
||||
// #given - undefined object
|
||||
const obj = undefined
|
||||
|
||||
// #when - lookup any key
|
||||
const result = findCaseInsensitive(obj, "key")
|
||||
|
||||
// #then - returns undefined
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
test("finds exact match first", () => {
|
||||
// #given - object with exact key
|
||||
const obj = { Oracle: "value1", oracle: "value2" }
|
||||
|
||||
// #when - lookup with exact case
|
||||
const result = findCaseInsensitive(obj, "Oracle")
|
||||
|
||||
// #then - returns exact match
|
||||
expect(result).toBe("value1")
|
||||
})
|
||||
|
||||
test("finds case-insensitive match when no exact match", () => {
|
||||
// #given - object with lowercase key
|
||||
const obj = { oracle: "value" }
|
||||
|
||||
// #when - lookup with uppercase
|
||||
const result = findCaseInsensitive(obj, "ORACLE")
|
||||
|
||||
// #then - returns case-insensitive match
|
||||
expect(result).toBe("value")
|
||||
})
|
||||
|
||||
test("returns undefined when key not found", () => {
|
||||
// #given - object without target key
|
||||
const obj = { other: "value" }
|
||||
|
||||
// #when - lookup missing key
|
||||
const result = findCaseInsensitive(obj, "oracle")
|
||||
|
||||
// #then - returns undefined
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("includesCaseInsensitive", () => {
|
||||
test("returns true for exact match", () => {
|
||||
// #given - array with exact value
|
||||
const arr = ["explore", "librarian"]
|
||||
|
||||
// #when - check exact match
|
||||
const result = includesCaseInsensitive(arr, "explore")
|
||||
|
||||
// #then - returns true
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test("returns true for case-insensitive match", () => {
|
||||
// #given - array with lowercase values
|
||||
const arr = ["explore", "librarian"]
|
||||
|
||||
// #when - check uppercase value
|
||||
const result = includesCaseInsensitive(arr, "EXPLORE")
|
||||
|
||||
// #then - returns true
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test("returns true for mixed case match", () => {
|
||||
// #given - array with mixed case values
|
||||
const arr = ["Oracle", "Sisyphus"]
|
||||
|
||||
// #when - check different case
|
||||
const result = includesCaseInsensitive(arr, "oracle")
|
||||
|
||||
// #then - returns true
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false when value not found", () => {
|
||||
// #given - array without target value
|
||||
const arr = ["explore", "librarian"]
|
||||
|
||||
// #when - check missing value
|
||||
const result = includesCaseInsensitive(arr, "oracle")
|
||||
|
||||
// #then - returns false
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
test("returns false for empty array", () => {
|
||||
// #given - empty array
|
||||
const arr: string[] = []
|
||||
|
||||
// #when - check any value
|
||||
const result = includesCaseInsensitive(arr, "explore")
|
||||
|
||||
// #then - returns false
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("findByNameCaseInsensitive", () => {
|
||||
test("finds element by exact name", () => {
|
||||
// #given - array with named objects
|
||||
const arr = [{ name: "Oracle", value: 1 }, { name: "explore", value: 2 }]
|
||||
|
||||
// #when - find by exact name
|
||||
const result = findByNameCaseInsensitive(arr, "Oracle")
|
||||
|
||||
// #then - returns matching element
|
||||
expect(result).toEqual({ name: "Oracle", value: 1 })
|
||||
})
|
||||
|
||||
test("finds element by case-insensitive name", () => {
|
||||
// #given - array with named objects
|
||||
const arr = [{ name: "Oracle", value: 1 }, { name: "explore", value: 2 }]
|
||||
|
||||
// #when - find by different case
|
||||
const result = findByNameCaseInsensitive(arr, "oracle")
|
||||
|
||||
// #then - returns matching element
|
||||
expect(result).toEqual({ name: "Oracle", value: 1 })
|
||||
})
|
||||
|
||||
test("returns undefined when name not found", () => {
|
||||
// #given - array without target name
|
||||
const arr = [{ name: "Oracle", value: 1 }]
|
||||
|
||||
// #when - find missing name
|
||||
const result = findByNameCaseInsensitive(arr, "librarian")
|
||||
|
||||
// #then - returns undefined
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("equalsIgnoreCase", () => {
|
||||
test("returns true for same case", () => {
|
||||
// #given - same strings
|
||||
// #when - compare
|
||||
// #then - returns true
|
||||
expect(equalsIgnoreCase("oracle", "oracle")).toBe(true)
|
||||
})
|
||||
|
||||
test("returns true for different case", () => {
|
||||
// #given - strings with different case
|
||||
// #when - compare
|
||||
// #then - returns true
|
||||
expect(equalsIgnoreCase("Oracle", "ORACLE")).toBe(true)
|
||||
expect(equalsIgnoreCase("Sisyphus-Junior", "sisyphus-junior")).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false for different strings", () => {
|
||||
// #given - different strings
|
||||
// #when - compare
|
||||
// #then - returns false
|
||||
expect(equalsIgnoreCase("oracle", "explore")).toBe(false)
|
||||
})
|
||||
})
|
||||
46
src/shared/case-insensitive.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Case-insensitive lookup and comparison utilities for agent/config names.
|
||||
* Used throughout the codebase to allow "Oracle", "oracle", "ORACLE" to work the same.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Find a value in an object using case-insensitive key matching.
|
||||
* First tries exact match, then falls back to lowercase comparison.
|
||||
*/
|
||||
export function findCaseInsensitive<T>(obj: Record<string, T> | undefined, key: string): T | undefined {
|
||||
if (!obj) return undefined
|
||||
const exactMatch = obj[key]
|
||||
if (exactMatch !== undefined) return exactMatch
|
||||
const lowerKey = key.toLowerCase()
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (k.toLowerCase() === lowerKey) return v
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an array includes a value using case-insensitive comparison.
|
||||
*/
|
||||
export function includesCaseInsensitive(arr: string[], value: string): boolean {
|
||||
const lowerValue = value.toLowerCase()
|
||||
return arr.some((item) => item.toLowerCase() === lowerValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an element in array using case-insensitive name matching.
|
||||
* Useful for finding agents/categories by name.
|
||||
*/
|
||||
export function findByNameCaseInsensitive<T extends { name: string }>(
|
||||
arr: T[],
|
||||
name: string
|
||||
): T | undefined {
|
||||
const lowerName = name.toLowerCase()
|
||||
return arr.find((item) => item.name.toLowerCase() === lowerName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two strings are equal (case-insensitive).
|
||||
*/
|
||||
export function equalsIgnoreCase(a: string, b: string): boolean {
|
||||
return a.toLowerCase() === b.toLowerCase()
|
||||
}
|
||||
@@ -20,7 +20,6 @@ export * from "./opencode-version"
|
||||
export * from "./permission-compat"
|
||||
export * from "./external-plugin-detector"
|
||||
export * from "./zip-extractor"
|
||||
export * from "./binary-downloader"
|
||||
export * from "./agent-variant"
|
||||
export * from "./session-cursor"
|
||||
export * from "./shell-env"
|
||||
@@ -28,14 +27,9 @@ export * from "./system-directive"
|
||||
export * from "./agent-tool-restrictions"
|
||||
export * from "./model-requirements"
|
||||
export * from "./model-resolver"
|
||||
export {
|
||||
resolveModelPipeline,
|
||||
type ModelResolutionRequest,
|
||||
type ModelResolutionResult as ModelResolutionPipelineResult,
|
||||
type ModelResolutionProvenance,
|
||||
} from "./model-resolution-pipeline"
|
||||
export * from "./model-availability"
|
||||
export * from "./connected-providers-cache"
|
||||
export * from "./case-insensitive"
|
||||
export * from "./session-utils"
|
||||
export * from "./tmux"
|
||||
export * from "./model-suggestion-retry"
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import { log } from "./logger"
|
||||
import { readConnectedProvidersCache } from "./connected-providers-cache"
|
||||
import { fuzzyMatchModel } from "./model-availability"
|
||||
import type { FallbackEntry } from "./model-requirements"
|
||||
|
||||
export type ModelResolutionRequest = {
|
||||
intent?: {
|
||||
uiSelectedModel?: string
|
||||
userModel?: string
|
||||
categoryDefaultModel?: string
|
||||
}
|
||||
constraints: {
|
||||
availableModels: Set<string>
|
||||
}
|
||||
policy?: {
|
||||
fallbackChain?: FallbackEntry[]
|
||||
systemDefaultModel?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type ModelResolutionProvenance =
|
||||
| "override"
|
||||
| "category-default"
|
||||
| "provider-fallback"
|
||||
| "system-default"
|
||||
|
||||
export type ModelResolutionResult = {
|
||||
model: string
|
||||
provenance: ModelResolutionProvenance
|
||||
variant?: string
|
||||
attempted?: string[]
|
||||
reason?: string
|
||||
}
|
||||
|
||||
function normalizeModel(model?: string): string | undefined {
|
||||
const trimmed = model?.trim()
|
||||
return trimmed || undefined
|
||||
}
|
||||
|
||||
export function resolveModelPipeline(
|
||||
request: ModelResolutionRequest,
|
||||
): ModelResolutionResult | undefined {
|
||||
const attempted: string[] = []
|
||||
const { intent, constraints, policy } = request
|
||||
const availableModels = constraints.availableModels
|
||||
const fallbackChain = policy?.fallbackChain
|
||||
const systemDefaultModel = policy?.systemDefaultModel
|
||||
|
||||
const normalizedUiModel = normalizeModel(intent?.uiSelectedModel)
|
||||
if (normalizedUiModel) {
|
||||
log("Model resolved via UI selection", { model: normalizedUiModel })
|
||||
return { model: normalizedUiModel, provenance: "override" }
|
||||
}
|
||||
|
||||
const normalizedUserModel = normalizeModel(intent?.userModel)
|
||||
if (normalizedUserModel) {
|
||||
log("Model resolved via config override", { model: normalizedUserModel })
|
||||
return { model: normalizedUserModel, provenance: "override" }
|
||||
}
|
||||
|
||||
const normalizedCategoryDefault = normalizeModel(intent?.categoryDefaultModel)
|
||||
if (normalizedCategoryDefault) {
|
||||
attempted.push(normalizedCategoryDefault)
|
||||
if (availableModels.size > 0) {
|
||||
const parts = normalizedCategoryDefault.split("/")
|
||||
const providerHint = parts.length >= 2 ? [parts[0]] : undefined
|
||||
const match = fuzzyMatchModel(normalizedCategoryDefault, availableModels, providerHint)
|
||||
if (match) {
|
||||
log("Model resolved via category default (fuzzy matched)", {
|
||||
original: normalizedCategoryDefault,
|
||||
matched: match,
|
||||
})
|
||||
return { model: match, provenance: "category-default", attempted }
|
||||
}
|
||||
} else {
|
||||
const connectedProviders = readConnectedProvidersCache()
|
||||
if (connectedProviders === null) {
|
||||
log("Model resolved via category default (no cache, first run)", {
|
||||
model: normalizedCategoryDefault,
|
||||
})
|
||||
return { model: normalizedCategoryDefault, provenance: "category-default", attempted }
|
||||
}
|
||||
const parts = normalizedCategoryDefault.split("/")
|
||||
if (parts.length >= 2) {
|
||||
const provider = parts[0]
|
||||
if (connectedProviders.includes(provider)) {
|
||||
log("Model resolved via category default (connected provider)", {
|
||||
model: normalizedCategoryDefault,
|
||||
})
|
||||
return { model: normalizedCategoryDefault, provenance: "category-default", attempted }
|
||||
}
|
||||
}
|
||||
}
|
||||
log("Category default model not available, falling through to fallback chain", {
|
||||
model: normalizedCategoryDefault,
|
||||
})
|
||||
}
|
||||
|
||||
if (fallbackChain && fallbackChain.length > 0) {
|
||||
if (availableModels.size === 0) {
|
||||
const connectedProviders = readConnectedProvidersCache()
|
||||
const connectedSet = connectedProviders ? new Set(connectedProviders) : null
|
||||
|
||||
if (connectedSet === null) {
|
||||
log("Model fallback chain skipped (no connected providers cache) - falling through to system default")
|
||||
} else {
|
||||
for (const entry of fallbackChain) {
|
||||
for (const provider of entry.providers) {
|
||||
if (connectedSet.has(provider)) {
|
||||
const model = `${provider}/${entry.model}`
|
||||
log("Model resolved via fallback chain (connected provider)", {
|
||||
provider,
|
||||
model: entry.model,
|
||||
variant: entry.variant,
|
||||
})
|
||||
return {
|
||||
model,
|
||||
provenance: "provider-fallback",
|
||||
variant: entry.variant,
|
||||
attempted,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log("No connected provider found in fallback chain, falling through to system default")
|
||||
}
|
||||
} else {
|
||||
for (const entry of fallbackChain) {
|
||||
for (const provider of entry.providers) {
|
||||
const fullModel = `${provider}/${entry.model}`
|
||||
const match = fuzzyMatchModel(fullModel, availableModels, [provider])
|
||||
if (match) {
|
||||
log("Model resolved via fallback chain (availability confirmed)", {
|
||||
provider,
|
||||
model: entry.model,
|
||||
match,
|
||||
variant: entry.variant,
|
||||
})
|
||||
return {
|
||||
model: match,
|
||||
provenance: "provider-fallback",
|
||||
variant: entry.variant,
|
||||
attempted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const crossProviderMatch = fuzzyMatchModel(entry.model, availableModels)
|
||||
if (crossProviderMatch) {
|
||||
log("Model resolved via fallback chain (cross-provider fuzzy match)", {
|
||||
model: entry.model,
|
||||
match: crossProviderMatch,
|
||||
variant: entry.variant,
|
||||
})
|
||||
return {
|
||||
model: crossProviderMatch,
|
||||
provenance: "provider-fallback",
|
||||
variant: entry.variant,
|
||||
attempted,
|
||||
}
|
||||
}
|
||||
}
|
||||
log("No available model found in fallback chain, falling through to system default")
|
||||
}
|
||||
}
|
||||
|
||||
if (systemDefaultModel === undefined) {
|
||||
log("No model resolved - systemDefaultModel not configured")
|
||||
return undefined
|
||||
}
|
||||
|
||||
log("Model resolved via system default", { model: systemDefaultModel })
|
||||
return { model: systemDefaultModel, provenance: "system-default", attempted }
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { log } from "./logger"
|
||||
import { fuzzyMatchModel } from "./model-availability"
|
||||
import type { FallbackEntry } from "./model-requirements"
|
||||
import { resolveModelPipeline } from "./model-resolution-pipeline"
|
||||
import { readConnectedProvidersCache } from "./connected-providers-cache"
|
||||
|
||||
export type ModelResolutionInput = {
|
||||
userModel?: string
|
||||
@@ -46,19 +47,107 @@ export function resolveModelWithFallback(
|
||||
input: ExtendedModelResolutionInput,
|
||||
): ModelResolutionResult | undefined {
|
||||
const { uiSelectedModel, userModel, categoryDefaultModel, fallbackChain, availableModels, systemDefaultModel } = input
|
||||
const resolved = resolveModelPipeline({
|
||||
intent: { uiSelectedModel, userModel, categoryDefaultModel },
|
||||
constraints: { availableModels },
|
||||
policy: { fallbackChain, systemDefaultModel },
|
||||
})
|
||||
|
||||
if (!resolved) {
|
||||
// Step 1: UI Selection (highest priority - respects user's model choice in OpenCode UI)
|
||||
const normalizedUiModel = normalizeModel(uiSelectedModel)
|
||||
if (normalizedUiModel) {
|
||||
log("Model resolved via UI selection", { model: normalizedUiModel })
|
||||
return { model: normalizedUiModel, source: "override" }
|
||||
}
|
||||
|
||||
// Step 2: Config Override (from oh-my-opencode.json user config)
|
||||
const normalizedUserModel = normalizeModel(userModel)
|
||||
if (normalizedUserModel) {
|
||||
log("Model resolved via config override", { model: normalizedUserModel })
|
||||
return { model: normalizedUserModel, source: "override" }
|
||||
}
|
||||
|
||||
// Step 2.5: Category Default Model (from DEFAULT_CATEGORIES, with fuzzy matching)
|
||||
const normalizedCategoryDefault = normalizeModel(categoryDefaultModel)
|
||||
if (normalizedCategoryDefault) {
|
||||
if (availableModels.size > 0) {
|
||||
const parts = normalizedCategoryDefault.split("/")
|
||||
const providerHint = parts.length >= 2 ? [parts[0]] : undefined
|
||||
const match = fuzzyMatchModel(normalizedCategoryDefault, availableModels, providerHint)
|
||||
if (match) {
|
||||
log("Model resolved via category default (fuzzy matched)", { original: normalizedCategoryDefault, matched: match })
|
||||
return { model: match, source: "category-default" }
|
||||
}
|
||||
} else {
|
||||
const connectedProviders = readConnectedProvidersCache()
|
||||
if (connectedProviders === null) {
|
||||
log("Model resolved via category default (no cache, first run)", { model: normalizedCategoryDefault })
|
||||
return { model: normalizedCategoryDefault, source: "category-default" }
|
||||
}
|
||||
const parts = normalizedCategoryDefault.split("/")
|
||||
if (parts.length >= 2) {
|
||||
const provider = parts[0]
|
||||
if (connectedProviders.includes(provider)) {
|
||||
log("Model resolved via category default (connected provider)", { model: normalizedCategoryDefault })
|
||||
return { model: normalizedCategoryDefault, source: "category-default" }
|
||||
}
|
||||
}
|
||||
}
|
||||
log("Category default model not available, falling through to fallback chain", { model: normalizedCategoryDefault })
|
||||
}
|
||||
|
||||
// Step 3: Provider fallback chain (exact match → fuzzy match → next provider)
|
||||
if (fallbackChain && fallbackChain.length > 0) {
|
||||
if (availableModels.size === 0) {
|
||||
const connectedProviders = readConnectedProvidersCache()
|
||||
const connectedSet = connectedProviders ? new Set(connectedProviders) : null
|
||||
|
||||
if (connectedSet === null) {
|
||||
log("Model fallback chain skipped (no connected providers cache) - falling through to system default")
|
||||
} else {
|
||||
for (const entry of fallbackChain) {
|
||||
for (const provider of entry.providers) {
|
||||
if (connectedSet.has(provider)) {
|
||||
const model = `${provider}/${entry.model}`
|
||||
log("Model resolved via fallback chain (connected provider)", {
|
||||
provider,
|
||||
model: entry.model,
|
||||
variant: entry.variant,
|
||||
})
|
||||
return { model, source: "provider-fallback", variant: entry.variant }
|
||||
}
|
||||
}
|
||||
}
|
||||
log("No connected provider found in fallback chain, falling through to system default")
|
||||
}
|
||||
} else {
|
||||
for (const entry of fallbackChain) {
|
||||
// Step 1: Try with provider filter (preferred providers first)
|
||||
for (const provider of entry.providers) {
|
||||
const fullModel = `${provider}/${entry.model}`
|
||||
const match = fuzzyMatchModel(fullModel, availableModels, [provider])
|
||||
if (match) {
|
||||
log("Model resolved via fallback chain (availability confirmed)", { provider, model: entry.model, match, variant: entry.variant })
|
||||
return { model: match, source: "provider-fallback", variant: entry.variant }
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Try without provider filter (cross-provider fuzzy match)
|
||||
const crossProviderMatch = fuzzyMatchModel(entry.model, availableModels)
|
||||
if (crossProviderMatch) {
|
||||
log("Model resolved via fallback chain (cross-provider fuzzy match)", {
|
||||
model: entry.model,
|
||||
match: crossProviderMatch,
|
||||
variant: entry.variant,
|
||||
})
|
||||
return { model: crossProviderMatch, source: "provider-fallback", variant: entry.variant }
|
||||
}
|
||||
}
|
||||
log("No available model found in fallback chain, falling through to system default")
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: System default (if provided)
|
||||
if (systemDefaultModel === undefined) {
|
||||
log("No model resolved - systemDefaultModel not configured")
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
model: resolved.model,
|
||||
source: resolved.provenance,
|
||||
variant: resolved.variant,
|
||||
}
|
||||
log("Model resolved via system default", { model: systemDefaultModel })
|
||||
return { model: systemDefaultModel, source: "system-default" }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import {
|
||||
parseVersion,
|
||||
compareVersions,
|
||||
isVersionGte,
|
||||
isVersionLt,
|
||||
getOpenCodeVersion,
|
||||
isOpenCodeVersionAtLeast,
|
||||
resetVersionCache,
|
||||
@@ -101,6 +103,32 @@ describe("opencode-version", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("isVersionGte", () => {
|
||||
test("returns true when a >= b", () => {
|
||||
expect(isVersionGte("1.1.1", "1.1.1")).toBe(true)
|
||||
expect(isVersionGte("1.1.2", "1.1.1")).toBe(true)
|
||||
expect(isVersionGte("1.2.0", "1.1.1")).toBe(true)
|
||||
expect(isVersionGte("2.0.0", "1.1.1")).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false when a < b", () => {
|
||||
expect(isVersionGte("1.1.0", "1.1.1")).toBe(false)
|
||||
expect(isVersionGte("1.0.9", "1.1.1")).toBe(false)
|
||||
expect(isVersionGte("0.9.9", "1.1.1")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isVersionLt", () => {
|
||||
test("returns true when a < b", () => {
|
||||
expect(isVersionLt("1.1.0", "1.1.1")).toBe(true)
|
||||
expect(isVersionLt("1.0.150", "1.1.1")).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false when a >= b", () => {
|
||||
expect(isVersionLt("1.1.1", "1.1.1")).toBe(false)
|
||||
expect(isVersionLt("1.1.2", "1.1.1")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getOpenCodeVersion", () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -37,6 +37,13 @@ export function compareVersions(a: string, b: string): -1 | 0 | 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
export function isVersionGte(a: string, b: string): boolean {
|
||||
return compareVersions(a, b) >= 0
|
||||
}
|
||||
|
||||
export function isVersionLt(a: string, b: string): boolean {
|
||||
return compareVersions(a, b) < 0
|
||||
}
|
||||
|
||||
export function getOpenCodeVersion(): string | null {
|
||||
if (cachedVersion !== NOT_CACHED) {
|
||||
@@ -62,7 +69,7 @@ export function getOpenCodeVersion(): string | null {
|
||||
export function isOpenCodeVersionAtLeast(version: string): boolean {
|
||||
const current = getOpenCodeVersion()
|
||||
if (!current) return true
|
||||
return compareVersions(current, version) >= 0
|
||||
return isVersionGte(current, version)
|
||||
}
|
||||
|
||||
export function resetVersionCache(): void {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
export interface InjectedPathsData {
|
||||
sessionID: string;
|
||||
injectedPaths: string[];
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export function createInjectedPathsStorage(storageDir: string) {
|
||||
const getStoragePath = (sessionID: string): string =>
|
||||
join(storageDir, `${sessionID}.json`);
|
||||
|
||||
const loadInjectedPaths = (sessionID: string): Set<string> => {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (!existsSync(filePath)) return new Set();
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const data: InjectedPathsData = JSON.parse(content);
|
||||
return new Set(data.injectedPaths);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
};
|
||||
|
||||
const saveInjectedPaths = (sessionID: string, paths: Set<string>): void => {
|
||||
if (!existsSync(storageDir)) {
|
||||
mkdirSync(storageDir, { recursive: true });
|
||||
}
|
||||
|
||||
const data: InjectedPathsData = {
|
||||
sessionID,
|
||||
injectedPaths: [...paths],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
writeFileSync(getStoragePath(sessionID), JSON.stringify(data, null, 2));
|
||||
};
|
||||
|
||||
const clearInjectedPaths = (sessionID: string): void => {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (existsSync(filePath)) {
|
||||
unlinkSync(filePath);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loadInjectedPaths,
|
||||
saveInjectedPaths,
|
||||
clearInjectedPaths,
|
||||
};
|
||||
}
|
||||
@@ -8,37 +8,42 @@ export function snakeToCamel(str: string): string {
|
||||
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
export function transformObjectKeys(
|
||||
obj: Record<string, unknown>,
|
||||
transformer: (key: string) => string,
|
||||
deep: boolean = true
|
||||
): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const transformedKey = transformer(key)
|
||||
if (deep && isPlainObject(value)) {
|
||||
result[transformedKey] = transformObjectKeys(value, transformer, true)
|
||||
} else if (deep && Array.isArray(value)) {
|
||||
result[transformedKey] = value.map((item) =>
|
||||
isPlainObject(item) ? transformObjectKeys(item, transformer, true) : item
|
||||
)
|
||||
} else {
|
||||
result[transformedKey] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function objectToSnakeCase(
|
||||
obj: Record<string, unknown>,
|
||||
deep: boolean = true
|
||||
): Record<string, unknown> {
|
||||
return transformObjectKeys(obj, camelToSnake, deep)
|
||||
}
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const snakeKey = camelToSnake(key)
|
||||
if (deep && isPlainObject(value)) {
|
||||
result[snakeKey] = objectToSnakeCase(value, true)
|
||||
} else if (deep && Array.isArray(value)) {
|
||||
result[snakeKey] = value.map((item) =>
|
||||
isPlainObject(item) ? objectToSnakeCase(item, true) : item
|
||||
)
|
||||
} else {
|
||||
result[snakeKey] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function objectToCamelCase(
|
||||
obj: Record<string, unknown>,
|
||||
deep: boolean = true
|
||||
): Record<string, unknown> {
|
||||
return transformObjectKeys(obj, snakeToCamel, deep)
|
||||
}
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const camelKey = snakeToCamel(key)
|
||||
if (deep && isPlainObject(value)) {
|
||||
result[camelKey] = objectToCamelCase(value, true)
|
||||
} else if (deep && Array.isArray(value)) {
|
||||
result[camelKey] = value.map((item) =>
|
||||
isPlainObject(item) ? objectToCamelCase(item, true) : item
|
||||
)
|
||||
} else {
|
||||
result[camelKey] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -139,22 +139,10 @@ export async function spawnTmuxPane(
|
||||
}
|
||||
|
||||
const title = `omo-subagent-${description.slice(0, 20)}`
|
||||
const titleProc = spawn([tmux, "select-pane", "-t", paneId, "-T", title], {
|
||||
spawn([tmux, "select-pane", "-t", paneId, "-T", title], {
|
||||
stdout: "ignore",
|
||||
stderr: "pipe",
|
||||
stderr: "ignore",
|
||||
})
|
||||
// Drain stderr immediately to avoid backpressure
|
||||
const stderrPromise = new Response(titleProc.stderr).text().catch(() => "")
|
||||
const titleExitCode = await titleProc.exited
|
||||
if (titleExitCode !== 0) {
|
||||
const titleStderr = await stderrPromise
|
||||
log("[spawnTmuxPane] WARNING: failed to set pane title", {
|
||||
paneId,
|
||||
title,
|
||||
exitCode: titleExitCode,
|
||||
stderr: titleStderr.trim(),
|
||||
})
|
||||
}
|
||||
|
||||
return { success: true, paneId }
|
||||
}
|
||||
@@ -229,21 +217,10 @@ export async function replaceTmuxPane(
|
||||
}
|
||||
|
||||
const title = `omo-subagent-${description.slice(0, 20)}`
|
||||
const titleProc = spawn([tmux, "select-pane", "-t", paneId, "-T", title], {
|
||||
spawn([tmux, "select-pane", "-t", paneId, "-T", title], {
|
||||
stdout: "ignore",
|
||||
stderr: "pipe",
|
||||
stderr: "ignore",
|
||||
})
|
||||
// Drain stderr immediately to avoid backpressure
|
||||
const stderrPromise = new Response(titleProc.stderr).text().catch(() => "")
|
||||
const titleExitCode = await titleProc.exited
|
||||
if (titleExitCode !== 0) {
|
||||
const titleStderr = await stderrPromise
|
||||
log("[replaceTmuxPane] WARNING: failed to set pane title", {
|
||||
paneId,
|
||||
exitCode: titleExitCode,
|
||||
stderr: titleStderr.trim(),
|
||||
})
|
||||
}
|
||||
|
||||
log("[replaceTmuxPane] SUCCESS", { paneId, sessionId })
|
||||
return { success: true, paneId }
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { existsSync } from "fs"
|
||||
import { existsSync, mkdirSync, chmodSync, unlinkSync } from "fs"
|
||||
import { join } from "path"
|
||||
import { homedir } from "os"
|
||||
import { createRequire } from "module"
|
||||
import {
|
||||
cleanupArchive,
|
||||
downloadArchive,
|
||||
ensureCacheDir,
|
||||
ensureExecutable,
|
||||
extractZipArchive,
|
||||
getCachedBinaryPath as getCachedBinaryPathShared,
|
||||
} from "../../shared/binary-downloader"
|
||||
import { extractZip } from "../../shared"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
const REPO = "ast-grep/ast-grep"
|
||||
@@ -60,7 +53,8 @@ export function getBinaryName(): string {
|
||||
}
|
||||
|
||||
export function getCachedBinaryPath(): string | null {
|
||||
return getCachedBinaryPathShared(getCacheDir(), getBinaryName())
|
||||
const binaryPath = join(getCacheDir(), getBinaryName())
|
||||
return existsSync(binaryPath) ? binaryPath : null
|
||||
}
|
||||
|
||||
|
||||
@@ -89,12 +83,29 @@ export async function downloadAstGrep(version: string = DEFAULT_VERSION): Promis
|
||||
log(`[oh-my-opencode] Downloading ast-grep binary...`)
|
||||
|
||||
try {
|
||||
if (!existsSync(cacheDir)) {
|
||||
mkdirSync(cacheDir, { recursive: true })
|
||||
}
|
||||
|
||||
const response = await fetch(downloadUrl, { redirect: "follow" })
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const archivePath = join(cacheDir, assetName)
|
||||
ensureCacheDir(cacheDir)
|
||||
await downloadArchive(downloadUrl, archivePath)
|
||||
await extractZipArchive(archivePath, cacheDir)
|
||||
cleanupArchive(archivePath)
|
||||
ensureExecutable(binaryPath)
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
await Bun.write(archivePath, arrayBuffer)
|
||||
|
||||
await extractZip(archivePath, cacheDir)
|
||||
|
||||
if (existsSync(archivePath)) {
|
||||
unlinkSync(archivePath)
|
||||
}
|
||||
|
||||
if (process.platform !== "win32" && existsSync(binaryPath)) {
|
||||
chmodSync(binaryPath, 0o755)
|
||||
}
|
||||
|
||||
log(`[oh-my-opencode] ast-grep binary ready.`)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join } from "node:path"
|
||||
import { ALLOWED_AGENTS, CALL_OMO_AGENT_DESCRIPTION } from "./constants"
|
||||
import type { CallOmoAgentArgs } from "./types"
|
||||
import type { BackgroundManager } from "../../features/background-agent"
|
||||
import { log, getAgentToolRestrictions } from "../../shared"
|
||||
import { log, getAgentToolRestrictions, includesCaseInsensitive } from "../../shared"
|
||||
import { consumeNewMessages } from "../../shared/session-cursor"
|
||||
import { findFirstMessageWithAgent, findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector"
|
||||
import { getSessionAgent } from "../../features/claude-code-session-state"
|
||||
@@ -58,9 +58,7 @@ export function createCallOmoAgent(
|
||||
log(`[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`)
|
||||
|
||||
// Case-insensitive agent validation - allows "Explore", "EXPLORE", "explore" etc.
|
||||
if (![...ALLOWED_AGENTS].some(
|
||||
(name) => name.toLowerCase() === args.subagent_type.toLowerCase()
|
||||
)) {
|
||||
if (!includesCaseInsensitive([...ALLOWED_AGENTS], args.subagent_type)) {
|
||||
return `Error: Invalid agent type "${args.subagent_type}". Only ${ALLOWED_AGENTS.join(", ")} are allowed.`
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@ import { discoverSkills } from "../../features/opencode-skill-loader"
|
||||
import { getTaskToastManager } from "../../features/task-toast-manager"
|
||||
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
|
||||
import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state"
|
||||
import { log, getAgentToolRestrictions, resolveModel, resolveModelPipeline, getOpenCodeConfigPaths, promptWithModelSuggestionRetry } from "../../shared"
|
||||
import { log, getAgentToolRestrictions, resolveModel, getOpenCodeConfigPaths, findByNameCaseInsensitive, equalsIgnoreCase, promptWithModelSuggestionRetry } from "../../shared"
|
||||
import { fetchAvailableModels, isModelAvailable } from "../../shared/model-availability"
|
||||
import { readConnectedProvidersCache } from "../../shared/connected-providers-cache"
|
||||
import { resolveModelWithFallback } from "../../shared/model-resolver"
|
||||
import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
||||
|
||||
type OpencodeClient = PluginInput["client"]
|
||||
@@ -551,20 +552,16 @@ To continue this session: session_id="${args.session_id}"`
|
||||
modelInfo = { model: actualModel, type: "system-default", source: "system-default" }
|
||||
}
|
||||
} else {
|
||||
const resolution = resolveModelPipeline({
|
||||
intent: {
|
||||
const resolution = resolveModelWithFallback({
|
||||
userModel: userCategories?.[args.category]?.model,
|
||||
categoryDefaultModel: resolved.model ?? sisyphusJuniorModel,
|
||||
},
|
||||
constraints: { availableModels },
|
||||
policy: {
|
||||
fallbackChain: requirement.fallbackChain,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
if (resolution) {
|
||||
const { model: resolvedModel, provenance, variant: resolvedVariant } = resolution
|
||||
if (resolution) {
|
||||
const { model: resolvedModel, source, variant: resolvedVariant } = resolution
|
||||
actualModel = resolvedModel
|
||||
|
||||
if (!parseModelString(actualModel)) {
|
||||
@@ -572,8 +569,7 @@ To continue this session: session_id="${args.session_id}"`
|
||||
}
|
||||
|
||||
let type: "user-defined" | "inherited" | "category-default" | "system-default"
|
||||
const source = provenance
|
||||
switch (provenance) {
|
||||
switch (source) {
|
||||
case "override":
|
||||
type = "user-defined"
|
||||
break
|
||||
@@ -586,7 +582,7 @@ To continue this session: session_id="${args.session_id}"`
|
||||
break
|
||||
}
|
||||
|
||||
modelInfo = { model: actualModel, type, source }
|
||||
modelInfo = { model: actualModel, type, source }
|
||||
|
||||
const parsedModel = parseModelString(actualModel)
|
||||
const variantToUse = userCategories?.[args.category]?.variant ?? resolvedVariant ?? resolved.config.variant
|
||||
@@ -784,7 +780,7 @@ To continue this session: session_id="${sessionID}"`
|
||||
}
|
||||
const agentName = args.subagent_type.trim()
|
||||
|
||||
if (agentName.toLowerCase() === SISYPHUS_JUNIOR_AGENT.toLowerCase()) {
|
||||
if (equalsIgnoreCase(agentName, SISYPHUS_JUNIOR_AGENT)) {
|
||||
return `Cannot use subagent_type="${SISYPHUS_JUNIOR_AGENT}" directly. Use category parameter instead (e.g., ${categoryExamples}).
|
||||
|
||||
Sisyphus-Junior is spawned automatically when you specify a category. Pick the appropriate category for your task domain.`
|
||||
@@ -807,13 +803,12 @@ Create the work plan directly - that's your job as the planning agent.`
|
||||
|
||||
const callableAgents = agents.filter((a) => a.mode !== "primary")
|
||||
|
||||
const matchedAgent = callableAgents.find(
|
||||
(agent) => agent.name.toLowerCase() === agentToUse.toLowerCase()
|
||||
)
|
||||
const matchedAgent = findByNameCaseInsensitive(callableAgents, agentToUse)
|
||||
if (!matchedAgent) {
|
||||
const isPrimaryAgent = agents
|
||||
.filter((a) => a.mode === "primary")
|
||||
.find((agent) => agent.name.toLowerCase() === agentToUse.toLowerCase())
|
||||
const isPrimaryAgent = findByNameCaseInsensitive(
|
||||
agents.filter((a) => a.mode === "primary"),
|
||||
agentToUse
|
||||
)
|
||||
if (isPrimaryAgent) {
|
||||
return `Cannot call primary agent "${isPrimaryAgent.name}" via delegate_task. Primary agents are top-level orchestrators.`
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { existsSync, readdirSync } from "node:fs"
|
||||
import { existsSync, mkdirSync, chmodSync, unlinkSync, readdirSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { spawn } from "bun"
|
||||
import { extractZip as extractZipBase } from "../../shared"
|
||||
import {
|
||||
cleanupArchive,
|
||||
downloadArchive,
|
||||
ensureCacheDir,
|
||||
ensureExecutable,
|
||||
extractTarGz as extractTarGzArchive,
|
||||
} from "../../shared/binary-downloader"
|
||||
|
||||
export function findFileRecursive(dir: string, filename: string): string | null {
|
||||
try {
|
||||
@@ -47,6 +41,16 @@ function getRgPath(): string {
|
||||
return join(getInstallDir(), isWindows ? "rg.exe" : "rg")
|
||||
}
|
||||
|
||||
async function downloadFile(url: string, destPath: string): Promise<void> {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer()
|
||||
await Bun.write(destPath, buffer)
|
||||
}
|
||||
|
||||
async function extractTarGz(archivePath: string, destDir: string): Promise<void> {
|
||||
const platformKey = getPlatformKey()
|
||||
|
||||
@@ -58,7 +62,17 @@ async function extractTarGz(archivePath: string, destDir: string): Promise<void>
|
||||
args.push("--wildcards", "*/rg")
|
||||
}
|
||||
|
||||
await extractTarGzArchive(archivePath, destDir, { args, cwd: destDir })
|
||||
const proc = spawn(args, {
|
||||
cwd: destDir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
const exitCode = await proc.exited
|
||||
if (exitCode !== 0) {
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
throw new Error(`Failed to extract tar.gz: ${stderr}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function extractZip(archivePath: string, destDir: string): Promise<void> {
|
||||
@@ -90,14 +104,14 @@ export async function downloadAndInstallRipgrep(): Promise<string> {
|
||||
return rgPath
|
||||
}
|
||||
|
||||
ensureCacheDir(installDir)
|
||||
mkdirSync(installDir, { recursive: true })
|
||||
|
||||
const filename = `ripgrep-${RG_VERSION}-${config.platform}.${config.extension}`
|
||||
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${filename}`
|
||||
const archivePath = join(installDir, filename)
|
||||
|
||||
try {
|
||||
await downloadArchive(url, archivePath)
|
||||
await downloadFile(url, archivePath)
|
||||
|
||||
if (config.extension === "tar.gz") {
|
||||
await extractTarGz(archivePath, installDir)
|
||||
@@ -105,7 +119,9 @@ export async function downloadAndInstallRipgrep(): Promise<string> {
|
||||
await extractZip(archivePath, installDir)
|
||||
}
|
||||
|
||||
ensureExecutable(rgPath)
|
||||
if (process.platform !== "win32") {
|
||||
chmodSync(rgPath, 0o755)
|
||||
}
|
||||
|
||||
if (!existsSync(rgPath)) {
|
||||
throw new Error("ripgrep binary not found after extraction")
|
||||
@@ -113,10 +129,12 @@ export async function downloadAndInstallRipgrep(): Promise<string> {
|
||||
|
||||
return rgPath
|
||||
} finally {
|
||||
try {
|
||||
cleanupArchive(archivePath)
|
||||
} catch {
|
||||
// Cleanup failures are non-critical
|
||||
if (existsSync(archivePath)) {
|
||||
try {
|
||||
unlinkSync(archivePath)
|
||||
} catch {
|
||||
// Cleanup failures are non-critical
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,19 +96,10 @@ The Bash tool can execute these commands directly. Do NOT retry with interactive
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
const id = setTimeout(() => {
|
||||
const timeoutError = new Error(`Timeout after ${DEFAULT_TIMEOUT_MS}ms`)
|
||||
try {
|
||||
proc.kill()
|
||||
// Fire-and-forget: wait for process exit in background to avoid zombies
|
||||
void proc.exited.catch(() => {})
|
||||
} catch {
|
||||
// Ignore kill errors; we'll still reject with timeoutError below
|
||||
}
|
||||
reject(timeoutError)
|
||||
proc.kill()
|
||||
reject(new Error(`Timeout after ${DEFAULT_TIMEOUT_MS}ms`))
|
||||
}, DEFAULT_TIMEOUT_MS)
|
||||
proc.exited
|
||||
.then(() => clearTimeout(id))
|
||||
.catch(() => clearTimeout(id))
|
||||
proc.exited.then(() => clearTimeout(id))
|
||||
})
|
||||
|
||||
// Read stdout and stderr in parallel to avoid race conditions
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { ToolContext } from "@opencode-ai/plugin/tool"
|
||||
import { normalizeArgs, validateArgs, createLookAt } from "./tools"
|
||||
|
||||
describe("look-at tool", () => {
|
||||
@@ -93,15 +92,11 @@ describe("look-at tool", () => {
|
||||
directory: "/project",
|
||||
} as any)
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
const toolContext = {
|
||||
sessionID: "parent-session",
|
||||
messageID: "parent-message",
|
||||
agent: "sisyphus",
|
||||
directory: "/project",
|
||||
worktree: "/project",
|
||||
abort: new AbortController().signal,
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
const result = await tool.execute(
|
||||
@@ -135,15 +130,11 @@ describe("look-at tool", () => {
|
||||
directory: "/project",
|
||||
} as any)
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
const toolContext = {
|
||||
sessionID: "parent-session",
|
||||
messageID: "parent-message",
|
||||
agent: "sisyphus",
|
||||
directory: "/project",
|
||||
worktree: "/project",
|
||||
abort: new AbortController().signal,
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
const result = await tool.execute(
|
||||
@@ -195,15 +186,11 @@ describe("look-at tool", () => {
|
||||
directory: "/project",
|
||||
} as any)
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
const toolContext = {
|
||||
sessionID: "parent-session",
|
||||
messageID: "parent-message",
|
||||
agent: "sisyphus",
|
||||
directory: "/project",
|
||||
worktree: "/project",
|
||||
abort: new AbortController().signal,
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
await tool.execute(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { pathToFileURL } from "node:url"
|
||||
import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin"
|
||||
import { LOOK_AT_DESCRIPTION, MULTIMODAL_LOOKER_AGENT } from "./constants"
|
||||
import type { LookAtArgs } from "./types"
|
||||
import { log, promptWithModelSuggestionRetry } from "../../shared"
|
||||
import { findByNameCaseInsensitive, log, promptWithModelSuggestionRetry } from "../../shared"
|
||||
|
||||
interface LookAtArgsWithAlias extends LookAtArgs {
|
||||
path?: string
|
||||
@@ -143,9 +143,7 @@ Original error: ${createResult.error}`
|
||||
}
|
||||
const agents = ((agentsResult as { data?: AgentInfo[] })?.data ?? agentsResult) as AgentInfo[] | undefined
|
||||
if (agents?.length) {
|
||||
const matchedAgent = agents.find(
|
||||
(agent) => agent.name.toLowerCase() === MULTIMODAL_LOOKER_AGENT.toLowerCase()
|
||||
)
|
||||
const matchedAgent = findByNameCaseInsensitive(agents, MULTIMODAL_LOOKER_AGENT)
|
||||
if (matchedAgent?.model) {
|
||||
agentModel = matchedAgent.model
|
||||
}
|
||||
|
||||
@@ -13,37 +13,6 @@ import { getLanguageId } from "./config"
|
||||
import type { Diagnostic, ResolvedServer } from "./types"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
/**
|
||||
* Check if the current Bun version is affected by Windows LSP crash bug.
|
||||
* Bun v1.3.5 and earlier have a known segmentation fault issue on Windows
|
||||
* when spawning LSP servers. This was fixed in Bun v1.3.6.
|
||||
* See: https://github.com/oven-sh/bun/issues/25798
|
||||
*/
|
||||
function checkWindowsBunVersion(): { isAffected: boolean; message: string } | null {
|
||||
if (process.platform !== "win32") return null
|
||||
|
||||
const version = Bun.version
|
||||
const [major, minor, patch] = version.split(".").map((v) => parseInt(v.split("-")[0], 10))
|
||||
|
||||
// Bun v1.3.5 and earlier are affected
|
||||
if (major < 1 || (major === 1 && minor < 3) || (major === 1 && minor === 3 && patch < 6)) {
|
||||
return {
|
||||
isAffected: true,
|
||||
message:
|
||||
`⚠️ Windows + Bun v${version} detected: Known segmentation fault bug with LSP.\n` +
|
||||
` This causes crashes when using LSP tools (lsp_diagnostics, lsp_goto_definition, etc.).\n` +
|
||||
` \n` +
|
||||
` SOLUTION: Upgrade to Bun v1.3.6 or later:\n` +
|
||||
` powershell -c "irm bun.sh/install.ps1|iex"\n` +
|
||||
` \n` +
|
||||
` WORKAROUND: Use WSL instead of native Windows.\n` +
|
||||
` See: https://github.com/oven-sh/bun/issues/25798`,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
interface ManagedClient {
|
||||
client: LSPClient
|
||||
lastUsedAt: number
|
||||
@@ -64,12 +33,10 @@ class LSPServerManager {
|
||||
}
|
||||
|
||||
private registerProcessCleanup(): void {
|
||||
// Synchronous cleanup for 'exit' event (cannot await)
|
||||
const syncCleanup = () => {
|
||||
const cleanup = () => {
|
||||
for (const [, managed] of this.clients) {
|
||||
try {
|
||||
// Fire-and-forget during sync exit - process is terminating
|
||||
void managed.client.stop().catch(() => {})
|
||||
managed.client.stop()
|
||||
} catch {}
|
||||
}
|
||||
this.clients.clear()
|
||||
@@ -79,30 +46,23 @@ class LSPServerManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Async cleanup for signal handlers - properly await all stops
|
||||
const asyncCleanup = async () => {
|
||||
const stopPromises: Promise<void>[] = []
|
||||
for (const [, managed] of this.clients) {
|
||||
stopPromises.push(managed.client.stop().catch(() => {}))
|
||||
}
|
||||
await Promise.allSettled(stopPromises)
|
||||
this.clients.clear()
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval)
|
||||
this.cleanupInterval = null
|
||||
}
|
||||
}
|
||||
process.on("exit", cleanup)
|
||||
|
||||
process.on("exit", syncCleanup)
|
||||
process.on("SIGINT", () => {
|
||||
cleanup()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
// Don't call process.exit() here - let other handlers complete their cleanup first
|
||||
// The background-agent manager handles the final exit call
|
||||
// Use async handlers to properly await LSP subprocess cleanup
|
||||
process.on("SIGINT", () => void asyncCleanup().catch(() => {}))
|
||||
process.on("SIGTERM", () => void asyncCleanup().catch(() => {}))
|
||||
process.on("SIGTERM", () => {
|
||||
cleanup()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
if (process.platform === "win32") {
|
||||
process.on("SIGBREAK", () => void asyncCleanup().catch(() => {}))
|
||||
process.on("SIGBREAK", () => {
|
||||
cleanup()
|
||||
process.exit(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,13 +226,6 @@ export class LSPClient {
|
||||
) {}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const windowsCheck = checkWindowsBunVersion()
|
||||
if (windowsCheck?.isAffected) {
|
||||
throw new Error(
|
||||
`LSP server cannot be started safely.\n\n${windowsCheck.message}`
|
||||
)
|
||||
}
|
||||
|
||||
this.proc = spawn(this.server.command, {
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
@@ -579,34 +532,8 @@ export class LSPClient {
|
||||
this.connection.dispose()
|
||||
this.connection = null
|
||||
}
|
||||
const proc = this.proc
|
||||
if (proc) {
|
||||
this.proc = null
|
||||
let exitedBeforeTimeout = false
|
||||
try {
|
||||
proc.kill()
|
||||
// Wait for exit with timeout to prevent indefinite hang
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||
const timeoutPromise = new Promise<void>((resolve) => {
|
||||
timeoutId = setTimeout(resolve, 5000)
|
||||
})
|
||||
await Promise.race([
|
||||
proc.exited.then(() => { exitedBeforeTimeout = true }).finally(() => timeoutId && clearTimeout(timeoutId)),
|
||||
timeoutPromise,
|
||||
])
|
||||
if (!exitedBeforeTimeout) {
|
||||
log("[LSPClient] Process did not exit within timeout, escalating to SIGKILL")
|
||||
try {
|
||||
proc.kill("SIGKILL")
|
||||
// Wait briefly for SIGKILL to take effect
|
||||
await Promise.race([
|
||||
proc.exited,
|
||||
new Promise<void>((resolve) => setTimeout(resolve, 1000)),
|
||||
])
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
this.proc?.kill()
|
||||
this.proc = null
|
||||
this.processExited = true
|
||||
this.diagnosticsStore.clear()
|
||||
}
|
||||
|
||||
@@ -2,17 +2,11 @@ import { describe, test, expect } from "bun:test"
|
||||
import { session_list, session_read, session_search, session_info } from "./tools"
|
||||
import type { ToolContext } from "@opencode-ai/plugin/tool"
|
||||
|
||||
const projectDir = "/Users/yeongyu/local-workspaces/oh-my-opencode"
|
||||
|
||||
const mockContext: ToolContext = {
|
||||
sessionID: "test-session",
|
||||
messageID: "test-message",
|
||||
agent: "test-agent",
|
||||
directory: projectDir,
|
||||
worktree: projectDir,
|
||||
abort: new AbortController().signal,
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
describe("session-manager tools", () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, mock } from "bun:test"
|
||||
import type { ToolContext } from "@opencode-ai/plugin/tool"
|
||||
import { createSkillMcpTool, applyGrepFilter } from "./tools"
|
||||
import { SkillMcpManager } from "../../features/skill-mcp-manager"
|
||||
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
|
||||
@@ -19,15 +18,11 @@ function createMockSkillWithMcp(name: string, mcpServers: Record<string, unknown
|
||||
}
|
||||
}
|
||||
|
||||
const mockContext: ToolContext = {
|
||||
const mockContext = {
|
||||
sessionID: "test-session",
|
||||
messageID: "msg-1",
|
||||
agent: "test-agent",
|
||||
directory: "/test",
|
||||
worktree: "/test",
|
||||
abort: new AbortController().signal,
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
describe("skill_mcp tool", () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test"
|
||||
import type { ToolContext } from "@opencode-ai/plugin/tool"
|
||||
import * as fs from "node:fs"
|
||||
import { createSkillTool } from "./tools"
|
||||
import { SkillMcpManager } from "../../features/skill-mcp-manager"
|
||||
@@ -51,15 +50,11 @@ function createMockSkillWithMcp(name: string, mcpServers: Record<string, unknown
|
||||
}
|
||||
}
|
||||
|
||||
const mockContext: ToolContext = {
|
||||
const mockContext = {
|
||||
sessionID: "test-session",
|
||||
messageID: "msg-1",
|
||||
agent: "test-agent",
|
||||
directory: "/test",
|
||||
worktree: "/test",
|
||||
abort: new AbortController().signal,
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
describe("skill tool - synchronous description", () => {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./types"
|
||||
export { slashcommand, createSlashcommandTool, discoverCommandsSync } from "./tools"
|
||||
export { slash_command, createSlashcommandTool, discoverCommandsSync } from "./tools"
|
||||
|
||||
@@ -269,4 +269,4 @@ export function createSlashcommandTool(options: SlashcommandToolOptions = {}): T
|
||||
}
|
||||
|
||||
// Default instance for backward compatibility (lazy loading)
|
||||
export const slashcommand: ToolDefinition = createSlashcommandTool()
|
||||
export const slash_command: ToolDefinition = createSlashcommandTool()
|
||||
|
||||
BIN
ultrawork-manifesto-web/public/images/footer-pattern.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
ultrawork-manifesto-web/public/images/hero.png
Normal file
|
After Width: | Height: | Size: 4.5 MiB |
BIN
ultrawork-manifesto-web/public/images/orb-divider.png
Normal file
|
After Width: | Height: | Size: 704 KiB |
341
ultrawork-manifesto-web/src/es/index.html
Normal file
@@ -0,0 +1,341 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<title>Manifiesto Ultrawork | La Filosofía de la Ingeniería de Alto Rendimiento</title>
|
||||
<meta name="description" content="Un marco filosófico para desarrolladores e ingenieros de IA para lograr una productividad extrema a través del enfoque profundo, la ejecución atómica y la metodología Ultrawork.">
|
||||
<meta name="keywords" content="ultrawork, productividad de desarrolladores, ingeniería de IA, trabajo profundo, manifiesto tecnológico, oh my opencode, sisyphus">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large">
|
||||
<link rel="canonical" href="https://ulw.dev/es/">
|
||||
<link rel="alternate" hreflang="en" href="https://ulw.dev/">
|
||||
<link rel="alternate" hreflang="ko" href="https://ulw.dev/ko/">
|
||||
<link rel="alternate" hreflang="ja" href="https://ulw.dev/ja/">
|
||||
<link rel="alternate" hreflang="zh" href="https://ulw.dev/zh/">
|
||||
<link rel="alternate" hreflang="es" href="https://ulw.dev/es/">
|
||||
<link rel="alternate" hreflang="x-default" href="https://ulw.dev/">
|
||||
<meta name="theme-color" content="#0a0a0a">
|
||||
<meta name="author" content="Yeongyu Kim">
|
||||
<link rel="icon" type="image/png" href="../images/favicon.png">
|
||||
<link rel="apple-touch-icon" href="../images/favicon.png">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://ulw.dev/es/">
|
||||
<meta property="og:title" content="Manifiesto Ultrawork | La Filosofía de la Ingeniería de Alto Rendimiento">
|
||||
<meta property="og:description" content="Un marco filosófico para desarrolladores e ingenieros de IA para lograr una productividad extrema a través del enfoque profundo, la ejecución atómica y la metodología Ultrawork.">
|
||||
<meta property="og:image" content="https://ulw.dev/images/og-image.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="Manifiesto Ultrawork - Un Plano para el Trabajo Significativo">
|
||||
<meta property="og:site_name" content="Ultrawork">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@justsisyphus">
|
||||
<meta name="twitter:creator" content="@justsisyphus">
|
||||
<meta name="twitter:title" content="Manifiesto Ultrawork | La Filosofía de la Ingeniería de Alto Rendimiento">
|
||||
<meta name="twitter:description" content="Un marco filosófico para desarrolladores e ingenieros de IA para lograr una productividad extrema a través del enfoque profundo, la ejecución atómica y la metodología Ultrawork.">
|
||||
<meta name="twitter:image" content="https://ulw.dev/images/og-image.png">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="hero-container">
|
||||
<img src="../images/hero.png" alt="Ultrawork Hero" class="hero-image">
|
||||
<h1 class="hero-title">Manifiesto Ultrawork</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Section 1 -->
|
||||
<section id="human-intervention">
|
||||
<h2>La Intervención Humana es una Señal de Fallo</h2>
|
||||
|
||||
<div class="gold-gradient-text bottleneck-text">
|
||||
HUMANO EN EL BUCLE = CUELLO DE BOTELLA<br>
|
||||
HUMANO EN EL BUCLE = CUELLO DE BOTELLA<br>
|
||||
HUMANO EN EL BUCLE = CUELLO DE BOTELLA
|
||||
</div>
|
||||
|
||||
<p>Piensen en la conducción autónoma. Cuando un humano tiene que tomar el volante, eso no es una característica, es un fallo del sistema. El auto no pudo manejar la situación por sí mismo.</p>
|
||||
|
||||
<h3>¿Por qué la programación sería diferente?</h3>
|
||||
|
||||
<p>Cuando se encuentran:</p>
|
||||
<ul>
|
||||
<li>Arreglando el código a medio terminar de la IA</li>
|
||||
<li>Corrigiendo manualmente errores obvios</li>
|
||||
<li>Guiando al agente paso a paso a través de una tarea</li>
|
||||
<li>Aclarando repetidamente los mismos requerimientos</li>
|
||||
</ul>
|
||||
|
||||
<p>...eso no es "colaboración humano-IA". Eso es la IA fallando en hacer su trabajo.</p>
|
||||
|
||||
<p><strong><a href="https://github.com/code-yeongyu/oh-my-opencode" target="_blank" rel="noopener">Oh My OpenCode</a> está construido bajo esta premisa</strong>: La intervención humana durante el trabajo agéntico es fundamentalmente una señal equivocada. Si el sistema está diseñado correctamente, el agente debería completar el trabajo sin requerir que ustedes lo vigilen.</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 2 -->
|
||||
<section id="indistinguishable-code">
|
||||
<h2>Código Indistinguible</h2>
|
||||
|
||||
<p class="highlight-box"><strong>Objetivo: El código escrito por el agente debe ser indistinguible del código escrito por un ingeniero senior.</strong></p>
|
||||
|
||||
<p>No "código generado por IA que necesita limpieza". No "un buen punto de partida". El código real, final y listo para producción.</p>
|
||||
|
||||
<p>Esto significa:</p>
|
||||
<ul>
|
||||
<li>Seguir exactamente los patrones existentes del código base</li>
|
||||
<li>Manejo adecuado de errores sin que se lo pidan</li>
|
||||
<li>Pruebas que realmente prueban lo correcto</li>
|
||||
<li>Sin basura de IA (sobreingeniería, abstracciones innecesarias, alcance no solicitado)</li>
|
||||
<li>Comentarios solo cuando agregan valor</li>
|
||||
</ul>
|
||||
|
||||
<p>Si pueden distinguir si un commit fue hecho por un humano o un agente, el agente ha fallado.</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 3 -->
|
||||
<section id="token-cost">
|
||||
<h2>Costo de Tokens vs. Productividad</h2>
|
||||
|
||||
<p><strong>Un mayor uso de tokens es aceptable si incrementa significativamente la productividad.</strong></p>
|
||||
|
||||
<p>Usar más tokens para:</p>
|
||||
<ul>
|
||||
<li>Tener múltiples agentes especializados investigando en paralelo</li>
|
||||
<li>Terminar el trabajo completamente sin intervención humana</li>
|
||||
<li>Verificar el trabajo minuciosamente antes de completarlo</li>
|
||||
<li>Acumular conocimiento a través de las tareas</li>
|
||||
</ul>
|
||||
|
||||
<p>...es una inversión que vale la pena cuando significa ganancias de productividad de 10x, 20x o 100x.</p>
|
||||
|
||||
<h3>Sin embargo:</h3>
|
||||
|
||||
<p>No se busca el desperdicio innecesario de tokens. El sistema optimiza para:</p>
|
||||
<ul>
|
||||
<li>Usar modelos más baratos (Haiku, Flash) para tareas simples</li>
|
||||
<li>Evitar exploración redundante</li>
|
||||
<li>Guardar en caché los aprendizajes entre sesiones</li>
|
||||
<li>Detener la investigación cuando se ha reunido suficiente contexto</li>
|
||||
</ul>
|
||||
|
||||
<p>La eficiencia de tokens importa. Pero no a costa de la calidad del trabajo o la carga cognitiva humana.</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 4 -->
|
||||
<section id="cognitive-load">
|
||||
<h2>Minimizar la Carga Cognitiva Humana</h2>
|
||||
|
||||
<p><strong>El humano solo debería necesitar decir lo que quiere. Todo lo demás es trabajo del agente.</strong></p>
|
||||
|
||||
<p>Dos enfoques para lograr esto:</p>
|
||||
|
||||
<div class="approach-container">
|
||||
<div class="approach ultrawork-approach">
|
||||
<h3>Enfoque 1: Ultrawork</h3>
|
||||
<p class="approach-tagline">Solo digan "ulw" y aléjense.</p>
|
||||
<p>Ustedes dicen: <code>ulw add authentication</code></p>
|
||||
<p>El agente autónomamente:</p>
|
||||
<ul>
|
||||
<li>Analiza sus patrones de código y arquitectura</li>
|
||||
<li>Investiga las mejores prácticas de la documentación oficial</li>
|
||||
<li>Planea la estrategia de implementación internamente</li>
|
||||
<li>Implementa siguiendo sus convenciones existentes</li>
|
||||
<li>Verifica con pruebas y diagnósticos LSP</li>
|
||||
<li>Se autocorrige cuando algo sale mal</li>
|
||||
<li><strong>Sigue empujando la roca hasta completar el 100%</strong></li>
|
||||
</ul>
|
||||
<p class="approach-summary"><strong>Cero intervención. Autonomía total. Solo resultados.</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="approach prometheus-approach">
|
||||
<h3>Enfoque 2: Prometheus + Atlas</h3>
|
||||
<p class="approach-tagline">Cuando quieren control estratégico.</p>
|
||||
<p>Presionen <kbd>Tab</kbd> para cambiar de agente, luego: <code>add authentication</code></p>
|
||||
<p><strong>Prometheus</strong> (Planificador Estratégico):</p>
|
||||
<ul>
|
||||
<li>Realiza una investigación profunda del código base vía agentes paralelos</li>
|
||||
<li>Los entrevista con preguntas inteligentes y contextuales</li>
|
||||
<li>Identifica casos borde e implicaciones arquitectónicas</li>
|
||||
<li>Genera un plan de trabajo detallado en YAML con dependencias</li>
|
||||
</ul>
|
||||
<p><strong>Atlas</strong> (Orquestador Maestro):</p>
|
||||
<ul>
|
||||
<li>Ejecuta el plan vía <code>/start-work</code></li>
|
||||
<li>Delega tareas a agentes especializados (Oracle, Frontend Engineer, etc.)</li>
|
||||
<li>Gestiona olas de ejecución paralela para eficiencia</li>
|
||||
<li>Rastrea el progreso, maneja fallos, asegura la finalización</li>
|
||||
</ul>
|
||||
<p class="approach-summary"><strong>Ustedes arquitectan. Los agentes ejecutan. Transparencia total.</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>En ambos casos, el trabajo del humano es <strong>expresar lo que quieren</strong>, no gestionar cómo se hace.</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 5 -->
|
||||
<section id="predictable-continuous">
|
||||
<h2>Predecible, Continuo, Delegable</h2>
|
||||
|
||||
<p><strong>El agente ideal debería trabajar como un compilador</strong>: entra un documento markdown, sale código funcional.</p>
|
||||
|
||||
<h3>Predecible</h3>
|
||||
<p>Dados los mismos inputs:</p>
|
||||
<ul>
|
||||
<li>Mismos patrones de código base</li>
|
||||
<li>Mismos requerimientos</li>
|
||||
<li>Mismas restricciones</li>
|
||||
</ul>
|
||||
<p>...la salida debe ser consistente. No aleatoria, no sorpresiva, no "creativa" en formas que no pidieron.</p>
|
||||
|
||||
<h3>Continuo</h3>
|
||||
<p>El trabajo debe sobrevivir a las interrupciones:</p>
|
||||
<ul>
|
||||
<li>¿Se cae la sesión? Reanuden con <code>/start-work</code></li>
|
||||
<li>¿Necesitan alejarse? El progreso se rastrea</li>
|
||||
<li>¿Proyecto de varios días? El contexto se preserva</li>
|
||||
</ul>
|
||||
<p>El agente mantiene el estado. Ustedes no tienen que hacerlo.</p>
|
||||
|
||||
<h3>Delegable</h3>
|
||||
<p>Así como pueden asignar una tarea a un miembro capaz del equipo y confiar en que la maneje, deberían poder delegar al agente.</p>
|
||||
<p>Esto significa:</p>
|
||||
<ul>
|
||||
<li>Criterios de aceptación claros, verificados independientemente</li>
|
||||
<li>Comportamiento autocorrectivo cuando algo sale mal</li>
|
||||
<li>Escalamiento (a Oracle, al usuario) solo cuando es verdaderamente necesario</li>
|
||||
<li>Trabajo completo, no "casi hecho"</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 6 -->
|
||||
<section id="core-loop">
|
||||
<h2>El Bucle Central</h2>
|
||||
|
||||
<div class="ascii-art">
|
||||
Intención Humana → Ejecución del Agente → Resultado Verificado
|
||||
↑ ↓
|
||||
└──────────── Mínimo ──────────────┘
|
||||
(intervención solo en fallo verdadero)
|
||||
</div>
|
||||
|
||||
<p>Todo en <a href="https://github.com/code-yeongyu/oh-my-opencode" target="_blank" rel="noopener">Oh My OpenCode</a> está diseñado para hacer que este bucle funcione:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Característica</th>
|
||||
<th>Propósito</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Prometheus</td>
|
||||
<td>Extraer intención a través de entrevista inteligente</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Metis</td>
|
||||
<td>Capturar ambigüedades antes de que se conviertan en bugs</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Momus</td>
|
||||
<td>Verificar que los planes estén completos antes de la ejecución</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Orchestrator</td>
|
||||
<td>Coordinar el trabajo sin microgestión humana</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Todo Continuation</td>
|
||||
<td>Forzar finalización, prevenir mentiras de "ya terminé"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sistema de Categorías</td>
|
||||
<td>Enrutar al modelo óptimo sin decisión humana</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Agentes en Segundo Plano</td>
|
||||
<td>Investigación paralela sin bloquear al usuario</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Acumulación de Sabiduría</td>
|
||||
<td>Aprender del trabajo, no repetir errores</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 7 -->
|
||||
<section id="future">
|
||||
<h2>El Futuro Que Estamos Construyendo</h2>
|
||||
|
||||
<p>Un mundo donde:</p>
|
||||
<ul>
|
||||
<li>Los desarrolladores humanos se enfocan en <strong>qué</strong> construir, no en <strong>cómo</strong> hacer que la IA lo construya</li>
|
||||
<li>La calidad del código es independiente de quién (o qué) lo escribió</li>
|
||||
<li>Los proyectos complejos son tan fáciles como los simples (solo toman más tiempo)</li>
|
||||
<li>La "ingeniería de prompts" se vuelve tan obsoleta como la "depuración de compiladores"</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>El agente debería ser invisible.</strong> No en el sentido de que esté oculto, sino en el sentido de que simplemente funciona - como la electricidad, como el agua corriente, como el internet.</p>
|
||||
|
||||
<p>Ustedes tocan el interruptor. La luz se enciende. No piensan en la red eléctrica.</p>
|
||||
|
||||
<p class="final-statement">Esa es la meta.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="footer-content">
|
||||
<a href="https://github.com/code-yeongyu/oh-my-opencode" class="cta-link" target="_blank" rel="noopener">
|
||||
Obtén Oh My OpenCode →
|
||||
</a>
|
||||
<p><strong>just ulw ulw</strong></p>
|
||||
|
||||
<nav class="language-selector" aria-label="Selección de idioma">
|
||||
<span class="language-selector-label">Idioma</span>
|
||||
<div class="language-links">
|
||||
<a href="../" class="language-link" lang="en">English</a>
|
||||
<a href="../ko/" class="language-link" lang="ko">한국어</a>
|
||||
<a href="../ja/" class="language-link" lang="ja">日本語</a>
|
||||
<a href="../zh/" class="language-link" lang="zh">简体中文</a>
|
||||
<a href="./" class="language-link active" lang="es">Español</a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
BIN
ultrawork-manifesto-web/src/images/favicon.png
Normal file
|
After Width: | Height: | Size: 383 KiB |
BIN
ultrawork-manifesto-web/src/images/footer-pattern.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
ultrawork-manifesto-web/src/images/hero.png
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
BIN
ultrawork-manifesto-web/src/images/og-image-old.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
ultrawork-manifesto-web/src/images/og-image.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
ultrawork-manifesto-web/src/images/orb-divider.png
Normal file
|
After Width: | Height: | Size: 944 KiB |
341
ultrawork-manifesto-web/src/index.html
Normal file
@@ -0,0 +1,341 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<title>Ultrawork Manifesto | The Philosophy of High-Output Engineering</title>
|
||||
<meta name="description" content="A philosophical framework for developers and AI engineers to achieve extreme productivity through deep focus, atomic execution, and the Ultrawork methodology.">
|
||||
<meta name="keywords" content="ultrawork, developer productivity, AI engineering, deep work, tech manifesto, oh my opencode, sisyphus">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large">
|
||||
<link rel="canonical" href="https://ulw.dev/">
|
||||
<link rel="alternate" hreflang="en" href="https://ulw.dev/">
|
||||
<link rel="alternate" hreflang="ko" href="https://ulw.dev/ko/">
|
||||
<link rel="alternate" hreflang="ja" href="https://ulw.dev/ja/">
|
||||
<link rel="alternate" hreflang="zh" href="https://ulw.dev/zh/">
|
||||
<link rel="alternate" hreflang="es" href="https://ulw.dev/es/">
|
||||
<link rel="alternate" hreflang="x-default" href="https://ulw.dev/">
|
||||
<meta name="theme-color" content="#0a0a0a">
|
||||
<meta name="author" content="Yeongyu Kim">
|
||||
<link rel="icon" type="image/png" href="./images/favicon.png">
|
||||
<link rel="apple-touch-icon" href="./images/favicon.png">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://ulw.dev/">
|
||||
<meta property="og:title" content="Ultrawork Manifesto | The Philosophy of High-Output Engineering">
|
||||
<meta property="og:description" content="A philosophical framework for developers and AI engineers to achieve extreme productivity through deep focus, atomic execution, and the Ultrawork methodology.">
|
||||
<meta property="og:image" content="https://ulw.dev/images/og-image.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="Ultrawork Manifesto - A Blueprint for Meaningful Work">
|
||||
<meta property="og:site_name" content="Ultrawork">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@justsisyphus">
|
||||
<meta name="twitter:creator" content="@justsisyphus">
|
||||
<meta name="twitter:title" content="Ultrawork Manifesto | The Philosophy of High-Output Engineering">
|
||||
<meta name="twitter:description" content="A philosophical framework for developers and AI engineers to achieve extreme productivity through deep focus, atomic execution, and the Ultrawork methodology.">
|
||||
<meta name="twitter:image" content="https://ulw.dev/images/og-image.png">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="hero-container">
|
||||
<img src="./images/hero.png" alt="Ultrawork Hero" class="hero-image">
|
||||
<h1 class="hero-title">Ultrawork Manifesto</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Section 1 -->
|
||||
<section id="human-intervention">
|
||||
<h2>Human Intervention is a Failure Signal</h2>
|
||||
|
||||
<div class="gold-gradient-text bottleneck-text">
|
||||
HUMAN IN THE LOOP = BOTTLENECK<br>
|
||||
HUMAN IN THE LOOP = BOTTLENECK<br>
|
||||
HUMAN IN THE LOOP = BOTTLENECK
|
||||
</div>
|
||||
|
||||
<p>Think about autonomous driving. When a human has to take over the wheel, that's not a feature - it's a failure of the system. The car couldn't handle the situation on its own.</p>
|
||||
|
||||
<h3>Why is coding any different?</h3>
|
||||
|
||||
<p>When you find yourself:</p>
|
||||
<ul>
|
||||
<li>Fixing the AI's half-finished code</li>
|
||||
<li>Manually correcting obvious mistakes</li>
|
||||
<li>Guiding the agent step-by-step through a task</li>
|
||||
<li>Repeatedly clarifying the same requirements</li>
|
||||
</ul>
|
||||
|
||||
<p>...that's not "human-AI collaboration." That's the AI failing to do its job.</p>
|
||||
|
||||
<p><strong><a href="https://github.com/code-yeongyu/oh-my-opencode" target="_blank" rel="noopener">Oh My OpenCode</a> is built on this premise</strong>: Human intervention during agentic work is fundamentally a wrong signal. If the system is designed correctly, the agent should complete the work without requiring you to babysit it.</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="./images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 2 -->
|
||||
<section id="indistinguishable-code">
|
||||
<h2>Indistinguishable Code</h2>
|
||||
|
||||
<p class="highlight-box"><strong>Goal: Code written by the agent should be indistinguishable from code written by a senior engineer.</strong></p>
|
||||
|
||||
<p>Not "AI-generated code that needs cleanup." Not "a good starting point." The actual, final, production-ready code.</p>
|
||||
|
||||
<p>This means:</p>
|
||||
<ul>
|
||||
<li>Following existing codebase patterns exactly</li>
|
||||
<li>Proper error handling without being asked</li>
|
||||
<li>Tests that actually test the right things</li>
|
||||
<li>No AI slop (over-engineering, unnecessary abstractions, scope creep)</li>
|
||||
<li>Comments only when they add value</li>
|
||||
</ul>
|
||||
|
||||
<p>If you can tell whether a commit was made by a human or an agent, the agent has failed.</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="./images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 3 -->
|
||||
<section id="token-cost">
|
||||
<h2>Token Cost vs. Productivity</h2>
|
||||
|
||||
<p><strong>Higher token usage is acceptable if it significantly increases productivity.</strong></p>
|
||||
|
||||
<p>Using more tokens to:</p>
|
||||
<ul>
|
||||
<li>Have multiple specialized agents research in parallel</li>
|
||||
<li>Get the job done completely without human intervention</li>
|
||||
<li>Verify work thoroughly before completion</li>
|
||||
<li>Accumulate knowledge across tasks</li>
|
||||
</ul>
|
||||
|
||||
<p>...is a worthwhile investment when it means 10x, 20x, or 100x productivity gains.</p>
|
||||
|
||||
<h3>However:</h3>
|
||||
|
||||
<p>Unnecessary token waste is not pursued. The system optimizes for:</p>
|
||||
<ul>
|
||||
<li>Using cheaper models (Haiku, Flash) for simple tasks</li>
|
||||
<li>Avoiding redundant exploration</li>
|
||||
<li>Caching learnings across sessions</li>
|
||||
<li>Stopping research when sufficient context is gathered</li>
|
||||
</ul>
|
||||
|
||||
<p>Token efficiency matters. But not at the cost of work quality or human cognitive load.</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="./images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 4 -->
|
||||
<section id="cognitive-load">
|
||||
<h2>Minimize Human Cognitive Load</h2>
|
||||
|
||||
<p><strong>The human should only need to say what they want. Everything else is the agent's job.</strong></p>
|
||||
|
||||
<p>Two approaches to achieve this:</p>
|
||||
|
||||
<div class="approach-container">
|
||||
<div class="approach ultrawork-approach">
|
||||
<h3>Approach 1: Ultrawork</h3>
|
||||
<p class="approach-tagline">Just say "ulw" and walk away.</p>
|
||||
<p>You say: <code>ulw add authentication</code></p>
|
||||
<p>The agent autonomously:</p>
|
||||
<ul>
|
||||
<li>Analyzes your codebase patterns and architecture</li>
|
||||
<li>Researches best practices from official docs</li>
|
||||
<li>Plans the implementation strategy internally</li>
|
||||
<li>Implements following your existing conventions</li>
|
||||
<li>Verifies with tests and LSP diagnostics</li>
|
||||
<li>Self-corrects when something goes wrong</li>
|
||||
<li><strong>Keeps bouldering until 100% complete</strong></li>
|
||||
</ul>
|
||||
<p class="approach-summary"><strong>Zero intervention. Full autonomy. Just results.</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="approach prometheus-approach">
|
||||
<h3>Approach 2: Prometheus + Atlas</h3>
|
||||
<p class="approach-tagline">When you want strategic control.</p>
|
||||
<p>Press <kbd>Tab</kbd> to switch agents, then: <code>add authentication</code></p>
|
||||
<p><strong>Prometheus</strong> (Strategic Planner):</p>
|
||||
<ul>
|
||||
<li>Conducts deep codebase research via parallel agents</li>
|
||||
<li>Interviews you with intelligent, contextual questions</li>
|
||||
<li>Identifies edge cases and architectural implications</li>
|
||||
<li>Generates a detailed YAML work plan with dependencies</li>
|
||||
</ul>
|
||||
<p><strong>Atlas</strong> (Master Orchestrator):</p>
|
||||
<ul>
|
||||
<li>Executes the plan via <code>/start-work</code></li>
|
||||
<li>Delegates tasks to specialized agents (Oracle, Frontend Engineer, etc.)</li>
|
||||
<li>Manages parallel execution waves for efficiency</li>
|
||||
<li>Tracks progress, handles failures, ensures completion</li>
|
||||
</ul>
|
||||
<p class="approach-summary"><strong>You architect. Agents execute. Full transparency.</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>In both cases, the human's job is to <strong>express what they want</strong>, not to manage how it gets done.</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="./images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 5 -->
|
||||
<section id="predictable-continuous">
|
||||
<h2>Predictable, Continuous, Delegatable</h2>
|
||||
|
||||
<p><strong>The ideal agent should work like a compiler</strong>: markdown document goes in, working code comes out.</p>
|
||||
|
||||
<h3>Predictable</h3>
|
||||
<p>Given the same inputs:</p>
|
||||
<ul>
|
||||
<li>Same codebase patterns</li>
|
||||
<li>Same requirements</li>
|
||||
<li>Same constraints</li>
|
||||
</ul>
|
||||
<p>...the output should be consistent. Not random, not surprising, not "creative" in ways you didn't ask for.</p>
|
||||
|
||||
<h3>Continuous</h3>
|
||||
<p>Work should survive interruptions:</p>
|
||||
<ul>
|
||||
<li>Session crashes? Resume with <code>/start-work</code></li>
|
||||
<li>Need to step away? Progress is tracked</li>
|
||||
<li>Multi-day project? Context is preserved</li>
|
||||
</ul>
|
||||
<p>The agent maintains state. You don't have to.</p>
|
||||
|
||||
<h3>Delegatable</h3>
|
||||
<p>Just like you can assign a task to a capable team member and trust them to handle it, you should be able to delegate to the agent.</p>
|
||||
<p>This means:</p>
|
||||
<ul>
|
||||
<li>Clear acceptance criteria, verified independently</li>
|
||||
<li>Self-correcting behavior when something goes wrong</li>
|
||||
<li>Escalation (to Oracle, to user) only when truly needed</li>
|
||||
<li>Complete work, not "mostly done"</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="./images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 6 -->
|
||||
<section id="core-loop">
|
||||
<h2>The Core Loop</h2>
|
||||
|
||||
<div class="ascii-art">
|
||||
Human Intent → Agent Execution → Verified Result
|
||||
↑ ↓
|
||||
└──────── Minimum ─────────────┘
|
||||
(intervention only on true failure)
|
||||
</div>
|
||||
|
||||
<p>Everything in <a href="https://github.com/code-yeongyu/oh-my-opencode" target="_blank" rel="noopener">Oh My OpenCode</a> is designed to make this loop work:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Prometheus</td>
|
||||
<td>Extract intent through intelligent interview</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Metis</td>
|
||||
<td>Catch ambiguities before they become bugs</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Momus</td>
|
||||
<td>Verify plans are complete before execution</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Orchestrator</td>
|
||||
<td>Coordinate work without human micromanagement</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Todo Continuation</td>
|
||||
<td>Force completion, prevent "I'm done" lies</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Category System</td>
|
||||
<td>Route to optimal model without human decision</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Background Agents</td>
|
||||
<td>Parallel research without blocking user</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wisdom Accumulation</td>
|
||||
<td>Learn from work, don't repeat mistakes</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="./images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 7 -->
|
||||
<section id="future">
|
||||
<h2>The Future We're Building</h2>
|
||||
|
||||
<p>A world where:</p>
|
||||
<ul>
|
||||
<li>Human developers focus on <strong>what</strong> to build, not <strong>how</strong> to get AI to build it</li>
|
||||
<li>Code quality is independent of who (or what) wrote it</li>
|
||||
<li>Complex projects are as easy as simple ones (just take longer)</li>
|
||||
<li>"Prompt engineering" becomes as obsolete as "compiler debugging"</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>The agent should be invisible.</strong> Not in the sense that it's hidden, but in the sense that it just works - like electricity, like running water, like the internet.</p>
|
||||
|
||||
<p>You flip the switch. The light turns on. You don't think about the power grid.</p>
|
||||
|
||||
<p class="final-statement">That's the goal.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="footer-content">
|
||||
<a href="https://github.com/code-yeongyu/oh-my-opencode" class="cta-link" target="_blank" rel="noopener">
|
||||
Get Oh My OpenCode →
|
||||
</a>
|
||||
<p><strong>just ulw ulw</strong></p>
|
||||
|
||||
<nav class="language-selector" aria-label="Language selection">
|
||||
<span class="language-selector-label">Language</span>
|
||||
<div class="language-links">
|
||||
<a href="/" class="language-link active" lang="en">English</a>
|
||||
<a href="/ko/" class="language-link" lang="ko">한국어</a>
|
||||
<a href="/ja/" class="language-link" lang="ja">日本語</a>
|
||||
<a href="/zh/" class="language-link" lang="zh">简体中文</a>
|
||||
<a href="/es/" class="language-link" lang="es">Español</a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
341
ultrawork-manifesto-web/src/ja/index.html
Normal file
@@ -0,0 +1,341 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<title>Ultrawork Manifesto | 高出力エンジニアリングの哲学</title>
|
||||
<meta name="description" content="開発者とAIエンジニアが、深い集中、原子的実行、そしてUltraworkメソッドを通じて極限の生産性を達成するための哲学的フレームワーク。">
|
||||
<meta name="keywords" content="ultrawork, 開発者の生産性, AIエンジニアリング, ディープワーク, 技術マニフェスト, oh my opencode, sisyphus">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large">
|
||||
<link rel="canonical" href="https://ulw.dev/ja/">
|
||||
<link rel="alternate" hreflang="en" href="https://ulw.dev/">
|
||||
<link rel="alternate" hreflang="ko" href="https://ulw.dev/ko/">
|
||||
<link rel="alternate" hreflang="ja" href="https://ulw.dev/ja/">
|
||||
<link rel="alternate" hreflang="zh" href="https://ulw.dev/zh/">
|
||||
<link rel="alternate" hreflang="es" href="https://ulw.dev/es/">
|
||||
<link rel="alternate" hreflang="x-default" href="https://ulw.dev/">
|
||||
<meta name="theme-color" content="#0a0a0a">
|
||||
<meta name="author" content="Yeongyu Kim">
|
||||
<link rel="icon" type="image/png" href="../images/favicon.png">
|
||||
<link rel="apple-touch-icon" href="../images/favicon.png">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://ulw.dev/ja/">
|
||||
<meta property="og:title" content="Ultrawork Manifesto | 高出力エンジニアリングの哲学">
|
||||
<meta property="og:description" content="開発者とAIエンジニアが、深い集中、原子的実行、そしてUltraworkメソッドを通じて極限の生産性を達成するための哲学的フレームワーク。">
|
||||
<meta property="og:image" content="https://ulw.dev/images/og-image.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="Ultrawork Manifesto - 意義ある仕事への青写真">
|
||||
<meta property="og:site_name" content="Ultrawork">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@justsisyphus">
|
||||
<meta name="twitter:creator" content="@justsisyphus">
|
||||
<meta name="twitter:title" content="Ultrawork Manifesto | 高出力エンジニアリングの哲学">
|
||||
<meta name="twitter:description" content="開発者とAIエンジニアが、深い集中、原子的実行、そしてUltraworkメソッドを通じて極限の生産性を達成するための哲学的フレームワーク。">
|
||||
<meta name="twitter:image" content="https://ulw.dev/images/og-image.png">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="hero-container">
|
||||
<img src="../images/hero.png" alt="Ultrawork Hero" class="hero-image">
|
||||
<h1 class="hero-title">Ultrawork Manifesto</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Section 1 -->
|
||||
<section id="human-intervention">
|
||||
<h2>人間の介入は失敗のシグナル</h2>
|
||||
|
||||
<div class="gold-gradient-text bottleneck-text">
|
||||
ヒューマン・イン・ザ・ループ = ボトルネック<br>
|
||||
ヒューマン・イン・ザ・ループ = ボトルネック<br>
|
||||
ヒューマン・イン・ザ・ループ = ボトルネック
|
||||
</div>
|
||||
|
||||
<p>自動運転について考えてみてください。人間がハンドルを握らなければならない時、それは機能ではなく、システムの失敗です。車がその状況を自力で処理できなかったのです。</p>
|
||||
|
||||
<h3>なぜコーディングは違うと言えるのでしょうか?</h3>
|
||||
|
||||
<p>次のような状況に陥った時:</p>
|
||||
<ul>
|
||||
<li>AIの中途半端なコードを修正している</li>
|
||||
<li>明らかな間違いを手動で直している</li>
|
||||
<li>タスクを通してエージェントを一歩一歩導いている</li>
|
||||
<li>同じ要件を繰り返し説明している</li>
|
||||
</ul>
|
||||
|
||||
<p>...それは「人間とAIの協調」ではありません。AIが仕事を果たせていないだけです。</p>
|
||||
|
||||
<p><strong><a href="https://github.com/code-yeongyu/oh-my-opencode" target="_blank" rel="noopener">Oh My OpenCode</a> はこの前提の上に構築されています</strong>:エージェンティックな作業への人間の介入は、根本的に誤ったシグナルです。システムが正しく設計されていれば、エージェントはあなたの子守りを必要とせず、仕事を完遂するはずです。</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 2 -->
|
||||
<section id="indistinguishable-code">
|
||||
<h2>見分けのつかないコード</h2>
|
||||
|
||||
<p class="highlight-box"><strong>目標:エージェントが書いたコードは、シニアエンジニアが書いたコードと見分けがつかないものであるべきです。</strong></p>
|
||||
|
||||
<p>「手直しが必要なAI生成コード」ではありません。「良い出発点」でもありません。実際の、最終的な、本番環境で使えるコードです。</p>
|
||||
|
||||
<p>これは以下を意味します:</p>
|
||||
<ul>
|
||||
<li>既存のコードベースのパターンに正確に従う</li>
|
||||
<li>指示されなくても適切なエラーハンドリングを行う</li>
|
||||
<li>実際に正しいことをテストするテストコード</li>
|
||||
<li>AIによる粗雑なコード(オーバーエンジニアリング、不必要な抽象化、スコープクリープ)がない</li>
|
||||
<li>価値がある場合にのみコメントを追加する</li>
|
||||
</ul>
|
||||
|
||||
<p>コミットが人間によるものかエージェントによるものか見分けがつくなら、そのエージェントは失敗しています。</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 3 -->
|
||||
<section id="token-cost">
|
||||
<h2>トークンコスト vs 生産性</h2>
|
||||
|
||||
<p><strong>生産性を著しく向上させるのであれば、より高いトークン使用量は許容されます。</strong></p>
|
||||
|
||||
<p>より多くのトークンを使って:</p>
|
||||
<ul>
|
||||
<li>複数の専門エージェントに並行して調査させる</li>
|
||||
<li>人間の介入なしに仕事を完全に終わらせる</li>
|
||||
<li>完了前に作業を徹底的に検証する</li>
|
||||
<li>タスクを超えて知識を蓄積する</li>
|
||||
</ul>
|
||||
|
||||
<p>...これらは、10倍、20倍、あるいは100倍の生産性向上を意味するのであれば、価値ある投資です。</p>
|
||||
|
||||
<h3>しかし:</h3>
|
||||
|
||||
<p>不必要なトークンの浪費は追求しません。システムは以下に向けて最適化されます:</p>
|
||||
<ul>
|
||||
<li>単純なタスクには安価なモデル(Haiku, Flash)を使用する</li>
|
||||
<li>重複する探索を避ける</li>
|
||||
<li>セッション間で学習内容をキャッシュする</li>
|
||||
<li>十分なコンテキストが集まったら調査を停止する</li>
|
||||
</ul>
|
||||
|
||||
<p>トークン効率は重要です。しかし、仕事の質や人間の認知的負荷を犠牲にしてまで優先されるべきではありません。</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 4 -->
|
||||
<section id="cognitive-load">
|
||||
<h2>人間の認知的負荷を最小化する</h2>
|
||||
|
||||
<p><strong>人間は「何が欲しいか」を言うだけでいいはずです。それ以外はすべてエージェントの仕事です。</strong></p>
|
||||
|
||||
<p>これを達成するための2つのアプローチ:</p>
|
||||
|
||||
<div class="approach-container">
|
||||
<div class="approach ultrawork-approach">
|
||||
<h3>アプローチ 1: Ultrawork</h3>
|
||||
<p class="approach-tagline">ただ "ulw" と言って立ち去るだけ。</p>
|
||||
<p>あなたの発言: <code>ulw add authentication</code></p>
|
||||
<p>エージェントは自律的に:</p>
|
||||
<ul>
|
||||
<li>コードベースのパターンとアーキテクチャを分析する</li>
|
||||
<li>公式ドキュメントからベストプラクティスを調査する</li>
|
||||
<li>実装戦略を内部的に計画する</li>
|
||||
<li>既存の規約に従って実装する</li>
|
||||
<li>テストとLSP診断で検証する</li>
|
||||
<li>何か問題があれば自己修正する</li>
|
||||
<li><strong>100%完了するまで岩を押し続ける</strong></li>
|
||||
</ul>
|
||||
<p class="approach-summary"><strong>介入ゼロ。完全な自律性。結果だけを。</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="approach prometheus-approach">
|
||||
<h3>アプローチ 2: Prometheus + Atlas</h3>
|
||||
<p class="approach-tagline">戦略的なコントロールが必要な時。</p>
|
||||
<p><kbd>Tab</kbd>を押してエージェントを切り替えた後: <code>add authentication</code></p>
|
||||
<p><strong>Prometheus</strong> (戦略プランナー):</p>
|
||||
<ul>
|
||||
<li>並列エージェントを通じて深いコードベース調査を行う</li>
|
||||
<li>知的で文脈に沿った質問であなたにインタビューする</li>
|
||||
<li>エッジケースとアーキテクチャへの影響を特定する</li>
|
||||
<li>依存関係を含む詳細なYAML作業計画を生成する</li>
|
||||
</ul>
|
||||
<p><strong>Atlas</strong> (マスターオーケストレーター):</p>
|
||||
<ul>
|
||||
<li><code>/start-work</code> を通じて計画を実行する</li>
|
||||
<li>専門エージェント(Oracle, Frontend Engineerなど)にタスクを委譲する</li>
|
||||
<li>効率のために並列実行の波を管理する</li>
|
||||
<li>進捗を追跡し、失敗を処理し、完了を保証する</li>
|
||||
</ul>
|
||||
<p class="approach-summary"><strong>あなたが設計し、エージェントが実行する。完全な透明性。</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>どちらの場合も、人間の仕事は<strong>何が欲しいかを表現すること</strong>であり、どうやってそれを実現するかを管理することではありません。</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 5 -->
|
||||
<section id="predictable-continuous">
|
||||
<h2>予測可能、継続的、委譲可能</h2>
|
||||
|
||||
<p><strong>理想的なエージェントはコンパイラのように動作すべきです</strong>:Markdownドキュメントを入力すれば、動作するコードが出力されるのです。</p>
|
||||
|
||||
<h3>予測可能</h3>
|
||||
<p>同じ入力があれば:</p>
|
||||
<ul>
|
||||
<li>同じコードベースのパターン</li>
|
||||
<li>同じ要件</li>
|
||||
<li>同じ制約</li>
|
||||
</ul>
|
||||
<p>...出力は一貫しているべきです。ランダムでも、驚くようなものでもなく、頼んでもいない「創造的」なものであってはなりません。</p>
|
||||
|
||||
<h3>継続的</h3>
|
||||
<p>作業は中断に耐えうるべきです:</p>
|
||||
<ul>
|
||||
<li>セッションがクラッシュした? <code>/start-work</code> で再開</li>
|
||||
<li>席を外す必要がある? 進捗は追跡されています</li>
|
||||
<li>数日にわたるプロジェクト? コンテキストは保持されます</li>
|
||||
</ul>
|
||||
<p>エージェントが状態を維持します。あなたがする必要はありません。</p>
|
||||
|
||||
<h3>委譲可能</h3>
|
||||
<p>優秀なチームメンバーにタスクを割り当てて任せることができるように、エージェントにも委譲できるべきです。</p>
|
||||
<p>これは以下を意味します:</p>
|
||||
<ul>
|
||||
<li>明確な受け入れ基準、独立して検証される</li>
|
||||
<li>何か問題が起きた時の自己修正動作</li>
|
||||
<li>本当に必要な時だけの(Oracleやユーザーへの)エスカレーション</li>
|
||||
<li>「ほぼ完了」ではなく、完全な仕事</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 6 -->
|
||||
<section id="core-loop">
|
||||
<h2>コアループ</h2>
|
||||
|
||||
<div class="ascii-art">
|
||||
人間の意図 → エージェントの実行 → 検証された結果
|
||||
↑ ↓
|
||||
└────────── 最小限 ────────────┘
|
||||
(真の失敗時のみ介入)
|
||||
</div>
|
||||
|
||||
<p>[Oh My OpenCode](https://github.com/code-yeongyu/oh-my-opencode) のすべては、このループを機能させるために設計されています:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>機能</th>
|
||||
<th>目的</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Prometheus</td>
|
||||
<td>知的なインタビューを通じて意図を抽出する</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Metis</td>
|
||||
<td>バグになる前に曖昧さを捉える</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Momus</td>
|
||||
<td>実行前に計画が完全であることを検証する</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Orchestrator</td>
|
||||
<td>人間のマイクロマネジメントなしに作業を調整する</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Todo Continuation</td>
|
||||
<td>完了を強制し、「終わりました」という嘘を防ぐ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Category System</td>
|
||||
<td>人間の判断なしに最適なモデルへルーティングする</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Background Agents</td>
|
||||
<td>ユーザーをブロックせずに並列調査を行う</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wisdom Accumulation</td>
|
||||
<td>作業から学び、過ちを繰り返さない</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 7 -->
|
||||
<section id="future">
|
||||
<h2>私たちが築く未来</h2>
|
||||
|
||||
<p>次のような世界:</p>
|
||||
<ul>
|
||||
<li>人間の開発者は、AIにどう作らせるかではなく、<strong>何を</strong>作るかに集中する</li>
|
||||
<li>コードの品質は、誰が(あるいは何が)書いたかとは無関係である</li>
|
||||
<li>複雑なプロジェクトも単純なプロジェクトと同じくらい簡単になる(ただ時間がかかるだけ)</li>
|
||||
<li>「プロンプトエンジニアリング」が「コンパイラデバッグ」と同じくらい時代遅れになる</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>エージェントは不可視であるべきです。</strong> 隠されているという意味ではなく、電気や水道、インターネットのように、ただ当たり前に機能するという意味で。</p>
|
||||
|
||||
<p>スイッチを入れる。明かりがつく。送電網のことなど考えもしない。</p>
|
||||
|
||||
<p class="final-statement">それが目標です。</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="footer-content">
|
||||
<a href="https://github.com/code-yeongyu/oh-my-opencode" class="cta-link" target="_blank" rel="noopener">
|
||||
Oh My OpenCode を入手 →
|
||||
</a>
|
||||
<p><strong>just ulw ulw</strong></p>
|
||||
|
||||
<nav class="language-selector" aria-label="Language selection">
|
||||
<span class="language-selector-label">Language</span>
|
||||
<div class="language-links">
|
||||
<a href="../" class="language-link" lang="en">English</a>
|
||||
<a href="../ko/" class="language-link" lang="ko">한국어</a>
|
||||
<a href="./" class="language-link active" lang="ja">日本語</a>
|
||||
<a href="../zh/" class="language-link" lang="zh">简体中文</a>
|
||||
<a href="../es/" class="language-link" lang="es">Español</a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
341
ultrawork-manifesto-web/src/ko/index.html
Normal file
@@ -0,0 +1,341 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<title>Ultrawork Manifesto | 고성능 엔지니어링의 철학</title>
|
||||
<meta name="description" content="개발자와 AI 엔지니어가 깊은 몰입, 원자적 실행, 그리고 Ultrawork 방법론을 통해 극한의 생산성을 달성하기 위한 철학적 프레임워크입니다.">
|
||||
<meta name="keywords" content="ultrawork, 개발자 생산성, AI 엔지니어링, 딥워크, 기술 선언문, oh my opencode, sisyphus">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large">
|
||||
<link rel="canonical" href="https://ulw.dev/ko/">
|
||||
<link rel="alternate" hreflang="en" href="https://ulw.dev/">
|
||||
<link rel="alternate" hreflang="ko" href="https://ulw.dev/ko/">
|
||||
<link rel="alternate" hreflang="ja" href="https://ulw.dev/ja/">
|
||||
<link rel="alternate" hreflang="zh" href="https://ulw.dev/zh/">
|
||||
<link rel="alternate" hreflang="es" href="https://ulw.dev/es/">
|
||||
<link rel="alternate" hreflang="x-default" href="https://ulw.dev/">
|
||||
<meta name="theme-color" content="#0a0a0a">
|
||||
<meta name="author" content="Yeongyu Kim">
|
||||
<link rel="icon" type="image/png" href="../images/favicon.png">
|
||||
<link rel="apple-touch-icon" href="../images/favicon.png">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://ulw.dev/ko/">
|
||||
<meta property="og:title" content="Ultrawork Manifesto | 고성능 엔지니어링의 철학">
|
||||
<meta property="og:description" content="개발자와 AI 엔지니어가 깊은 몰입, 원자적 실행, 그리고 Ultrawork 방법론을 통해 극한의 생산성을 달성하기 위한 철학적 프레임워크입니다.">
|
||||
<meta property="og:image" content="https://ulw.dev/images/og-image.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="Ultrawork Manifesto - 의미 있는 작업을 위한 청사진">
|
||||
<meta property="og:site_name" content="Ultrawork">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@justsisyphus">
|
||||
<meta name="twitter:creator" content="@justsisyphus">
|
||||
<meta name="twitter:title" content="Ultrawork Manifesto | 고성능 엔지니어링의 철학">
|
||||
<meta name="twitter:description" content="개발자와 AI 엔지니어가 깊은 몰입, 원자적 실행, 그리고 Ultrawork 방법론을 통해 극한의 생산성을 달성하기 위한 철학적 프레임워크입니다.">
|
||||
<meta name="twitter:image" content="https://ulw.dev/images/og-image.png">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="hero-container">
|
||||
<img src="../images/hero.png" alt="Ultrawork Hero" class="hero-image">
|
||||
<h1 class="hero-title">Ultrawork Manifesto</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Section 1 -->
|
||||
<section id="human-intervention">
|
||||
<h2>인간의 개입은 실패 신호다</h2>
|
||||
|
||||
<div class="gold-gradient-text bottleneck-text">
|
||||
HUMAN IN THE LOOP = BOTTLENECK<br>
|
||||
HUMAN IN THE LOOP = BOTTLENECK<br>
|
||||
HUMAN IN THE LOOP = BOTTLENECK
|
||||
</div>
|
||||
|
||||
<p>자율 주행을 생각해 보십시오. 인간이 운전대를 잡아야 한다면, 그것은 기능이 아니라 시스템의 실패입니다. 자동차가 스스로 상황을 감당하지 못한 것입니다.</p>
|
||||
|
||||
<h3>코딩이라고 다를 이유가 있습니까?</h3>
|
||||
|
||||
<p>당신이 다음과 같은 상황에 처해 있다면:</p>
|
||||
<ul>
|
||||
<li>AI가 작성하다 만 코드를 고치고 있거나</li>
|
||||
<li>뻔한 실수를 직접 수정하고 있거나</li>
|
||||
<li>에이전트에게 작업을 단계별로 하나하나 지시하고 있거나</li>
|
||||
<li>같은 요구사항을 반복해서 설명하고 있다면</li>
|
||||
</ul>
|
||||
|
||||
<p>...그것은 "인간과 AI의 협업"이 아닙니다. AI가 제 역할을 못하고 있는 것입니다.</p>
|
||||
|
||||
<p><strong><a href="https://github.com/code-yeongyu/oh-my-opencode" target="_blank" rel="noopener">Oh My OpenCode</a>는 이 전제 위에 구축되었습니다</strong>: 에이전트 작업 중 인간의 개입은 근본적으로 잘못된 신호입니다. 시스템이 올바르게 설계되었다면, 에이전트는 당신이 돌봐주지 않아도 작업을 완수해야 합니다.</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 2 -->
|
||||
<section id="indistinguishable-code">
|
||||
<h2>구별할 수 없는 코드</h2>
|
||||
|
||||
<p class="highlight-box"><strong>목표: 에이전트가 작성한 코드는 시니어 엔지니어가 작성한 코드와 구별할 수 없어야 한다.</strong></p>
|
||||
|
||||
<p>"정리가 필요한 AI 생성 코드"가 아닙니다. "좋은 시작점"도 아닙니다. 실제, 최종, 프로덕션 준비가 완료된 코드여야 합니다.</p>
|
||||
|
||||
<p>이는 다음을 의미합니다:</p>
|
||||
<ul>
|
||||
<li>기존 코드베이스 패턴을 정확히 따르는 것</li>
|
||||
<li>요청하지 않아도 적절한 에러 처리를 하는 것</li>
|
||||
<li>실제로 필요한 것을 검증하는 테스트를 작성하는 것</li>
|
||||
<li>AI Slop(과도한 엔지니어링, 불필요한 추상화, 범위 확장)이 없는 것</li>
|
||||
<li>가치가 있을 때만 주석을 다는 것</li>
|
||||
</ul>
|
||||
|
||||
<p>커밋을 인간이 했는지 에이전트가 했는지 구분할 수 있다면, 그 에이전트는 실패한 것입니다.</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 3 -->
|
||||
<section id="token-cost">
|
||||
<h2>토큰 비용 vs. 생산성</h2>
|
||||
|
||||
<p><strong>생산성을 획기적으로 높일 수 있다면 더 많은 토큰 사용은 허용됩니다.</strong></p>
|
||||
|
||||
<p>더 많은 토큰을 사용하여:</p>
|
||||
<ul>
|
||||
<li>여러 전문 에이전트가 병렬로 조사하게 하고</li>
|
||||
<li>인간의 개입 없이 작업을 완전히 끝내고</li>
|
||||
<li>완료 전에 작업을 철저히 검증하고</li>
|
||||
<li>작업 전반에 걸쳐 지식을 축적하는 것</li>
|
||||
</ul>
|
||||
|
||||
<p>...이것이 10배, 20배, 100배의 생산성 향상을 의미한다면 가치 있는 투자입니다.</p>
|
||||
|
||||
<h3>하지만:</h3>
|
||||
|
||||
<p>불필요한 토큰 낭비는 지양합니다. 시스템은 다음을 위해 최적화합니다:</p>
|
||||
<ul>
|
||||
<li>단순 작업에는 더 저렴한 모델(Haiku, Flash) 사용</li>
|
||||
<li>중복 탐색 방지</li>
|
||||
<li>세션 간 학습 내용 캐싱</li>
|
||||
<li>충분한 맥락이 수집되면 조사 중단</li>
|
||||
</ul>
|
||||
|
||||
<p>토큰 효율성은 중요합니다. 하지만 작업 품질이나 인간의 인지 부하를 희생해서는 안 됩니다.</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 4 -->
|
||||
<section id="cognitive-load">
|
||||
<h2>인간의 인지 부하 최소화</h2>
|
||||
|
||||
<p><strong>인간은 원하는 것이 무엇인지만 말하면 됩니다. 나머지는 모두 에이전트의 몫입니다.</strong></p>
|
||||
|
||||
<p>이를 달성하기 위한 두 가지 접근 방식:</p>
|
||||
|
||||
<div class="approach-container">
|
||||
<div class="approach ultrawork-approach">
|
||||
<h3>접근 방식 1: Ultrawork</h3>
|
||||
<p class="approach-tagline">그냥 "ulw"라고 말하고 자리를 비우십시오.</p>
|
||||
<p>당신이 말합니다: <code>ulw add authentication</code></p>
|
||||
<p>에이전트는 자율적으로:</p>
|
||||
<ul>
|
||||
<li>코드베이스 패턴과 아키텍처를 분석하고</li>
|
||||
<li>공식 문서에서 모범 사례를 조사하고</li>
|
||||
<li>내부적으로 구현 전략을 수립하고</li>
|
||||
<li>기존 컨벤션을 따르며 구현하고</li>
|
||||
<li>테스트와 LSP 진단으로 검증하고</li>
|
||||
<li>문제가 생기면 스스로 수정하고</li>
|
||||
<li><strong>100% 완료될 때까지 계속 밀어붙입니다</strong></li>
|
||||
</ul>
|
||||
<p class="approach-summary"><strong>개입 제로. 완전 자율. 오직 결과뿐.</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="approach prometheus-approach">
|
||||
<h3>접근 방식 2: Prometheus + Atlas</h3>
|
||||
<p class="approach-tagline">전략적인 제어가 필요할 때.</p>
|
||||
<p><kbd>Tab</kbd>을 눌러 에이전트를 전환한 뒤: <code>add authentication</code></p>
|
||||
<p><strong>Prometheus</strong> (전략 기획자):</p>
|
||||
<ul>
|
||||
<li>병렬 에이전트를 통해 심층적인 코드베이스 조사를 수행하고</li>
|
||||
<li>지능적이고 맥락에 맞는 질문으로 당신을 인터뷰하고</li>
|
||||
<li>엣지 케이스와 아키텍처에 미칠 영향을 식별하고</li>
|
||||
<li>의존성이 포함된 상세한 YAML 작업 계획을 생성합니다</li>
|
||||
</ul>
|
||||
<p><strong>Atlas</strong> (마스터 오케스트레이터):</p>
|
||||
<ul>
|
||||
<li><code>/start-work</code>를 통해 계획을 실행하고</li>
|
||||
<li>전문 에이전트(Oracle, Frontend Engineer 등)에게 작업을 위임하고</li>
|
||||
<li>효율성을 위해 병렬 실행 웨이브를 관리하고</li>
|
||||
<li>진행 상황을 추적하고, 실패를 처리하며, 완료를 보장합니다</li>
|
||||
</ul>
|
||||
<p class="approach-summary"><strong>당신은 설계하고, 에이전트는 실행합니다. 완전한 투명성.</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>두 경우 모두, 인간의 역할은 <strong>원하는 것을 표현하는 것</strong>이지, 어떻게 할지를 관리하는 것이 아닙니다.</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 5 -->
|
||||
<section id="predictable-continuous">
|
||||
<h2>예측 가능성, 지속성, 위임 가능성</h2>
|
||||
|
||||
<p><strong>이상적인 에이전트는 컴파일러처럼 작동해야 합니다</strong>: 마크다운 문서가 들어가면, 작동하는 코드가 나와야 합니다.</p>
|
||||
|
||||
<h3>예측 가능성 (Predictable)</h3>
|
||||
<p>동일한 입력이 주어졌을 때:</p>
|
||||
<ul>
|
||||
<li>동일한 코드베이스 패턴</li>
|
||||
<li>동일한 요구사항</li>
|
||||
<li>동일한 제약조건</li>
|
||||
</ul>
|
||||
<p>...출력은 일관되어야 합니다. 무작위적이거나, 놀랍거나, 요청하지 않은 방식으로 "창의적"이어서는 안 됩니다.</p>
|
||||
|
||||
<h3>지속성 (Continuous)</h3>
|
||||
<p>작업은 중단되어도 지속되어야 합니다:</p>
|
||||
<ul>
|
||||
<li>세션이 충돌했나요? <code>/start-work</code>로 재개하십시오</li>
|
||||
<li>자리를 비워야 하나요? 진행 상황은 추적됩니다</li>
|
||||
<li>며칠 걸리는 프로젝트인가요? 맥락은 보존됩니다</li>
|
||||
</ul>
|
||||
<p>상태 유지는 에이전트가 합니다. 당신이 할 필요가 없습니다.</p>
|
||||
|
||||
<h3>위임 가능성 (Delegatable)</h3>
|
||||
<p>유능한 팀원에게 업무를 맡기고 믿는 것처럼, 에이전트에게도 위임할 수 있어야 합니다.</p>
|
||||
<p>이는 다음을 의미합니다:</p>
|
||||
<ul>
|
||||
<li>독립적으로 검증된 명확한 인수 조건</li>
|
||||
<li>문제가 발생했을 때의 자가 수정 행동</li>
|
||||
<li>정말 필요할 때만 (Oracle이나 사용자에게) 에스컬레이션</li>
|
||||
<li>"거의 다 된" 것이 아닌, 완전히 끝난 작업</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 6 -->
|
||||
<section id="core-loop">
|
||||
<h2>핵심 루프 (The Core Loop)</h2>
|
||||
|
||||
<div class="ascii-art">
|
||||
Human Intent → Agent Execution → Verified Result
|
||||
↑ ↓
|
||||
└──────── Minimum ─────────────┘
|
||||
(intervention only on true failure)
|
||||
</div>
|
||||
|
||||
<p><a href="https://github.com/code-yeongyu/oh-my-opencode" target="_blank" rel="noopener">Oh My OpenCode</a>의 모든 것은 이 루프가 작동하도록 설계되었습니다:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>기능</th>
|
||||
<th>목적</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Prometheus</td>
|
||||
<td>지능형 인터뷰를 통해 의도 추출</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Metis</td>
|
||||
<td>버그가 되기 전에 모호함 포착</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Momus</td>
|
||||
<td>실행 전 계획의 완전성 검증</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Orchestrator</td>
|
||||
<td>인간의 마이크로매니지먼트 없이 작업 조정</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Todo Continuation</td>
|
||||
<td>완료를 강제하고, "다 했어요"라는 거짓말 방지</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Category System</td>
|
||||
<td>인간의 결정 없이 최적의 모델로 라우팅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Background Agents</td>
|
||||
<td>사용자를 차단하지 않고 병렬 조사</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wisdom Accumulation</td>
|
||||
<td>작업에서 학습하여 실수 반복 방지</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 7 -->
|
||||
<section id="future">
|
||||
<h2>우리가 만드는 미래</h2>
|
||||
|
||||
<p>다음과 같은 세상입니다:</p>
|
||||
<ul>
|
||||
<li>인간 개발자는 AI에게 어떻게 만들게 할지가 아니라, <strong>무엇</strong>을 만들지에 집중합니다</li>
|
||||
<li>코드 품질이 누가(또는 무엇이) 작성했는지와 무관합니다</li>
|
||||
<li>복잡한 프로젝트도 단순한 프로젝트만큼 쉽습니다 (단지 시간이 더 걸릴 뿐)</li>
|
||||
<li>"프롬프트 엔지니어링"이 "컴파일러 디버깅"만큼이나 구시대의 유물이 됩니다</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>에이전트는 보이지 않아야 합니다.</strong> 숨겨져 있다는 뜻이 아니라, 전기나 수돗물, 인터넷처럼 그저 작동한다는 의미에서 그렇습니다.</p>
|
||||
|
||||
<p>스위치를 켜면 불이 들어옵니다. 전력망에 대해서는 생각하지 않습니다.</p>
|
||||
|
||||
<p class="final-statement">그것이 목표입니다.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="footer-content">
|
||||
<a href="https://github.com/code-yeongyu/oh-my-opencode" class="cta-link" target="_blank" rel="noopener">
|
||||
Oh My OpenCode 받기 →
|
||||
</a>
|
||||
<p><strong>just ulw ulw</strong></p>
|
||||
|
||||
<nav class="language-selector" aria-label="Language selection">
|
||||
<span class="language-selector-label">Language</span>
|
||||
<div class="language-links">
|
||||
<a href="../" class="language-link" lang="en">English</a>
|
||||
<a href="./" class="language-link active" lang="ko">한국어</a>
|
||||
<a href="../ja/" class="language-link" lang="ja">日本語</a>
|
||||
<a href="../zh/" class="language-link" lang="zh">简体中文</a>
|
||||
<a href="../es/" class="language-link" lang="es">Español</a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
674
ultrawork-manifesto-web/src/styles.css
Normal file
@@ -0,0 +1,674 @@
|
||||
/* Design System */
|
||||
:root {
|
||||
/* Colors */
|
||||
--bg-primary: #030303; /* Darker, deeper black */
|
||||
--bg-secondary: #0a0a0a;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #a1a1aa;
|
||||
--text-muted: #52525b;
|
||||
|
||||
/* Accents */
|
||||
--accent-cyan: #64D2FF; /* More refined cyan */
|
||||
--accent-cyan-dim: rgba(100, 210, 255, 0.1);
|
||||
--accent-gold: #F5D061; /* Premium gold */
|
||||
--accent-gold-dim: rgba(245, 208, 97, 0.1);
|
||||
|
||||
/* Gradients */
|
||||
--gold-gradient: linear-gradient(135deg, #F5D061 0%, #E1B32E 100%);
|
||||
--cyan-gradient: linear-gradient(135deg, #64D2FF 0%, #2E93E1 100%);
|
||||
--dark-gradient: linear-gradient(to bottom, #050505, #0a0a0a);
|
||||
|
||||
/* Typography */
|
||||
--font-serif: 'Cormorant Garamond', serif;
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-section: 160px; /* Increased breathing room */
|
||||
--max-width: 1100px; /* Slightly wider for drama */
|
||||
|
||||
/* Glassmorphism */
|
||||
--glass-bg: rgba(255, 255, 255, 0.03);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--glass-blur: 12px;
|
||||
|
||||
/* Glows */
|
||||
--glow-cyan: 0 0 40px rgba(100, 210, 255, 0.15);
|
||||
--glow-gold: 0 0 40px rgba(245, 208, 97, 0.15);
|
||||
--radial-glow: radial-gradient(circle at center, rgba(100, 210, 255, 0.08) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
/* Reset & Base */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.7;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Noise Texture Overlay */
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
opacity: 0.03;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3 {
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(4rem, 8vw, 7rem);
|
||||
background: linear-gradient(to bottom, #fff 0%, #ccc 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 0 80px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 3.5rem;
|
||||
margin-bottom: 2rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--accent-gold);
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 2rem;
|
||||
max-width: 65ch;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-cyan);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:focus-visible {
|
||||
outline: 2px solid var(--accent-cyan);
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
header {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 8rem 2rem 4rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Hero glow effect */
|
||||
.hero-container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
background: var(--radial-glow);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
height: auto;
|
||||
border-radius: 16px;
|
||||
opacity: 0.95;
|
||||
animation: fadeUp 1s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
box-shadow: var(--glow-cyan);
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(2.5rem, 5vw, 4rem);
|
||||
margin-bottom: 0;
|
||||
animation: fadeUp 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) 0.2s both;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 2rem 2rem 0;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: var(--spacing-section);
|
||||
}
|
||||
|
||||
/* Components */
|
||||
.divider {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: var(--spacing-section) 0;
|
||||
position: relative;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.divider::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent 0%, var(--accent-cyan) 20%, var(--accent-gold) 80%, transparent 100%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.divider::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--accent-cyan);
|
||||
border-radius: 50%;
|
||||
box-shadow: var(--glow-cyan);
|
||||
}
|
||||
|
||||
.divider img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Feature: Gold Gradient Text */
|
||||
.gold-gradient-text {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
margin: 3rem 0;
|
||||
border: 1px solid var(--accent-gold-dim);
|
||||
background: radial-gradient(circle at center, rgba(245, 208, 97, 0.03) 0%, transparent 70%);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gold-gradient-text::before, .gold-gradient-text::after {
|
||||
content: "+";
|
||||
position: absolute;
|
||||
color: var(--accent-gold);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.gold-gradient-text::before { top: -10px; left: -5px; }
|
||||
.gold-gradient-text::after { bottom: -10px; right: -5px; }
|
||||
|
||||
.bottleneck-text {
|
||||
background: var(--gold-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
/* Feature: Highlight Box */
|
||||
.highlight-box {
|
||||
border-left: 2px solid var(--accent-cyan);
|
||||
font-style: italic;
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, var(--accent-cyan-dim) 0%, transparent 100%);
|
||||
padding: 2rem;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
/* Feature: Cards */
|
||||
.approach-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.approach-container { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
|
||||
.approach {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
padding: 3rem;
|
||||
border-radius: 20px;
|
||||
transition: all 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
}
|
||||
|
||||
.approach:hover {
|
||||
transform: translateY(-8px);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 25px 50px -15px rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
.ultrawork-approach:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: var(--glow-cyan), 0 25px 50px -15px rgba(0,0,0,0.6);
|
||||
}
|
||||
.prometheus-approach:hover {
|
||||
border-color: var(--accent-gold);
|
||||
box-shadow: var(--glow-gold), 0 25px 50px -15px rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
.approach h3 {
|
||||
margin-top: 0;
|
||||
font-size: 1.5rem;
|
||||
color: #fff;
|
||||
font-family: var(--font-serif);
|
||||
}
|
||||
|
||||
.approach-tagline {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
ul li {
|
||||
position: relative;
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
ul li::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.prometheus-approach ul li::before { color: var(--accent-gold); }
|
||||
|
||||
/* Code & ASCII */
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--accent-cyan);
|
||||
background: rgba(100, 210, 255, 0.1);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
kbd {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85em;
|
||||
color: var(--text-primary);
|
||||
background: linear-gradient(180deg, #3a3a3a 0%, #222 100%);
|
||||
padding: 0.15em 0.5em;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #555;
|
||||
box-shadow: 0 2px 0 #111, inset 0 1px 0 rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.ascii-art {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
color: var(--accent-cyan);
|
||||
background: #000;
|
||||
padding: 2rem;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
box-shadow: inset 0 0 20px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
/* Table */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 3rem 0;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #333;
|
||||
color: var(--accent-gold);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #222;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
tr:hover td {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
margin-top: var(--spacing-section);
|
||||
padding: 8rem 2rem;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: radial-gradient(ellipse at center bottom, rgba(100, 210, 255, 0.05) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
footer::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--accent-cyan-dim), var(--accent-gold-dim), transparent);
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.footer-pattern {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a.cta-link,
|
||||
a.cta-link:hover,
|
||||
a.cta-link:focus,
|
||||
a.cta-link:active {
|
||||
display: inline-block;
|
||||
padding: 1.2rem 3rem;
|
||||
background: var(--text-primary);
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
border-radius: 50px;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 2rem;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
a.cta-link:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 40px rgba(255, 255, 255, 0.4), var(--glow-cyan);
|
||||
}
|
||||
|
||||
.final-statement {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 2.5rem;
|
||||
font-style: italic;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeUp {
|
||||
from { opacity: 0; transform: translateY(30px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ===== RESPONSIVE DESIGN ===== */
|
||||
|
||||
/* Tablet */
|
||||
@media (max-width: 1024px) {
|
||||
:root {
|
||||
--max-width: 90%;
|
||||
--spacing-section: 100px;
|
||||
}
|
||||
|
||||
h1 { font-size: clamp(3rem, 6vw, 5rem); }
|
||||
h2 { font-size: 2.5rem; }
|
||||
}
|
||||
|
||||
/* Mobile Landscape / Small Tablet */
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--spacing-section: 80px;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 4rem 1.5rem 2rem;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1.5rem 1.5rem 0;
|
||||
}
|
||||
|
||||
h2 { font-size: 2rem; }
|
||||
|
||||
.hero-image {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.approach-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.approach {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.divider::before {
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
/* Table to card layout */
|
||||
table, thead, tbody, th, td, tr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tr {
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
color: var(--accent-gold);
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile Portrait */
|
||||
@media (max-width: 480px) {
|
||||
:root {
|
||||
--spacing-section: 60px;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 2rem 1rem 1rem;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1rem 1rem 0;
|
||||
}
|
||||
|
||||
h1 { font-size: 2rem; }
|
||||
h2 { font-size: 1.75rem; }
|
||||
h3 { font-size: 1.1rem; }
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.gold-gradient-text {
|
||||
padding: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.highlight-box {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.approach {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.approach h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.approach-tagline {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.ascii-art {
|
||||
font-size: 0.7rem;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.cta-link {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.final-statement {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 4rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== LANGUAGE SELECTOR ===== */
|
||||
.language-selector {
|
||||
margin-top: 3rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.language-selector-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
margin-bottom: 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.language-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.language-link {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur));
|
||||
}
|
||||
|
||||
.language-link:hover {
|
||||
color: var(--text-primary);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: var(--glow-cyan);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.language-link.active {
|
||||
color: var(--accent-cyan);
|
||||
border-color: var(--accent-cyan);
|
||||
background: rgba(100, 210, 255, 0.08);
|
||||
}
|
||||
|
||||
/* Mobile responsive for language selector */
|
||||
@media (max-width: 480px) {
|
||||
.language-links {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.language-link {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
}
|
||||
341
ultrawork-manifesto-web/src/zh/index.html
Normal file
@@ -0,0 +1,341 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<title>Ultrawork Manifesto | 高产出工程哲学</title>
|
||||
<meta name="description" content="一个为开发者和 AI 工程师设计的哲学框架,通过深度专注、原子化执行和 Ultrawork 方法论实现极致生产力。">
|
||||
<meta name="keywords" content="ultrawork, 开发者生产力, AI 工程, 深度工作, 技术宣言, oh my opencode, sisyphus">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large">
|
||||
<link rel="canonical" href="https://ulw.dev/zh/">
|
||||
<link rel="alternate" hreflang="en" href="https://ulw.dev/">
|
||||
<link rel="alternate" hreflang="ko" href="https://ulw.dev/ko/">
|
||||
<link rel="alternate" hreflang="ja" href="https://ulw.dev/ja/">
|
||||
<link rel="alternate" hreflang="zh" href="https://ulw.dev/zh/">
|
||||
<link rel="alternate" hreflang="es" href="https://ulw.dev/es/">
|
||||
<link rel="alternate" hreflang="x-default" href="https://ulw.dev/">
|
||||
<meta name="theme-color" content="#0a0a0a">
|
||||
<meta name="author" content="Yeongyu Kim">
|
||||
<link rel="icon" type="image/png" href="../images/favicon.png">
|
||||
<link rel="apple-touch-icon" href="../images/favicon.png">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://ulw.dev/zh/">
|
||||
<meta property="og:title" content="Ultrawork Manifesto | 高产出工程哲学">
|
||||
<meta property="og:description" content="一个为开发者和 AI 工程师设计的哲学框架,通过深度专注、原子化执行和 Ultrawork 方法论实现极致生产力。">
|
||||
<meta property="og:image" content="https://ulw.dev/images/og-image.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="Ultrawork Manifesto - 有意义工作的蓝图">
|
||||
<meta property="og:site_name" content="Ultrawork">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@justsisyphus">
|
||||
<meta name="twitter:creator" content="@justsisyphus">
|
||||
<meta name="twitter:title" content="Ultrawork Manifesto | 高产出工程哲学">
|
||||
<meta name="twitter:description" content="一个为开发者和 AI 工程师设计的哲学框架,通过深度专注、原子化执行和 Ultrawork 方法论实现极致生产力。">
|
||||
<meta name="twitter:image" content="https://ulw.dev/images/og-image.png">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="hero-container">
|
||||
<img src="../images/hero.png" alt="Ultrawork Hero" class="hero-image">
|
||||
<h1 class="hero-title">Ultrawork Manifesto</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Section 1 -->
|
||||
<section id="human-intervention">
|
||||
<h2>人工干预是失败的信号</h2>
|
||||
|
||||
<div class="gold-gradient-text bottleneck-text">
|
||||
人在回路即瓶颈<br>
|
||||
人在回路即瓶颈<br>
|
||||
人在回路即瓶颈
|
||||
</div>
|
||||
|
||||
<p>试想一下自动驾驶。当人类必须接管方向盘时,这并非一项功能——而是系统的失败。这意味着汽车无法独自应对当前状况。</p>
|
||||
|
||||
<h3>编程又有何不同?</h3>
|
||||
|
||||
<p>当你发现自己正在:</p>
|
||||
<ul>
|
||||
<li>修复 AI 半成品的代码</li>
|
||||
<li>手动修正明显的错误</li>
|
||||
<li>一步步引导 Agent 完成任务</li>
|
||||
<li>反复澄清相同的需求</li>
|
||||
</ul>
|
||||
|
||||
<p>……这不叫“人机协作”。这是 AI 未能履行其职责。</p>
|
||||
|
||||
<p><strong><a href="https://github.com/code-yeongyu/oh-my-opencode" target="_blank" rel="noopener">Oh My OpenCode</a> 正是基于这一前提构建的</strong>:在 Agent 工作期间,人工干预本质上是一个错误的信号。如果系统设计得当,Agent 应当能够独立完成工作,而无需你像保姆一样照看。</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 2 -->
|
||||
<section id="indistinguishable-code">
|
||||
<h2>无法区分的代码</h2>
|
||||
|
||||
<p class="highlight-box"><strong>目标:Agent 编写的代码应与高级工程师编写的代码无法区分。</strong></p>
|
||||
|
||||
<p>不是“需要清理的 AI 生成代码”。不是“一个好的起点”。而是真正的、最终的、生产就绪的代码。</p>
|
||||
|
||||
<p>这意味着:</p>
|
||||
<ul>
|
||||
<li>严格遵循现有的代码库模式</li>
|
||||
<li>无需被要求即可进行恰当的错误处理</li>
|
||||
<li>编写真正测试核心逻辑的测试用例</li>
|
||||
<li>拒绝 AI 垃圾代码(过度设计、不必要的抽象、范围蔓延)</li>
|
||||
<li>仅在有价值时添加注释</li>
|
||||
</ul>
|
||||
|
||||
<p>如果你能分辨出一次提交是由人类还是 Agent 完成的,那么这个 Agent 就失败了。</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 3 -->
|
||||
<section id="token-cost">
|
||||
<h2>Token 成本 vs. 生产力</h2>
|
||||
|
||||
<p><strong>如果能显著提高生产力,较高的 Token 使用量是可以接受的。</strong></p>
|
||||
|
||||
<p>使用更多的 Token 来:</p>
|
||||
<ul>
|
||||
<li>让多个专业 Agent 并行研究</li>
|
||||
<li>在无人工干预的情况下彻底完成工作</li>
|
||||
<li>在完成前彻底验证工作</li>
|
||||
<li>跨任务积累知识</li>
|
||||
</ul>
|
||||
|
||||
<p>……当这意味着 10 倍、20 倍甚至 100 倍的生产力提升时,这是一笔值得的投资。</p>
|
||||
|
||||
<h3>然而:</h3>
|
||||
|
||||
<p>我们不追求无谓的 Token 浪费。系统致力于优化:</p>
|
||||
<ul>
|
||||
<li>对简单任务使用更便宜的模型(Haiku, Flash)</li>
|
||||
<li>避免冗余的探索</li>
|
||||
<li>跨会话缓存学习成果</li>
|
||||
<li>当收集到足够上下文时停止研究</li>
|
||||
</ul>
|
||||
|
||||
<p>Token 效率很重要。但绝不能以牺牲工作质量或人类认知负荷为代价。</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 4 -->
|
||||
<section id="cognitive-load">
|
||||
<h2>最小化人类认知负荷</h2>
|
||||
|
||||
<p><strong>人类只需要说出他们想要什么。其余的一切都是 Agent 的工作。</strong></p>
|
||||
|
||||
<p>实现这一点的两种方法:</p>
|
||||
|
||||
<div class="approach-container">
|
||||
<div class="approach ultrawork-approach">
|
||||
<h3>方法 1:Ultrawork</h3>
|
||||
<p class="approach-tagline">只需说 "ulw" 然后走开。</p>
|
||||
<p>你说:<code>ulw add authentication</code></p>
|
||||
<p>Agent 自主地:</p>
|
||||
<ul>
|
||||
<li>分析你的代码库模式和架构</li>
|
||||
<li>从官方文档研究最佳实践</li>
|
||||
<li>内部规划实施策略</li>
|
||||
<li>遵循你现有的惯例进行实现</li>
|
||||
<li>使用测试和 LSP 诊断进行验证</li>
|
||||
<li>出错时自我修正</li>
|
||||
<li><strong>坚持攻坚,直到 100% 完成</strong></li>
|
||||
</ul>
|
||||
<p class="approach-summary"><strong>零干预。全自主。只看结果。</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="approach prometheus-approach">
|
||||
<h3>方法 2:Prometheus + Atlas</h3>
|
||||
<p class="approach-tagline">当你想要战略控制权时。</p>
|
||||
<p>按 <kbd>Tab</kbd> 切换 Agent 后:<code>add authentication</code></p>
|
||||
<p><strong>Prometheus</strong>(战略规划者):</p>
|
||||
<ul>
|
||||
<li>通过并行 Agent 进行深度代码库研究</li>
|
||||
<li>用智能的、结合上下文的问题采访你</li>
|
||||
<li>识别边缘情况和架构影响</li>
|
||||
<li>生成带有依赖关系的详细 YAML 工作计划</li>
|
||||
</ul>
|
||||
<p><strong>Atlas</strong>(首席编排者):</p>
|
||||
<ul>
|
||||
<li>通过 <code>/start-work</code> 执行计划</li>
|
||||
<li>将任务委派给专业 Agent(Oracle, 前端工程师等)</li>
|
||||
<li>管理并行执行波次以提高效率</li>
|
||||
<li>追踪进度,处理失败,确保完成</li>
|
||||
</ul>
|
||||
<p class="approach-summary"><strong>你来架构。Agent 执行。完全透明。</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>在这两种情况下,人类的工作是<strong>表达他们想要什么</strong>,而不是管理如何完成。</p>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 5 -->
|
||||
<section id="predictable-continuous">
|
||||
<h2>可预测,连续性,可委派</h2>
|
||||
|
||||
<p><strong>理想的 Agent 应该像编译器一样工作</strong>:输入 Markdown 文档,输出可工作的代码。</p>
|
||||
|
||||
<h3>可预测 (Predictable)</h3>
|
||||
<p>给定相同的输入:</p>
|
||||
<ul>
|
||||
<li>相同的代码库模式</li>
|
||||
<li>相同的需求</li>
|
||||
<li>相同的约束</li>
|
||||
</ul>
|
||||
<p>……输出应该是一致的。不是随机的,不是令人惊讶的,也不是在你未要求的地方“发挥创意”。</p>
|
||||
|
||||
<h3>连续性 (Continuous)</h3>
|
||||
<p>工作应能经受住中断:</p>
|
||||
<ul>
|
||||
<li>会话崩溃?用 <code>/start-work</code> 恢复</li>
|
||||
<li>需要离开?进度会被追踪</li>
|
||||
<li>多日项目?上下文会被保留</li>
|
||||
</ul>
|
||||
<p>Agent 维护状态。你不需要。</p>
|
||||
|
||||
<h3>可委派 (Delegatable)</h3>
|
||||
<p>就像你可以把任务分配给得力的团队成员并信任他们能搞定一样,你应该能够委派给 Agent。</p>
|
||||
<p>这意味着:</p>
|
||||
<ul>
|
||||
<li>清晰的验收标准,独立验证</li>
|
||||
<li>出错时的自我修正行为</li>
|
||||
<li>仅在真正需要时升级(给 Oracle,给用户)</li>
|
||||
<li>完成工作,而不是“差不多做完了”</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 6 -->
|
||||
<section id="core-loop">
|
||||
<h2>核心循环</h2>
|
||||
|
||||
<div class="ascii-art">
|
||||
人类意图 → Agent 执行 → 验证结果
|
||||
↑ ↓
|
||||
└─────── 最小化 ───────┘
|
||||
(仅在真正失败时干预)
|
||||
</div>
|
||||
|
||||
<p><a href="https://github.com/code-yeongyu/oh-my-opencode" target="_blank" rel="noopener">Oh My OpenCode</a> 中的一切都是为了让这个循环运转而设计的:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>功能</th>
|
||||
<th>目的</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Prometheus</td>
|
||||
<td>通过智能访谈提取意图</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Metis</td>
|
||||
<td>在歧义变成 Bug 之前捕捉它们</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Momus</td>
|
||||
<td>在执行前验证计划是否完整</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Orchestrator</td>
|
||||
<td>协调工作,无需人类微观管理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Todo Continuation</td>
|
||||
<td>强制完成,防止“我做完了”的谎言</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Category System</td>
|
||||
<td>无需人工决策即可路由至最佳模型</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Background Agents</td>
|
||||
<td>并行研究而不阻塞用户</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wisdom Accumulation</td>
|
||||
<td>从工作中学习,不重复错误</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="divider">
|
||||
<img src="../images/orb-divider.png" alt="Divider">
|
||||
</div>
|
||||
|
||||
<!-- Section 7 -->
|
||||
<section id="future">
|
||||
<h2>我们正在构建的未来</h2>
|
||||
|
||||
<p>一个这样的世界:</p>
|
||||
<ul>
|
||||
<li>人类开发者专注于<strong>构建什么</strong>,而不是<strong>如何</strong>让 AI 去构建它</li>
|
||||
<li>代码质量与谁(或什么)编写了它无关</li>
|
||||
<li>复杂项目像简单项目一样容易(只是耗时更长)</li>
|
||||
<li>“提示词工程”变得像“编译器调试”一样过时</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Agent 应该是隐形的。</strong> 不是说它被隐藏起来了,而是说它自然而然地工作——就像电,像自来水,像互联网。</p>
|
||||
|
||||
<p>你按下开关。灯亮了。你不会去思考电网。</p>
|
||||
|
||||
<p class="final-statement">这就是目标。</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="footer-content">
|
||||
<a href="https://github.com/code-yeongyu/oh-my-opencode" class="cta-link" target="_blank" rel="noopener">
|
||||
获取 Oh My OpenCode →
|
||||
</a>
|
||||
<p><strong>just ulw ulw</strong></p>
|
||||
|
||||
<nav class="language-selector" aria-label="Language selection">
|
||||
<span class="language-selector-label">语言</span>
|
||||
<div class="language-links">
|
||||
<a href="../" class="language-link" lang="en">English</a>
|
||||
<a href="../ko/" class="language-link" lang="ko">한국어</a>
|
||||
<a href="../ja/" class="language-link" lang="ja">日本語</a>
|
||||
<a href="./" class="language-link active" lang="zh">简体中文</a>
|
||||
<a href="../es/" class="language-link" lang="es">Español</a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
3
ultrawork-manifesto-web/wrangler.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
name = "ultrawork-manifesto"
|
||||
compatibility_date = "2024-01-01"
|
||||
pages_build_output_dir = "./src"
|
||||