create-arete-workspace 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/bin/arete.js +156 -0
- package/bin/create.js +111 -0
- package/lib/install-openclaw.js +50 -0
- package/lib/scaffold.js +213 -0
- package/lib/setup-wizard.js +88 -0
- package/lib/updater.js +130 -0
- package/package.json +34 -0
- package/packages/gatsaeng-os/README.md +36 -0
- package/packages/gatsaeng-os/components.json +23 -0
- package/packages/gatsaeng-os/eslint.config.mjs +18 -0
- package/packages/gatsaeng-os/next.config.ts +7 -0
- package/packages/gatsaeng-os/package.json +59 -0
- package/packages/gatsaeng-os/postcss.config.mjs +7 -0
- package/packages/gatsaeng-os/public/file.svg +1 -0
- package/packages/gatsaeng-os/public/globe.svg +1 -0
- package/packages/gatsaeng-os/public/next.svg +1 -0
- package/packages/gatsaeng-os/public/vercel.svg +1 -0
- package/packages/gatsaeng-os/public/window.svg +1 -0
- package/packages/gatsaeng-os/python/api_server.py +248 -0
- package/packages/gatsaeng-os/python/briefing.py +145 -0
- package/packages/gatsaeng-os/python/config.py +55 -0
- package/packages/gatsaeng-os/python/goal_context_agent.py +193 -0
- package/packages/gatsaeng-os/python/gyeokguk.py +171 -0
- package/packages/gatsaeng-os/python/proactive.py +158 -0
- package/packages/gatsaeng-os/python/requirements.txt +11 -0
- package/packages/gatsaeng-os/python/run.py +28 -0
- package/packages/gatsaeng-os/python/scoring.py +44 -0
- package/packages/gatsaeng-os/python/streak.py +70 -0
- package/packages/gatsaeng-os/python/telegram_bot.py +331 -0
- package/packages/gatsaeng-os/python/timing_engine.py +117 -0
- package/packages/gatsaeng-os/python/vault_io.py +423 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/areas/[id]/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/areas/page.tsx +161 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/books/[id]/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/books/page.tsx +268 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/calendar/page.tsx +379 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/error.tsx +30 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/focus/page.tsx +293 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/goals/[id]/page.tsx +426 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/goals/page.tsx +178 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/layout.tsx +29 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/notes/[id]/page.tsx +147 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/notes/page.tsx +254 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/page.tsx +26 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/projects/[id]/page.tsx +86 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/projects/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/review/page.tsx +475 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/routines/page.tsx +436 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/tasks/[id]/page.tsx +210 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/tasks/page.tsx +307 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/voice/page.tsx +212 -0
- package/packages/gatsaeng-os/src/app/api/areas/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/areas/route.ts +22 -0
- package/packages/gatsaeng-os/src/app/api/auth/login/route.ts +52 -0
- package/packages/gatsaeng-os/src/app/api/auth/logout/route.ts +8 -0
- package/packages/gatsaeng-os/src/app/api/books/[id]/route.ts +27 -0
- package/packages/gatsaeng-os/src/app/api/books/route.ts +20 -0
- package/packages/gatsaeng-os/src/app/api/calendar/[id]/route.ts +24 -0
- package/packages/gatsaeng-os/src/app/api/calendar/import/route.ts +52 -0
- package/packages/gatsaeng-os/src/app/api/calendar/route.ts +37 -0
- package/packages/gatsaeng-os/src/app/api/daily/route.ts +51 -0
- package/packages/gatsaeng-os/src/app/api/goals/[id]/route.ts +34 -0
- package/packages/gatsaeng-os/src/app/api/goals/route.ts +30 -0
- package/packages/gatsaeng-os/src/app/api/logs/energy/route.ts +40 -0
- package/packages/gatsaeng-os/src/app/api/logs/focus/route.ts +22 -0
- package/packages/gatsaeng-os/src/app/api/logs/routine/route.ts +54 -0
- package/packages/gatsaeng-os/src/app/api/milestones/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/milestones/route.ts +47 -0
- package/packages/gatsaeng-os/src/app/api/notes/[id]/route.ts +29 -0
- package/packages/gatsaeng-os/src/app/api/notes/route.ts +37 -0
- package/packages/gatsaeng-os/src/app/api/profile/route.ts +17 -0
- package/packages/gatsaeng-os/src/app/api/projects/[id]/route.ts +27 -0
- package/packages/gatsaeng-os/src/app/api/projects/route.ts +25 -0
- package/packages/gatsaeng-os/src/app/api/reviews/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/reviews/route.ts +29 -0
- package/packages/gatsaeng-os/src/app/api/routines/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/routines/route.ts +28 -0
- package/packages/gatsaeng-os/src/app/api/tasks/[id]/route.ts +28 -0
- package/packages/gatsaeng-os/src/app/api/tasks/route.ts +66 -0
- package/packages/gatsaeng-os/src/app/api/timing/current/route.ts +63 -0
- package/packages/gatsaeng-os/src/app/api/voice/chat/route.ts +50 -0
- package/packages/gatsaeng-os/src/app/api/voice/transcribe/route.ts +25 -0
- package/packages/gatsaeng-os/src/app/api/voice/tts/route.ts +36 -0
- package/packages/gatsaeng-os/src/app/error.tsx +30 -0
- package/packages/gatsaeng-os/src/app/favicon.ico +0 -0
- package/packages/gatsaeng-os/src/app/globals.css +208 -0
- package/packages/gatsaeng-os/src/app/layout.tsx +33 -0
- package/packages/gatsaeng-os/src/app/login/page.tsx +87 -0
- package/packages/gatsaeng-os/src/app/providers.tsx +27 -0
- package/packages/gatsaeng-os/src/components/ErrorBoundary.tsx +46 -0
- package/packages/gatsaeng-os/src/components/dashboard/DashboardGrid.tsx +86 -0
- package/packages/gatsaeng-os/src/components/dashboard/DdayWidget.tsx +88 -0
- package/packages/gatsaeng-os/src/components/dashboard/EnergyTracker.tsx +87 -0
- package/packages/gatsaeng-os/src/components/dashboard/FocusTimer.tsx +139 -0
- package/packages/gatsaeng-os/src/components/dashboard/GatsaengScore.tsx +30 -0
- package/packages/gatsaeng-os/src/components/dashboard/GoalRings.tsx +107 -0
- package/packages/gatsaeng-os/src/components/dashboard/ProactiveBar.tsx +98 -0
- package/packages/gatsaeng-os/src/components/dashboard/RoutineChecklist.tsx +81 -0
- package/packages/gatsaeng-os/src/components/dashboard/TimingWidget.tsx +86 -0
- package/packages/gatsaeng-os/src/components/dashboard/WidgetCustomizer.tsx +95 -0
- package/packages/gatsaeng-os/src/components/dashboard/WidgetWrapper.tsx +33 -0
- package/packages/gatsaeng-os/src/components/dashboard/ZeigarnikPanel.tsx +43 -0
- package/packages/gatsaeng-os/src/components/editor/EditorToolbar.tsx +186 -0
- package/packages/gatsaeng-os/src/components/editor/TiptapEditor.tsx +114 -0
- package/packages/gatsaeng-os/src/components/layout/Header.tsx +47 -0
- package/packages/gatsaeng-os/src/components/layout/MobileBottomNav.tsx +122 -0
- package/packages/gatsaeng-os/src/components/layout/MobileSidebar.tsx +29 -0
- package/packages/gatsaeng-os/src/components/layout/Sidebar.tsx +142 -0
- package/packages/gatsaeng-os/src/components/onboarding/OnboardingFlow.tsx +229 -0
- package/packages/gatsaeng-os/src/components/onboarding/OnboardingGate.tsx +78 -0
- package/packages/gatsaeng-os/src/components/projects/CalendarView.tsx +152 -0
- package/packages/gatsaeng-os/src/components/projects/KanbanView.tsx +180 -0
- package/packages/gatsaeng-os/src/components/projects/ListView.tsx +82 -0
- package/packages/gatsaeng-os/src/components/projects/TableView.tsx +206 -0
- package/packages/gatsaeng-os/src/components/projects/TaskCard.tsx +154 -0
- package/packages/gatsaeng-os/src/components/projects/TaskForm.tsx +128 -0
- package/packages/gatsaeng-os/src/components/projects/ViewSwitcher.tsx +40 -0
- package/packages/gatsaeng-os/src/components/search/GlobalSearch.tsx +179 -0
- package/packages/gatsaeng-os/src/components/shared/InlineEdit.tsx +77 -0
- package/packages/gatsaeng-os/src/components/shared/PinButton.tsx +42 -0
- package/packages/gatsaeng-os/src/components/tasks/DDayBadge.tsx +34 -0
- package/packages/gatsaeng-os/src/components/ui/badge.tsx +48 -0
- package/packages/gatsaeng-os/src/components/ui/button.tsx +64 -0
- package/packages/gatsaeng-os/src/components/ui/card.tsx +92 -0
- package/packages/gatsaeng-os/src/components/ui/checkbox.tsx +32 -0
- package/packages/gatsaeng-os/src/components/ui/command.tsx +184 -0
- package/packages/gatsaeng-os/src/components/ui/dialog.tsx +158 -0
- package/packages/gatsaeng-os/src/components/ui/input.tsx +21 -0
- package/packages/gatsaeng-os/src/components/ui/label.tsx +24 -0
- package/packages/gatsaeng-os/src/components/ui/popover.tsx +89 -0
- package/packages/gatsaeng-os/src/components/ui/progress.tsx +31 -0
- package/packages/gatsaeng-os/src/components/ui/select.tsx +190 -0
- package/packages/gatsaeng-os/src/components/ui/sheet.tsx +143 -0
- package/packages/gatsaeng-os/src/components/ui/tabs.tsx +91 -0
- package/packages/gatsaeng-os/src/components/ui/toggle-group.tsx +83 -0
- package/packages/gatsaeng-os/src/components/ui/toggle.tsx +47 -0
- package/packages/gatsaeng-os/src/components/ui/tooltip.tsx +57 -0
- package/packages/gatsaeng-os/src/hooks/useAreas.ts +53 -0
- package/packages/gatsaeng-os/src/hooks/useBooks.ts +62 -0
- package/packages/gatsaeng-os/src/hooks/useCalendar.ts +59 -0
- package/packages/gatsaeng-os/src/hooks/useDaily.ts +15 -0
- package/packages/gatsaeng-os/src/hooks/useGlobalTasks.ts +45 -0
- package/packages/gatsaeng-os/src/hooks/useGoals.ts +53 -0
- package/packages/gatsaeng-os/src/hooks/useMilestones.ts +75 -0
- package/packages/gatsaeng-os/src/hooks/useNotes.ts +65 -0
- package/packages/gatsaeng-os/src/hooks/useProjects.ts +102 -0
- package/packages/gatsaeng-os/src/hooks/useRoutines.ts +76 -0
- package/packages/gatsaeng-os/src/hooks/useTiming.ts +27 -0
- package/packages/gatsaeng-os/src/lib/apiFetch.ts +14 -0
- package/packages/gatsaeng-os/src/lib/auth.ts +32 -0
- package/packages/gatsaeng-os/src/lib/date.ts +7 -0
- package/packages/gatsaeng-os/src/lib/editor/markdown.ts +35 -0
- package/packages/gatsaeng-os/src/lib/llm-governor.ts +167 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/energyCycle.ts +35 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/habitStack.ts +22 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/scoring.ts +32 -0
- package/packages/gatsaeng-os/src/lib/routes.ts +15 -0
- package/packages/gatsaeng-os/src/lib/utils.ts +6 -0
- package/packages/gatsaeng-os/src/lib/vault/config.ts +29 -0
- package/packages/gatsaeng-os/src/lib/vault/frontmatter.ts +84 -0
- package/packages/gatsaeng-os/src/lib/vault/index.ts +180 -0
- package/packages/gatsaeng-os/src/lib/vault/schemas.ts +274 -0
- package/packages/gatsaeng-os/src/middleware.ts +34 -0
- package/packages/gatsaeng-os/src/stores/dashboardStore.ts +26 -0
- package/packages/gatsaeng-os/src/stores/favoritesStore.ts +47 -0
- package/packages/gatsaeng-os/src/stores/timerStore.ts +65 -0
- package/packages/gatsaeng-os/src/types/index.ts +320 -0
- package/packages/gatsaeng-os/tsconfig.json +34 -0
- package/templates/scripts/forge_qa.sh.tmpl +237 -0
- package/templates/scripts/forge_ship.sh.tmpl +183 -0
- package/templates/scripts/session_indexer.py.tmpl +420 -0
- package/templates/scripts/tracer.py.tmpl +266 -0
- package/templates/workspace/AGENTS.md.tmpl +190 -0
- package/templates/workspace/BOOTSTRAP.md.tmpl +27 -0
- package/templates/workspace/HEARTBEAT.md.tmpl +23 -0
- package/templates/workspace/MEMORY.md.tmpl +35 -0
- package/templates/workspace/SOUL.md.tmpl +258 -0
- package/templates/workspace/TOOLS.md.tmpl +28 -0
- package/templates/workspace/USER.md.tmpl +43 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# forge_ship.sh — Commit → Version bump → Changelog → Push → Draft PR
|
|
5
|
+
# Usage: forge_ship.sh --repo <path> --msg "<pr title>" [--patch|--minor|--major] [--dry-run]
|
|
6
|
+
|
|
7
|
+
REPO=""
|
|
8
|
+
MSG=""
|
|
9
|
+
BUMP="patch"
|
|
10
|
+
DRY_RUN=false
|
|
11
|
+
|
|
12
|
+
while [[ $# -gt 0 ]]; do
|
|
13
|
+
case $1 in
|
|
14
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
15
|
+
--msg) MSG="$2"; shift 2 ;;
|
|
16
|
+
--patch) BUMP="patch"; shift ;;
|
|
17
|
+
--minor) BUMP="minor"; shift ;;
|
|
18
|
+
--major) BUMP="major"; shift ;;
|
|
19
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
20
|
+
*) echo "Unknown option: $1"; exit 1 ;;
|
|
21
|
+
esac
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
if [[ -z "$REPO" ]]; then
|
|
25
|
+
echo "ERROR: --repo <path> is required"
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [[ ! -d "$REPO/.git" ]]; then
|
|
30
|
+
echo "ERROR: $REPO is not a git repository"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
cd "$REPO"
|
|
35
|
+
BRANCH=$(git branch --show-current)
|
|
36
|
+
echo "=== forge_ship: $REPO (branch: $BRANCH) ==="
|
|
37
|
+
|
|
38
|
+
# ── Step 1: Auto-commit uncommitted changes ──
|
|
39
|
+
if [[ -n $(git status --porcelain) ]]; then
|
|
40
|
+
echo "[1/6] Uncommitted changes detected — auto-committing..."
|
|
41
|
+
if $DRY_RUN; then
|
|
42
|
+
echo " [dry-run] would: git add -A && git commit -m 'chore: pre-ship snapshot'"
|
|
43
|
+
else
|
|
44
|
+
git add -A
|
|
45
|
+
git commit -m "chore: pre-ship snapshot"
|
|
46
|
+
fi
|
|
47
|
+
else
|
|
48
|
+
echo "[1/6] Working tree clean — no auto-commit needed."
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# ── Step 2: Extract new commits since origin/main ──
|
|
52
|
+
echo "[2/6] Extracting new commits..."
|
|
53
|
+
COMMITS=$(git log --oneline origin/main..HEAD 2>/dev/null || git log --oneline -10)
|
|
54
|
+
COMMIT_COUNT=$(echo "$COMMITS" | grep -c . || echo 0)
|
|
55
|
+
|
|
56
|
+
if [[ "$COMMIT_COUNT" -eq 0 ]]; then
|
|
57
|
+
echo " No new commits since origin/main. Nothing to ship."
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo " Found $COMMIT_COUNT new commit(s):"
|
|
62
|
+
echo "$COMMITS" | sed 's/^/ /'
|
|
63
|
+
|
|
64
|
+
# ── Step 3: Semver bump (if VERSION file exists) ──
|
|
65
|
+
NEW_VERSION=""
|
|
66
|
+
if [[ -f VERSION ]]; then
|
|
67
|
+
CURRENT=$(cat VERSION | tr -d '[:space:]')
|
|
68
|
+
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
|
69
|
+
case $BUMP in
|
|
70
|
+
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
|
|
71
|
+
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
|
|
72
|
+
patch) PATCH=$((PATCH + 1)) ;;
|
|
73
|
+
esac
|
|
74
|
+
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
|
75
|
+
echo "[3/6] VERSION bump: $CURRENT → $NEW_VERSION ($BUMP)"
|
|
76
|
+
if $DRY_RUN; then
|
|
77
|
+
echo " [dry-run] would write $NEW_VERSION to VERSION"
|
|
78
|
+
else
|
|
79
|
+
echo "$NEW_VERSION" > VERSION
|
|
80
|
+
fi
|
|
81
|
+
else
|
|
82
|
+
echo "[3/6] No VERSION file — skipping semver bump."
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# ── Step 4: Update CHANGELOG.md ──
|
|
86
|
+
TODAY=$(date +%Y-%m-%d)
|
|
87
|
+
CHANGELOG_ENTRY="## [$TODAY]"
|
|
88
|
+
if [[ -n "$NEW_VERSION" ]]; then
|
|
89
|
+
CHANGELOG_ENTRY="## [${NEW_VERSION}] — $TODAY"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
CHANGELOG_BODY=""
|
|
93
|
+
while IFS= read -r line; do
|
|
94
|
+
HASH="${line%% *}"
|
|
95
|
+
DESC="${line#* }"
|
|
96
|
+
CHANGELOG_BODY="${CHANGELOG_BODY}\n- ${DESC} (\`${HASH}\`)"
|
|
97
|
+
done <<< "$COMMITS"
|
|
98
|
+
|
|
99
|
+
if [[ -f CHANGELOG.md ]]; then
|
|
100
|
+
echo "[4/6] Updating CHANGELOG.md..."
|
|
101
|
+
if $DRY_RUN; then
|
|
102
|
+
echo " [dry-run] would prepend section: $CHANGELOG_ENTRY"
|
|
103
|
+
printf " [dry-run] entries:$CHANGELOG_BODY\n"
|
|
104
|
+
else
|
|
105
|
+
TMPFILE=$(mktemp)
|
|
106
|
+
{
|
|
107
|
+
echo "$CHANGELOG_ENTRY"
|
|
108
|
+
printf "%b\n" "$CHANGELOG_BODY"
|
|
109
|
+
echo ""
|
|
110
|
+
cat CHANGELOG.md
|
|
111
|
+
} > "$TMPFILE"
|
|
112
|
+
mv "$TMPFILE" CHANGELOG.md
|
|
113
|
+
fi
|
|
114
|
+
else
|
|
115
|
+
echo "[4/6] Creating CHANGELOG.md..."
|
|
116
|
+
if $DRY_RUN; then
|
|
117
|
+
echo " [dry-run] would create CHANGELOG.md with section: $CHANGELOG_ENTRY"
|
|
118
|
+
printf " [dry-run] entries:$CHANGELOG_BODY\n"
|
|
119
|
+
else
|
|
120
|
+
{
|
|
121
|
+
echo "# Changelog"
|
|
122
|
+
echo ""
|
|
123
|
+
echo "$CHANGELOG_ENTRY"
|
|
124
|
+
printf "%b\n" "$CHANGELOG_BODY"
|
|
125
|
+
} > CHANGELOG.md
|
|
126
|
+
fi
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# ── Step 5: Commit VERSION/CHANGELOG changes ──
|
|
130
|
+
if $DRY_RUN; then
|
|
131
|
+
echo "[5/6] [dry-run] would commit VERSION/CHANGELOG updates"
|
|
132
|
+
else
|
|
133
|
+
if [[ -n $(git status --porcelain) ]]; then
|
|
134
|
+
echo "[5/6] Committing VERSION/CHANGELOG updates..."
|
|
135
|
+
git add VERSION CHANGELOG.md 2>/dev/null || git add CHANGELOG.md
|
|
136
|
+
COMMIT_MSG="chore: bump"
|
|
137
|
+
[[ -n "$NEW_VERSION" ]] && COMMIT_MSG="chore: bump v${NEW_VERSION}"
|
|
138
|
+
git commit -m "$COMMIT_MSG"
|
|
139
|
+
else
|
|
140
|
+
echo "[5/6] No VERSION/CHANGELOG changes to commit."
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# ── Step 6: Push + Draft PR ──
|
|
145
|
+
PR_URL=""
|
|
146
|
+
if $DRY_RUN; then
|
|
147
|
+
echo "[6/6] [dry-run] would: git push origin HEAD"
|
|
148
|
+
if command -v gh &>/dev/null; then
|
|
149
|
+
PR_TITLE="${MSG:-Ship $BRANCH}"
|
|
150
|
+
echo " [dry-run] would: gh pr create --title \"$PR_TITLE\" --fill --draft"
|
|
151
|
+
else
|
|
152
|
+
echo " [dry-run] gh CLI not found. Would push only."
|
|
153
|
+
fi
|
|
154
|
+
else
|
|
155
|
+
echo "[6/6] Pushing..."
|
|
156
|
+
git push origin HEAD
|
|
157
|
+
|
|
158
|
+
if command -v gh &>/dev/null; then
|
|
159
|
+
PR_TITLE="${MSG:-Ship $BRANCH}"
|
|
160
|
+
PR_URL=$(gh pr create --title "$PR_TITLE" --fill --draft 2>&1) || true
|
|
161
|
+
if echo "$PR_URL" | grep -q "https://"; then
|
|
162
|
+
PR_URL=$(echo "$PR_URL" | grep -o 'https://[^ ]*')
|
|
163
|
+
else
|
|
164
|
+
echo " PR creation failed or already exists: $PR_URL"
|
|
165
|
+
PR_URL=""
|
|
166
|
+
fi
|
|
167
|
+
else
|
|
168
|
+
echo " gh CLI not found. Push complete. Create PR manually."
|
|
169
|
+
fi
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# ── Summary ──
|
|
173
|
+
echo ""
|
|
174
|
+
echo "=============================="
|
|
175
|
+
echo " forge_ship complete"
|
|
176
|
+
echo "------------------------------"
|
|
177
|
+
echo " Repo: $REPO"
|
|
178
|
+
echo " Branch: $BRANCH"
|
|
179
|
+
echo " Commits: $COMMIT_COUNT"
|
|
180
|
+
[[ -n "$NEW_VERSION" ]] && echo " Version: $NEW_VERSION"
|
|
181
|
+
[[ -n "$PR_URL" ]] && echo " PR: $PR_URL"
|
|
182
|
+
$DRY_RUN && echo " Mode: DRY-RUN (no changes made)"
|
|
183
|
+
echo "=============================="
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Session Auto-Indexer v2 — ARETE Memory System
|
|
4
|
+
Session end → JSONL → memory/daily log auto-extraction
|
|
5
|
+
|
|
6
|
+
v2 changes:
|
|
7
|
+
- Domain agent support (configurable)
|
|
8
|
+
- Topic session (-topic-*.jsonl) inclusion
|
|
9
|
+
- Domain agents: result summary only (cron noise removed)
|
|
10
|
+
- FORGE git log summary
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import re
|
|
17
|
+
import hashlib
|
|
18
|
+
import subprocess
|
|
19
|
+
from datetime import datetime, timezone, timedelta
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
# ── Config ──────────────────────────────────────────────────────
|
|
23
|
+
AGENTS_DIR = Path("{{OPENCLAW_PATH}}/agents")
|
|
24
|
+
MEMORY_DIR = Path("{{WORKSPACE_PATH}}/memory")
|
|
25
|
+
STATE_FILE = MEMORY_DIR / ".session_indexer_state.json"
|
|
26
|
+
KST = timezone(timedelta(hours=9))
|
|
27
|
+
|
|
28
|
+
# Full conversation indexing (including user messages)
|
|
29
|
+
FULL_INDEX_AGENTS = ["main"]
|
|
30
|
+
|
|
31
|
+
# Domain agents: result summary only (last assistant messages)
|
|
32
|
+
DOMAIN_AGENTS = {{DOMAIN_AGENTS_CONFIG}}
|
|
33
|
+
|
|
34
|
+
# Include topic sessions
|
|
35
|
+
INCLUDE_TOPIC_SESSIONS = True
|
|
36
|
+
|
|
37
|
+
# Minimum user messages per session
|
|
38
|
+
MIN_USER_MESSAGES = 2
|
|
39
|
+
|
|
40
|
+
# FORGE git repository paths (add your project repos here)
|
|
41
|
+
FORGE_REPOS = []
|
|
42
|
+
|
|
43
|
+
# ── Utils ──────────────────────────────────────────────────────
|
|
44
|
+
def load_state():
|
|
45
|
+
if STATE_FILE.exists():
|
|
46
|
+
return json.loads(STATE_FILE.read_text())
|
|
47
|
+
return {"indexed": {}, "forge_last_commit": {}}
|
|
48
|
+
|
|
49
|
+
def save_state(state):
|
|
50
|
+
STATE_FILE.write_text(json.dumps(state, indent=2))
|
|
51
|
+
|
|
52
|
+
def file_hash(path: Path) -> str:
|
|
53
|
+
return hashlib.md5(str(path.stat().st_mtime).encode()).hexdigest()[:8]
|
|
54
|
+
|
|
55
|
+
def is_cron_session(first_user_text: str) -> bool:
|
|
56
|
+
cron_kw = [
|
|
57
|
+
"[cron:", "Read HEARTBEAT.md", "[HOOK-SESSION-IDLE]",
|
|
58
|
+
"Heartbeat prompt", "heartbeat poll",
|
|
59
|
+
]
|
|
60
|
+
return any(kw in first_user_text for kw in cron_kw)
|
|
61
|
+
|
|
62
|
+
def extract_text_from_content(content) -> str:
|
|
63
|
+
if isinstance(content, list):
|
|
64
|
+
return " ".join(
|
|
65
|
+
c.get("text", "")
|
|
66
|
+
for c in content
|
|
67
|
+
if isinstance(c, dict) and c.get("type") == "text"
|
|
68
|
+
).strip()
|
|
69
|
+
elif isinstance(content, str):
|
|
70
|
+
return content.strip()
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
def clean_metadata(text: str) -> str:
|
|
74
|
+
"""Remove conversation info JSON blocks"""
|
|
75
|
+
text = re.sub(r'Conversation info \(untrusted metadata\):\s*```json.*?```', '', text, flags=re.DOTALL)
|
|
76
|
+
text = re.sub(r'Sender \(untrusted metadata\):\s*```json.*?```', '', text, flags=re.DOTALL)
|
|
77
|
+
return text.strip()
|
|
78
|
+
|
|
79
|
+
def guard_text(text: str):
|
|
80
|
+
"""Basic text sanitization. Replace with inbound_guard if available."""
|
|
81
|
+
class Result:
|
|
82
|
+
def __init__(self, t):
|
|
83
|
+
self.redacted_text = t
|
|
84
|
+
self.risk_level = "low"
|
|
85
|
+
self.warnings = []
|
|
86
|
+
return Result(text)
|
|
87
|
+
|
|
88
|
+
# ── Full indexing (main) ──────────────────────────────────────────
|
|
89
|
+
def extract_session_full(jsonl_path: Path) -> dict | None:
|
|
90
|
+
"""Extract user-assistant conversation core from JSONL (main agent)"""
|
|
91
|
+
messages = []
|
|
92
|
+
session_ts = None
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
with open(jsonl_path) as f:
|
|
96
|
+
for line in f:
|
|
97
|
+
obj = json.loads(line.strip())
|
|
98
|
+
t = obj.get("type")
|
|
99
|
+
|
|
100
|
+
if t == "session":
|
|
101
|
+
session_ts = obj.get("timestamp")
|
|
102
|
+
|
|
103
|
+
elif t == "message":
|
|
104
|
+
msg = obj.get("message", {})
|
|
105
|
+
role = msg.get("role", "")
|
|
106
|
+
content = msg.get("content", "")
|
|
107
|
+
text = extract_text_from_content(content)
|
|
108
|
+
|
|
109
|
+
if not text:
|
|
110
|
+
continue
|
|
111
|
+
if role == "user" and (
|
|
112
|
+
"Read HEARTBEAT.md" in text or
|
|
113
|
+
"[cron:" in text or
|
|
114
|
+
"[HOOK-SESSION-IDLE]" in text or
|
|
115
|
+
len(text) < 5
|
|
116
|
+
):
|
|
117
|
+
continue
|
|
118
|
+
if role == "assistant" and text in ("HEARTBEAT_OK", "NO_REPLY"):
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
if role in ("user", "assistant"):
|
|
122
|
+
clean = clean_metadata(text)
|
|
123
|
+
if not clean:
|
|
124
|
+
continue
|
|
125
|
+
guarded = guard_text(clean[:800])
|
|
126
|
+
messages.append({
|
|
127
|
+
"role": role,
|
|
128
|
+
"text": guarded.redacted_text,
|
|
129
|
+
"risk": guarded.risk_level,
|
|
130
|
+
"warnings": guarded.warnings,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
print(f" Warning: parse failed {jsonl_path.name}: {e}", file=sys.stderr)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
user_msgs = [m for m in messages if m["role"] == "user"]
|
|
138
|
+
if len(user_msgs) < MIN_USER_MESSAGES:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
"session_id": jsonl_path.stem,
|
|
143
|
+
"timestamp": session_ts,
|
|
144
|
+
"messages": messages,
|
|
145
|
+
"mode": "full",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def summarize_session_full(session: dict, agent: str = "main") -> str:
|
|
149
|
+
ts = session.get("timestamp", "")
|
|
150
|
+
if ts:
|
|
151
|
+
dt = datetime.fromisoformat(ts.replace("Z", "+00:00")).astimezone(KST)
|
|
152
|
+
time_str = dt.strftime("%H:%M")
|
|
153
|
+
else:
|
|
154
|
+
time_str = "??"
|
|
155
|
+
|
|
156
|
+
label = "Agent" if agent == "main" else agent
|
|
157
|
+
lines = [f"### Session {session['session_id'][:8]} ({time_str} KST)"]
|
|
158
|
+
|
|
159
|
+
for msg in session["messages"]:
|
|
160
|
+
prefix = "**User**" if msg["role"] == "user" else f"**{label}**"
|
|
161
|
+
text = msg["text"]
|
|
162
|
+
if msg["role"] == "assistant" and len(text) > 400:
|
|
163
|
+
text = text[:400] + "..."
|
|
164
|
+
lines.append(f"{prefix}: {text}")
|
|
165
|
+
|
|
166
|
+
lines.append("")
|
|
167
|
+
return "\n".join(lines)
|
|
168
|
+
|
|
169
|
+
# ── Domain agent summary indexing ────────────────────────────────
|
|
170
|
+
def extract_session_domain(jsonl_path: Path, tag: str) -> dict | None:
|
|
171
|
+
"""Domain agents: extract result summaries only"""
|
|
172
|
+
session_ts = None
|
|
173
|
+
first_user_text = ""
|
|
174
|
+
all_assistant_texts = []
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
with open(jsonl_path) as f:
|
|
178
|
+
for line in f:
|
|
179
|
+
obj = json.loads(line.strip())
|
|
180
|
+
t = obj.get("type")
|
|
181
|
+
|
|
182
|
+
if t == "session":
|
|
183
|
+
session_ts = obj.get("timestamp")
|
|
184
|
+
|
|
185
|
+
elif t == "message":
|
|
186
|
+
msg = obj.get("message", {})
|
|
187
|
+
role = msg.get("role", "")
|
|
188
|
+
text = extract_text_from_content(msg.get("content", ""))
|
|
189
|
+
|
|
190
|
+
if not text:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
if role == "user" and not first_user_text:
|
|
194
|
+
first_user_text = text
|
|
195
|
+
|
|
196
|
+
if role == "assistant" and text not in ("HEARTBEAT_OK", "NO_REPLY"):
|
|
197
|
+
if len(text) > 20:
|
|
198
|
+
all_assistant_texts.append(text)
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
print(f" Warning: parse failed {jsonl_path.name}: {e}", file=sys.stderr)
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
if not all_assistant_texts:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
last_assistant_texts = all_assistant_texts[-3:]
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
"session_id": jsonl_path.stem,
|
|
211
|
+
"timestamp": session_ts,
|
|
212
|
+
"is_cron": is_cron_session(first_user_text),
|
|
213
|
+
"first_user": first_user_text[:100],
|
|
214
|
+
"results": last_assistant_texts,
|
|
215
|
+
"tag": tag,
|
|
216
|
+
"mode": "domain",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
def summarize_session_domain(session: dict) -> str:
|
|
220
|
+
ts = session.get("timestamp", "")
|
|
221
|
+
if ts:
|
|
222
|
+
dt = datetime.fromisoformat(ts.replace("Z", "+00:00")).astimezone(KST)
|
|
223
|
+
time_str = dt.strftime("%H:%M")
|
|
224
|
+
else:
|
|
225
|
+
time_str = "??"
|
|
226
|
+
|
|
227
|
+
tag = session.get("tag", "Agent")
|
|
228
|
+
session_type = "cron" if session.get("is_cron") else "direct"
|
|
229
|
+
lines = [f"### {tag} Session {session['session_id'][:8]} ({time_str} KST, {session_type})"]
|
|
230
|
+
|
|
231
|
+
for result in session["results"]:
|
|
232
|
+
if len(result) > 300:
|
|
233
|
+
result = result[:300] + "..."
|
|
234
|
+
lines.append(result)
|
|
235
|
+
|
|
236
|
+
lines.append("")
|
|
237
|
+
return "\n".join(lines)
|
|
238
|
+
|
|
239
|
+
# ── FORGE git log summary ──────────────────────────────────────────
|
|
240
|
+
def get_forge_git_summary(state: dict) -> str | None:
|
|
241
|
+
"""Extract new commits from FORGE work repos"""
|
|
242
|
+
summaries = []
|
|
243
|
+
|
|
244
|
+
for repo in FORGE_REPOS:
|
|
245
|
+
repo = Path(repo)
|
|
246
|
+
if not repo.exists():
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
repo_key = str(repo)
|
|
250
|
+
last_commit = state.get("forge_last_commit", {}).get(repo_key, "")
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
result = subprocess.run(
|
|
254
|
+
["git", "-C", str(repo), "log", "--oneline", "-10",
|
|
255
|
+
"--format=%H|%s|%ai"],
|
|
256
|
+
capture_output=True, text=True, timeout=10
|
|
257
|
+
)
|
|
258
|
+
if result.returncode != 0:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
commits = []
|
|
262
|
+
for line in result.stdout.strip().split("\n"):
|
|
263
|
+
if not line:
|
|
264
|
+
continue
|
|
265
|
+
parts = line.split("|", 2)
|
|
266
|
+
if len(parts) < 3:
|
|
267
|
+
continue
|
|
268
|
+
sha, msg, date_str = parts
|
|
269
|
+
commits.append((sha[:8], msg, date_str))
|
|
270
|
+
|
|
271
|
+
if not commits:
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
new_commits = []
|
|
275
|
+
for sha, msg, date_str in commits:
|
|
276
|
+
if sha == last_commit:
|
|
277
|
+
break
|
|
278
|
+
new_commits.append((sha, msg, date_str))
|
|
279
|
+
|
|
280
|
+
if new_commits:
|
|
281
|
+
repo_name = repo.name
|
|
282
|
+
lines = [f"**FORGE [{repo_name}]** {len(new_commits)} new commits:"]
|
|
283
|
+
for sha, msg, _ in new_commits[:5]:
|
|
284
|
+
lines.append(f" - `{sha}` {msg}")
|
|
285
|
+
summaries.append("\n".join(lines))
|
|
286
|
+
state.setdefault("forge_last_commit", {})[repo_key] = commits[0][0]
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
print(f" Warning: FORGE git log failed {repo}: {e}", file=sys.stderr)
|
|
290
|
+
|
|
291
|
+
if summaries:
|
|
292
|
+
return "\n\n".join(summaries)
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
# ── Daily log ──────────────────────────────────────────────────────
|
|
296
|
+
def get_daily_log_path(date_str: str) -> Path:
|
|
297
|
+
MEMORY_DIR.mkdir(exist_ok=True)
|
|
298
|
+
return MEMORY_DIR / f"{date_str}.md"
|
|
299
|
+
|
|
300
|
+
def append_to_daily_log(date_str: str, content: str):
|
|
301
|
+
path = get_daily_log_path(date_str)
|
|
302
|
+
header = f"\n## Auto-Indexed Sessions ({date_str})\n"
|
|
303
|
+
if path.exists():
|
|
304
|
+
existing = path.read_text()
|
|
305
|
+
if "## Auto-Indexed Sessions" not in existing:
|
|
306
|
+
with open(path, "a") as f:
|
|
307
|
+
f.write(header + content)
|
|
308
|
+
else:
|
|
309
|
+
with open(path, "a") as f:
|
|
310
|
+
f.write(content)
|
|
311
|
+
else:
|
|
312
|
+
path.write_text(header + content)
|
|
313
|
+
|
|
314
|
+
# ── Session file collection ──────────────────────────────────────
|
|
315
|
+
def collect_session_files(agent: str) -> list[Path]:
|
|
316
|
+
sessions_dir = AGENTS_DIR / agent / "sessions"
|
|
317
|
+
if not sessions_dir.exists():
|
|
318
|
+
return []
|
|
319
|
+
|
|
320
|
+
files = []
|
|
321
|
+
for f in sessions_dir.glob("*.jsonl*"):
|
|
322
|
+
if "deleted" in f.name or "reset" in f.name:
|
|
323
|
+
continue
|
|
324
|
+
if not f.name.endswith(".jsonl"):
|
|
325
|
+
continue
|
|
326
|
+
is_topic = "topic" in f.name
|
|
327
|
+
if is_topic and not INCLUDE_TOPIC_SESSIONS:
|
|
328
|
+
continue
|
|
329
|
+
files.append(f)
|
|
330
|
+
|
|
331
|
+
return sorted(files)
|
|
332
|
+
|
|
333
|
+
# ── Main ──────────────────────────────────────────────────────
|
|
334
|
+
def main():
|
|
335
|
+
state = load_state()
|
|
336
|
+
indexed = state["indexed"]
|
|
337
|
+
today = datetime.now(KST).strftime("%Y-%m-%d")
|
|
338
|
+
|
|
339
|
+
new_count = 0
|
|
340
|
+
skipped = 0
|
|
341
|
+
|
|
342
|
+
# 1. Full indexing agents (main)
|
|
343
|
+
for agent in FULL_INDEX_AGENTS:
|
|
344
|
+
for jsonl_file in collect_session_files(agent):
|
|
345
|
+
fhash = file_hash(jsonl_file)
|
|
346
|
+
fkey = f"{agent}/{jsonl_file.name}"
|
|
347
|
+
|
|
348
|
+
if indexed.get(fkey) == fhash:
|
|
349
|
+
skipped += 1
|
|
350
|
+
continue
|
|
351
|
+
|
|
352
|
+
session = extract_session_full(jsonl_file)
|
|
353
|
+
if not session:
|
|
354
|
+
indexed[fkey] = fhash
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
ts = session.get("timestamp", "")
|
|
358
|
+
date_str = today
|
|
359
|
+
if ts:
|
|
360
|
+
try:
|
|
361
|
+
dt = datetime.fromisoformat(ts.replace("Z", "+00:00")).astimezone(KST)
|
|
362
|
+
date_str = dt.strftime("%Y-%m-%d")
|
|
363
|
+
except:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
summary = summarize_session_full(session, agent)
|
|
367
|
+
append_to_daily_log(date_str, summary)
|
|
368
|
+
|
|
369
|
+
indexed[fkey] = fhash
|
|
370
|
+
new_count += 1
|
|
371
|
+
print(f" Indexed: {agent}/{jsonl_file.stem[:12]} -> memory/{date_str}.md")
|
|
372
|
+
|
|
373
|
+
# 2. Domain agent summary indexing
|
|
374
|
+
for agent_name, config in DOMAIN_AGENTS.items():
|
|
375
|
+
if not isinstance(config, dict):
|
|
376
|
+
continue
|
|
377
|
+
tag = config.get("tag", agent_name)
|
|
378
|
+
for jsonl_file in collect_session_files(agent_name):
|
|
379
|
+
fhash = file_hash(jsonl_file)
|
|
380
|
+
fkey = f"{agent_name}/{jsonl_file.name}"
|
|
381
|
+
|
|
382
|
+
if indexed.get(fkey) == fhash:
|
|
383
|
+
skipped += 1
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
session = extract_session_domain(jsonl_file, tag)
|
|
387
|
+
if not session:
|
|
388
|
+
indexed[fkey] = fhash
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
ts = session.get("timestamp", "")
|
|
392
|
+
date_str = today
|
|
393
|
+
if ts:
|
|
394
|
+
try:
|
|
395
|
+
dt = datetime.fromisoformat(ts.replace("Z", "+00:00")).astimezone(KST)
|
|
396
|
+
date_str = dt.strftime("%Y-%m-%d")
|
|
397
|
+
except:
|
|
398
|
+
pass
|
|
399
|
+
|
|
400
|
+
summary = summarize_session_domain(session)
|
|
401
|
+
append_to_daily_log(date_str, summary)
|
|
402
|
+
|
|
403
|
+
indexed[fkey] = fhash
|
|
404
|
+
new_count += 1
|
|
405
|
+
print(f" Indexed: {agent_name}/{jsonl_file.stem[:12]} -> memory/{date_str}.md")
|
|
406
|
+
|
|
407
|
+
# 3. FORGE git log summary
|
|
408
|
+
forge_summary = get_forge_git_summary(state)
|
|
409
|
+
if forge_summary:
|
|
410
|
+
forge_block = f"\n### FORGE Work Summary\n{forge_summary}\n"
|
|
411
|
+
append_to_daily_log(today, forge_block)
|
|
412
|
+
new_count += 1
|
|
413
|
+
print(f" Indexed: FORGE git summary -> memory/{today}.md")
|
|
414
|
+
|
|
415
|
+
save_state({"indexed": indexed, "forge_last_commit": state.get("forge_last_commit", {})})
|
|
416
|
+
|
|
417
|
+
print(f"\nDone: {new_count} new indexed, {skipped} skipped")
|
|
418
|
+
|
|
419
|
+
if __name__ == "__main__":
|
|
420
|
+
main()
|