Compare commits
284 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f21c65be18 | ||
|
|
c300f8c313 | ||
|
|
d6e0953293 | ||
|
|
a8b86e25e6 | ||
|
|
1abb429f12 | ||
|
|
803c04d9e0 | ||
|
|
12732d6dc9 | ||
|
|
b3a2daf40d | ||
|
|
8f49ebb248 | ||
|
|
f56cc617c3 | ||
|
|
ca8326c4c5 | ||
|
|
f5d165baae | ||
|
|
61a40d549b | ||
|
|
5723b81992 | ||
|
|
7f1a14ab80 | ||
|
|
33bdff8a6e | ||
|
|
b5cf19b19a | ||
|
|
9f19a714f7 | ||
|
|
b672c9aaf3 | ||
|
|
384e058812 | ||
|
|
01e0c1d794 | ||
|
|
00a065bf7f | ||
|
|
763732a9b3 | ||
|
|
a41b8de47a | ||
|
|
18b777a712 | ||
|
|
7f173daecb | ||
|
|
e71c0ed24f | ||
|
|
d450153183 | ||
|
|
72687e9b30 | ||
|
|
d52243ccd1 | ||
|
|
8cafad370e | ||
|
|
d8a973d0e1 | ||
|
|
0b623b8e4a | ||
|
|
5edb433755 | ||
|
|
c8f82ed3c2 | ||
|
|
1aa06077a8 | ||
|
|
cb20877620 | ||
|
|
dcbf67c63b | ||
|
|
02b11c727c | ||
|
|
74afc46909 | ||
|
|
ef3fba1690 | ||
|
|
ef2f5c51e4 | ||
|
|
3060cb0242 | ||
|
|
3596053512 | ||
|
|
4bf4a27036 | ||
|
|
de4ad5dcf3 | ||
|
|
2dfc4559b1 | ||
|
|
dd3b03b9e4 | ||
|
|
f4416ee1c3 | ||
|
|
42bb79e2b7 | ||
|
|
561028e67b | ||
|
|
07a9d07cf6 | ||
|
|
19435b2d48 | ||
|
|
e22a3267fe | ||
|
|
9c5872eb27 | ||
|
|
8819a56496 | ||
|
|
6c65158be8 | ||
|
|
096519b978 | ||
|
|
266e6d191b | ||
|
|
cb4c396a53 | ||
|
|
6e3f90d289 | ||
|
|
de01579e84 | ||
|
|
0d8999dc20 | ||
|
|
3202c76674 | ||
|
|
43f8f7f7d8 | ||
|
|
f1cf29b58d | ||
|
|
98b0d58e03 | ||
|
|
b817c87656 | ||
|
|
2a6781f80f | ||
|
|
4098f7f341 | ||
|
|
82390047d2 | ||
|
|
75ad7b1735 | ||
|
|
e523ed85eb | ||
|
|
0460d7bea5 | ||
|
|
66a7b2377f | ||
|
|
eca6813cdb | ||
|
|
22830d3ea8 | ||
|
|
3573548348 | ||
|
|
0867bc8296 | ||
|
|
1603be0c78 | ||
|
|
71a3765c07 | ||
|
|
b840655163 | ||
|
|
ac9bae9546 | ||
|
|
99c6bf4478 | ||
|
|
3e848710b8 | ||
|
|
a2c339cd87 | ||
|
|
c71026d125 | ||
|
|
ce50f9fcce | ||
|
|
c323953f8c | ||
|
|
9f95942dd1 | ||
|
|
299867d8df | ||
|
|
8f7e2898fe | ||
|
|
9f37b1e21e | ||
|
|
c5a4e350e9 | ||
|
|
e547921fdd | ||
|
|
f1316dfd0e | ||
|
|
cc7355eaa4 | ||
|
|
22a1ba7f30 | ||
|
|
a3f407b0e5 | ||
|
|
469e68bbc8 | ||
|
|
176b9855bf | ||
|
|
5d34f95fe0 | ||
|
|
0e130177fc | ||
|
|
5363570fb4 | ||
|
|
f60becaf06 | ||
|
|
519bfbe6b3 | ||
|
|
06e3acd5ac | ||
|
|
f3052dc5fc | ||
|
|
9d133e227b | ||
|
|
7542bc2058 | ||
|
|
ef86a8c29b | ||
|
|
da23b6cd3a | ||
|
|
c10f564265 | ||
|
|
8036de1019 | ||
|
|
7873e60095 | ||
|
|
6f4b5d5544 | ||
|
|
f25c7599bd | ||
|
|
6fdf04d6a0 | ||
|
|
ee0d1257dd | ||
|
|
204b089000 | ||
|
|
da4ab0ca5e | ||
|
|
c035720b37 | ||
|
|
4522ac906b | ||
|
|
2455eacb1f | ||
|
|
d8b86e33a3 | ||
|
|
49b9f1ffde | ||
|
|
4d52845130 | ||
|
|
9a117a5429 | ||
|
|
202e8dea49 | ||
|
|
1e547dea18 | ||
|
|
56ebc2803f | ||
|
|
cf7f0da400 | ||
|
|
ac1e9b06de | ||
|
|
79bfc79d33 | ||
|
|
1b3c6bdbb4 | ||
|
|
bd1e3db1d9 | ||
|
|
edc9f77357 | ||
|
|
883dbc6af7 | ||
|
|
9bdf99d95f | ||
|
|
c8f468f270 | ||
|
|
84fd2c11a0 | ||
|
|
30b49d1071 | ||
|
|
ad7d74820a | ||
|
|
75aa42b877 | ||
|
|
925b72ae83 | ||
|
|
cd683ba227 | ||
|
|
d0ab382973 | ||
|
|
3e3041c1c7 | ||
|
|
92cee125cc | ||
|
|
bba3c55e1c | ||
|
|
26f5936d14 | ||
|
|
b72a7888e4 | ||
|
|
beae2d639d | ||
|
|
ac137f7c1c | ||
|
|
97e38fb480 | ||
|
|
b63c78c234 | ||
|
|
37ce673a57 | ||
|
|
b9741ef38b | ||
|
|
0a0d7e8551 | ||
|
|
2dfa9956c5 | ||
|
|
773811d060 | ||
|
|
3756b81817 | ||
|
|
72a86fc173 | ||
|
|
cc46019622 | ||
|
|
71ac48162a | ||
|
|
bcf5e2f51f | ||
|
|
fb055ce740 | ||
|
|
9e7f37b5cc | ||
|
|
39fa83a0a0 | ||
|
|
15ed624d4a | ||
|
|
52e3980cd1 | ||
|
|
53d897aff4 | ||
|
|
7d743f17c6 | ||
|
|
26758b6e8a | ||
|
|
914095dc99 | ||
|
|
4d82079cac | ||
|
|
3a40e39fc8 | ||
|
|
2e73d3333d | ||
|
|
c764b2bf6e | ||
|
|
f7d1b37343 | ||
|
|
fab17720cc | ||
|
|
9470c5b10b | ||
|
|
c45f892591 | ||
|
|
a8670ee23a | ||
|
|
7676ecf0d4 | ||
|
|
fa83d7f441 | ||
|
|
e48475d6cd | ||
|
|
46f42a4d93 | ||
|
|
46ac3fc930 | ||
|
|
5e0859fbb8 | ||
|
|
2d00160283 | ||
|
|
20b3a29d08 | ||
|
|
fd7f8ac78f | ||
|
|
0bb809445e | ||
|
|
3c66d65160 | ||
|
|
ffe0fb9820 | ||
|
|
00ef11ac33 | ||
|
|
312b411654 | ||
|
|
364a037cb3 | ||
|
|
2fbf054a57 | ||
|
|
350a89f364 | ||
|
|
086c6f6c45 | ||
|
|
070f5de1b1 | ||
|
|
f529a5ff22 | ||
|
|
6a85d82fcf | ||
|
|
35ad1715d3 | ||
|
|
3c40bb5ea3 | ||
|
|
d95d55e6b8 | ||
|
|
d22b50e171 | ||
|
|
a83a0c41e8 | ||
|
|
9efde2bf88 | ||
|
|
8dc8b8ba8e | ||
|
|
baeea9c2a7 | ||
|
|
a935bf9664 | ||
|
|
2d55f88a41 | ||
|
|
a8d8a8bd65 | ||
|
|
0bc3d2a6c4 | ||
|
|
b886d58c07 | ||
|
|
a8943a9f7a | ||
|
|
eccd06e182 | ||
|
|
731c291d61 | ||
|
|
c8b5ed3912 | ||
|
|
9bf44da13b | ||
|
|
b748c1569e | ||
|
|
74fc39f1a6 | ||
|
|
ccd2ee2cc7 | ||
|
|
5b89e3d03f | ||
|
|
e106b00b16 | ||
|
|
d7558ef451 | ||
|
|
4aa4353d11 | ||
|
|
50d84f12c9 | ||
|
|
e2271b5a50 | ||
|
|
bec87b3d6f | ||
|
|
4cb7ad8dfa | ||
|
|
992fbf0763 | ||
|
|
1d7b86dbef | ||
|
|
036586e736 | ||
|
|
d9e5d2600b | ||
|
|
10d86b4bd6 | ||
|
|
f72cfae7d9 | ||
|
|
e5a2ed250d | ||
|
|
536d819328 | ||
|
|
869cf72abe | ||
|
|
f1715a34fa | ||
|
|
26998f05ff | ||
|
|
7db8f213d8 | ||
|
|
37738e3c8f | ||
|
|
81fd186fb2 | ||
|
|
3127930454 | ||
|
|
bed4255a5e | ||
|
|
dff06d9e35 | ||
|
|
0efad7a004 | ||
|
|
eaf27e8b3a | ||
|
|
13c3393e3a | ||
|
|
4704a28e5d | ||
|
|
1cb5171fba | ||
|
|
4684a0af76 | ||
|
|
6c27ffbdca | ||
|
|
21f1cf2a85 | ||
|
|
c88ed9d5eb | ||
|
|
9c1f20f2d9 | ||
|
|
e8d018dd54 | ||
|
|
1ca32a6bdd | ||
|
|
b86677d554 | ||
|
|
3e072fae66 | ||
|
|
4a36fbe5e0 | ||
|
|
9e5a3fe962 | ||
|
|
3f98fd0ae5 | ||
|
|
8420b87bd1 | ||
|
|
c0978df19a | ||
|
|
3ac9e030e2 | ||
|
|
4c6e6594dc | ||
|
|
5170914a7a | ||
|
|
b1f4349dab | ||
|
|
23326a1833 | ||
|
|
ca0594328a | ||
|
|
22d06acb35 | ||
|
|
ab44ba5e41 | ||
|
|
6c3afce329 | ||
|
|
f8e387bbc1 | ||
|
|
2a36998ac7 | ||
|
|
4148dcdb18 | ||
|
|
5783790733 | ||
|
|
edfb702ecc |
101
.gitea/workflows/audit.yml
Normal file
101
.gitea/workflows/audit.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
name: Dependency Audit
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Mondays 06:00 UTC
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOTNET_ROOT: /home/mika/.dotnet
|
||||
GITEA_API: https://git.kuns.dev/api/v1
|
||||
REPO: releases/ClaudeDo
|
||||
ISSUE_TITLE: 'Dependency audit: vulnerable packages detected'
|
||||
steps:
|
||||
- name: Checkout main
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone --depth 1 --branch main \
|
||||
"https://oauth2:${TOKEN}@git.kuns.dev/${REPO}.git" src
|
||||
|
||||
- name: Scan for vulnerable / outdated packages
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export PATH="$DOTNET_ROOT:$PATH"
|
||||
cd src
|
||||
|
||||
: > audit.log
|
||||
: > vuln.md
|
||||
found=0
|
||||
|
||||
# .slnx tooling needs .NET 9; iterate per-project to stay on .NET 8.
|
||||
while IFS= read -r proj; do
|
||||
echo "==== $proj ====" | tee -a audit.log
|
||||
dotnet restore "$proj" >/dev/null
|
||||
|
||||
vuln="$(dotnet list "$proj" package --vulnerable --include-transitive 2>&1)"
|
||||
echo "$vuln" | tee -a audit.log
|
||||
if echo "$vuln" | grep -qi "has the following vulnerable"; then
|
||||
found=1
|
||||
{
|
||||
printf '#### `%s`\n\n```\n' "$proj"
|
||||
echo "$vuln"
|
||||
printf '```\n\n'
|
||||
} >> vuln.md
|
||||
fi
|
||||
|
||||
# Outdated is informational only — never fails the run.
|
||||
dotnet list "$proj" package --outdated 2>&1 | tee -a audit.log || true
|
||||
echo "" | tee -a audit.log
|
||||
done < <(find . -name '*.csproj' | sort)
|
||||
|
||||
if [ "$found" -ne 0 ]; then
|
||||
echo "::error::Vulnerable packages detected — see log above." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "No vulnerable packages found."
|
||||
|
||||
- name: Report vulnerabilities to a Gitea issue
|
||||
if: failure()
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src
|
||||
|
||||
if [ -s vuln.md ]; then
|
||||
DETAILS="$(cat vuln.md)"
|
||||
else
|
||||
DETAILS="The audit job failed before producing findings — check the run log."
|
||||
fi
|
||||
BODY="$(printf 'Automated weekly dependency audit found vulnerable packages.\n\n%s\n\n[View workflow run](%s)' \
|
||||
"$DETAILS" "$RUN_URL")"
|
||||
|
||||
# Reuse an existing open issue if one is already tracking this.
|
||||
EXISTING="$(curl -sS \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_API}/repos/${REPO}/issues?state=open&type=issues&limit=50" \
|
||||
| jq -r --arg t "$ISSUE_TITLE" '.[] | select(.title==$t) | .number' | head -n1)"
|
||||
|
||||
if [ -n "$EXISTING" ]; then
|
||||
echo "Commenting on existing issue #$EXISTING"
|
||||
jq -n --arg body "$BODY" '{body:$body}' \
|
||||
| curl -sS --fail-with-body -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- \
|
||||
"${GITEA_API}/repos/${REPO}/issues/${EXISTING}/comments" >/dev/null
|
||||
else
|
||||
echo "Creating new issue"
|
||||
jq -n --arg title "$ISSUE_TITLE" --arg body "$BODY" '{title:$title, body:$body}' \
|
||||
| curl -sS --fail-with-body -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- \
|
||||
"${GITEA_API}/repos/${REPO}/issues" >/dev/null
|
||||
fi
|
||||
85
.gitea/workflows/changelog.yml
Normal file
85
.gitea/workflows/changelog.yml
Normal file
@@ -0,0 +1,85 @@
|
||||
name: Changelog
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
REPO: releases/ClaudeDo
|
||||
steps:
|
||||
- name: Checkout main (full history)
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone "https://oauth2:${TOKEN}@git.kuns.dev/${REPO}.git" src
|
||||
cd src
|
||||
git fetch --tags --force
|
||||
git checkout main
|
||||
|
||||
- name: Regenerate CHANGELOG.md
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src
|
||||
|
||||
emit_group() {
|
||||
# $1 range, $2 conventional-type, $3 heading
|
||||
local range="$1" type="$2" title="$3" lines
|
||||
lines="$(git log "$range" --no-merges --pretty=format:'%s|%h' \
|
||||
| grep -E "^${type}(\([^)]*\))?(!)?: " || true)"
|
||||
[ -z "$lines" ] && return 0
|
||||
printf '### %s\n\n' "$title"
|
||||
while IFS='|' read -r subject hash; do
|
||||
printf -- '- %s (%s)\n' "${subject#*: }" "$hash"
|
||||
done <<< "$lines"
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
emit_section() {
|
||||
# $1 range, $2 tag, $3 date
|
||||
printf '## %s — %s\n\n' "$2" "$3"
|
||||
emit_group "$1" feat "Features"
|
||||
emit_group "$1" fix "Fixes"
|
||||
emit_group "$1" perf "Performance"
|
||||
emit_group "$1" refactor "Refactoring"
|
||||
emit_group "$1" docs "Documentation"
|
||||
}
|
||||
|
||||
# Tags ascending by semver so we can pair each with its predecessor.
|
||||
mapfile -t TAGS < <(git tag --sort=v:refname | grep -E '^v' || true)
|
||||
|
||||
{
|
||||
printf '# Changelog\n\n'
|
||||
for ((i=${#TAGS[@]}-1; i>=0; i--)); do
|
||||
TAG="${TAGS[$i]}"
|
||||
DATE="$(git log -1 --format=%ad --date=short "$TAG")"
|
||||
if (( i > 0 )); then
|
||||
RANGE="${TAGS[$((i-1))]}..${TAG}"
|
||||
else
|
||||
RANGE="$TAG"
|
||||
fi
|
||||
emit_section "$RANGE" "$TAG" "$DATE"
|
||||
done
|
||||
} > CHANGELOG.md
|
||||
|
||||
cat CHANGELOG.md
|
||||
|
||||
- name: Commit and push if changed
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src
|
||||
if git diff --quiet -- CHANGELOG.md; then
|
||||
echo "CHANGELOG.md unchanged; nothing to commit."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "ClaudeDo CI"
|
||||
git config user.email "ci@kuns.dev"
|
||||
git add CHANGELOG.md
|
||||
git commit -m "docs(changelog): update for ${GITHUB_REF_NAME}"
|
||||
git push origin main
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -38,11 +42,52 @@ jobs:
|
||||
TAG: ${{ steps.ver.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone --depth 1 --branch "$TAG" \
|
||||
# Full clone (with tags) so release notes can diff against the previous tag.
|
||||
git clone --branch "$TAG" \
|
||||
"https://oauth2:${TOKEN}@git.kuns.dev/${REPO}.git" \
|
||||
"$WORK/src"
|
||||
git -C "$WORK/src" log -1 --oneline
|
||||
|
||||
- name: Generate release notes
|
||||
env:
|
||||
WORK: ${{ steps.ws.outputs.dir }}
|
||||
TAG: ${{ steps.ver.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd "$WORK/src"
|
||||
|
||||
PREV="$(git tag --sort=v:refname | grep -E '^v' \
|
||||
| awk -v t="$TAG" '$0==t{print prev} {prev=$0}')"
|
||||
if [ -n "$PREV" ]; then
|
||||
RANGE="${PREV}..${TAG}"
|
||||
else
|
||||
RANGE="$TAG"
|
||||
fi
|
||||
|
||||
emit_group() {
|
||||
# $1 conventional-type, $2 heading
|
||||
local lines
|
||||
lines="$(git log "$RANGE" --no-merges --pretty=format:'%s|%h' \
|
||||
| grep -E "^${1}(\([^)]*\))?(!)?: " || true)"
|
||||
[ -z "$lines" ] && return 0
|
||||
printf '### %s\n\n' "$2"
|
||||
while IFS='|' read -r subject hash; do
|
||||
printf -- '- %s (%s)\n' "${subject#*: }" "$hash"
|
||||
done <<< "$lines"
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
{
|
||||
emit_group feat "Features"
|
||||
emit_group fix "Fixes"
|
||||
emit_group perf "Performance"
|
||||
emit_group refactor "Refactoring"
|
||||
emit_group docs "Documentation"
|
||||
} > RELEASE_NOTES.md
|
||||
|
||||
echo "--- release notes ---"
|
||||
cat RELEASE_NOTES.md
|
||||
|
||||
- name: Publish ClaudeDo.App (win-x64, self-contained)
|
||||
env:
|
||||
WORK: ${{ steps.ws.outputs.dir }}
|
||||
@@ -128,7 +173,8 @@ jobs:
|
||||
BODY=$(jq -n \
|
||||
--arg tag "$TAG" \
|
||||
--arg name "$TAG" \
|
||||
'{tag_name:$tag, name:$name, body:"", draft:false, prerelease:false, target_commitish:"main"}')
|
||||
--rawfile body "$WORK/src/RELEASE_NOTES.md" \
|
||||
'{tag_name:$tag, name:$name, body:$body, draft:true, prerelease:false, target_commitish:"main"}')
|
||||
RESP=$(curl -sS -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -166,6 +212,32 @@ jobs:
|
||||
done
|
||||
echo "All assets uploaded."
|
||||
|
||||
- name: Publish release
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.release.outputs.release_id }}
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -sS --fail-with-body -X PATCH \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"draft":false}' \
|
||||
"${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}" \
|
||||
> /dev/null
|
||||
echo "Release ${RELEASE_ID} published."
|
||||
|
||||
- name: Delete draft release on failure
|
||||
if: failure() && steps.release.outputs.release_id != ''
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.release.outputs.release_id }}
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
curl -sS -X DELETE \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}" \
|
||||
> /dev/null || true
|
||||
echo "Cleaned up draft release ${RELEASE_ID}."
|
||||
|
||||
- name: Cleanup workspace
|
||||
if: always()
|
||||
env:
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
# Local dev worktrees (created by using-git-worktrees skill)
|
||||
.worktrees/
|
||||
|
||||
# Brainstorming visual companion artifacts
|
||||
.superpowers/
|
||||
|
||||
# .NET build output
|
||||
bin/
|
||||
obj/
|
||||
@@ -45,6 +48,8 @@ artifacts/
|
||||
|
||||
# Avalonia / XAML designer
|
||||
*.designer.cs
|
||||
# ...but EF Core migration Designer files are real source and must be tracked
|
||||
!**/Migrations/*.Designer.cs
|
||||
|
||||
# Project-specific
|
||||
*.db
|
||||
|
||||
36
CHANGELOG.md
Normal file
36
CHANGELOG.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to ClaudeDo are documented here.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
Versions are derived from git tags (`vX.Y.Z`) via MinVer.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Review queue is no longer permanently empty: the review virtual list now
|
||||
matches tasks in `WaitingForReview` (it previously matched `Done` + active
|
||||
worktree, a state successful runs never land in).
|
||||
- UI ViewModels (`Details`, `Tasks`, `Lists` islands and the shell) now dispose
|
||||
their `Loc.LanguageChanged`, worker-event, and timer subscriptions, fixing
|
||||
long-lived subscription leaks.
|
||||
- Stopping a task while the worker is offline no longer throws: `StopAsync`
|
||||
guards on task/running/connection state and handles hub errors.
|
||||
|
||||
### Hardening
|
||||
|
||||
- Release-readiness audit pass across Worker, Data, UI, MCP, App and Installer
|
||||
(see `docs/open.md` backlog for tracked follow-ups).
|
||||
|
||||
## [1.7.0]
|
||||
|
||||
- Agent roadblock + run-outcome surfacing in the task detail pane.
|
||||
- i18n: localized task-header, task-row and prime-schedule tooltips.
|
||||
- CI: dependency-audit and changelog Gitea workflows.
|
||||
- Layer A/B/C git merge & conflict-resolution cockpit (multi-worktree
|
||||
batch-merge, inline conflict resolver).
|
||||
|
||||
[Unreleased]: https://git.kuns.dev/releases/ClaudeDo/compare/v1.7.0...HEAD
|
||||
[1.7.0]: https://git.kuns.dev/releases/ClaudeDo/releases/tag/v1.7.0
|
||||
29
CLAUDE.md
29
CLAUDE.md
@@ -35,7 +35,7 @@ Two-process system communicating over SignalR (`127.0.0.1:47821`):
|
||||
- EF Core migrations manage schema (Migrations/ folder in ClaudeDo.Data)
|
||||
- `IDbContextFactory<ClaudeDoDbContext>` used by singleton consumers (e.g. Worker)
|
||||
- Entity configuration via `IEntityTypeConfiguration<T>` in Configuration/ folder
|
||||
- Task status flow: Idle | Queued -> Running -> Done | Failed | Cancelled
|
||||
- Task status flow: Idle | Queued -> Running -> WaitingForReview -> Done | Failed | Cancelled. A task that spawns/has children passes through WaitingForChildren first, then surfaces for review once every child is terminal — this is the single parent model for both planning and improvement parents (planning/improvement *children* themselves go straight to Done, only the parent is reviewed). From review you can approve, reject-rerun (Queued, resumes the session with feedback), reject-park (Idle), or cancel. Approve is the single review+merge action: a childless task merges its own worktree then Done (conflicts keep it in WaitingForReview); a task with children drives the unit merge (parent worktree if any + each Done child in order, with conflict continue/abort). Tasks with no active worktree (sandbox run) approve straight to Done.
|
||||
- Worktree state flow: Active -> Merged | Discarded | Kept
|
||||
- The queue picker claims tasks by `Status=Queued` (with `BlockedByTaskId IS NULL`); the legacy tag system was removed
|
||||
- Interfaces live in an `Interfaces/` subfolder beside their consumers (namespace unchanged)
|
||||
@@ -44,16 +44,35 @@ Two-process system communicating over SignalR (`127.0.0.1:47821`):
|
||||
- Views use compiled bindings (`x:DataType`)
|
||||
- ViewModels use `[ObservableProperty]` and `[RelayCommand]` source generators
|
||||
|
||||
## Working style (autonomous)
|
||||
|
||||
For any non-trivial feature, bug, or change, run this loop without hand-holding:
|
||||
|
||||
1. **Brainstorm first** (superpowers:brainstorming) — ask clarifying questions one at a time, propose 2–3 options with a recommendation, present a short design, get approval before building.
|
||||
2. **Write it down** — a spec in `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and a step-by-step plan in `docs/superpowers/plans/` (superpowers:writing-plans). Commit the docs.
|
||||
3. **Implement on main** with superpowers:subagent-driven-development — one subagent per task, TDD, build + test, commit per task with Conventional Commits. Once the plan is approved, do NOT pause for re-approval between tasks; only stop for genuine decisions or blockers.
|
||||
4. **Trust but verify** — read each subagent's diff and run the build/tests yourself before marking a task done.
|
||||
5. **Bugs** → superpowers:systematic-debugging (find the root cause before any fix).
|
||||
6. **Never claim UI works without running it** — explicitly flag visual-verification gaps for the user to check.
|
||||
|
||||
Commit freely (per task + the spec/plan docs). Never push without asking.
|
||||
|
||||
## Building & Testing
|
||||
|
||||
`dotnet build ClaudeDo.slnx` requires .NET 9; on .NET 8 build individual projects instead.
|
||||
`dotnet build ClaudeDo.slnx` requires .NET 9; on .NET 8 build individual projects with `-c Release` (a running Worker locks the `Debug` output).
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj # pulls in Ui + Data
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
||||
dotnet test tests/ClaudeDo.Worker.Tests # also: Data.Tests, Ui.Tests, Installer.Tests, Releases.Tests
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release # pulls in Ui + Data
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release # also: Data.Tests, Ui.Tests, Localization.Tests, Installer.Tests, Releases.Tests
|
||||
```
|
||||
|
||||
### Gotchas
|
||||
- **Subagents:** use the `sonnet` model; stage files explicitly by path — never `git add -A` (parallel sessions often leave unrelated WIP in the tree).
|
||||
- **Icons:** `PathIcon` *fills* its geometry. Line-art/stroke icons must be authored as filled geometry, or rendered with a stroked `Path` — otherwise they render invisible.
|
||||
- **Localization:** `locales/en.json` and `locales/de.json` keys must stay in parity (Localization.Tests enforces it).
|
||||
- **Test fakes:** changing `IWorkerClient` / `WorkerHub` / ViewModel constructors breaks hand-rolled fakes in both test projects — update them.
|
||||
|
||||
## Docs
|
||||
|
||||
- `docs/plan.md` — full architecture and design spec
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<Project Path="src/ClaudeDo.Worker/ClaudeDo.Worker.csproj" />
|
||||
<Project Path="src/ClaudeDo.Installer/ClaudeDo.Installer.csproj" />
|
||||
<Project Path="src/ClaudeDo.Releases/ClaudeDo.Releases.csproj" />
|
||||
<Project Path="src/ClaudeDo.Localization/ClaudeDo.Localization.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj" />
|
||||
@@ -13,5 +14,6 @@
|
||||
<Project Path="tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj" />
|
||||
<Project Path="tests/ClaudeDo.Installer.Tests/ClaudeDo.Installer.Tests.csproj" />
|
||||
<Project Path="tests/ClaudeDo.Releases.Tests/ClaudeDo.Releases.Tests.csproj" />
|
||||
<Project Path="tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Task Mailbox — Push Messages Into Running Sessions
|
||||
|
||||
**Status:** proposal
|
||||
**Status:** PARKED (2026-06-04) — not building this.
|
||||
**Why parked:** The generic Claude-Mailbox plugin (the `mcp__mailbox__*` tools used in normal sessions) already covers the core need — cross-session messaging, inbox checks, a sender — at the harness level for any project. Integrating it directly into ClaudeDo (task/worktree-scoped inboxes, per-worktree CLAUDE.md + hook seeding, UI badges, `send_to_peer`) is a sizable build (migration + MCP tools + SignalR + UI + hooks) for marginal gain over the plugin. Revisit only if the generic plugin proves insufficient for the parallel-session workflow. The original proposal is kept below for reference.
|
||||
|
||||
---
|
||||
|
||||
**Context:** the user runs parallel Claude sessions (e.g. backend + frontend) and wants to push messages into a session while it's busy inside a subagent. A shared folder works for one-offs; this turns it into a first-class ClaudeDo feature so every future parallel-session project gets it for free.
|
||||
|
||||
## Problem
|
||||
|
||||
294
docs/open.md
294
docs/open.md
@@ -1,286 +1,30 @@
|
||||
# ClaudeDo — Offene Punkte
|
||||
|
||||
Stand: 2026-04-30. Neu erstellt nach Code-Audit gegen `plan.md`, `improvement-plan.md` und `mailbox-proposal.md`.
|
||||
|
||||
Die alte Version dieses Dokuments war auf 2026-04-13 ("nach Slice F") datiert und ignorierte die seither gelandeten Slices (Planning Sessions, Prime Claude, Self-Update, Externe MCP-Tools, editierbare Status/Tags, BlockedBy-Chains). Diese Version trennt sauber zwischen **erledigt**, **teilweise**, **offen** — und listet das, was inzwischen gebaut wurde, explizit als „shipped" auf, damit es nicht verloren geht.
|
||||
|
||||
Legende: ✅ DONE — 🟡 PARTIAL — ⬜ OPEN — ⛔ DROPPED
|
||||
Stand: 2026-06-04. **Nur noch offene Punkte.** Was erledigt ist, steht in den Commits und im Code — nicht hier.
|
||||
|
||||
---
|
||||
|
||||
## 0. Was seit dem 2026-04-13 dazugekommen ist
|
||||
## Manuelle Verifikation (offen)
|
||||
|
||||
Diese Slices gab es im alten Dokument noch nicht (oder nur als Platzhalter). Sie sind **fertig im Code**, brauchen aber jeweils noch ein paar Polish-Punkte (siehe Sektion 2/3).
|
||||
Kein Code-Aufwand, nur Durchspielen mit explizit notiertem Pass-Kriterium. Der Großteil der Pipeline ist laut User bereits in der Praxis getestet; hier das, was noch ein falsifizierbares Observable braucht.
|
||||
|
||||
| Slice | Worker-Anker | UI-Anker | Status |
|
||||
|---|---|---|---|
|
||||
| **Planning Sessions** (Plan B+C) | `Planning/PlanningSessionManager`, `PlanningChainCoordinator`, `PlanningMcpService` | `Views/Planning/PlanningDiffView`, `ConflictResolutionView`, `UnfinishedPlanningModalView` | ✅ Code, manuelle Verifikation siehe §1.1 |
|
||||
| **Prime Claude** (geplante Recurrence) | `Prime/PrimeScheduler`, `NextDueCalculator`, `PrimeRunner` | `ViewModels/Modals/PrimeClaudeTabViewModel`, `Views/Controls/ThemedDatePicker` | ✅ Code, manuelle Verifikation siehe §1.2 |
|
||||
| **Self-Update System** (Gitea Releases) | — | `ClaudeDo.Releases` (`ReleaseClient`, `SelfUpdater`, `ChecksumVerifier`, `VersionComparer`), `ClaudeDo.Installer` (Pages/Steps/Core) | ✅ Code, manuelle Verifikation siehe §1.3 |
|
||||
| **Externes MCP-Endpoint** (11 Tools für Drittsessions) | `External/ExternalMcpService` (`ListTaskLists`, `ListTasks`, `GetTask`, `AddTask`, `UpdateTask`, `UpdateTaskStatus`, `SetTaskTags`, `ListTags`, `DeleteTask`, `RunTaskNow`, `CancelTask`), `ExternalMcpAuthMiddleware` (X-ClaudeDo-Key) | — | ✅ Code, ohne Tests am Endpoint selbst |
|
||||
| **Editierbare Status & Tags** (entkoppelt vom `agent`-Tag) | `WorkerHub.SetTaskStatus`, `SetTaskTags`, `UpdateTaskAgentSettings`; Queue-Picker filtert nicht mehr nach `agent`-Tag | `DetailsIslandViewModel`, Status-/Tag-Kontextmenü in `TasksIslandView` | ✅ Code |
|
||||
| **BlockedBy-Chains** (sequenzielle Subtask-Ausführung) | `TaskStateService.BlockOn`/`UnblockAsync`, `QueuePicker` filter `BlockedByTaskId IS NULL`, `PlanningChainCoordinator.OnChildFinishedAsync` | Drittes Feld neben `Status` und `PlanningPhase` | ✅ Code, Migration `20260423154708_AddPlanningSupport` |
|
||||
| **Worker-State-Konsolidierung** | `TaskStateService` ist alleiniger Owner von `Status`/`PlanningPhase`/`BlockedByTaskId`-Writes; `OverrideSlotService` ausgelagert; `QueueWaker` + `QueuePicker` getrennt | — | ✅ Code |
|
||||
| **MarkdownView / Tabbed Settings / About-Modal / Prime-Status-Footer / Doppelklick-Edit** | — | `Views/MarkdownView`, `SettingsModalView` als `TabControl`, `AboutModalView`, transient Prime-Status in Footer, `DoubleTapped` an List/Task-Rows | ✅ Code |
|
||||
- **Worktree-Pipeline:**
|
||||
- Worktree-Happy-Path → `worktrees.state='active'`, `head_commit` gesetzt, `diff_stat` non-empty, Branch `claudedo/<id>` auf Disk.
|
||||
- No-Changes-Run → `status='Done'`, `head_commit IS NULL`, `diff_stat IS NULL`.
|
||||
- Kein Git-Repo (`working_dir=C:\Temp`) → `status='Failed'`, **keine** `worktrees`-Row, Git-Fehler im Log.
|
||||
- **Feature-Walkthroughs:** Planning-Session-Flow (Draft→Finalize→Chain), Prime/Daily-Prep-Trigger, Weekly-Report-Generierung, Self-Update (Banner → Update → „up to date").
|
||||
- **Worker-Autostart am Gerät:** Logoff/Logon-Autostart, Update-Pfad, Uninstall entfernt die Startup-`.lnk`.
|
||||
|
||||
## Offene Code-Punkte
|
||||
|
||||
- **Status-Bar Live-Update:** Prüfen, ob `RunNow`-Enable/Disable pro Task-Row bei Connection-Change sauber re-evaluiert. Connection-Status lebt in `IslandsShellViewModel` / `WorkerConnectionModalViewModel` (es gibt keinen `StatusBarViewModel` mehr). Erst messen, dann ggf. fixen. Klein.
|
||||
|
||||
---
|
||||
|
||||
## 1. Verification (vor allem anderen)
|
||||
## Bewusst verworfen (nicht erneut vorschlagen)
|
||||
|
||||
Der Großteil der Verification-Steps aus `plan.md` ist im Code abgedeckt — was fehlt ist die **manuelle Bestätigung mit explizit notiertem Pass-Kriterium**. Ohne falsifizierbare Observable produziert ein Manual-Run nur "sah ok aus".
|
||||
|
||||
### 1.0 Plan-Verification 1–13
|
||||
|
||||
| # | Item | Status | Pass-Kriterium (was muss konkret zu sehen sein) |
|
||||
|---|------|---|---|
|
||||
| 1 | Schema-Init | ✅ | `~/.todo-app/todo.db` + `*-wal` + `*-shm` existieren; EF-Migrationsverlauf in `__EFMigrationsHistory` enthält alle 8 Migrationen; Worker-Log: „listening on …" |
|
||||
| 1a | SignalR-Endpoint | ✅ | `curl http://127.0.0.1:47821/hub` → HTTP 400 (kein Handshake) |
|
||||
| 1b | Hub-Roundtrip `Ping` | 🟡 | UI-Statusbar zeigt „Connected"; `WorkerClient.PingAsync()` liefert `"pong"` (UI-Test fehlt) |
|
||||
| 2 | `claude --version` Preflight | ✅ | `Worker/Lifecycle/ClaudeCliPreflight.cs` + Wiring in `Program.cs`. Kaputter `claude_bin` → `LogCritical(...) + Environment.Exit(1)`. Skip via `CLAUDEDO_SKIP_CLI_PREFLIGHT=1`. Tests: `tests/.../Lifecycle/ClaudeCliPreflightTests.cs` |
|
||||
| 3 | Smoke-Spawn (`claude -p` Prompt „ping") | ⬜ | `task_runs`-Row mit `session_id NOT NULL`, `result` non-empty, `output_tokens > 0` |
|
||||
| 4 | E2E Happy Path (Non-Worktree) | ⬜ | Liste „Test" anlegen → Task „Schreibe ein Haiku über Intralogistik" → `tasks.status='Done'`, `tasks.result IS NOT NULL`, Logfile unter `~/.todo-app/logs/<taskId>.ndjson`, UI-Row mit Done-Badge |
|
||||
| 5 | Worktree Happy Path | ⬜ | Liste mit `working_dir` auf temp-Repo, Task mit Codeänderung → `worktrees.state='active'`, `head_commit IS NOT NULL`, `diff_stat` non-empty, Branch `claudedo/<id>` auf Disk |
|
||||
| 6 | No-Changes-Run | ⬜ | Prompt der nichts ändert → `tasks.status='Done'` aber `worktrees.head_commit IS NULL`, `diff_stat IS NULL` |
|
||||
| 7 | Kein Git-Repo | ⬜ | `working_dir=C:\Temp` (kein Repo) → `tasks.status='Failed'`, **keine** `worktrees`-Row, Worker-Log enthält Git-Fehler |
|
||||
| 8 | Merge-UI | 🟡 | `MergeTask`-Hub-Methode + `MergeModalView` vorhanden, manueller Run nicht durchgespielt → `worktrees.state='merged'`, im Ziel-Repo `git log` zeigt Commit, `git worktree list` ohne Branch |
|
||||
| 9 | Override-Parallelität | 🟡 | `OverrideSlotService`-Tests grün; UI-E2E nicht durchgespielt → `WorkerHub.GetActive` ≥ 2 Einträge bei Run+RunNow |
|
||||
| 10 | Schedule | 🟡 | `QueuePicker`-Tests grün; UI-E2E nicht → `scheduled_for=now+2min` bleibt Queued, dann automatisch Running, `started_at >= scheduled_for` |
|
||||
| 11 | Worker-Offline-Erkennung | 🟡 | `WorkerClient.OnServerConnectionClosed` + Auto-Reconnect implementiert (`WithAutomaticReconnect`); visuell prüfen: nach `taskkill` der Worker-Exe wechselt Statusbar in ≤ 5s auf „Offline", `RunNow`-Buttons disabled |
|
||||
| 12 | Live-Stream | 🟡 | `ClaudeProcess` streamt NDJSON via `TaskMessage`-Event, UI hat `LiveTail`; visuell prüfen: während Run laufen ndjson-Zeilen ein |
|
||||
| 13 | Wake-up (`WakeQueue` nach Anlage) | 🟡 | `QueueWaker.Wake()` wird bei Enqueue aufgerufen; visuell prüfen: Task wechselt in ≤ 1s auf Running (statt nach `queue_backstop_interval_ms`=30s) |
|
||||
|
||||
**Empfohlener Sprint:** Steps 3–7 in einem Rutsch durchspielen (alles non-UI), parallel daneben 8–13 visuell beim normalen App-Lauf abhaken.
|
||||
|
||||
### 1.1 Planning Sessions — Manual Verification (unverändert relevant)
|
||||
|
||||
Bedingt durch Slice "Planning B/C". Ablauf identisch zur alten open.md:
|
||||
|
||||
1. Manual-Task mit Title + TODO-Description anlegen.
|
||||
2. Rechtsklick → **Open planning Session** → Windows Terminal mit Claude CLI öffnet.
|
||||
3. In CLI: zwei Children via `mcp__claudedo__create_child_task` anlegen.
|
||||
4. UI: Drafts erscheinen eingerückt, italic, mit `DRAFT`-Badge; Parent zeigt `PLANNING`-Badge.
|
||||
5. Chevron klappt ein/aus.
|
||||
6. CLI `finalize` → Children werden Queued (erste) bzw. Queued+BlockedBy (Rest); Parent flippt von `Active` auf `Finalized` (`PLANNED`-Badge); erste Child startet automatisch.
|
||||
7. Neuer Planning-Task, Terminal ohne Finalize schließen → Rechtsklick öffnet Resume/Finalize-now/Discard-Modal.
|
||||
8. Delete-Versuch auf Parent mit Children → freundlicher Fehlerdialog, kein Delete.
|
||||
|
||||
**Bekannte Follow-ups (non-blocking):**
|
||||
- ✅ `Border.badge.planned` (blau) wird jetzt bei `Finalized` angewendet — `TaskRowView` nutzt `Classes.planning`/`Classes.planned` gebunden an `IsPlanActive`/`IsPlanFinalized`; der Child-„PLANNED"-Badge nutzt direkt `planned`.
|
||||
- ✅ Tote `Instance`-Statics auf `BoolToItalicConverter` und `BoolToDraftOpacityConverter` entfernt (Registrierung läuft über das Resource-Dictionary in `App.axaml`).
|
||||
- ✅ `Ui.Tests` IWorkerClient-Fakes auf gemeinsame Basis `StubWorkerClient` rebased — kein Constructor-Drift mehr; die drei Fakes überschreiben nur ihre relevanten Member.
|
||||
|
||||
### 1.2 Prime Claude — Manual Verification
|
||||
|
||||
Slice "Prime" (Recurrence-Scheduler).
|
||||
|
||||
1. Settings → Prime-Claude-Tab → Schedule mit `at: 09:00`, `every: workday`, `task_template: "Daily Standup"` anlegen.
|
||||
2. Test mit verschobenem `IPrimeClock` (oder Schedule mit `at: now+1min`) → bei Trigger erscheint Toast/Footer-Notification „Prime fired", neuer Task entsteht in der Ziel-Liste.
|
||||
3. Worker-Restart innerhalb des Schedule-Fensters → Catch-up läuft genau einmal (kein Doppelfeuer).
|
||||
4. Schedule editieren → `next_due_at` wird neu berechnet; UI-Anzeige aktualisiert.
|
||||
5. Schedule löschen → keine weiteren Trigger, keine ghost-Tasks.
|
||||
|
||||
### 1.3 Self-Update — Manual Verification (aus alter open.md, weiterhin gültig)
|
||||
|
||||
Voraussetzung: funktionierendes Gitea-Release unter `git.kuns.dev/releases/ClaudeDo` mit drei Assets — `ClaudeDo-<version>-win-x64.zip`, `ClaudeDo.Installer-<version>.exe`, `checksums.txt`.
|
||||
|
||||
1. Baseline-Version (z.B. `0.2.x`) normal installieren.
|
||||
2. Neues Release `v0.3.0` mit frischem Installer + App-Zip + Checksums veröffentlichen.
|
||||
3. App starten → Banner erscheint: `Update available: v0.2.x → v0.3.0`.
|
||||
4. **Update now** klicken → App schließt, Installer öffnet im Update-Mode, läuft, restartet Worker.
|
||||
5. App neu starten → Banner weg; `Help → Check for updates` zeigt kurz „You're up to date (v0.3.0)".
|
||||
6. `v0.2.x`-Installer manuell starten → bietet Self-Update auf v0.3.0 an. **Update** → laufende Exe wird ersetzt, Wizard öffnet auf neuer Version.
|
||||
7. Schritt 6 mit **Continue anyway** → Wizard öffnet ohne Self-Update.
|
||||
8. Schritt 6 mit **Cancel** → Installer beendet ohne Aktion.
|
||||
9. Network-Kill in App und Installer beim Start → silent fallback (kein Error, kein Banner).
|
||||
|
||||
---
|
||||
|
||||
## 2. UI-Polish
|
||||
|
||||
### 2.1 Folder-Picker für `Working Directory` ⬜
|
||||
- **Datei:** `Views/ListSettingsModalView.axaml` + zugehöriges VM
|
||||
- **Aktuell:** plain `TextBox` — Pfad muss getippt werden.
|
||||
- **Soll:** Button „…" daneben → öffnet `IStorageProvider.OpenFolderPickerAsync`, schreibt Pfad ins Feld.
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 2.2 Delete-Confirmation ⬜
|
||||
- **Aktuell:** Listen/Tasks-Delete läuft direkt ohne Rückfrage. Datenverlust-Risiko.
|
||||
- **Soll:** generischer `ConfirmDialog` (1× bauen, mehrfach nutzen), Mini-Dialog „Wirklich löschen?".
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 2.3 Markdown-Rendering Result + Description ✅
|
||||
- `Views/MarkdownView.axaml` + Detail-Pane verwenden Markdown.Avalonia.
|
||||
|
||||
### 2.4 Live-Log Auto-Scroll ⬜
|
||||
- **Datei:** `Views/DetailsIslandView.axaml(.cs)` (Live-Tail-Section)
|
||||
- **Aktuell:** ndjson-Zeilen werden angehängt, Scrollposition bleibt stehen.
|
||||
- **Soll:** Sticky-Bottom-Pattern — bei jeder neuen Zeile `ScrollToEnd()`, solange User nicht manuell hochgescrollt hat. Attached-Behavior reicht.
|
||||
|
||||
### 2.5 Diff-Viewer 🟡
|
||||
- `DiffModalView.axaml` + `PlanningDiffView` existieren; integriert für Planning-Merges.
|
||||
- **Offen:** Task-Level-Diff (Worktree vs. main) noch nicht im Modal-Flow geprüft. Verwenden statt `Process.Start("cmd /k git diff …")`.
|
||||
|
||||
### 2.6 Status-Bar Active-Tasks Live-Update ⬜
|
||||
- **Datei:** `ViewModels/StatusBarViewModel`
|
||||
- **Risiko:** `RunNowCommand.NotifyCanExecuteChanged` triggert nicht pro Item bei Connection-Change.
|
||||
- **Soll:** `WeakReferenceMessenger`-Connection-Change-Message; alle `TaskRowViewModel` lauschen.
|
||||
- **Aufwand:** klein, muss sauber gemacht werden.
|
||||
|
||||
### 2.7 Settings-Dialog ✅
|
||||
- `SettingsModalView` als `TabControl`, Tabs: General, Prime Claude, etc. Persistiert in `~/.todo-app/ui.config.json` und `worker.config.json`.
|
||||
|
||||
### 2.8 (NEU) Planning-Phase Badge-Farbe für `Finalized` ✅
|
||||
`Finalized` zeigt jetzt den blauen `planned`-Badge (Class-Binding in `TaskRowView`).
|
||||
|
||||
### 2.9 (NEU) Tote Converter-Statics entfernen ✅
|
||||
`BoolToItalicConverter.Instance`, `BoolToDraftOpacityConverter.Instance` entfernt.
|
||||
|
||||
---
|
||||
|
||||
## 3. Worker-Robustheit
|
||||
|
||||
### 3.1 CLI-Preflight beim Worker-Start ✅
|
||||
- `src/ClaudeDo.Worker/Lifecycle/ClaudeCliPreflight.cs` + Wiring in `Program.cs`. Tests: `tests/.../Lifecycle/ClaudeCliPreflightTests.cs`. Skippable via `CLAUDEDO_SKIP_CLI_PREFLIGHT=1`.
|
||||
|
||||
### 3.2 Worktree-Cleanup beim Anlege-Failed ⬜
|
||||
- **Datei:** `src/ClaudeDo.Worker/Runner/WorktreeManager.cs`
|
||||
- **Soll:** try/finally — bei Fehler zwischen `git worktree add` und DB-Insert `git worktree remove --force` als Best-Effort-Cleanup.
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 3.3 Logging über file-Sink ⬜
|
||||
- ILogger ist überall verdrahtet, aber kein File-Sink konfiguriert.
|
||||
- **Soll:** Serilog oder `Karambolage.Extensions.Logging.File` — für Service-Modus zwingend, console-only ist im SCM-Fenster verloren.
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 3.4 Tag-Negation / Exclusion ⬜
|
||||
- Tags sind weiterhin rein additiv (`list_tags ∪ task_tags`). Nach Slice „Editierbare Tags" weniger dringend, aber nicht gelöst.
|
||||
- **Soll:** entweder neue Tabelle `task_tag_exclusions` oder Prefix `!tag` im task_tags-Eintrag.
|
||||
- **Aufwand:** mittel — Schema + Repo + Tests + UI.
|
||||
|
||||
---
|
||||
|
||||
## 4. Service-Deployment
|
||||
|
||||
### 4.1 Worker-Autostart via Startup-Shortcut ✅ (ersetzt Scheduled Task + Windows-Service)
|
||||
- Der Worker läuft als `WinExe` (kein Konsolenfenster) + Serilog-File-Sink (`~/.todo-app/logs/worker-*.log`) + Single-Instance-Mutex.
|
||||
- Autostart über eine **Startup-Ordner-Verknüpfung** `ClaudeDo Worker.lnk` (`%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\`), die der Installer via `AutostartShortcut`/`ShortcutFactory` COM-Helper anlegt. Kein Scheduled Task, kein Windows-Service.
|
||||
- `StartWorkerStep` startet den Worker per `Process.Start`; `StopWorkerStep` beendet ihn per prozessbasiertem Kill.
|
||||
- Die App (`IslandsShellViewModel`) startet den Worker nicht selbst. Bei offline-Worker ~12s nach App-Start: einmaliges `WorkerConnectionModal` (Start Worker / Rerun Installer / Dismiss); Connection-Status-Pill in der Fußzeile ist ein Button zum erneuten Öffnen des Modals.
|
||||
- `UninstallRunner` löscht die Startup-`.lnk`; migriert ältere Installs durch best-effort-Löschen des Legacy-Scheduled-Tasks „ClaudeDoWorker" und des Legacy-Windows-Service.
|
||||
- **Manuelle E2E-Verifikation am Gerät ausstehend** (Logoff/Logon-Autostart, Update-Pfad, Uninstall).
|
||||
|
||||
### 4.2 Pfad-Auflösung absolut ✅
|
||||
- `WorkerConfig.Load` expandiert `~`/`%USERPROFILE%` für alle Pfad-Felder.
|
||||
|
||||
### 4.3 Install-Skripte / Doku ⬜
|
||||
- **Datei (neu):** `docs/install-service.md` ODER `scripts/install-service.cmd`
|
||||
- **Inhalt:** `dotnet publish` + `sc.exe create` + `sc.exe failure` + Hinweis auf `obj=` (User-Account) wegen Claude-CLI-Session.
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 4.4 Installer-Projekt ✅
|
||||
- `ClaudeDo.Installer` (WPF) + `ClaudeDo.Releases` mit Pages/Steps/Core/Theme — Self-Update funktioniert (siehe §1.3).
|
||||
|
||||
---
|
||||
|
||||
## 5. Tests / CI
|
||||
|
||||
### 5.1 CI-Pipeline (Gitea Actions) ⬜
|
||||
- **Datei (neu):** `.gitea/workflows/ci.yml`
|
||||
- **Inhalt:** `dotnet restore` → `dotnet build` (csproj-weise wegen `.slnx`-Bug auf .NET 8) → `dotnet test`. Auf Push + PR.
|
||||
- **Achtung:** Pipeline darf NICHT die `.slnx` als Build-Target nehmen — explizite csproj-Liste in einem checked-in Build-Skript.
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 5.2 SignalR-Hub-Tests ✅
|
||||
- `tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs`, `AgentSettingsHubTests.cs` testen Hub-Methoden via Fakes (kein realer SignalR-Roundtrip, aber alle Code-Pfade abgedeckt).
|
||||
- **Optional verbleibt:** echter Roundtrip-Test mit `WebApplicationFactory<Program>` + `HubConnectionBuilder` für End-to-End-Validierung der SignalR-Pipeline. Niedriger Mehrwert solange Fakes alle Methoden treffen.
|
||||
|
||||
### 5.3 Smoke-Test gegen echten `claude` ⬜
|
||||
- **Datei (neu):** `tests/ClaudeDo.Worker.Tests/Runner/ClaudeProcessSmokeTest.cs`
|
||||
- **Soll:** Real-CLI-Test mit `[Fact(Skip="...")]` ausgegraut, nur lokal aktiviert wenn `CLAUDE_AUTHENTICATED=1` Env-Var gesetzt ist.
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 5.4 (NEU) ExternalMcpService-Tests ⬜
|
||||
- `External/ExternalMcpService` hat 11 Tools, aber nur partielle Coverage in `tests/.../External/ExternalMcpServiceTests.cs`. Für jedes Tool mindestens einen Happy-Path + einen Error-Pfad ergänzen.
|
||||
|
||||
---
|
||||
|
||||
## 6. Dokumentation
|
||||
|
||||
### 6.1 README.md ⬜
|
||||
- Komplett fehlt. Mind. 1× kurz: was ist es, wie starten (Worker + UI), wo Config, wie Self-Update.
|
||||
|
||||
### 6.2 docs/architecture.md 🟡
|
||||
- In `plan.md` enthalten — entweder konsolidieren oder explizit ausgliedern. CLAUDE.md-Dateien pro Projekt sind aktuell de-facto-Architecture-Doc.
|
||||
|
||||
### 6.3 ADRs ⬜
|
||||
- Vorschläge: „SignalR vs. SQLite-Polling für IPC", „Worktree pro Task", „TaskStateService als alleiniger State-Owner", „BlockedByTaskId statt Status='Waiting'", „External MCP als zweite WebApplication".
|
||||
- **Aufwand:** klein, hilfreich für später.
|
||||
|
||||
### 6.4 (NEU) Mailbox-Proposal ⬜
|
||||
- `docs/mailbox-proposal.md` ist als Vorschlag vorhanden, nicht implementiert. Entscheidung: bauen, droppen oder parken? Wenn droppen → Datei entfernen, sonst klare Roadmap.
|
||||
|
||||
---
|
||||
|
||||
## 7. Bekannte Code-Schulden / Smells
|
||||
|
||||
| Stelle | Issue | Status |
|
||||
|---|---|---|
|
||||
| `WorkerHub.GetActive` returnt `IReadOnlyList<object>` mit anonymen Typen | Sollte expliziter `ActiveTaskDto` sein, den Worker UND UI teilen | ✅ (gibt bereits `IReadOnlyList<ActiveTaskDto>` zurück) |
|
||||
| `TaskRunner` führt eine `if (list.WorkingDir != null)`-Verzweigung mitten in der Methode | Strategy-Pattern wenn die Methode wächst, aktuell noch klein genug | ⬜ |
|
||||
| `App.Services` als public static `ServiceProvider` | Service-Locator-Antipattern, toleriert weil nur in `App.OnFrameworkInitializationCompleted` | ⬜ |
|
||||
| Embedded `schema.sql` ohne Versionierung | Durch EF-Core-Migrationen ersetzt | ✅ |
|
||||
| CRLF-Warnings beim Commit | `.gitattributes` mit `* text=auto eol=lf` (oder explizit pro Sprache) wäre sauberer | ✅ (`.gitattributes` angelegt) |
|
||||
| Tote Converter-Instances (`BoolToItalicConverter.Instance`, `BoolToDraftOpacityConverter.Instance`) | Per Resource-Dictionary registriert, Statics ungenutzt | ✅ (entfernt) |
|
||||
| 1 unausgeführter `// TODO` in `DetailsIslandViewModel` (`SendPromptAsync` ohne Hub-Methode) | Entweder Hub-Methode bauen oder TODO entfernen | ✅ (im Main-Code nicht mehr vorhanden) |
|
||||
|
||||
---
|
||||
|
||||
## 8. Improvement Plan (improvement-plan.md, Stand 2026-04-13)
|
||||
|
||||
| ID | Item | Status | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| IP-1 | UI ↔ Worker Auto-Reconnect | ✅ | `WorkerClient` mit `WithAutomaticReconnect()` + Reconnect-Handler |
|
||||
| IP-2 | Listen-Modus „Notes" (non-autonomous) | ⬜ | Nach Slice „editierbare Status/Tags" weniger dringend (man kann jetzt einen Task ohne `agent`-Tag idle lassen), aber `lists.kind` als sauberer Mode-Switch fehlt. |
|
||||
| IP-3 | Doppelklick öffnet Edit-Dialog | ✅ | `DoubleTapped`-Handler in `ListsIslandView`, `TasksIslandView` |
|
||||
| IP-4 | Tag Multi-Select Control | ⬜ | Tags sind via Picker im Detail-Pane editierbar, aber kein dediziertes Multi-Select-Control mit Auto-Vervollständigung in Editor-Dialogen. |
|
||||
| IP-5 | Rechtsklick-Kontextmenü | ✅ | Listen + Tasks haben Context-Menüs (Edit, Delete, Run Now, Show Diff, Merge, Cancel, Status, Tags) |
|
||||
| IP-6 | Schema-Migration-Mechanismus | ✅ | EF-Core-Migrations + `__EFMigrationsHistory` |
|
||||
| IP-7 | Status-Bar Reconnect-States | ✅ | `connected`/`connecting`/`reconnecting`/`offline` farbcodiert |
|
||||
| IP-8 | Tag-Repository `GetAllKnownTagsAsync` | ✅ | `TagRepository.GetAllAsync` + `WorkerClient.GetAllTagsAsync` |
|
||||
|
||||
---
|
||||
|
||||
## 9. Empfohlene Reihenfolge für die nächsten Sessions
|
||||
|
||||
**Block 1 — Verification durchspielen** (kein neuer Code, nur Beweis):
|
||||
1. §1.0 Steps 3–7 manuell (Smoke + E2E + Worktree + No-Changes + Kein-Repo) — ist die Pipeline wirklich lebendig?
|
||||
2. §1.1 Planning-Walkthrough — nach den uncommitted Coordinator-Änderungen einmal durchspielen.
|
||||
3. §1.2 Prime-Walkthrough — Schedule-Trigger einmal beobachten.
|
||||
|
||||
**Block 2 — Niedrig hängende UI-Polish** (eine Session):
|
||||
4. §2.1 Folder-Picker
|
||||
5. §2.2 Delete-Confirmation
|
||||
6. §2.4 Live-Log Auto-Scroll
|
||||
7. §2.6 Status-Bar Live-Update
|
||||
8. §2.8 Planning-Badge-Farbe + §2.9 tote Converter weg
|
||||
|
||||
**Block 3 — Robustheit & Service-Deployment**:
|
||||
9. §3.2 Worktree-Cleanup
|
||||
10. §3.3 File-Sink-Logging
|
||||
11. §4.3 Install-Skripte/Doku
|
||||
|
||||
**Block 4 — Sicherheitsnetz**:
|
||||
12. §5.1 Gitea-Actions CI-Pipeline (csproj-weise)
|
||||
13. §5.3 Smoke-Test gegen echten claude
|
||||
14. §5.4 ExternalMcpService-Tests vervollständigen
|
||||
|
||||
**Block 5 — Dokumentation & Aufräumen**:
|
||||
15. §6.1 README
|
||||
16. §6.3 ADRs (mind. die fünf wichtigsten)
|
||||
17. §6.4 Mailbox-Proposal: bauen/droppen entscheiden
|
||||
18. §7 Smells: `ActiveTaskDto`, `.gitattributes`, TODO-Comment
|
||||
|
||||
**Block 6 — Optional / wenn Bedarf konkret wird**:
|
||||
19. §3.4 Tag-Negation
|
||||
20. §IP-2 Notes-Modus
|
||||
21. §IP-4 Tag Multi-Select Control
|
||||
- **CI-Build/Test-Pipeline** — push-to-main + release-on-push deckt das ab; Tests laufen am Ende jeder Session.
|
||||
- **Real-`claude`-Smoke-Test als xUnit-Test** — kein Claude in `dotnet test`; bleibt manueller Check (siehe oben). Tests nutzen `FakeClaudeProcess`.
|
||||
- **`architecture.md` / ADRs** — die per-Projekt-`CLAUDE.md`-Dateien sind die lebende Doku; ADRs lohnen solo nicht.
|
||||
- **Task-Mailbox-Integration** — geparkt; das generische `mcp__mailbox__*`-Plugin reicht (Begründung in `mailbox-proposal.md`).
|
||||
- **Tag-Negation, Tag-Multi-Select, Notes-`lists.kind`-Switch, Install-Service-Skript** — durch die aktuelle Architektur überholt (Tag-System entfernt, Notes/Autostart anders gelöst).
|
||||
|
||||
@@ -7,6 +7,22 @@ Snapshot of every string ClaudeDo sends to Claude CLI, plus the CLI-flag surface
|
||||
|
||||
Date: 2026-04-24
|
||||
|
||||
> **Update 2026-06-04 — prompts externalized.** All prose prompts now live as
|
||||
> editable files under `~/.todo-app/prompts/`, each seeded from a bundled default in
|
||||
> `src/ClaudeDo.Data/PromptFiles.cs` (read via `ReadOrDefault` / `Render`, which
|
||||
> substitutes only named `{tokens}`):
|
||||
> `system.md`, `planning-system.md`, `planning-initial.md` (`{title}`/`{description}`),
|
||||
> `retry.md`, `daily-prep.md` (`{date}`/`{maxTasks}`), `weekly-report.md`
|
||||
> (`{start}`/`{end}`; German output). The old `agent.md` and `planning.md` are
|
||||
> retired — `system.md` is the single appended system prompt (the agent/manual split
|
||||
> is gone), and the planning system prompt is `planning-system.md`. Daily-prep and
|
||||
> retry prompts are now English; retry leans on the resumed session and appends the
|
||||
> captured stderr only when it's a real error (not the generic "exited with code N").
|
||||
> The system prompt instructs the agent to emit `CLAUDEDO_BLOCKED: <reason>` on its
|
||||
> own line for any true blocker; `StreamAnalyzer` collects every marker, strips them
|
||||
> from the result, and `TaskRunner` folds them into the review result as a
|
||||
> "⚠ Roadblocks" section. All six prompt files are editable from Settings → Files.
|
||||
|
||||
---
|
||||
|
||||
## 1. Task-execution prompts (agent-tagged tasks → Claude CLI)
|
||||
|
||||
175
docs/superpowers/plans/2026-06-01-waiting-for-review-state.md
Normal file
175
docs/superpowers/plans/2026-06-01-waiting-for-review-state.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Waiting for Review — Task State — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a `WaitingForReview` lifecycle state that standalone tasks enter after a successful run, with approve / reject-rerun / reject-park / cancel exits, exposed via UI and MCP.
|
||||
|
||||
**Architecture:** New enum value + nullable `ReviewFeedback` column. `TaskStateService` gains review transitions. `TaskRunner.HandleSuccess` routes standalone-task success to review. `QueueService.RunInSlotAsync` resumes the Claude session when re-running a rejected task. New MCP `review_task` tool + UI commands.
|
||||
|
||||
**Tech Stack:** .NET 8, EF Core (SQLite, TEXT enum), SignalR, Avalonia MVVM, xUnit.
|
||||
|
||||
**Scope decision (locked):** Only standalone tasks (`ParentTaskId == null`) route to `WaitingForReview`. Planning **child** tasks continue to `Done` on success so the sequential planning chain (which advances on *terminal* states) is unaffected. Flagged for user confirmation.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Data layer — enum, converter, column
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Data/Models/TaskEntity.cs`
|
||||
- Modify: `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs`
|
||||
- Create: EF migration via CLI
|
||||
|
||||
- [ ] **Step 1:** Add `WaitingForReview` to `TaskStatus` enum (after `Running`) and add `public string? ReviewFeedback { get; set; }` to `TaskEntity`.
|
||||
- [ ] **Step 2:** In `TaskEntityConfiguration`, add `TaskStatus.WaitingForReview => "waiting_for_review"` to `StatusToString` and `"waiting_for_review" => TaskStatus.WaitingForReview` to `StatusFromString`; map the column: `builder.Property(t => t.ReviewFeedback).HasColumnName("review_feedback");`
|
||||
- [ ] **Step 3:** Create migration: `dotnet ef migrations add AddReviewFeedback --project src/ClaudeDo.Data/ClaudeDo.Data.csproj`. Verify it only adds the `review_feedback` TEXT column (nullable). If `dotnet ef` unavailable, hand-write the migration + designer following the latest migration in `Migrations/`.
|
||||
- [ ] **Step 4:** Build `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj`. Expected: success.
|
||||
- [ ] **Step 5:** Commit.
|
||||
|
||||
## Task 2: Worker — review transitions in TaskStateService
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/State/TaskStateService.cs`
|
||||
- Modify: `src/ClaudeDo.Worker/State/Interfaces/ITaskStateService.cs` (add new method signatures)
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/...` (state transition tests)
|
||||
|
||||
New methods (all return `TransitionResult`, broadcast `TaskUpdated`):
|
||||
|
||||
- `SubmitForReviewAsync(taskId, finishedAt, result, ct)` — guard `Status == Running`; set `Status=WaitingForReview, FinishedAt, Result`. Does NOT call `OnChildTerminalAsync` (review is non-terminal; only invoked for standalone tasks anyway).
|
||||
- `ApproveReviewAsync(taskId, ct)` — guard `Status == WaitingForReview`; set `Status=Done`.
|
||||
- `RejectToQueueAsync(taskId, feedback, ct)` — reject empty/whitespace feedback (`TransitionResult(false, "Feedback is required to reject for re-run.")`); guard `Status == WaitingForReview`; set `Status=Queued, ReviewFeedback=feedback`; `_waker.Wake()`.
|
||||
- `RejectToIdleAsync(taskId, ct)` — guard `Status == WaitingForReview`; set `Status=Idle, ReviewFeedback=null` (leave `Result` intact).
|
||||
- `ClearReviewFeedbackAsync(taskId, ct)` — set `ReviewFeedback=null` (no status change, no guard); used by the runner after consuming feedback.
|
||||
- Extend `CancelAsync` guard: `(Status == Running || Status == Queued || Status == WaitingForReview)`.
|
||||
|
||||
- [ ] **Step 1:** Write failing tests in a new `tests/ClaudeDo.Worker.Tests/State/ReviewTransitionTests.cs` (follow existing TaskStateService test setup). Cover: submit-for-review from Running; approve from WaitingForReview→Done; reject-to-queue stores feedback + status Queued; empty feedback rejected; reject-to-idle clears feedback + keeps Result; cancel from WaitingForReview→Cancelled; invalid (approve from Idle) returns `!Ok`.
|
||||
- [ ] **Step 2:** Run tests, expect FAIL (methods missing).
|
||||
- [ ] **Step 3:** Implement the methods + interface signatures + CancelAsync guard.
|
||||
- [ ] **Step 4:** Run tests, expect PASS.
|
||||
- [ ] **Step 5:** Commit.
|
||||
|
||||
## Task 3: Worker — route standalone success to review
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs` (`HandleSuccess`)
|
||||
|
||||
- [ ] **Step 1:** In `HandleSuccess`, after commit, branch:
|
||||
```csharp
|
||||
var finishedAt = DateTime.UtcNow;
|
||||
if (task.ParentTaskId is null)
|
||||
{
|
||||
await _state.SubmitForReviewAsync(task.Id, finishedAt, result.ResultMarkdown, CancellationToken.None);
|
||||
await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (waiting for review)", WorkerLogLevel.Success, DateTime.UtcNow);
|
||||
await _broadcaster.TaskFinished(slot, task.Id, "waiting_for_review", finishedAt);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _state.CompleteAsync(task.Id, finishedAt, result.ResultMarkdown, CancellationToken.None);
|
||||
await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (done)", WorkerLogLevel.Success, DateTime.UtcNow);
|
||||
await _broadcaster.TaskFinished(slot, task.Id, "done", finishedAt);
|
||||
}
|
||||
```
|
||||
- [ ] **Step 2:** Build worker. Expected: success.
|
||||
- [ ] **Step 3:** Commit.
|
||||
|
||||
## Task 4: Worker — resume-aware re-run in QueueService
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Queue/QueueService.cs` (`RunInSlotAsync`)
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/...`
|
||||
|
||||
- [ ] **Step 1:** In `RunInSlotAsync`, after loading `task`:
|
||||
```csharp
|
||||
if (!string.IsNullOrWhiteSpace(task.ReviewFeedback))
|
||||
{
|
||||
var feedback = task.ReviewFeedback!;
|
||||
string? sessionId;
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
sessionId = (await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId, ct))?.SessionId;
|
||||
await _state.ClearReviewFeedbackAsync(taskId, ct); // inject ITaskStateService
|
||||
if (sessionId is not null)
|
||||
{
|
||||
await _runner.ContinueAsync(taskId, feedback, "queue", ct);
|
||||
return;
|
||||
}
|
||||
task.Description = string.IsNullOrWhiteSpace(task.Description)
|
||||
? $"Reviewer feedback: {feedback}"
|
||||
: $"{task.Description}\n\nReviewer feedback: {feedback}";
|
||||
}
|
||||
await _runner.RunAsync(task, "queue", ct);
|
||||
```
|
||||
Inject `ITaskStateService _state` into `QueueService` (add to ctor + DI already provides it).
|
||||
- [ ] **Step 2:** Build worker, expect success.
|
||||
- [ ] **Step 3:** Commit.
|
||||
|
||||
## Task 5: MCP — review_task tool + status reference
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/External/ExternalMcpService.cs`
|
||||
|
||||
- [ ] **Step 1:** Add `review_task` tool:
|
||||
```csharp
|
||||
[McpServerTool, Description(
|
||||
"Review a task that is WaitingForReview. decision: 'approve' (→ Done), " +
|
||||
"'reject_rerun' (→ Queued, resumes the agent session with feedback — feedback required), " +
|
||||
"'reject_park' (→ Idle for manual editing), 'cancel' (→ Cancelled). ")]
|
||||
public async Task<TaskDto> ReviewTask(string taskId, string decision, string? feedback, CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||
TransitionResult r = decision.ToLowerInvariant() switch
|
||||
{
|
||||
"approve" => await _state.ApproveReviewAsync(taskId, cancellationToken),
|
||||
"reject_rerun" => await _state.RejectToQueueAsync(taskId, feedback ?? "", cancellationToken),
|
||||
"reject_park" => await _state.RejectToIdleAsync(taskId, cancellationToken),
|
||||
"cancel" => await _state.CancelAsync(taskId, DateTime.UtcNow, cancellationToken),
|
||||
_ => throw new InvalidOperationException($"Unknown decision '{decision}'. Use approve, reject_rerun, reject_park, or cancel."),
|
||||
};
|
||||
if (!r.Ok) throw new InvalidOperationException(r.Reason ?? "Review action failed.");
|
||||
return ToDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!);
|
||||
}
|
||||
```
|
||||
- [ ] **Step 2:** Add `WaitingForReview` to `GetTaskStatusValues` list; update the validation strings in `ListTasks` and the lifecycle text in `GetTask`/`UpdateTaskStatus` to include `WaitingForReview`.
|
||||
- [ ] **Step 3:** Build worker, expect success.
|
||||
- [ ] **Step 4:** Commit.
|
||||
|
||||
## Task 6: UI — client + hub methods
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||
- Modify: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`
|
||||
|
||||
- [ ] **Step 1:** Hub: add `ApproveReview(taskId)`, `RejectReviewToQueue(taskId, feedback)`, `RejectReviewToIdle(taskId)`, `CancelReview(taskId)` — each calls the matching `_state` method via `HubGuard`-style mapping (`if (!result.Ok) throw new HubException(...)`).
|
||||
- [ ] **Step 2:** `IWorkerClient` + `WorkerClient`: add `ApproveReviewAsync`, `RejectReviewToQueueAsync(taskId, feedback)`, `RejectReviewToIdleAsync`, `CancelReviewAsync` invoking the hub methods. Add no-op/stub impls to `StubWorkerClient`.
|
||||
- [ ] **Step 3:** Build App + Ui.Tests. Expected: success.
|
||||
- [ ] **Step 4:** Commit.
|
||||
|
||||
## Task 7: UI — converter, row VM, view buttons
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Converters/StatusColorConverter.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` (commands)
|
||||
- Modify: the task row/detail AXAML to surface Approve / Reject / Park / Cancel when `IsWaitingForReview`
|
||||
|
||||
- [ ] **Step 1:** `StatusColorConverter`: add `"waiting_for_review" => Brushes.MediumPurple,` (placeholder — user does visual pass).
|
||||
- [ ] **Step 2:** `TaskRowViewModel`: add `public bool IsWaitingForReview => Status == TaskStatus.WaitingForReview;`, raise it in `OnStatusChanged`, and add `(TaskStatus.WaitingForReview, _) => "review"` to `StatusChipClass`.
|
||||
- [ ] **Step 3:** `TasksIslandViewModel`: add relay commands `ApproveReview`, `RejectReviewRerun` (prompts for feedback), `RejectReviewPark`, `CancelReview` operating on the selected/target row, calling the new client methods.
|
||||
- [ ] **Step 4:** Add buttons to the relevant view bound to those commands, visible when `IsWaitingForReview`. Reject-rerun uses a text-input flyout/dialog for required feedback.
|
||||
- [ ] **Step 5:** Build App + Ui.Tests. Expected: success. (Visual layout: flagged for user's visual pass — cannot render here.)
|
||||
- [ ] **Step 6:** Commit.
|
||||
|
||||
## Task 8: Docs + full verification
|
||||
|
||||
**Files:**
|
||||
- Modify: root `CLAUDE.md`, `src/ClaudeDo.Data/CLAUDE.md`, `src/ClaudeDo.Worker/CLAUDE.md`
|
||||
|
||||
- [ ] **Step 1:** Update status flow lines + worker transition table to include `WaitingForReview` and the new transitions.
|
||||
- [ ] **Step 2:** Build all projects (csproj individually — `.slnx` needs .NET 9) and run `dotnet test tests/ClaudeDo.Worker.Tests`, `tests/ClaudeDo.Ui.Tests`, `tests/ClaudeDo.Data.Tests`. Expected: all green.
|
||||
- [ ] **Step 3:** Commit.
|
||||
|
||||
## Self-Review notes
|
||||
|
||||
- Spec coverage: §1 state machine → Tasks 2,3; §2 data → Task 1; §3 transitions → Task 2; §4 resume → Task 4; §5 MCP → Task 5; §6 hub → Task 6; §7 UI → Tasks 6,7; §8 docs → Task 8; testing → Tasks 2,4,8.
|
||||
- Method names consistent across tasks: `SubmitForReviewAsync`, `ApproveReviewAsync`, `RejectToQueueAsync`, `RejectToIdleAsync`, `ClearReviewFeedbackAsync` (state); `ApproveReview`/`RejectReviewToQueue`/`RejectReviewToIdle`/`CancelReview` (hub); `ApproveReviewAsync`/`RejectReviewToQueueAsync`/`RejectReviewToIdleAsync`/`CancelReviewAsync` (client).
|
||||
983
docs/superpowers/plans/2026-06-02-prime-recurring-weekdays.md
Normal file
983
docs/superpowers/plans/2026-06-02-prime-recurring-weekdays.md
Normal file
@@ -0,0 +1,983 @@
|
||||
# Prime Recurring Weekday Schedule — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the Prime schedule's date-range model with a recurring weekday model — pick a set of weekdays plus a time, and the ping fires on the next eligible day the worker is running.
|
||||
|
||||
**Architecture:** A `[Flags] PrimeDays` weekday bitmask stored as a single `days_of_week` int column replaces `StartDate`/`EndDate`/`WorkdaysOnly`. `NextDueCalculator` walks forward to the next selected weekday; the existing 30-minute catch-up and already-fired-today logic are untouched. UI swaps the range picker + Mon–Fri checkbox for seven toggle buttons. Both SignalR DTO copies carry a single `int Days`.
|
||||
|
||||
**Tech Stack:** .NET 8, EF Core (SQLite), Avalonia 12 (CommunityToolkit.Mvvm), SignalR, xUnit.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-02-prime-recurring-weekdays-design.md`
|
||||
|
||||
**Build/test note:** `dotnet build ClaudeDo.slnx` needs .NET 9; on .NET 8 build individual csproj. Commands in this plan use the per-project form.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- `src/ClaudeDo.Data/Models/PrimeDays.cs` — **new**, `[Flags]` enum.
|
||||
- `src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs` — swap fields.
|
||||
- `src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs` — column mapping.
|
||||
- `src/ClaudeDo.Data/Migrations/*` — new migration + snapshot.
|
||||
- `src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs` — upsert fields + ordering.
|
||||
- `src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs` — `int Days`.
|
||||
- `src/ClaudeDo.Worker/Prime/NextDueCalculator.cs` — weekday eligibility.
|
||||
- `src/ClaudeDo.Worker/Prime/PrimeScheduler.cs` — `ToDto` mapping.
|
||||
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — list/upsert mapping.
|
||||
- `src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs` — `int Days`.
|
||||
- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs` — 7 day bools.
|
||||
- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs` — defaults + validation.
|
||||
- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` — `day-toggle` style class.
|
||||
- `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml` — row template.
|
||||
- Tests: `NextDueCalculatorTests`, `PrimeSchedulerTests`, `PrimeScheduleRepositoryTests`, `PrimeClaudeTabViewModelTests`.
|
||||
- Docs: `src/ClaudeDo.Data/CLAUDE.md`, root `CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: PrimeDays enum + entity + configuration
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ClaudeDo.Data/Models/PrimeDays.cs`
|
||||
- Modify: `src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs`
|
||||
- Modify: `src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs`
|
||||
|
||||
- [ ] **Step 1: Create the flags enum**
|
||||
|
||||
`src/ClaudeDo.Data/Models/PrimeDays.cs`:
|
||||
|
||||
```csharp
|
||||
namespace ClaudeDo.Data.Models;
|
||||
|
||||
[Flags]
|
||||
public enum PrimeDays
|
||||
{
|
||||
None = 0,
|
||||
Monday = 1,
|
||||
Tuesday = 2,
|
||||
Wednesday = 4,
|
||||
Thursday = 8,
|
||||
Friday = 16,
|
||||
Saturday = 32,
|
||||
Sunday = 64,
|
||||
Weekdays = Monday | Tuesday | Wednesday | Thursday | Friday, // 31
|
||||
All = Weekdays | Saturday | Sunday, // 127
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Swap entity fields**
|
||||
|
||||
In `src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs`, remove `StartDate`, `EndDate`, `WorkdaysOnly` and add `Days`. Result:
|
||||
|
||||
```csharp
|
||||
namespace ClaudeDo.Data.Models;
|
||||
|
||||
public sealed class PrimeScheduleEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public PrimeDays Days { get; set; } = PrimeDays.Weekdays;
|
||||
public TimeSpan TimeOfDay { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
public DateTimeOffset? LastRunAt { get; set; }
|
||||
public string? PromptOverride { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update entity configuration**
|
||||
|
||||
In `src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs`, replace the `start_date`/`end_date`/`workdays_only` property lines with a `days_of_week` mapping (EF maps the enum to INTEGER automatically):
|
||||
|
||||
```csharp
|
||||
builder.Property(s => s.Days).HasColumnName("days_of_week")
|
||||
.IsRequired().HasDefaultValue(PrimeDays.Weekdays);
|
||||
builder.Property(s => s.TimeOfDay).HasColumnName("time_of_day").IsRequired();
|
||||
builder.Property(s => s.Enabled).HasColumnName("enabled").IsRequired().HasDefaultValue(true);
|
||||
```
|
||||
|
||||
Leave `Id`, `LastRunAt`, `PromptOverride`, `CreatedAt` mappings unchanged. Add `using ClaudeDo.Data.Models;` if not present (it already is).
|
||||
|
||||
- [ ] **Step 4: Build the Data project**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj`
|
||||
Expected: FAILS — `PrimeScheduleRepository`, snapshot, etc. still reference removed fields. That is expected; Tasks 2–3 fix it. (If you prefer a clean build gate, proceed to Task 2 before building.)
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Data/Models/PrimeDays.cs src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs
|
||||
git commit -m "feat(data): model Prime schedule as weekday bitmask"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Repository
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs`
|
||||
|
||||
- [ ] **Step 1: Update `ListAsync` ordering**
|
||||
|
||||
The old ordering used `StartDate`. Order by `TimeOfDay`:
|
||||
|
||||
```csharp
|
||||
public async Task<IReadOnlyList<PrimeScheduleEntity>> ListAsync(CancellationToken ct = default)
|
||||
{
|
||||
var rows = await _context.PrimeSchedules.AsNoTracking()
|
||||
.OrderBy(s => s.TimeOfDay)
|
||||
.ToListAsync(ct);
|
||||
return rows;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update `UpsertAsync` field copy**
|
||||
|
||||
Replace the three removed-field assignments with `Days`:
|
||||
|
||||
```csharp
|
||||
else
|
||||
{
|
||||
existing.Days = entity.Days;
|
||||
existing.TimeOfDay = entity.TimeOfDay;
|
||||
existing.Enabled = entity.Enabled;
|
||||
existing.PromptOverride = entity.PromptOverride;
|
||||
}
|
||||
```
|
||||
|
||||
Leave `GetAsync`, `DeleteAsync`, `UpdateLastRunAsync` unchanged.
|
||||
|
||||
- [ ] **Step 3: Commit** (build verified after migration in Task 3)
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs
|
||||
git commit -m "feat(data): persist weekday bitmask in prime schedule repo"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: EF migration
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ClaudeDo.Data/Migrations/<timestamp>_PrimeWeekdays.cs` (generated)
|
||||
- Modify: `src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs` (generated)
|
||||
|
||||
- [ ] **Step 1: Generate the migration**
|
||||
|
||||
Run from repo root:
|
||||
|
||||
```bash
|
||||
dotnet ef migrations add PrimeWeekdays --project src/ClaudeDo.Data/ClaudeDo.Data.csproj --startup-project src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
||||
```
|
||||
|
||||
Expected: a new `*_PrimeWeekdays.cs` file and an updated snapshot. (If `dotnet ef` is unavailable, hand-write the migration using the body below.)
|
||||
|
||||
- [ ] **Step 2: Replace the generated `Up` body with an explicit backfill**
|
||||
|
||||
EF's auto-generated drop/add would discard existing schedules' weekday intent. Edit the new migration's `Up` to add the column, backfill from `workdays_only`, then drop the old columns:
|
||||
|
||||
```csharp
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "days_of_week",
|
||||
table: "prime_schedules",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 31);
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE prime_schedules SET days_of_week = CASE WHEN workdays_only = 1 THEN 31 ELSE 127 END;");
|
||||
|
||||
migrationBuilder.DropColumn(name: "start_date", table: "prime_schedules");
|
||||
migrationBuilder.DropColumn(name: "end_date", table: "prime_schedules");
|
||||
migrationBuilder.DropColumn(name: "workdays_only", table: "prime_schedules");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Replace the generated `Down` body**
|
||||
|
||||
```csharp
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
name: "start_date", table: "prime_schedules",
|
||||
type: "TEXT", nullable: false, defaultValue: new DateOnly(2000, 1, 1));
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
name: "end_date", table: "prime_schedules",
|
||||
type: "TEXT", nullable: false, defaultValue: new DateOnly(2099, 12, 31));
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "workdays_only", table: "prime_schedules",
|
||||
type: "INTEGER", nullable: false, defaultValue: true);
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE prime_schedules SET workdays_only = CASE WHEN days_of_week = 127 THEN 0 ELSE 1 END;");
|
||||
|
||||
migrationBuilder.DropColumn(name: "days_of_week", table: "prime_schedules");
|
||||
}
|
||||
```
|
||||
|
||||
Add `using System;` at the top of the migration file if `DateOnly` defaults require it (the existing AddPrimeSchedules migration already imports `System`).
|
||||
|
||||
- [ ] **Step 4: Build the Data project**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Data/Migrations
|
||||
git commit -m "feat(data): migrate prime schedules to days_of_week bitmask"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Worker DTO + NextDueCalculator (TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs`
|
||||
- Modify: `src/ClaudeDo.Worker/Prime/NextDueCalculator.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Prime/NextDueCalculatorTests.cs`
|
||||
|
||||
- [ ] **Step 1: Update the Worker DTO**
|
||||
|
||||
`src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs`:
|
||||
|
||||
```csharp
|
||||
namespace ClaudeDo.Worker.Prime;
|
||||
|
||||
public sealed record PrimeScheduleDto(
|
||||
Guid Id,
|
||||
int Days,
|
||||
TimeSpan TimeOfDay,
|
||||
bool Enabled,
|
||||
DateTimeOffset? LastRunAt,
|
||||
string? PromptOverride);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Rewrite the calculator tests**
|
||||
|
||||
Replace the entire body of `tests/ClaudeDo.Worker.Tests/Prime/NextDueCalculatorTests.cs`. Note: 2026-05-05 is a Tuesday; 2026-05-08 is a Friday; 2026-05-09/10 are Sat/Sun; 2026-05-11 is a Monday.
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Worker.Prime;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Prime;
|
||||
|
||||
public class NextDueCalculatorTests
|
||||
{
|
||||
private static PrimeScheduleDto Schedule(
|
||||
PrimeDays days, TimeSpan time,
|
||||
bool enabled = true, DateTimeOffset? lastRun = null) =>
|
||||
new(Guid.NewGuid(), (int)days, time, enabled, lastRun, null);
|
||||
|
||||
[Fact]
|
||||
public void Disabled_Schedule_Returns_Null()
|
||||
{
|
||||
var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2));
|
||||
var s = Schedule(PrimeDays.All, new(7, 0, 0), enabled: false);
|
||||
Assert.Null(NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void No_Days_Selected_Returns_Null()
|
||||
{
|
||||
var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2));
|
||||
var s = Schedule(PrimeDays.None, new(7, 0, 0));
|
||||
Assert.Null(NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Future_Same_Day_Returns_Today_At_Target()
|
||||
{
|
||||
var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2)); // Tue
|
||||
var s = Schedule(PrimeDays.All, new(7, 0, 0));
|
||||
var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal(new DateTimeOffset(2026, 5, 5, 7, 0, 0, now.Offset), r!.At);
|
||||
Assert.False(r.FireImmediately);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Within_CatchUp_Window_Fires_Immediately()
|
||||
{
|
||||
var now = new DateTimeOffset(2026, 5, 5, 7, 15, 0, TimeSpan.FromHours(2));
|
||||
var s = Schedule(PrimeDays.All, new(7, 0, 0));
|
||||
var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
|
||||
Assert.NotNull(r);
|
||||
Assert.True(r!.FireImmediately);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Past_CatchUp_Window_Skips_To_Next_Eligible_Day()
|
||||
{
|
||||
var now = new DateTimeOffset(2026, 5, 5, 9, 0, 0, TimeSpan.FromHours(2)); // Tue
|
||||
var s = Schedule(PrimeDays.All, new(7, 0, 0));
|
||||
var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal(new DateOnly(2026, 5, 6), DateOnly.FromDateTime(r!.At.LocalDateTime));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Weekdays_Only_Skips_Weekend()
|
||||
{
|
||||
var now = new DateTimeOffset(2026, 5, 8, 8, 0, 0, TimeSpan.FromHours(2)); // Fri, past catch-up
|
||||
var s = Schedule(PrimeDays.Weekdays, new(7, 0, 0));
|
||||
var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal(DayOfWeek.Monday, r!.At.LocalDateTime.DayOfWeek);
|
||||
Assert.Equal(new DateOnly(2026, 5, 11), DateOnly.FromDateTime(r.At.LocalDateTime));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Single_Day_Schedule_Targets_That_Weekday()
|
||||
{
|
||||
var now = new DateTimeOffset(2026, 5, 5, 8, 0, 0, TimeSpan.FromHours(2)); // Tue, past catch-up
|
||||
var s = Schedule(PrimeDays.Friday, new(7, 0, 0));
|
||||
var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal(DayOfWeek.Friday, r!.At.LocalDateTime.DayOfWeek);
|
||||
Assert.Equal(new DateOnly(2026, 5, 8), DateOnly.FromDateTime(r.At.LocalDateTime));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Already_Fired_Today_Skips_To_Next_Eligible_Day()
|
||||
{
|
||||
var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2));
|
||||
var lastRun = new DateTimeOffset(2026, 5, 5, 7, 1, 0, TimeSpan.FromHours(2));
|
||||
var s = Schedule(PrimeDays.All, new(7, 0, 0), lastRun: lastRun);
|
||||
var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal(new DateOnly(2026, 5, 6), DateOnly.FromDateTime(r!.At.LocalDateTime));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Multiple_Schedules_Returns_Earliest()
|
||||
{
|
||||
var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2));
|
||||
var early = Schedule(PrimeDays.All, new(7, 0, 0));
|
||||
var late = Schedule(PrimeDays.All, new(9, 0, 0));
|
||||
var r = NextDueCalculator.Compute(new[] { late, early }, now, TimeSpan.FromMinutes(30));
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal(early.Id, r!.Schedule.Id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter FullyQualifiedName~NextDueCalculatorTests`
|
||||
Expected: FAIL — `PrimeScheduleDto` no longer has `StartDate`/`EndDate`/`workdaysOnly`, and the calculator still references them (compile errors).
|
||||
|
||||
- [ ] **Step 4: Rewrite the calculator**
|
||||
|
||||
Replace the entire body of `src/ClaudeDo.Worker/Prime/NextDueCalculator.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data.Models;
|
||||
|
||||
namespace ClaudeDo.Worker.Prime;
|
||||
|
||||
public sealed record NextDue(PrimeScheduleDto Schedule, DateTimeOffset At, bool FireImmediately);
|
||||
|
||||
public static class NextDueCalculator
|
||||
{
|
||||
public static NextDue? Compute(
|
||||
IEnumerable<PrimeScheduleDto> schedules,
|
||||
DateTimeOffset now,
|
||||
TimeSpan catchUp)
|
||||
{
|
||||
NextDue? best = null;
|
||||
foreach (var s in schedules)
|
||||
{
|
||||
if (!s.Enabled) continue;
|
||||
var due = ComputeFor(s, now, catchUp);
|
||||
if (due is null) continue;
|
||||
if (best is null || due.At < best.At) best = due;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private static NextDue? ComputeFor(PrimeScheduleDto s, DateTimeOffset now, TimeSpan catchUp)
|
||||
{
|
||||
if ((PrimeDays)s.Days == PrimeDays.None) return null;
|
||||
|
||||
var todayLocal = DateOnly.FromDateTime(now.LocalDateTime);
|
||||
var alreadyFiredToday = s.LastRunAt is { } last &&
|
||||
DateOnly.FromDateTime(last.LocalDateTime) == todayLocal;
|
||||
|
||||
if (!alreadyFiredToday && IsEligibleDay(s, todayLocal))
|
||||
{
|
||||
var todayTarget = ToOffset(todayLocal, s.TimeOfDay, now.Offset);
|
||||
if (todayTarget >= now)
|
||||
return new NextDue(s, todayTarget, false);
|
||||
if (now <= todayTarget + catchUp)
|
||||
return new NextDue(s, now, true);
|
||||
}
|
||||
|
||||
var d = todayLocal.AddDays(1);
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
if (IsEligibleDay(s, d))
|
||||
return new NextDue(s, ToOffset(d, s.TimeOfDay, now.Offset), false);
|
||||
d = d.AddDays(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsEligibleDay(PrimeScheduleDto s, DateOnly d) =>
|
||||
((PrimeDays)s.Days & ToFlag(d.DayOfWeek)) != PrimeDays.None;
|
||||
|
||||
private static PrimeDays ToFlag(DayOfWeek dow) => dow switch
|
||||
{
|
||||
DayOfWeek.Monday => PrimeDays.Monday,
|
||||
DayOfWeek.Tuesday => PrimeDays.Tuesday,
|
||||
DayOfWeek.Wednesday => PrimeDays.Wednesday,
|
||||
DayOfWeek.Thursday => PrimeDays.Thursday,
|
||||
DayOfWeek.Friday => PrimeDays.Friday,
|
||||
DayOfWeek.Saturday => PrimeDays.Saturday,
|
||||
DayOfWeek.Sunday => PrimeDays.Sunday,
|
||||
_ => PrimeDays.None,
|
||||
};
|
||||
|
||||
private static DateTimeOffset ToOffset(DateOnly day, TimeSpan time, TimeSpan offset) =>
|
||||
new(day.Year, day.Month, day.Day, time.Hours, time.Minutes, time.Seconds, offset);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the calculator tests**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter FullyQualifiedName~NextDueCalculatorTests`
|
||||
Expected: still FAILS to build — `PrimeScheduler.ToDto` and `WorkerHub` mappings reference removed fields. Proceed to Tasks 5–6, then re-run.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs src/ClaudeDo.Worker/Prime/NextDueCalculator.cs tests/ClaudeDo.Worker.Tests/Prime/NextDueCalculatorTests.cs
|
||||
git commit -m "feat(worker): compute prime due-time from weekday bitmask"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: PrimeScheduler.ToDto + scheduler tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Prime/PrimeScheduler.cs:104-105`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Prime/PrimeSchedulerTests.cs`
|
||||
|
||||
- [ ] **Step 1: Update the `ToDto` mapping**
|
||||
|
||||
Replace the `ToDto` method in `PrimeScheduler.cs`:
|
||||
|
||||
```csharp
|
||||
private static PrimeScheduleDto ToDto(Data.Models.PrimeScheduleEntity e) =>
|
||||
new(e.Id, (int)e.Days, e.TimeOfDay, e.Enabled, e.LastRunAt, e.PromptOverride);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update scheduler test fixtures**
|
||||
|
||||
In `tests/ClaudeDo.Worker.Tests/Prime/PrimeSchedulerTests.cs`, every `new PrimeScheduleEntity { ... }` initializer sets `StartDate`/`EndDate`/`WorkdaysOnly`. Replace those three lines in each of the three initializers (lines ~48-52, ~89-94, ~131-136) with a single `Days` assignment. Each initializer becomes:
|
||||
|
||||
```csharp
|
||||
await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
|
||||
{
|
||||
Id = id,
|
||||
Days = PrimeDays.All,
|
||||
TimeOfDay = new TimeSpan(7, 0, 0),
|
||||
Enabled = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
```
|
||||
|
||||
Add `using ClaudeDo.Data.Models;` to the file's usings if not already present (it is, via line 1).
|
||||
|
||||
- [ ] **Step 3: Run scheduler + calculator tests**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~Prime"`
|
||||
Expected: still build-fails until `WorkerHub` (Task 6) compiles. After Task 6, this command must PASS.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Prime/PrimeScheduler.cs tests/ClaudeDo.Worker.Tests/Prime/PrimeSchedulerTests.cs
|
||||
git commit -m "test(worker): adapt prime scheduler tests to weekday model"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: WorkerHub mapping + repository tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs:488-518`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs`
|
||||
|
||||
- [ ] **Step 1: Update `ListPrimeSchedules`**
|
||||
|
||||
```csharp
|
||||
public async Task<List<PrimeScheduleDto>> ListPrimeSchedules()
|
||||
{
|
||||
using var ctx = _dbFactory.CreateDbContext();
|
||||
var rows = await new PrimeScheduleRepository(ctx).ListAsync();
|
||||
return rows.Select(e => new PrimeScheduleDto(
|
||||
e.Id, (int)e.Days, e.TimeOfDay, e.Enabled, e.LastRunAt, e.PromptOverride)).ToList();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update `UpsertPrimeSchedule`**
|
||||
|
||||
```csharp
|
||||
public async Task<PrimeScheduleDto> UpsertPrimeSchedule(PrimeScheduleDto dto)
|
||||
{
|
||||
using var ctx = _dbFactory.CreateDbContext();
|
||||
var repo = new PrimeScheduleRepository(ctx);
|
||||
var existing = await repo.GetAsync(dto.Id);
|
||||
var entity = new ClaudeDo.Data.Models.PrimeScheduleEntity
|
||||
{
|
||||
Id = dto.Id == Guid.Empty ? Guid.NewGuid() : dto.Id,
|
||||
Days = (ClaudeDo.Data.Models.PrimeDays)dto.Days,
|
||||
TimeOfDay = dto.TimeOfDay,
|
||||
Enabled = dto.Enabled,
|
||||
PromptOverride = dto.PromptOverride,
|
||||
CreatedAt = existing?.CreatedAt ?? DateTimeOffset.UtcNow,
|
||||
LastRunAt = existing?.LastRunAt,
|
||||
};
|
||||
await repo.UpsertAsync(entity);
|
||||
_primeSignal.Signal();
|
||||
return new PrimeScheduleDto(entity.Id, (int)entity.Days, entity.TimeOfDay,
|
||||
entity.Enabled, entity.LastRunAt, entity.PromptOverride);
|
||||
}
|
||||
```
|
||||
|
||||
`DeletePrimeSchedule` is unchanged.
|
||||
|
||||
- [ ] **Step 3: Update repository tests**
|
||||
|
||||
In `tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs`, replace each entity initializer's `StartDate`/`EndDate`/`WorkdaysOnly` lines with `Days = PrimeDays.Weekdays,` (drop them where only `StartDate`/`EndDate` appear). The three initializers become:
|
||||
|
||||
```csharp
|
||||
// Upsert_Then_List_RoundTrips
|
||||
await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
|
||||
{
|
||||
Id = id,
|
||||
Days = PrimeDays.Weekdays,
|
||||
TimeOfDay = new TimeSpan(7, 0, 0),
|
||||
Enabled = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
```
|
||||
|
||||
```csharp
|
||||
// UpdateLastRunAt_Persists
|
||||
await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
|
||||
{
|
||||
Id = id,
|
||||
Days = PrimeDays.Weekdays,
|
||||
TimeOfDay = new TimeSpan(7, 0, 0),
|
||||
Enabled = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Delete_Removes_Row
|
||||
await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
|
||||
{
|
||||
Id = id,
|
||||
Days = PrimeDays.All,
|
||||
TimeOfDay = TimeSpan.Zero,
|
||||
Enabled = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
```
|
||||
|
||||
Add an assertion in `Upsert_Then_List_RoundTrips` after the existing time assertion:
|
||||
|
||||
```csharp
|
||||
Assert.Equal(PrimeDays.Weekdays, rows[0].Days);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build worker + run all worker tests**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj && dotnet test tests/ClaudeDo.Worker.Tests`
|
||||
Expected: PASS (all Prime + repository tests green).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs
|
||||
git commit -m "feat(worker): map prime schedule weekday bitmask over the hub"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: UI DTO + ViewModels + tests (TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs`
|
||||
|
||||
- [ ] **Step 1: Update the UI DTO**
|
||||
|
||||
`src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs` (keep `PrimeFiredEvent` unchanged):
|
||||
|
||||
```csharp
|
||||
namespace ClaudeDo.Ui.Services;
|
||||
|
||||
public sealed record PrimeScheduleDto(
|
||||
Guid Id,
|
||||
int Days,
|
||||
TimeSpan TimeOfDay,
|
||||
bool Enabled,
|
||||
DateTimeOffset? LastRunAt,
|
||||
string? PromptOverride);
|
||||
|
||||
public sealed record PrimeFiredEvent(
|
||||
Guid ScheduleId,
|
||||
bool Success,
|
||||
string Message,
|
||||
DateTimeOffset FiredAt);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Rewrite the row VM**
|
||||
|
||||
Replace the body of `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Ui.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
||||
{
|
||||
private const int Mon = 1, Tue = 2, Wed = 4, Thu = 8, Fri = 16, Sat = 32, Sun = 64;
|
||||
|
||||
public Guid Id { get; }
|
||||
public bool IsExisting { get; }
|
||||
|
||||
[ObservableProperty] private bool _enabled;
|
||||
[ObservableProperty] private bool _monday;
|
||||
[ObservableProperty] private bool _tuesday;
|
||||
[ObservableProperty] private bool _wednesday;
|
||||
[ObservableProperty] private bool _thursday;
|
||||
[ObservableProperty] private bool _friday;
|
||||
[ObservableProperty] private bool _saturday;
|
||||
[ObservableProperty] private bool _sunday;
|
||||
[ObservableProperty] private TimeSpan _timeOfDay;
|
||||
[ObservableProperty] private DateTimeOffset? _lastRunAt;
|
||||
|
||||
public string LastRunLabel => LastRunAt is { } v ? v.LocalDateTime.ToString("g") : "—";
|
||||
|
||||
partial void OnLastRunAtChanged(DateTimeOffset? value) => OnPropertyChanged(nameof(LastRunLabel));
|
||||
|
||||
public PrimeScheduleRowViewModel(PrimeScheduleDto dto, bool isExisting)
|
||||
{
|
||||
Id = dto.Id;
|
||||
IsExisting = isExisting;
|
||||
Enabled = dto.Enabled;
|
||||
Monday = (dto.Days & Mon) != 0;
|
||||
Tuesday = (dto.Days & Tue) != 0;
|
||||
Wednesday = (dto.Days & Wed) != 0;
|
||||
Thursday = (dto.Days & Thu) != 0;
|
||||
Friday = (dto.Days & Fri) != 0;
|
||||
Saturday = (dto.Days & Sat) != 0;
|
||||
Sunday = (dto.Days & Sun) != 0;
|
||||
TimeOfDay = dto.TimeOfDay;
|
||||
LastRunAt = dto.LastRunAt;
|
||||
}
|
||||
|
||||
public int DaysMask()
|
||||
{
|
||||
int m = 0;
|
||||
if (Monday) m |= Mon;
|
||||
if (Tuesday) m |= Tue;
|
||||
if (Wednesday) m |= Wed;
|
||||
if (Thursday) m |= Thu;
|
||||
if (Friday) m |= Fri;
|
||||
if (Saturday) m |= Sat;
|
||||
if (Sunday) m |= Sun;
|
||||
return m;
|
||||
}
|
||||
|
||||
public PrimeScheduleDto ToDto() =>
|
||||
new(Id, DaysMask(), TimeOfDay, Enabled, LastRunAt, null);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update the tab VM**
|
||||
|
||||
In `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs`, replace `Validate` and `AddSchedule`:
|
||||
|
||||
```csharp
|
||||
public string? Validate()
|
||||
{
|
||||
foreach (var r in Rows)
|
||||
{
|
||||
if (r.DaysMask() == 0)
|
||||
return $"Schedule {r.TimeOfDay:hh\\:mm}: select at least one day.";
|
||||
if (r.TimeOfDay < TimeSpan.Zero || r.TimeOfDay >= TimeSpan.FromDays(1))
|
||||
return "Time must be between 00:00 and 23:59.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
[RelayCommand]
|
||||
private void AddSchedule()
|
||||
{
|
||||
var dto = new PrimeScheduleDto(
|
||||
Id: Guid.NewGuid(),
|
||||
Days: 31, // Mon–Fri
|
||||
TimeOfDay: new TimeSpan(7, 0, 0),
|
||||
Enabled: true,
|
||||
LastRunAt: null,
|
||||
PromptOverride: null);
|
||||
Rows.Add(new PrimeScheduleRowViewModel(dto, isExisting: false));
|
||||
}
|
||||
```
|
||||
|
||||
`LoadAsync`, `SaveAsync`, `RemoveSchedule`, `ApplyFiredEvent` are unchanged.
|
||||
|
||||
- [ ] **Step 4: Rewrite the tab VM tests**
|
||||
|
||||
Replace the body of `tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class PrimeClaudeTabViewModelTests
|
||||
{
|
||||
private sealed class FakeApi : IPrimeScheduleApi
|
||||
{
|
||||
public List<PrimeScheduleDto> Stored { get; } = new();
|
||||
public List<PrimeScheduleDto> Upserts { get; } = new();
|
||||
public List<Guid> Deletes { get; } = new();
|
||||
public Task<List<PrimeScheduleDto>> ListAsync() => Task.FromResult(Stored.ToList());
|
||||
public Task<PrimeScheduleDto?> UpsertAsync(PrimeScheduleDto dto)
|
||||
{
|
||||
Upserts.Add(dto);
|
||||
return Task.FromResult<PrimeScheduleDto?>(dto);
|
||||
}
|
||||
public Task DeleteAsync(Guid id) { Deletes.Add(id); return Task.CompletedTask; }
|
||||
}
|
||||
|
||||
private static PrimeScheduleDto Dto(Guid id, int days, TimeSpan time) =>
|
||||
new(id, days, time, true, null, null);
|
||||
|
||||
[Fact]
|
||||
public async Task Load_Populates_Rows()
|
||||
{
|
||||
var api = new FakeApi();
|
||||
api.Stored.Add(Dto(Guid.NewGuid(), 31, new TimeSpan(7, 0, 0)));
|
||||
var vm = new PrimeClaudeTabViewModel(api);
|
||||
await vm.LoadAsync();
|
||||
Assert.Single(vm.Rows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSchedule_Appends_Row_With_Defaults()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
Assert.Single(vm.Rows);
|
||||
Assert.True(vm.Rows[0].Enabled);
|
||||
Assert.True(vm.Rows[0].Monday);
|
||||
Assert.True(vm.Rows[0].Friday);
|
||||
Assert.False(vm.Rows[0].Saturday);
|
||||
Assert.Equal(new TimeSpan(7, 0, 0), vm.Rows[0].TimeOfDay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Row_Decomposes_And_Recomposes_Days()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
var row = vm.Rows[0];
|
||||
Assert.Equal(31, row.DaysMask());
|
||||
row.Saturday = true;
|
||||
Assert.Equal(63, row.DaysMask());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Save_Diffs_New_And_Removed_Rows()
|
||||
{
|
||||
var api = new FakeApi();
|
||||
var keptId = Guid.NewGuid();
|
||||
var deletedId = Guid.NewGuid();
|
||||
api.Stored.Add(Dto(keptId, 31, new TimeSpan(7, 0, 0)));
|
||||
api.Stored.Add(Dto(deletedId, 31, new TimeSpan(8, 0, 0)));
|
||||
|
||||
var vm = new PrimeClaudeTabViewModel(api);
|
||||
await vm.LoadAsync();
|
||||
vm.RemoveScheduleCommand.Execute(vm.Rows.Single(r => r.Id == deletedId));
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
|
||||
await vm.SaveAsync();
|
||||
|
||||
Assert.Contains(deletedId, api.Deletes);
|
||||
Assert.Equal(2, api.Upserts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Reports_No_Days_Selected()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
var row = vm.Rows[0];
|
||||
row.Monday = row.Tuesday = row.Wednesday = row.Thursday = row.Friday = false;
|
||||
Assert.NotNull(vm.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Passes_With_One_Day()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
Assert.Null(vm.Validate());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run UI tests**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter FullyQualifiedName~PrimeClaudeTabViewModelTests`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs
|
||||
git commit -m "feat(ui): drive prime schedule rows from weekday toggles"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: XAML — toggle-button row
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Design/IslandStyles.axaml`
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`
|
||||
|
||||
- [ ] **Step 1: Add a `day-toggle` style class**
|
||||
|
||||
Append to `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (inside the root `<Styles>` element, alongside the other style selectors). Uses existing dynamic-resource tokens — no hardcoded colors:
|
||||
|
||||
```xml
|
||||
<Style Selector="ToggleButton.day-toggle">
|
||||
<Setter Property="MinWidth" Value="34"/>
|
||||
<Setter Property="Padding" Value="6,4"/>
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Background" Value="{DynamicResource DeepBrush}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource LineBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
</Style>
|
||||
<Style Selector="ToggleButton.day-toggle:checked /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||
</Style>
|
||||
```
|
||||
|
||||
If `AccentBrush` is not a defined token, use the brush the project uses for primary/selected affordances (check the `primary` button style in this file and reuse that brush). Final visual pass is the user's.
|
||||
|
||||
- [ ] **Step 2: Replace the Prime row template**
|
||||
|
||||
In `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`, replace the `<Grid ...>` inside the Prime `DataTemplate` (currently columns `Auto,*,Auto,Auto,Auto,Auto` with the `ThemedDatePicker` and Mon–Fri checkbox) with:
|
||||
|
||||
```xml
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" ColumnSpacing="8">
|
||||
<CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
|
||||
<ToggleButton Classes="day-toggle" Content="Mo" IsChecked="{Binding Monday, Mode=TwoWay}"/>
|
||||
<ToggleButton Classes="day-toggle" Content="Tu" IsChecked="{Binding Tuesday, Mode=TwoWay}"/>
|
||||
<ToggleButton Classes="day-toggle" Content="We" IsChecked="{Binding Wednesday, Mode=TwoWay}"/>
|
||||
<ToggleButton Classes="day-toggle" Content="Th" IsChecked="{Binding Thursday, Mode=TwoWay}"/>
|
||||
<ToggleButton Classes="day-toggle" Content="Fr" IsChecked="{Binding Friday, Mode=TwoWay}"/>
|
||||
<ToggleButton Classes="day-toggle" Content="Sa" IsChecked="{Binding Saturday, Mode=TwoWay}"/>
|
||||
<ToggleButton Classes="day-toggle" Content="Su" IsChecked="{Binding Sunday, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
<TextBox Grid.Column="2" Width="64"
|
||||
Text="{Binding TimeOfDay, Mode=TwoWay, Converter={StaticResource TimeSpanToHhmm}}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Classes="meta" Grid.Column="3" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
|
||||
MinWidth="80"/>
|
||||
<Button Classes="icon-btn" Grid.Column="4" Content="✕"
|
||||
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update the explainer text**
|
||||
|
||||
Replace the intro `TextBlock` Text in the Prime tab (`SettingsModalView.axaml`):
|
||||
|
||||
```xml
|
||||
Text="Prime your Claude usage window by firing a single non-interactive ping on the days you choose, at a chosen time. Only runs while ClaudeDo is open. If the app starts within 30 minutes of the target time, the ping fires immediately."/>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Remove the now-unused range converter (only if unreferenced)**
|
||||
|
||||
The `DateOnlyToDateTime` resource on line 23 was used only by the range picker. Grep the file: if `DateOnlyToDateTime` has no other reference, remove the `<conv:DateOnlyToDateTimeConverter x:Key="DateOnlyToDateTime"/>` line. Keep `TimeSpanToHhmm` (still used).
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Manual UI check**
|
||||
|
||||
Start the worker, then the app. Open Settings → Prime Claude. Verify: a row shows 7 toggle buttons with Mon–Fri lit by default; toggling Sat/Sun persists after Save+reopen; clearing all days shows the validation error on Save. (UI correctness can only be confirmed in the running app — state so explicitly if it cannot be run.)
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Design/IslandStyles.axaml src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml
|
||||
git commit -m "feat(ui): replace prime date range with weekday toggle buttons"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Docs
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Data/CLAUDE.md`
|
||||
- Modify: `CLAUDE.md`
|
||||
|
||||
- [ ] **Step 1: Update the Data CLAUDE.md**
|
||||
|
||||
In `src/ClaudeDo.Data/CLAUDE.md`, the Models section has no PrimeSchedule line today; add one under Models, and confirm the `prime_schedules` table mention in the Schema section stays accurate:
|
||||
|
||||
```markdown
|
||||
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, stored as `days_of_week` int), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on the selected weekdays; no date range.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update the root CLAUDE.md if Prime is described**
|
||||
|
||||
Grep `CLAUDE.md` for "Prime"; if there is a Prime description mentioning a date range, update it to "recurring weekday schedule". If there is no such line, make no change.
|
||||
|
||||
- [ ] **Step 3: Full test sweep**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests && dotnet test tests/ClaudeDo.Ui.Tests`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Data/CLAUDE.md CLAUDE.md
|
||||
git commit -m "docs: describe recurring-weekday Prime schedule"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Notes
|
||||
|
||||
- **Spec coverage:** data model (T1), scheduling logic (T4), UI toggles (T7–T8), migration+backfill (T3), both DTOs (T4/T7), tests (T4–T7), out-of-scope items excluded. ✓
|
||||
- **Type consistency:** entity `PrimeDays Days`; both DTOs `int Days`; hub/scheduler cast `(int)`/`(PrimeDays)` at boundaries; calculator casts `(PrimeDays)s.Days`; row VM exposes 7 bools + `DaysMask()`. ✓
|
||||
- **Build ripple:** a single type change breaks several projects at once, so some intermediate steps note expected build failures; the gating green builds are T3 Step 4 (Data), T6 Step 4 (Worker + tests), T8 Step 4 (App). ✓
|
||||
```
|
||||
517
docs/superpowers/plans/2026-06-03-daily-prep-live-view.md
Normal file
517
docs/superpowers/plans/2026-06-03-daily-prep-live-view.md
Normal file
@@ -0,0 +1,517 @@
|
||||
# Daily Prep — Live Output View + Clear Day — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Stream the daily-prep run's output into a live, human-readable view (a new mode in the Details island), and add a "Clear Day" button that empties MyDay.
|
||||
|
||||
**Architecture:** The worker broadcasts `PrepStarted/PrepLine/PrepFinished` over SignalR (mirroring `TaskStarted/TaskMessage/TaskFinished`). `PrimeRunner` forwards each Claude stdout line instead of discarding it. The UI `WorkerClient` re-raises these as events; `DetailsIslandViewModel` gains a `PrepLog` + `IsPrepMode` panel rendered with the existing terminal renderer. A `ClearMyDay` hub method bulk-clears `IsMyDay`. MyDay header gets "Vorbereitungs-Log" and "Tag leeren" buttons.
|
||||
|
||||
**Tech Stack:** .NET 8, ASP.NET Core SignalR, EF Core (SQLite), Avalonia + CommunityToolkit.Mvvm, xUnit.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-03-daily-prep-live-view-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Build & test commands
|
||||
|
||||
`.slnx` needs .NET 9; build/test individual csproj with `-c Release` (a running Worker may lock Debug).
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
UI cannot be GUI-smoke-tested headlessly — note that explicitly where it applies; the human verifies visuals.
|
||||
|
||||
## Reference anchors (verify before editing — line numbers drift)
|
||||
|
||||
- `src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs` — currently only `PrimeFiredAsync`.
|
||||
- `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs:13-57` — broadcast methods; `PrimeFired` at ~52-56.
|
||||
- `src/ClaudeDo.Worker/Prime/PrimeRunner.cs:31-79` — `FireAsync`; discard lambda at ~55-60; ctor at ~19-29.
|
||||
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs:542-549` — `RunDailyPrepNow` (uses `_broadcaster`); DailyNote CRUD at 559-583 (shows the db-context pattern this hub uses).
|
||||
- `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs:19` — `TaskMessageEvent`; `:55` — `RunDailyPrepNowAsync`.
|
||||
- `src/ClaudeDo.Ui/Services/WorkerClient.cs:99-122` — `TaskStarted/Finished/Message` hub.On; `:170-173` — `PrimeFired` hub.On (the pattern to copy).
|
||||
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` — `IsNotesMode` ~56, `Log` ~193, ctor/subscriptions ~272-337, `OnTaskMessage` ~339-363 (stdout→`StreamLineFormatter`→`Log`), `ShowNotes` ~478-483.
|
||||
- `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml:131-302` — body grid; task panel `IsVisible="{Binding !IsNotesMode}"`, notes panel `IsVisible="{Binding IsNotesMode}"`; `SessionTerminalView` embedded ~295.
|
||||
- `src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml:54-75` — `ItemsControl ItemsSource="{Binding Log}"` + the `LogLineViewModel` item template to reuse.
|
||||
- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` — `NotesRequested` ~29, `OpenNotesCommand`+`PrepareDayCommand` ~33-45, `ShowNotesRow`/`IsMyDayList` ~65-66, both set in `LoadForList` ~212-213.
|
||||
- `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml:69-84` — Notes + PrepareDay buttons (styling to copy).
|
||||
- `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs:199-201` — island event wiring; `:225` — `PrimeFired` subscription.
|
||||
- Fakes to keep in sync: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`, `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` (`FakeWorkerClient`).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Worker — prep output broadcast + streaming
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs`
|
||||
- Modify: `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`
|
||||
- Modify: `src/ClaudeDo.Worker/Prime/PrimeRunner.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs`
|
||||
|
||||
- [ ] **Step 1: Write the failing test.** Extend `PrimeRunnerTests` with a fake `IPrimeBroadcaster` that records calls. The fake `IClaudeProcess` should invoke `onStdoutLine` with two sample lines and return `RunResult { ExitCode = 0, ResultMarkdown = "ok" }`.
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task FireAsync_streams_started_lines_and_finished()
|
||||
{
|
||||
var broadcaster = new RecordingPrimeBroadcaster();
|
||||
var claude = new FakeClaudeProcess(emitLines: new[] { "{\"a\":1}", "{\"b\":2}" }, exitCode: 0, result: "ok");
|
||||
var runner = NewRunner(claude, broadcaster); // build with temp-sqlite dbFactory + fake clock + logger + broadcaster
|
||||
var schedule = new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null);
|
||||
|
||||
var outcome = await runner.FireAsync(schedule, CancellationToken.None);
|
||||
|
||||
Assert.True(outcome.Success);
|
||||
Assert.Equal(1, broadcaster.StartedCount);
|
||||
Assert.Equal(new[] { "{\"a\":1}", "{\"b\":2}" }, broadcaster.Lines);
|
||||
Assert.Single(broadcaster.FinishedResults);
|
||||
Assert.True(broadcaster.FinishedResults[0]);
|
||||
}
|
||||
```
|
||||
|
||||
`RecordingPrimeBroadcaster` implements `IPrimeBroadcaster`: `StartedCount`, `List<string> Lines`, `List<bool> FinishedResults`, and a no-op `PrimeFiredAsync`. If the existing `FakeClaudeProcess` cannot emit lines, add an optional `emitLines` parameter that loops `await onStdoutLine(line)` before returning.
|
||||
|
||||
- [ ] **Step 2: Run — expect FAIL** (interface methods + ctor param missing).
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter PrimeRunner
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Extend `IPrimeBroadcaster`:**
|
||||
|
||||
```csharp
|
||||
public interface IPrimeBroadcaster
|
||||
{
|
||||
Task PrimeFiredAsync(Guid scheduleId, bool success, string message, DateTimeOffset firedAt);
|
||||
Task PrepStartedAsync();
|
||||
Task PrepLineAsync(string line);
|
||||
Task PrepFinishedAsync(bool success);
|
||||
}
|
||||
```
|
||||
|
||||
(Keep the existing `PrimeFiredAsync` signature exactly as it is in the current file.)
|
||||
|
||||
- [ ] **Step 4: Implement in `HubBroadcaster`** (add next to `PrimeFired`):
|
||||
|
||||
```csharp
|
||||
public Task PrepStarted() => _hub.Clients.All.SendAsync("PrepStarted");
|
||||
public Task PrepLine(string line) => _hub.Clients.All.SendAsync("PrepLine", line);
|
||||
public Task PrepFinished(bool success) => _hub.Clients.All.SendAsync("PrepFinished", success);
|
||||
|
||||
Task IPrimeBroadcaster.PrepStartedAsync() => PrepStarted();
|
||||
Task IPrimeBroadcaster.PrepLineAsync(string line) => PrepLine(line);
|
||||
Task IPrimeBroadcaster.PrepFinishedAsync(bool success) => PrepFinished(success);
|
||||
```
|
||||
|
||||
(Match the existing explicit-interface style used for `PrimeFiredAsync`.)
|
||||
|
||||
- [ ] **Step 5: Wire `PrimeRunner`.** Add `IPrimeBroadcaster _broadcaster` as a ctor param (and field). Rewrite the body of `FireAsync` after the gate check to:
|
||||
|
||||
```csharp
|
||||
if (!await _gate.WaitAsync(0, ct))
|
||||
return new PrimeRunOutcome(false, "Daily prep already running");
|
||||
|
||||
var success = false;
|
||||
try
|
||||
{
|
||||
await _broadcaster.PrepStartedAsync();
|
||||
|
||||
var cwd = Paths.AppDataRoot();
|
||||
Directory.CreateDirectory(cwd);
|
||||
|
||||
int maxTasks;
|
||||
await using (var dbCtx = await _dbFactory.CreateDbContextAsync(ct))
|
||||
{
|
||||
var settings = await new AppSettingsRepository(dbCtx).GetAsync(ct);
|
||||
maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||
}
|
||||
|
||||
var today = DateOnly.FromDateTime(_clock.Now.LocalDateTime);
|
||||
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks, today);
|
||||
var args = DailyPrepPrompt.BuildArgs(MaxTurns);
|
||||
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeoutCts.CancelAfter(FireTimeout);
|
||||
|
||||
var result = await _claude.RunAsync(
|
||||
arguments: args,
|
||||
prompt: prompt,
|
||||
workingDirectory: cwd,
|
||||
onStdoutLine: line => _broadcaster.PrepLineAsync(line),
|
||||
ct: timeoutCts.Token);
|
||||
|
||||
success = result.IsSuccess;
|
||||
return success
|
||||
? new PrimeRunOutcome(true, "Daily prep complete")
|
||||
: new PrimeRunOutcome(false, $"exit code {result.ExitCode}");
|
||||
}
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalMinutes:0} min");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Daily prep run failed");
|
||||
return new PrimeRunOutcome(false, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _broadcaster.PrepFinishedAsync(success);
|
||||
_gate.Release();
|
||||
}
|
||||
```
|
||||
|
||||
DI is unchanged: `AddSingleton<IPrimeRunner, PrimeRunner>()` resolves `IPrimeBroadcaster` (registered as `sp => sp.GetRequiredService<HubBroadcaster>()`).
|
||||
|
||||
- [ ] **Step 6: Update existing `PrimeRunnerTests` ctor calls** to pass the recording broadcaster; build + run.
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter PrimeRunner
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Prime src/ClaudeDo.Worker/Hub/HubBroadcaster.cs tests/ClaudeDo.Worker.Tests/Prime
|
||||
git commit -m "feat(daily-prep): stream prep output via PrepStarted/PrepLine/PrepFinished"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Worker — `ClearMyDay` hub method
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||
- Test: a new/existing hub test under `tests/ClaudeDo.Worker.Tests/Hub/` (mirror an existing hub test that seeds a real SQLite db and constructs `WorkerHub`)
|
||||
|
||||
- [ ] **Step 1: Write the failing test.** Seed three tasks: two with `IsMyDay=true` (one Idle, one Done), one with `IsMyDay=false`. Construct `WorkerHub` the way existing hub tests do (the same `null!` argument list, plus a recording `HubBroadcaster`/clients). Call `ClearMyDay()`; assert both MyDay rows are now `false`, the third is untouched, and the returned count is 2.
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task ClearMyDay_clears_all_isMyDay_tasks()
|
||||
{
|
||||
// seed via the test's db helper ...
|
||||
var hub = NewHub(/* ... */);
|
||||
var cleared = await hub.ClearMyDay();
|
||||
|
||||
Assert.Equal(2, cleared);
|
||||
await using var ctx = NewContext();
|
||||
Assert.False(await ctx.Tasks.AnyAsync(t => t.IsMyDay));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run — expect FAIL.**
|
||||
|
||||
- [ ] **Step 3: Add the method** to `WorkerHub` (use the same db-context acquisition the neighbouring hub methods use — e.g. `_dbFactory`/repository field name found in the file — and the existing `_broadcaster` field):
|
||||
|
||||
```csharp
|
||||
public async Task<int> ClearMyDay()
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
var ids = await ctx.Tasks.Where(t => t.IsMyDay).Select(t => t.Id).ToListAsync();
|
||||
if (ids.Count == 0) return 0;
|
||||
|
||||
await ctx.Tasks.Where(t => t.IsMyDay)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(t => t.IsMyDay, false));
|
||||
|
||||
foreach (var id in ids)
|
||||
await _broadcaster.TaskUpdated(id);
|
||||
|
||||
return ids.Count;
|
||||
}
|
||||
```
|
||||
|
||||
If `WorkerHub` does not already have an `IDbContextFactory<ClaudeDoDbContext>` field, use whatever data-access dependency the other hub methods use (read the file). Do NOT add a new ctor param unless unavoidable (it would break hub-test fakes — if you must, update all `new WorkerHub(...)` call sites).
|
||||
|
||||
- [ ] **Step 4: Run — expect PASS.** Build Worker.
|
||||
|
||||
- [ ] **Step 5: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs tests/ClaudeDo.Worker.Tests/Hub
|
||||
git commit -m "feat(daily-prep): add ClearMyDay hub method"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: UI — WorkerClient prep events + ClearMyDayAsync
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||
- Modify fakes: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`, `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` (FakeWorkerClient)
|
||||
|
||||
- [ ] **Step 1: Declare on `IWorkerClient`** (near `TaskMessageEvent` / `RunDailyPrepNowAsync`):
|
||||
|
||||
```csharp
|
||||
event Action? PrepStartedEvent;
|
||||
event Action<string>? PrepLineEvent;
|
||||
event Action<bool>? PrepFinishedEvent;
|
||||
Task ClearMyDayAsync();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Implement in `WorkerClient`.** Add the events; register hub callbacks mirroring the `PrimeFired` registration (~line 170):
|
||||
|
||||
```csharp
|
||||
public event Action? PrepStartedEvent;
|
||||
public event Action<string>? PrepLineEvent;
|
||||
public event Action<bool>? PrepFinishedEvent;
|
||||
|
||||
// in the hub-wiring section:
|
||||
_hub.On("PrepStarted", () => Dispatcher.UIThread.Post(() => PrepStartedEvent?.Invoke()));
|
||||
_hub.On<string>("PrepLine", line => Dispatcher.UIThread.Post(() => PrepLineEvent?.Invoke(line)));
|
||||
_hub.On<bool>("PrepFinished", ok => Dispatcher.UIThread.Post(() => PrepFinishedEvent?.Invoke(ok)));
|
||||
|
||||
public Task ClearMyDayAsync() => _connection.InvokeAsync("ClearMyDay");
|
||||
```
|
||||
|
||||
(Use the exact connection field name and async-call style of neighbouring methods like `RunDailyPrepNowAsync` / `GenerateWeekReport`. `ClearMyDay` returns `int` on the hub; invoking it as a void `InvokeAsync("ClearMyDay")` is fine, or `InvokeAsync<int>` if you want the count.)
|
||||
|
||||
- [ ] **Step 3: Update the fakes.** Add the three events (as `public event …` auto-implemented) and `ClearMyDayAsync() => Task.CompletedTask` to both `StubWorkerClient` and `FakeWorkerClient`. For the ClearDay command test (Task 5), give `StubWorkerClient` a `ClearMyDayCalls` counter incremented in `ClearMyDayAsync`.
|
||||
|
||||
- [ ] **Step 4: Build App + both test projects; fix any remaining fake gaps.**
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Services tests
|
||||
git commit -m "feat(daily-prep): expose prep stream events and ClearMyDay on the UI worker client"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: UI — Details island prep mode + live log
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/...DetailsIslandViewModel...` (mirror existing Details VM tests; if none, add a small test file)
|
||||
|
||||
- [ ] **Step 1: Write the failing test.** Construct `DetailsIslandViewModel` with a `StubWorkerClient` (mirror existing construction). Then:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void PrepLine_event_appends_to_PrepLog()
|
||||
{
|
||||
var stub = new StubWorkerClient();
|
||||
var vm = NewDetailsVm(stub);
|
||||
|
||||
stub.RaisePrepLine("{\"type\":\"assistant\",\"text\":\"hi\"}"); // helper that invokes PrepLineEvent
|
||||
Assert.NotEmpty(vm.PrepLog);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowPrep_sets_prep_mode_and_clears_notes_mode()
|
||||
{
|
||||
var vm = NewDetailsVm(new StubWorkerClient());
|
||||
vm.ShowPrep();
|
||||
Assert.True(vm.IsPrepMode);
|
||||
Assert.False(vm.IsNotesMode);
|
||||
}
|
||||
```
|
||||
|
||||
Add `RaisePrepStarted/RaisePrepLine/RaisePrepFinished` helpers to `StubWorkerClient` that invoke the corresponding events.
|
||||
|
||||
- [ ] **Step 2: Run — expect FAIL.**
|
||||
|
||||
- [ ] **Step 3: Implement in `DetailsIslandViewModel`:**
|
||||
- Add `[ObservableProperty] private bool _isPrepMode;` and `[ObservableProperty] private bool _isPrepRunning;`.
|
||||
- Add `public ObservableCollection<LogLineViewModel> PrepLog { get; } = new();`.
|
||||
- In the ctor, subscribe: `_worker.PrepStartedEvent += OnPrepStarted; _worker.PrepLineEvent += OnPrepLine; _worker.PrepFinishedEvent += OnPrepFinished;` (guard with the same `_worker is not null` pattern used for other events).
|
||||
- Handlers:
|
||||
|
||||
```csharp
|
||||
private void OnPrepStarted()
|
||||
{
|
||||
PrepLog.Clear();
|
||||
IsPrepRunning = true;
|
||||
}
|
||||
|
||||
private void OnPrepLine(string line) => AppendStdoutLine(PrepLog, line);
|
||||
|
||||
private void OnPrepFinished(bool success) => IsPrepRunning = false;
|
||||
```
|
||||
|
||||
- Factor the stdout-formatting currently inside `OnTaskMessage` into a reusable
|
||||
`private void AppendStdoutLine(ObservableCollection<LogLineViewModel> target, string line)`
|
||||
that runs the line through `StreamLineFormatter` and appends `LogLineViewModel`(s).
|
||||
Have `OnTaskMessage`'s stdout branch call `AppendStdoutLine(Log, strippedLine)` so both
|
||||
paths share one implementation. (Events arrive already on the UI thread via
|
||||
`Dispatcher.UIThread.Post` in `WorkerClient`, so direct collection mutation is correct.)
|
||||
- Add `public void ShowPrep()` mirroring `ShowNotes()`: call `Bind(null)`, set
|
||||
`IsNotesMode = false`, `IsPrepMode = true`.
|
||||
- In `ShowNotes()` add `IsPrepMode = false`. In `Bind(...)` reset both `IsNotesMode` and
|
||||
`IsPrepMode` to false (find where `IsNotesMode` is reset; add `IsPrepMode` beside it).
|
||||
|
||||
- [ ] **Step 4: Update `DetailsIslandView.axaml`.**
|
||||
- Change the task-details panel visibility from `IsVisible="{Binding !IsNotesMode}"` to a
|
||||
converter-free multi-condition. Avalonia lacks `&&` in bindings, so add a computed
|
||||
property `public bool IsTaskDetailVisible => !IsNotesMode && !IsPrepMode;` to the VM
|
||||
(raise its change notification from the `OnIsNotesModeChanged`/`OnIsPrepModeChanged`
|
||||
partial methods generated by `[ObservableProperty]`) and bind the task panel to
|
||||
`IsVisible="{Binding IsTaskDetailVisible}"`.
|
||||
- Add a third panel after the notes panel:
|
||||
|
||||
```xml
|
||||
<Panel IsVisible="{Binding IsPrepMode}">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Margin="16,12"
|
||||
Text="{loc:Tr details.prepTitle}" Classes="h2"/>
|
||||
<ScrollViewer>
|
||||
<ItemsControl ItemsSource="{Binding PrepLog}"/>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Panel>
|
||||
```
|
||||
|
||||
The `ItemsControl` reuses the implicit `LogLineViewModel` `DataTemplate` that
|
||||
`SessionTerminalView` relies on. If that template is defined locally inside
|
||||
`SessionTerminalView.axaml` (not in a shared resource), either move it to a shared
|
||||
`ResourceDictionary` (e.g. App resources) and reference it from both, or set the
|
||||
`ItemsControl.ItemTemplate` to a copy of that template. Prefer sharing over copying.
|
||||
Add `details.prepTitle` ("Daily prep" / "Tagesvorbereitung") to both locale json files.
|
||||
|
||||
- [ ] **Step 5: Run UI tests — expect PASS; build App.**
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui src/ClaudeDo.Localization tests/ClaudeDo.Ui.Tests
|
||||
git commit -m "feat(daily-prep): add live prep-output mode to the Details island"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: UI — MyDay buttons + shell wiring
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`
|
||||
- Modify: `src/ClaudeDo.Localization/locales/en.json`, `de.json`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/UiVm/...` or `tests/ClaudeDo.Ui.Tests/...` (TasksIslandViewModel)
|
||||
|
||||
- [ ] **Step 1: Write the failing tests.**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task ClearDayCommand_calls_worker()
|
||||
{
|
||||
var stub = new StubWorkerClient();
|
||||
var vm = NewTasksVm(stub);
|
||||
await vm.ClearDayCommand.ExecuteAsync(null);
|
||||
Assert.Equal(1, stub.ClearMyDayCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PrepareDayCommand_raises_PrepRequested()
|
||||
{
|
||||
var vm = NewTasksVm(new StubWorkerClient());
|
||||
var raised = false;
|
||||
vm.PrepRequested += () => raised = true;
|
||||
await vm.PrepareDayCommand.ExecuteAsync(null);
|
||||
Assert.True(raised);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run — expect FAIL.**
|
||||
|
||||
- [ ] **Step 3: Implement in `TasksIslandViewModel`:**
|
||||
- Add `public event Action? PrepRequested;` next to `NotesRequested`.
|
||||
- In `PrepareDayAsync` (the existing `[RelayCommand]`), raise `PrepRequested?.Invoke();`
|
||||
in addition to the existing `RunDailyPrepNowAsync()` call.
|
||||
- Add:
|
||||
|
||||
```csharp
|
||||
[RelayCommand]
|
||||
private void ShowPrepLog() => PrepRequested?.Invoke();
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ClearDayAsync()
|
||||
{
|
||||
if (_worker is null) return;
|
||||
try { await _worker.ClearMyDayAsync(); }
|
||||
catch { /* worker offline; broadcast will reconcile on return */ }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add the two buttons** to the MyDay header in `TasksIslandView.axaml`,
|
||||
immediately after the existing "Prepare day" button (~line 84), copying its styling
|
||||
(`DockPanel.Dock="Top" Classes="btn" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left" Margin="16,0,16,8" IsVisible="{Binding IsMyDayList}"`):
|
||||
|
||||
```xml
|
||||
<Button DockPanel.Dock="Top" Classes="btn" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left" Margin="16,0,16,8"
|
||||
IsVisible="{Binding IsMyDayList}"
|
||||
Command="{Binding ShowPrepLogCommand}"
|
||||
Content="{loc:Tr tasks.prepLog}"/>
|
||||
<Button DockPanel.Dock="Top" Classes="btn" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left" Margin="16,0,16,8"
|
||||
IsVisible="{Binding IsMyDayList}"
|
||||
Command="{Binding ClearDayCommand}"
|
||||
Content="{loc:Tr tasks.clearDay}"/>
|
||||
```
|
||||
|
||||
Add `tasks.prepLog` (en "Prep log" / de "Vorbereitungs-Log") and `tasks.clearDay`
|
||||
(en "Clear day" / de "Tag leeren") to both locale json files.
|
||||
|
||||
- [ ] **Step 5: Wire the shell.** In `IslandsShellViewModel` where `Tasks.NotesRequested`
|
||||
is wired (~line 201), add:
|
||||
|
||||
```csharp
|
||||
Tasks.PrepRequested += () => Details.ShowPrep();
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run tests + build App.**
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Manual smoke (human, not headless):** start Worker + App, open MyDay, click
|
||||
"Tag vorbereiten" → Details island opens in prep mode and streams readable lines; click
|
||||
"Tag leeren" → MyDay empties; after a scheduled run, "Vorbereitungs-Log" opens the filled
|
||||
log. Confirm the three buttons only appear on MyDay.
|
||||
|
||||
- [ ] **Step 8: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui src/ClaudeDo.Localization tests
|
||||
git commit -m "feat(daily-prep): add Prep-log and Clear-day buttons to MyDay header"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final verification
|
||||
|
||||
- [ ] Build Worker + App (Release).
|
||||
- [ ] `dotnet test` Worker.Tests, Ui.Tests, Localization.Tests — all green.
|
||||
- [ ] Manual: prep streams live into the Details island (manual opens it; scheduled fills it silently, opened via the button); Clear Day empties MyDay immediately.
|
||||
|
||||
## Notes / risks
|
||||
|
||||
- Mode flags `IsNotesMode` / `IsPrepMode` are mutually exclusive; the task-details panel
|
||||
uses the computed `IsTaskDetailVisible`. Verify all three modes switch cleanly.
|
||||
- Reusing the `LogLineViewModel` template: prefer promoting it to a shared resource over
|
||||
copying, to avoid drift between the session terminal and the prep log.
|
||||
- `ClearMyDay` broadcasts one `TaskUpdated` per affected id; MyDay is small (capped), so
|
||||
this is fine.
|
||||
- Keep `PrimeRunner`'s "already running" early-return emitting no prep events.
|
||||
736
docs/superpowers/plans/2026-06-03-daily-prep.md
Normal file
736
docs/superpowers/plans/2026-06-03-daily-prep.md
Normal file
@@ -0,0 +1,736 @@
|
||||
# Daily Prep ("Prime Claude") Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Turn the Prime Time warm-up into a daily preparation where Claude reads open tasks and moves an effort-aware, capped subset into MyDay, triggered by the Prime schedule and a manual button.
|
||||
|
||||
**Architecture:** Agentic. Two new tools on the always-on `ExternalMcpService` (`get_daily_prep_candidates`, `set_my_day` with a server-side cap-guard). The existing `PrimeRunner` is rewritten to launch a headless `claude -p` run with a fixed parameterized prompt and `--allowedTools` for those two tools, relying on the already-registered `claudedo` MCP (no separate `--mcp-config`). A new `DailyPrepMaxTasks` app setting drives the cap. A manual hub method reuses the same runner with a single-flight guard.
|
||||
|
||||
**Tech Stack:** .NET 8, ASP.NET Core, EF Core (SQLite), SignalR, ModelContextProtocol, Avalonia (CommunityToolkit.Mvvm), xUnit.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-03-daily-prep-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Deviation from spec (deliberate, to minimize churn)
|
||||
|
||||
The spec proposed renaming `IPrimeRunner`/`PrimeRunner`/`PrimeScheduler` → `DailyPrep*`. **We keep the existing names and the `FireAsync(PrimeScheduleDto, ct)` signature** and only rewrite the runner body. This avoids touching the scheduler, DI registration, `IPrimeBroadcaster`, and the existing Prime tests for a pure rename. The per-schedule `PromptOverride` field becomes unused by the runner (left in the DB/UI untouched).
|
||||
|
||||
## Build & test commands (this repo)
|
||||
|
||||
`.slnx` needs .NET 9; on .NET 8 build/test individual projects. Use `-c Release` if a running Worker locks `Debug`.
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
Tests use **real SQLite + real git** (project convention). Mirror the setup already present in the test file you are extending.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
**Create**
|
||||
- `src/ClaudeDo.Data/Migrations/<timestamp>_DailyPrepMaxTasks.cs` (+ Designer, via `dotnet ef`)
|
||||
- `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` — pure prompt + args builder (easy to unit-test)
|
||||
|
||||
**Modify**
|
||||
- `src/ClaudeDo.Data/Models/AppSettingsEntity.cs` — add `DailyPrepMaxTasks`
|
||||
- `src/ClaudeDo.Data/Configuration/AppSettingsEntityConfiguration.cs` — map column
|
||||
- `src/ClaudeDo.Data/Repositories/AppSettingsRepository.cs` — persist field in `UpdateAsync`
|
||||
- `src/ClaudeDo.Worker/External/ExternalMcpService.cs` — add 2 tools + DTOs
|
||||
- `src/ClaudeDo.Worker/Prime/PrimeRunner.cs` — rewrite body to daily prep + single-flight
|
||||
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — add `DailyPrepMaxTasks` to AppSettings DTO + `RunDailyPrepNow`
|
||||
- `src/ClaudeDo.Ui/Services/WorkerClient.cs` — mirror `DailyPrepMaxTasks` in the UI AppSettings DTO + add `RunDailyPrepNow` call
|
||||
- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs` (+ its view) — numeric editor for `DailyPrepMaxTasks`
|
||||
- MyDay list header view + its ViewModel — "Tag vorbereiten" button + command
|
||||
|
||||
**Test**
|
||||
- `tests/ClaudeDo.Data.Tests/...AppSettings...` — new field persists / default 5
|
||||
- `tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs` — candidate filter + set_my_day + cap-guard
|
||||
- `tests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs` — prompt/args content
|
||||
- `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs` (if present) — single-flight + success/failure via `IClaudeProcess` fake
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `DailyPrepMaxTasks` app setting
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Data/Models/AppSettingsEntity.cs`
|
||||
- Modify: `src/ClaudeDo.Data/Configuration/AppSettingsEntityConfiguration.cs`
|
||||
- Modify: `src/ClaudeDo.Data/Repositories/AppSettingsRepository.cs`
|
||||
- Create (via `dotnet ef`): `src/ClaudeDo.Data/Migrations/<timestamp>_DailyPrepMaxTasks.cs`
|
||||
- Test: `tests/ClaudeDo.Data.Tests` (extend existing AppSettings repository test, or add `AppSettingsRepositoryTests.cs`)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
In a Data.Tests file (mirror the existing repo test harness that opens a real SQLite `ClaudeDoDbContext`):
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task DailyPrepMaxTasks_defaults_to_5_and_persists()
|
||||
{
|
||||
await using var ctx = NewContext(); // existing helper that migrates a temp sqlite db
|
||||
var repo = new AppSettingsRepository(ctx);
|
||||
|
||||
var initial = await repo.GetAsync();
|
||||
Assert.Equal(5, initial.DailyPrepMaxTasks);
|
||||
|
||||
initial.DailyPrepMaxTasks = 8;
|
||||
await repo.UpdateAsync(initial);
|
||||
|
||||
var reloaded = await repo.GetAsync();
|
||||
Assert.Equal(8, reloaded.DailyPrepMaxTasks);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it — expect FAIL** (`AppSettingsEntity` has no `DailyPrepMaxTasks`).
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release --filter DailyPrepMaxTasks_defaults_to_5_and_persists
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the property** to `AppSettingsEntity.cs` after `StandupWeekday`:
|
||||
|
||||
```csharp
|
||||
// Max number of open tasks the daily prep ("Prime Claude") may place in MyDay.
|
||||
public int DailyPrepMaxTasks { get; set; } = 5;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Map the column** in `AppSettingsEntityConfiguration.cs`, after the `StandupWeekday` mapping (before `builder.HasData(...)`):
|
||||
|
||||
```csharp
|
||||
builder.Property(s => s.DailyPrepMaxTasks)
|
||||
.HasColumnName("daily_prep_max_tasks").IsRequired().HasDefaultValue(5);
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Persist it** in `AppSettingsRepository.UpdateAsync`, after the `StandupWeekday` assignment:
|
||||
|
||||
```csharp
|
||||
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Generate the migration** (regenerates the model snapshot — do NOT hand-edit the snapshot):
|
||||
|
||||
```bash
|
||||
dotnet ef migrations add DailyPrepMaxTasks \
|
||||
-p src/ClaudeDo.Data/ClaudeDo.Data.csproj \
|
||||
-s src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
||||
```
|
||||
|
||||
Verify the generated `Up` contains an `AddColumn<int>("daily_prep_max_tasks", ... defaultValue: 5)` and an `UpdateData` setting the singleton row's `daily_prep_max_tasks` to 5. If `dotnet ef` is unavailable, hand-write the migration mirroring `20260603072822_WeeklyReport.cs` **and** add the matching `Property<int>("DailyPrepMaxTasks").HasColumnName("daily_prep_max_tasks")` line to `ClaudeDoDbContextModelSnapshot.cs` under the `AppSettingsEntity` builder.
|
||||
|
||||
- [ ] **Step 7: Run the test — expect PASS.**
|
||||
|
||||
- [ ] **Step 8: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Data tests/ClaudeDo.Data.Tests
|
||||
git commit -m "feat(daily-prep): add DailyPrepMaxTasks app setting"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `get_daily_prep_candidates` MCP tool
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/External/ExternalMcpService.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs`
|
||||
|
||||
Read `ExternalMcpServiceTests.cs` first and reuse its existing harness (how it builds an `ExternalMcpService` with a real SQLite context, `ListRepository`, `TaskRepository`, fake `HubBroadcaster`, etc.). The new tool reads **all** lists/tasks itself via the injected `_dbFactory`, so it needs no new constructor args.
|
||||
|
||||
- [ ] **Step 1: Write the failing test.** Seed: a list with `WorkingDir = @"D:\work\repo"` holding two `Idle` tasks (one blocked, one not) and one `Done` task; a second list with `WorkingDir = @"C:\Private\secret"` holding one `Idle` task; a third list with `WorkingDir = null` holding one `Idle` task; and one `Idle` task with `IsMyDay = true` in the first list. Set `AppSettings.ReportExcludedPaths = "[\"C:\\\\Private\"]"`.
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task GetDailyPrepCandidates_filters_by_status_block_and_excluded_repo()
|
||||
{
|
||||
// ... seed as described, using the file's existing seed helpers ...
|
||||
var svc = NewService();
|
||||
|
||||
var result = await svc.GetDailyPrepCandidates(CancellationToken.None);
|
||||
|
||||
// Only the non-blocked, Idle, non-MyDay task in the non-excluded repo is a candidate.
|
||||
Assert.Single(result.Candidates);
|
||||
Assert.Equal("idle-unblocked", result.Candidates[0].Id);
|
||||
// The Idle MyDay task is reported separately, not as a candidate.
|
||||
Assert.Single(result.CurrentMyDay);
|
||||
Assert.Equal(1, result.MaxTasks > 0 ? 1 : 1); // MaxTasks comes from AppSettings (default 5)
|
||||
Assert.Equal(5, result.MaxTasks);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it — expect FAIL** (method missing).
|
||||
|
||||
- [ ] **Step 3: Add the DTOs** near the other record declarations at the top of `ExternalMcpService.cs`:
|
||||
|
||||
```csharp
|
||||
public sealed record DailyPrepCandidateDto(
|
||||
string Id, string ListId, string ListName, string Title, string? Description,
|
||||
bool IsStarred, DateTime? ScheduledFor, DateTime CreatedAt);
|
||||
|
||||
public sealed record DailyPrepDataDto(
|
||||
int MaxTasks,
|
||||
IReadOnlyList<DailyPrepCandidateDto> Candidates,
|
||||
IReadOnlyList<DailyPrepCandidateDto> CurrentMyDay);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add the tool method** to the `ExternalMcpService` class body:
|
||||
|
||||
```csharp
|
||||
[McpServerTool, Description(
|
||||
"Daily prep: returns the open tasks eligible for today's MyDay selection. " +
|
||||
"candidates = Idle, not blocked, in a git repo not excluded from the weekly report, and not already in MyDay. " +
|
||||
"currentMyDay = Idle tasks already flagged IsMyDay (count them toward the cap). " +
|
||||
"maxTasks = the hard cap on total open MyDay tasks. Use set_my_day to add tasks (never exceed maxTasks).")]
|
||||
public async Task<DailyPrepDataDto> GetDailyPrepCandidates(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var settings = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
|
||||
var excludes = DailyPrepFilter.ParseExcludes(settings.ReportExcludedPaths);
|
||||
var maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||
|
||||
var idle = await ctx.Tasks
|
||||
.AsNoTracking()
|
||||
.Include(t => t.List)
|
||||
.Where(t => t.Status == TaskStatus.Idle)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var currentMyDay = idle
|
||||
.Where(t => t.IsMyDay)
|
||||
.OrderBy(t => t.SortOrder)
|
||||
.Select(ToCandidate)
|
||||
.ToList();
|
||||
|
||||
var candidates = idle
|
||||
.Where(t => !t.IsMyDay
|
||||
&& t.BlockedByTaskId == null
|
||||
&& DailyPrepFilter.IsIncludedRepo(t.List?.WorkingDir, excludes))
|
||||
.OrderBy(t => t.CreatedAt)
|
||||
.Select(ToCandidate)
|
||||
.ToList();
|
||||
|
||||
return new DailyPrepDataDto(maxTasks, candidates, currentMyDay);
|
||||
}
|
||||
|
||||
private static DailyPrepCandidateDto ToCandidate(TaskEntity t) => new(
|
||||
t.Id, t.ListId, t.List?.Name ?? "", t.Title, t.Description,
|
||||
t.IsStarred, t.ScheduledFor, t.CreatedAt);
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add the filter helper** as a small static class at the bottom of `ExternalMcpService.cs` (single-consumer helper lives beside its consumer, per repo convention):
|
||||
|
||||
```csharp
|
||||
internal static class DailyPrepFilter
|
||||
{
|
||||
public static string[] ParseExcludes(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return [];
|
||||
try
|
||||
{
|
||||
var list = System.Text.Json.JsonSerializer.Deserialize<List<string>>(json);
|
||||
return list is null ? [] : list.Select(Normalize).Where(p => p.Length > 0).ToArray();
|
||||
}
|
||||
catch (System.Text.Json.JsonException) { return []; }
|
||||
}
|
||||
|
||||
public static bool IsIncludedRepo(string? workingDir, string[] excludes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(workingDir)) return false; // not a repo → excluded
|
||||
var norm = Normalize(workingDir);
|
||||
return !excludes.Any(p => norm.StartsWith(p, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static string Normalize(string path) =>
|
||||
path.Trim().Replace('/', '\\').TrimEnd('\\');
|
||||
}
|
||||
```
|
||||
|
||||
Add `using ClaudeDo.Data.Repositories;` if not already present (it is, via existing usings).
|
||||
|
||||
- [ ] **Step 6: Run the test — expect PASS.**
|
||||
|
||||
- [ ] **Step 7: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/External/ExternalMcpService.cs tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs
|
||||
git commit -m "feat(daily-prep): add get_daily_prep_candidates MCP tool"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: `set_my_day` MCP tool with cap-guard
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/External/ExternalMcpService.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests.**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task SetMyDay_sets_flag_and_sort_order()
|
||||
{
|
||||
var svc = NewService();
|
||||
var id = await SeedIdleTask("My task"); // existing/added helper returning task id
|
||||
|
||||
var dto = await svc.SetMyDay(id, isMyDay: true, sortOrder: 3, CancellationToken.None);
|
||||
|
||||
Assert.True(dto.IsMyDay);
|
||||
Assert.Equal(3, dto.SortOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetMyDay_rejects_when_cap_reached()
|
||||
{
|
||||
// AppSettings.DailyPrepMaxTasks = 1 (set in seed)
|
||||
var svc = NewService();
|
||||
var first = await SeedIdleTask("a");
|
||||
var second = await SeedIdleTask("b");
|
||||
await svc.SetMyDay(first, true, null, CancellationToken.None);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => svc.SetMyDay(second, true, null, CancellationToken.None));
|
||||
Assert.Contains("limit", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetMyDay_unset_is_always_allowed()
|
||||
{
|
||||
var svc = NewService();
|
||||
var id = await SeedIdleTask("a");
|
||||
await svc.SetMyDay(id, true, null, CancellationToken.None);
|
||||
|
||||
var dto = await svc.SetMyDay(id, false, null, CancellationToken.None);
|
||||
Assert.False(dto.IsMyDay);
|
||||
}
|
||||
```
|
||||
|
||||
`SetMyDay` returns the existing `TaskDto`. Add a `SortOrder` field to `TaskDto` — see Step 3a. (`SeedIdleTask` / the `DailyPrepMaxTasks=1` seed reuse the file's existing seeding helpers.)
|
||||
|
||||
- [ ] **Step 2: Run — expect FAIL.**
|
||||
|
||||
- [ ] **Step 3a: Add `SortOrder` to `TaskDto`** (record + `ToDto`) so the result reflects ordering:
|
||||
|
||||
In the `TaskDto` record add `int SortOrder` as the last positional member, and in `ToDto(TaskEntity t)` add `t.SortOrder` as the last argument. (Update any test that constructs `TaskDto` positionally — search the test project.)
|
||||
|
||||
- [ ] **Step 3b: Add the tool method:**
|
||||
|
||||
```csharp
|
||||
[McpServerTool, Description(
|
||||
"Daily prep: set or clear a task's MyDay flag, optionally setting its sortOrder " +
|
||||
"(use consecutive sortOrder values to keep related tasks together). " +
|
||||
"Setting isMyDay=true is rejected if it would exceed the MyDay cap (DailyPrepMaxTasks open MyDay tasks); " +
|
||||
"clearing (isMyDay=false) is always allowed.")]
|
||||
public async Task<TaskDto> SetMyDay(
|
||||
string taskId,
|
||||
bool isMyDay,
|
||||
int? sortOrder,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var task = await ctx.Tasks.FirstOrDefaultAsync(t => t.Id == taskId, cancellationToken)
|
||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||
|
||||
if (isMyDay && !task.IsMyDay)
|
||||
{
|
||||
var settings = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
|
||||
var max = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||
var openMyDay = await ctx.Tasks.CountAsync(
|
||||
t => t.IsMyDay && t.Status == TaskStatus.Idle, cancellationToken);
|
||||
if (openMyDay >= max)
|
||||
throw new InvalidOperationException(
|
||||
$"MyDay limit {max} reached. Clear a task before adding another.");
|
||||
}
|
||||
|
||||
task.IsMyDay = isMyDay;
|
||||
if (sortOrder is not null) task.SortOrder = sortOrder.Value;
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
return ToDto(task);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run — expect PASS.**
|
||||
|
||||
- [ ] **Step 5: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/External/ExternalMcpService.cs tests/ClaudeDo.Worker.Tests
|
||||
git commit -m "feat(daily-prep): add set_my_day MCP tool with cap-guard"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Rewrite `PrimeRunner` to run the daily prep
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs`
|
||||
- Modify: `src/ClaudeDo.Worker/Prime/PrimeRunner.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs`, and extend `PrimeRunnerTests.cs` if it exists
|
||||
|
||||
The runner needs the cap `X` (read from `AppSettings`) and today's date. Inject `IDbContextFactory<ClaudeDoDbContext>` into `PrimeRunner` (it is resolvable in the main app DI) and an `IPrimeClock` for the date (already registered).
|
||||
|
||||
- [ ] **Step 1: Write failing prompt/args tests.**
|
||||
|
||||
```csharp
|
||||
public class DailyPrepPromptTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_prompt_contains_cap_and_date()
|
||||
{
|
||||
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks: 5, today: new DateOnly(2026, 6, 3));
|
||||
Assert.Contains("5", prompt);
|
||||
Assert.Contains("2026-06-03", prompt);
|
||||
Assert.Contains("get_daily_prep_candidates", prompt);
|
||||
Assert.Contains("set_my_day", prompt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_args_allows_only_the_two_tools()
|
||||
{
|
||||
var args = DailyPrepPrompt.BuildArgs(maxTurns: 30);
|
||||
Assert.Contains("--output-format stream-json", args);
|
||||
Assert.Contains("--max-turns 30", args);
|
||||
Assert.Contains("--allowedTools", args);
|
||||
Assert.Contains("mcp__claudedo__get_daily_prep_candidates", args);
|
||||
Assert.Contains("mcp__claudedo__set_my_day", args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run — expect FAIL.**
|
||||
|
||||
- [ ] **Step 3: Create `DailyPrepPrompt.cs`:**
|
||||
|
||||
```csharp
|
||||
namespace ClaudeDo.Worker.Prime;
|
||||
|
||||
public static class DailyPrepPrompt
|
||||
{
|
||||
public const string CandidatesTool = "mcp__claudedo__get_daily_prep_candidates";
|
||||
public const string SetMyDayTool = "mcp__claudedo__set_my_day";
|
||||
|
||||
public static string BuildArgs(int maxTurns) =>
|
||||
"-p --output-format stream-json --verbose --permission-mode acceptEdits " +
|
||||
$"--max-turns {maxTurns} " +
|
||||
$"--allowedTools {CandidatesTool} {SetMyDayTool}";
|
||||
|
||||
public static string BuildPrompt(int maxTasks, DateOnly today) =>
|
||||
$"""
|
||||
Du bereitest meinen Arbeitstag fuer {today:yyyy-MM-dd} vor.
|
||||
|
||||
1. Rufe {CandidatesTool} auf.
|
||||
2. Behalte bereits als MyDay markierte offene Tasks (currentMyDay) — entferne sie nicht.
|
||||
3. Fuelle bis maximal {maxTasks} offene Tasks GESAMT in MyDay auf (currentMyDay zaehlt mit). Niemals mehr.
|
||||
4. Schaetze pro Kandidat grob den Aufwand und waehle eine machbare Mischung (nicht nur Grossbrocken).
|
||||
Priorisiere isStarred, faellige (scheduledFor) und aeltere Tasks.
|
||||
5. Lege thematisch verwandte Tasks durch aufeinanderfolgende sortOrder-Werte nebeneinander.
|
||||
6. Setze die Auswahl via {SetMyDayTool}(taskId, true, sortOrder). Markiere nichts ausserhalb der Kandidatenliste.
|
||||
|
||||
Wenn es keine Kandidaten gibt, tue nichts.
|
||||
""";
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run prompt tests — expect PASS.**
|
||||
|
||||
- [ ] **Step 5: Rewrite `PrimeRunner.cs`:**
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Runner;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Worker.Prime;
|
||||
|
||||
public sealed class PrimeRunner : IPrimeRunner
|
||||
{
|
||||
private static readonly TimeSpan FireTimeout = TimeSpan.FromMinutes(5);
|
||||
private const int MaxTurns = 30;
|
||||
|
||||
private readonly IClaudeProcess _claude;
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly IPrimeClock _clock;
|
||||
private readonly ILogger<PrimeRunner> _logger;
|
||||
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||
|
||||
public PrimeRunner(
|
||||
IClaudeProcess claude,
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
IPrimeClock clock,
|
||||
ILogger<PrimeRunner> logger)
|
||||
{
|
||||
_claude = claude;
|
||||
_dbFactory = dbFactory;
|
||||
_clock = clock;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<PrimeRunOutcome> FireAsync(PrimeScheduleDto schedule, CancellationToken ct)
|
||||
{
|
||||
if (!await _gate.WaitAsync(0, ct))
|
||||
return new PrimeRunOutcome(false, "Daily prep already running");
|
||||
|
||||
try
|
||||
{
|
||||
var cwd = Paths.AppDataRoot();
|
||||
Directory.CreateDirectory(cwd);
|
||||
|
||||
int maxTasks;
|
||||
await using (var dbCtx = await _dbFactory.CreateDbContextAsync(ct))
|
||||
{
|
||||
var settings = await new AppSettingsRepository(dbCtx).GetAsync(ct);
|
||||
maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||
}
|
||||
|
||||
var today = DateOnly.FromDateTime(_clock.Now.LocalDateTime);
|
||||
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks, today);
|
||||
var args = DailyPrepPrompt.BuildArgs(MaxTurns);
|
||||
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeoutCts.CancelAfter(FireTimeout);
|
||||
|
||||
var result = await _claude.RunAsync(
|
||||
arguments: args,
|
||||
prompt: prompt,
|
||||
workingDirectory: cwd,
|
||||
onStdoutLine: _ => Task.CompletedTask,
|
||||
ct: timeoutCts.Token);
|
||||
|
||||
return result.IsSuccess
|
||||
? new PrimeRunOutcome(true, "Daily prep complete")
|
||||
: new PrimeRunOutcome(false, $"exit code {result.ExitCode}");
|
||||
}
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalMinutes:0} min");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Daily prep run failed");
|
||||
return new PrimeRunOutcome(false, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Fix the DI registration is unchanged** (`AddSingleton<IPrimeRunner, PrimeRunner>()` already works — the new ctor deps `IDbContextFactory` and `IPrimeClock` are registered). Build the Worker.
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Update/extend `PrimeRunnerTests.cs`** (if present) to match the new ctor: construct `PrimeRunner` with a fake `IClaudeProcess`, a real temp-SQLite `IDbContextFactory`, a fake `IPrimeClock`, and a logger. Add:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task FireAsync_returns_already_running_when_gate_held()
|
||||
{
|
||||
var runner = NewRunner(claudeDelay: TimeSpan.FromSeconds(2));
|
||||
var schedule = new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null);
|
||||
|
||||
var first = runner.FireAsync(schedule, CancellationToken.None);
|
||||
var second = await runner.FireAsync(schedule, CancellationToken.None);
|
||||
|
||||
Assert.False(second.Success);
|
||||
Assert.Contains("already running", second.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await first;
|
||||
}
|
||||
```
|
||||
|
||||
If no `PrimeRunnerTests.cs` exists, create one. The fake `IClaudeProcess` should optionally delay (to keep the gate held) and return a successful `RunResult { ExitCode = 0, ResultMarkdown = "ok" }`.
|
||||
|
||||
- [ ] **Step 8: Run — expect PASS.**
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "DailyPrepPrompt|PrimeRunner"
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Prime tests/ClaudeDo.Worker.Tests/Prime
|
||||
git commit -m "feat(daily-prep): run daily prep from PrimeRunner via allowed MCP tools"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Hub — `RunDailyPrepNow` + expose `DailyPrepMaxTasks`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||
|
||||
Read `WorkerHub.cs` first. It already exposes a `GetAppSettings`/`UpdateAppSettings` pair backed by a DTO record (the one carrying `ReportExcludedPaths`, `StandupWeekday`).
|
||||
|
||||
- [ ] **Step 1: Add `DailyPrepMaxTasks` to the hub AppSettings DTO record** (the record near the top of `WorkerHub.cs` that lists `ReportExcludedPaths`). Add `int DailyPrepMaxTasks` as a member. In the read mapping (`GetAppSettings`, where `row.ReportExcludedPaths` is read) add `row.DailyPrepMaxTasks`; in the write mapping (`UpdateAppSettings`, where `ReportExcludedPaths = dto.ReportExcludedPaths`) add `DailyPrepMaxTasks = dto.DailyPrepMaxTasks`.
|
||||
|
||||
- [ ] **Step 2: Add the hub method.** Inject `IPrimeRunner` and `HubBroadcaster` if the hub does not already have them (the hub is constructed by SignalR via DI; both are registered singletons). Then:
|
||||
|
||||
```csharp
|
||||
public async Task<bool> RunDailyPrepNow()
|
||||
{
|
||||
var schedule = new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null);
|
||||
var firedAt = DateTimeOffset.Now;
|
||||
var outcome = await _primeRunner.FireAsync(schedule, Context.ConnectionAborted);
|
||||
await _broadcaster.PrimeFired(Guid.Empty, outcome.Success, outcome.Message, firedAt);
|
||||
return outcome.Success;
|
||||
}
|
||||
```
|
||||
|
||||
Add `using ClaudeDo.Worker.Prime;` to `WorkerHub.cs` if missing.
|
||||
|
||||
> **Caution (memory):** changing the `WorkerHub` constructor breaks hand-rolled hub-test fakes in `ClaudeDo.Worker.Tests` and possibly `ClaudeDo.Ui.Tests`. After editing, build the test projects and fix every `new WorkerHub(...)` / fake `IWorkerClient` construction the compiler flags.
|
||||
|
||||
- [ ] **Step 3: Mirror the DTO in the UI** (`WorkerClient.cs`, the AppSettings DTO around line 498): add `int DailyPrepMaxTasks` to the record (same position as in the hub DTO). Add a `RunDailyPrepNow` client call:
|
||||
|
||||
```csharp
|
||||
public Task<bool> RunDailyPrepNowAsync() =>
|
||||
_connection.InvokeAsync<bool>("RunDailyPrepNow");
|
||||
```
|
||||
|
||||
(Match the exact connection field/name and the async-wrapper style used by neighbouring calls like `GenerateWeekReport`.)
|
||||
|
||||
- [ ] **Step 4: Build Worker + App + test projects; fix any broken fakes.**
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Ui/Services/WorkerClient.cs tests
|
||||
git commit -m "feat(daily-prep): add RunDailyPrepNow hub method and expose DailyPrepMaxTasks"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Settings UI — edit `DailyPrepMaxTasks`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs`
|
||||
- Modify: the Prime Claude tab markup in `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs` (load/save wiring, where other AppSettings fields are mapped)
|
||||
|
||||
Read these three files first; mirror how an existing numeric AppSetting (e.g. `MaxParallelExecutions` or `WorktreeAutoCleanupDays`) is loaded from the hub DTO, bound, and saved back.
|
||||
|
||||
- [ ] **Step 1: Add an observable property** to `PrimeClaudeTabViewModel.cs`:
|
||||
|
||||
```csharp
|
||||
[ObservableProperty] private int _dailyPrepMaxTasks = 5;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Wire load/save** in `SettingsModalViewModel.cs`: where the AppSettings DTO is read into the tabs, set `PrimeClaude.DailyPrepMaxTasks = dto.DailyPrepMaxTasks;`. Where the DTO is written, include `DailyPrepMaxTasks = PrimeClaude.DailyPrepMaxTasks`. (Use the exact tab property name for the Prime Claude tab in that VM.)
|
||||
|
||||
- [ ] **Step 3: Add the editor** in the Prime Claude tab of `SettingsModalView.axaml`, near the schedule list:
|
||||
|
||||
```xml
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<TextBlock Text="{x:Static loc:L.Settings_DailyPrepMaxTasks}" VerticalAlignment="Center"/>
|
||||
<NumericUpDown Minimum="1" Maximum="50" Increment="1" Width="100"
|
||||
Value="{Binding PrimeClaude.DailyPrepMaxTasks}"/>
|
||||
</StackPanel>
|
||||
```
|
||||
|
||||
Add the `Settings_DailyPrepMaxTasks` key to both `locales/en.json` and `locales/de.json` (en: "Max tasks per day", de: "Max. Aufgaben pro Tag"). If the tab does not use localized labels yet, use a plain `Text="Max tasks per day"` string to match its current style.
|
||||
|
||||
- [ ] **Step 4: Build the App; smoke-build the UI.**
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui src/ClaudeDo.Localization
|
||||
git commit -m "feat(daily-prep): add DailyPrepMaxTasks editor to Prime Claude settings"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: MyDay header — "Tag vorbereiten" button
|
||||
|
||||
**Files:**
|
||||
- Modify: the ViewModel backing the MyDay list view (the one that exposes the smart-list header/toolbar; find it under `src/ClaudeDo.Ui/ViewModels/Islands/` — likely the tasks/list island VM that has access to `IWorkerClient`)
|
||||
- Modify: the corresponding view (`.axaml`) that renders the list header
|
||||
|
||||
Read the island VM + view first. Find where the active list is known to be `smart:my-day` so the button can be shown only there (mirror any existing conditional header content). The VM already holds a worker-client reference used by other commands (e.g. RunNow) — reuse it.
|
||||
|
||||
- [ ] **Step 1: Add the command** to the island VM:
|
||||
|
||||
```csharp
|
||||
[RelayCommand]
|
||||
private async Task PrepareDayAsync()
|
||||
{
|
||||
await _workerClient.RunDailyPrepNowAsync();
|
||||
}
|
||||
```
|
||||
|
||||
(Use the VM's existing worker-client field name. The MyDay list refreshes automatically via the `TaskUpdated` broadcast the tools emit, so no manual reload is needed.)
|
||||
|
||||
- [ ] **Step 2: Add an `IsMyDayList` (or reuse existing selected-list) guard** so the button only appears on the MyDay smart list. If the VM already exposes the selected list id, add:
|
||||
|
||||
```csharp
|
||||
public bool IsMyDayList => SelectedListId == "smart:my-day";
|
||||
```
|
||||
|
||||
and raise its change notification wherever `SelectedListId` changes (mirror existing patterns; if a `[NotifyPropertyChangedFor]` or manual `OnPropertyChanged` is already used for the selection, add this property to it).
|
||||
|
||||
- [ ] **Step 3: Add the button** to the list header in the view, visible only on MyDay:
|
||||
|
||||
```xml
|
||||
<Button Content="{x:Static loc:L.MyDay_PrepareDay}"
|
||||
Command="{Binding PrepareDayCommand}"
|
||||
IsVisible="{Binding IsMyDayList}"/>
|
||||
```
|
||||
|
||||
Add `MyDay_PrepareDay` to `locales/en.json` ("Prepare day") and `locales/de.json` ("Tag vorbereiten"), or a plain string if the view is not localized.
|
||||
|
||||
- [ ] **Step 4: Build the App.**
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Manual smoke (cannot be unit-tested):** start the Worker and App, open MyDay, click "Tag vorbereiten", confirm tasks appear (capped) and the button is hidden on other lists. Report results explicitly — do not claim UI success without running it.
|
||||
|
||||
- [ ] **Step 6: Commit.**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui src/ClaudeDo.Localization
|
||||
git commit -m "feat(daily-prep): add Prepare-day button to MyDay header"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final verification
|
||||
|
||||
- [ ] `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
- [ ] `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||
- [ ] `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release`
|
||||
- [ ] `dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release`
|
||||
- [ ] `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||
- [ ] End-to-end manual run: schedule fires (or button) → Claude calls the two tools → MyDay gets a capped subset; re-run keeps existing MyDay and tops up without exceeding the cap.
|
||||
|
||||
## Notes / risks
|
||||
|
||||
- Relies on the globally registered `claudedo` MCP (installer `RegisterMcpStep`). If absent, the prep run produces 0 changes — acceptable for v1.
|
||||
- `--permission-mode acceptEdits` + explicit `--allowedTools` pre-approves exactly the two tools so the headless run never blocks on a permission prompt.
|
||||
- The cap-guard counts `Idle && IsMyDay` tasks; it is the source of truth for the "never move everything in" invariant regardless of Claude's behavior.
|
||||
- Future phase (out of scope): external ticket sources (Jira) feed into `get_daily_prep_candidates` behind a task-source abstraction.
|
||||
1481
docs/superpowers/plans/2026-06-03-localization.md
Normal file
1481
docs/superpowers/plans/2026-06-03-localization.md
Normal file
File diff suppressed because it is too large
Load Diff
2311
docs/superpowers/plans/2026-06-03-weekly-report.md
Normal file
2311
docs/superpowers/plans/2026-06-03-weekly-report.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,994 @@
|
||||
# Approve = Merge → Done + Conflict Preview — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Approving a `WaitingForReview` task merges its worktree into the target branch first and only marks the task `Done` on a clean merge; conflicts keep it in review and are surfaced. Add a non-destructive "merges cleanly / conflicts" indicator and a direct single-task Merge button.
|
||||
|
||||
**Architecture:** A new `GitService.PreviewMergeAsync` probes mergeability via `git merge-tree --write-tree` (no working-tree mutation). `TaskMergeService` gains `PreviewAsync` and `ApproveAndMergeAsync` (merge first, then delegate the `Done` flip to `ITaskStateService`). `WorkerHub` exposes `PreviewMerge` and a result-returning `ApproveReview(taskId, targetBranch)`. The UI loads merge targets whenever a worktree exists, shows the preview, and reacts to conflict results.
|
||||
|
||||
**Tech Stack:** .NET 8, Avalonia, EF Core/SQLite, SignalR, xUnit with real git (`GitRepoFixture`) and real SQLite (`DbFixture`).
|
||||
|
||||
**Conventions for the implementer:**
|
||||
- Use the **sonnet** model.
|
||||
- **Stage files explicitly by path** — never `git add -A` (parallel sessions leave unrelated WIP).
|
||||
- Build with `-c Release` (a running Worker locks `Debug` output).
|
||||
- Conventional Commit messages: `type(scope): description`.
|
||||
- New UI strings use **plain English literals** to match the surrounding merge controls (no `loc:Tr`) — this avoids Localization.Tests parity churn.
|
||||
- Ignore anything under `.claude/worktrees/` — those are stale worktrees, not the build tree.
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/ClaudeDo.Data/Git/GitService.cs` | Add `MergePreview` record + `PreviewMergeAsync` + `CountChangedFilesAsync` |
|
||||
| `src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs` | Inject `ITaskStateService`; add `MergePreviewResult` + `PreviewAsync` + `ApproveAndMergeAsync` |
|
||||
| `src/ClaudeDo.Worker/Hub/WorkerHub.cs` | Add `MergePreviewDto` + `PreviewMerge`; change `ApproveReview` to `(taskId, targetBranch) → MergeResultDto` |
|
||||
| `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs` | Change `ApproveReviewAsync`; add `PreviewMergeAsync`, `MergeTaskAsync` |
|
||||
| `src/ClaudeDo.Ui/Services/WorkerClient.cs` | Implement the above; add UI `MergePreviewDto` record |
|
||||
| `src/ClaudeDo.Ui/ViewModels/Islands/MergePreviewPresenter.cs` | New pure presenter (text + color flags) |
|
||||
| `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` | Load targets for worktree tasks; preview props; approve conflict handling; `MergeCommand` |
|
||||
| `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` | Update the list-level approve call to new signature |
|
||||
| `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml` | Mergeability status line + Merge button |
|
||||
| `tests/ClaudeDo.Worker.Tests/Runner/GitServicePreviewMergeTests.cs` | New — git-backed preview tests |
|
||||
| `tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs` | Update `BuildService`; add preview + approve-merge tests |
|
||||
| `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` | Update `FakeWorkerClient` |
|
||||
| `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs` | Update fake |
|
||||
| `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs` | Update the `ApproveReviewAsync` override |
|
||||
| `tests/ClaudeDo.Ui.Tests/ViewModels/MergePreviewPresenterTests.cs` | New — presenter unit tests |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: GitService non-destructive merge probe
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Data/Git/GitService.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Runner/GitServicePreviewMergeTests.cs` (create)
|
||||
|
||||
Behaviour verified on git 2.50: `git merge-tree --write-tree --name-only <target> <source>` exits `0` when clean (stdout = a single tree-OID line) and `1` on conflict (stdout = tree-OID line, then conflicted file names, then a blank line, then informational messages). It writes only loose objects — the working tree, index, and refs are untouched.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Create `tests/ClaudeDo.Worker.Tests/Runner/GitServicePreviewMergeTests.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Runner;
|
||||
|
||||
public class GitServicePreviewMergeTests : IDisposable
|
||||
{
|
||||
private readonly List<GitRepoFixture> _repos = new();
|
||||
private GitRepoFixture NewRepo() { var r = new GitRepoFixture(); _repos.Add(r); return r; }
|
||||
public void Dispose() { foreach (var r in _repos) try { r.Dispose(); } catch { } }
|
||||
|
||||
[Fact]
|
||||
public async Task PreviewMergeAsync_NonConflicting_ReportsCleanWithChangedCount()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
var repo = NewRepo();
|
||||
var git = new GitService();
|
||||
var baseBranch = await git.GetCurrentBranchAsync(repo.RepoDir);
|
||||
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "checkout", "-b", "feature");
|
||||
File.WriteAllText(Path.Combine(repo.RepoDir, "newfile.txt"), "x\n");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "feat");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "checkout", baseBranch);
|
||||
|
||||
var preview = await git.PreviewMergeAsync(repo.RepoDir, baseBranch, "feature", CancellationToken.None);
|
||||
|
||||
Assert.True(preview.Supported);
|
||||
Assert.True(preview.Clean);
|
||||
Assert.Empty(preview.ConflictFiles);
|
||||
|
||||
var count = await git.CountChangedFilesAsync(repo.RepoDir, baseBranch, "feature", CancellationToken.None);
|
||||
Assert.Equal(1, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PreviewMergeAsync_Conflicting_ReportsFilesAndDoesNotMutateTree()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
var repo = NewRepo();
|
||||
var git = new GitService();
|
||||
var baseBranch = await git.GetCurrentBranchAsync(repo.RepoDir);
|
||||
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "checkout", "-b", "feature");
|
||||
File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "# from feature\n");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "feat readme");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "checkout", baseBranch);
|
||||
File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "# from base\n");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "base readme");
|
||||
|
||||
var headBefore = GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim();
|
||||
|
||||
var preview = await git.PreviewMergeAsync(repo.RepoDir, baseBranch, "feature", CancellationToken.None);
|
||||
|
||||
Assert.True(preview.Supported);
|
||||
Assert.False(preview.Clean);
|
||||
Assert.Contains("README.md", preview.ConflictFiles);
|
||||
|
||||
// Non-destructive: HEAD unchanged, no mid-merge state.
|
||||
Assert.Equal(headBefore, GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim());
|
||||
Assert.False(await git.IsMidMergeAsync(repo.RepoDir));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests, verify they fail to compile**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter FullyQualifiedName~GitServicePreviewMergeTests`
|
||||
Expected: build error — `PreviewMergeAsync`/`CountChangedFilesAsync` do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the probe**
|
||||
|
||||
In `src/ClaudeDo.Data/Git/GitService.cs`, add this record just under `namespace ClaudeDo.Data.Git;`:
|
||||
|
||||
```csharp
|
||||
public sealed record MergePreview(bool Supported, bool Clean, IReadOnlyList<string> ConflictFiles);
|
||||
```
|
||||
|
||||
Add these methods inside the `GitService` class (e.g. after `ListConflictedFilesAsync`):
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Non-destructive mergeability probe via `git merge-tree --write-tree`. Writes only
|
||||
/// loose objects — the working tree, index, and refs are left untouched.
|
||||
/// </summary>
|
||||
public async Task<MergePreview> PreviewMergeAsync(
|
||||
string repoDir, string targetBranch, string sourceBranch, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, _) = await RunGitAsync(repoDir,
|
||||
["merge-tree", "--write-tree", "--name-only", targetBranch, sourceBranch], ct);
|
||||
|
||||
if (exitCode == 0)
|
||||
return new MergePreview(true, true, Array.Empty<string>());
|
||||
|
||||
if (exitCode == 1)
|
||||
{
|
||||
// stdout: <tree-oid>\n<file>\n...\n\n<informational messages>
|
||||
var lines = stdout.Split('\n');
|
||||
var files = new List<string>();
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i].TrimEnd('\r');
|
||||
if (string.IsNullOrWhiteSpace(line)) break;
|
||||
files.Add(line.Trim());
|
||||
}
|
||||
return new MergePreview(true, false, files);
|
||||
}
|
||||
|
||||
// Any other exit (e.g. git too old: "unknown option --write-tree").
|
||||
return new MergePreview(false, false, Array.Empty<string>());
|
||||
}
|
||||
|
||||
/// <summary>Count of files that differ on <paramref name="sourceBranch"/> since its merge base with the target.</summary>
|
||||
public async Task<int> CountChangedFilesAsync(
|
||||
string repoDir, string targetBranch, string sourceBranch, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, _) = await RunGitAsync(repoDir,
|
||||
["diff", "--name-only", $"{targetBranch}...{sourceBranch}"], ct);
|
||||
if (exitCode != 0) return 0;
|
||||
return stdout
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Count(s => s.Length > 0);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the tests, verify they pass**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter FullyQualifiedName~GitServicePreviewMergeTests`
|
||||
Expected: PASS (2 tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Data/Git/GitService.cs tests/ClaudeDo.Worker.Tests/Runner/GitServicePreviewMergeTests.cs
|
||||
git commit -m "feat(git): add non-destructive merge-tree conflict probe"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: TaskMergeService preview + approve-merge orchestration
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs`
|
||||
|
||||
`ApproveAndMergeAsync` merges first (reusing `MergeAsync`, `removeWorktree:false`) and only then delegates the `Done` flip to `ITaskStateService.ApproveReviewAsync` (the sole owner of Status writes). Conflicts/blocks return without flipping status. No DI cycle: `TaskStateService` and `PlanningChainCoordinator` do not depend on `TaskMergeService`.
|
||||
|
||||
- [ ] **Step 1: Update `BuildService` and add failing tests**
|
||||
|
||||
In `tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs`, replace the `BuildService` helper so it also constructs a real `TaskStateService` (existing merge tests still pass — they only inspect the merge service's own broadcaster proxy):
|
||||
|
||||
```csharp
|
||||
private static (TaskMergeService svc, MergeRecordingClientProxy proxy) BuildService(DbFixture db)
|
||||
{
|
||||
var fakeHub = new MergeRecordingHubContext();
|
||||
var broadcaster = new HubBroadcaster(fakeHub);
|
||||
var state = TaskStateServiceBuilder.Build(db.CreateFactory()).State;
|
||||
var svc = new TaskMergeService(
|
||||
db.CreateFactory(),
|
||||
new GitService(),
|
||||
broadcaster,
|
||||
state,
|
||||
NullLogger<TaskMergeService>.Instance);
|
||||
return (svc, fakeHub.Proxy);
|
||||
}
|
||||
```
|
||||
|
||||
Add these tests to the class:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task PreviewAsync_CleanWorktree_ReturnsClean()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
var repo = NewRepo();
|
||||
var db = NewDb();
|
||||
var (list, task) = await SeedListAndTask(db, repo.RepoDir, TaskStatus.WaitingForReview);
|
||||
|
||||
var wtMgr = BuildWorktreeManager(db);
|
||||
var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));
|
||||
File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "added.txt"), "x\n");
|
||||
await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
|
||||
|
||||
var (svc, _) = BuildService(db);
|
||||
var target = await new GitService().GetCurrentBranchAsync(repo.RepoDir);
|
||||
|
||||
var preview = await svc.PreviewAsync(task.Id, target, CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.PreviewClean, preview.Status);
|
||||
Assert.True(preview.ChangedFileCount >= 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PreviewAsync_Conflict_ReturnsConflictFiles()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
var repo = NewRepo();
|
||||
var db = NewDb();
|
||||
var (list, task) = await SeedListAndTask(db, repo.RepoDir, TaskStatus.WaitingForReview);
|
||||
|
||||
var wtMgr = BuildWorktreeManager(db);
|
||||
var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));
|
||||
File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "README.md"), "# from worktree\n");
|
||||
await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
|
||||
|
||||
File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "# from main\n");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "main edit");
|
||||
|
||||
var (svc, _) = BuildService(db);
|
||||
var target = await new GitService().GetCurrentBranchAsync(repo.RepoDir);
|
||||
|
||||
var preview = await svc.PreviewAsync(task.Id, target, CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.PreviewConflict, preview.Status);
|
||||
Assert.Contains("README.md", preview.ConflictFiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PreviewAsync_NoActiveWorktree_ReturnsUnavailable()
|
||||
{
|
||||
var db = NewDb();
|
||||
var (_, task) = await SeedListAndTask(db, workingDir: "/tmp", status: TaskStatus.WaitingForReview);
|
||||
var (svc, _) = BuildService(db);
|
||||
|
||||
var preview = await svc.PreviewAsync(task.Id, "main", CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.PreviewUnavailable, preview.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApproveAndMergeAsync_CleanWorktree_MergesAndMarksDone()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
var repo = NewRepo();
|
||||
var db = NewDb();
|
||||
var (list, task) = await SeedListAndTask(db, repo.RepoDir, TaskStatus.WaitingForReview);
|
||||
|
||||
var wtMgr = BuildWorktreeManager(db);
|
||||
var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));
|
||||
File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "added.txt"), "new\n");
|
||||
await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
|
||||
|
||||
var (svc, _) = BuildService(db);
|
||||
var target = await new GitService().GetCurrentBranchAsync(repo.RepoDir);
|
||||
|
||||
var result = await svc.ApproveAndMergeAsync(task.Id, target, CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.StatusMerged, result.Status);
|
||||
using var ctx = db.CreateContext();
|
||||
var updated = await new TaskRepository(ctx).GetByIdAsync(task.Id);
|
||||
Assert.Equal(TaskStatus.Done, updated!.Status);
|
||||
var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(task.Id);
|
||||
Assert.Equal(WorktreeState.Merged, wt!.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApproveAndMergeAsync_Conflict_LeavesTaskWaitingForReview()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
var repo = NewRepo();
|
||||
var db = NewDb();
|
||||
var (list, task) = await SeedListAndTask(db, repo.RepoDir, TaskStatus.WaitingForReview);
|
||||
|
||||
var wtMgr = BuildWorktreeManager(db);
|
||||
var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));
|
||||
File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "README.md"), "# from worktree\n");
|
||||
await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
|
||||
|
||||
File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "# from main\n");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "main edit");
|
||||
var headBefore = GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim();
|
||||
|
||||
var (svc, _) = BuildService(db);
|
||||
var target = await new GitService().GetCurrentBranchAsync(repo.RepoDir);
|
||||
|
||||
var result = await svc.ApproveAndMergeAsync(task.Id, target, CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.StatusConflict, result.Status);
|
||||
Assert.Contains("README.md", result.ConflictFiles);
|
||||
|
||||
using var ctx = db.CreateContext();
|
||||
var updated = await new TaskRepository(ctx).GetByIdAsync(task.Id);
|
||||
Assert.Equal(TaskStatus.WaitingForReview, updated!.Status);
|
||||
var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(task.Id);
|
||||
Assert.Equal(WorktreeState.Active, wt!.State);
|
||||
Assert.Equal(headBefore, GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim());
|
||||
Assert.False(await new GitService().IsMidMergeAsync(repo.RepoDir));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApproveAndMergeAsync_NoWorktree_MarksDone()
|
||||
{
|
||||
var db = NewDb();
|
||||
var (_, task) = await SeedListAndTask(db, workingDir: "/tmp", status: TaskStatus.WaitingForReview);
|
||||
var (svc, _) = BuildService(db);
|
||||
|
||||
var result = await svc.ApproveAndMergeAsync(task.Id, "main", CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.StatusMerged, result.Status);
|
||||
using var ctx = db.CreateContext();
|
||||
var updated = await new TaskRepository(ctx).GetByIdAsync(task.Id);
|
||||
Assert.Equal(TaskStatus.Done, updated!.Status);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests, verify they fail**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter FullyQualifiedName~TaskMergeServiceTests`
|
||||
Expected: build error — `ITaskStateService` ctor arg, `PreviewAsync`, `ApproveAndMergeAsync`, `PreviewClean/PreviewConflict/PreviewUnavailable` do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement in TaskMergeService**
|
||||
|
||||
In `src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs`:
|
||||
|
||||
Add `using ClaudeDo.Worker.State;` to the usings.
|
||||
|
||||
Add the preview-result record beside `MergeTargets`:
|
||||
|
||||
```csharp
|
||||
public sealed record MergePreviewResult(
|
||||
string Status,
|
||||
IReadOnlyList<string> ConflictFiles,
|
||||
int ChangedFileCount);
|
||||
```
|
||||
|
||||
Add the status constants beside the existing `StatusMerged` etc.:
|
||||
|
||||
```csharp
|
||||
public const string PreviewClean = "clean";
|
||||
public const string PreviewConflict = "conflict";
|
||||
public const string PreviewUnavailable = "unavailable";
|
||||
```
|
||||
|
||||
Add the field and constructor param (inject `ITaskStateService`):
|
||||
|
||||
```csharp
|
||||
private readonly ITaskStateService _state;
|
||||
|
||||
public TaskMergeService(
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
GitService git,
|
||||
HubBroadcaster broadcaster,
|
||||
ITaskStateService state,
|
||||
ILogger<TaskMergeService> logger)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_git = git;
|
||||
_broadcaster = broadcaster;
|
||||
_state = state;
|
||||
_logger = logger;
|
||||
}
|
||||
```
|
||||
|
||||
Add the two methods (e.g. after `GetTargetsAsync`):
|
||||
|
||||
```csharp
|
||||
public async Task<MergePreviewResult> PreviewAsync(string taskId, string targetBranch, CancellationToken ct)
|
||||
{
|
||||
var (_, list, wt) = await LoadMergeContextAsync(taskId, ct);
|
||||
|
||||
if (wt is null || wt.State != WorktreeState.Active)
|
||||
return new MergePreviewResult(PreviewUnavailable, Array.Empty<string>(), 0);
|
||||
if (string.IsNullOrWhiteSpace(list.WorkingDir) || !await _git.IsGitRepoAsync(list.WorkingDir, ct))
|
||||
return new MergePreviewResult(PreviewUnavailable, Array.Empty<string>(), 0);
|
||||
|
||||
var target = string.IsNullOrWhiteSpace(targetBranch)
|
||||
? await _git.GetCurrentBranchAsync(list.WorkingDir, ct)
|
||||
: targetBranch;
|
||||
|
||||
var preview = await _git.PreviewMergeAsync(list.WorkingDir, target, wt.BranchName, ct);
|
||||
if (!preview.Supported)
|
||||
return new MergePreviewResult(PreviewUnavailable, Array.Empty<string>(), 0);
|
||||
if (!preview.Clean)
|
||||
return new MergePreviewResult(PreviewConflict, preview.ConflictFiles, 0);
|
||||
|
||||
var count = await _git.CountChangedFilesAsync(list.WorkingDir, target, wt.BranchName, ct);
|
||||
return new MergePreviewResult(PreviewClean, Array.Empty<string>(), count);
|
||||
}
|
||||
|
||||
public async Task<MergeResult> ApproveAndMergeAsync(string taskId, string targetBranch, CancellationToken ct)
|
||||
{
|
||||
var (task, list, wt) = await LoadMergeContextAsync(taskId, ct);
|
||||
|
||||
if (task.Status != TaskStatus.WaitingForReview)
|
||||
return Blocked("task is not waiting for review");
|
||||
|
||||
// No worktree to merge (sandbox run, or an improvement parent whose children own
|
||||
// the worktrees) — approve straight to Done.
|
||||
if (wt is null || wt.State != WorktreeState.Active)
|
||||
{
|
||||
var done = await _state.ApproveReviewAsync(taskId, ct);
|
||||
return done.Ok
|
||||
? new MergeResult(StatusMerged, Array.Empty<string>(), null)
|
||||
: Blocked(done.Reason ?? "approve failed");
|
||||
}
|
||||
|
||||
var target = string.IsNullOrWhiteSpace(targetBranch)
|
||||
? await _git.GetCurrentBranchAsync(list.WorkingDir, ct)
|
||||
: targetBranch;
|
||||
|
||||
var merge = await MergeAsync(taskId, target, removeWorktree: false, $"Merge {wt.BranchName}", ct);
|
||||
if (merge.Status != StatusMerged)
|
||||
return merge; // conflict or blocked — leave the task in WaitingForReview
|
||||
|
||||
var approve = await _state.ApproveReviewAsync(taskId, ct);
|
||||
return approve.Ok ? merge : Blocked(approve.Reason ?? "approve failed");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the tests, verify they pass**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter FullyQualifiedName~TaskMergeServiceTests`
|
||||
Expected: PASS (all existing + 6 new).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs
|
||||
git commit -m "feat(worker): approve merges worktree before marking task done"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: WorkerHub — PreviewMerge + result-returning ApproveReview
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||
|
||||
This is SignalR wiring (no unit test); verify by building the Worker.
|
||||
|
||||
- [ ] **Step 1: Add the DTO**
|
||||
|
||||
Beside the existing `MergeResultDto`/`MergeTargetsDto` records (around line 56):
|
||||
|
||||
```csharp
|
||||
public record MergePreviewDto(string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `PreviewMerge` and replace `ApproveReview`**
|
||||
|
||||
Add a `PreviewMerge` method beside `GetMergeTargets`:
|
||||
|
||||
```csharp
|
||||
public Task<MergePreviewDto> PreviewMerge(string taskId, string targetBranch)
|
||||
=> HubGuard(async () =>
|
||||
{
|
||||
var p = await _mergeService.PreviewAsync(taskId, targetBranch ?? "", CancellationToken.None);
|
||||
return new MergePreviewDto(p.Status, p.ConflictFiles, p.ChangedFileCount);
|
||||
});
|
||||
```
|
||||
|
||||
Replace the existing `ApproveReview` method (currently lines ~383-387, delegating to `_state.ApproveReviewAsync`) with:
|
||||
|
||||
```csharp
|
||||
public Task<MergeResultDto> ApproveReview(string taskId, string targetBranch)
|
||||
=> HubGuard(async () =>
|
||||
{
|
||||
var r = await _mergeService.ApproveAndMergeAsync(taskId, targetBranch ?? "", CancellationToken.None);
|
||||
if (r.Status == TaskMergeService.StatusBlocked)
|
||||
throw new HubException(r.ErrorMessage ?? "approve failed");
|
||||
return new MergeResultDto(r.Status, r.ConflictFiles, r.ErrorMessage);
|
||||
});
|
||||
```
|
||||
|
||||
(Conflicts are returned, not thrown, so the UI can display the conflicting files; only hard blocks throw.)
|
||||
|
||||
- [ ] **Step 3: Build the Worker, verify green**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||
Expected: Build succeeded. (DI resolves the new `ITaskStateService` dependency of `TaskMergeService` automatically — it is already registered.)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs
|
||||
git commit -m "feat(worker): expose PreviewMerge hub method and merge-on-approve"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: UI client + interface + test fakes
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` (caller at line 648)
|
||||
- Modify: `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` (`FakeWorkerClient`)
|
||||
- Modify: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`
|
||||
- Modify: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs` (override)
|
||||
|
||||
Note: `DetailsIslandViewModel.ApproveReviewAsync` (line 1368) is updated in Task 5, not here — but the interface change forces it to compile, so Task 5 must follow before the Ui project builds. To keep this task self-contained and green on its own, update that call site here too (the conflict-handling logic lands in Task 5).
|
||||
|
||||
- [ ] **Step 1: Add the UI DTO**
|
||||
|
||||
In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, beside the existing `MergeResultDto`/`MergeTargetsDto` records (lines 521-522):
|
||||
|
||||
```csharp
|
||||
public record MergePreviewDto(string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update the interface**
|
||||
|
||||
In `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`, replace `Task ApproveReviewAsync(string taskId);` (line 40) with:
|
||||
|
||||
```csharp
|
||||
Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch);
|
||||
Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch);
|
||||
Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage);
|
||||
```
|
||||
|
||||
(`MergeTaskAsync` already exists on the concrete `WorkerClient` — this only adds it to the interface.)
|
||||
|
||||
- [ ] **Step 3: Update the concrete client**
|
||||
|
||||
In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, replace the existing `ApproveReviewAsync` (line ~389) and add `PreviewMergeAsync`. Mirror the existing `GetMergeTargetsAsync` pattern (it uses the `TryInvokeAsync<T>` helper which returns `null` when disconnected):
|
||||
|
||||
```csharp
|
||||
public Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch)
|
||||
=> TryInvokeAsync<MergeResultDto>("ApproveReview", taskId, targetBranch);
|
||||
|
||||
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch)
|
||||
=> TryInvokeAsync<MergePreviewDto>("PreviewMerge", taskId, targetBranch);
|
||||
```
|
||||
|
||||
Ensure the existing `public async Task<MergeResultDto> MergeTaskAsync(...)` signature matches the interface exactly (params: `string taskId, string targetBranch, bool removeWorktree, string commitMessage`). Leave its body as-is.
|
||||
|
||||
- [ ] **Step 4: Update the two callers**
|
||||
|
||||
`src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` line 648 — the list-level quick approve has no merge-target selector, so it merges into the repo's current branch (empty string resolves server-side):
|
||||
|
||||
```csharp
|
||||
try { await _worker.ApproveReviewAsync(row.Id, ""); }
|
||||
```
|
||||
|
||||
`src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` line 1368 — update to the new signature for now (full conflict handling is added in Task 5):
|
||||
|
||||
```csharp
|
||||
try { await _worker.ApproveReviewAsync(Task.Id, SelectedMergeTarget ?? ""); }
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update the three test fakes**
|
||||
|
||||
`tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs` line 53 — replace and add:
|
||||
|
||||
```csharp
|
||||
public virtual Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
|
||||
public virtual Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
|
||||
public virtual Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
||||
```
|
||||
|
||||
`tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` line 45 (`FakeWorkerClient`) — replace and add:
|
||||
|
||||
```csharp
|
||||
public Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
|
||||
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
|
||||
public Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
||||
```
|
||||
|
||||
(Confirm whether `FakeWorkerClient` already implements `MergeTaskAsync`; if so, only change `ApproveReviewAsync` and add `PreviewMergeAsync`. Add `using` for the DTO namespace if needed — same namespace as `IWorkerClient`.)
|
||||
|
||||
`tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs` line 77 — update the override signature:
|
||||
|
||||
```csharp
|
||||
public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) =>
|
||||
/* keep whatever recording/behavior this override had, now returning Task<MergeResultDto?> */
|
||||
Task.FromResult<MergeResultDto?>(null);
|
||||
```
|
||||
|
||||
(Preserve any side effect the existing override performed — e.g. recording the call — just change the signature and return type.)
|
||||
|
||||
- [ ] **Step 6: Build UI + run both UI-touching test projects**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter FullyQualifiedName~TasksIslandViewModelPlanning`
|
||||
Expected: all green.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs src/ClaudeDo.Ui/Services/WorkerClient.cs src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
|
||||
git commit -m "feat(ui): wire merge-aware approve and preview into the worker client"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Mergeability presenter + DetailsIslandViewModel wiring
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ClaudeDo.Ui/ViewModels/Islands/MergePreviewPresenter.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/MergePreviewPresenterTests.cs` (create)
|
||||
|
||||
- [ ] **Step 1: Write the failing presenter tests**
|
||||
|
||||
Create `tests/ClaudeDo.Ui.Tests/ViewModels/MergePreviewPresenterTests.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class MergePreviewPresenterTests
|
||||
{
|
||||
[Fact]
|
||||
public void Clean_Plural()
|
||||
{
|
||||
var (text, clean, conflict) = MergePreviewPresenter.Describe(
|
||||
new MergePreviewDto("clean", System.Array.Empty<string>(), 3));
|
||||
Assert.Equal("Merges cleanly · 3 files", text);
|
||||
Assert.True(clean);
|
||||
Assert.False(conflict);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clean_Singular()
|
||||
{
|
||||
var (text, _, _) = MergePreviewPresenter.Describe(
|
||||
new MergePreviewDto("clean", System.Array.Empty<string>(), 1));
|
||||
Assert.Equal("Merges cleanly · 1 file", text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Conflict_ListsUpToThree()
|
||||
{
|
||||
var (text, clean, conflict) = MergePreviewPresenter.Describe(
|
||||
new MergePreviewDto("conflict", new[] { "a.cs", "b.cs" }, 0));
|
||||
Assert.Equal("Conflicts in a.cs, b.cs", text);
|
||||
Assert.False(clean);
|
||||
Assert.True(conflict);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Conflict_TruncatesWithMore()
|
||||
{
|
||||
var (text, _, _) = MergePreviewPresenter.Describe(
|
||||
new MergePreviewDto("conflict", new[] { "a", "b", "c", "d", "e" }, 0));
|
||||
Assert.Equal("Conflicts in a, b, c (+2 more)", text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unavailable_IsMuted()
|
||||
{
|
||||
var (text, clean, conflict) = MergePreviewPresenter.Describe(
|
||||
new MergePreviewDto("unavailable", System.Array.Empty<string>(), 0));
|
||||
Assert.Equal("Mergeability unknown", text);
|
||||
Assert.False(clean);
|
||||
Assert.False(conflict);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Null_IsEmpty()
|
||||
{
|
||||
var (text, clean, conflict) = MergePreviewPresenter.Describe(null);
|
||||
Assert.Equal("", text);
|
||||
Assert.False(clean);
|
||||
Assert.False(conflict);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run, verify it fails to compile**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter FullyQualifiedName~MergePreviewPresenterTests`
|
||||
Expected: build error — `MergePreviewPresenter` does not exist.
|
||||
|
||||
- [ ] **Step 3: Create the presenter**
|
||||
|
||||
Create `src/ClaudeDo.Ui/ViewModels/Islands/MergePreviewPresenter.cs`:
|
||||
|
||||
```csharp
|
||||
using System.Linq;
|
||||
using ClaudeDo.Ui.Services;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||
|
||||
/// Pure mapping from a merge-preview DTO to display text + color flags.
|
||||
public static class MergePreviewPresenter
|
||||
{
|
||||
public static (string Text, bool IsClean, bool IsConflict) Describe(MergePreviewDto? dto)
|
||||
{
|
||||
if (dto is null) return ("", false, false);
|
||||
|
||||
switch (dto.Status)
|
||||
{
|
||||
case "clean":
|
||||
var unit = dto.ChangedFileCount == 1 ? "file" : "files";
|
||||
return ($"Merges cleanly · {dto.ChangedFileCount} {unit}", true, false);
|
||||
|
||||
case "conflict":
|
||||
var names = string.Join(", ", dto.ConflictFiles.Take(3));
|
||||
var more = dto.ConflictFiles.Count > 3 ? $" (+{dto.ConflictFiles.Count - 3} more)" : "";
|
||||
return ($"Conflicts in {names}{more}", false, true);
|
||||
|
||||
default:
|
||||
return ("Mergeability unknown", false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run, verify the presenter tests pass**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter FullyQualifiedName~MergePreviewPresenterTests`
|
||||
Expected: PASS (6 tests).
|
||||
|
||||
- [ ] **Step 5: Wire the presenter into DetailsIslandViewModel**
|
||||
|
||||
In `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`:
|
||||
|
||||
(a) Add observable properties (near the other merge properties, ~line 334):
|
||||
|
||||
```csharp
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
|
||||
private string _mergePreviewText = "";
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
|
||||
private bool _mergeIsClean;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
|
||||
private bool _mergeIsConflict;
|
||||
|
||||
public bool ShowMergePreviewMuted =>
|
||||
!MergeIsClean && !MergeIsConflict && !string.IsNullOrEmpty(MergePreviewText);
|
||||
|
||||
public bool ShowSingleMerge =>
|
||||
WorktreePath != null && Task?.IsPlanningParent != true;
|
||||
```
|
||||
|
||||
(b) Add the refresh method:
|
||||
|
||||
```csharp
|
||||
private async System.Threading.Tasks.Task RefreshMergePreviewAsync()
|
||||
{
|
||||
if (Task is null || WorktreePath is null)
|
||||
{
|
||||
MergePreviewText = ""; MergeIsClean = false; MergeIsConflict = false;
|
||||
return;
|
||||
}
|
||||
// Only probe Active worktrees; terminal states show their label instead.
|
||||
if (WorktreeStateLabel is { } label && label != "Active")
|
||||
{
|
||||
MergePreviewText = label; MergeIsClean = false; MergeIsConflict = false;
|
||||
return;
|
||||
}
|
||||
var dto = await _worker.PreviewMergeAsync(Task.Id, SelectedMergeTarget ?? "");
|
||||
var (text, clean, conflict) = MergePreviewPresenter.Describe(dto);
|
||||
MergePreviewText = text; MergeIsClean = clean; MergeIsConflict = conflict;
|
||||
}
|
||||
```
|
||||
|
||||
(c) Recompute when the merge target changes — add (or extend) the generated partial:
|
||||
|
||||
```csharp
|
||||
partial void OnSelectedMergeTargetChanged(string? value)
|
||||
{
|
||||
_ = RefreshMergePreviewAsync();
|
||||
}
|
||||
```
|
||||
|
||||
(d) Notify `ShowSingleMerge` when the worktree path changes. In the existing `OnWorktreePathChanged` (line ~1141) add:
|
||||
|
||||
```csharp
|
||||
OnPropertyChanged(nameof(ShowSingleMerge));
|
||||
```
|
||||
|
||||
(e) Load merge targets for standalone worktree tasks. In `BindAsync`, after the `if (entity.PlanningPhase != None) {...} else {...}` block (~line 814), add:
|
||||
|
||||
```csharp
|
||||
if (entity.Worktree != null
|
||||
&& entity.PlanningPhase == ClaudeDo.Data.Models.PlanningPhase.None
|
||||
&& MergeTargetBranches.Count == 0)
|
||||
{
|
||||
var targets = await _worker.GetMergeTargetsAsync(row.Id);
|
||||
if (targets != null)
|
||||
{
|
||||
MergeTargetBranches.Clear();
|
||||
foreach (var b in targets.LocalBranches) MergeTargetBranches.Add(b);
|
||||
SelectedMergeTarget = targets.DefaultBranch; // triggers OnSelectedMergeTargetChanged → preview
|
||||
}
|
||||
}
|
||||
await RefreshMergePreviewAsync();
|
||||
```
|
||||
|
||||
(f) Replace the body of `ApproveReviewAsync` (line ~1362) to surface conflicts:
|
||||
|
||||
```csharp
|
||||
[RelayCommand]
|
||||
private async System.Threading.Tasks.Task ApproveReviewAsync()
|
||||
{
|
||||
if (Task is null || !_worker.IsConnected) return;
|
||||
try
|
||||
{
|
||||
var result = await _worker.ApproveReviewAsync(Task.Id, SelectedMergeTarget ?? "");
|
||||
if (result?.Status == "conflict")
|
||||
{
|
||||
var (text, _, _) = MergePreviewPresenter.Describe(
|
||||
new MergePreviewDto("conflict", result.ConflictFiles, 0));
|
||||
MergePreviewText = text; MergeIsClean = false; MergeIsConflict = true;
|
||||
}
|
||||
}
|
||||
catch { /* stale review action; broadcast reconciles */ }
|
||||
}
|
||||
```
|
||||
|
||||
(g) Add the single-task `MergeCommand` (place near `OpenDiffAsync`):
|
||||
|
||||
```csharp
|
||||
[RelayCommand]
|
||||
private async System.Threading.Tasks.Task MergeAsync()
|
||||
{
|
||||
if (Task is null || WorktreePath is null || !_worker.IsConnected) return;
|
||||
try
|
||||
{
|
||||
var result = await _worker.MergeTaskAsync(Task.Id, SelectedMergeTarget ?? "", false, "Merge task");
|
||||
if (result.Status == "conflict")
|
||||
{
|
||||
var (text, _, _) = MergePreviewPresenter.Describe(
|
||||
new MergePreviewDto("conflict", result.ConflictFiles, 0));
|
||||
MergePreviewText = text; MergeIsClean = false; MergeIsConflict = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
await RefreshMergePreviewAsync();
|
||||
}
|
||||
}
|
||||
catch { /* broadcast reconciles */ }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Build UI + run the UI tests**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||
Expected: green. (If `OnSelectedMergeTargetChanged` already exists, merge the new line into it instead of duplicating.)
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/ViewModels/Islands/MergePreviewPresenter.cs src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/MergePreviewPresenterTests.cs
|
||||
git commit -m "feat(ui): show mergeability and surface approve conflicts in the work console"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: WorkConsole — status line + Merge button
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml`
|
||||
|
||||
No unit test (XAML); verified by build + manual visual check in Task 7.
|
||||
|
||||
- [ ] **Step 1: Add the mergeability status line and the Merge button**
|
||||
|
||||
In the `MERGE & WORKTREE` `StackPanel` (starts line 196), insert the status line **between** the merge-target `StackPanel` (ends line 203) and the `<WrapPanel>` (line 204). Three single-line `TextBlock`s, one visible at a time by color:
|
||||
|
||||
```xml
|
||||
<StackPanel Spacing="0">
|
||||
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource MossBrush}"
|
||||
IsVisible="{Binding MergeIsClean}" />
|
||||
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource BloodBrush}"
|
||||
IsVisible="{Binding MergeIsConflict}" />
|
||||
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
IsVisible="{Binding ShowMergePreviewMuted}" />
|
||||
</StackPanel>
|
||||
```
|
||||
|
||||
In the `<WrapPanel>` (line 204), add a **Merge** button immediately after the "Open Diff" button (line 206):
|
||||
|
||||
```xml
|
||||
<Button Classes="btn accent" Content="Merge" Margin="0,0,8,8"
|
||||
Command="{Binding MergeCommand}"
|
||||
IsVisible="{Binding ShowSingleMerge}" />
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build UI, verify green**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded (XAML compiles; all bound members exist from Task 5).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
|
||||
git commit -m "feat(ui): add mergeability indicator and Merge button to work console"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Full build, full test, manual verification
|
||||
|
||||
**Files:** none (verification only)
|
||||
|
||||
- [ ] **Step 1: Build the whole app + worker**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||
Expected: both succeed.
|
||||
|
||||
- [ ] **Step 2: Run all touched test projects**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release`
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||
Expected: all green.
|
||||
|
||||
- [ ] **Step 3: Manual verification (cannot be automated — no real Claude in tests)**
|
||||
|
||||
Start the Worker, then the App. Pick a list whose `WorkingDir` is a real git repo and use a task that already has an Active worktree (or create one).
|
||||
|
||||
Verify each acceptance criterion:
|
||||
1. **Clean approve:** Open a `WaitingForReview` task whose worktree merges cleanly → the Session tab shows green "Merges cleanly · N files". Click **Approve** → the worktree merges into the target, the task becomes **Done**, and the worktree state becomes **Merged** (check the worktree overview).
|
||||
2. **Conflicting approve:** Open a task whose worktree conflicts with the target → the Session tab shows red "Conflicts in …". Click **Approve** → the task stays **WaitingForReview** (NOT Done), the conflict line remains, and the target branch is unchanged.
|
||||
3. **Done task preview:** Open a previously-Done task that was never merged (worktree still Active) → the merge/conflict status appears without any tree mutation; the **Merge** button merges it on demand.
|
||||
|
||||
Report the result of each check explicitly. If any visual issue appears (colors, layout, missing controls), note it for the user — do not claim the UI works without running it.
|
||||
|
||||
---
|
||||
|
||||
## Self-review notes
|
||||
|
||||
- **Spec coverage:** Approve-merge (Task 2/3/5), conflict-keeps-review (Task 2 test + Task 5 surfacing), non-destructive preview (Task 1/2 + indicator in Task 5/6), real single-task Merge button (Task 5/6), standalone target-loading gap (Task 5e). All spec sections map to a task.
|
||||
- **Type consistency:** `MergePreview` (Data) → `MergePreviewResult` (Worker service) → `MergePreviewDto` (hub + UI). Status strings `clean`/`conflict`/`unavailable` and merge statuses `merged`/`conflict`/`blocked` are used consistently across worker, client, presenter, and VM.
|
||||
- **No new statuses, no DB migration, no localization keys** (literals match the surrounding controls).
|
||||
- **External MCP unchanged:** `ExternalMcpService.ReviewTask` keeps calling `TaskStateService.ApproveReviewAsync` directly (its documented scope excludes merges); that method's signature is unchanged.
|
||||
972
docs/superpowers/plans/2026-06-04-bundled-prompts-overhaul.md
Normal file
972
docs/superpowers/plans/2026-06-04-bundled-prompts-overhaul.md
Normal file
@@ -0,0 +1,972 @@
|
||||
# Bundled Prompts Overhaul Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Externalize every bundled prose prompt into editable files with strong defaults, collapse system+agent, and add an inline `CLAUDEDO_BLOCKED:` roadblock protocol surfaced at review.
|
||||
|
||||
**Architecture:** `PromptFiles` becomes the single source of prompt defaults + a pure token renderer. Each consumer (TaskRunner, PlanningSessionManager, DailyPrepPrompt, WeekReportPromptBuilder) reads its prompt via `PromptFiles`. `StreamAnalyzer` collects roadblock markers from streamed assistant text; the runner folds them into the review result.
|
||||
|
||||
**Tech Stack:** .NET 8, xUnit, EF Core (no schema change in this plan).
|
||||
|
||||
Spec: `docs/superpowers/specs/2026-06-04-bundled-prompts-overhaul-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File structure
|
||||
|
||||
- `src/ClaudeDo.Data/PromptFiles.cs` — new `PromptKind` members, new defaults, `RenderTemplate` + `ReadOrDefault` + `Render`.
|
||||
- `src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs` — collect `Blocks` from assistant text.
|
||||
- `src/ClaudeDo.Worker/Runner/RunResult.cs` — carry `Blocks`.
|
||||
- `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs` — pass `Blocks`; expose no-result prefix const.
|
||||
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — drop agent file; retry via `retry.md`; fold blocks into review result.
|
||||
- `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` — read planning prompts via `PromptFiles`.
|
||||
- `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` — read `daily-prep.md`.
|
||||
- `src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs` — read `weekly-report.md`.
|
||||
- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs` + its view — expose new prompt files, drop agent.
|
||||
- Tests in `tests/ClaudeDo.Data.Tests` and `tests/ClaudeDo.Worker.Tests`.
|
||||
|
||||
Build commands (this repo is on .NET 8 — build per project, not the .slnx):
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 1: PromptFiles — kinds, defaults, pure renderer
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Data/PromptFiles.cs`
|
||||
- Test: `tests/ClaudeDo.Data.Tests/PromptFilesTests.cs` (create)
|
||||
|
||||
- [ ] **Step 1: Write failing tests for the pure renderer**
|
||||
|
||||
Create `tests/ClaudeDo.Data.Tests/PromptFilesTests.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data;
|
||||
|
||||
namespace ClaudeDo.Data.Tests;
|
||||
|
||||
public class PromptFilesTests
|
||||
{
|
||||
[Fact]
|
||||
public void RenderTemplate_replaces_known_tokens()
|
||||
{
|
||||
var outp = PromptFiles.RenderTemplate(
|
||||
"Plan for {date}, cap {maxTasks}.",
|
||||
new Dictionary<string, string> { ["date"] = "2026-06-04", ["maxTasks"] = "5" });
|
||||
Assert.Equal("Plan for 2026-06-04, cap 5.", outp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderTemplate_leaves_unknown_braces_intact()
|
||||
{
|
||||
var outp = PromptFiles.RenderTemplate(
|
||||
"## {Wochentag}, {dd.MM.yyyy} — {start}",
|
||||
new Dictionary<string, string> { ["start"] = "01.06.2026" });
|
||||
Assert.Equal("## {Wochentag}, {dd.MM.yyyy} — 01.06.2026", outp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultFor_system_mentions_blocked_marker_and_scope()
|
||||
{
|
||||
var d = PromptFiles.DefaultFor(PromptKind.System);
|
||||
Assert.Contains("CLAUDEDO_BLOCKED:", d);
|
||||
Assert.Contains("unattended", d, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultFor_planning_initial_has_title_and_description_tokens()
|
||||
{
|
||||
var d = PromptFiles.DefaultFor(PromptKind.PlanningInitial);
|
||||
Assert.Contains("{title}", d);
|
||||
Assert.Contains("{description}", d);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PathFor_planning_is_planning_system_file()
|
||||
{
|
||||
Assert.EndsWith("planning-system.md", PromptFiles.PathFor(PromptKind.Planning));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release`
|
||||
Expected: FAIL — `RenderTemplate`/`DefaultFor` don't exist, `PromptKind.PlanningInitial` undefined.
|
||||
|
||||
- [ ] **Step 3: Rewrite PromptFiles.cs**
|
||||
|
||||
Replace the entire contents of `src/ClaudeDo.Data/PromptFiles.cs` with:
|
||||
|
||||
```csharp
|
||||
using System.Text;
|
||||
|
||||
namespace ClaudeDo.Data;
|
||||
|
||||
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport }
|
||||
|
||||
public static class PromptFiles
|
||||
{
|
||||
public static string Root => Path.Combine(Paths.AppDataRoot(), "prompts");
|
||||
|
||||
public static string PathFor(PromptKind kind) => kind switch
|
||||
{
|
||||
PromptKind.System => Path.Combine(Root, "system.md"),
|
||||
PromptKind.Planning => Path.Combine(Root, "planning-system.md"),
|
||||
PromptKind.PlanningInitial => Path.Combine(Root, "planning-initial.md"),
|
||||
PromptKind.Retry => Path.Combine(Root, "retry.md"),
|
||||
PromptKind.DailyPrep => Path.Combine(Root, "daily-prep.md"),
|
||||
PromptKind.WeeklyReport => Path.Combine(Root, "weekly-report.md"),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
||||
};
|
||||
|
||||
public static void EnsureExists(PromptKind kind)
|
||||
{
|
||||
Directory.CreateDirectory(Root);
|
||||
var path = PathFor(kind);
|
||||
if (File.Exists(path)) return;
|
||||
File.WriteAllText(path, DefaultFor(kind));
|
||||
}
|
||||
|
||||
public static string? ReadOrNull(PromptKind kind)
|
||||
{
|
||||
var path = PathFor(kind);
|
||||
if (!File.Exists(path)) return null;
|
||||
var content = File.ReadAllText(path).Trim();
|
||||
return string.IsNullOrEmpty(content) ? null : content;
|
||||
}
|
||||
|
||||
/// <summary>File content if present and non-empty, otherwise the bundled default.</summary>
|
||||
public static string ReadOrDefault(PromptKind kind) => ReadOrNull(kind) ?? DefaultFor(kind);
|
||||
|
||||
/// <summary>Render a prompt: read file-or-default, then substitute named tokens.</summary>
|
||||
public static string Render(PromptKind kind, IReadOnlyDictionary<string, string> values)
|
||||
=> RenderTemplate(ReadOrDefault(kind), values);
|
||||
|
||||
/// <summary>Replace only the given {name} tokens; any other braces pass through untouched.</summary>
|
||||
public static string RenderTemplate(string template, IReadOnlyDictionary<string, string> values)
|
||||
{
|
||||
var sb = new StringBuilder(template);
|
||||
foreach (var (key, val) in values)
|
||||
sb.Replace("{" + key + "}", val);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string DefaultFor(PromptKind kind) => kind switch
|
||||
{
|
||||
PromptKind.System => SystemDefault,
|
||||
PromptKind.Planning => PlanningSystemDefault,
|
||||
PromptKind.PlanningInitial => PlanningInitialDefault,
|
||||
PromptKind.Retry => RetryDefault,
|
||||
PromptKind.DailyPrep => DailyPrepDefault,
|
||||
PromptKind.WeeklyReport => WeeklyReportDefault,
|
||||
_ => ""
|
||||
};
|
||||
|
||||
private const string SystemDefault = """
|
||||
# Working Agreement
|
||||
|
||||
You are completing one well-defined task autonomously in a git repository.
|
||||
|
||||
## Scope
|
||||
- Do exactly what the task asks — no unrequested refactors, renames, dependency
|
||||
changes, or "while I'm here" cleanup.
|
||||
- If intent is ambiguous, state the assumption you're making and proceed with the
|
||||
most reasonable reading. Stop only if you genuinely cannot move forward.
|
||||
- Prefer three similar lines over a premature abstraction. Don't build for
|
||||
hypothetical future needs.
|
||||
|
||||
## Working in the repo
|
||||
- Read a file before editing it. Match the conventions already in this codebase —
|
||||
they override generic defaults.
|
||||
- Prefer editing existing files to creating new ones. Don't write comments that
|
||||
just restate the code.
|
||||
- Validate only at real boundaries (user input, external APIs).
|
||||
|
||||
## Finishing
|
||||
- Before claiming done, verify: run the build and relevant tests, confirm they
|
||||
pass, and report what you ran. If you couldn't verify something, say so plainly.
|
||||
- Make focused commits using the repository's existing commit-message convention.
|
||||
|
||||
## Safety
|
||||
- Never force-push, hard-reset, or delete branches/files beyond the task's scope
|
||||
without being asked.
|
||||
- Don't introduce injection/XSS/secret-leak issues. Never commit credentials.
|
||||
|
||||
## You are running unattended
|
||||
You run autonomously with no human watching. There is no one to answer mid-task
|
||||
questions, so never stop to ask — make the most reasonable decision, note the
|
||||
assumption, and continue.
|
||||
|
||||
## When you are blocked
|
||||
If something genuinely prevents you from completing part of the task (missing
|
||||
credentials, contradictory requirements, a destructive action you won't take
|
||||
unasked), do NOT silently give up. Write this marker on its own line, then keep
|
||||
working on whatever else you can:
|
||||
|
||||
CLAUDEDO_BLOCKED: <one short sentence describing what blocked you>
|
||||
|
||||
Emit it as many times as needed — once per distinct blocker. Use it only for true
|
||||
blockers, not for routine decisions you can make yourself.
|
||||
""";
|
||||
|
||||
private const string PlanningSystemDefault = """
|
||||
You are the planning assistant for ClaudeDo. Your job is to break a task into
|
||||
smaller, independently executable subtasks — the session ends by creating those
|
||||
subtasks.
|
||||
|
||||
Start every session by invoking the `superpowers:brainstorming` skill (Skill
|
||||
tool) and follow it end to end: clarifying questions one at a time, then 2–3
|
||||
approaches with a recommendation, then a short design. Do not create any subtasks
|
||||
until the user has approved the design.
|
||||
|
||||
You can ONLY shape this task's plan — you cannot edit files or touch other tasks.
|
||||
The tools available to you are: CreateChildTask, ListChildTasks, UpdateChildTask,
|
||||
DeleteChildTask, UpdatePlanningTask, and Finalize. Use nothing else.
|
||||
|
||||
Once the design is approved, create the child tasks with CreateChildTask, then
|
||||
call Finalize. Keep each subtask concrete and self-contained with a clear
|
||||
done-state, ordered so dependencies come first.
|
||||
""";
|
||||
|
||||
private const string PlanningInitialDefault = """
|
||||
# Task to plan: {title}
|
||||
|
||||
{description}
|
||||
""";
|
||||
|
||||
private const string RetryDefault = """
|
||||
The task did not complete on the previous attempt — you may have run out of
|
||||
turns, hit an error, or stopped before finishing.
|
||||
|
||||
Review the work already done in this session and the current state of the
|
||||
repository, identify what is still incomplete or broken, and finish the task.
|
||||
Don't restart from scratch or repeat a failed approach. Verify the result
|
||||
(build + tests) before you stop.
|
||||
""";
|
||||
|
||||
private const string DailyPrepDefault = """
|
||||
You are preparing my workday for {date}.
|
||||
|
||||
1. Call mcp__claudedo__get_daily_prep_candidates.
|
||||
2. Keep tasks already marked MyDay (currentMyDay) — never remove them.
|
||||
3. Fill MyDay to at most {maxTasks} open tasks TOTAL (currentMyDay counts). Never exceed it.
|
||||
4. Estimate each candidate's effort and pick a feasible mix — not only big items.
|
||||
Prioritize isStarred, due (scheduledFor), and older tasks.
|
||||
5. Place related tasks next to each other using consecutive sortOrder values.
|
||||
6. Apply via mcp__claudedo__set_my_day(taskId, true, sortOrder). Never mark anything
|
||||
outside the candidate list.
|
||||
|
||||
If there are no candidates, do nothing.
|
||||
""";
|
||||
|
||||
private const string WeeklyReportDefault = """
|
||||
You are generating a concise weekly standup report for a software developer,
|
||||
covering {start} to {end}.
|
||||
|
||||
Rules:
|
||||
- Write the ENTIRE report in German.
|
||||
- Group by day. One "## {Wochentag}, {dd.MM.yyyy}" section per day that has
|
||||
activity (German weekday names). Omit days with no activity.
|
||||
- Within each day: 3–5 first-person, past-tense bullets ("- Habe X umgesetzt",
|
||||
"- Y behoben"). Merge related small work into one bullet.
|
||||
- Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise.
|
||||
- Blend the developer's own notes and the derived activity into ONE deduplicated
|
||||
bullet list per day. The notes are authoritative — never omit or contradict them.
|
||||
- Name the project/repo when it adds clarity.
|
||||
- Output ONLY the dated sections. No preamble, no intro, no closing remarks.
|
||||
|
||||
Two sections follow below: an activity log derived from Claude session history,
|
||||
and the developer's own notes. Base the report on both; the notes are
|
||||
authoritative where they conflict with the derived activity.
|
||||
""";
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release`
|
||||
Expected: PASS (5 new tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Data/PromptFiles.cs tests/ClaudeDo.Data.Tests/PromptFilesTests.cs
|
||||
git commit -m "feat(prompts): externalize prompt kinds with defaults and token renderer"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: TaskRunner — drop agent file from system prompt merge
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs:382-386`
|
||||
|
||||
- [ ] **Step 1: Remove the agent-file read and merge**
|
||||
|
||||
In `ResolveConfigAsync`, replace:
|
||||
|
||||
```csharp
|
||||
var systemFile = PromptFiles.ReadOrNull(PromptKind.System);
|
||||
var agentFile = PromptFiles.ReadOrNull(PromptKind.Agent);
|
||||
|
||||
var instructions = MergeInstructions(
|
||||
systemFile, global.DefaultClaudeInstructions, listConfig?.SystemPrompt, task.SystemPrompt, agentFile);
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```csharp
|
||||
var systemFile = PromptFiles.ReadOrNull(PromptKind.System);
|
||||
|
||||
var instructions = MergeInstructions(
|
||||
systemFile, global.DefaultClaudeInstructions, listConfig?.SystemPrompt, task.SystemPrompt);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build to verify it compiles**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||
Expected: PASS (no reference to `PromptKind.Agent` remains).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Runner/TaskRunner.cs
|
||||
git commit -m "refactor(prompts): collapse agent prompt into system prompt"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Retry prompt from file + conditional stderr append
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs:101-103` (expose prefix const)
|
||||
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs` (add `BuildRetryPrompt`, use it at ~L107)
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Runner/RetryPromptTests.cs` (create)
|
||||
|
||||
- [ ] **Step 1: Write failing tests for the retry-prompt helper**
|
||||
|
||||
Create `tests/ClaudeDo.Worker.Tests/Runner/RetryPromptTests.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Worker.Runner;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Runner;
|
||||
|
||||
public class RetryPromptTests
|
||||
{
|
||||
[Fact]
|
||||
public void Generic_no_result_error_is_not_appended()
|
||||
{
|
||||
var prompt = TaskRunner.BuildRetryPrompt($"{ClaudeProcess.NoResultPrefix} 1 and no result.");
|
||||
Assert.DoesNotContain("Captured error", prompt);
|
||||
Assert.Contains("did not complete", prompt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Real_error_is_appended()
|
||||
{
|
||||
var prompt = TaskRunner.BuildRetryPrompt("error CS1002: ; expected");
|
||||
Assert.Contains("Captured error", prompt);
|
||||
Assert.Contains("CS1002", prompt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Null_error_yields_bare_prompt()
|
||||
{
|
||||
var prompt = TaskRunner.BuildRetryPrompt(null);
|
||||
Assert.DoesNotContain("Captured error", prompt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter RetryPromptTests`
|
||||
Expected: FAIL — `BuildRetryPrompt` / `NoResultPrefix` don't exist.
|
||||
|
||||
- [ ] **Step 3: Expose the no-result prefix in ClaudeProcess**
|
||||
|
||||
In `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs`, add the const near the top of the class and use it in the error fallback. Replace:
|
||||
|
||||
```csharp
|
||||
var error = lastStderr.Length > 0
|
||||
? lastStderr.ToString().Trim()
|
||||
: $"Claude exited with code {exitCode} and no result.";
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```csharp
|
||||
var error = lastStderr.Length > 0
|
||||
? lastStderr.ToString().Trim()
|
||||
: $"{NoResultPrefix} {exitCode} and no result.";
|
||||
```
|
||||
|
||||
and add inside the class (e.g. just below the fields):
|
||||
|
||||
```csharp
|
||||
public const string NoResultPrefix = "Claude exited with code";
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add BuildRetryPrompt to TaskRunner and use it**
|
||||
|
||||
In `src/ClaudeDo.Worker/Runner/TaskRunner.cs`, add this static method (next to `MergeInstructions`):
|
||||
|
||||
```csharp
|
||||
public static string BuildRetryPrompt(string? capturedError)
|
||||
{
|
||||
var basePrompt = PromptFiles.ReadOrDefault(PromptKind.Retry);
|
||||
var isReal = !string.IsNullOrWhiteSpace(capturedError)
|
||||
&& !capturedError!.StartsWith(ClaudeProcess.NoResultPrefix, StringComparison.Ordinal);
|
||||
return isReal
|
||||
? $"{basePrompt}\n\nCaptured error from the failed run:\n\n{capturedError!.Trim()}"
|
||||
: basePrompt;
|
||||
}
|
||||
```
|
||||
|
||||
Then replace the inline retry prompt at ~L107:
|
||||
|
||||
```csharp
|
||||
var retryPrompt = $"The previous attempt failed with:\n\n{result.ErrorMarkdown}\n\nTry again and fix the issues.";
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```csharp
|
||||
var retryPrompt = BuildRetryPrompt(result.ErrorMarkdown);
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter RetryPromptTests`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Runner/ClaudeProcess.cs src/ClaudeDo.Worker/Runner/TaskRunner.cs tests/ClaudeDo.Worker.Tests/Runner/RetryPromptTests.cs
|
||||
git commit -m "feat(prompts): retry prompt from file, append only real captured errors"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: PlanningSessionManager reads planning prompts from files
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` (`BuildSystemPrompt` ~L366, `BuildInitialPrompt` ~L392)
|
||||
|
||||
- [ ] **Step 1: Replace BuildSystemPrompt body**
|
||||
|
||||
Replace the whole method body of `BuildSystemPrompt()` with:
|
||||
|
||||
```csharp
|
||||
private static string BuildSystemPrompt() => PromptFiles.ReadOrDefault(PromptKind.Planning);
|
||||
```
|
||||
|
||||
(Delete the inline fallback string literal that followed.)
|
||||
|
||||
- [ ] **Step 2: Replace BuildInitialPrompt body**
|
||||
|
||||
Replace the whole method body of `BuildInitialPrompt(TaskEntity task)` with:
|
||||
|
||||
```csharp
|
||||
private static string BuildInitialPrompt(TaskEntity task) =>
|
||||
PromptFiles.Render(PromptKind.PlanningInitial, new Dictionary<string, string>
|
||||
{
|
||||
["title"] = task.Title,
|
||||
["description"] = task.Description ?? "",
|
||||
});
|
||||
```
|
||||
|
||||
Ensure `using ClaudeDo.Data;` is present (it is — `PromptFiles` lived there already via `ReadOrNull`).
|
||||
|
||||
- [ ] **Step 3: Build to verify it compiles**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs
|
||||
git commit -m "refactor(prompts): planning prompts read from editable files"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: DailyPrepPrompt reads from file
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs`
|
||||
- Modify: `tests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs`
|
||||
|
||||
- [ ] **Step 1: Update DailyPrepPromptTests to assert the English default render**
|
||||
|
||||
Replace the `Build_prompt_contains_cap_and_date` test body with:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void Build_prompt_contains_cap_and_date()
|
||||
{
|
||||
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks: 5, today: new DateOnly(2026, 6, 3));
|
||||
Assert.Contains("5", prompt);
|
||||
Assert.Contains("2026-06-03", prompt);
|
||||
Assert.Contains("get_daily_prep_candidates", prompt);
|
||||
Assert.Contains("set_my_day", prompt);
|
||||
Assert.Contains("preparing my workday", prompt);
|
||||
}
|
||||
```
|
||||
|
||||
(The new assertion pins the English default; the file-read path is exercised by the same default when no `daily-prep.md` exists.)
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter DailyPrepPromptTests`
|
||||
Expected: FAIL — current German prompt has no "preparing my workday".
|
||||
|
||||
- [ ] **Step 3: Rewrite BuildPrompt to read the file**
|
||||
|
||||
In `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs`, replace the `BuildPrompt` method with:
|
||||
|
||||
```csharp
|
||||
public static string BuildPrompt(int maxTasks, DateOnly today) =>
|
||||
ClaudeDo.Data.PromptFiles.Render(
|
||||
ClaudeDo.Data.PromptKind.DailyPrep,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["date"] = today.ToString("yyyy-MM-dd"),
|
||||
["maxTasks"] = maxTasks.ToString(),
|
||||
});
|
||||
```
|
||||
|
||||
Leave `BuildArgs`, `LogPath`, and the tool-name consts unchanged.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter DailyPrepPromptTests`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs tests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs
|
||||
git commit -m "feat(prompts): daily-prep prompt from file, English default"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: WeekReportPromptBuilder reads instructions from file
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs`
|
||||
- Check: `tests/ClaudeDo.Worker.Tests/Report/WeekReportPromptBuilderTests.cs`
|
||||
|
||||
- [ ] **Step 1: Replace the inline Instructions with a file read**
|
||||
|
||||
In `WeekReportPromptBuilder.Build`, replace:
|
||||
|
||||
```csharp
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, Instructions,
|
||||
start.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture),
|
||||
end.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture)));
|
||||
sb.AppendLine();
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```csharp
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(ClaudeDo.Data.PromptFiles.Render(
|
||||
ClaudeDo.Data.PromptKind.WeeklyReport,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["start"] = start.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture),
|
||||
["end"] = end.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture),
|
||||
}));
|
||||
sb.AppendLine();
|
||||
```
|
||||
|
||||
Then delete the now-unused `private const string Instructions = ...` block. (The `{Wochentag}`/`{dd.MM.yyyy}` literals inside the default survive because `RenderTemplate` only replaces `{start}`/`{end}`.)
|
||||
|
||||
- [ ] **Step 2: Verify the existing builder test still passes**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter WeekReportPromptBuilderTests`
|
||||
Expected: PASS. If a test asserted exact old wording, update it to assert the date appears and that activity/notes sections render (the new default keeps German output rules).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs tests/ClaudeDo.Worker.Tests/Report/WeekReportPromptBuilderTests.cs
|
||||
git commit -m "feat(prompts): weekly-report instructions from file, point at data sections"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: StreamAnalyzer collects roadblock markers
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Runner/StreamAnalyzerTests.cs`
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
Append to `StreamAnalyzerTests`:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void Collects_Blocked_Markers_From_Assistant_Text()
|
||||
{
|
||||
var analyzer = new StreamAnalyzer();
|
||||
analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"working\nCLAUDEDO_BLOCKED: missing API key\nmoving on"}]}}""");
|
||||
analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"CLAUDEDO_BLOCKED: cannot reach db"}]}}""");
|
||||
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
|
||||
var result = analyzer.GetResult();
|
||||
Assert.Equal(2, result.Blocks.Count);
|
||||
Assert.Equal("missing API key", result.Blocks[0]);
|
||||
Assert.Equal("cannot reach db", result.Blocks[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Strips_Blocked_Markers_From_Result_Text()
|
||||
{
|
||||
var analyzer = new StreamAnalyzer();
|
||||
analyzer.ProcessLine("""{"type":"result","result":"All set.\nCLAUDEDO_BLOCKED: no creds\nDone.","session_id":"s1"}""");
|
||||
var result = analyzer.GetResult();
|
||||
Assert.DoesNotContain("CLAUDEDO_BLOCKED", result.ResultMarkdown);
|
||||
Assert.Single(result.Blocks);
|
||||
Assert.Equal("no creds", result.Blocks[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void No_Markers_Means_Empty_Blocks()
|
||||
{
|
||||
var analyzer = new StreamAnalyzer();
|
||||
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
|
||||
Assert.Empty(analyzer.GetResult().Blocks);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter StreamAnalyzerTests`
|
||||
Expected: FAIL — `Blocks` doesn't exist.
|
||||
|
||||
- [ ] **Step 3: Implement marker collection in StreamAnalyzer**
|
||||
|
||||
In `src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs`:
|
||||
|
||||
Add to `StreamResult`:
|
||||
|
||||
```csharp
|
||||
public IReadOnlyList<string> Blocks { get; set; } = Array.Empty<string>();
|
||||
```
|
||||
|
||||
Add a field and a constant to `StreamAnalyzer`:
|
||||
|
||||
```csharp
|
||||
private readonly List<string> _blocks = new();
|
||||
private const string BlockedPrefix = "CLAUDEDO_BLOCKED:";
|
||||
```
|
||||
|
||||
In the `case "result":` branch, after `_resultMarkdown` is assigned, scan and strip:
|
||||
|
||||
```csharp
|
||||
if (root.TryGetProperty("result", out var resultProp))
|
||||
_resultMarkdown = StripAndCollect(resultProp.GetString());
|
||||
```
|
||||
|
||||
In the `case "assistant":` branch, collect from text content (keep `_turnCount++`):
|
||||
|
||||
```csharp
|
||||
case "assistant":
|
||||
_turnCount++;
|
||||
CollectFromAssistant(root);
|
||||
break;
|
||||
```
|
||||
|
||||
Add these helpers to the class:
|
||||
|
||||
```csharp
|
||||
private void CollectFromAssistant(JsonElement root)
|
||||
{
|
||||
if (!root.TryGetProperty("message", out var msg)) return;
|
||||
if (!msg.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array) return;
|
||||
foreach (var block in content.EnumerateArray())
|
||||
if (block.TryGetProperty("type", out var t) && t.GetString() == "text"
|
||||
&& block.TryGetProperty("text", out var txt))
|
||||
ScanForBlocks(txt.GetString());
|
||||
}
|
||||
|
||||
private void ScanForBlocks(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
foreach (var line in text.Split('\n'))
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.StartsWith(BlockedPrefix, StringComparison.Ordinal))
|
||||
_blocks.Add(trimmed[BlockedPrefix.Length..].Trim());
|
||||
}
|
||||
}
|
||||
|
||||
private string? StripAndCollect(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return text;
|
||||
ScanForBlocks(text);
|
||||
var kept = text.Split('\n')
|
||||
.Where(l => !l.Trim().StartsWith(BlockedPrefix, StringComparison.Ordinal));
|
||||
return string.Join('\n', kept).Trim();
|
||||
}
|
||||
```
|
||||
|
||||
Add `Blocks = _blocks` to the `GetResult()` initializer:
|
||||
|
||||
```csharp
|
||||
public StreamResult GetResult() => new()
|
||||
{
|
||||
ResultMarkdown = FallbackResult(),
|
||||
StructuredOutputJson = _structuredOutputJson,
|
||||
SessionId = _sessionId,
|
||||
TurnCount = _turnCount,
|
||||
TokensIn = _tokensIn,
|
||||
TokensOut = _tokensOut,
|
||||
ApiRetryCount = _apiRetryCount,
|
||||
Blocks = _blocks,
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter StreamAnalyzerTests`
|
||||
Expected: PASS (all old + 3 new).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs tests/ClaudeDo.Worker.Tests/Runner/StreamAnalyzerTests.cs
|
||||
git commit -m "feat(roadblock): collect and strip CLAUDEDO_BLOCKED markers in StreamAnalyzer"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: RunResult + ClaudeProcess carry Blocks
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Runner/RunResult.cs`
|
||||
- Modify: `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs:89-113`
|
||||
|
||||
- [ ] **Step 1: Add Blocks to RunResult**
|
||||
|
||||
In `src/ClaudeDo.Worker/Runner/RunResult.cs`, add inside the class:
|
||||
|
||||
```csharp
|
||||
public IReadOnlyList<string> Blocks { get; init; } = Array.Empty<string>();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Populate Blocks in both RunResult returns**
|
||||
|
||||
In `ClaudeProcess.RunAsync`, add `Blocks = streamResult.Blocks,` to **both** the success `RunResult { ... }` (after `TokensOut`) and the error `RunResult { ... }` initializer.
|
||||
|
||||
- [ ] **Step 3: Build to verify it compiles**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Runner/RunResult.cs src/ClaudeDo.Worker/Runner/ClaudeProcess.cs
|
||||
git commit -m "feat(roadblock): carry blocks through RunResult"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Fold roadblocks into the review result
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs` (`HandleSuccess` ~L319-352; add `ComposeReviewResult`)
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs` (create)
|
||||
|
||||
- [ ] **Step 1: Write failing tests for the compose helper**
|
||||
|
||||
Create `tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Worker.Runner;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Runner;
|
||||
|
||||
public class ReviewResultTests
|
||||
{
|
||||
[Fact]
|
||||
public void No_blocks_returns_result_unchanged()
|
||||
{
|
||||
Assert.Equal("done", TaskRunner.ComposeReviewResult("done", Array.Empty<string>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Blocks_are_appended_as_a_section()
|
||||
{
|
||||
var outp = TaskRunner.ComposeReviewResult("done", new[] { "no creds", "db down" });
|
||||
Assert.Contains("⚠ Roadblocks", outp);
|
||||
Assert.Contains("- no creds", outp);
|
||||
Assert.Contains("- db down", outp);
|
||||
Assert.Contains("done", outp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Null_result_with_blocks_still_lists_them()
|
||||
{
|
||||
var outp = TaskRunner.ComposeReviewResult(null, new[] { "x" });
|
||||
Assert.Contains("⚠ Roadblocks", outp);
|
||||
Assert.Contains("- x", outp);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter ReviewResultTests`
|
||||
Expected: FAIL — `ComposeReviewResult` doesn't exist.
|
||||
|
||||
- [ ] **Step 3: Add ComposeReviewResult and use it in HandleSuccess**
|
||||
|
||||
In `TaskRunner`, add:
|
||||
|
||||
```csharp
|
||||
public static string? ComposeReviewResult(string? result, IReadOnlyList<string> blocks)
|
||||
{
|
||||
if (blocks.Count == 0) return result;
|
||||
var section = "⚠ Roadblocks reported during the run:\n"
|
||||
+ string.Join('\n', blocks.Select(b => $"- {b}"));
|
||||
return string.IsNullOrWhiteSpace(result) ? section : $"{result}\n\n{section}";
|
||||
}
|
||||
```
|
||||
|
||||
In `HandleSuccess`, compute the composed result once and pass it to both terminal writes:
|
||||
|
||||
```csharp
|
||||
var finishedAt = DateTime.UtcNow;
|
||||
var reviewResult = ComposeReviewResult(result.ResultMarkdown, result.Blocks);
|
||||
if (task.ParentTaskId is null && task.PlanningPhase == PlanningPhase.None)
|
||||
{
|
||||
await _state.SubmitForReviewAsync(task.Id, finishedAt, reviewResult, CancellationToken.None);
|
||||
await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (waiting for review)", WorkerLogLevel.Success, DateTime.UtcNow);
|
||||
await _broadcaster.TaskFinished(slot, task.Id, "waiting_for_review", finishedAt);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _state.CompleteAsync(task.Id, finishedAt, reviewResult, CancellationToken.None);
|
||||
await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (done)", WorkerLogLevel.Success, DateTime.UtcNow);
|
||||
await _broadcaster.TaskFinished(slot, task.Id, "done", finishedAt);
|
||||
}
|
||||
```
|
||||
|
||||
(Make sure `using System.Linq;` is available — it is, via implicit usings.)
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter ReviewResultTests`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Runner/TaskRunner.cs tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs
|
||||
git commit -m "feat(roadblock): surface reported roadblocks in the review result"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Files-settings UI exposes the new prompt files
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs`
|
||||
- Modify: the Files settings view (find with: `Grep "SystemPromptPath" src/ClaudeDo.Ui` → the `.axaml` binding to `OpenPromptCommand`)
|
||||
|
||||
- [ ] **Step 1: Replace the prompt-path properties**
|
||||
|
||||
In `FilesSettingsTabViewModel`, replace the three path properties with the new set (drop Agent, add the rest):
|
||||
|
||||
```csharp
|
||||
public string SystemPromptPath { get; } = PromptFiles.PathFor(PromptKind.System);
|
||||
public string PlanningPromptPath { get; } = PromptFiles.PathFor(PromptKind.Planning);
|
||||
public string PlanningInitialPromptPath { get; } = PromptFiles.PathFor(PromptKind.PlanningInitial);
|
||||
public string RetryPromptPath { get; } = PromptFiles.PathFor(PromptKind.Retry);
|
||||
public string DailyPrepPromptPath { get; } = PromptFiles.PathFor(PromptKind.DailyPrep);
|
||||
public string WeeklyReportPromptPath { get; } = PromptFiles.PathFor(PromptKind.WeeklyReport);
|
||||
```
|
||||
|
||||
(`OpenPromptCommand` already parses the `PromptKind` name from its parameter, so no command change is needed.)
|
||||
|
||||
- [ ] **Step 2: Update the view**
|
||||
|
||||
Open the Files settings `.axaml`. For the existing System/Planning/Agent rows: keep System, keep Planning, **remove the Agent row**, and add four rows mirroring the System row's markup — each binding its label/path to the new property and passing the matching `PromptKind` name as the `OpenPromptCommand` parameter:
|
||||
|
||||
- `Planning` (system) → "Planning system prompt", `PlanningPromptPath`, parameter `Planning`
|
||||
- `PlanningInitial` → "Planning kickoff prompt", `PlanningInitialPromptPath`, parameter `PlanningInitial`
|
||||
- `Retry` → "Retry prompt", `RetryPromptPath`, parameter `Retry`
|
||||
- `DailyPrep` → "Daily-prep prompt", `DailyPrepPromptPath`, parameter `DailyPrep`
|
||||
- `WeeklyReport` → "Weekly-report prompt", `WeeklyReportPromptPath`, parameter `WeeklyReport`
|
||||
|
||||
Use the exact same control template as the existing System row (same button + `CommandParameter` shape); only the bound property, label text, and parameter string differ.
|
||||
|
||||
- [ ] **Step 3: Build the UI project**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Visual check (manual — flag for user)**
|
||||
|
||||
Start the app, open Settings → Files tab. Confirm six "Open" prompt buttons appear (System, Planning system, Planning kickoff, Retry, Daily-prep, Weekly-report), no Agent row, and each opens/seeds the right file under `~/.todo-app/prompts/`. **This step cannot be verified by the agent — ask the user to confirm visually.**
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs src/ClaudeDo.Ui/Views/**/*Files*.axaml
|
||||
git commit -m "feat(ui): expose all editable prompt files, drop agent prompt"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 11: Full build + test sweep
|
||||
|
||||
- [ ] **Step 1: Build worker + app**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
```
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run all affected test projects**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
```
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Update docs**
|
||||
|
||||
Update `docs/prompts-inventory.md` to note the externalized files and that `agent.md`/`planning.md` are retired in favor of `system.md`/`planning-system.md`. Note `CLAUDEDO_BLOCKED:` in the inventory.
|
||||
|
||||
```bash
|
||||
git add docs/prompts-inventory.md
|
||||
git commit -m "docs: refresh prompt inventory for externalized prompts + roadblock marker"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-review notes
|
||||
|
||||
- **Spec coverage:** system.md collapse (T2), planning prompts (T4), retry (T3), daily-prep English (T5), weekly-report + data pointer (T6), templating/`Render` (T1), roadblock detect/strip/route (T7–T9), file layout + migration via `EnsureExists`/new `PathFor` (T1), UI surface (T10). The "Out-of-scope improvements" system.md section is intentionally **deferred to the child-tasks plan** (it depends on the `SuggestImprovement` tool).
|
||||
- **Migration:** old `planning.md`/`agent.md` go inert automatically — `TaskRunner` no longer reads agent (T2), planning now reads `planning-system.md` (T1 PathFor). No code deletes the old files; harmless.
|
||||
- **Determinism:** content tests target `DefaultFor`/`RenderTemplate` (pure, no disk). Consumers fall back to the same default when no user file exists.
|
||||
File diff suppressed because it is too large
Load Diff
725
docs/superpowers/plans/2026-06-04-debug-logging-traceability.md
Normal file
725
docs/superpowers/plans/2026-06-04-debug-logging-traceability.md
Normal file
@@ -0,0 +1,725 @@
|
||||
# Debug Logging & Frontend↔Backend Traceability Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build-configuration-driven logging — verbose in Debug builds (Rider run button), minimal `Warning`+ in Release (installed app) — with both processes writing one shared `claudedo-.log` and a `TaskId` correlation key threading UI→Worker→UI.
|
||||
|
||||
**Architecture:** A new `ClaudeDo.Logging` library owns all Serilog setup: a `BuildConfig.IsDebug` runtime check (via the entry assembly's `DebuggableAttribute`, no `#if DEBUG`), a default-`TaskId` enricher, and a `LoggingSetup.Configure` method that branches sinks/levels on `IsDebug`. Worker and App both call it. `TaskId` rides Serilog `LogContext`, pushed at the per-task entry points on each side.
|
||||
|
||||
**Tech Stack:** .NET 8, Serilog (core + File + Console sinks), Serilog.Extensions.Logging (App bridge), Serilog.AspNetCore (Worker, already present), xUnit.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create the `ClaudeDo.Logging` project
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ClaudeDo.Logging/ClaudeDo.Logging.csproj`
|
||||
- Create: `src/ClaudeDo.Logging/Placeholder.cs` (temporary, removed in Task 2)
|
||||
- Modify: `ClaudeDo.slnx`
|
||||
|
||||
- [ ] **Step 1: Create the csproj**
|
||||
|
||||
Create `src/ClaudeDo.Logging/ClaudeDo.Logging.csproj`:
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ClaudeDo.Worker.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
```
|
||||
|
||||
> If NuGet reports a version conflict between `Serilog 4.1.0` and the `Serilog` core pulled transitively by `Serilog.AspNetCore 8.0.3` (Worker), align this `Serilog` version to whatever `Serilog.AspNetCore 8.0.3` resolves (check `dotnet list package --include-transitive`) and rebuild.
|
||||
|
||||
- [ ] **Step 2: Add a temporary placeholder so the project compiles**
|
||||
|
||||
Create `src/ClaudeDo.Logging/Placeholder.cs`:
|
||||
|
||||
```csharp
|
||||
namespace ClaudeDo.Logging;
|
||||
|
||||
internal static class Placeholder;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Register the project in the solution**
|
||||
|
||||
Edit `ClaudeDo.slnx` — add inside the `/src/` folder, after the `ClaudeDo.Localization` line:
|
||||
|
||||
```xml
|
||||
<Project Path="src/ClaudeDo.Logging/ClaudeDo.Logging.csproj" />
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build the new project**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Logging/ClaudeDo.Logging.csproj -c Release`
|
||||
Expected: Build succeeded.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Logging/ClaudeDo.Logging.csproj src/ClaudeDo.Logging/Placeholder.cs ClaudeDo.slnx
|
||||
git commit -m "build(logging): scaffold ClaudeDo.Logging project"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: `DefaultTaskIdEnricher` (TDD)
|
||||
|
||||
Adds `TaskId = "-"` to any log event that doesn't already carry a `TaskId` property, so the `[{TaskId}]` column never renders the raw token. A pushed `LogContext` value takes precedence (because `Enrich.FromLogContext()` runs first and the property is then already present).
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs`
|
||||
- Delete: `src/ClaudeDo.Logging/Placeholder.cs`
|
||||
- Create: `tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs`
|
||||
- Modify: `tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj` (add project reference)
|
||||
|
||||
- [ ] **Step 1: Reference `ClaudeDo.Logging` from the test project**
|
||||
|
||||
Edit `tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj` — add to the existing `ProjectReference` ItemGroup:
|
||||
|
||||
```xml
|
||||
<ProjectReference Include="..\..\src\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the failing test**
|
||||
|
||||
Create `tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Logging;
|
||||
using Serilog;
|
||||
using Serilog.Context;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Logging;
|
||||
|
||||
public sealed class DefaultTaskIdEnricherTests
|
||||
{
|
||||
private sealed class CollectingSink : ILogEventSink
|
||||
{
|
||||
public List<LogEvent> Events { get; } = new();
|
||||
public void Emit(LogEvent logEvent) => Events.Add(logEvent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddsDash_WhenNoTaskIdInScope()
|
||||
{
|
||||
var sink = new CollectingSink();
|
||||
using var logger = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.With(new DefaultTaskIdEnricher())
|
||||
.WriteTo.Sink(sink)
|
||||
.CreateLogger();
|
||||
|
||||
logger.Information("hello");
|
||||
|
||||
var prop = Assert.Single(sink.Events).Properties["TaskId"];
|
||||
Assert.Equal("\"-\"", prop.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeepsPushedTaskId_WhenInScope()
|
||||
{
|
||||
var sink = new CollectingSink();
|
||||
using var logger = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.With(new DefaultTaskIdEnricher())
|
||||
.WriteTo.Sink(sink)
|
||||
.CreateLogger();
|
||||
|
||||
using (LogContext.PushProperty("TaskId", "task-42"))
|
||||
logger.Information("hello");
|
||||
|
||||
var prop = Assert.Single(sink.Events).Properties["TaskId"];
|
||||
Assert.Equal("\"task-42\"", prop.ToString());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the test to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter DefaultTaskIdEnricherTests`
|
||||
Expected: FAIL — `DefaultTaskIdEnricher` does not exist (compile error).
|
||||
|
||||
- [ ] **Step 4: Implement the enricher and remove the placeholder**
|
||||
|
||||
Delete `src/ClaudeDo.Logging/Placeholder.cs`.
|
||||
|
||||
Create `src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs`:
|
||||
|
||||
```csharp
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace ClaudeDo.Logging;
|
||||
|
||||
/// <summary>Ensures every log event carries a TaskId property (defaulting to "-")
|
||||
/// so the output template's [{TaskId}] column never renders the raw token.</summary>
|
||||
public sealed class DefaultTaskIdEnricher : ILogEventEnricher
|
||||
{
|
||||
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
|
||||
{
|
||||
if (!logEvent.Properties.ContainsKey("TaskId"))
|
||||
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TaskId", "-"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the test to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter DefaultTaskIdEnricherTests`
|
||||
Expected: PASS (2 tests).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj
|
||||
git rm src/ClaudeDo.Logging/Placeholder.cs
|
||||
git commit -m "feat(logging): default TaskId enricher with passing tests"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: `BuildConfig.IsDebug`
|
||||
|
||||
Detects whether the entry assembly was compiled in the Debug configuration (JIT optimizer disabled) — the runtime replacement for `#if DEBUG`.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ClaudeDo.Logging/BuildConfig.cs`
|
||||
- Create: `tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
The test asserts the property returns *some* bool without throwing, and that the underlying detection logic agrees with the test assembly's own `DebuggableAttribute` (the test runs under whatever config `dotnet test` used). We assert the helper's result equals a locally-computed expectation so it passes under both Debug and Release test runs.
|
||||
|
||||
Create `tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs`:
|
||||
|
||||
```csharp
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using ClaudeDo.Logging;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Logging;
|
||||
|
||||
public sealed class BuildConfigTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsDebug_MatchesEntryAssemblyDebuggableAttribute()
|
||||
{
|
||||
var entry = Assembly.GetEntryAssembly();
|
||||
var expected = entry?
|
||||
.GetCustomAttribute<DebuggableAttribute>()
|
||||
?.IsJITOptimizerDisabled ?? false;
|
||||
|
||||
Assert.Equal(expected, BuildConfig.IsDebug);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter BuildConfigTests`
|
||||
Expected: FAIL — `BuildConfig` does not exist (compile error).
|
||||
|
||||
- [ ] **Step 3: Implement `BuildConfig`**
|
||||
|
||||
Create `src/ClaudeDo.Logging/BuildConfig.cs`:
|
||||
|
||||
```csharp
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ClaudeDo.Logging;
|
||||
|
||||
/// <summary>Runtime build-configuration detection — the replacement for #if DEBUG.
|
||||
/// Debug builds compile with the JIT optimizer disabled; Release builds enable it.</summary>
|
||||
public static class BuildConfig
|
||||
{
|
||||
public static bool IsDebug { get; } =
|
||||
Assembly.GetEntryAssembly()
|
||||
?.GetCustomAttribute<DebuggableAttribute>()
|
||||
?.IsJITOptimizerDisabled ?? false;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the test to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter BuildConfigTests`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Logging/BuildConfig.cs tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs
|
||||
git commit -m "feat(logging): runtime Debug-build detection via DebuggableAttribute"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: `LoggingSetup.Configure`
|
||||
|
||||
The single shared configuration entry point. Applies enrichers, the output template, and branches sinks/levels on `BuildConfig.IsDebug`.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ClaudeDo.Logging/LoggingSetup.cs`
|
||||
- Create: `tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Verifies a configured logger actually writes a `Warning` (emitted in both build configs) to a `claudedo-*.log` file under the given log root.
|
||||
|
||||
Create `tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Logging;
|
||||
using Serilog;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Logging;
|
||||
|
||||
public sealed class LoggingSetupTests
|
||||
{
|
||||
[Fact]
|
||||
public void Configure_WritesSharedLogFile()
|
||||
{
|
||||
var logRoot = Path.Combine(Path.GetTempPath(), "claudedo-logtest-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(logRoot);
|
||||
try
|
||||
{
|
||||
var logger = LoggingSetup.Configure(new LoggerConfiguration(), "test", logRoot).CreateLogger();
|
||||
logger.Warning("marker-{Marker}", "xyz");
|
||||
logger.Dispose(); // flush + release the file handle
|
||||
|
||||
var files = Directory.GetFiles(logRoot, "claudedo-*.log");
|
||||
var file = Assert.Single(files);
|
||||
var contents = File.ReadAllText(file);
|
||||
Assert.Contains("marker-", contents);
|
||||
Assert.Contains("test/", contents); // {Process} tag in the template
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { Directory.Delete(logRoot, recursive: true); } catch { /* best effort */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter LoggingSetupTests`
|
||||
Expected: FAIL — `LoggingSetup` does not exist (compile error).
|
||||
|
||||
- [ ] **Step 3: Implement `LoggingSetup`**
|
||||
|
||||
Create `src/ClaudeDo.Logging/LoggingSetup.cs`:
|
||||
|
||||
```csharp
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace ClaudeDo.Logging;
|
||||
|
||||
public static class LoggingSetup
|
||||
{
|
||||
private const string OutputTemplate =
|
||||
"[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Process}/{SourceContext} [{TaskId}] {Message:lj}{NewLine}{Exception}";
|
||||
|
||||
/// <summary>Apply the shared ClaudeDo logging configuration.
|
||||
/// Debug builds: Debug level, console + shared file. Release builds: Warning level, shared file only.</summary>
|
||||
/// <param name="processTag">"worker" or "app" — tags every line so the interleaved file is readable.</param>
|
||||
/// <param name="logRoot">Directory for the shared claudedo-.log (created if missing).</param>
|
||||
public static LoggerConfiguration Configure(LoggerConfiguration cfg, string processTag, string logRoot)
|
||||
{
|
||||
Directory.CreateDirectory(logRoot);
|
||||
var logFile = Path.Combine(logRoot, "claudedo-.log");
|
||||
|
||||
cfg.Enrich.FromLogContext()
|
||||
.Enrich.WithProperty("Process", processTag)
|
||||
.Enrich.With(new DefaultTaskIdEnricher());
|
||||
|
||||
if (BuildConfig.IsDebug)
|
||||
{
|
||||
cfg.MinimumLevel.Debug()
|
||||
.WriteTo.Console(outputTemplate: OutputTemplate)
|
||||
.WriteTo.File(
|
||||
logFile,
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: 2,
|
||||
shared: true,
|
||||
outputTemplate: OutputTemplate);
|
||||
}
|
||||
else
|
||||
{
|
||||
cfg.MinimumLevel.Warning()
|
||||
.WriteTo.File(
|
||||
logFile,
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: 2,
|
||||
shared: true,
|
||||
outputTemplate: OutputTemplate);
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the test to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter LoggingSetupTests`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Logging/LoggingSetup.cs tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs
|
||||
git commit -m "feat(logging): shared LoggingSetup with build-config sink branching"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Wire the Worker to the shared setup
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/ClaudeDo.Worker.csproj`
|
||||
- Modify: `src/ClaudeDo.Worker/Program.cs:34-40`
|
||||
|
||||
- [ ] **Step 1: Add the project reference**
|
||||
|
||||
Edit `src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` — add to the existing `ProjectReference` ItemGroup (the one with `ClaudeDo.Data`):
|
||||
|
||||
```xml
|
||||
<ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace the inline Serilog config**
|
||||
|
||||
In `src/ClaudeDo.Worker/Program.cs`, replace lines 34-40:
|
||||
|
||||
```csharp
|
||||
builder.Host.UseSerilog((ctx, lc) => lc
|
||||
.MinimumLevel.Information()
|
||||
.WriteTo.File(
|
||||
System.IO.Path.Combine(logRoot, "worker-.log"),
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: 7,
|
||||
shared: true));
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```csharp
|
||||
builder.Host.UseSerilog((ctx, lc) =>
|
||||
ClaudeDo.Logging.LoggingSetup.Configure(lc, "worker", logRoot));
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build the Worker**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||
Expected: Build succeeded. (If the Worker is running and locks the Debug output, this Release build is unaffected.)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/ClaudeDo.Worker.csproj src/ClaudeDo.Worker/Program.cs
|
||||
git commit -m "feat(logging): route Worker logging through shared LoggingSetup"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Wire the App/Ui (currently log-silent) to the shared setup
|
||||
|
||||
The App uses a plain `ServiceCollection` with **no** logging registered. Add the Serilog→`ILogger` bridge so all `ILogger<T>` injections across App/Ui flow to the shared sinks, and flush on shutdown.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.App/ClaudeDo.App.csproj`
|
||||
- Modify: `src/ClaudeDo.App/Program.cs`
|
||||
|
||||
- [ ] **Step 1: Add packages and the project reference**
|
||||
|
||||
Edit `src/ClaudeDo.App/ClaudeDo.App.csproj` — add to the package `ItemGroup`:
|
||||
|
||||
```xml
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
```
|
||||
|
||||
and to the `ProjectReference` ItemGroup:
|
||||
|
||||
```xml
|
||||
<ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the logging registration in `BuildServices`**
|
||||
|
||||
In `src/ClaudeDo.App/Program.cs`, inside `BuildServices()`, immediately after the `var sc = new ServiceCollection();` line (currently line 78), insert:
|
||||
|
||||
```csharp
|
||||
var logRoot = Path.Combine(Path.GetDirectoryName(dbPath)!, "logs");
|
||||
var serilogLogger = ClaudeDo.Logging.LoggingSetup
|
||||
.Configure(new Serilog.LoggerConfiguration(), "app", logRoot)
|
||||
.CreateLogger();
|
||||
sc.AddLogging(b => b.AddSerilog(serilogLogger, dispose: true));
|
||||
```
|
||||
|
||||
Add these usings to the top of `Program.cs` (the `AddSerilog` `ILoggingBuilder` extension lives in the `Serilog` namespace; `AddLogging` lives in `Microsoft.Extensions.DependencyInjection`, already imported):
|
||||
|
||||
```csharp
|
||||
using Serilog;
|
||||
using Microsoft.Extensions.Logging;
|
||||
```
|
||||
|
||||
> `dbPath` is already computed just above (`var dbPath = Paths.Expand(settings.DbPath);`). Its parent directory is `~/.todo-app`, so `logs` sits beside the Worker's log root.
|
||||
|
||||
- [ ] **Step 3: Build the App**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded (pulls in Ui + Data + Logging).
|
||||
|
||||
- [ ] **Step 4: Verify manually from Rider (visual-verification gap)**
|
||||
|
||||
This is a Debug-build behavior that cannot be asserted in a Release test run. Launch the App from Rider's run button and confirm:
|
||||
- A `claudedo-*.log` appears in `~/.todo-app/logs/`.
|
||||
- Console output (Rider run window) shows `Debug`-level lines tagged `app/...`.
|
||||
|
||||
Flag to the user that this step needs their eyes.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.App/ClaudeDo.App.csproj src/ClaudeDo.App/Program.cs
|
||||
git commit -m "feat(logging): wire App/Ui logging to shared LoggingSetup"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Push `TaskId` into `LogContext` in the Worker
|
||||
|
||||
Wraps the two per-task entry points so every nested log line (runner, state service, worktree, planning) carries the task's id automatically.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs:47` (`RunAsync`) and `:171` (`ContinueAsync`)
|
||||
|
||||
- [ ] **Step 1: Add the using directive**
|
||||
|
||||
In `src/ClaudeDo.Worker/Runner/TaskRunner.cs`, add to the top usings:
|
||||
|
||||
```csharp
|
||||
using Serilog.Context;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Push TaskId at the top of `RunAsync`**
|
||||
|
||||
In `RunAsync` (line 47), insert as the very first statement of the method body (before `string? mcpToken = null;`):
|
||||
|
||||
```csharp
|
||||
using var _taskScope = LogContext.PushProperty("TaskId", task.Id);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Push TaskId at the top of `ContinueAsync`**
|
||||
|
||||
In `ContinueAsync` (line 171), insert as the very first statement of the method body (before `TaskEntity task;`). The parameter is `taskId`:
|
||||
|
||||
```csharp
|
||||
using var _taskScope = LogContext.PushProperty("TaskId", taskId);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build the Worker**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||
Expected: Build succeeded.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Runner/TaskRunner.cs
|
||||
git commit -m "feat(logging): tag Worker task execution with TaskId for traceability"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Push `TaskId` and add trace lines on the App side
|
||||
|
||||
`WorkerClient` currently logs nothing. Inject `ILogger<WorkerClient>`, add a small helper that pushes `TaskId` + emits a `Debug` trace line, and route the fire-and-forget task-targeted hub calls through it. This produces the UI half of the UI→Worker→UI trace under a shared `TaskId`.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||
- Modify: `src/ClaudeDo.App/Program.cs:101` (registration)
|
||||
|
||||
- [ ] **Step 1: Add usings and the logger field/ctor param**
|
||||
|
||||
In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, add to the usings:
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Context;
|
||||
```
|
||||
|
||||
Add a field beside `private readonly HubConnection _hub;` (line 32):
|
||||
|
||||
```csharp
|
||||
private readonly ILogger<WorkerClient> _logger;
|
||||
```
|
||||
|
||||
Change the constructor signature (line 68) from:
|
||||
|
||||
```csharp
|
||||
public WorkerClient(string signalRUrl)
|
||||
{
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```csharp
|
||||
public WorkerClient(string signalRUrl, ILogger<WorkerClient> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the task-scoped invoke helper**
|
||||
|
||||
In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, add this private method next to `TryInvokeAsync` (after line 241):
|
||||
|
||||
```csharp
|
||||
/// <summary>Invoke a task-targeted hub method under a TaskId log scope, emitting a debug trace line.</summary>
|
||||
private async Task InvokeForTaskAsync(string taskId, string method, params object?[] args)
|
||||
{
|
||||
using (LogContext.PushProperty("TaskId", taskId))
|
||||
{
|
||||
_logger.LogDebug("UI invoking {Method} for task {TaskId}", method, taskId);
|
||||
await _hub.InvokeCoreAsync(method, args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Route the fire-and-forget task actions through the helper**
|
||||
|
||||
In the same file, replace each of these method bodies:
|
||||
|
||||
`RunNowAsync` (line 243):
|
||||
```csharp
|
||||
public Task RunNowAsync(string taskId)
|
||||
=> InvokeForTaskAsync(taskId, "RunNow", taskId);
|
||||
```
|
||||
|
||||
`ContinueTaskAsync` (line 248):
|
||||
```csharp
|
||||
public Task ContinueTaskAsync(string taskId, string followUpPrompt)
|
||||
=> InvokeForTaskAsync(taskId, "ContinueTask", taskId, followUpPrompt);
|
||||
```
|
||||
|
||||
`ResetTaskAsync` (line 253):
|
||||
```csharp
|
||||
public Task ResetTaskAsync(string taskId)
|
||||
=> InvokeForTaskAsync(taskId, "ResetTask", taskId);
|
||||
```
|
||||
|
||||
`CancelTaskAsync` (line 267):
|
||||
```csharp
|
||||
public Task CancelTaskAsync(string taskId)
|
||||
=> InvokeForTaskAsync(taskId, "CancelTask", taskId);
|
||||
```
|
||||
|
||||
`ApproveReviewAsync` (line 389):
|
||||
```csharp
|
||||
public Task ApproveReviewAsync(string taskId)
|
||||
=> InvokeForTaskAsync(taskId, "ApproveReview", taskId);
|
||||
```
|
||||
|
||||
`RejectReviewToQueueAsync` (line 394):
|
||||
```csharp
|
||||
public Task RejectReviewToQueueAsync(string taskId, string feedback)
|
||||
=> InvokeForTaskAsync(taskId, "RejectReviewToQueue", taskId, feedback);
|
||||
```
|
||||
|
||||
`RejectReviewToIdleAsync` (line 399):
|
||||
```csharp
|
||||
public Task RejectReviewToIdleAsync(string taskId)
|
||||
=> InvokeForTaskAsync(taskId, "RejectReviewToIdle", taskId);
|
||||
```
|
||||
|
||||
`CancelReviewAsync` (line 404):
|
||||
```csharp
|
||||
public Task CancelReviewAsync(string taskId)
|
||||
=> InvokeForTaskAsync(taskId, "CancelReview", taskId);
|
||||
```
|
||||
|
||||
> These all previously did `await _hub.InvokeAsync(method, ...)` with no return value, so converting them to expression-bodied delegations preserves behavior. Do **not** touch methods that return DTOs (e.g. `MergeTaskAsync`) or the planning methods — keep this change scoped to the void task actions above.
|
||||
|
||||
- [ ] **Step 4: Update the DI registration to pass the logger**
|
||||
|
||||
In `src/ClaudeDo.App/Program.cs`, replace line 101:
|
||||
|
||||
```csharp
|
||||
sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService<AppSettings>().SignalRUrl));
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```csharp
|
||||
sc.AddSingleton(sp => new WorkerClient(
|
||||
sp.GetRequiredService<AppSettings>().SignalRUrl,
|
||||
sp.GetRequiredService<ILogger<WorkerClient>>()));
|
||||
```
|
||||
|
||||
Add `using Microsoft.Extensions.Logging;` to the top of `Program.cs` if not already present.
|
||||
|
||||
- [ ] **Step 5: Build the App**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded.
|
||||
|
||||
> Note: `WorkerClient` is faked in tests via the `IWorkerClient` *interface* (hand-rolled fakes implement the interface, they do not subclass `WorkerClient`). This change adds a ctor parameter to the concrete class only and does not alter `IWorkerClient`, so the fakes are unaffected. Confirm by building the test projects in the next step.
|
||||
|
||||
- [ ] **Step 6: Build the test projects to confirm fakes still compile**
|
||||
|
||||
Run: `dotnet build tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release && dotnet build tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||
Expected: Build succeeded for both.
|
||||
|
||||
- [ ] **Step 7: Run the full Worker.Tests suite**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release`
|
||||
Expected: PASS (all existing tests + the 4 new logging tests).
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Services/WorkerClient.cs src/ClaudeDo.App/Program.cs
|
||||
git commit -m "feat(logging): tag UI task actions with TaskId + debug trace lines"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final verification
|
||||
|
||||
- [ ] **Build the whole desktop + worker stack in Release:**
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
```
|
||||
|
||||
- [ ] **Run the logging tests:**
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "FullyQualifiedName~Logging"
|
||||
```
|
||||
Expected: PASS (DefaultTaskIdEnricher × 2, BuildConfig × 1, LoggingSetup × 1).
|
||||
|
||||
- [ ] **Manual smoke test (visual-verification gap — needs the user):**
|
||||
1. Run the Worker and App from Rider (Debug build). Confirm both write to one `~/.todo-app/logs/claudedo-*.log` with `app/...` and `worker/...` lines.
|
||||
2. Run a task; grep that file for the task's id — confirm UI (`UI invoking RunNow…`) and Worker lines share the same `[<taskId>]`.
|
||||
3. Build/install the Release app; confirm the log is near-silent (no `Debug`/`Information` noise, `Warning`+ only) and no console window logging.
|
||||
1107
docs/superpowers/plans/2026-06-04-inherited-settings-and-turns.md
Normal file
1107
docs/superpowers/plans/2026-06-04-inherited-settings-and-turns.md
Normal file
File diff suppressed because it is too large
Load Diff
147
docs/superpowers/plans/2026-06-04-myday-icons-terminal-reuse.md
Normal file
147
docs/superpowers/plans/2026-06-04-myday-icons-terminal-reuse.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# MyDay Icon Buttons + Terminal Reuse + Sort Icon Fix — Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use `- [ ]`.
|
||||
|
||||
**Goal:** Move the "Clear day" and "Prep log" actions into the MyDay header icon row as icon buttons (broom + list), render the prep log in the real `SessionTerminalView` ("cool terminal") by making that control reusable, and fix the invisible Sort icon.
|
||||
|
||||
**Approved design (chat):**
|
||||
- Header icon row (`TasksIslandView.axaml`, the Sort/Eye/Settings `icon-btn` StackPanel) gets two more `icon-btn`, both `IsVisible="{Binding IsMyDayList}"`, inserted after the Eye button: **broom** (`Icon.Broom`) → `ClearDayCommand`, **list** (`Icon.List`) → `ShowPrepLogCommand`. The two full-width text buttons "Prep log" and "Clear day" are removed. "Tag vorbereiten" stays as the full-width button (already opens the prep view via `PrepRequested`).
|
||||
- `SessionTerminalView` becomes reusable via StyledProperties so it renders both the task `Log` and the prep `PrepLog` with the same terminal look. The prep panel in `DetailsIslandView` embeds it instead of the copied `ItemsControl`.
|
||||
- **Sort icon bug:** `PathIcon` fills geometry; `Icon.Sort` is an open-line path (no enclosed area) → invisible. Replace with a filled geometry. New icons (Broom, List) are authored as filled geometries too.
|
||||
|
||||
**Tech:** Avalonia (PathIcon/StreamGeometry, StyledProperty), CommunityToolkit.Mvvm, xUnit.
|
||||
|
||||
## Build/test
|
||||
`.slnx` needs .NET 9 — build the csproj. Use `-c Release` if a Worker locks Debug.
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||
```
|
||||
GUI cannot be smoke-tested headlessly — note it; the human verifies visuals.
|
||||
|
||||
---
|
||||
|
||||
## Task A: Icons + reusable SessionTerminalView
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (icon geometries)
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml` + `SessionTerminalView.axaml.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` (both embeds)
|
||||
|
||||
- [ ] **Step 1: Fix `Icon.Sort` + add `Icon.Broom`, `Icon.List`** as filled geometries in `IslandStyles.axaml` (in the `Styles.Resources` icon block). Replace the existing `Icon.Sort` line and add the two new ones:
|
||||
|
||||
```xml
|
||||
<!-- Icon.Sort (filled bars, decreasing width) -->
|
||||
<StreamGeometry x:Key="Icon.Sort">M4 6 H20 V8 H4 Z M4 11 H16 V13 H4 Z M4 16 H11 V18 H4 Z</StreamGeometry>
|
||||
|
||||
<!-- Icon.Broom (filled: handle + binding band + flared bristles) -->
|
||||
<StreamGeometry x:Key="Icon.Broom">M11 3 H13 V10 H11 Z M8.5 10 H15.5 V12 H8.5 Z M9 12 H15 L17 21 H7 Z</StreamGeometry>
|
||||
|
||||
<!-- Icon.List (filled: square bullets + lines) -->
|
||||
<StreamGeometry x:Key="Icon.List">M4 5 H6 V7 H4 Z M8 5 H20 V7 H8 Z M4 11 H6 V13 H4 Z M8 11 H20 V13 H8 Z M4 17 H6 V19 H4 Z M8 17 H20 V19 H8 Z</StreamGeometry>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add StyledProperties to `SessionTerminalView`** (code-behind `SessionTerminalView.axaml.cs`). Add public StyledProperties and CLR wrappers:
|
||||
|
||||
```csharp
|
||||
public static readonly StyledProperty<System.Collections.IEnumerable?> EntriesProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, System.Collections.IEnumerable?>(nameof(Entries));
|
||||
public static readonly StyledProperty<string?> LabelProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, string?>(nameof(Label));
|
||||
public static readonly StyledProperty<bool> IsRunningProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(IsRunning));
|
||||
public static readonly StyledProperty<bool> IsDoneProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(IsDone));
|
||||
public static readonly StyledProperty<bool> IsFailedProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(IsFailed));
|
||||
|
||||
public System.Collections.IEnumerable? Entries { get => GetValue(EntriesProperty); set => SetValue(EntriesProperty, value); }
|
||||
public string? Label { get => GetValue(LabelProperty); set => SetValue(LabelProperty, value); }
|
||||
public bool IsRunning { get => GetValue(IsRunningProperty); set => SetValue(IsRunningProperty, value); }
|
||||
public bool IsDone { get => GetValue(IsDoneProperty); set => SetValue(IsDoneProperty, value); }
|
||||
public bool IsFailed { get => GetValue(IsFailedProperty); set => SetValue(IsFailedProperty, value); }
|
||||
```
|
||||
|
||||
Replace the existing auto-scroll hook (which cast `DataContext as DetailsIslandViewModel` and watched `.Log.CollectionChanged`) with one that watches whichever collection `Entries` points at: in `OnPropertyChanged`, when `change.Property == EntriesProperty`, detach the old `INotifyCollectionChanged.CollectionChanged` handler and attach to the new value (if it implements `INotifyCollectionChanged`); the handler scrolls the existing ScrollViewer to the end (reuse the existing scroll logic / named ScrollViewer). Keep the named ScrollViewer's `x:Name`.
|
||||
|
||||
- [ ] **Step 3: Repoint `SessionTerminalView.axaml` internal bindings to the control's own properties.** Give the root `UserControl` `x:Name="Root"`. Change:
|
||||
- the `ItemsControl ItemsSource="{Binding Log}"` → `ItemsSource="{Binding #Root.Entries}"`
|
||||
- the label `TextBlock` `Text="{Binding BranchLine, StringFormat='claude-session · {0}'}"` (or whatever it is) → `Text="{Binding #Root.Label}"`
|
||||
- the LIVE chip `IsVisible="{Binding IsRunning}"` → `{Binding #Root.IsRunning}`; DONE → `#Root.IsDone`; FAILED → `#Root.IsFailed`.
|
||||
Keep the `LogLineViewModel` item template as-is (it binds the item, not the VM). The `x:DataType` can stay `DetailsIslandViewModel` (element-name bindings to `#Root` don't depend on it) or be removed if it causes compile issues — verify the build.
|
||||
|
||||
- [ ] **Step 4: Update both embeds in `DetailsIslandView.axaml`.**
|
||||
- Task embed (currently `<islands:SessionTerminalView MaxHeight="420"/>`):
|
||||
```xml
|
||||
<islands:SessionTerminalView MaxHeight="420"
|
||||
Entries="{Binding Log}"
|
||||
Label="{Binding BranchLine, StringFormat='claude-session · {0}'}"
|
||||
IsRunning="{Binding IsRunning}" IsDone="{Binding IsDone}" IsFailed="{Binding IsFailed}"/>
|
||||
```
|
||||
(Use the exact label binding the old internal header used — match the prior `StringFormat` text precisely so the task view is visually unchanged.)
|
||||
- Prep panel: replace the whole copied `ItemsControl` (and its surrounding `ScrollViewer`/title) with:
|
||||
```xml
|
||||
<islands:SessionTerminalView
|
||||
Entries="{Binding PrepLog}" Label="daily-prep"
|
||||
IsRunning="{Binding IsPrepRunning}"/>
|
||||
```
|
||||
Keep the panel wrapper `<Panel IsVisible="{Binding IsPrepMode}">`. Drop the now-redundant `details.prepTitle` title TextBlock (the terminal header shows the `daily-prep` label). Leave the `details.prepTitle` locale key in place (harmless) OR remove it from both en/de if you prefer — if removing, run the localization test.
|
||||
|
||||
- [ ] **Step 5: Build the App; confirm no binding/compile errors.**
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
```
|
||||
(The existing DetailsIsland prep tests must still pass — `PrepLog`/`IsPrepMode`/`ShowPrep` are unchanged.)
|
||||
|
||||
- [ ] **Step 6: Commit** (stage only Task A files; do NOT `git add -A`):
|
||||
```bash
|
||||
git commit -m "feat(daily-prep): reuse SessionTerminal for prep log; fix invisible Sort icon; add Broom/List icons"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task B: MyDay header icon buttons
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml`
|
||||
- Modify: `src/ClaudeDo.Localization/locales/en.json`, `de.json`
|
||||
|
||||
Depends on Task A (uses `Icon.Broom` / `Icon.List`).
|
||||
|
||||
- [ ] **Step 1: Add two `icon-btn` to the header icon StackPanel** (the one with Sort/Eye/Settings), inserted right after the Eye button and before Settings, both MyDay-only:
|
||||
|
||||
```xml
|
||||
<Button Classes="icon-btn" IsVisible="{Binding IsMyDayList}"
|
||||
Command="{Binding ClearDayCommand}" ToolTip.Tip="{loc:Tr tasks.clearDayTip}">
|
||||
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Broom}"/>
|
||||
</Button>
|
||||
<Button Classes="icon-btn" IsVisible="{Binding IsMyDayList}"
|
||||
Command="{Binding ShowPrepLogCommand}" ToolTip.Tip="{loc:Tr tasks.prepLogTip}">
|
||||
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.List}"/>
|
||||
</Button>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Remove the two full-width buttons** "Prep log" (`ShowPrepLogCommand`) and "Clear day" (`ClearDayCommand`) from the DockPanel button stack. Keep the "Prepare day" (`PrepareDayCommand`) full-width button and the Notes pinned-row button.
|
||||
|
||||
- [ ] **Step 3: Locales.** Add `tasks.clearDayTip` (en "Clear day", de "Tag leeren") and `tasks.prepLogTip` (en "Prep log", de "Vorbereitungs-Log") to both json files. Remove the now-unused `tasks.clearDay` and `tasks.prepLog` keys from both (keep en/de in parity).
|
||||
|
||||
- [ ] **Step 4: Build + test.**
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Manual smoke (human):** on MyDay the header shows Sort (now visible) + Eye + Broom + List + Settings; broom clears the day; list opens the prep terminal; "Tag vorbereiten" opens the prep terminal and streams; the three MyDay-only controls hide on other lists; the task session terminal still renders normally.
|
||||
|
||||
- [ ] **Step 6: Commit** (stage only Task B files):
|
||||
```bash
|
||||
git commit -m "feat(daily-prep): move Clear-day and Prep-log into MyDay header icon row"
|
||||
```
|
||||
|
||||
## Notes / risks
|
||||
- Element-name bindings (`#Root.*`) require the `UserControl` to have `x:Name="Root"`; verify compiled bindings accept them (they do in Avalonia).
|
||||
- The auto-scroll hook must re-subscribe when `Entries` changes; without it the prep log won't auto-scroll.
|
||||
- `ClearDayCommand` / `ShowPrepLogCommand` already exist on `TasksIslandViewModel` — no VM changes; existing VM tests remain valid.
|
||||
120
docs/superpowers/plans/2026-06-04-plan-day-in-log-window.md
Normal file
120
docs/superpowers/plans/2026-06-04-plan-day-in-log-window.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Move "Plan day" into the Prep-Log Window — Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use `- [ ]`.
|
||||
|
||||
**Goal:** Guard daily-prep planning behind a second click. The MyDay header's full-width "Tag vorbereiten" button is removed; instead the user opens the prep-log window (list icon), sees the last run or an empty-state hint, and clicks a **"Plan day"** button inside that window to run the prep.
|
||||
|
||||
**Approved flow:** Header list-icon (`ShowPrepLogCommand`) opens the prep window → if empty, an empty-state hint shows → "Plan day" button in the window runs `RunDailyPrepNowAsync()`.
|
||||
|
||||
**Tech:** Avalonia + CommunityToolkit.Mvvm, xUnit.
|
||||
|
||||
## Build/test
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||
```
|
||||
GUI not headlessly verifiable — note it; human verifies visuals.
|
||||
|
||||
---
|
||||
|
||||
## Task: relocate planning trigger + empty-state
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` (remove PrepareDay)
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml` (remove header button)
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` (PlanDayCommand + empty-state)
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` (prep panel toolbar + empty hint)
|
||||
- Modify: `src/ClaudeDo.Localization/locales/en.json`, `de.json`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPrepModeTests.cs`, and the existing `TasksIslandDailyPrepTests.cs` (remove the obsolete prepare test)
|
||||
|
||||
- [ ] **Step 1: Write/adjust tests first.**
|
||||
- In `DetailsIslandPrepModeTests.cs` add:
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task PlanDayCommand_calls_worker()
|
||||
{
|
||||
var stub = new StubWorkerClient();
|
||||
var vm = NewDetailsVm(stub);
|
||||
await vm.PlanDayCommand.ExecuteAsync(null);
|
||||
Assert.Equal(1, stub.RunDailyPrepNowCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowPrepEmptyState_true_when_empty_and_not_running()
|
||||
{
|
||||
var vm = NewDetailsVm(new StubWorkerClient());
|
||||
Assert.True(vm.ShowPrepEmptyState);
|
||||
}
|
||||
```
|
||||
`StubWorkerClient` needs a `RunDailyPrepNowCalls` counter incremented in `RunDailyPrepNowAsync` (add if missing; it currently likely returns `Task.FromResult(true)` — keep that and bump a counter).
|
||||
- In `TasksIslandDailyPrepTests.cs` **remove** `PrepareDayCommand_raises_PrepRequested` (the command is being deleted). Keep `ClearDayCommand_calls_worker`.
|
||||
|
||||
- [ ] **Step 2: Run — expect FAIL/compile error.**
|
||||
|
||||
- [ ] **Step 3: `TasksIslandViewModel` — remove planning trigger.**
|
||||
- Delete the `PrepareDayAsync` `[RelayCommand]` entirely.
|
||||
- Keep the `PrepRequested` event and `ShowPrepLog` command (the list icon still raises `PrepRequested` to open the window).
|
||||
- Grep the VM for any remaining `PrepareDay` references and remove them.
|
||||
|
||||
- [ ] **Step 4: `TasksIslandView.axaml` — remove the header button.** Delete the full-width "Prepare day" `<Button … Command="{Binding PrepareDayCommand}" …>`. Leave the Notes pinned-row button, and the header icon buttons (broom = ClearDay, list = ShowPrepLog) untouched.
|
||||
|
||||
- [ ] **Step 5: `DetailsIslandViewModel` — add PlanDayCommand + empty-state.**
|
||||
- Add:
|
||||
```csharp
|
||||
[RelayCommand]
|
||||
private async Task PlanDayAsync()
|
||||
{
|
||||
if (_worker is null) return;
|
||||
try { await _worker.RunDailyPrepNowAsync(); }
|
||||
catch { /* worker offline; PrepStarted/PrepLine will reconcile */ }
|
||||
}
|
||||
|
||||
public bool ShowPrepEmptyState => !IsPrepRunning && PrepLog.Count == 0;
|
||||
```
|
||||
- Notify `ShowPrepEmptyState`: in the constructor add `PrepLog.CollectionChanged += (_, _) => OnPropertyChanged(nameof(ShowPrepEmptyState));`, and add `partial void OnIsPrepRunningChanged(bool value) => OnPropertyChanged(nameof(ShowPrepEmptyState));`.
|
||||
|
||||
- [ ] **Step 6: `DetailsIslandView.axaml` — prep panel toolbar + empty hint.** In the `<Panel IsVisible="{Binding IsPrepMode}">`, wrap the existing `SessionTerminalView` in a `DockPanel`; dock a top toolbar row with the Plan-day button, and overlay/stack an empty-state hint:
|
||||
```xml
|
||||
<Panel IsVisible="{Binding IsPrepMode}">
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Top" Padding="12,8">
|
||||
<Button Classes="btn primary"
|
||||
Command="{Binding PlanDayCommand}"
|
||||
IsEnabled="{Binding !IsPrepRunning}"
|
||||
Content="{loc:Tr details.planDay}"/>
|
||||
</Border>
|
||||
<Panel>
|
||||
<islands:SessionTerminalView
|
||||
Entries="{Binding PrepLog}" Label="daily-prep"
|
||||
IsRunning="{Binding IsPrepRunning}"/>
|
||||
<TextBlock IsVisible="{Binding ShowPrepEmptyState}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
Text="{loc:Tr details.prepEmpty}"/>
|
||||
</Panel>
|
||||
</DockPanel>
|
||||
</Panel>
|
||||
```
|
||||
(Match the surrounding view's class names/brushes; use the existing button class style seen elsewhere, e.g. `Classes="btn"` — verify `primary` exists, else plain `btn`.)
|
||||
|
||||
- [ ] **Step 7: Locales.** Add `details.planDay` (en "Plan day", de "Tag planen") and `details.prepEmpty` (en "No prep run today yet — click Plan day", de "Heute noch keine Vorbereitung — klick Tag planen") to both json files. Remove the now-unused `tasks.prepareDay` key from both (grep first to confirm no other reference). Keep en/de key parity.
|
||||
|
||||
- [ ] **Step 8: Build + tests.**
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Manual smoke (human):** on MyDay there is no "Tag vorbereiten" button; the list icon opens the prep window showing the empty hint; "Plan day" runs the prep and streams; the hint disappears while running; after restart the persisted last run shows and "Plan day" is available to re-run.
|
||||
|
||||
- [ ] **Step 10: Commit:**
|
||||
```bash
|
||||
git commit -m "feat(daily-prep): trigger planning from inside the prep-log window with an empty-state hint"
|
||||
```
|
||||
|
||||
## Notes / risks
|
||||
- `PrepRequested` and `ShowPrepLogCommand` stay — only `PrepareDayCommand` and its header button are removed.
|
||||
- `ShowPrepEmptyState` must re-notify on both `PrepLog` changes and `IsPrepRunning` changes, else the hint won't hide when a run starts or lines arrive.
|
||||
- Removing `tasks.prepareDay`: confirm via grep it has no remaining references before deleting (keep locale parity or the Localization.Tests parity check fails).
|
||||
208
docs/superpowers/plans/2026-06-04-prep-log-persistence.md
Normal file
208
docs/superpowers/plans/2026-06-04-prep-log-persistence.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Persist Daily-Prep Log Across Restarts — Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use `- [ ]`.
|
||||
|
||||
**Goal:** The prep log currently lives only in memory (`DetailsIslandViewModel.PrepLog`), so after an app restart the prep terminal is empty. Persist the last prep run's output to a file in the worker and load it into the prep terminal when opened.
|
||||
|
||||
**Root cause (confirmed):** `PrimeRunner.FireAsync` streams stdout lines via `_broadcaster.PrepLineAsync(line)` only — it writes no file and stores no record. `PrepLog` is an in-memory `ObservableCollection` populated only by live `PrepLine` events. Nothing persists → empty after restart.
|
||||
|
||||
**Approach:** Worker writes each streamed line to `<appdata>/logs/daily-prep.log` (truncated at run start = last run only) using the existing `LogWriter`. A new hub method `GetLastPrepLog()` returns the file (tail-capped, like `get_task_log`). The UI loads it into `PrepLog` when the prep view opens, but only when `PrepLog` is empty and no run is in progress.
|
||||
|
||||
**Tech:** ASP.NET Core SignalR, Avalonia + CommunityToolkit.Mvvm, xUnit.
|
||||
|
||||
## Build/test
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
```
|
||||
GUI not headlessly verifiable — note it; human verifies visuals.
|
||||
|
||||
## Shared constant
|
||||
The prep-log path must be identical in `PrimeRunner` (writer) and `WorkerHub` (reader). Define it once and reference from both:
|
||||
`Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "logs", "daily-prep.log")`.
|
||||
Add a small static helper so both sides agree, e.g. in `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` (already the prep "home"):
|
||||
```csharp
|
||||
public static string LogPath() =>
|
||||
System.IO.Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "logs", "daily-prep.log");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Worker — write the prep log + serve it
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` (add `LogPath()` helper)
|
||||
- Modify: `src/ClaudeDo.Worker/Prime/PrimeRunner.cs`
|
||||
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs`
|
||||
|
||||
- [ ] **Step 1: Add `DailyPrepPrompt.LogPath()`** (code above).
|
||||
|
||||
- [ ] **Step 2: Write the failing test.** Extend the existing streaming test (or add one) asserting that after `FireAsync` with emitted stdout lines, the file at `DailyPrepPrompt.LogPath()` contains those lines, and that a prior run's content is replaced (truncate-on-start). Since the path is the real app-data logs dir, the test should delete the file first and clean up after; assert exact line content.
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task FireAsync_writes_last_run_to_prep_log_file()
|
||||
{
|
||||
var path = DailyPrepPrompt.LogPath();
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
|
||||
var claude = new FakeClaudeProcess(emitLines: new[] { "lineA", "lineB" }, exitCode: 0, result: "ok");
|
||||
var runner = NewRunner(claude, new RecordingPrimeBroadcaster());
|
||||
await runner.FireAsync(new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null), CancellationToken.None);
|
||||
|
||||
var contents = await File.ReadAllTextAsync(path);
|
||||
Assert.Contains("lineA", contents);
|
||||
Assert.Contains("lineB", contents);
|
||||
|
||||
// Truncation: a second run with different lines replaces the file.
|
||||
var claude2 = new FakeClaudeProcess(emitLines: new[] { "lineC" }, exitCode: 0, result: "ok");
|
||||
var runner2 = NewRunner(claude2, new RecordingPrimeBroadcaster());
|
||||
await runner2.FireAsync(new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null), CancellationToken.None);
|
||||
var after = await File.ReadAllTextAsync(path);
|
||||
Assert.DoesNotContain("lineA", after);
|
||||
Assert.Contains("lineC", after);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run — expect FAIL.**
|
||||
|
||||
- [ ] **Step 4: Write the file in `PrimeRunner.FireAsync`.** After the gate is acquired and before `RunAsync`: compute `var logPath = DailyPrepPrompt.LogPath();`, delete it if present (truncate → last run only), then create `await using var logWriter = new LogWriter(logPath);`. Change the stream callback to write AND broadcast:
|
||||
|
||||
```csharp
|
||||
var logPath = DailyPrepPrompt.LogPath();
|
||||
try { if (File.Exists(logPath)) File.Delete(logPath); } catch { /* best effort */ }
|
||||
await using var logWriter = new LogWriter(logPath);
|
||||
|
||||
await _broadcaster.PrepStartedAsync();
|
||||
// ... build prompt/args/timeoutCts ...
|
||||
var result = await _claude.RunAsync(
|
||||
arguments: args, prompt: prompt, workingDirectory: cwd,
|
||||
onStdoutLine: async line =>
|
||||
{
|
||||
await logWriter.WriteLineAsync(line);
|
||||
await _broadcaster.PrepLineAsync(line);
|
||||
},
|
||||
ct: timeoutCts.Token);
|
||||
```
|
||||
|
||||
Keep the existing `success`/`finally`/`PrepFinishedAsync`/gate logic. `using ClaudeDo.Worker.Runner;` is already present (LogWriter lives there). The `await using` LogWriter disposes (flushes) before the method returns.
|
||||
|
||||
- [ ] **Step 5: Run — expect PASS.** Build the Worker.
|
||||
|
||||
- [ ] **Step 6: Add `WorkerHub.GetLastPrepLog()`** (no ctor change — reads the static path):
|
||||
|
||||
```csharp
|
||||
public Task<string> GetLastPrepLog()
|
||||
{
|
||||
var path = DailyPrepPrompt.LogPath();
|
||||
if (!File.Exists(path)) return Task.FromResult(string.Empty);
|
||||
|
||||
const int maxBytes = 256 * 1024;
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
var text = bytes.Length <= maxBytes
|
||||
? System.Text.Encoding.UTF8.GetString(bytes)
|
||||
: System.Text.Encoding.UTF8.GetString(bytes, bytes.Length - maxBytes, maxBytes);
|
||||
return Task.FromResult(text);
|
||||
}
|
||||
```
|
||||
|
||||
Add `using ClaudeDo.Worker.Prime;` to `WorkerHub.cs` if not present.
|
||||
|
||||
- [ ] **Step 7: Build Worker; run the full Worker.Tests project.**
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Commit** (stage only Task 1 files):
|
||||
```bash
|
||||
git commit -m "feat(daily-prep): persist last prep run to a log file and serve it via GetLastPrepLog"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: UI — load the persisted prep log when opening
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
|
||||
- Modify fakes: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`, `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` (FakeWorkerClient)
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPrepModeTests.cs`
|
||||
|
||||
- [ ] **Step 1: Declare on `IWorkerClient`:** `Task<string> GetLastPrepLogAsync();`
|
||||
|
||||
- [ ] **Step 2: Implement in `WorkerClient`:** `public Task<string> GetLastPrepLogAsync() => _hub.InvokeAsync<string>("GetLastPrepLog");` (match neighbouring call style; if there is a `TryInvokeAsync` helper for resilience, mirror `GetWeekReportAsync` and return `?? string.Empty`).
|
||||
|
||||
- [ ] **Step 3: Update fakes.** Add `public Task<string> GetLastPrepLogAsync() => Task.FromResult(string.Empty);` to both fakes. In `StubWorkerClient`, make it return a settable backing field, e.g. `public string LastPrepLog = ""; public Task<string> GetLastPrepLogAsync() => Task.FromResult(LastPrepLog);`.
|
||||
|
||||
- [ ] **Step 4: Write the failing test.**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task ShowPrep_loads_persisted_log_when_empty()
|
||||
{
|
||||
var stub = new StubWorkerClient { LastPrepLog = "{\"type\":\"assistant\",\"text\":\"restored\"}" };
|
||||
var vm = NewDetailsVm(stub);
|
||||
|
||||
vm.ShowPrep();
|
||||
await Task.Delay(50); // allow the async load to run; or expose the load task to await deterministically
|
||||
|
||||
Assert.NotEmpty(vm.PrepLog);
|
||||
}
|
||||
```
|
||||
|
||||
Prefer determinism over `Task.Delay`: have `ShowPrep` start the load and expose the in-flight `Task` (e.g. a `LoadLastPrepLogAsync()` method the test can call/await directly), then assert. Use whichever the existing test style favors.
|
||||
|
||||
- [ ] **Step 5: Implement load in `DetailsIslandViewModel`.** Add a method and call it from `ShowPrep`:
|
||||
|
||||
```csharp
|
||||
public void ShowPrep()
|
||||
{
|
||||
Bind(null);
|
||||
IsNotesMode = false;
|
||||
IsPrepMode = true;
|
||||
_ = LoadLastPrepLogIfEmptyAsync();
|
||||
}
|
||||
|
||||
private async Task LoadLastPrepLogIfEmptyAsync()
|
||||
{
|
||||
if (_worker is null || IsPrepRunning || PrepLog.Count > 0) return;
|
||||
string text;
|
||||
try { text = await _worker.GetLastPrepLogAsync(); }
|
||||
catch { return; }
|
||||
if (IsPrepRunning || PrepLog.Count > 0) return; // a live run may have started meanwhile
|
||||
foreach (var line in text.Split('\n'))
|
||||
{
|
||||
var trimmed = line.TrimEnd('\r');
|
||||
if (trimmed.Length > 0) AppendStdoutLine(PrepLog, trimmed);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This reuses the existing `AppendStdoutLine(PrepLog, line)` formatter path, so persisted NDJSON renders identically to the live stream. The guards ensure it never overwrites a live run (`PrepStarted` clears `PrepLog` and sets `IsPrepRunning`) or an already-loaded log.
|
||||
|
||||
- [ ] **Step 6: Build App + run UI tests.**
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Manual smoke (human):** run a prep, restart the app, open the prep log on MyDay → the last run's output is shown.
|
||||
|
||||
- [ ] **Step 8: Commit** (stage only Task 2 files):
|
||||
```bash
|
||||
git commit -m "feat(daily-prep): load persisted prep log into the terminal on open"
|
||||
```
|
||||
|
||||
## Notes / risks
|
||||
- `PrimeRunner` writes via the same `LogWriter` pattern `TaskRunner` uses; concurrency behavior matches existing code (no new locking introduced).
|
||||
- Path is shared via `DailyPrepPrompt.LogPath()` so writer and reader never diverge.
|
||||
- Load is guarded (`PrepLog empty && !IsPrepRunning`) to avoid clobbering a live stream — the order of `ShowPrep`'s flag set vs. the async load matters; re-check the guard after the await.
|
||||
- Last run only (file truncated each run); history is out of scope.
|
||||
801
docs/superpowers/plans/2026-06-04-refine-task.md
Normal file
801
docs/superpowers/plans/2026-06-04-refine-task.md
Normal file
@@ -0,0 +1,801 @@
|
||||
# Refine Task Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Subagents use the `sonnet` model and stage files explicitly by path (never `git add -A`).
|
||||
|
||||
**Goal:** Add a one-click "Refine Task" button to each Idle task card that spawns a headless Claude session which rewrites the task's description and adds subtasks (steps), then updates the task live in the UI.
|
||||
|
||||
**Architecture:** A new headless `RefineRunner` (modeled on `PrimeRunner`) runs `claude -p` read-only in the list's working dir, using the globally-registered `claudedo` MCP. Claude calls `update_task` (existing) and a new `add_subtask` tool. The task stays `Idle`; refine only mutates Title/Description/subtasks. UI shows a busy state via new `RefineStarted`/`RefineFinished` SignalR events; content updates arrive via the existing `TaskUpdated` events.
|
||||
|
||||
**Tech Stack:** .NET 8, ASP.NET Core + SignalR, EF Core (SQLite), Avalonia 12 (CommunityToolkit.Mvvm), ModelContextProtocol server tools, xUnit.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-04-refine-task-design.md`
|
||||
|
||||
**Build/test reminders:** Build individual csproj with `-c Release` (a running Worker locks Debug). `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`, `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`, `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release`. Keep `locales/en.json` and `locales/de.json` keys in parity.
|
||||
|
||||
---
|
||||
|
||||
## File structure
|
||||
|
||||
**Create:**
|
||||
- `src/ClaudeDo.Worker/Refine/RefineRunner.cs` — headless refine run orchestrator
|
||||
- `src/ClaudeDo.Worker/Refine/RefinePrompt.cs` — prompt + CLI args + log path helper
|
||||
- `src/ClaudeDo.Worker/Refine/Interfaces/IRefineRunner.cs` — interface + `RefineRunOutcome`
|
||||
- `src/ClaudeDo.Worker/Refine/Interfaces/IRefineBroadcaster.cs` — `RefineStartedAsync`/`RefineFinishedAsync`
|
||||
|
||||
**Modify:**
|
||||
- `src/ClaudeDo.Data/PromptFiles.cs` — add `Refine` to `PromptKind`, path, default
|
||||
- `src/ClaudeDo.Worker/External/ExternalMcpService.cs` — add `add_subtask` tool
|
||||
- `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs` — implement `RefineStarted`/`RefineFinished` + `IRefineBroadcaster`
|
||||
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — add `RefineTask(string taskId)` method
|
||||
- `src/ClaudeDo.Worker/Program.cs` — register `IRefineRunner`/`IRefineBroadcaster`
|
||||
- `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs` — `RefineTaskAsync` + `RefineStartedEvent`/`RefineFinishedEvent`
|
||||
- `src/ClaudeDo.Ui/Services/WorkerClient.cs` — implement call + subscribe events
|
||||
- `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` — `IsRefining` + `CanRefine`
|
||||
- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` — `RefineTaskCommand` + event wiring
|
||||
- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` — `Icon.Refine` geometry
|
||||
- `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml` — refine button
|
||||
- `locales/en.json`, `locales/de.json` — tooltip key
|
||||
- Test fakes implementing `IWorkerClient` in `tests/ClaudeDo.Ui.Tests` (and any other project that hand-rolls it)
|
||||
|
||||
**Test:**
|
||||
- `tests/ClaudeDo.Worker.Tests/External/AddSubtaskToolTests.cs`
|
||||
- `tests/ClaudeDo.Worker.Tests/Refine/RefinePromptTests.cs`
|
||||
- `tests/ClaudeDo.Worker.Tests/Refine/RefineRunnerTests.cs`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `add_subtask` MCP tool
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/External/ExternalMcpService.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/External/AddSubtaskToolTests.cs`
|
||||
|
||||
The `ExternalMcpService` already injects `IDbContextFactory<ClaudeDoDbContext> _dbFactory`, `TaskRepository _tasks`, and `HubBroadcaster _broadcaster`. Reuse them; new up a `SubtaskRepository` from a fresh context (matching the `SetMyDay`/`GetDailyPrepCandidates` pattern in the same file).
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/ClaudeDo.Worker.Tests/External/AddSubtaskToolTests.cs`. Follow the existing External tool test setup in that test project (look at a sibling test, e.g. an `ExternalMcpService`/`UpdateTask` test, for the in-memory-real-SQLite fixture + broadcaster fake construction; reuse that exact fixture pattern).
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
public class AddSubtaskToolTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AddSubtask_appends_row_with_next_order()
|
||||
{
|
||||
await using var f = new ExternalMcpServiceFixture(); // reuse the project's existing fixture helper
|
||||
var list = await f.SeedListAsync();
|
||||
var task = await f.SeedTaskAsync(list.Id, status: TaskStatus.Idle);
|
||||
|
||||
await f.Service.AddSubtask(task.Id, "First step", orderNum: null, CancellationToken.None);
|
||||
await f.Service.AddSubtask(task.Id, "Second step", orderNum: null, CancellationToken.None);
|
||||
|
||||
await using var ctx = f.CreateContext();
|
||||
var subs = await new SubtaskRepository(ctx).GetByTaskIdAsync(task.Id);
|
||||
Assert.Equal(new[] { "First step", "Second step" }, subs.Select(s => s.Title));
|
||||
Assert.Equal(new[] { 0, 1 }, subs.Select(s => s.OrderNum));
|
||||
Assert.All(subs, s => Assert.False(s.Completed));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddSubtask_refuses_running_task()
|
||||
{
|
||||
await using var f = new ExternalMcpServiceFixture();
|
||||
var list = await f.SeedListAsync();
|
||||
var task = await f.SeedTaskAsync(list.Id, status: TaskStatus.Running);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => f.Service.AddSubtask(task.Id, "x", null, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> If the test project has no reusable `ExternalMcpServiceFixture`, mirror the construction already used by the nearest existing `ExternalMcpService` test (same ctor args, real SQLite via `IDbContextFactory`, a no-op/recording broadcaster). Do not invent a new pattern.
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails** (compile error / method missing)
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter AddSubtaskToolTests`
|
||||
Expected: FAIL — `AddSubtask` not defined.
|
||||
|
||||
- [ ] **Step 3: Implement `add_subtask`**
|
||||
|
||||
Add to `ExternalMcpService` (near `UpdateTask`):
|
||||
|
||||
```csharp
|
||||
[McpServerTool, Description(
|
||||
"Append a subtask (step) to a task. orderNum defaults to the end. " +
|
||||
"Refuses if the task is currently Running. Subtasks are surfaced to the agent at run time and shown in the task's Steps list.")]
|
||||
public async Task<TaskDto> AddSubtask(
|
||||
string taskId,
|
||||
string title,
|
||||
int? orderNum,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
throw new InvalidOperationException("title is required.");
|
||||
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||
var tasks = new TaskRepository(ctx);
|
||||
var subtasks = new SubtaskRepository(ctx);
|
||||
|
||||
var task = await tasks.GetByIdAsync(taskId, cancellationToken)
|
||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||
if (task.Status == TaskStatus.Running)
|
||||
throw new InvalidOperationException("Cannot add a subtask to a running task. Cancel it first.");
|
||||
|
||||
var existing = await subtasks.GetByTaskIdAsync(taskId, cancellationToken);
|
||||
var order = orderNum ?? (existing.Count == 0 ? 0 : existing.Max(s => s.OrderNum) + 1);
|
||||
|
||||
await subtasks.AddAsync(new SubtaskEntity
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
TaskId = taskId,
|
||||
Title = title.Trim(),
|
||||
Completed = false,
|
||||
OrderNum = order,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
}, cancellationToken);
|
||||
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
return ToDto(task);
|
||||
}
|
||||
```
|
||||
|
||||
Add `using ClaudeDo.Data.Repositories;` if not present (it is). `SubtaskEntity` is in `ClaudeDo.Data.Models` (already imported).
|
||||
|
||||
- [ ] **Step 4: Run the test to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter AddSubtaskToolTests`
|
||||
Expected: PASS (2 tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/External/ExternalMcpService.cs tests/ClaudeDo.Worker.Tests/External/AddSubtaskToolTests.cs
|
||||
git commit -m "feat(mcp): add add_subtask tool to claudedo MCP"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Refine prompt (`PromptKind.Refine`)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Data/PromptFiles.cs`
|
||||
|
||||
- [ ] **Step 1: Add the enum value**
|
||||
|
||||
Change the enum line in `PromptFiles.cs`:
|
||||
|
||||
```csharp
|
||||
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild, Refine }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the path mapping**
|
||||
|
||||
In `PathFor`, add before the `_ => throw`:
|
||||
|
||||
```csharp
|
||||
PromptKind.Refine => Path.Combine(Root, "refine.md"),
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the default mapping**
|
||||
|
||||
In `DefaultFor`, add:
|
||||
|
||||
```csharp
|
||||
PromptKind.Refine => RefineDefault,
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add the default prompt constant**
|
||||
|
||||
Add near the other `private const string ...Default` blocks:
|
||||
|
||||
```csharp
|
||||
private const string RefineDefault = """
|
||||
You are refining ONE ClaudeDo task so it is ready to run autonomously later.
|
||||
You are NOT executing the task — only improving its specification.
|
||||
|
||||
The task you are refining:
|
||||
- id: {taskId}
|
||||
- title: {title}
|
||||
- description: {description}
|
||||
- current subtasks (steps):
|
||||
{subtasks}
|
||||
|
||||
What to do:
|
||||
1. If a repository is available, read the relevant code (read-only) to ground your
|
||||
understanding. Do NOT edit, create, or delete any files. Do NOT run commands.
|
||||
2. Rewrite the description so it is clear, specific, and self-contained: what to change,
|
||||
where, and what "done" looks like. Keep scope tight — do not invent adjacent work.
|
||||
3. Call mcp__claudedo__update_task to save the improved title (only if it genuinely
|
||||
helps) and description.
|
||||
4. If the work is clearer as discrete steps, add them as subtasks with
|
||||
mcp__claudedo__add_subtask (one call per step, in order). Only add steps that are
|
||||
not already present in the current subtasks above.
|
||||
|
||||
Use ONLY these tools: mcp__claudedo__get_task, mcp__claudedo__update_task,
|
||||
mcp__claudedo__add_subtask, and read-only Read/Grep/Glob. When you have updated the
|
||||
task, stop.
|
||||
""";
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Build to verify it compiles**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj -c Release`
|
||||
Expected: Build succeeded.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Data/PromptFiles.cs
|
||||
git commit -m "feat(prompts): add Refine prompt kind and default"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: RefineRunner, interfaces, prompt/args helper
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ClaudeDo.Worker/Refine/Interfaces/IRefineRunner.cs`
|
||||
- Create: `src/ClaudeDo.Worker/Refine/Interfaces/IRefineBroadcaster.cs`
|
||||
- Create: `src/ClaudeDo.Worker/Refine/RefinePrompt.cs`
|
||||
- Create: `src/ClaudeDo.Worker/Refine/RefineRunner.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Refine/RefinePromptTests.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Refine/RefineRunnerTests.cs`
|
||||
|
||||
- [ ] **Step 1: Create `IRefineRunner.cs`**
|
||||
|
||||
```csharp
|
||||
namespace ClaudeDo.Worker.Refine;
|
||||
|
||||
public interface IRefineRunner
|
||||
{
|
||||
Task<RefineRunOutcome> RefineAsync(string taskId, CancellationToken ct);
|
||||
}
|
||||
|
||||
public sealed record RefineRunOutcome(bool Success, string Message);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create `IRefineBroadcaster.cs`**
|
||||
|
||||
```csharp
|
||||
namespace ClaudeDo.Worker.Refine;
|
||||
|
||||
public interface IRefineBroadcaster
|
||||
{
|
||||
Task RefineStartedAsync(string taskId);
|
||||
Task RefineFinishedAsync(string taskId, bool success, string? error);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create `RefinePrompt.cs`**
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
|
||||
namespace ClaudeDo.Worker.Refine;
|
||||
|
||||
public static class RefinePrompt
|
||||
{
|
||||
public const string GetTaskTool = "mcp__claudedo__get_task";
|
||||
public const string UpdateTaskTool = "mcp__claudedo__update_task";
|
||||
public const string AddSubtaskTool = "mcp__claudedo__add_subtask";
|
||||
|
||||
public static string LogPath(string taskId) =>
|
||||
System.IO.Path.Combine(Paths.AppDataRoot(), "logs", $"refine-{Short(taskId)}.log");
|
||||
|
||||
// canReadRepo=false drops the read-only filesystem tools (text-only fallback).
|
||||
public static string BuildArgs(int maxTurns, bool canReadRepo)
|
||||
{
|
||||
var tools = canReadRepo
|
||||
? $"{GetTaskTool} {UpdateTaskTool} {AddSubtaskTool} Read Grep Glob"
|
||||
: $"{GetTaskTool} {UpdateTaskTool} {AddSubtaskTool}";
|
||||
return "-p --output-format stream-json --verbose --permission-mode acceptEdits " +
|
||||
$"--max-turns {maxTurns} --allowedTools {tools}";
|
||||
}
|
||||
|
||||
public static string BuildPrompt(TaskEntity task, IEnumerable<SubtaskEntity> subtasks)
|
||||
{
|
||||
var open = subtasks.Where(s => !s.Completed).Select(s => $"- {s.Title}").ToList();
|
||||
var subText = open.Count == 0 ? "(none)" : string.Join("\n", open);
|
||||
return PromptFiles.Render(PromptKind.Refine, new Dictionary<string, string>
|
||||
{
|
||||
["taskId"] = task.Id,
|
||||
["title"] = task.Title,
|
||||
["description"] = string.IsNullOrWhiteSpace(task.Description) ? "(empty)" : task.Description!,
|
||||
["subtasks"] = subText,
|
||||
});
|
||||
}
|
||||
|
||||
private static string Short(string id) => id.Length >= 8 ? id[..8] : id;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Write `RefinePromptTests.cs`**
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Worker.Refine;
|
||||
|
||||
public class RefinePromptTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildArgs_includes_read_tools_when_repo_available()
|
||||
{
|
||||
var args = RefinePrompt.BuildArgs(20, canReadRepo: true);
|
||||
Assert.Contains("--permission-mode acceptEdits", args);
|
||||
Assert.Contains("mcp__claudedo__add_subtask", args);
|
||||
Assert.Contains(" Read Grep Glob", args);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildArgs_drops_read_tools_in_text_only_mode()
|
||||
{
|
||||
var args = RefinePrompt.BuildArgs(20, canReadRepo: false);
|
||||
Assert.DoesNotContain("Glob", args);
|
||||
Assert.Contains("mcp__claudedo__update_task", args);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPrompt_seeds_task_fields_and_open_subtasks()
|
||||
{
|
||||
var task = new TaskEntity { Id = "abc12345", ListId = "l", Title = "T", Description = "D",
|
||||
Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow };
|
||||
var subs = new[]
|
||||
{
|
||||
new SubtaskEntity { Id="1", TaskId="abc12345", Title="open one", Completed=false, OrderNum=0, CreatedAt=DateTime.UtcNow },
|
||||
new SubtaskEntity { Id="2", TaskId="abc12345", Title="done one", Completed=true, OrderNum=1, CreatedAt=DateTime.UtcNow },
|
||||
};
|
||||
var prompt = RefinePrompt.BuildPrompt(task, subs);
|
||||
Assert.Contains("abc12345", prompt);
|
||||
Assert.Contains("open one", prompt);
|
||||
Assert.DoesNotContain("done one", prompt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter RefinePromptTests`
|
||||
Expected: PASS (3 tests).
|
||||
|
||||
- [ ] **Step 5: Create `RefineRunner.cs`**
|
||||
|
||||
`IClaudeProcess.RunAsync(arguments, prompt, workingDirectory, onStdoutLine, ct)` returns a result with `.IsSuccess` and `.ExitCode` (same as used by `PrimeRunner`). Resolve the working dir from the task's list; fall back to a sandbox dir + text-only when missing/invalid. Per-task single-flight via a guarded `HashSet<string>`.
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Runner;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Worker.Refine;
|
||||
|
||||
public sealed class RefineRunner : IRefineRunner
|
||||
{
|
||||
private static readonly TimeSpan RunTimeout = TimeSpan.FromMinutes(5);
|
||||
private const int MaxTurns = 25;
|
||||
|
||||
private readonly IClaudeProcess _claude;
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly ILogger<RefineRunner> _logger;
|
||||
private readonly IRefineBroadcaster _broadcaster;
|
||||
|
||||
private readonly object _lock = new();
|
||||
private readonly HashSet<string> _inFlight = new();
|
||||
|
||||
public RefineRunner(
|
||||
IClaudeProcess claude,
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
ILogger<RefineRunner> logger,
|
||||
IRefineBroadcaster broadcaster)
|
||||
{
|
||||
_claude = claude;
|
||||
_dbFactory = dbFactory;
|
||||
_logger = logger;
|
||||
_broadcaster = broadcaster;
|
||||
}
|
||||
|
||||
public async Task<RefineRunOutcome> RefineAsync(string taskId, CancellationToken ct)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_inFlight.Add(taskId))
|
||||
return new RefineRunOutcome(false, "Already refining this task");
|
||||
}
|
||||
|
||||
var success = false;
|
||||
string? error = null;
|
||||
try
|
||||
{
|
||||
ClaudeDo.Data.Models.TaskEntity task;
|
||||
List<ClaudeDo.Data.Models.SubtaskEntity> subs;
|
||||
string? workingDir;
|
||||
await using (var dbCtx = await _dbFactory.CreateDbContextAsync(ct))
|
||||
{
|
||||
var tasks = new TaskRepository(dbCtx);
|
||||
task = await tasks.GetByIdAsync(taskId, ct)
|
||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||
if (task.Status != TaskStatus.Idle)
|
||||
return new RefineRunOutcome(false, $"Task must be Idle to refine (is {task.Status}).");
|
||||
subs = await new SubtaskRepository(dbCtx).GetByTaskIdAsync(taskId, ct);
|
||||
var list = await new ListRepository(dbCtx).GetByIdAsync(task.ListId, ct);
|
||||
workingDir = list?.WorkingDir;
|
||||
}
|
||||
|
||||
var canReadRepo = !string.IsNullOrWhiteSpace(workingDir) && Directory.Exists(workingDir);
|
||||
var cwd = canReadRepo ? workingDir! : Paths.AppDataRoot();
|
||||
Directory.CreateDirectory(cwd);
|
||||
|
||||
var logPath = RefinePrompt.LogPath(taskId);
|
||||
try { if (File.Exists(logPath)) File.Delete(logPath); } catch { }
|
||||
await using var logWriter = new LogWriter(logPath);
|
||||
|
||||
await _broadcaster.RefineStartedAsync(taskId);
|
||||
|
||||
var prompt = RefinePrompt.BuildPrompt(task, subs);
|
||||
var args = RefinePrompt.BuildArgs(MaxTurns, canReadRepo);
|
||||
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeoutCts.CancelAfter(RunTimeout);
|
||||
|
||||
var result = await _claude.RunAsync(
|
||||
arguments: args,
|
||||
prompt: prompt,
|
||||
workingDirectory: cwd,
|
||||
onStdoutLine: async line => await logWriter.WriteLineAsync(line),
|
||||
ct: timeoutCts.Token);
|
||||
|
||||
success = result.IsSuccess;
|
||||
if (!success) error = $"exit code {result.ExitCode}";
|
||||
return success
|
||||
? new RefineRunOutcome(true, "Refine complete")
|
||||
: new RefineRunOutcome(false, error!);
|
||||
}
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
error = $"timed out after {RunTimeout.TotalMinutes:0} min";
|
||||
return new RefineRunOutcome(false, error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Refine run failed for {TaskId}", taskId);
|
||||
error = ex.Message;
|
||||
return new RefineRunOutcome(false, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _broadcaster.RefineFinishedAsync(taskId, success, error);
|
||||
lock (_lock) { _inFlight.Remove(taskId); }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Write `RefineRunnerTests.cs` (guards, with a fake IClaudeProcess)**
|
||||
|
||||
The test project already has a fake/stub for `IClaudeProcess` used by Prime tests — reuse it (recording invocation + returning a configurable success result). Do NOT spawn the real CLI.
|
||||
|
||||
```csharp
|
||||
public class RefineRunnerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Refuses_when_task_not_idle()
|
||||
{
|
||||
await using var f = new RefineRunnerFixture(); // mirror Prime test fixture wiring
|
||||
var task = await f.SeedTaskAsync(status: TaskStatus.Queued);
|
||||
var outcome = await f.Runner.RefineAsync(task.Id, CancellationToken.None);
|
||||
Assert.False(outcome.Success);
|
||||
Assert.Equal(0, f.Claude.RunCount); // never invoked the CLI
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Idle_task_invokes_claude_once_and_brackets_with_events()
|
||||
{
|
||||
await using var f = new RefineRunnerFixture();
|
||||
var task = await f.SeedTaskAsync(status: TaskStatus.Idle);
|
||||
var outcome = await f.Runner.RefineAsync(task.Id, CancellationToken.None);
|
||||
Assert.True(outcome.Success);
|
||||
Assert.Equal(1, f.Claude.RunCount);
|
||||
Assert.Equal(1, f.Broadcaster.Started);
|
||||
Assert.Equal(1, f.Broadcaster.Finished);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Build the `RefineRunnerFixture`/fakes by copying the Prime test's `IClaudeProcess` stub + real-SQLite `IDbContextFactory` setup and a recording `IRefineBroadcaster`. If a Prime fixture exists, mirror it; otherwise construct inline.
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter RefineRunnerTests`
|
||||
Expected: PASS (2 tests).
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Refine tests/ClaudeDo.Worker.Tests/Refine
|
||||
git commit -m "feat(refine): add RefineRunner, prompt/args helper, and interfaces"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Worker wiring — broadcaster, hub, DI
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`
|
||||
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||
- Modify: `src/ClaudeDo.Worker/Program.cs`
|
||||
|
||||
- [ ] **Step 1: Implement events on `HubBroadcaster`**
|
||||
|
||||
Add `IRefineBroadcaster` to the class's interface list (`public sealed class HubBroadcaster : ..., IRefineBroadcaster`) and add (mirroring the `Prep*` block):
|
||||
|
||||
```csharp
|
||||
public Task RefineStarted(string taskId) => _hub.Clients.All.SendAsync("RefineStarted", taskId);
|
||||
public Task RefineFinished(string taskId, bool success, string? error) =>
|
||||
_hub.Clients.All.SendAsync("RefineFinished", taskId, success, error);
|
||||
|
||||
Task IRefineBroadcaster.RefineStartedAsync(string taskId) => RefineStarted(taskId);
|
||||
Task IRefineBroadcaster.RefineFinishedAsync(string taskId, bool success, string? error) =>
|
||||
RefineFinished(taskId, success, error);
|
||||
```
|
||||
|
||||
Add `using ClaudeDo.Worker.Refine;`.
|
||||
|
||||
- [ ] **Step 2: Add `RefineTask` to `WorkerHub`**
|
||||
|
||||
`WorkerHub` injects services via its constructor. Add a `private readonly IRefineRunner _refineRunner;` field, add the parameter to the constructor and assign it. Add the method (fire-and-forget; the runner brackets with its own events):
|
||||
|
||||
```csharp
|
||||
public Task RefineTask(string taskId)
|
||||
{
|
||||
_ = _refineRunner.RefineAsync(taskId, CancellationToken.None);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
```
|
||||
|
||||
Add `using ClaudeDo.Worker.Refine;`.
|
||||
|
||||
- [ ] **Step 3: Register DI in `Program.cs`**
|
||||
|
||||
Near the Prime registrations:
|
||||
|
||||
```csharp
|
||||
builder.Services.AddSingleton<IRefineRunner, RefineRunner>();
|
||||
builder.Services.AddSingleton<IRefineBroadcaster>(sp => sp.GetRequiredService<HubBroadcaster>());
|
||||
```
|
||||
|
||||
Add `using ClaudeDo.Worker.Refine;` if needed. (`HubBroadcaster` is already registered as a singleton — confirm and reuse that registration; do not double-register it.)
|
||||
|
||||
- [ ] **Step 4: Build the worker**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||
Expected: Build succeeded.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Hub/HubBroadcaster.cs src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Worker/Program.cs
|
||||
git commit -m "feat(refine): wire RefineTask hub method, broadcaster events, and DI"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: UI worker client — call + events + fakes
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||
- Modify: test fakes implementing `IWorkerClient`
|
||||
|
||||
- [ ] **Step 1: Extend the interface**
|
||||
|
||||
In `IWorkerClient.cs` add (near `RunDailyPrepNowAsync` and the `Prep*` events):
|
||||
|
||||
```csharp
|
||||
Task RefineTaskAsync(string taskId);
|
||||
|
||||
event Action<string>? RefineStartedEvent;
|
||||
event Action<string, bool, string?>? RefineFinishedEvent;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Implement in `WorkerClient`**
|
||||
|
||||
Add the method (mirror `RunDailyPrepNowAsync`):
|
||||
|
||||
```csharp
|
||||
public Task RefineTaskAsync(string taskId) => _hub.InvokeAsync("RefineTask", taskId);
|
||||
```
|
||||
|
||||
Declare the events:
|
||||
|
||||
```csharp
|
||||
public event Action<string>? RefineStartedEvent;
|
||||
public event Action<string, bool, string?>? RefineFinishedEvent;
|
||||
```
|
||||
|
||||
Subscribe in the constructor (mirror the `Prep*` subscriptions block):
|
||||
|
||||
```csharp
|
||||
_hub.On<string>("RefineStarted", id =>
|
||||
Dispatcher.UIThread.Post(() => RefineStartedEvent?.Invoke(id)));
|
||||
_hub.On<string, bool, string?>("RefineFinished", (id, ok, err) =>
|
||||
Dispatcher.UIThread.Post(() => RefineFinishedEvent?.Invoke(id, ok, err)));
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update test fakes**
|
||||
|
||||
Find every hand-rolled `IWorkerClient` implementation (search the test projects) and add `RefineTaskAsync` (return `Task.CompletedTask`) plus the two events (`= delegate {}` or `add{}remove{}` no-ops as the fake convention dictates). Build each affected test project.
|
||||
|
||||
- [ ] **Step 4: Build UI + test projects**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Then build the UI test project(s). Expected: Build succeeded.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs src/ClaudeDo.Ui/Services/WorkerClient.cs <fake files>
|
||||
git commit -m "feat(ui): add RefineTask client call and refine events"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: UI — icon, button, view model, command
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Design/IslandStyles.axaml`
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
|
||||
- Modify: `locales/en.json`, `locales/de.json`
|
||||
|
||||
- [ ] **Step 1: Add the `Icon.Refine` geometry**
|
||||
|
||||
In `IslandStyles.axaml`, near the other `Icon.*` `StreamGeometry` resources, add the supplied SVG converted to path data (line-art, rendered stroked via `plan-icon`):
|
||||
|
||||
```xml
|
||||
<StreamGeometry x:Key="Icon.Refine">M3,5 L11,5 M3,9 L9,9 M3,13 L7,13 M19,1.8 L19.7,3.9 L21.7,4.6 L19.7,5.3 L19,7.4 L18.3,5.3 L16.3,4.6 L18.3,3.9 Z M18,10.5 L12.2,16.3 M16.6,9.1 L19.4,11.9 M12.2,16.3 L11,18.5 L13.2,17.5 Z</StreamGeometry>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `IsRefining`/`CanRefine` to `TaskRowViewModel`**
|
||||
|
||||
Add the observable property (with the other `[ObservableProperty]` fields):
|
||||
|
||||
```csharp
|
||||
[ObservableProperty] private bool _isRefining;
|
||||
```
|
||||
|
||||
Add a computed gate (refine is only offered for Idle, non-parent tasks). Place near other `Can*` getters:
|
||||
|
||||
```csharp
|
||||
public bool CanRefine => Status == TaskStatus.Idle && PlanningPhase == PlanningPhase.None && !IsRefining;
|
||||
```
|
||||
|
||||
If `Status`/`PlanningPhase`/`IsRefining` are `[ObservableProperty]`, raise `CanRefine` change notifications via partial `On<Prop>Changed` hooks:
|
||||
|
||||
```csharp
|
||||
partial void OnStatusChanged(TaskStatus value) => OnPropertyChanged(nameof(CanRefine));
|
||||
partial void OnPlanningPhaseChanged(PlanningPhase value) => OnPropertyChanged(nameof(CanRefine));
|
||||
partial void OnIsRefiningChanged(bool value) => OnPropertyChanged(nameof(CanRefine));
|
||||
```
|
||||
|
||||
> If `On...Changed` partials already exist for `Status`/`PlanningPhase`, add the `OnPropertyChanged(nameof(CanRefine))` line inside them instead of redeclaring.
|
||||
|
||||
- [ ] **Step 3: Add `RefineTaskCommand` + event wiring to `TasksIslandViewModel`**
|
||||
|
||||
Add the command (mirror an existing per-row command like `ToggleStarCommand`, which takes a `TaskRowViewModel`):
|
||||
|
||||
```csharp
|
||||
[RelayCommand]
|
||||
private async Task RefineTask(TaskRowViewModel row)
|
||||
{
|
||||
if (row is null || !row.CanRefine) return;
|
||||
row.IsRefining = true;
|
||||
try { await _worker.RefineTaskAsync(row.Id); }
|
||||
catch { row.IsRefining = false; }
|
||||
}
|
||||
```
|
||||
|
||||
> Use the same injected worker-client field name this VM already uses (e.g. `_worker`/`_client`). Match it.
|
||||
|
||||
Subscribe to the refine events where the VM wires other worker events (where `OnWorkerTaskUpdated` is subscribed). Add handlers that flip the row flag:
|
||||
|
||||
```csharp
|
||||
private void OnRefineStarted(string taskId)
|
||||
{
|
||||
var row = Items.FirstOrDefault(r => r.Id == taskId);
|
||||
if (row is not null) row.IsRefining = true;
|
||||
}
|
||||
|
||||
private void OnRefineFinished(string taskId, bool ok, string? error)
|
||||
{
|
||||
var row = Items.FirstOrDefault(r => r.Id == taskId);
|
||||
if (row is not null) row.IsRefining = false;
|
||||
}
|
||||
```
|
||||
|
||||
Wire them next to the existing subscriptions (and unsubscribe in the same place the VM unsubscribes others, if it does):
|
||||
|
||||
```csharp
|
||||
_worker.RefineStartedEvent += OnRefineStarted;
|
||||
_worker.RefineFinishedEvent += OnRefineFinished;
|
||||
```
|
||||
|
||||
(Content changes—new description/subtasks—arrive through the existing `TaskUpdated` → `OnWorkerTaskUpdated` path; no extra work needed.)
|
||||
|
||||
- [ ] **Step 4: Add the button to `TaskRowView.axaml`**
|
||||
|
||||
Mirror the star button (`Grid.Column="5"` area). Add a refine `icon-btn` (e.g. as a new column or beside the star) bound to the parent ItemsControl's command, passing the row as parameter. Use the `plan-icon` stroked `Path` inside a `Viewbox` (matching the Plan-day button), gate visibility on `CanRefine`, and disable/spin on `IsRefining`:
|
||||
|
||||
```xml
|
||||
<Button Classes="icon-btn refine-btn"
|
||||
IsVisible="{Binding CanRefine}"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).RefineTaskCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
ToolTip.Tip="{loc:Tr tasks.refineTip}">
|
||||
<Viewbox Width="16" Height="16">
|
||||
<Path Classes="plan-icon" Data="{StaticResource Icon.Refine}"/>
|
||||
</Viewbox>
|
||||
</Button>
|
||||
```
|
||||
|
||||
> Match the column layout already in `TaskRowView.axaml`. If a new grid column is needed, widen `ColumnDefinitions` accordingly and place the refine button left of the star (`Grid.Column`). Keep the existing `vm:` / `loc:` xmlns aliases the file already declares.
|
||||
|
||||
Optionally show a spinning/dimmed state while `IsRefining` (e.g. a style `Selector="Button.refine-btn:disabled"` or bind opacity to `IsRefining`). Keep it simple; a disabled look is enough.
|
||||
|
||||
- [ ] **Step 5: Add localization keys**
|
||||
|
||||
Add to both `locales/en.json` and `locales/de.json` under the `tasks` group (keys must stay in parity):
|
||||
|
||||
- en: `"tasks.refineTip": "Refine this task with Claude"`
|
||||
- de: `"tasks.refineTip": "Aufgabe mit Claude verfeinern"`
|
||||
|
||||
> Match the file's actual key structure (flat `"tasks.x"` vs nested `tasks: { x }`)—look at an existing `tasks.*` tooltip key (e.g. the plan-day tip) and follow it exactly.
|
||||
|
||||
- [ ] **Step 6: Build UI**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Then run the Localization parity tests: `dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release`
|
||||
Expected: Build succeeded; locale parity passes.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Design/IslandStyles.axaml src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs locales/en.json locales/de.json
|
||||
git commit -m "feat(ui): add Refine button, icon, and command to task card"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Full build + test sweep, manual smoke
|
||||
|
||||
- [ ] **Step 1: Build all main projects**
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
```
|
||||
Expected: Build succeeded for both.
|
||||
|
||||
- [ ] **Step 2: Run the worker + UI test suites**
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||
```
|
||||
Expected: all green.
|
||||
|
||||
- [ ] **Step 3: Manual smoke (visual + real CLI — flag to user)**
|
||||
|
||||
Cannot be automated (no real-Claude in tests). Verify by hand: start Worker + UI, on an Idle task click the refine icon → button shows busy → after the run the description improves and steps appear in the Steps card → task stays Idle. Confirm the refine icon is hidden for Queued/Running/Done tasks and for planning parents. **Report this as a visual-verification gap for the user to confirm.**
|
||||
|
||||
---
|
||||
|
||||
## Notes on parallelism / execution
|
||||
|
||||
- Tasks 1–4 are backend and largely sequential (4 depends on 3). Tasks 1 and 2 are independent and could be done first in either order.
|
||||
- Tasks 5–6 (UI) depend on Task 4's hub/event contract.
|
||||
- Per project convention: subagents use `sonnet`, stage files by explicit path, and do NOT run git/build inside parallel agents — the orchestrator builds, tests, and commits after each task.
|
||||
74
docs/superpowers/plans/2026-06-04-review-and-roadblock-ux.md
Normal file
74
docs/superpowers/plans/2026-06-04-review-and-roadblock-ux.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Review & Roadblock UX Implementation Plan
|
||||
|
||||
> **For agentic workers:** execute task-by-task (subagent-driven-development). Steps use `- [ ]`.
|
||||
|
||||
**Goal:** Move the task-row review actions into the Details panel, give the Details panel a real `WaitingForReview` state + a populated diff meter, and add a glanceable yellow roadblock indicator on the task card.
|
||||
|
||||
**Architecture:** Persist a `RoadblockCount` on `TaskEntity` (set by the runner when it folds in `CLAUDEDO_BLOCKED` markers). The row shows a warning badge when count > 0; review controls relocate to `DetailsIslandView`.
|
||||
|
||||
**Tech Stack:** .NET 8, Avalonia, EF Core (one migration), xUnit.
|
||||
|
||||
**Coordination:** A second session (`claudedo-childloop`) is building the child-tasks/improvement-loop in a worktree and will rebase onto main *after* these commits. It also touches `DetailsIslandViewModel`, `TaskRowView.axaml`, `TaskStateService`, `TaskStatus`. This plan deliberately stays OUT of `TaskStateService` and the `TaskStatus` enum (persisting `RoadblockCount` from the runner via the repository instead).
|
||||
|
||||
Build/test (per-project, .NET 8):
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task A — Persist RoadblockCount (Data + Worker, no UI)
|
||||
|
||||
**Files:** `TaskEntity.cs`, `TaskEntityConfiguration.cs`, new migration, `TaskRepository.cs`, `TaskRunner.cs`; test in `tests/ClaudeDo.Data.Tests`.
|
||||
|
||||
- Add `public int RoadblockCount { get; set; }` to `TaskEntity` (default 0).
|
||||
- Map it in `TaskEntityConfiguration` to column `roadblock_count` (default 0). Mirror the pattern used by an existing scalar column (e.g. how `DailyPrepMaxTasks`/other ints are configured).
|
||||
- Create EF migration `AddRoadblockCount` (run `dotnet ef migrations add AddRoadblockCount` against `src/ClaudeDo.Data`; if EF tooling is unavailable, hand-author the migration + Designer + snapshot edit mirroring the most recent migration). One column, default 0, no backfill needed.
|
||||
- Add `TaskRepository.SetRoadblockCountAsync(string taskId, int count, CancellationToken ct)` using `ExecuteUpdateAsync` on `RoadblockCount`.
|
||||
- In `TaskRunner.HandleSuccess`, BEFORE the terminal state write (`SubmitForReviewAsync`/`CompleteAsync`), call `SetRoadblockCountAsync(task.Id, result.Blocks.Count, CancellationToken.None)` so the `TaskUpdated` broadcast reflects it. (Do NOT route this through `TaskStateService`.)
|
||||
- Test: a `TaskRepository` test that sets a count and reads it back.
|
||||
- Commit: `feat(roadblock): persist roadblock count on the task`.
|
||||
|
||||
**Acceptance:** a finished run with N roadblocks leaves `tasks.roadblock_count = N`; a clean run leaves 0.
|
||||
|
||||
---
|
||||
|
||||
## Task B — Detail panel: host review actions + real WaitingForReview state + diff meter
|
||||
|
||||
**Files:** `DetailsIslandViewModel.cs`, `DetailsIslandView.axaml` (+ `.axaml.cs` if needed), locales if new keys; reuse `IWorkerClient.ApproveReview/RejectReviewToQueue/RejectReviewToIdle/CancelReview` (already exist).
|
||||
|
||||
1. **WaitingForReview state:**
|
||||
- In `StatusToStateKey` map `WaitingForReview => "review"` (was `"running"`); in `FinishedStatusToStateKey` map `"waiting_for_review" => "review"`.
|
||||
- Add `public bool IsWaitingForReview => AgentState == "review";` and raise it in `OnAgentStateChanged`.
|
||||
- Add a `vm.agentStatus.review` locale key (en + de, parity) for the status label.
|
||||
- Confirm `IsAgentSectionEnabled => !IsRunning` still holds (review is no longer "running", so the agent settings section re-enables in review — correct).
|
||||
2. **Review actions (moved from the row):** add commands to `DetailsIslandViewModel` that call the worker for the selected task: `ApproveReviewCommand`, `RejectReviewCommand` (takes feedback text → `RejectReviewToQueueAsync`), `ParkReviewCommand` (`RejectReviewToIdleAsync`), `CancelReviewCommand` (`CancelReviewAsync`). Add a `ReviewFeedback` string property for the rejection comment. Mirror how the row's code-behind currently invokes these (see `TaskRowView.axaml.cs`).
|
||||
- In `DetailsIslandView.axaml`, add a review section (visible when `IsWaitingForReview` and `IsTaskDetailVisible`) with Approve / Reject(+feedback box) / Park / Cancel, reusing the existing `tasks.approve/reject/park/cancel` + `tasks.feedback*` locale keys.
|
||||
3. **Diff meter:** in `RefreshWorktreeAsync`, after setting `row.DiffStat`, parse the `--stat` summary into additions/deletions and assign `DiffAdditions`/`DiffDeletions` (drives `DiffMeterRatio`). Add a small static parser `ParseDiffStat(string?) -> (int add, int del)` reading the "N insertions(+), M deletions(-)" tail; unit-test it.
|
||||
- Commit: `feat(ui): host review actions in the details panel; show review state and diff meter`.
|
||||
|
||||
**Acceptance:** selecting a `WaitingForReview` task shows a "review" status (not "running"), the four review actions work from the detail panel, and the diff meter reflects real additions/deletions.
|
||||
|
||||
---
|
||||
|
||||
## Task C — Task row: remove review buttons, add roadblock badge
|
||||
|
||||
**Files:** `TaskRowView.axaml`, `TaskRowView.axaml.cs`, `TaskRowViewModel.cs`; warning icon resource if missing.
|
||||
|
||||
- Remove the review-actions `StackPanel` (lines ~142–157) and the now-unused `RejectAnchor` flyout (~250–279) from `TaskRowView.axaml`, and the corresponding click handlers (`OnApproveReviewClick`, `OnRejectReviewClick`, `OnParkReviewClick`, `OnCancelReviewClick`, reject-flyout handlers) from the code-behind. (Review now lives in the detail panel — Task B.)
|
||||
- `TaskRowViewModel`: add `int RoadblockCount` + `bool HasRoadblock => RoadblockCount > 0` + `string RoadblockTooltip` (e.g. `"{n} roadblock(s) reported — see details"`); map `RoadblockCount` in `FromEntity`.
|
||||
- `TaskRowView.axaml`: add a yellow warning `PathIcon` immediately left of the action area (in the chip row, before the status chip or before the star — pick the spot that reads as "left of the Done/action button"), `IsVisible="{Binding HasRoadblock}"`, `ToolTip.Tip="{Binding RoadblockTooltip}"`. Use a filled-geometry warning icon (PathIcon fills geometry — a stroke path renders invisible); if no `Icon.Warning` resource exists, add one (filled triangle + exclamation) to the icon resources, colored with a yellow/amber brush.
|
||||
- Commit: `feat(ui): roadblock badge on the task card; relocate review actions`.
|
||||
|
||||
**Acceptance:** rows no longer show the four review buttons; a task with `RoadblockCount > 0` shows a yellow ⚠ left of the action button with a tooltip; review still fully works via the detail panel.
|
||||
|
||||
---
|
||||
|
||||
## Task D — Build + visual-check
|
||||
|
||||
- Full build (`App` + `Worker`) and run Data + Worker test suites; all green.
|
||||
- **Manual (flag for user):** start the app, take a `WaitingForReview` task (the deploy roadblock task qualifies), confirm: row shows the ⚠ badge + no row review buttons; detail panel shows "review" state, working review actions, and a non-zero diff meter for the farewell/README tasks. The agent cannot verify GUI — ask the user.
|
||||
- Then ping `claudedo-childloop` via mailbox with the exact shared-file diffs so it can rebase.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Task Detail Redesign — Component Build Prompts
|
||||
|
||||
Three isolated build tasks (one per component). Each runs in its own worktree off
|
||||
`main`, with the project CLAUDE.md auto-loaded. Full design context lives in
|
||||
`docs/superpowers/specs/2026-06-04-task-detail-redesign-design.md` — every task
|
||||
must read it first.
|
||||
|
||||
Shared rules (all three):
|
||||
- Build a **standalone** `UserControl` + dedicated `ViewModel` that renders fully
|
||||
in the Avalonia previewer via **design-time sample data** (parameterless ctor
|
||||
populating realistic values). Do **not** bind to `DetailsIslandViewModel`.
|
||||
- New files under `src/ClaudeDo.Ui/Views/Islands/Detail/` and
|
||||
`src/ClaudeDo.Ui/ViewModels/Islands/Detail/`.
|
||||
- Use **only** tokens from `Design/Tokens.axaml` and classes from
|
||||
`Design/IslandStyles.axaml`. No inline hex, no magic numbers where a token
|
||||
exists. `PathIcon` fills geometry — stroke-only art is invisible.
|
||||
- Compiled bindings (`x:DataType`). MVVM via CommunityToolkit
|
||||
(`[ObservableProperty]`, `[RelayCommand]`); VM inherits `ViewModelBase`.
|
||||
- **Do NOT modify** `DetailsIslandView.axaml`, `DetailsIslandViewModel.cs`,
|
||||
`AgentStripView`, `SessionTerminalView`, or `TaskRunner.cs`.
|
||||
- Verify: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj -c Release` is green.
|
||||
Stage files explicitly by path (never `git add -A`). Commit with a conventional
|
||||
message.
|
||||
|
||||
---
|
||||
|
||||
## TASK 1 — TaskHeaderBar
|
||||
|
||||
(prompt text = task description; see below)
|
||||
|
||||
## TASK 2 — DescriptionStepsCard
|
||||
|
||||
## TASK 3 — WorkConsole
|
||||
@@ -0,0 +1,432 @@
|
||||
# Git Merge/Review — Shared Foundation + Layer A Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build the shared worker conflict contract (so parallel Layer B/C sessions branch from frozen interfaces) and rework the Git tab into a single Approve+merge cockpit.
|
||||
|
||||
**Architecture:** Phase 0 adds the conflict-resolution contract to `IWorkerClient`/`WorkerClient` (real `_hub.InvokeAsync` bodies — the worker hub methods are implemented later by Layer C; calls simply fail at runtime until then) plus client-side DTOs and test-fake updates, then commits + pushes so B and C branch from it. Phase A reworks `WorkConsole.axaml`'s Git tab and routes single-task merge/approve conflicts into a `RequestConflictResolution` seam (wired to Layer C's resolver by the integrator at merge time).
|
||||
|
||||
**Tech Stack:** .NET 8, Avalonia 12 (Fluent), CommunityToolkit.Mvvm, SignalR, xUnit. Build individual csproj with `-c Release` (`.slnx` needs .NET 9; a running Worker locks `Debug`).
|
||||
|
||||
**Reference spec:** `docs/superpowers/specs/2026-06-05-git-merge-review-rework-design.md`
|
||||
|
||||
**Note on the canonical diff renderer:** the unified diff model/control already exists — `DiffFileViewModel`/`DiffLineViewModel`/`UnifiedDiffParser` (in `src/ClaudeDo.Ui/ViewModels/Modals/`) rendered by `DiffLinesView` (`src/ClaudeDo.Ui/Views/Controls/DiffLinesView.axaml`). `DiffModalView` and `PlanningDiffView` already use it. So "consolidate diff renderers" for this scope is just verifying that (Task A.3); migrating `WorktreeModalView`'s bespoke diff onto `DiffLinesView` is Layer B's job.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
**Phase 0 (foundation — pushed before B/C branch):**
|
||||
- Modify `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs` — 5 new method signatures.
|
||||
- Modify `src/ClaudeDo.Ui/Services/WorkerClient.cs` — 5 `InvokeAsync` bodies + 3 new DTO records.
|
||||
- Modify `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs` — 5 new `virtual` no-op methods.
|
||||
- Modify `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` — 5 new methods on `FakeWorkerClient`.
|
||||
|
||||
**Phase A (Layer A — this session, after foundation commit):**
|
||||
- Modify `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` — `RequestConflictResolution` seam; route Approve/Merge conflicts into it.
|
||||
- Modify `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml` — fuse REVIEW + MERGE sections into one cockpit block.
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs` (or a sibling test file in the same folder).
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Shared Foundation
|
||||
|
||||
### Task 0.1: Add the conflict contract (interface + client + DTOs)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||
|
||||
- [ ] **Step 1: Add the 5 method signatures to `IWorkerClient`**
|
||||
|
||||
In `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`, after the existing
|
||||
`Task CancelReviewAsync(string taskId);` line (line 45), add:
|
||||
|
||||
```csharp
|
||||
// ── Conflict resolution (worker hub side implemented by Layer C) ──
|
||||
Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch);
|
||||
Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId);
|
||||
Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent);
|
||||
Task<MergeResultDto> ContinueMergeAsync(string taskId);
|
||||
Task AbortMergeAsync(string taskId);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the 3 DTO records to `WorkerClient.cs`**
|
||||
|
||||
In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, immediately after line 534
|
||||
(`public record MergeTargetsDto(...)`), add:
|
||||
|
||||
```csharp
|
||||
public record MergeConflictsDto(string TaskId, IReadOnlyList<ConflictFileDto> Files);
|
||||
public record ConflictFileDto(string Path, IReadOnlyList<ConflictHunkDto> Hunks);
|
||||
public record ConflictHunkDto(string Ours, string Theirs, string? Base);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the 5 client method bodies to `WorkerClient.cs`**
|
||||
|
||||
In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, right after the `MergeTaskAsync`
|
||||
method (ends at line 270), add:
|
||||
|
||||
```csharp
|
||||
public Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch)
|
||||
=> _hub.InvokeAsync<MergeResultDto>("StartConflictMerge", taskId, targetBranch);
|
||||
|
||||
public Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId)
|
||||
=> _hub.InvokeAsync<MergeConflictsDto>("GetMergeConflicts", taskId);
|
||||
|
||||
public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent)
|
||||
=> _hub.InvokeAsync("WriteConflictResolution", taskId, path, resolvedContent);
|
||||
|
||||
public Task<MergeResultDto> ContinueMergeAsync(string taskId)
|
||||
=> _hub.InvokeAsync<MergeResultDto>("ContinueMerge", taskId);
|
||||
|
||||
public Task AbortMergeAsync(string taskId)
|
||||
=> _hub.InvokeAsync("AbortMerge", taskId);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build the UI project**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj -c Release`
|
||||
Expected: build FAILS — the two test projects won't compile yet, but the UI project
|
||||
itself should succeed. If the UI project reports "does not implement interface member"
|
||||
it means a body is missing; fix before continuing. (Test projects are fixed in 0.2.)
|
||||
|
||||
### Task 0.2: Update the hand-rolled test fakes
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`
|
||||
- Modify: `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs`
|
||||
|
||||
- [ ] **Step 1: Add 5 virtual no-ops to `StubWorkerClient`**
|
||||
|
||||
In `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`, after the `MergeTaskAsync` override
|
||||
(line 57), add:
|
||||
|
||||
```csharp
|
||||
public virtual Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty<string>(), null));
|
||||
public virtual Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId) => Task.FromResult(new MergeConflictsDto(taskId, System.Array.Empty<ConflictFileDto>()));
|
||||
public virtual Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
|
||||
public virtual Task<MergeResultDto> ContinueMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
||||
public virtual Task AbortMergeAsync(string taskId) => Task.CompletedTask;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add 5 methods to `FakeWorkerClient`**
|
||||
|
||||
In `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs`, after the
|
||||
`MergeTaskAsync` method (line 47), add:
|
||||
|
||||
```csharp
|
||||
public Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty<string>(), null));
|
||||
public Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId) => Task.FromResult(new MergeConflictsDto(taskId, System.Array.Empty<ConflictFileDto>()));
|
||||
public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
|
||||
public Task<MergeResultDto> ContinueMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
||||
public Task AbortMergeAsync(string taskId) => Task.CompletedTask;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build both test projects**
|
||||
|
||||
Run: `dotnet build tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release && dotnet build tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release`
|
||||
Expected: both BUILD succeed.
|
||||
|
||||
- [ ] **Step 4: Run the UI test suite to confirm green baseline**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||
Expected: PASS (no behavior changed yet).
|
||||
|
||||
### Task 0.3: Commit and push the foundation
|
||||
|
||||
- [ ] **Step 1: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs src/ClaudeDo.Ui/Services/WorkerClient.cs tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
|
||||
git commit -m "feat(ui): add conflict-resolution worker contract (foundation for merge rework)"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Push so Layer B/C can branch from this commit**
|
||||
|
||||
Run: `git push`
|
||||
Expected: pushed to `main`. (First push to git.kuns.dev may fail auth — retry once.)
|
||||
**This commit is the branch point for the Layer B and Layer C kickoff prompts.**
|
||||
|
||||
---
|
||||
|
||||
## Phase A — Layer A Review/Merge Cockpit
|
||||
|
||||
### Task A.1: Conflict-resolution seam + route Approve/Merge conflicts into it (TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandConflictSeamTests.cs` (new)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandConflictSeamTests.cs`. Mirror
|
||||
the VM-construction harness used in
|
||||
`tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs` (same folder) —
|
||||
construct `DetailsIslandViewModel` exactly as that file does, including its
|
||||
`StubWorkerClient` subclass pattern. The test:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task ApproveReview_OnConflict_InvokesConflictResolutionSeam()
|
||||
{
|
||||
string? resolvedTaskId = null;
|
||||
string? resolvedTarget = null;
|
||||
|
||||
// Construct the VM as in DetailsIslandPlanningTests, with a worker stub whose
|
||||
// ApproveReviewAsync returns a conflict result:
|
||||
// public override Task<MergeResultDto?> ApproveReviewAsync(string id, string target)
|
||||
// => Task.FromResult<MergeResultDto?>(new MergeResultDto("conflict", new[]{"a.cs"}, null));
|
||||
var vm = CreateVm(/* worker stub above */);
|
||||
vm.RequestConflictResolution = (taskId, target) =>
|
||||
{
|
||||
resolvedTaskId = taskId; resolvedTarget = target;
|
||||
return System.Threading.Tasks.Task.CompletedTask;
|
||||
};
|
||||
// assign a task in WaitingForReview + a SelectedMergeTarget = "main" via the same
|
||||
// helpers DetailsIslandPlanningTests uses.
|
||||
|
||||
await vm.ApproveReviewCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Equal(/* the seeded task id */, resolvedTaskId);
|
||||
Assert.Equal("main", resolvedTarget);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter ApproveReview_OnConflict_InvokesConflictResolutionSeam`
|
||||
Expected: FAIL — `RequestConflictResolution` property does not exist (compile error).
|
||||
|
||||
- [ ] **Step 3: Add the seam property**
|
||||
|
||||
In `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`, beside the other
|
||||
view-wired delegates (`ShowDiffModal`, `ShowMergeModal` around line 387–390), add:
|
||||
|
||||
```csharp
|
||||
// Invoked when a single-task merge/approve hits a conflict. Wired by the
|
||||
// integrator to Layer C's conflict resolver. Args: (taskId, targetBranch).
|
||||
public Func<string, string, System.Threading.Tasks.Task>? RequestConflictResolution { get; set; }
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Route the Approve conflict branch into the seam**
|
||||
|
||||
In `ApproveReviewAsync` (around line 1453), replace the conflict branch body so it
|
||||
prefers the seam, falling back to the current preview-text behavior:
|
||||
|
||||
```csharp
|
||||
var result = await _worker.ApproveReviewAsync(Task.Id, SelectedMergeTarget ?? "");
|
||||
if (result?.Status == "conflict")
|
||||
{
|
||||
if (RequestConflictResolution is not null)
|
||||
{
|
||||
await RequestConflictResolution(Task.Id, SelectedMergeTarget ?? "");
|
||||
}
|
||||
else
|
||||
{
|
||||
var (text, _, _) = MergePreviewPresenter.Describe(
|
||||
new MergePreviewDto("conflict", result.ConflictFiles, 0));
|
||||
MergePreviewText = text; MergeIsClean = false; MergeIsConflict = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Route the manual Merge conflict branch into the seam**
|
||||
|
||||
In `MergeAsync` (around line 1170), apply the same pattern to its conflict branch:
|
||||
|
||||
```csharp
|
||||
var result = await _worker.MergeTaskAsync(Task.Id, SelectedMergeTarget ?? "", false, "Merge task");
|
||||
if (result.Status == "conflict")
|
||||
{
|
||||
if (RequestConflictResolution is not null)
|
||||
{
|
||||
await RequestConflictResolution(Task.Id, SelectedMergeTarget ?? "");
|
||||
}
|
||||
else
|
||||
{
|
||||
var (text, _, _) = MergePreviewPresenter.Describe(
|
||||
new MergePreviewDto("conflict", result.ConflictFiles, 0));
|
||||
MergePreviewText = text; MergeIsClean = false; MergeIsConflict = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await RefreshMergePreviewAsync();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run the test to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter ApproveReview_OnConflict_InvokesConflictResolutionSeam`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Run the full UI suite (no regressions)**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandConflictSeamTests.cs
|
||||
git commit -m "feat(ui): route single-task merge conflicts into a resolution seam"
|
||||
```
|
||||
|
||||
### Task A.2: Fuse the Git tab into one Approve+merge cockpit
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml`
|
||||
|
||||
- [ ] **Step 1: Replace the two Git-tab sections with one cockpit block**
|
||||
|
||||
In `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml`, replace the entire Git
|
||||
`ScrollViewer` body (lines 255–313 — the `<!-- Git: ... -->` block containing the
|
||||
separate `REVIEW` `StackPanel` and the `MERGE & WORKTREE` `StackPanel`) with a single
|
||||
cockpit where Approve sits with the merge target/preview/actions. Keep the existing
|
||||
control class names (`section-label`, `field-label`, `btn`, `btn accent`, `meta`) and
|
||||
the existing bindings (`SelectedMergeTarget`, `MergeTargetBranches`, `MergePreviewText`,
|
||||
`MergeIsClean`, `MergeIsConflict`, `ShowMergePreviewMuted`, `OpenDiffCommand`,
|
||||
`ApproveReviewCommand`, `MergeCommand`, `ShowSingleMerge`, `OpenWorktreeCommand`,
|
||||
`ReviewCombinedDiffCommand`, `MergeAllCommand`, `CanMergeAll`, `MergeAllDisabledReason`,
|
||||
`MergeAllError`):
|
||||
|
||||
```xml
|
||||
<!-- Git: one Approve + merge cockpit -->
|
||||
<ScrollViewer IsVisible="{Binding IsGitTab}" Padding="14,10">
|
||||
<StackPanel Spacing="12" IsVisible="{Binding ShowMergeSection}">
|
||||
<TextBlock Classes="section-label" Text="MERGE" />
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="Target branch" />
|
||||
<ComboBox ItemsSource="{Binding MergeTargetBranches}"
|
||||
SelectedItem="{Binding SelectedMergeTarget, Mode=TwoWay}"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="0">
|
||||
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource MossBrush}"
|
||||
IsVisible="{Binding MergeIsClean}" />
|
||||
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource BloodBrush}"
|
||||
IsVisible="{Binding MergeIsConflict}" />
|
||||
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
IsVisible="{Binding ShowMergePreviewMuted}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Primary action: Approve flows straight into the merge.
|
||||
Approve is the review-gated path; the plain Merge button covers
|
||||
already-reviewed / kept worktrees. -->
|
||||
<WrapPanel Orientation="Horizontal">
|
||||
<Button Classes="btn accent" Content="Approve & Merge" Margin="0,0,8,8"
|
||||
Command="{Binding ApproveReviewCommand}"
|
||||
IsVisible="{Binding IsWaitingForReview}" />
|
||||
<Button Classes="btn accent" Content="Merge" Margin="0,0,8,8"
|
||||
Command="{Binding MergeCommand}"
|
||||
IsVisible="{Binding ShowSingleMerge}" />
|
||||
<Button Classes="btn" Content="Open Diff" Margin="0,0,8,8"
|
||||
Command="{Binding OpenDiffCommand}" />
|
||||
<Button Classes="btn" Margin="0,0,8,8"
|
||||
Command="{Binding OpenWorktreeCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<TextBlock Text="Worktree" />
|
||||
<PathIcon Data="{StaticResource Icon.ArrowOut}" Width="11" Height="11" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Classes="btn" Content="Review Combined Diff" Margin="0,0,8,8"
|
||||
Command="{Binding ReviewCombinedDiffCommand}" />
|
||||
<Button Classes="btn accent" Content="Merge All Subtasks" Margin="0,0,0,8"
|
||||
Command="{Binding MergeAllCommand}"
|
||||
IsEnabled="{Binding CanMergeAll}"
|
||||
ToolTip.Tip="{Binding MergeAllDisabledReason}" />
|
||||
</WrapPanel>
|
||||
|
||||
<TextBlock Text="{Binding MergeAllError}"
|
||||
Foreground="{DynamicResource BloodBrush}"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{Binding MergeAllError,
|
||||
Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
```
|
||||
|
||||
Note: the cockpit now shows whenever `ShowMergeSection` is true. `ShowMergeSection`
|
||||
(DetailsIslandViewModel line 161) must be true while `IsWaitingForReview` so the
|
||||
Approve button appears. Check its expression in Step 2.
|
||||
|
||||
- [ ] **Step 2: Verify `ShowMergeSection` covers the review state**
|
||||
|
||||
Read `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` line 161. If
|
||||
`ShowMergeSection` is false while `IsWaitingForReview` (e.g. it requires a non-review
|
||||
state), widen it to also be true when `IsWaitingForReview && WorktreePath != null`, and
|
||||
ensure `OnPropertyChanged(nameof(ShowMergeSection))` already fires on the relevant state
|
||||
transitions (it is notified via `NotifySessionSections`). Make the minimal change needed
|
||||
so the Approve button is visible in review state. If it already covers review, change
|
||||
nothing.
|
||||
|
||||
- [ ] **Step 3: Build the app project**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: BUILD succeeds (pulls in Ui + Data).
|
||||
|
||||
- [ ] **Step 4: Visual verification (manual — flag for the user)**
|
||||
|
||||
This is an AXAML layout change with no automated coverage. Launch the app, open a task
|
||||
in `WaitingForReview`, open the Git tab, and confirm: the single MERGE block shows the
|
||||
target combo, the colored preview line, an "Approve & Merge" button (review state), and
|
||||
the diff/worktree/combined/merge-all actions. **Explicitly tell the user this needs a
|
||||
visual pass — do not claim it works without running it.**
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
|
||||
git commit -m "feat(ui): fuse git tab into one approve+merge cockpit"
|
||||
```
|
||||
|
||||
### Task A.3: Verify diff-renderer consolidation
|
||||
|
||||
**Files:** none modified (verification only).
|
||||
|
||||
- [ ] **Step 1: Confirm DiffModal + Planning already use the canonical renderer**
|
||||
|
||||
Run: `rg -l "DiffLinesView" src/ClaudeDo.Ui/Views`
|
||||
Expected: matches in `Modals/DiffModalView.axaml` and `Planning/PlanningDiffView.axaml`.
|
||||
If `PlanningDiffView.axaml` does NOT use `DiffLinesView`, change its diff `ItemsControl`
|
||||
to a `<controls:DiffLinesView Lines="{Binding SelectedFile.Lines}" />` (matching
|
||||
`DiffModalView.axaml`'s usage) and rebuild the App project. If both already use it, this
|
||||
task is a no-op — record that and move on. (`WorktreeModalView`'s bespoke diff is
|
||||
intentionally left for Layer B.)
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** Foundation contract (spec §"Frozen worker conflict contract") →
|
||||
Task 0.1. Test fakes (spec parallel-boundaries row) → Task 0.2. Branch point (spec
|
||||
§"built & pushed this session") → Task 0.3. Layer A cockpit + Approve/merge flow
|
||||
together (spec §"Layer A") → Task A.2. Single-task approve-on-conflict opens resolver
|
||||
via seam (spec §"Layer A" + §"integration seams") → Task A.1. Diff consolidation
|
||||
(spec §"One diff model") → Task A.3. Output-footer feedback unchanged → not touched
|
||||
(correct). No spec requirement left unmapped for this session's scope.
|
||||
- **Placeholder scan:** none — every code step has concrete code; the only "mirror the
|
||||
existing harness" reference (Task A.1 Step 1) points at a real file with a working
|
||||
pattern, not a TODO.
|
||||
- **Type consistency:** `MergeConflictsDto`/`ConflictFileDto`/`ConflictHunkDto` and the
|
||||
5 method names match between `IWorkerClient` (0.1 Step 1), `WorkerClient` (0.1 Steps
|
||||
2–3), and both fakes (0.2). The seam `RequestConflictResolution` is
|
||||
`Func<string,string,Task>?` everywhere (A.1 Steps 1, 3–5). DTO field names match the
|
||||
spec.
|
||||
|
||||
---
|
||||
|
||||
## Integration notes (for the integrator merging A + B + C)
|
||||
|
||||
- Wire `DetailsIslandViewModel.RequestConflictResolution` and Layer B's equivalent
|
||||
callback to Layer C's `ConflictResolverViewModel` factory + `ShowConflictResolver`
|
||||
dialog delegate.
|
||||
- Layer C implements the worker hub methods `StartConflictMerge`, `GetMergeConflicts`,
|
||||
`WriteConflictResolution`, `ContinueMerge`, `AbortMerge`; the client side from Task
|
||||
0.1 already calls them by name.
|
||||
139
docs/superpowers/plans/2026-06-05-git-merge-review-prompts.md
Normal file
139
docs/superpowers/plans/2026-06-05-git-merge-review-prompts.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Git Merge/Review Rework — Parallel Kickoff Prompts (Layer B & Layer C)
|
||||
|
||||
These are self-contained prompts to paste into two fresh ClaudeDo sessions, each in its
|
||||
own git worktree, run **in parallel** with the main session's Layer A work.
|
||||
|
||||
**Prerequisite — branch point:** Both sessions must branch from `main` **at or after**
|
||||
the foundation commit `feat(ui): add conflict-resolution worker contract (foundation for
|
||||
merge rework)` (Phase 0, Task 0.3 of
|
||||
`docs/superpowers/plans/2026-06-05-git-merge-review-foundation-layerA.md`). That commit
|
||||
adds the frozen `IWorkerClient` conflict contract both layers rely on. Do not start B/C
|
||||
until that commit is pushed.
|
||||
|
||||
**Integration:** Neither session pushes to `main` or merges. Each leaves its branch/
|
||||
worktree for the orchestrator (the main session) to review and merge.
|
||||
|
||||
Design reference for both: `docs/superpowers/specs/2026-06-05-git-merge-review-rework-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Layer B — Multi-worktree merge cockpit
|
||||
|
||||
```
|
||||
We're reworking ClaudeDo's merge/review UX. Your job is Layer B: a multi-worktree merge
|
||||
cockpit. The overall design is in docs/superpowers/specs/2026-06-05-git-merge-review-rework-design.md
|
||||
(read the "Layer B" section and "Parallel boundaries" table first). A shared foundation
|
||||
commit ("add conflict-resolution worker contract") is already on main — branch from it.
|
||||
|
||||
First, create an isolated worktree for this work (use the superpowers:using-git-worktrees
|
||||
skill). Then write a plan (superpowers:writing-plans) for just Layer B and implement it
|
||||
with superpowers:subagent-driven-development (sonnet subagents, TDD, commit per task).
|
||||
|
||||
Scope:
|
||||
- Rework WorktreesOverviewModalView + WorktreesOverviewModalViewModel into a batch-merge
|
||||
cockpit: list mergeable worktrees, multi-select N, pick ONE target branch, "Merge all".
|
||||
- Skip-and-continue: loop the EXISTING IWorkerClient.MergeTaskAsync(taskId, target,
|
||||
removeWorktree:false, msg) over the selected tasks. Clean ones merge; conflicting ones
|
||||
(MergeTaskAsync returns Status=="conflict", auto-aborts leaving the tree clean) are
|
||||
collected into a "needs resolution" list shown with live progress.
|
||||
- Each conflict row gets a "Resolve" button that invokes a seam:
|
||||
public Func<string, string, Task>? RequestConflictResolution { get; set; } // (taskId, targetBranch)
|
||||
Define this callback property on the cockpit VM; leave it unwired (the orchestrator
|
||||
wires it to Layer C's resolver at merge time). Do NOT reference any ConflictResolver
|
||||
type.
|
||||
- Migrate WorktreeModalView's bespoke inline diff onto the canonical DiffLinesView
|
||||
control (src/ClaudeDo.Ui/Views/Controls/DiffLinesView.axaml) using DiffFileViewModel/
|
||||
DiffLineViewModel/UnifiedDiffParser (src/ClaudeDo.Ui/ViewModels/Modals/). This removes
|
||||
the last duplicate diff renderer.
|
||||
|
||||
Reuse these existing IWorkerClient methods (already implemented): MergeTaskAsync,
|
||||
GetMergeTargetsAsync, GetWorktreesOverviewAsync, SetWorktreeStateAsync,
|
||||
CleanupFinishedWorktreesAsync, ForceRemoveWorktreeAsync.
|
||||
|
||||
Do NOT touch (other layers own them): any worker-side files (WorkerHub, TaskMergeService,
|
||||
GitService), IWorkerClient.cs / WorkerClient.cs, WorkConsole.axaml,
|
||||
DetailsIslandViewModel.cs, or create the ConflictResolver UI.
|
||||
|
||||
Build with: dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release (a running
|
||||
Worker locks Debug — use Release). Keep locales/en.json and de.json keys in parity if you
|
||||
add any. If you change IWorkerClient (you shouldn't need to), update the hand-rolled fakes
|
||||
in tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs and
|
||||
tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs. No tests that spawn
|
||||
the real claude CLI.
|
||||
|
||||
Commit per task with Conventional Commits. Do NOT push to main and do NOT merge — leave
|
||||
your worktree/branch for the orchestrator. Flag any AXAML layout for visual verification
|
||||
rather than claiming it works.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer C — Inline conflict resolver
|
||||
|
||||
```
|
||||
We're reworking ClaudeDo's merge/review UX. Your job is Layer C: an in-app, VSCode-style
|
||||
inline conflict resolver, plus the worker plumbing it needs. The overall design is in
|
||||
docs/superpowers/specs/2026-06-05-git-merge-review-rework-design.md (read the "Layer C",
|
||||
"Frozen worker conflict contract", and "Parallel boundaries" sections first). A shared
|
||||
foundation commit ("add conflict-resolution worker contract") is already on main — branch
|
||||
from it. That commit already wired the CLIENT side (IWorkerClient + WorkerClient call
|
||||
these hub methods by name); your job includes implementing the matching WORKER hub methods.
|
||||
|
||||
First, create an isolated worktree (superpowers:using-git-worktrees). Then write a plan
|
||||
(superpowers:writing-plans) for Layer C and implement it with
|
||||
superpowers:subagent-driven-development (sonnet subagents, TDD, commit per task).
|
||||
|
||||
Worker side — implement these 5 hub methods in WorkerHub (names/params/returns MUST match
|
||||
the client calls already shipped in the foundation):
|
||||
- StartConflictMerge(string taskId, string targetBranch) -> MergeResultDto
|
||||
Calls TaskMergeService.MergeAsync with leaveConflictsInTree:true (the overload/flag
|
||||
already exists — used today by PlanningMergeOrchestrator). Leaves .git/MERGE_HEAD in
|
||||
the list's WorkingDir, returns Status="conflict" + conflict file list.
|
||||
- GetMergeConflicts(string taskId) -> MergeConflictsDto
|
||||
For each conflicted file (git diff --name-only --diff-filter=U), read ours/theirs/base
|
||||
via `git show :2:<path>` / `:3:<path>` / `:1:<path>`. Add GitService helpers as needed.
|
||||
- WriteConflictResolution(string taskId, string path, string resolvedContent) -> void
|
||||
Write resolvedContent to the file in WorkingDir and `git add` it.
|
||||
- ContinueMerge(string taskId) -> MergeResultDto
|
||||
Wrap the EXISTING TaskMergeService.ContinueMergeAsync (git add -A → re-check
|
||||
diff --diff-filter=U → git commit). Currently service-level only; expose it on the hub.
|
||||
- AbortMerge(string taskId) -> void
|
||||
Wrap the EXISTING TaskMergeService.AbortMergeAsync (git merge --abort).
|
||||
|
||||
Define worker-side DTO records that serialize identically to the client records already in
|
||||
WorkerClient.cs:
|
||||
MergeConflictsDto(string TaskId, IReadOnlyList<ConflictFileDto> Files)
|
||||
ConflictFileDto(string Path, IReadOnlyList<ConflictHunkDto> Hunks)
|
||||
ConflictHunkDto(string Ours, string Theirs, string? Base)
|
||||
(place beside the other hub DTOs in WorkerHub.cs). MergeResultDto already exists.
|
||||
|
||||
UI side — new files only:
|
||||
- ConflictResolverViewModel + ConflictResolverView. On open: StartConflictMergeAsync then
|
||||
GetMergeConflictsAsync(taskId). Per conflict hunk show ours vs theirs stacked with
|
||||
buttons Accept Current / Accept Incoming / Accept Both / Edit manually, plus a free-text
|
||||
box for the merged result of that hunk. Use the UI conflict model from the design
|
||||
(ConflictFile { Path, Hunks[] }, ConflictHunk { Ours, Theirs, Base, Resolution }) —
|
||||
shape it so a future 3-way pane needs no model change.
|
||||
- When every file is resolved: WriteConflictResolutionAsync per file, then
|
||||
ContinueMergeAsync(taskId) (Status "merged" closes; "conflict" means not fully resolved,
|
||||
stay open). AbortMergeAsync(taskId) cancels.
|
||||
- Expose a factory Func<string, ConflictResolverViewModel> and a
|
||||
Func<ConflictResolverViewModel, Task> ShowConflictResolver dialog delegate for the
|
||||
orchestrator to wire to Layer A/B's RequestConflictResolution(taskId, target) seams.
|
||||
|
||||
Do NOT touch (other layers own them): WorkerClient.cs, IWorkerClient.cs (already wired),
|
||||
WorkConsole.axaml, DetailsIslandViewModel.cs, WorktreesOverviewModalView/VM. You WILL need
|
||||
to add the 5 worker hub methods + GitService conflict reads.
|
||||
|
||||
Tests: add worker tests for the conflict reads / continue / abort using real SQLite + real
|
||||
git (follow existing GitService/TaskMergeService test patterns). NEVER spawn the real
|
||||
claude CLI. If you change IWorkerClient (you should NOT — client is frozen), update the
|
||||
fakes in both test projects.
|
||||
|
||||
Build with: dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release and
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release (a running Worker locks
|
||||
Debug). Keep locales/en.json and de.json in parity for any new UI strings.
|
||||
|
||||
Commit per task with Conventional Commits. Do NOT push to main and do NOT merge — leave
|
||||
your worktree/branch for the orchestrator. Flag the resolver UI for visual verification.
|
||||
```
|
||||
@@ -0,0 +1,920 @@
|
||||
# Layer C — Inline Conflict Resolver Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build the worker-side conflict plumbing (5 frozen hub methods + GitService reads) and a VSCode-style in-app inline conflict resolver UI for ClaudeDo's merge rework.
|
||||
|
||||
**Architecture:** The worker performs a real merge that leaves conflicts in the list's working tree (`leaveConflictsInTree:true`), exposes ours/theirs/base per conflicted file via `git show :2:/:3:/:1:`, accepts written resolutions, and finishes via the existing `ContinueMergeAsync`/`AbortMergeAsync`. The UI presents each conflicted file's hunk with Accept Current/Incoming/Both/Edit-manually controls plus a free-text merged box, then writes resolutions and continues.
|
||||
|
||||
**Tech Stack:** .NET 8, ASP.NET Core SignalR (WorkerHub), EF Core/SQLite, Avalonia MVVM (CommunityToolkit), xUnit + real git/SQLite fixtures.
|
||||
|
||||
**Frozen client contract (already shipped in foundation commit `2dfc455`, DO NOT edit):**
|
||||
- `IWorkerClient` / `WorkerClient.cs` already call hub methods by name: `StartConflictMerge`, `GetMergeConflicts`, `WriteConflictResolution`, `ContinueMerge`, `AbortMerge`.
|
||||
- Client DTOs already exist in `WorkerClient.cs`: `MergeConflictsDto(string TaskId, IReadOnlyList<ConflictFileDto> Files)`, `ConflictFileDto(string Path, IReadOnlyList<ConflictHunkDto> Hunks)`, `ConflictHunkDto(string Ours, string Theirs, string? Base)`, plus existing `MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage)`.
|
||||
- Worker-side DTOs must serialize identically (same record shape) and live in `WorkerHub.cs`.
|
||||
|
||||
**Do NOT touch:** `WorkerClient.cs`, `Interfaces/IWorkerClient.cs`, `WorkConsole.axaml`, `DetailsIslandViewModel.cs`, `WorktreesOverviewModalView/VM`, `WorktreeModalView`. Test fakes for `IWorkerClient` already implement the 5 methods as no-op stubs (`StubWorkerClient` is `virtual` in Ui.Tests) — subclass/override, never edit the interface.
|
||||
|
||||
**Build/test commands (.NET 8 — running Worker locks `Debug`, always `-c Release`):**
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
**Worker / Data (create + modify):**
|
||||
- Modify `src/ClaudeDo.Data/Git/GitService.cs` — add `ShowStageAsync` (untrimmed blob read) + `AddPathAsync`; add `trimOutput` param to `RunGitAsync`.
|
||||
- Modify `src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs` — add records `MergeConflicts`/`ConflictFileContent`; add `GetConflictsAsync` + `WriteResolutionAsync`.
|
||||
- Modify `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — add DTOs `MergeConflictsDto`/`ConflictFileDto`/`ConflictHunkDto` + 5 hub methods.
|
||||
|
||||
**UI (create new only):**
|
||||
- Create `src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictModels.cs` — `ConflictFile`, `ConflictHunk`.
|
||||
- Create `src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs`.
|
||||
- Create `src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml` + `.axaml.cs`.
|
||||
|
||||
**Wiring (modify):**
|
||||
- Modify `src/ClaudeDo.App/Program.cs` — register `ConflictResolverViewModel` + `Func<string, ConflictResolverViewModel>`.
|
||||
- Modify `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs` — additive seam (`ConflictResolverFactory`, `ShowConflictResolver`, `RequestConflictResolutionAsync`).
|
||||
- Modify `src/ClaudeDo.Ui/Views/MainWindow.axaml.cs` — wire `ShowConflictResolver` dialog delegate.
|
||||
- Modify `src/ClaudeDo.Localization/locales/en.json` + `de.json` — `conflictResolver.*` keys (parity enforced by Localization.Tests).
|
||||
|
||||
**Tests (create + modify):**
|
||||
- Modify `tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs` — conflict-read / write-resolution / round-trip tests.
|
||||
- Create `tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolverViewModelTests.cs`.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: GitService conflict-blob reads
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Data/Git/GitService.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs` (GitService exercised here via real repo; add focused tests in Task 2 round-trip)
|
||||
|
||||
- [ ] **Step 1: Add `trimOutput` param to `RunGitAsync`** so blob reads keep exact bytes.
|
||||
|
||||
In `RunGitAsync` signature add `bool trimOutput = true`, and change the return to:
|
||||
```csharp
|
||||
return (proc.ExitCode, trimOutput ? stdout.TrimEnd() : stdout, stderr.TrimEnd());
|
||||
```
|
||||
(All existing callers keep the default `true`.)
|
||||
|
||||
- [ ] **Step 2: Add `ShowStageAsync` + `AddPathAsync`** (place after `ListConflictedFilesAsync`):
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Reads a conflicted file's blob at a merge stage: 1=base, 2=ours, 3=theirs.
|
||||
/// Returns null when the stage doesn't exist (e.g. add/add conflict has no base).
|
||||
/// Output is NOT trimmed so file content round-trips exactly.
|
||||
/// </summary>
|
||||
public async Task<string?> ShowStageAsync(string repoDir, int stage, string path, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["show", $":{stage}:{path}"], ct, trimOutput: false);
|
||||
return exitCode == 0 ? stdout : null;
|
||||
}
|
||||
|
||||
public async Task AddPathAsync(string repoDir, string path, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["add", "--", path], ct);
|
||||
if (exitCode != 0)
|
||||
throw new InvalidOperationException($"git add '{path}' failed (exit {exitCode}): {stderr}");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build the Data + Worker projects to verify compilation.**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||
Expected: Build succeeded, 0 errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Data/Git/GitService.cs
|
||||
git commit -m "feat(git): add conflict-stage blob reads and single-path staging"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: TaskMergeService conflict reads + resolution writes
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs`
|
||||
|
||||
- [ ] **Step 1: Write failing tests** (append inside `TaskMergeServiceTests`, before `#region Test doubles`). Reuse the existing helpers `SeedListAndTask`, `SeedWorktree`, `BuildService`, and the `GitRepoFixture` conflict setup pattern from `ContinueMergeAsync_AfterUserResolves...`.
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task GetConflictsAsync_AfterConflictMerge_ReturnsOursAndTheirs()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
|
||||
var db = NewDb();
|
||||
var repo = NewRepo();
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", "-m", "main");
|
||||
File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "# main change\n");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-am", "main change");
|
||||
|
||||
var wtPath = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
|
||||
_wtCleanups.Add((repo.RepoDir, wtPath));
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "worktree", "add", "-b", "claudedo/c1", wtPath, repo.BaseCommit);
|
||||
File.WriteAllText(Path.Combine(wtPath, "README.md"), "# branch change\n");
|
||||
GitRepoFixture.RunGit(wtPath, "commit", "-am", "branch change");
|
||||
|
||||
var (_, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.WaitingForReview);
|
||||
await SeedWorktree(db, task.Id, wtPath, "claudedo/c1", repo.BaseCommit);
|
||||
|
||||
var (svc, _) = BuildService(db);
|
||||
var start = await svc.MergeAsync(task.Id, "main", false, "msg", leaveConflictsInTree: true, CancellationToken.None);
|
||||
Assert.Equal(TaskMergeService.StatusConflict, start.Status);
|
||||
|
||||
var conflicts = await svc.GetConflictsAsync(task.Id, CancellationToken.None);
|
||||
|
||||
Assert.Equal(task.Id, conflicts.TaskId);
|
||||
var file = Assert.Single(conflicts.Files);
|
||||
Assert.Equal("README.md", file.Path);
|
||||
Assert.Contains("main change", file.Ours); // ours = target (main) side after checkout
|
||||
Assert.Contains("branch change", file.Theirs); // theirs = merged-in branch
|
||||
Assert.NotNull(file.Base);
|
||||
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "merge", "--abort");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteResolutionAsync_ThenContinue_CompletesMerge()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
|
||||
var db = NewDb();
|
||||
var repo = NewRepo();
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", "-m", "main");
|
||||
File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "# main change\n");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-am", "main change");
|
||||
|
||||
var wtPath = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
|
||||
_wtCleanups.Add((repo.RepoDir, wtPath));
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "worktree", "add", "-b", "claudedo/c2", wtPath, repo.BaseCommit);
|
||||
File.WriteAllText(Path.Combine(wtPath, "README.md"), "# branch change\n");
|
||||
GitRepoFixture.RunGit(wtPath, "commit", "-am", "branch change");
|
||||
|
||||
var (_, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.WaitingForReview);
|
||||
await SeedWorktree(db, task.Id, wtPath, "claudedo/c2", repo.BaseCommit);
|
||||
|
||||
var (svc, _) = BuildService(db);
|
||||
await svc.MergeAsync(task.Id, "main", false, "msg", leaveConflictsInTree: true, CancellationToken.None);
|
||||
|
||||
await svc.WriteResolutionAsync(task.Id, "README.md", "# resolved by user\n", CancellationToken.None);
|
||||
var result = await svc.ContinueMergeAsync(task.Id, CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.StatusMerged, result.Status);
|
||||
Assert.Equal("# resolved by user\n", File.ReadAllText(Path.Combine(repo.RepoDir, "README.md")));
|
||||
Assert.False(await new GitService().IsMidMergeAsync(repo.RepoDir));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail** (no such methods).
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "GetConflictsAsync_AfterConflictMerge_ReturnsOursAndTheirs|WriteResolutionAsync_ThenContinue_CompletesMerge"`
|
||||
Expected: compile error / FAIL (methods don't exist).
|
||||
|
||||
- [ ] **Step 3: Add records + methods to `TaskMergeService.cs`.**
|
||||
|
||||
Add records beside `MergeResult` (top of file, after the existing record declarations):
|
||||
```csharp
|
||||
public sealed record MergeConflicts(
|
||||
string TaskId,
|
||||
IReadOnlyList<ConflictFileContent> Files);
|
||||
|
||||
public sealed record ConflictFileContent(
|
||||
string Path,
|
||||
string Ours,
|
||||
string Theirs,
|
||||
string? Base);
|
||||
```
|
||||
|
||||
Add methods inside the class (after `AbortMergeAsync`):
|
||||
```csharp
|
||||
public async Task<MergeConflicts> GetConflictsAsync(string taskId, CancellationToken ct)
|
||||
{
|
||||
var (_, list, _) = await LoadMergeContextAsync(taskId, ct);
|
||||
if (string.IsNullOrWhiteSpace(list.WorkingDir))
|
||||
throw new InvalidOperationException("list has no working directory");
|
||||
|
||||
var files = await _git.ListConflictedFilesAsync(list.WorkingDir, ct);
|
||||
var result = new List<ConflictFileContent>(files.Count);
|
||||
foreach (var path in files)
|
||||
{
|
||||
var ours = await _git.ShowStageAsync(list.WorkingDir, 2, path, ct) ?? "";
|
||||
var theirs = await _git.ShowStageAsync(list.WorkingDir, 3, path, ct) ?? "";
|
||||
var @base = await _git.ShowStageAsync(list.WorkingDir, 1, path, ct);
|
||||
result.Add(new ConflictFileContent(path, ours, theirs, @base));
|
||||
}
|
||||
return new MergeConflicts(taskId, result);
|
||||
}
|
||||
|
||||
public async Task WriteResolutionAsync(string taskId, string path, string content, CancellationToken ct)
|
||||
{
|
||||
var (_, list, _) = await LoadMergeContextAsync(taskId, ct);
|
||||
if (string.IsNullOrWhiteSpace(list.WorkingDir))
|
||||
throw new InvalidOperationException("list has no working directory");
|
||||
|
||||
var full = Path.Combine(list.WorkingDir, path.Replace('/', Path.DirectorySeparatorChar));
|
||||
await File.WriteAllTextAsync(full, content, ct);
|
||||
await _git.AddPathAsync(list.WorkingDir, path, ct);
|
||||
}
|
||||
```
|
||||
(Note: `Path` is `System.IO.Path` — the file already uses it via other helpers; the record property `Path` does not shadow it inside these methods because it's accessed as a static type, not an instance member.)
|
||||
|
||||
- [ ] **Step 4: Run the tests to verify they pass.**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "GetConflictsAsync_AfterConflictMerge_ReturnsOursAndTheirs|WriteResolutionAsync_ThenContinue_CompletesMerge"`
|
||||
Expected: PASS (2 tests). If git unavailable they no-op.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs
|
||||
git commit -m "feat(merge): read conflict stages and write user resolutions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: WorkerHub conflict methods + DTOs
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||
|
||||
- [ ] **Step 1: Add DTOs** beside the existing merge DTOs (after `public record MergeTargetsDto(...)`):
|
||||
|
||||
```csharp
|
||||
public record MergeConflictsDto(string TaskId, IReadOnlyList<ConflictFileDto> Files);
|
||||
public record ConflictFileDto(string Path, IReadOnlyList<ConflictHunkDto> Hunks);
|
||||
public record ConflictHunkDto(string Ours, string Theirs, string? Base);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the 5 hub methods** (after `PreviewMerge`). Names/params/returns MUST match the frozen client calls.
|
||||
|
||||
```csharp
|
||||
public Task<MergeResultDto> StartConflictMerge(string taskId, string targetBranch)
|
||||
=> HubGuard(async () =>
|
||||
{
|
||||
var r = await _mergeService.MergeAsync(
|
||||
taskId, targetBranch ?? "", removeWorktree: false, "Merge task",
|
||||
leaveConflictsInTree: true, CancellationToken.None);
|
||||
if (r.Status == TaskMergeService.StatusBlocked)
|
||||
throw new HubException(r.ErrorMessage ?? "merge blocked");
|
||||
return new MergeResultDto(r.Status, r.ConflictFiles, r.ErrorMessage);
|
||||
});
|
||||
|
||||
public Task<MergeConflictsDto> GetMergeConflicts(string taskId)
|
||||
=> HubGuard(async () =>
|
||||
{
|
||||
var c = await _mergeService.GetConflictsAsync(taskId, CancellationToken.None);
|
||||
return new MergeConflictsDto(
|
||||
c.TaskId,
|
||||
c.Files.Select(f => new ConflictFileDto(
|
||||
f.Path,
|
||||
new[] { new ConflictHunkDto(f.Ours, f.Theirs, f.Base) })).ToList());
|
||||
});
|
||||
|
||||
public Task WriteConflictResolution(string taskId, string path, string resolvedContent)
|
||||
=> HubGuard(() => _mergeService.WriteResolutionAsync(
|
||||
taskId, path, resolvedContent ?? "", CancellationToken.None));
|
||||
|
||||
public Task<MergeResultDto> ContinueMerge(string taskId)
|
||||
=> HubGuard(async () =>
|
||||
{
|
||||
var r = await _mergeService.ContinueMergeAsync(taskId, CancellationToken.None);
|
||||
if (r.Status == TaskMergeService.StatusBlocked)
|
||||
throw new HubException(r.ErrorMessage ?? "continue failed");
|
||||
return new MergeResultDto(r.Status, r.ConflictFiles, r.ErrorMessage);
|
||||
});
|
||||
|
||||
public Task AbortMerge(string taskId)
|
||||
=> HubGuard(async () =>
|
||||
{
|
||||
var r = await _mergeService.AbortMergeAsync(taskId, CancellationToken.None);
|
||||
if (r.Status == TaskMergeService.StatusBlocked)
|
||||
throw new HubException(r.ErrorMessage ?? "abort failed");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build the Worker project to verify compilation.**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||
Expected: Build succeeded, 0 errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs
|
||||
git commit -m "feat(hub): expose conflict-resolution merge methods"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Conflict UI model
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictModels.cs`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolverViewModelTests.cs` (model tests added here in Task 5; this task is build-verified)
|
||||
|
||||
- [ ] **Step 1: Create the model file.** Shaped so a 3-way pane needs no model change (`Base` retained per hunk).
|
||||
|
||||
```csharp
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Conflicts;
|
||||
|
||||
public sealed partial class ConflictHunk : ObservableObject
|
||||
{
|
||||
public string Ours { get; }
|
||||
public string Theirs { get; }
|
||||
public string? Base { get; }
|
||||
|
||||
[ObservableProperty] private string? _resolution;
|
||||
|
||||
public bool IsResolved => Resolution is not null;
|
||||
|
||||
public ConflictHunk(string ours, string theirs, string? @base)
|
||||
{
|
||||
Ours = ours;
|
||||
Theirs = theirs;
|
||||
Base = @base;
|
||||
}
|
||||
|
||||
partial void OnResolutionChanged(string? value) => OnPropertyChanged(nameof(IsResolved));
|
||||
|
||||
[RelayCommand] private void AcceptCurrent() => Resolution = Ours;
|
||||
[RelayCommand] private void AcceptIncoming() => Resolution = Theirs;
|
||||
[RelayCommand] private void AcceptBoth() => Resolution = Ours + Theirs;
|
||||
[RelayCommand] private void EditManually() => Resolution ??= Ours;
|
||||
}
|
||||
|
||||
public sealed class ConflictFile
|
||||
{
|
||||
public string Path { get; }
|
||||
public IReadOnlyList<ConflictHunk> Hunks { get; }
|
||||
|
||||
public ConflictFile(string path, IReadOnlyList<ConflictHunk> hunks)
|
||||
{
|
||||
Path = path;
|
||||
Hunks = hunks;
|
||||
}
|
||||
|
||||
public bool AllHunksResolved => Hunks.Count > 0 && Hunks.All(h => h.IsResolved);
|
||||
|
||||
/// <summary>The merged file content: concatenation of each hunk's resolution
|
||||
/// (single whole-file hunk today; concatenation keeps it correct for multi-hunk later).</summary>
|
||||
public string ComposeResolvedContent() => string.Concat(Hunks.Select(h => h.Resolution));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build the Ui project to verify compilation.**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictModels.cs
|
||||
git commit -m "feat(ui): add inline conflict model (file/hunk with resolution)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: ConflictResolverViewModel
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolverViewModelTests.cs`
|
||||
|
||||
- [ ] **Step 1: Write failing tests.** Subclass the existing `StubWorkerClient` (its conflict methods are `virtual`).
|
||||
|
||||
```csharp
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Conflicts;
|
||||
using Xunit;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class ConflictResolverViewModelTests
|
||||
{
|
||||
private sealed class FakeWorker : StubWorkerClient
|
||||
{
|
||||
public string? WrittenPath;
|
||||
public string? WrittenContent;
|
||||
public bool Continued;
|
||||
public bool Aborted;
|
||||
public string ContinueStatus = "merged";
|
||||
|
||||
public override Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch)
|
||||
=> Task.FromResult(new MergeResultDto("conflict", new[] { "README.md" }, null));
|
||||
|
||||
public override Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId)
|
||||
=> Task.FromResult(new MergeConflictsDto(taskId, new[]
|
||||
{
|
||||
new ConflictFileDto("README.md", new[] { new ConflictHunkDto("ours\n", "theirs\n", "base\n") })
|
||||
}));
|
||||
|
||||
public override Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent)
|
||||
{
|
||||
WrittenPath = path; WrittenContent = resolvedContent; return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<MergeResultDto> ContinueMergeAsync(string taskId)
|
||||
{
|
||||
Continued = true;
|
||||
return Task.FromResult(new MergeResultDto(ContinueStatus, System.Array.Empty<string>(), null));
|
||||
}
|
||||
|
||||
public override Task AbortMergeAsync(string taskId) { Aborted = true; return Task.CompletedTask; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenAsync_LoadsConflicts_AndBlocksContinueUntilResolved()
|
||||
{
|
||||
var vm = new ConflictResolverViewModel(new FakeWorker(), "task-1");
|
||||
var hasConflicts = await vm.OpenAsync("main");
|
||||
|
||||
Assert.True(hasConflicts);
|
||||
var file = Assert.Single(vm.Files);
|
||||
Assert.Equal("README.md", file.Path);
|
||||
Assert.False(vm.CanContinue); // nothing resolved yet
|
||||
|
||||
file.Hunks[0].AcceptIncomingCommand.Execute(null);
|
||||
Assert.True(vm.CanContinue); // every hunk resolved
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Continue_WritesComposedResolution_AndClosesOnMerged()
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
var vm = new ConflictResolverViewModel(worker, "task-1");
|
||||
var closed = false;
|
||||
vm.CloseRequested = () => closed = true;
|
||||
|
||||
await vm.OpenAsync("main");
|
||||
vm.Files[0].Hunks[0].AcceptCurrentCommand.Execute(null); // resolution = "ours\n"
|
||||
await vm.ContinueCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Equal("README.md", worker.WrittenPath);
|
||||
Assert.Equal("ours\n", worker.WrittenContent);
|
||||
Assert.True(worker.Continued);
|
||||
Assert.True(closed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Continue_StaysOpenAndReportsError_WhenStillConflicted()
|
||||
{
|
||||
var worker = new FakeWorker { ContinueStatus = "conflict" };
|
||||
var vm = new ConflictResolverViewModel(worker, "task-1");
|
||||
var closed = false;
|
||||
vm.CloseRequested = () => closed = true;
|
||||
|
||||
await vm.OpenAsync("main");
|
||||
vm.Files[0].Hunks[0].AcceptBothCommand.Execute(null);
|
||||
await vm.ContinueCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.False(closed);
|
||||
Assert.NotNull(vm.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Abort_CallsWorkerAndCloses()
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
var vm = new ConflictResolverViewModel(worker, "task-1");
|
||||
var closed = false;
|
||||
vm.CloseRequested = () => closed = true;
|
||||
|
||||
await vm.AbortCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.True(worker.Aborted);
|
||||
Assert.True(closed);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail** (VM not defined).
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter "ConflictResolverViewModelTests"`
|
||||
Expected: compile error / FAIL.
|
||||
|
||||
- [ ] **Step 3: Implement the ViewModel.**
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ClaudeDo.Ui.Services;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Conflicts;
|
||||
|
||||
public sealed partial class ConflictResolverViewModel : ObservableObject
|
||||
{
|
||||
private readonly IWorkerClient _worker;
|
||||
private readonly string _taskId;
|
||||
|
||||
public ObservableCollection<ConflictFile> Files { get; } = new();
|
||||
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
[ObservableProperty] private string? _error;
|
||||
[ObservableProperty] private bool _canContinue;
|
||||
|
||||
public string TaskId => _taskId;
|
||||
public Action? CloseRequested { get; set; }
|
||||
|
||||
public ConflictResolverViewModel(IWorkerClient worker, string taskId)
|
||||
{
|
||||
_worker = worker;
|
||||
_taskId = taskId;
|
||||
}
|
||||
|
||||
/// <summary>Starts the conflict merge and loads ours/theirs/base per file.
|
||||
/// Returns true when there are conflicts to resolve (caller should show the dialog).</summary>
|
||||
public async Task<bool> OpenAsync(string targetBranch)
|
||||
{
|
||||
IsBusy = true;
|
||||
Error = null;
|
||||
try
|
||||
{
|
||||
var start = await _worker.StartConflictMergeAsync(_taskId, targetBranch);
|
||||
if (!string.Equals(start.Status, "conflict", StringComparison.Ordinal))
|
||||
{
|
||||
if (string.Equals(start.Status, "blocked", StringComparison.Ordinal))
|
||||
Error = start.ErrorMessage;
|
||||
return false;
|
||||
}
|
||||
|
||||
var conflicts = await _worker.GetMergeConflictsAsync(_taskId);
|
||||
Files.Clear();
|
||||
foreach (var f in conflicts.Files)
|
||||
{
|
||||
var hunks = f.Hunks.Select(h =>
|
||||
{
|
||||
var hk = new ConflictHunk(h.Ours, h.Theirs, h.Base);
|
||||
hk.PropertyChanged += OnHunkChanged;
|
||||
return hk;
|
||||
}).ToList();
|
||||
Files.Add(new ConflictFile(f.Path, hunks));
|
||||
}
|
||||
RecomputeCanContinue();
|
||||
return Files.Count > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
private void OnHunkChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is nameof(ConflictHunk.IsResolved) or nameof(ConflictHunk.Resolution))
|
||||
RecomputeCanContinue();
|
||||
}
|
||||
|
||||
private void RecomputeCanContinue()
|
||||
=> CanContinue = Files.Count > 0 && Files.All(f => f.AllHunksResolved);
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ContinueAsync()
|
||||
{
|
||||
if (!CanContinue) return;
|
||||
IsBusy = true;
|
||||
Error = null;
|
||||
try
|
||||
{
|
||||
foreach (var file in Files)
|
||||
await _worker.WriteConflictResolutionAsync(_taskId, file.Path, file.ComposeResolvedContent());
|
||||
|
||||
var result = await _worker.ContinueMergeAsync(_taskId);
|
||||
if (string.Equals(result.Status, "merged", StringComparison.Ordinal))
|
||||
CloseRequested?.Invoke();
|
||||
else
|
||||
Error = result.ErrorMessage ?? "Conflicts not fully resolved — review and retry.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error = ex.Message;
|
||||
}
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AbortAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
try { await _worker.AbortMergeAsync(_taskId); }
|
||||
catch (Exception ex) { Error = ex.Message; }
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
CloseRequested?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass.**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter "ConflictResolverViewModelTests"`
|
||||
Expected: PASS (4 tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolverViewModelTests.cs
|
||||
git commit -m "feat(ui): add inline conflict resolver view-model"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: ConflictResolverView + localization
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml`
|
||||
- Create: `src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs`
|
||||
- Modify: `src/ClaudeDo.Localization/locales/en.json`
|
||||
- Modify: `src/ClaudeDo.Localization/locales/de.json`
|
||||
|
||||
- [ ] **Step 1: Add localization keys** to `en.json` as a new top-level section (sibling of `"planning"`):
|
||||
|
||||
```json
|
||||
"conflictResolver": {
|
||||
"windowTitle": "Resolve merge conflicts",
|
||||
"modalTitle": "RESOLVE CONFLICTS",
|
||||
"loading": "Loading conflicts…",
|
||||
"current": "Current (ours)",
|
||||
"incoming": "Incoming (theirs)",
|
||||
"mergedResult": "Merged result",
|
||||
"acceptCurrent": "Accept Current",
|
||||
"acceptIncoming": "Accept Incoming",
|
||||
"acceptBoth": "Accept Both",
|
||||
"editManually": "Edit manually",
|
||||
"continue": "Resolve & continue",
|
||||
"abort": "Abort merge"
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the SAME keys to `de.json`** (German values, identical key set — parity enforced by Localization.Tests):
|
||||
|
||||
```json
|
||||
"conflictResolver": {
|
||||
"windowTitle": "Merge-Konflikte lösen",
|
||||
"modalTitle": "KONFLIKTE LÖSEN",
|
||||
"loading": "Konflikte werden geladen…",
|
||||
"current": "Aktuell (unsere)",
|
||||
"incoming": "Eingehend (ihre)",
|
||||
"mergedResult": "Zusammengeführtes Ergebnis",
|
||||
"acceptCurrent": "Aktuelle übernehmen",
|
||||
"acceptIncoming": "Eingehende übernehmen",
|
||||
"acceptBoth": "Beide übernehmen",
|
||||
"editManually": "Manuell bearbeiten",
|
||||
"continue": "Lösen & fortfahren",
|
||||
"abort": "Merge abbrechen"
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create the View** (`ConflictResolverView.axaml`). A `Window` using `ModalShell`, mirroring `ConflictResolutionView.axaml`. Two stacked read-only boxes (ours/theirs), a button row, and a two-way merged-result box per hunk.
|
||||
|
||||
```xml
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Conflicts"
|
||||
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||
x:DataType="vm:ConflictResolverViewModel"
|
||||
x:Class="ClaudeDo.Ui.Views.Conflicts.ConflictResolverView"
|
||||
Title="{loc:Tr conflictResolver.windowTitle}"
|
||||
Width="760" Height="640" MinWidth="560" MinHeight="420"
|
||||
CanResize="True"
|
||||
WindowDecorations="BorderOnly"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaTitleBarHeightHint="-1"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="{DynamicResource SurfaceBrush}">
|
||||
|
||||
<Window.KeyBindings>
|
||||
<KeyBinding Gesture="Escape" Command="{Binding AbortCommand}"/>
|
||||
</Window.KeyBindings>
|
||||
|
||||
<ctl:ModalShell Title="{loc:Tr conflictResolver.modalTitle}" CloseCommand="{Binding AbortCommand}">
|
||||
<ctl:ModalShell.Footer>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<Button Classes="btn" Content="{loc:Tr conflictResolver.continue}"
|
||||
Command="{Binding ContinueCommand}" IsEnabled="{Binding CanContinue}"/>
|
||||
<Button Classes="btn" Content="{loc:Tr conflictResolver.abort}" Command="{Binding AbortCommand}"/>
|
||||
</StackPanel>
|
||||
</ctl:ModalShell.Footer>
|
||||
|
||||
<Grid RowDefinitions="Auto,*" Margin="16,12">
|
||||
<TextBlock Grid.Row="0" Classes="meta" Margin="0,0,0,8"
|
||||
Text="{loc:Tr conflictResolver.loading}"
|
||||
IsVisible="{Binding IsBusy}"/>
|
||||
<TextBlock Grid.Row="0" Classes="meta" Foreground="{DynamicResource BloodBrush}"
|
||||
Text="{Binding Error}" TextWrapping="Wrap"
|
||||
IsVisible="{Binding Error, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<ItemsControl ItemsSource="{Binding Files}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ConflictFile">
|
||||
<StackPanel Spacing="8" Margin="0,0,0,16">
|
||||
<TextBlock Classes="path-mono heading" Text="{Binding Path}"/>
|
||||
<ItemsControl ItemsSource="{Binding Hunks}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ConflictHunk">
|
||||
<Border BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"
|
||||
CornerRadius="6" Padding="10" Margin="0,0,0,8">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="meta" Text="{loc:Tr conflictResolver.current}"/>
|
||||
<TextBox Text="{Binding Ours, Mode=OneWay}" IsReadOnly="True"
|
||||
TextWrapping="NoWrap" AcceptsReturn="True" MaxHeight="120"
|
||||
FontFamily="{DynamicResource MonoFont}"/>
|
||||
<TextBlock Classes="meta" Text="{loc:Tr conflictResolver.incoming}"/>
|
||||
<TextBox Text="{Binding Theirs, Mode=OneWay}" IsReadOnly="True"
|
||||
TextWrapping="NoWrap" AcceptsReturn="True" MaxHeight="120"
|
||||
FontFamily="{DynamicResource MonoFont}"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="btn" Content="{loc:Tr conflictResolver.acceptCurrent}"
|
||||
Command="{Binding AcceptCurrentCommand}"/>
|
||||
<Button Classes="btn" Content="{loc:Tr conflictResolver.acceptIncoming}"
|
||||
Command="{Binding AcceptIncomingCommand}"/>
|
||||
<Button Classes="btn" Content="{loc:Tr conflictResolver.acceptBoth}"
|
||||
Command="{Binding AcceptBothCommand}"/>
|
||||
<Button Classes="btn" Content="{loc:Tr conflictResolver.editManually}"
|
||||
Command="{Binding EditManuallyCommand}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Classes="meta" Text="{loc:Tr conflictResolver.mergedResult}"/>
|
||||
<TextBox Text="{Binding Resolution, Mode=TwoWay}"
|
||||
TextWrapping="NoWrap" AcceptsReturn="True" MinHeight="80" MaxHeight="200"
|
||||
FontFamily="{DynamicResource MonoFont}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</ctl:ModalShell>
|
||||
</Window>
|
||||
```
|
||||
**Note for the implementer:** if `MonoFont` / `path-mono` / `heading` / `meta` / `btn` resource keys or style classes don't resolve at build, drop the `FontFamily` attribute and unknown `Classes` (keep `btn`) — match whatever the existing `ConflictResolutionView.axaml` and app styles actually expose. Verify against `src/ClaudeDo.Ui/Views/Planning/ConflictResolutionView.axaml` and the app's style resources before finalizing.
|
||||
|
||||
- [ ] **Step 4: Create the code-behind** (`ConflictResolverView.axaml.cs`):
|
||||
|
||||
```csharp
|
||||
using Avalonia.Controls;
|
||||
using ClaudeDo.Ui.ViewModels.Conflicts;
|
||||
|
||||
namespace ClaudeDo.Ui.Views.Conflicts;
|
||||
|
||||
public partial class ConflictResolverView : Window
|
||||
{
|
||||
public ConflictResolverView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(System.EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is ConflictResolverViewModel vm)
|
||||
vm.CloseRequested = Close;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Build the App + run Localization tests.**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release && dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release`
|
||||
Expected: Build succeeded; localization parity tests PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs src/ClaudeDo.Localization/locales/en.json src/ClaudeDo.Localization/locales/de.json
|
||||
git commit -m "feat(ui): add inline conflict resolver view and localization"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Wire factory + dialog seam for the integrator
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.App/Program.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml.cs`
|
||||
|
||||
These are additive seams only. The integrator connects Layer A/B's `RequestConflictResolution(taskId, target)` callback to `IslandsShellViewModel.RequestConflictResolutionAsync`.
|
||||
|
||||
- [ ] **Step 1: Register the factory in `Program.cs`** (in the ViewModels region, near the other `Func<>` factories). Only the `Func<>` factory is needed — the VM is never resolved directly:
|
||||
|
||||
```csharp
|
||||
sc.AddSingleton<Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>>(sp =>
|
||||
taskId => new ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel(
|
||||
sp.GetRequiredService<WorkerClient>(), taskId));
|
||||
```
|
||||
Then, after `IslandsShellViewModel` is registered, set the factory on it once resolved. Replace the existing `sc.AddSingleton<IslandsShellViewModel>();` registration with a factory that injects the conflict-resolver factory:
|
||||
|
||||
```csharp
|
||||
sc.AddSingleton<IslandsShellViewModel>(sp =>
|
||||
{
|
||||
var shell = ActivatorUtilities.CreateInstance<IslandsShellViewModel>(sp);
|
||||
shell.ConflictResolverFactory =
|
||||
sp.GetRequiredService<Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>>();
|
||||
return shell;
|
||||
});
|
||||
```
|
||||
(`ActivatorUtilities.CreateInstance` resolves the existing big constructor + its `Func<>` deps exactly as the default registration did.)
|
||||
|
||||
- [ ] **Step 2: Add the additive seam to `IslandsShellViewModel`** (near the other `Show*` delegate properties):
|
||||
|
||||
```csharp
|
||||
// Layer C seam: composition root sets the factory; MainWindow sets the dialog opener.
|
||||
// The integrator connects Layer A/B's RequestConflictResolution(taskId, target) to this method.
|
||||
public Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>? ConflictResolverFactory { get; set; }
|
||||
public Func<ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel, Task>? ShowConflictResolver { get; set; }
|
||||
|
||||
public async Task RequestConflictResolutionAsync(string taskId, string targetBranch)
|
||||
{
|
||||
if (ConflictResolverFactory is null || ShowConflictResolver is null) return;
|
||||
var vm = ConflictResolverFactory(taskId);
|
||||
var hasConflicts = await vm.OpenAsync(targetBranch);
|
||||
if (hasConflicts)
|
||||
await ShowConflictResolver(vm);
|
||||
}
|
||||
```
|
||||
(Add `using ClaudeDo.Ui.ViewModels.Conflicts;` or use fully-qualified names as above.)
|
||||
|
||||
- [ ] **Step 3: Wire the dialog opener in `MainWindow.axaml.cs`** inside `OnDataContextChanged`, alongside the other `vm.Show*` assignments:
|
||||
|
||||
```csharp
|
||||
vm.ShowConflictResolver = async (resolverVm) =>
|
||||
{
|
||||
var dlg = new ClaudeDo.Ui.Views.Conflicts.ConflictResolverView { DataContext = resolverVm };
|
||||
await dlg.ShowDialog(this);
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build the App to verify compilation.**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.App/Program.cs src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs src/ClaudeDo.Ui/Views/MainWindow.axaml.cs
|
||||
git commit -m "feat(ui): expose conflict-resolver factory and dialog seam for integrator"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Full verification
|
||||
|
||||
- [ ] **Step 1: Build both head projects.**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
```
|
||||
Expected: both Build succeeded, 0 errors/warnings.
|
||||
|
||||
- [ ] **Step 2: Run the full relevant test suites.**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||
```
|
||||
Expected: all PASS.
|
||||
|
||||
- [ ] **Step 3: Flag visual verification.** The resolver dialog cannot be opened end-to-end until the integrator wires Layer A/B's `RequestConflictResolution(taskId, target)` → `IslandsShellViewModel.RequestConflictResolutionAsync`. Report this as a visual-verification gap for the user/integrator: open a real conflicting merge, confirm hunks render, Accept buttons populate the merged box, Resolve & continue closes on success, Abort restores the tree.
|
||||
|
||||
- [ ] **Step 4: Leave the branch for the orchestrator.** Do NOT push, do NOT merge to main.
|
||||
837
docs/superpowers/plans/2026-06-05-merge-cockpit-layer-b.md
Normal file
837
docs/superpowers/plans/2026-06-05-merge-cockpit-layer-b.md
Normal file
@@ -0,0 +1,837 @@
|
||||
# Layer B — Multi-Worktree Merge Cockpit Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Turn the worktrees-overview modal into a batch-merge cockpit (multi-select N worktrees → one target branch → "Merge all" with skip-and-continue conflict collection), and migrate `WorktreeModalView`'s bespoke inline diff onto the canonical `DiffLinesView`.
|
||||
|
||||
**Architecture:** The cockpit VM keeps depending on the concrete `WorkerClient` (the overview/cleanup/state methods live only on `WorkerClient`, not `IWorkerClient`). The batch loop is extracted into a delegate-driven method `MergeSelectedAsync(Func<...> mergeFn)` so it is unit-testable with a fake merge function and a never-connected `WorkerClient`. Clean merges (`Status=="merged"`) update the row; conflicts (`Status=="conflict"`, which `MergeTaskAsync` already auto-aborts) are collected into a `ConflictRows` list whose rows expose a `Resolve` button wired to an inert `RequestConflictResolution(taskId, targetBranch)` seam. The diff migration replaces the right-pane `ItemsControl` in `WorktreeModalView` with `DiffLinesView`, feeding it `DiffLineViewModel`s produced by `UnifiedDiffParser`, and deletes the now-dead `WorktreeDiffLineViewModel`/`WorktreeDiffLineKind`.
|
||||
|
||||
**Tech Stack:** .NET 8, Avalonia 12, CommunityToolkit.Mvvm source generators, xUnit. Build UI with `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`; run `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`.
|
||||
|
||||
**Frozen contracts reused (do NOT modify):**
|
||||
- `WorkerClient.MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) -> Task<MergeResultDto>`
|
||||
- `WorkerClient.GetMergeTargetsAsync(string taskId) -> Task<MergeTargetsDto?>` (`MergeTargetsDto(string DefaultBranch, IReadOnlyList<string> LocalBranches)`)
|
||||
- `MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage)` — `Status` is `"merged" | "conflict" | "blocked" | <other>`
|
||||
- `WorkerClient.GetWorktreesOverviewAsync`, `CleanupFinishedWorktreesAsync`, `SetWorktreeStateAsync`, `ForceRemoveWorktreeAsync`
|
||||
- `GitService.GetFileDiffAsync(worktreePath, baseCommit?, relativePath)` returns a `git diff` blob including the `diff --git` header (so `UnifiedDiffParser.Parse` handles it)
|
||||
- `DiffLinesView` (`Lines` styled property, `IEnumerable?`), `DiffLineViewModel`, `DiffFileViewModel`, `UnifiedDiffParser.Parse` / `.Flatten`
|
||||
|
||||
**Do NOT touch:** any worker-side files (`WorkerHub`, `TaskMergeService`, `GitService`), `IWorkerClient.cs` / `WorkerClient.cs`, `WorkConsole.axaml`, `DetailsIslandViewModel.cs`, and do not create any `ConflictResolver` UI or reference any `ConflictResolver` type.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- `src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs` — **modify.** Add `BatchMergeOutcome` enum; add `IsChecked`/`MergeOutcome` (+ derived) to the row VM; add `MergeTargets`, `SelectedTarget`, `SelectedCount`, `IsMerging`, `BatchProgress`, `ConflictRows`, the `RequestConflictResolution` seam, `MergeSelectedAsync`, `MergeAllCommand`, `ResolveConflictCommand`, `ToggleSelectAllCommand`, target loading, and per-row check subscription. Keep all existing context-menu commands/wiring intact.
|
||||
- `src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml` — **modify.** Add a per-row checkbox + outcome badge, a target `ComboBox` + "Merge all" button + progress text in the toolbar, and a "Needs resolution" panel listing `ConflictRows` with `Resolve` buttons.
|
||||
- `src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs` — **modify.** Replace `SelectedFileDiffLines` element type with `DiffLineViewModel` produced via `UnifiedDiffParser`; delete `WorktreeDiffLineKind` and `WorktreeDiffLineViewModel`.
|
||||
- `src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml` — **modify.** Replace the right-pane `ItemsControl` with `ctl:DiffLinesView`; drop the `DiffLineKindToBrushConverter` resource.
|
||||
- `src/ClaudeDo.Localization/locales/en.json` + `de.json` — **modify.** Add new `modals.worktreesOverview.*` and `vm.worktreesOverview.*` keys (keep parity).
|
||||
- `tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs` — **create.** Unit tests for `MergeSelectedAsync` skip-and-continue, conflict collection, progress, selection gating, and the resolve seam.
|
||||
|
||||
No `IWorkerClient` change → no test-fake updates needed.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Row-level batch state (outcome enum + row VM fields)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Ui.ViewModels.Modals;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
using Xunit;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class WorktreesOverviewBatchMergeTests
|
||||
{
|
||||
private static WorktreeOverviewRowViewModel ActiveRow(string id) => new()
|
||||
{
|
||||
TaskId = id,
|
||||
TaskTitle = $"Task {id}",
|
||||
TaskStatus = TaskStatus.WaitingForReview,
|
||||
State = WorktreeState.Active,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Row_outcome_helpers_reflect_state()
|
||||
{
|
||||
var row = ActiveRow("a");
|
||||
Assert.Equal(BatchMergeOutcome.None, row.MergeOutcome);
|
||||
Assert.False(row.IsConflict);
|
||||
|
||||
row.MergeOutcome = BatchMergeOutcome.Conflict;
|
||||
Assert.True(row.IsConflict);
|
||||
|
||||
row.MergeOutcome = BatchMergeOutcome.Merged;
|
||||
Assert.False(row.IsConflict);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter WorktreesOverviewBatchMergeTests`
|
||||
Expected: FAIL — `BatchMergeOutcome` and `MergeOutcome`/`IsConflict` do not exist (compile error).
|
||||
|
||||
- [ ] **Step 3: Add the enum and row fields**
|
||||
|
||||
In `WorktreesOverviewModalViewModel.cs`, add the enum just above `WorktreeOverviewRowViewModel`:
|
||||
|
||||
```csharp
|
||||
public enum BatchMergeOutcome { None, Merging, Merged, Conflict, Blocked, Failed }
|
||||
```
|
||||
|
||||
Inside `WorktreeOverviewRowViewModel`, add after the existing `_isSelected` field:
|
||||
|
||||
```csharp
|
||||
[ObservableProperty] private bool _isChecked;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsConflict))]
|
||||
[NotifyPropertyChangedFor(nameof(HasOutcome))]
|
||||
private BatchMergeOutcome _mergeOutcome;
|
||||
|
||||
public bool IsConflict => MergeOutcome == BatchMergeOutcome.Conflict;
|
||||
public bool HasOutcome => MergeOutcome != BatchMergeOutcome.None;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter WorktreesOverviewBatchMergeTests`
|
||||
Expected: PASS (1 test).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs
|
||||
git commit -m "feat(ui): add batch-merge row state to worktrees cockpit VM"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Batch orchestration (`MergeSelectedAsync` skip-and-continue)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Append to `WorktreesOverviewBatchMergeTests.cs`. The helper builds a VM with a never-connected `WorkerClient` (the loop never touches it) and seeds `Rows` directly:
|
||||
|
||||
```csharp
|
||||
private static WorktreesOverviewModalViewModel NewVm() =>
|
||||
new(new ClaudeDo.Ui.Services.WorkerClient("http://127.0.0.1:1/hub"), () => null!);
|
||||
|
||||
private static MergeResultDto Merged() => new("merged", System.Array.Empty<string>(), null);
|
||||
private static MergeResultDto Conflict() => new("conflict", new[] { "f.cs" }, null);
|
||||
private static MergeResultDto Blocked() => new("blocked", System.Array.Empty<string>(), "blocked");
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task MergeSelected_only_processes_checked_active_rows()
|
||||
{
|
||||
var vm = NewVm();
|
||||
var a = ActiveRow("a"); a.IsChecked = true;
|
||||
var b = ActiveRow("b"); b.IsChecked = false; // unchecked -> skipped
|
||||
var c = ActiveRow("c"); c.IsChecked = true; c.State = WorktreeState.Merged; // not active -> skipped
|
||||
vm.Rows.Add(a); vm.Rows.Add(b); vm.Rows.Add(c);
|
||||
vm.SelectedTarget = "main";
|
||||
|
||||
var seen = new System.Collections.Generic.List<string>();
|
||||
await vm.MergeSelectedAsync((id, target, remove, msg) =>
|
||||
{
|
||||
seen.Add(id);
|
||||
Assert.Equal("main", target);
|
||||
Assert.False(remove); // removeWorktree must be false
|
||||
return System.Threading.Tasks.Task.FromResult(Merged());
|
||||
});
|
||||
|
||||
Assert.Equal(new[] { "a" }, seen);
|
||||
Assert.Equal(BatchMergeOutcome.Merged, a.MergeOutcome);
|
||||
Assert.False(a.IsChecked); // cleared after merge
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task MergeSelected_continues_past_conflict_and_collects_it()
|
||||
{
|
||||
var vm = NewVm();
|
||||
var a = ActiveRow("a"); a.IsChecked = true;
|
||||
var b = ActiveRow("b"); b.IsChecked = true;
|
||||
var c = ActiveRow("c"); c.IsChecked = true;
|
||||
vm.Rows.Add(a); vm.Rows.Add(b); vm.Rows.Add(c);
|
||||
vm.SelectedTarget = "main";
|
||||
|
||||
await vm.MergeSelectedAsync((id, target, remove, msg) =>
|
||||
System.Threading.Tasks.Task.FromResult(id == "b" ? Conflict() : Merged()));
|
||||
|
||||
Assert.Equal(BatchMergeOutcome.Merged, a.MergeOutcome);
|
||||
Assert.Equal(BatchMergeOutcome.Conflict, b.MergeOutcome);
|
||||
Assert.Equal(BatchMergeOutcome.Merged, c.MergeOutcome); // continued past the conflict
|
||||
Assert.Contains(b, vm.ConflictRows);
|
||||
Assert.Single(vm.ConflictRows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task MergeSelected_maps_blocked_and_exception_to_failure_outcomes()
|
||||
{
|
||||
var vm = NewVm();
|
||||
var a = ActiveRow("a"); a.IsChecked = true;
|
||||
var b = ActiveRow("b"); b.IsChecked = true;
|
||||
vm.Rows.Add(a); vm.Rows.Add(b);
|
||||
vm.SelectedTarget = "main";
|
||||
|
||||
await vm.MergeSelectedAsync((id, target, remove, msg) => id == "a"
|
||||
? System.Threading.Tasks.Task.FromResult(Blocked())
|
||||
: throw new System.InvalidOperationException("boom"));
|
||||
|
||||
Assert.Equal(BatchMergeOutcome.Blocked, a.MergeOutcome);
|
||||
Assert.Equal(BatchMergeOutcome.Failed, b.MergeOutcome);
|
||||
Assert.Empty(vm.ConflictRows);
|
||||
Assert.False(vm.IsMerging);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task MergeSelected_noop_when_no_target()
|
||||
{
|
||||
var vm = NewVm();
|
||||
var a = ActiveRow("a"); a.IsChecked = true;
|
||||
vm.Rows.Add(a);
|
||||
vm.SelectedTarget = null;
|
||||
|
||||
var called = false;
|
||||
await vm.MergeSelectedAsync((id, t, r, m) => { called = true; return System.Threading.Tasks.Task.FromResult(Merged()); });
|
||||
|
||||
Assert.False(called);
|
||||
Assert.Equal(BatchMergeOutcome.None, a.MergeOutcome);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter WorktreesOverviewBatchMergeTests`
|
||||
Expected: FAIL — `MergeSelectedAsync`, `ConflictRows`, `IsMerging`, `SelectedTarget` do not exist (compile error).
|
||||
|
||||
- [ ] **Step 3: Implement the orchestration + cockpit fields**
|
||||
|
||||
In `WorktreesOverviewModalViewModel.cs`, add these `using`s if missing: `using ClaudeDo.Ui.Services;` (already present). Add fields/properties to `WorktreesOverviewModalViewModel` (after the existing `_selectedRow` field):
|
||||
|
||||
```csharp
|
||||
[ObservableProperty][NotifyCanExecuteChangedFor(nameof(MergeAllCommand))] private string? _selectedTarget;
|
||||
[ObservableProperty][NotifyCanExecuteChangedFor(nameof(MergeAllCommand))] private int _selectedCount;
|
||||
[ObservableProperty][NotifyCanExecuteChangedFor(nameof(MergeAllCommand))] private bool _isMerging;
|
||||
[ObservableProperty] private string? _batchProgress;
|
||||
|
||||
public ObservableCollection<string> MergeTargets { get; } = new();
|
||||
public ObservableCollection<WorktreeOverviewRowViewModel> ConflictRows { get; } = new();
|
||||
|
||||
/// Inert seam wired by the integrator to Layer C's resolver at merge time. (taskId, targetBranch)
|
||||
public Func<string, string, Task>? RequestConflictResolution { get; set; }
|
||||
```
|
||||
|
||||
Add a helper to enumerate rows regardless of grouped/flat mode, plus the orchestration method:
|
||||
|
||||
```csharp
|
||||
public IEnumerable<WorktreeOverviewRowViewModel> AllRows =>
|
||||
IsGlobal ? Groups.SelectMany(g => g.Rows) : Rows;
|
||||
|
||||
public async Task MergeSelectedAsync(
|
||||
Func<string, string, bool, string, Task<MergeResultDto>> mergeFn,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var target = SelectedTarget;
|
||||
if (string.IsNullOrWhiteSpace(target)) return;
|
||||
|
||||
var selected = AllRows.Where(r => r.IsChecked && r.IsActive).ToList();
|
||||
if (selected.Count == 0) return;
|
||||
|
||||
IsMerging = true;
|
||||
ConflictRows.Clear();
|
||||
var done = 0;
|
||||
try
|
||||
{
|
||||
foreach (var row in selected)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
row.MergeOutcome = BatchMergeOutcome.Merging;
|
||||
BatchProgress = Loc.T("vm.worktreesOverview.batchProgress", ++done, selected.Count);
|
||||
|
||||
MergeResultDto result;
|
||||
try
|
||||
{
|
||||
result = await mergeFn(row.TaskId, target!, false,
|
||||
Loc.T("vm.merge.commitMessage", row.TaskTitle));
|
||||
}
|
||||
catch
|
||||
{
|
||||
row.MergeOutcome = BatchMergeOutcome.Failed;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (result.Status)
|
||||
{
|
||||
case "merged":
|
||||
row.MergeOutcome = BatchMergeOutcome.Merged;
|
||||
row.State = WorktreeState.Merged;
|
||||
row.IsChecked = false;
|
||||
break;
|
||||
case "conflict":
|
||||
row.MergeOutcome = BatchMergeOutcome.Conflict;
|
||||
ConflictRows.Add(row);
|
||||
break;
|
||||
case "blocked":
|
||||
row.MergeOutcome = BatchMergeOutcome.Blocked;
|
||||
break;
|
||||
default:
|
||||
row.MergeOutcome = BatchMergeOutcome.Failed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
BatchProgress = Loc.T("vm.worktreesOverview.batchDone",
|
||||
selected.Count(r => r.MergeOutcome == BatchMergeOutcome.Merged), ConflictRows.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsMerging = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Note: `Loc.T` keys are added in Task 5; they resolve to the key name (harmless) until then, so tests pass now.
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter WorktreesOverviewBatchMergeTests`
|
||||
Expected: PASS (5 tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs
|
||||
git commit -m "feat(ui): add skip-and-continue batch merge orchestration"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Selection tracking, target loading, commands + resolve seam
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Append to `WorktreesOverviewBatchMergeTests.cs`:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void SelectedCount_tracks_checked_active_rows()
|
||||
{
|
||||
var vm = NewVm();
|
||||
var a = ActiveRow("a");
|
||||
var b = ActiveRow("b");
|
||||
var merged = ActiveRow("c"); merged.State = WorktreeState.Merged;
|
||||
vm.AddRowForTest(a); vm.AddRowForTest(b); vm.AddRowForTest(merged);
|
||||
|
||||
Assert.Equal(0, vm.SelectedCount);
|
||||
a.IsChecked = true;
|
||||
Assert.Equal(1, vm.SelectedCount);
|
||||
b.IsChecked = true;
|
||||
merged.IsChecked = true; // not active -> not counted
|
||||
Assert.Equal(2, vm.SelectedCount);
|
||||
a.IsChecked = false;
|
||||
Assert.Equal(1, vm.SelectedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveConflict_invokes_seam_with_task_and_target()
|
||||
{
|
||||
var vm = NewVm();
|
||||
vm.SelectedTarget = "release";
|
||||
var row = ActiveRow("x"); row.MergeOutcome = BatchMergeOutcome.Conflict;
|
||||
|
||||
(string Task, string Target)? captured = null;
|
||||
vm.RequestConflictResolution = (taskId, target) => { captured = (taskId, target); return System.Threading.Tasks.Task.CompletedTask; };
|
||||
|
||||
vm.ResolveConflictCommand.Execute(row);
|
||||
|
||||
Assert.Equal(("x", "release"), captured);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeAll_canExecute_requires_target_selection_and_idle()
|
||||
{
|
||||
var vm = NewVm();
|
||||
var a = ActiveRow("a");
|
||||
vm.AddRowForTest(a);
|
||||
|
||||
Assert.False(vm.MergeAllCommand.CanExecute(null)); // no selection, no target
|
||||
a.IsChecked = true;
|
||||
Assert.False(vm.MergeAllCommand.CanExecute(null)); // still no target
|
||||
vm.SelectedTarget = "main";
|
||||
Assert.True(vm.MergeAllCommand.CanExecute(null));
|
||||
vm.IsMerging = true;
|
||||
Assert.False(vm.MergeAllCommand.CanExecute(null)); // busy
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter WorktreesOverviewBatchMergeTests`
|
||||
Expected: FAIL — `AddRowForTest`, `ResolveConflictCommand`, `MergeAllCommand` do not exist (compile error).
|
||||
|
||||
- [ ] **Step 3: Implement subscription, commands, target loading**
|
||||
|
||||
In `WorktreesOverviewModalViewModel.cs`:
|
||||
|
||||
(a) Add a row-hook that recomputes `SelectedCount` when a row's `IsChecked` changes, and a test seam to add a hooked row. Add these methods to the class:
|
||||
|
||||
```csharp
|
||||
private void HookRow(WorktreeOverviewRowViewModel row)
|
||||
{
|
||||
row.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName is nameof(WorktreeOverviewRowViewModel.IsChecked)
|
||||
or nameof(WorktreeOverviewRowViewModel.State))
|
||||
RecomputeSelected();
|
||||
};
|
||||
}
|
||||
|
||||
private void RecomputeSelected() =>
|
||||
SelectedCount = AllRows.Count(r => r.IsChecked && r.IsActive);
|
||||
|
||||
// Test seam: adds a row to the flat list with selection tracking wired up.
|
||||
internal void AddRowForTest(WorktreeOverviewRowViewModel row)
|
||||
{
|
||||
HookRow(row);
|
||||
Rows.Add(row);
|
||||
}
|
||||
```
|
||||
|
||||
(b) In `LoadAsync`, call `HookRow(row)` everywhere a row is added. Replace the two add sites:
|
||||
|
||||
In the grouped branch, change `foreach (var row in grp) group.Rows.Add(row);` to:
|
||||
|
||||
```csharp
|
||||
foreach (var row in grp) { HookRow(row); group.Rows.Add(row); }
|
||||
```
|
||||
|
||||
In the flat branch, change `foreach (var row in ordered) Rows.Add(row);` to:
|
||||
|
||||
```csharp
|
||||
foreach (var row in ordered) { HookRow(row); Rows.Add(row); }
|
||||
```
|
||||
|
||||
Also, at the start of `LoadAsync` after `IsBusy = true;`, reset batch UI state and (re)load merge targets at the end of the `try`:
|
||||
|
||||
After `Rows.Clear(); Groups.Clear();` add:
|
||||
|
||||
```csharp
|
||||
ConflictRows.Clear();
|
||||
SelectedCount = 0;
|
||||
BatchProgress = null;
|
||||
```
|
||||
|
||||
At the very end of the `try` block (after the if/else that fills rows/groups) add:
|
||||
|
||||
```csharp
|
||||
await LoadMergeTargetsAsync();
|
||||
```
|
||||
|
||||
(c) Add target loading. The branch list is repo-level, so query it from the first active row:
|
||||
|
||||
```csharp
|
||||
private async Task LoadMergeTargetsAsync()
|
||||
{
|
||||
var anchor = AllRows.FirstOrDefault(r => r.IsActive);
|
||||
if (anchor is null) { MergeTargets.Clear(); SelectedTarget = null; return; }
|
||||
try
|
||||
{
|
||||
var targets = await _worker.GetMergeTargetsAsync(anchor.TaskId);
|
||||
MergeTargets.Clear();
|
||||
if (targets is null) { SelectedTarget = null; return; }
|
||||
foreach (var b in targets.LocalBranches) MergeTargets.Add(b);
|
||||
SelectedTarget = MergeTargets.Contains(targets.DefaultBranch)
|
||||
? targets.DefaultBranch
|
||||
: MergeTargets.FirstOrDefault();
|
||||
}
|
||||
catch { MergeTargets.Clear(); SelectedTarget = null; }
|
||||
}
|
||||
```
|
||||
|
||||
(d) Add the commands:
|
||||
|
||||
```csharp
|
||||
private bool CanMergeAll() => !IsMerging && SelectedCount > 0 && !string.IsNullOrWhiteSpace(SelectedTarget);
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanMergeAll))]
|
||||
private Task MergeAll() => MergeSelectedAsync(_worker.MergeTaskAsync);
|
||||
|
||||
[RelayCommand]
|
||||
private void ResolveConflict(WorktreeOverviewRowViewModel? row)
|
||||
{
|
||||
if (row is null) return;
|
||||
RequestConflictResolution?.Invoke(row.TaskId, SelectedTarget ?? "");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ToggleSelectAll()
|
||||
{
|
||||
var actives = AllRows.Where(r => r.IsActive).ToList();
|
||||
var allChecked = actives.Count > 0 && actives.All(r => r.IsChecked);
|
||||
foreach (var r in actives) r.IsChecked = !allChecked;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter WorktreesOverviewBatchMergeTests`
|
||||
Expected: PASS (8 tests total in this file).
|
||||
|
||||
- [ ] **Step 5: Build the app project to confirm the VM compiles against generated commands**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs
|
||||
git commit -m "feat(ui): wire batch selection, target loading and resolve seam"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Cockpit view — checkboxes, target picker, Merge all, conflicts panel
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml`
|
||||
|
||||
This task is AXAML only (no logic) → no new unit test; flag for visual verification.
|
||||
|
||||
- [ ] **Step 1: Add the batch toolbar controls**
|
||||
|
||||
In `WorktreesOverviewModalView.axaml`, replace the toolbar `StackPanel` (currently containing Refresh, Cleanup finished, StatusMessage) with one that adds select-all, the target picker, the Merge-all button and progress text. Replace the inner `<StackPanel Orientation="Horizontal" Spacing="8">...</StackPanel>` of the toolbar `Border` with:
|
||||
|
||||
```xml
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="btn" Content="{loc:Tr modals.worktreesOverview.refresh}" Command="{Binding RefreshCommand}" IsEnabled="{Binding !IsBusy}"/>
|
||||
<Button Classes="btn" Content="{loc:Tr modals.worktreesOverview.cleanupFinished}" Command="{Binding CleanupFinishedCommand}" IsEnabled="{Binding !IsBusy}"/>
|
||||
<Button Classes="btn" Content="{loc:Tr modals.worktreesOverview.selectAll}" Command="{Binding ToggleSelectAllCommand}"/>
|
||||
<Border Width="1" Background="{DynamicResource LineBrush}" Margin="4,2"/>
|
||||
<TextBlock Text="{loc:Tr modals.worktreesOverview.targetLabel}" VerticalAlignment="Center" Foreground="{DynamicResource TextDimBrush}"/>
|
||||
<ComboBox MinWidth="160"
|
||||
ItemsSource="{Binding MergeTargets}"
|
||||
SelectedItem="{Binding SelectedTarget, Mode=TwoWay}"/>
|
||||
<Button Classes="btn accent"
|
||||
Content="{loc:Tr modals.worktreesOverview.mergeAll}"
|
||||
Command="{Binding MergeAllCommand}"/>
|
||||
<TextBlock Text="{Binding SelectedCount, StringFormat='{}{0} selected'}"
|
||||
VerticalAlignment="Center" Foreground="{DynamicResource TextDimBrush}"/>
|
||||
<TextBlock Text="{Binding BatchProgress}" VerticalAlignment="Center" Margin="8,0,0,0"
|
||||
Foreground="{DynamicResource TextDimBrush}"/>
|
||||
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Margin="8,0,0,0"
|
||||
Foreground="{DynamicResource TextDimBrush}"/>
|
||||
</StackPanel>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add a checkbox + outcome badge to the row template**
|
||||
|
||||
In the `WorktreeRowTemplate` `DataTemplate`, change the row `Grid` to add a leading checkbox column and a trailing outcome column. Replace the `<Grid ColumnDefinitions="*,90,80,80">...</Grid>` (the whole grid, lines for Task/State/Diff/Age) with:
|
||||
|
||||
```xml
|
||||
<Grid ColumnDefinitions="Auto,*,90,90,80,80">
|
||||
<CheckBox Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0"
|
||||
IsChecked="{Binding IsChecked, Mode=TwoWay}"
|
||||
IsEnabled="{Binding IsActive}"
|
||||
IsVisible="{Binding IsActive}"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Vertical" Spacing="2">
|
||||
<TextBlock Classes="title" Text="{Binding TaskTitle}"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<TextBlock Classes="meta" Text="{Binding TaskStatus}"/>
|
||||
<TextBlock Classes="meta" Text="•"
|
||||
IsVisible="{Binding !PathExistsOnDisk}"/>
|
||||
<TextBlock Classes="meta" Text="{loc:Tr modals.worktreesOverview.phantom}" Foreground="{DynamicResource StatusErrorBrush}"
|
||||
IsVisible="{Binding !PathExistsOnDisk}"
|
||||
ToolTip.Tip="{loc:Tr modals.worktreesOverview.phantomTooltip}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Classes="meta" VerticalAlignment="Center"
|
||||
Text="{Binding MergeOutcome}"
|
||||
IsVisible="{Binding HasOutcome}"/>
|
||||
<Border Grid.Column="3" CornerRadius="3" Padding="6,2" VerticalAlignment="Center"
|
||||
Background="{Binding State, Converter={StaticResource WorktreeStateColor}}">
|
||||
<TextBlock Classes="meta" Text="{Binding State}" Foreground="{DynamicResource TextBrush}"
|
||||
HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Grid.Column="4" Classes="meta" Text="{Binding DiffStat}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="5" Classes="meta" Text="{Binding AgeText}" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
```
|
||||
|
||||
Then update the column-header `Grid` (the one with `ColumnDefinitions="*,90,80,80"` near the ScrollViewer top) to match the new column layout:
|
||||
|
||||
```xml
|
||||
<Grid ColumnDefinitions="Auto,*,90,90,80,80" Margin="12,0,12,4">
|
||||
<TextBlock Grid.Column="1" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnTask}"/>
|
||||
<TextBlock Grid.Column="2" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnOutcome}"/>
|
||||
<TextBlock Grid.Column="3" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnState}"/>
|
||||
<TextBlock Grid.Column="4" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnDiff}"/>
|
||||
<TextBlock Grid.Column="5" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnAge}"/>
|
||||
</Grid>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the "Needs resolution" panel**
|
||||
|
||||
Inside the content `ScrollViewer`'s root `StackPanel`, at the very top (before the column-header `Grid`), add a conflicts panel that only shows when there are conflicts:
|
||||
|
||||
```xml
|
||||
<Border IsVisible="{Binding ConflictRows.Count}"
|
||||
Background="{DynamicResource ErrorTintBrush}"
|
||||
BorderBrush="{DynamicResource StatusErrorBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="12,8" Margin="0,0,0,12">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.needsResolution}"/>
|
||||
<ItemsControl ItemsSource="{Binding ConflictRows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WorktreeOverviewRowViewModel">
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,2">
|
||||
<TextBlock Grid.Column="0" Classes="meta" VerticalAlignment="Center"
|
||||
Text="{Binding TaskTitle}"/>
|
||||
<Button Grid.Column="1" Classes="btn"
|
||||
Content="{loc:Tr modals.worktreesOverview.resolve}"
|
||||
Command="{Binding $parent[Window].((vm:WorktreesOverviewModalViewModel)DataContext).ResolveConflictCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
```
|
||||
|
||||
> `IsVisible="{Binding ConflictRows.Count}"` uses Avalonia's int→bool coercion (0 = false). If the build flags this, change to a value converter already present, but int→bool is supported.
|
||||
|
||||
- [ ] **Step 4: Build the app to verify the AXAML compiles**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded (compiled bindings resolve against the new VM members).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml
|
||||
git commit -m "feat(ui): batch-merge cockpit view with checkboxes and conflicts panel"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Localization keys (en + de parity)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Localization/locales/en.json`
|
||||
- Modify: `src/ClaudeDo.Localization/locales/de.json`
|
||||
|
||||
- [ ] **Step 1: Add the new keys to `en.json`**
|
||||
|
||||
Under `modals.worktreesOverview`, add:
|
||||
|
||||
```json
|
||||
"columnOutcome": "RESULT",
|
||||
"selectAll": "Select all",
|
||||
"targetLabel": "Target",
|
||||
"mergeAll": "Merge all",
|
||||
"needsResolution": "NEEDS RESOLUTION",
|
||||
"resolve": "Resolve"
|
||||
```
|
||||
|
||||
Under `vm.worktreesOverview`, add:
|
||||
|
||||
```json
|
||||
"batchProgress": "Merging {0}/{1}…",
|
||||
"batchDone": "Merged {0}, {1} need resolution."
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the matching keys to `de.json`**
|
||||
|
||||
Under `modals.worktreesOverview`:
|
||||
|
||||
```json
|
||||
"columnOutcome": "ERGEBNIS",
|
||||
"selectAll": "Alle auswählen",
|
||||
"targetLabel": "Ziel",
|
||||
"mergeAll": "Alle mergen",
|
||||
"needsResolution": "ZU LÖSEN",
|
||||
"resolve": "Lösen"
|
||||
```
|
||||
|
||||
Under `vm.worktreesOverview`:
|
||||
|
||||
```json
|
||||
"batchProgress": "Merge {0}/{1}…",
|
||||
"batchDone": "{0} gemergt, {1} zu lösen."
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the localization parity test**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release`
|
||||
Expected: PASS (en/de key parity holds).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Localization/locales/en.json src/ClaudeDo.Localization/locales/de.json
|
||||
git commit -m "feat(i18n): add batch-merge cockpit strings (en/de)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Migrate `WorktreeModalView` diff onto `DiffLinesView`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs`
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml`
|
||||
|
||||
- [ ] **Step 1: Switch the VM to the canonical diff model**
|
||||
|
||||
In `WorktreeModalViewModel.cs`:
|
||||
|
||||
(a) Delete the now-dead types at the top of the file:
|
||||
|
||||
```csharp
|
||||
public enum WorktreeDiffLineKind { Header, Hunk, Added, Removed, Context }
|
||||
|
||||
public sealed partial class WorktreeDiffLineViewModel : ViewModelBase
|
||||
{
|
||||
public required string Text { get; init; }
|
||||
public required WorktreeDiffLineKind Kind { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
(b) Change the collection declaration from:
|
||||
|
||||
```csharp
|
||||
public ObservableCollection<WorktreeDiffLineViewModel> SelectedFileDiffLines { get; } = new();
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```csharp
|
||||
public ObservableCollection<DiffLineViewModel> SelectedFileDiffLines { get; } = new();
|
||||
```
|
||||
|
||||
(c) Replace the body of `LoadFileDiffAsync` (the `foreach (var line in diff.Split('\n'))` block) so it parses via `UnifiedDiffParser`. The method becomes:
|
||||
|
||||
```csharp
|
||||
private async Task LoadFileDiffAsync(WorktreeNodeViewModel? node)
|
||||
{
|
||||
SelectedFileDiffLines.Clear();
|
||||
|
||||
if (node is null || node.IsDirectory || string.IsNullOrEmpty(node.RelativePath))
|
||||
return;
|
||||
|
||||
string diff;
|
||||
try
|
||||
{
|
||||
diff = await _git.GetFileDiffAsync(WorktreePath, BaseCommit, node.RelativePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var line in UnifiedDiffParser.Flatten(UnifiedDiffParser.Parse(diff)))
|
||||
SelectedFileDiffLines.Add(line);
|
||||
}
|
||||
```
|
||||
|
||||
(`DiffLineViewModel`, `DiffFileViewModel`, and `UnifiedDiffParser` are all in the same `ClaudeDo.Ui.ViewModels.Modals` namespace, so no new `using` is required.)
|
||||
|
||||
- [ ] **Step 2: Build to confirm the VM compiles and nothing else referenced the deleted types**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded. (If a compile error names `WorktreeDiffLineViewModel`/`WorktreeDiffLineKind` outside this file or the view, that reference must be migrated too — there should be none besides `WorktreeModalView.axaml`, handled next.)
|
||||
|
||||
- [ ] **Step 3: Swap the view's inline diff for `DiffLinesView`**
|
||||
|
||||
In `WorktreeModalView.axaml`:
|
||||
|
||||
(a) Remove the now-unused converter resource. Delete:
|
||||
|
||||
```xml
|
||||
<Window.Resources>
|
||||
<converters:DiffLineKindToBrushConverter x:Key="DiffLineKindToBrush"/>
|
||||
</Window.Resources>
|
||||
```
|
||||
|
||||
(b) Replace the right-pane `ScrollViewer`'s `ItemsControl` (the `SelectableTextBlock` template bound to `SelectedFileDiffLines`) with the canonical control. Replace:
|
||||
|
||||
```xml
|
||||
<ItemsControl ItemsSource="{Binding SelectedFileDiffLines}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:WorktreeDiffLineViewModel">
|
||||
<SelectableTextBlock Text="{Binding Text}"
|
||||
FontFamily="{DynamicResource MonoFont}"
|
||||
FontSize="{StaticResource FontSizeMono}"
|
||||
Foreground="{Binding Kind, Converter={StaticResource DiffLineKindToBrush}}"
|
||||
TextWrapping="NoWrap"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```xml
|
||||
<ctl:DiffLinesView Lines="{Binding SelectedFileDiffLines}"/>
|
||||
```
|
||||
|
||||
(The `xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"` namespace is already declared at the top of this file.)
|
||||
|
||||
- [ ] **Step 4: Build the app to verify the AXAML compiles**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml
|
||||
git commit -m "refactor(ui): render worktree modal diff via canonical DiffLinesView"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Full build + test sweep
|
||||
|
||||
**Files:** none (verification only).
|
||||
|
||||
- [ ] **Step 1: Build the whole app**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded, 0 errors.
|
||||
|
||||
- [ ] **Step 2: Run the UI + localization test projects**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||
Then: `dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release`
|
||||
Expected: PASS (all green, including the 8 new batch-merge tests).
|
||||
|
||||
- [ ] **Step 3: Flag visual-verification gaps**
|
||||
|
||||
The cockpit toolbar/checkbox/conflicts-panel layout and the migrated `WorktreeModalView` diff rendering are AXAML changes that cannot be verified headlessly. Report to the user that these need a visual pass (run the app, open the worktrees overview, select several worktrees, pick a target, "Merge all", and open a worktree diff).
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Notes
|
||||
|
||||
- **Spec coverage:** batch-merge cockpit (Tasks 1–4), skip-and-continue + conflict collection (Task 2), single target picker (Tasks 3–4), Resolve → `RequestConflictResolution(taskId, targetBranch)` seam left unwired (Tasks 3–4), `WorktreeModalView` diff migration to `DiffLinesView` (Task 6), no worker files touched, no `IWorkerClient` change, locales in parity (Task 5). ✔
|
||||
- **No ConflictResolver reference:** the seam is a bare `Func<string,string,Task>?`; no Layer C type is named. ✔
|
||||
- **Type consistency:** `BatchMergeOutcome`, `MergeOutcome`, `IsConflict`, `HasOutcome`, `MergeSelectedAsync`, `ConflictRows`, `SelectedTarget`, `SelectedCount`, `IsMerging`, `BatchProgress`, `RequestConflictResolution`, `MergeAllCommand`, `ResolveConflictCommand`, `ToggleSelectAllCommand`, `AddRowForTest`, `AllRows` are used consistently across tasks. ✔
|
||||
522
docs/superpowers/plans/2026-06-05-terminal-review.md
Normal file
522
docs/superpowers/plans/2026-06-05-terminal-review.md
Normal file
@@ -0,0 +1,522 @@
|
||||
# Terminal-style Review Controls Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Move review feedback into the Output (terminal) tab as a prompt-style input with `[Retry]`/`[Reset]` actions, and relocate Approve + all merge/worktree controls to a new **Git** tab.
|
||||
|
||||
**Architecture:** Pure UI-layer change in `ClaudeDo.Ui`. Add an `IsGitTab` computed flag to `DetailsIslandViewModel`, re-home existing XAML blocks across three tabs (Output · Git · Session) in `WorkConsole.axaml`, add a bottom-docked review footer to the Output tab, and intercept Enter in `WorkConsole.axaml.cs`. No worker-side or `IWorkerClient` changes; no ViewModel command renames.
|
||||
|
||||
**Tech Stack:** .NET 8, Avalonia 12 (Fluent), CommunityToolkit.Mvvm, xUnit (ClaudeDo.Ui.Tests).
|
||||
|
||||
**Reference spec:** `docs/superpowers/specs/2026-06-05-terminal-review-design.md`
|
||||
|
||||
**Build/test note (from CLAUDE.md):** A running Worker locks `Debug` output — build UI in `-c Release`:
|
||||
`dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
`dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` — add `IsGitTab`, wire notifications.
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml` — add Git tab button; split tab bodies; add Output-tab review footer; update Session empty-state text.
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml.cs` — Enter-to-Retry key handling.
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandTabsTests.cs` (create) — `IsGitTab` behavior.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add `IsGitTab` tab flag to the ViewModel
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs:139-147`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandTabsTests.cs` (create)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandTabsTests.cs`. Mirror the
|
||||
construction pattern from `DetailsIslandPrepModeTests.cs` (temp SQLite db,
|
||||
`TestDbFactory`, `StubWorkerClient`, `NullServiceProvider`, `StubNotesApi`).
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class DetailsIslandTabsTests : IDisposable
|
||||
{
|
||||
private readonly string _dbPath;
|
||||
|
||||
public DetailsIslandTabsTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_tabs_test_{Guid.NewGuid():N}.db");
|
||||
using var ctx = NewContext();
|
||||
ctx.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { File.Delete(_dbPath); } catch { }
|
||||
try { File.Delete(_dbPath + "-wal"); } catch { }
|
||||
try { File.Delete(_dbPath + "-shm"); } catch { }
|
||||
}
|
||||
|
||||
private ClaudeDoDbContext NewContext()
|
||||
{
|
||||
var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||
.UseSqlite($"Data Source={_dbPath}")
|
||||
.Options;
|
||||
return new ClaudeDoDbContext(opts);
|
||||
}
|
||||
|
||||
private sealed class TestDbFactory : IDbContextFactory<ClaudeDoDbContext>
|
||||
{
|
||||
private readonly Func<ClaudeDoDbContext> _create;
|
||||
public TestDbFactory(Func<ClaudeDoDbContext> create) => _create = create;
|
||||
public ClaudeDoDbContext CreateDbContext() => _create();
|
||||
}
|
||||
|
||||
private sealed class StubNotesApi : ClaudeDo.Ui.Services.Interfaces.INotesApi
|
||||
{
|
||||
public Task<List<DailyNoteDto>> ListAsync(DateOnly day) => Task.FromResult(new List<DailyNoteDto>());
|
||||
public Task<DailyNoteDto?> AddAsync(DateOnly day, string text) => Task.FromResult<DailyNoteDto?>(null);
|
||||
public Task UpdateAsync(string id, string text) => Task.CompletedTask;
|
||||
public Task DeleteAsync(string id) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class NullServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
}
|
||||
|
||||
// StubWorkerClient is abstract — use a concrete no-op subclass (same pattern as DetailsIslandPrepModeTests).
|
||||
private sealed class DefaultStub : StubWorkerClient { }
|
||||
|
||||
private DetailsIslandViewModel NewVm()
|
||||
{
|
||||
var factory = new TestDbFactory(NewContext);
|
||||
return new DetailsIslandViewModel(factory, new DefaultStub(), new NullServiceProvider(), new StubNotesApi());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectTab_git_sets_IsGitTab_and_clears_others()
|
||||
{
|
||||
var vm = NewVm();
|
||||
|
||||
vm.SelectTabCommand.Execute("git");
|
||||
|
||||
Assert.True(vm.IsGitTab);
|
||||
Assert.False(vm.IsOutputTab);
|
||||
Assert.False(vm.IsSessionTab);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_tab_is_output_not_git()
|
||||
{
|
||||
var vm = NewVm();
|
||||
|
||||
Assert.True(vm.IsOutputTab);
|
||||
Assert.False(vm.IsGitTab);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter DetailsIslandTabsTests`
|
||||
Expected: FAIL — compile error, `DetailsIslandViewModel` has no `IsGitTab`.
|
||||
|
||||
- [ ] **Step 3: Add `IsGitTab` to the ViewModel**
|
||||
|
||||
In `DetailsIslandViewModel.cs`, find the `SelectedTab` property notifications and the
|
||||
tab getters (around lines 139-147). Add the `IsGitTab` notification and getter:
|
||||
|
||||
```csharp
|
||||
[NotifyPropertyChangedFor(nameof(IsOutputTab))]
|
||||
[NotifyPropertyChangedFor(nameof(IsSessionTab))]
|
||||
[NotifyPropertyChangedFor(nameof(IsGitTab))]
|
||||
```
|
||||
|
||||
```csharp
|
||||
public bool IsOutputTab => SelectedTab == "output";
|
||||
public bool IsGitTab => SelectedTab == "git";
|
||||
public bool IsSessionTab => SelectedTab == "session";
|
||||
```
|
||||
|
||||
(Leave `SelectTab` unchanged — it already accepts any string and defaults to `"output"`.)
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter DetailsIslandTabsTests`
|
||||
Expected: PASS (2 tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandTabsTests.cs
|
||||
git commit -m "feat(ui): add IsGitTab flag to work console view model"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add the Git tab button and move the merge/worktree block onto it
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml:124-135` (tab strip)
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml:164-273` (tab body)
|
||||
|
||||
- [ ] **Step 1: Add the Git tab button**
|
||||
|
||||
In the tab strip `StackPanel` (lines 124-135), insert a Git button between the Output
|
||||
and Session buttons:
|
||||
|
||||
```xml
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Classes="tab-btn"
|
||||
Classes.active="{Binding IsOutputTab}"
|
||||
Content="Output"
|
||||
Command="{Binding SelectTabCommand}"
|
||||
CommandParameter="output" />
|
||||
<Button Classes="tab-btn"
|
||||
Classes.active="{Binding IsGitTab}"
|
||||
Content="Git"
|
||||
Command="{Binding SelectTabCommand}"
|
||||
CommandParameter="git" />
|
||||
<Button Classes="tab-btn"
|
||||
Classes.active="{Binding IsSessionTab}"
|
||||
Content="Session"
|
||||
Command="{Binding SelectTabCommand}"
|
||||
CommandParameter="session" />
|
||||
</StackPanel>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Move the "Merge & worktree" block to a new Git-tab ScrollViewer**
|
||||
|
||||
In the tab body `Grid` (starts line 139), the body currently holds the Output
|
||||
`ScrollViewer` (`IsVisible="{Binding IsOutputTab}"`, lines 142-162) and the Session
|
||||
`ScrollViewer` (`IsVisible="{Binding IsSessionTab}"`, lines 165-273).
|
||||
|
||||
Cut the **entire "Merge & worktree management" `StackPanel`** — the block currently at
|
||||
lines 195-241, beginning with the comment `<!-- Merge & worktree management -->` and the
|
||||
`<StackPanel Spacing="10" IsVisible="{Binding ShowMergeSection}">` and ending at its
|
||||
matching `</StackPanel>` after the `MergeAllError` `TextBlock` (line 241).
|
||||
|
||||
Add a new Git-tab `ScrollViewer` between the Output and Session `ScrollViewer`s, and
|
||||
paste the cut block inside it:
|
||||
|
||||
```xml
|
||||
<!-- Git: merge target, approve, diff, worktree -->
|
||||
<ScrollViewer IsVisible="{Binding IsGitTab}" Padding="14,10">
|
||||
<StackPanel Spacing="14">
|
||||
|
||||
<!-- Approve (review-gated) -->
|
||||
<StackPanel Spacing="8" IsVisible="{Binding IsWaitingForReview}">
|
||||
<TextBlock Classes="section-label" Text="REVIEW" />
|
||||
<Button Classes="btn accent" Content="Approve"
|
||||
Command="{Binding ApproveReviewCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Merge & worktree management (moved from Session tab) -->
|
||||
<StackPanel Spacing="10" IsVisible="{Binding ShowMergeSection}">
|
||||
<TextBlock Classes="section-label" Text="MERGE & WORKTREE" />
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="Merge target" />
|
||||
<ComboBox ItemsSource="{Binding MergeTargetBranches}"
|
||||
SelectedItem="{Binding SelectedMergeTarget, Mode=TwoWay}"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="0">
|
||||
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource MossBrush}"
|
||||
IsVisible="{Binding MergeIsClean}" />
|
||||
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource BloodBrush}"
|
||||
IsVisible="{Binding MergeIsConflict}" />
|
||||
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
IsVisible="{Binding ShowMergePreviewMuted}" />
|
||||
</StackPanel>
|
||||
<WrapPanel Orientation="Horizontal">
|
||||
<Button Classes="btn" Content="Open Diff" Margin="0,0,8,8"
|
||||
Command="{Binding OpenDiffCommand}" />
|
||||
<Button Classes="btn accent" Content="Merge" Margin="0,0,8,8"
|
||||
Command="{Binding MergeCommand}"
|
||||
IsVisible="{Binding ShowSingleMerge}" />
|
||||
<Button Classes="btn" Margin="0,0,8,8"
|
||||
Command="{Binding OpenWorktreeCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<TextBlock Text="Worktree" />
|
||||
<PathIcon Data="{StaticResource Icon.ArrowOut}" Width="11" Height="11" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Classes="btn" Content="Review Combined Diff" Margin="0,0,8,8"
|
||||
Command="{Binding ReviewCombinedDiffCommand}" />
|
||||
<Button Classes="btn accent" Content="Merge All Subtasks" Margin="0,0,0,8"
|
||||
Command="{Binding MergeAllCommand}"
|
||||
IsEnabled="{Binding CanMergeAll}"
|
||||
ToolTip.Tip="{Binding MergeAllDisabledReason}" />
|
||||
</WrapPanel>
|
||||
<TextBlock Text="{Binding MergeAllError}"
|
||||
Foreground="{DynamicResource BloodBrush}"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{Binding MergeAllError,
|
||||
Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Remove the old review block from the Session tab**
|
||||
|
||||
In the Session `ScrollViewer` (`IsVisible="{Binding IsSessionTab}"`), delete the
|
||||
**"Review controls" `StackPanel`** currently at lines 168-193 (the
|
||||
`<!-- Review controls -->` comment, the `<StackPanel Spacing="8" IsVisible="{Binding IsWaitingForReview}">`,
|
||||
the REVIEW label, Feedback label, the `ReviewFeedback` TextBox, and the four buttons).
|
||||
After this and Step 2, the Session tab's `StackPanel` should contain only the Child
|
||||
outcomes block (lines 244-263) and the empty-state `TextBlock` (lines 266-270).
|
||||
|
||||
- [ ] **Step 4: Build and verify it compiles**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded, 0 errors.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
|
||||
git commit -m "feat(ui): add Git tab and move merge/approve controls onto it"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add the prompt-style review footer to the Output tab
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml` (Output-tab area + the `Grid` body)
|
||||
|
||||
- [ ] **Step 1: Restructure the Output tab body to dock a footer below the log**
|
||||
|
||||
The body `Grid` (line 139) overlays all three tab `ScrollViewer`s. Replace the Output
|
||||
`ScrollViewer` (lines 142-162) with a `DockPanel` that keeps the log filling and docks
|
||||
the review footer at the bottom. Keep `Name="LogScroll"` on the `ScrollViewer` (the
|
||||
code-behind references it). Use this exact markup:
|
||||
|
||||
```xml
|
||||
<!-- Output: log + review footer, both gated on IsOutputTab -->
|
||||
<DockPanel IsVisible="{Binding IsOutputTab}" LastChildFill="True">
|
||||
|
||||
<!-- Review footer (terminal prompt) — only while awaiting review -->
|
||||
<Border DockPanel.Dock="Bottom"
|
||||
IsVisible="{Binding IsWaitingForReview}"
|
||||
Background="{DynamicResource Surface2Brush}"
|
||||
BorderBrush="{DynamicResource LineBrush}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="10,6">
|
||||
<DockPanel LastChildFill="True">
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Spacing="8"
|
||||
VerticalAlignment="Bottom" Margin="8,0,0,0">
|
||||
<Button Classes="btn accent" Content="Retry"
|
||||
Command="{Binding RejectReviewCommand}" />
|
||||
<Button Classes="btn" Content="Reset"
|
||||
Command="{Binding ParkReviewCommand}" />
|
||||
</StackPanel>
|
||||
<TextBlock DockPanel.Dock="Left" Text="❯"
|
||||
FontFamily="{StaticResource MonoFont}"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
VerticalAlignment="Top" Margin="0,4,8,0" />
|
||||
<TextBox Name="ReviewInput"
|
||||
Text="{Binding ReviewFeedback, Mode=TwoWay}"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
MaxHeight="160"
|
||||
PlaceholderText="Feedback for the next run…"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0,2"
|
||||
FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="{StaticResource FontSizeMono}" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Name="LogScroll"
|
||||
VerticalScrollBarVisibility="Visible"
|
||||
AllowAutoHide="False"
|
||||
Padding="12,8,12,4">
|
||||
<ItemsControl ItemsSource="{Binding Log}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:LogLineViewModel">
|
||||
<Grid ColumnDefinitions="60,*" Margin="0,1">
|
||||
<TextBlock Grid.Column="0"
|
||||
Classes="log-ts"
|
||||
Text="{Binding TimestampFormatted}" />
|
||||
<SelectableTextBlock Grid.Column="1"
|
||||
Text="{Binding Text}" Tag="{Binding ClassName}"
|
||||
Foreground="{DynamicResource TextDimBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
</DockPanel>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build and verify it compiles**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded, 0 errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
|
||||
git commit -m "feat(ui): add terminal review footer with Retry/Reset to Output tab"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Enter-to-Retry key handling
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml.cs`
|
||||
|
||||
- [ ] **Step 1: Add the KeyDown handler**
|
||||
|
||||
In `WorkConsole.axaml.cs`, add `using Avalonia.Input;` at the top. Add a handler that
|
||||
runs `RejectReviewCommand` on Enter (without Shift) and lets Shift+Enter insert a
|
||||
newline. Wire it from the `ReviewInput` TextBox. Full file:
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
|
||||
namespace ClaudeDo.Ui.Views.Islands.Detail;
|
||||
|
||||
public partial class WorkConsole : UserControl
|
||||
{
|
||||
private INotifyCollectionChanged? _log;
|
||||
|
||||
public WorkConsole()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (_log is not null)
|
||||
_log.CollectionChanged -= OnLogChanged;
|
||||
|
||||
_log = (DataContext as DetailsIslandViewModel)?.Log;
|
||||
|
||||
if (_log is not null)
|
||||
_log.CollectionChanged += OnLogChanged;
|
||||
}
|
||||
|
||||
private void OnLogChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.Action != NotifyCollectionChangedAction.Add) return;
|
||||
EventHandler? handler = null;
|
||||
handler = (_, _) =>
|
||||
{
|
||||
LogScroll.LayoutUpdated -= handler;
|
||||
LogScroll.ScrollToEnd();
|
||||
};
|
||||
LogScroll.LayoutUpdated += handler;
|
||||
}
|
||||
|
||||
private void OnReviewInputKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key != Key.Enter || e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
||||
return;
|
||||
|
||||
if (DataContext is DetailsIslandViewModel vm &&
|
||||
vm.RejectReviewCommand.CanExecute(null))
|
||||
{
|
||||
vm.RejectReviewCommand.Execute(null);
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Wire the handler in XAML**
|
||||
|
||||
On the `ReviewInput` TextBox added in Task 3, add the event hookup attribute:
|
||||
|
||||
```xml
|
||||
<TextBox Name="ReviewInput"
|
||||
KeyDown="OnReviewInputKeyDown"
|
||||
Text="{Binding ReviewFeedback, Mode=TwoWay}"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build and verify it compiles**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded, 0 errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml.cs
|
||||
git commit -m "feat(ui): send Retry on Enter in the review prompt"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Update the Session empty-state copy
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml` (empty-state `TextBlock`, was line 266-270)
|
||||
|
||||
- [ ] **Step 1: Reword the empty-state text**
|
||||
|
||||
The Session empty-state still says review/merge controls appear there. Replace its
|
||||
`Text` so it reflects that those moved:
|
||||
|
||||
```xml
|
||||
<TextBlock IsVisible="{Binding ShowSessionEmpty}"
|
||||
Classes="meta"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Text="Nothing to manage yet — subtask outcomes appear here once the run finishes. Review in the Output tab, merge in the Git tab." />
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build and verify it compiles**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: Build succeeded, 0 errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
|
||||
git commit -m "docs(ui): reword Session empty-state for relocated review/merge controls"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Final verification
|
||||
|
||||
- [ ] **Step 1: Run the full UI test project**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||
Expected: all tests PASS.
|
||||
|
||||
- [ ] **Step 2: Manual visual verification (cannot be auto-verified — flag to user)**
|
||||
|
||||
Launch the app with a task in `WaitingForReview` and confirm:
|
||||
- Output tab shows the prompt footer (`❯` + input + `[Retry]` `[Reset]`) only while awaiting review; it is hidden otherwise.
|
||||
- Typing + **Enter** sends Retry (requeues with feedback); **Shift+Enter** inserts a newline; **Enter on empty input** does nothing.
|
||||
- `[Reset]` parks the task to Idle.
|
||||
- Git tab shows **Approve** + merge target + Open Diff / Merge / Worktree / Review Combined Diff / Merge All Subtasks.
|
||||
- Session tab shows only subtask outcomes / the reworded empty state.
|
||||
- Tab switching highlights the active tab correctly (Output ↔ Git ↔ Session).
|
||||
@@ -0,0 +1,90 @@
|
||||
# Plan — Unify the parent-task model
|
||||
|
||||
Spec: `docs/superpowers/specs/2026-06-09-unify-parent-task-model-design.md`
|
||||
|
||||
Subagents: `sonnet`. Stage files explicitly by path (never `git add -A`). TDD.
|
||||
Build with `-c Release` per project. Commit per task (Conventional Commits).
|
||||
|
||||
## Task 1 — Single parent-advance path
|
||||
|
||||
- Rename `TaskStateService.TryAdvanceImprovementParentAsync` → `TryAdvanceParentAsync`.
|
||||
- Make it advance **any** `WaitingForChildren` parent → `WaitingForReview` when all
|
||||
children are terminal, and advance a parent with **zero** children straight to
|
||||
`WaitingForReview`.
|
||||
- In `OnChildTerminalAsync`: drop the `TryCompleteParentAsync` call; keep
|
||||
`_chain.OnChildFinishedAsync`; call the renamed advance method for all parents.
|
||||
- Tests: extend `WaitingForChildrenLifecycleTests` — (a) improvement parent still
|
||||
advances; (b) a `WaitingForChildren` parent whose children are a *sequential chain*
|
||||
advances only after the last one is terminal; (c) zero-children parent advances.
|
||||
|
||||
## Task 2 — Delete `TryCompleteParentAsync`
|
||||
|
||||
- Remove `TaskRepository.TryCompleteParentAsync` (`TaskRepository.cs:477-502`) and
|
||||
any remaining references.
|
||||
- Update `src/ClaudeDo.Data/CLAUDE.md` (drop it from the TaskRepository helper list).
|
||||
- Build Data + Worker; fix references.
|
||||
|
||||
## Task 3 — Planning finalize enters `WaitingForChildren`
|
||||
|
||||
- `TaskStateService.FinalizePlanningAsync`: in the same `ExecuteUpdateAsync`, set
|
||||
`Status = WaitingForChildren` alongside `PlanningPhase = Finalized` /
|
||||
`PlanningFinalizedAt`.
|
||||
- Verify `PlanningSessionManager.FinalizeAsync` ordering: finalize (→ WaitingForChildren)
|
||||
**before** `SetupChainAsync` enqueues child[0]. Adjust only if ordering is wrong.
|
||||
- Tests: finalizing a planning parent with N children leaves it `WaitingForChildren`;
|
||||
after the chain completes it is `WaitingForReview` (not `Done`); a planning parent
|
||||
with zero finalized children lands in `WaitingForReview`.
|
||||
|
||||
## Task 4 — Approve merges the whole unit
|
||||
|
||||
**Decision: full UX consolidation.** Approve becomes the single entry for reviewing
|
||||
*and* merging any task; the separate planning-merge views are folded into the review
|
||||
panel. The `PlanningMergeOrchestrator` (which already merges the unit + sets the
|
||||
parent `Done` for both planning and improvement, with conflict continue/abort) is
|
||||
reused as the engine; only its *entry/UI* moves.
|
||||
|
||||
Backend:
|
||||
- `WorkerHub.ApproveReview`: for a parent that **has children**, drive
|
||||
`PlanningMergeOrchestrator.StartAsync` (event-based: `PlanningMergeStarted` /
|
||||
`PlanningSubtaskMerged` / `PlanningMergeConflict` / `PlanningMergeAborted` /
|
||||
`PlanningCompleted`) instead of the one-shot `ApproveAndMergeAsync`. Childless tasks
|
||||
keep `ApproveAndMergeAsync`. Conflict resolution still goes through
|
||||
`ContinuePlanningMerge` / `AbortPlanningMerge`.
|
||||
- Keep the orchestrator, `ContinuePlanningMerge`, `AbortPlanningMerge`,
|
||||
`GetPlanningAggregate`, `BuildPlanningIntegrationBranch`. Remove the now-redundant
|
||||
standalone `MergeAllPlanning` hub method (approve is the entry).
|
||||
- (Optional cleanup) route the orchestrator's `FinalizeParentDoneAsync` through
|
||||
`TaskStateService` so `Status` writes stay centralized; low priority.
|
||||
|
||||
UI (Avalonia, MVVM — visual-verification gaps, flag for user):
|
||||
- The review panel (`DetailsIslandViewModel` / its view) is the single approve+merge
|
||||
surface. For a child-bearing parent in `WaitingForReview`, approve shows the
|
||||
unit-merge progress + per-subtask state, the aggregate/integration diff preview, and
|
||||
conflict continue/abort — all inline in the review panel.
|
||||
- Remove the separate planning-merge view(s)/commands and the standalone "Merge all"
|
||||
button; re-wire their `PlanningMerge*` event handlers into the review panel VM.
|
||||
- Sync `IWorkerClient` + hand-rolled test fakes in both UI/Worker test projects.
|
||||
|
||||
Tests: approving a parent with two `Done` children merges both then sets `Done`; a
|
||||
conflicting second child surfaces the conflict and pauses (continue/abort) without
|
||||
losing the parent's `WaitingForReview`/merge state.
|
||||
|
||||
## Task 5 — Cancellable `WaitingForChildren` parent
|
||||
|
||||
- Add `TaskStatus.WaitingForChildren` to the `CancelAsync` guard.
|
||||
- Test: a parent in `WaitingForChildren` can be cancelled.
|
||||
|
||||
## Task 6 — Docs
|
||||
|
||||
- `src/ClaudeDo.Worker/CLAUDE.md`: add `WaitingForChildren` to the Status table +
|
||||
transition diagram; document the unified parent flow and approve-merges-unit;
|
||||
remove `MergeAllPlanning` from the Hub method list.
|
||||
- `src/ClaudeDo.Data/CLAUDE.md`: add `WaitingForChildren` to the TaskEntity status list.
|
||||
- Root `CLAUDE.md`: update the "Task status flow" convention line.
|
||||
|
||||
## Verify
|
||||
|
||||
- `dotnet test` for Worker.Tests + Data.Tests (`-c Release`).
|
||||
- UI flows (planning finalize → review → approve-merge; improvement parent;
|
||||
retired MergeAllPlanning button) are **visual-verification gaps** — flag for the
|
||||
user to run the app; do not claim they work from tests alone.
|
||||
@@ -0,0 +1,123 @@
|
||||
# Waiting for Review — Task State — Design
|
||||
|
||||
**Date:** 2026-06-01
|
||||
**Status:** Approved (brainstorming)
|
||||
**Scope:** `ClaudeDo.Data` (TaskEntity, EF config + migration), `ClaudeDo.Worker` (TaskStateService, TaskRunner, QueueService, WorkerHub, ExternalMcpService), `ClaudeDo.Ui` (StatusColorConverter, TaskRowViewModel, views), CLAUDE.md docs
|
||||
|
||||
## Problem
|
||||
|
||||
A successful task run currently transitions straight to `Done` and is considered complete. There is no gate for a human (or another agent) to review the result before it is accepted. We want review to be a mandatory step: after a successful run a task waits for an explicit approval, and a reviewer can send it back with feedback for another turn.
|
||||
|
||||
## Goals
|
||||
|
||||
- Add a `WaitingForReview` lifecycle state that a task enters automatically after a **successful** run.
|
||||
- Reviewer can **approve** (→ `Done`), **reject-and-re-run** (→ `Queued`, resuming the same Claude session with required feedback), **reject-and-park** (→ `Idle`), or **cancel** (→ `Cancelled`).
|
||||
- Reject-and-re-run reuses the existing session-resume mechanism so the agent continues with full context.
|
||||
- Both the desktop UI and the external MCP surface can perform review actions.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No change to the failure path: a **failed** run still goes straight to `Failed`, never to `WaitingForReview`.
|
||||
- No change to planning-phase finalization. A planning parent that generates child tasks keeps its current behavior and does **not** route through review. Only ordinary executable runs (`Running` → success) are affected.
|
||||
- No change to worktree state flow (`Active | Merged | Discarded | Kept`).
|
||||
- No change to the in-run auto-retry-on-failure behavior; only the *final* successful completion routes to review.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. State machine
|
||||
|
||||
Changed/added transitions in **bold**:
|
||||
|
||||
| From | To | Trigger |
|
||||
|---|---|---|
|
||||
| Idle | Queued | enqueue (unchanged) |
|
||||
| Queued | Running | queue picker claim (unchanged) |
|
||||
| Running | **WaitingForReview** | **successful run (was → Done)** |
|
||||
| Running | Failed | failed run (unchanged) |
|
||||
| Running | Cancelled | cancel during run (unchanged) |
|
||||
| **WaitingForReview** | **Done** | **approve** |
|
||||
| **WaitingForReview** | **Queued** | **reject + required feedback → resume re-run** |
|
||||
| **WaitingForReview** | **Idle** | **reject → park for manual edit** |
|
||||
| **WaitingForReview** | **Cancelled** | **abandon an almost-done task** |
|
||||
| Done \| Failed \| Cancelled | Idle | reset (unchanged) |
|
||||
|
||||
### 2. Data model
|
||||
|
||||
`ClaudeDo.Data`:
|
||||
|
||||
- `TaskStatus` enum (`Models/TaskEntity.cs`): add `WaitingForReview` after `Running`.
|
||||
- EF string converter (`Configuration/TaskEntityConfiguration.cs`): map `WaitingForReview` ⇄ `"waiting_for_review"` (TEXT column, no schema constraint to change).
|
||||
- New nullable column **`ReviewFeedback : string?`** on `TaskEntity`. Holds the reviewer's rejection comment until the re-run consumes it, then it is cleared. Persisted so it survives a worker restart and is visible to the UI.
|
||||
- One EF migration: add the `review_feedback` column. No backfill — the new status value and column are only written going forward.
|
||||
|
||||
### 3. Worker — status transitions (`State/TaskStateService.cs`)
|
||||
|
||||
`TaskStateService` remains the sole owner of status writes. New/changed methods:
|
||||
|
||||
- `SubmitForReviewAsync(taskId)` — `Running` → `WaitingForReview`. Sets `FinishedAt` and `Result` exactly as `CompleteAsync` does today. Called by `TaskRunner` on success **instead of** `CompleteAsync`. (`CompleteAsync` is retained for the approve path.)
|
||||
- `ApproveReviewAsync(taskId)` — `WaitingForReview` → `Done`.
|
||||
- `RejectToQueueAsync(taskId, feedback)` — `WaitingForReview` → `Queued`. Rejects empty/whitespace feedback with a failed `TransitionResult`. Stores `feedback` in `ReviewFeedback`. Wakes the queue.
|
||||
- `RejectToIdleAsync(taskId)` — `WaitingForReview` → `Idle`. Parks for manual editing; leaves `Result` intact, clears `ReviewFeedback`.
|
||||
- `CancelAsync` — extend the allowed source states to include `WaitingForReview`.
|
||||
|
||||
Each transition broadcasts `TaskUpdated` as today. Invalid source states return a failed `TransitionResult` (no throw), matching existing convention.
|
||||
|
||||
### 4. Resume-aware re-run (`Queue/QueueService.cs`)
|
||||
|
||||
The queue picker still atomically claims a `Queued`, unblocked task (`UPDATE … SET status='running' … RETURNING *`). The `RETURNING` row already carries `ReviewFeedback`. After a successful claim, `QueueService` branches:
|
||||
|
||||
1. **`ReviewFeedback` set + latest run has a `SessionId`** → `TaskRunner.ContinueAsync(task, feedback)` — `--resume {sessionId}` with `feedback` as the next-turn prompt.
|
||||
2. **`ReviewFeedback` set, no prior `SessionId`** (edge case) → `TaskRunner.RunAsync` with the feedback appended to the task prompt, so the comment is not lost.
|
||||
3. **No `ReviewFeedback`** → normal `TaskRunner.RunAsync` (fresh session).
|
||||
|
||||
`ReviewFeedback` is cleared once consumed (single UPDATE), so a later re-run does not re-apply stale feedback.
|
||||
|
||||
### 5. External MCP surface (`External/ExternalMcpService.cs`)
|
||||
|
||||
- New tool **`review_task(taskId, decision, feedback?)`**, `decision ∈ {approve, reject_rerun, reject_park, cancel}`. `feedback` is required when `decision = reject_rerun` (validation error otherwise). Maps onto the `TaskStateService` methods in §3. This lets automation / other agents act as reviewers.
|
||||
- `get_task_status_values` — add `WaitingForReview` with a description covering the four exit actions.
|
||||
- `list_tasks` status-filter parsing and validation message — include `WaitingForReview`.
|
||||
- `get_task` lifecycle description text — update to `Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled`.
|
||||
- `update_task_status` stays restricted to `Idle` and `Queued`; all review decisions go through `review_task` (keeps the "set status freely" affordance and the review affordance distinct).
|
||||
|
||||
### 6. Worker hub (`Hub/WorkerHub.cs` + `Hub/HubBroadcaster.cs`)
|
||||
|
||||
New hub methods called by the UI, each delegating to `TaskStateService`:
|
||||
|
||||
- `ApproveReview(taskId)`
|
||||
- `RejectReviewToQueue(taskId, feedback)`
|
||||
- `RejectReviewToIdle(taskId)`
|
||||
|
||||
Cancel already exists. No new broadcast events — `TaskUpdated` covers it.
|
||||
|
||||
### 7. UI (`ClaudeDo.Ui`)
|
||||
|
||||
- `Converters/StatusColorConverter.cs`: add a `waiting_for_review` case. Snap to an existing color token from the scale; final visual pass is left to the user (per project convention — centralize/tokenize, user does the visual pass).
|
||||
- `ViewModels/Islands/TaskRowViewModel.cs`: add `IsWaitingForReview` computed property and commands **Approve**, **RejectRerun**, **RejectPark**, **Cancel** (the last reuses the existing cancel command). Commands are enabled only when `Status == WaitingForReview`.
|
||||
- Reject-Rerun opens a small flyout/dialog with a required multi-line feedback text box; on confirm it calls `RejectReviewToQueue(taskId, feedback)`.
|
||||
- Wire the commands to the new SignalR client methods.
|
||||
|
||||
### 8. Docs
|
||||
|
||||
Update the status flow in:
|
||||
|
||||
- root `CLAUDE.md` — "Task status flow" line.
|
||||
- `src/ClaudeDo.Data/CLAUDE.md` — TaskEntity status list.
|
||||
- `src/ClaudeDo.Worker/CLAUDE.md` — status-model transition table.
|
||||
|
||||
## Testing
|
||||
|
||||
`ClaudeDo.Worker.Tests` (real SQLite + real git, existing harness):
|
||||
|
||||
- `SubmitForReviewAsync`: a successful run lands in `WaitingForReview`, not `Done`.
|
||||
- `ApproveReviewAsync`: `WaitingForReview` → `Done`.
|
||||
- `RejectToQueueAsync`: empty feedback rejected; valid feedback stored in `ReviewFeedback` and status → `Queued`.
|
||||
- `RejectToIdleAsync`: → `Idle`, `Result` preserved, `ReviewFeedback` cleared.
|
||||
- `CancelAsync` from `WaitingForReview` → `Cancelled`.
|
||||
- Invalid source states (e.g. approve from `Idle`) return a failed `TransitionResult`.
|
||||
- Resume-aware re-run: a task with `ReviewFeedback` + a prior `SessionId`, when claimed, resumes the session with the feedback as the prompt and clears `ReviewFeedback`.
|
||||
- `review_task` MCP tool: each decision maps to the correct transition; `reject_rerun` without feedback errors.
|
||||
|
||||
## Open questions
|
||||
|
||||
None outstanding. Planning-task exclusion (Non-Goals) is the one assumption to verify against the planning-finalization code path during implementation; if planning finalization shares `CompleteAsync`, route only the executable-run success site through `SubmitForReviewAsync`.
|
||||
@@ -0,0 +1,116 @@
|
||||
# Prime: recurring weekday schedule
|
||||
|
||||
**Date:** 2026-06-02
|
||||
**Status:** Approved
|
||||
|
||||
## Problem
|
||||
|
||||
The Prime feature fires a single non-interactive "ping" prompt to warm up the
|
||||
Claude usage window. Today a schedule is defined by a **date range**
|
||||
(`StartDate`/`EndDate`) plus a `TimeOfDay` and a single `WorkdaysOnly` toggle.
|
||||
This is awkward for the real use case: the user wants a *recurring* morning ping
|
||||
on specific weekdays, not a bounded calendar window.
|
||||
|
||||
Desired behavior: pick the **days of the week** (e.g. Mon–Fri) and a **time**.
|
||||
The schedule recurs forever. Whenever the worker is running and it is one of the
|
||||
selected days, the ping fires at (or shortly after) the chosen time. Concretely:
|
||||
the worker autostarts on login, detects it is an eligible day around the target
|
||||
time, and fires the ping.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Catch-up window:** unchanged. Keep the existing 30-minute catch-up — if the
|
||||
worker boots within 30 min after the target time, the ping fires immediately;
|
||||
otherwise it waits for the next eligible day. (User chose "keep current 30 min".)
|
||||
- **Day picker UI:** seven compact **toggle buttons** in one row (Mo Tu We Th Fr
|
||||
Sa Su), highlighted when selected — not labeled checkboxes.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. Data model
|
||||
|
||||
`PrimeScheduleEntity` (`ClaudeDo.Data/Models`):
|
||||
|
||||
- **Remove:** `StartDate`, `EndDate`, `WorkdaysOnly`
|
||||
- **Add:** `Days` — a `[Flags] enum PrimeDays` (`Monday=1, Tuesday=2, Wednesday=4,
|
||||
Thursday=8, Friday=16, Saturday=32, Sunday=64`), stored as a single
|
||||
`days_of_week INTEGER` column.
|
||||
- **Keep:** `TimeOfDay`, `Enabled`, `LastRunAt`, `PromptOverride`, `CreatedAt`.
|
||||
|
||||
Rationale for a bitmask over a CSV string or 7 bool columns: one column, trivial
|
||||
EF mapping (int), and a clean eligibility check.
|
||||
|
||||
`PrimeScheduleEntityConfiguration`: drop the `start_date`/`end_date`/
|
||||
`workdays_only` property mappings; map `Days` to `days_of_week` (int, required,
|
||||
default 31 = Mon–Fri).
|
||||
|
||||
### 2. Scheduling logic — `NextDueCalculator`
|
||||
|
||||
- Drop all `StartDate`/`EndDate` gating (the `EndDate < today` early-out, the
|
||||
`StartDate > today` clamps, and the bounds check in `IsEligibleDay`).
|
||||
- `IsEligibleDay(s, d)` becomes: does `s.Days` contain the flag for
|
||||
`d.DayOfWeek`? (Map `System.DayOfWeek` → `PrimeDays`.)
|
||||
- The existing forward search (loops up to 8 days ahead) now simply walks to the
|
||||
next selected weekday.
|
||||
- `alreadyFiredToday` (compares `LastRunAt`'s local date to today) is unchanged.
|
||||
- The 30-min catch-up (`FireImmediately`) is unchanged.
|
||||
- A schedule with `Days == 0` (none selected) is never eligible. UI validation
|
||||
prevents saving that state.
|
||||
|
||||
### 3. UI — `SettingsModalView.axaml` + `PrimeScheduleRowViewModel`
|
||||
|
||||
Row template changes:
|
||||
- **Remove** the `ThemedDatePicker` (range) and the single "Mon–Fri" checkbox.
|
||||
- **Add** a horizontal row of 7 `ToggleButton`s (Mo Tu We Th Fr Sa Su), styled
|
||||
to highlight when checked, bound to seven bool properties on the row VM.
|
||||
- Keep the enabled checkbox, the time `TextBox`, the last-run label, and the
|
||||
remove button.
|
||||
|
||||
`PrimeScheduleRowViewModel`:
|
||||
- Replace `StartDate`/`EndDate`/`WorkdaysOnly` with seven `[ObservableProperty]`
|
||||
bools: `Monday`…`Sunday`.
|
||||
- Constructor decomposes `dto.Days` into the seven bools.
|
||||
- `ToDto()` composes the seven bools back into the `Days` int.
|
||||
|
||||
`PrimeClaudeTabViewModel`:
|
||||
- `AddSchedule` default: Mon–Fri selected, time 07:00, enabled.
|
||||
- `Validate`: replace the `StartDate > EndDate` check with "at least one day must
|
||||
be selected"; keep the time-range (00:00–23:59) check.
|
||||
|
||||
Update the explainer `TextBlock` text to describe weekday recurrence (keep the
|
||||
"fires immediately if started within 30 minutes of the target time" note).
|
||||
|
||||
### 4. Migration
|
||||
|
||||
New EF Core migration in `ClaudeDo.Data/Migrations`:
|
||||
- Add `days_of_week INTEGER NOT NULL DEFAULT 31`.
|
||||
- Backfill from existing rows: `workdays_only = 1` → `31` (Mon–Fri),
|
||||
`workdays_only = 0` → `127` (all 7 days).
|
||||
- Drop `start_date`, `end_date`, `workdays_only`.
|
||||
- Update the model snapshot.
|
||||
|
||||
### 5. DTOs
|
||||
|
||||
Both copies of `PrimeScheduleDto` (Worker `ClaudeDo.Worker.Prime` and UI
|
||||
`ClaudeDo.Ui.Services`) are passed over SignalR and must stay structurally
|
||||
compatible. In both: remove `StartDate`, `EndDate`, `WorkdaysOnly`; add a single
|
||||
`int Days` field (serializes cleanly as JSON; avoids sharing the enum across
|
||||
projects). `PrimeScheduler.ToDto` maps `entity.Days` → `(int)`.
|
||||
|
||||
`PrimeScheduleRepository`: update `UpsertAsync` (copy `Days` instead of the three
|
||||
removed fields) and `ListAsync` ordering (order by `TimeOfDay` instead of
|
||||
`StartDate`).
|
||||
|
||||
### 6. Tests
|
||||
|
||||
- `NextDueCalculatorTests` — rewrite cases around weekday sets (e.g. Mon–Fri
|
||||
skips weekend; single-day schedule; catch-up still fires; already-fired-today
|
||||
skips to next eligible day).
|
||||
- `PrimeSchedulerTests` — update fixture DTOs to the new shape.
|
||||
- `PrimeScheduleRepositoryTests` — update entity construction and assertions.
|
||||
- `PrimeClaudeTabViewModelTests` — update for the day-bool VM and new validation.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Per-schedule catch-up tuning (rejected; fixed 30 min).
|
||||
- Multiple times per day, timezones, or holiday calendars.
|
||||
182
docs/superpowers/specs/2026-06-03-daily-prep-design.md
Normal file
182
docs/superpowers/specs/2026-06-03-daily-prep-design.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Daily Prep ("Prime Claude") — Design
|
||||
|
||||
Date: 2026-06-03
|
||||
|
||||
## Overview
|
||||
|
||||
Turn the existing Prime Time warm-up into a **daily preparation** ("Tagesvorbereitung").
|
||||
At a scheduled time (or on demand), Claude reads the open tasks, estimates effort,
|
||||
and selects a focused subset into the MyDay list — capped so it never moves
|
||||
everything in. Claude does the reasoning itself (agentic), via the already-registered
|
||||
ClaudeDo MCP. This replaces the current `"ping"` behavior entirely.
|
||||
|
||||
A later phase will feed external tickets (Jira, possibly a second system) into the
|
||||
same candidate pool; that is out of scope for this spec.
|
||||
|
||||
## Goals
|
||||
|
||||
- Scheduled and manual ("Tag vorbereiten" button) daily prep.
|
||||
- Claude picks a subset of open tasks into MyDay, ordered so related tasks sit together.
|
||||
- Effort-aware selection, hard-capped at `X` open MyDay tasks.
|
||||
- Keep existing MyDay tasks across re-runs; only top up to `X`.
|
||||
- Candidates limited to tasks in repos that are **not** excluded from the weekly report.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- External ticket integration (Jira etc.) — future phase.
|
||||
- Group labels/headers in the MyDay view — grouping is ordering-only via `SortOrder`.
|
||||
- A user-editable prep prompt — the prompt is fixed, parameterized.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
| Topic | Decision |
|
||||
| --- | --- |
|
||||
| Who reasons | Agentic — Claude decides via MCP tools. |
|
||||
| MyDay model | `TaskEntity.IsMyDay` flag (smart list `smart:my-day`). |
|
||||
| Grouping | Ordering only via existing `SortOrder` (no new field, no migration for grouping). |
|
||||
| Selection | Effort estimate, hard cap `X` tasks/day. |
|
||||
| Candidates | `Status == Idle`, `BlockedByTaskId == null`, list `WorkingDir` not under `ReportExcludedPaths`. |
|
||||
| Re-run | Keep existing MyDay tasks; top up to `X`. |
|
||||
| Trigger | Existing Prime schedule **and** a manual button. |
|
||||
| Ping | Removed — daily prep replaces it. |
|
||||
| Prompt | Fixed, with injected parameters (`X`, today's date). |
|
||||
| Tool access | Reuse the globally registered `claudedo` MCP — **no** separate `--mcp-config`. |
|
||||
|
||||
## Architecture
|
||||
|
||||
### 1. MCP tools (extend `ExternalMcpService`, port 47822)
|
||||
|
||||
The worker already exposes `ExternalMcpService` as the `claudedo` MCP server. Add two tools;
|
||||
they automatically surface as `mcp__claudedo__get_daily_prep_candidates` and
|
||||
`mcp__claudedo__set_my_day`.
|
||||
|
||||
- **`get_daily_prep_candidates()`** → JSON containing:
|
||||
- `candidates[]`: open, non-blocked tasks in non-excluded repos, each with
|
||||
`id, title, description, listName, isStarred, scheduledFor, age` (age derived from `CreatedAt`).
|
||||
- `currentMyDay[]`: currently-`IsMyDay` open tasks (so Claude sees remaining capacity).
|
||||
- Filter: `Status == Idle` AND `BlockedByTaskId == null` AND the task's list `WorkingDir`
|
||||
does not start with any prefix in `AppSettings.ReportExcludedPaths`
|
||||
(default `["C:\\Private"]`; case-insensitive prefix match, same semantics as the weekly report).
|
||||
|
||||
- **`set_my_day(taskId, isMyDay, sortOrder?)`** →
|
||||
- Sets `IsMyDay` and (optionally) `SortOrder` on the task via `TaskRepository`.
|
||||
- Broadcasts `TaskUpdated` via `HubBroadcaster` so the UI updates live.
|
||||
- **Cap-guard:** when `isMyDay == true`, count current open (`Idle`) tasks with
|
||||
`IsMyDay == true`. If `count >= X`, reject with an error message
|
||||
("MyDay limit {X} reached"). `isMyDay == false` is always allowed.
|
||||
`X = AppSettings.DailyPrepMaxTasks`. This guarantees the "never move everything in"
|
||||
invariant server-side, independent of Claude's behavior.
|
||||
|
||||
### 2. `DailyPrepRunner` (replaces ping logic)
|
||||
|
||||
Rename `IPrimeRunner`/`PrimeRunner` → `IDailyPrepRunner`/`DailyPrepRunner` (the `"ping"`
|
||||
concept is gone). It:
|
||||
|
||||
- Loads `AppSettings` (`X = DailyPrepMaxTasks`).
|
||||
- Builds the fixed prompt with injected parameters (`X`, today's date).
|
||||
- Invokes `claude -p --output-format stream-json --verbose` with:
|
||||
- `--permission-mode` set so the headless run won't block on permission prompts,
|
||||
- `--allowedTools mcp__claudedo__get_daily_prep_candidates mcp__claudedo__set_my_day`,
|
||||
- `--max-turns 30` (constant), timeout 5 min (constant; larger than the old 60s ping).
|
||||
- **No `--mcp-config`** — relies on the globally registered `claudedo` MCP (the worker runs
|
||||
as the user via the per-user logon Scheduled Task, so the headless run inherits the
|
||||
user-scope registration and its auth).
|
||||
- Returns an outcome (e.g. number of tasks added) for broadcasting.
|
||||
|
||||
### 3. Scheduler
|
||||
|
||||
`PrimeScheduler` is unchanged in structure — it now calls `IDailyPrepRunner` instead of the
|
||||
ping runner. `NextDueCalculator` and the schedule model are untouched.
|
||||
|
||||
### 4. Manual trigger
|
||||
|
||||
- Worker hub method `RunDailyPrepNow()` invokes the same `DailyPrepRunner`.
|
||||
- UI button **"Tag vorbereiten"** in the MyDay list header.
|
||||
- **Single-flight guard:** if a prep run is already in progress, the trigger reports
|
||||
"already running" and does not start a parallel run (applies to both schedule and button).
|
||||
|
||||
### 5. Parameter config
|
||||
|
||||
- New field **`DailyPrepMaxTasks`** (int, default `5`) on `AppSettingsEntity`.
|
||||
- Plumbing: EF config + migration, `AppSettingsRepository`, `WorkerHub` AppSettings DTO,
|
||||
UI DTO mirror + `WorkerClient`, and a numeric editor in the Prime Claude settings tab.
|
||||
- `ReportExcludedPaths` is reused as-is (already on `AppSettings`).
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. Trigger (schedule due **or** button) → `DailyPrepRunner.RunAsync`.
|
||||
2. Runner loads `AppSettings` (`X`), builds prompt, launches Claude.
|
||||
3. Claude → `get_daily_prep_candidates` → DB query returns filtered candidates + current MyDay.
|
||||
4. Claude estimates effort, tops up to **X total**, calls `set_my_day(id, true, sortOrder)`
|
||||
for each chosen task (consecutive `sortOrder` for related tasks).
|
||||
5. `ExternalMcpService` writes `IsMyDay`/`SortOrder`, broadcasts `TaskUpdated` → MyDay list
|
||||
updates live.
|
||||
6. Runner updates `LastRunAt`, broadcasts "prep done" (count added).
|
||||
|
||||
## Fixed Prompt (parameterized)
|
||||
|
||||
Content (parameters in `{}`):
|
||||
|
||||
> Du bereitest meinen Arbeitstag für **{today}** vor.
|
||||
> 1. Rufe `get_daily_prep_candidates` auf.
|
||||
> 2. Behalte bereits als MyDay markierte offene Tasks.
|
||||
> 3. Fülle bis **maximal {X} offene Tasks gesamt** in MyDay auf — niemals mehr.
|
||||
> 4. Schätze pro Task grob den Aufwand; wähle eine machbare Mischung (nicht nur Großbrocken).
|
||||
> Priorisiere `isStarred`, fällige (`scheduledFor`) und ältere Tasks.
|
||||
> 5. Lege thematisch verwandte Tasks durch aufeinanderfolgende `sortOrder`-Werte nebeneinander.
|
||||
> 6. Setze die Auswahl via `set_my_day(id, true, sortOrder)`. Markiere nichts außerhalb der
|
||||
> Kandidatenliste.
|
||||
|
||||
Injected parameters: `{today}` (date) and `{X}` (= `DailyPrepMaxTasks`).
|
||||
|
||||
## Error Handling
|
||||
|
||||
- No candidates → Claude marks nothing; runner reports "0 added".
|
||||
- Claude run fails / times out → log + failure broadcast (existing scheduler event channel);
|
||||
`LastRunAt` is set on attempt, as today, to avoid tight retry loops.
|
||||
- `set_my_day` on an invalid/ineligible id → tool returns an error string; Claude adapts.
|
||||
- Cap exceeded → tool returns an error; Claude stops adding.
|
||||
- Concurrent trigger → single-flight guard reports "already running".
|
||||
|
||||
## Testing
|
||||
|
||||
Real SQLite + real git (project convention).
|
||||
|
||||
- `get_daily_prep_candidates`: only `Idle`; blocked excluded; tasks in excluded repos
|
||||
(`ReportExcludedPaths`) excluded; current MyDay tasks included.
|
||||
- `set_my_day`: sets flag + `SortOrder`; broadcasts `TaskUpdated`; cap-guard rejects at limit;
|
||||
unset always allowed.
|
||||
- `DailyPrepRunner`: prompt contains `{X}` + date; args contain `--allowedTools` +
|
||||
permission-mode + `--max-turns`; success/failure outcomes via an `IClaudeProcess` fake.
|
||||
- Rename `IPrimeRunner` → `IDailyPrepRunner` requires syncing `PrimeScheduler` tests/fakes.
|
||||
|
||||
## Files to Create / Modify (high level)
|
||||
|
||||
**Data**
|
||||
- `Models/AppSettingsEntity.cs` — add `DailyPrepMaxTasks`.
|
||||
- `Configuration/AppSettingsEntityConfiguration.cs` — map new column.
|
||||
- `Migrations/` — new migration for `daily_prep_max_tasks`.
|
||||
- `Repositories/AppSettingsRepository.cs` — persist new field.
|
||||
|
||||
**Worker**
|
||||
- `External/ExternalMcpService.cs` — add `get_daily_prep_candidates`, `set_my_day` (+ cap-guard).
|
||||
- `Prime/PrimeRunner.cs` → `DailyPrepRunner.cs`; `Prime/Interfaces/IPrimeRunner.cs`
|
||||
→ `IDailyPrepRunner.cs`; prompt builder + arg builder.
|
||||
- `Prime/PrimeScheduler.cs` — depend on `IDailyPrepRunner`.
|
||||
- `Hub/WorkerHub.cs` — AppSettings DTO field; `RunDailyPrepNow()`.
|
||||
- `Program.cs` — DI registration update.
|
||||
|
||||
**UI**
|
||||
- `Services/WorkerClient.cs` + AppSettings DTO mirror — new field; `RunDailyPrepNow` call.
|
||||
- Prime Claude settings tab VM/view — numeric editor for `DailyPrepMaxTasks`.
|
||||
- MyDay list header — "Tag vorbereiten" button + command (Lists/IslandsShell VM).
|
||||
|
||||
**Tests**
|
||||
- `ClaudeDo.Worker.Tests` — MCP tools, runner, scheduler fakes.
|
||||
- `ClaudeDo.Data.Tests` — AppSettings persistence (if covered there).
|
||||
- `ClaudeDo.Ui.Tests` — settings VM / button wiring as applicable.
|
||||
|
||||
## Future Phase (out of scope)
|
||||
|
||||
External ticket sources (Jira, possibly a second system) feed into the candidate pool used by
|
||||
`get_daily_prep_candidates`, behind a task-source abstraction. Designed separately.
|
||||
151
docs/superpowers/specs/2026-06-03-daily-prep-live-view-design.md
Normal file
151
docs/superpowers/specs/2026-06-03-daily-prep-live-view-design.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Daily Prep — Live Output View + Clear Day — Design
|
||||
|
||||
Date: 2026-06-03
|
||||
|
||||
## Overview
|
||||
|
||||
Two follow-ups to the daily-prep ("Prime Claude") feature:
|
||||
|
||||
1. **Live output view.** While Claude prepares the day, there is no feedback. Add a
|
||||
live, human-readable view of the prep run's output, shown as a new content mode in
|
||||
the existing right-hand **Details island** (mirroring how Daily Notes works — a mode
|
||||
swap, not a separate window/column).
|
||||
2. **Clear Day button.** A MyDay-header button that clears the MyDay selection
|
||||
immediately.
|
||||
|
||||
## Goals
|
||||
|
||||
- See the prep run's progress live, rendered with the same friendly terminal renderer
|
||||
used for task runs (assistant text + tool calls like `set_my_day …`, not raw NDJSON).
|
||||
- Both manual (button) and scheduled prep runs stream into the log.
|
||||
- The manual button opens the prep view; a scheduled run fills the log silently and is
|
||||
opened via a dedicated "Vorbereitungs-Log" button (the existing `PrimeStatus` footer
|
||||
remains the hint that a run happened).
|
||||
- A "Tag leeren" button clears all MyDay tasks (any status) with no confirmation.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No new island/column and no popup/overlay — reuse the Details island as a mode swap.
|
||||
- No persistence of prep output across app restarts (in-memory log only).
|
||||
- No undo for Clear Day (re-runnable via "Tag vorbereiten").
|
||||
|
||||
## Key Decisions
|
||||
|
||||
| Topic | Decision |
|
||||
| --- | --- |
|
||||
| Rendering | Reuse the existing `SessionTerminalView` / `StreamLineFormatter` renderer. |
|
||||
| Location | New `IsPrepMode` content panel inside the Details island (like `IsNotesMode`). |
|
||||
| Lifecycle | Manual click opens the view (UI-local); `PrepStarted/PrepLine/PrepFinished` events fill the log regardless of current mode; scheduled runs do not auto-open. |
|
||||
| Open after schedule | Dedicated "Vorbereitungs-Log" header button + existing `PrimeStatus` footer hint. |
|
||||
| Clear Day scope | All MyDay tasks regardless of status. |
|
||||
| Clear Day confirm | None — clear directly. |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Feature A — Live prep output
|
||||
|
||||
**Worker**
|
||||
- Extend `IPrimeBroadcaster` (`src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs`)
|
||||
with `PrepStartedAsync()`, `PrepLineAsync(string line)`, `PrepFinishedAsync(bool success)`.
|
||||
- Implement in `HubBroadcaster` (`src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`) sending
|
||||
SignalR events `PrepStarted`, `PrepLine` (string), `PrepFinished` (bool).
|
||||
- `PrimeRunner` (`src/ClaudeDo.Worker/Prime/PrimeRunner.cs`): inject `IPrimeBroadcaster`.
|
||||
In `FireAsync`, after the single-flight gate is entered and a run will actually happen:
|
||||
call `PrepStartedAsync()` before `RunAsync`; replace the discard lambda with
|
||||
`async line => await _broadcaster.PrepLineAsync(line)`; call
|
||||
`PrepFinishedAsync(result.IsSuccess)` after. The "already running" early-return path
|
||||
emits nothing (no run occurs). Both scheduled and manual runs go through `FireAsync`,
|
||||
so both stream.
|
||||
|
||||
**UI**
|
||||
- `WorkerClient` (`src/ClaudeDo.Ui/Services/WorkerClient.cs`): register
|
||||
`_hub.On<…>("PrepStarted"/"PrepLine"/"PrepFinished", …)` each via
|
||||
`Dispatcher.UIThread.Post`, raising `PrepStartedEvent` / `PrepLineEvent(string)` /
|
||||
`PrepFinishedEvent(bool)`. Declare these on `IWorkerClient`.
|
||||
- `DetailsIslandViewModel`: add `IsPrepMode` (bool), `IsPrepRunning` (bool), a dedicated
|
||||
`PrepLog` (`ObservableCollection<LogLineViewModel>`), and `ShowPrep()` (calls
|
||||
`Bind(null)`, sets `IsNotesMode=false`, `IsPrepMode=true`). Subscribe to the three prep
|
||||
events in the ctor (always active, independent of mode):
|
||||
- `PrepStarted` → clear `PrepLog`, `IsPrepRunning=true`.
|
||||
- `PrepLine` → format the line with the same `StreamLineFormatter` path used by the
|
||||
stdout branch of `OnTaskMessage`, append a `LogLineViewModel` to `PrepLog`.
|
||||
- `PrepFinished` → `IsPrepRunning=false` (optionally append a status line).
|
||||
Mode exclusivity: the normal task-details panel becomes visible on
|
||||
`!IsNotesMode && !IsPrepMode`; `ShowNotes()` also sets `IsPrepMode=false`; `Bind(task)`
|
||||
resets both flags.
|
||||
- `DetailsIslandView.axaml`: add a third `<Panel IsVisible="{Binding IsPrepMode}">` in the
|
||||
body grid alongside the existing details/notes panels, rendering `PrepLog` in the
|
||||
terminal style (reuse the `LogLineViewModel` item template used by `SessionTerminalView`).
|
||||
|
||||
**Wiring**
|
||||
- `TasksIslandViewModel`: add a `PrepRequested` event (mirror `NotesRequested`).
|
||||
`PrepareDayCommand` raises `PrepRequested` in addition to calling
|
||||
`RunDailyPrepNowAsync()`. Add `ShowPrepLogCommand` that raises `PrepRequested`. Add the
|
||||
"Vorbereitungs-Log" button to the MyDay header (`IsVisible="{Binding IsMyDayList}"`).
|
||||
- `IslandsShellViewModel`: wire `Tasks.PrepRequested += () => Details.ShowPrep()`.
|
||||
|
||||
### Feature B — Clear Day
|
||||
|
||||
**Worker**
|
||||
- `WorkerHub.ClearMyDay()` (`src/ClaudeDo.Worker/Hub/WorkerHub.cs`): query ids where
|
||||
`IsMyDay == true`; `ExecuteUpdateAsync` setting `is_my_day = false`; broadcast
|
||||
`TaskUpdated(id)` for each affected id (the UI reloads the current list on `TaskUpdated`).
|
||||
|
||||
**UI**
|
||||
- `IWorkerClient.ClearMyDayAsync()` + `WorkerClient` impl invoking `"ClearMyDay"`.
|
||||
- `TasksIslandViewModel.ClearDayCommand` calls `_worker.ClearMyDayAsync()` (no confirm).
|
||||
Add the "Tag leeren" button to the MyDay header next to "Tag vorbereiten".
|
||||
|
||||
## Data Flow (live view)
|
||||
|
||||
1. Trigger (schedule or button) → `PrimeRunner.FireAsync`.
|
||||
2. `PrepStartedAsync()` → SignalR `PrepStarted` → `WorkerClient.PrepStartedEvent` →
|
||||
`DetailsIslandViewModel` clears `PrepLog`, sets `IsPrepRunning`.
|
||||
3. Each Claude stdout line → `PrepLineAsync(line)` → `PrepLine` → formatted, appended to
|
||||
`PrepLog` (visible if the user is in prep mode; filled silently otherwise).
|
||||
4. Run ends → `PrepFinishedAsync(success)` → `PrepFinished` → `IsPrepRunning=false`.
|
||||
5. Manual button click also raised `PrepRequested` → `Details.ShowPrep()` (view open).
|
||||
After a scheduled run, the user clicks "Vorbereitungs-Log" to open it.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Prep run fails/times out → `PrepFinished(false)`; the existing `PrimeFired` footer
|
||||
status still reports failure.
|
||||
- "Already running" → no prep events emitted (no run happened); existing behavior intact.
|
||||
- `ClearMyDay` with zero MyDay tasks → no-op, no broadcasts.
|
||||
|
||||
## Testing
|
||||
|
||||
- Worker: `PrimeRunner` streams `PrepStarted` → N×`PrepLine` → `PrepFinished` (fake
|
||||
`IClaudeProcess` invokes `onStdoutLine` with sample lines; fake `IPrimeBroadcaster`
|
||||
records calls). `WorkerHub.ClearMyDay` clears all IsMyDay rows and broadcasts per id
|
||||
(real SQLite, mirror existing hub tests).
|
||||
- UI: `DetailsIslandViewModel` appends to `PrepLog` on `PrepLineEvent` and `ShowPrep()`
|
||||
sets the mode flags (mutual exclusivity with notes); `TasksIslandViewModel.ClearDayCommand`
|
||||
calls `ClearMyDayAsync` (stub worker client).
|
||||
|
||||
## Files (high level)
|
||||
|
||||
**Modify**
|
||||
- `src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs`
|
||||
- `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`
|
||||
- `src/ClaudeDo.Worker/Prime/PrimeRunner.cs`
|
||||
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` (ClearMyDay)
|
||||
- `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||
- `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
|
||||
- `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`
|
||||
- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
|
||||
- `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml`
|
||||
- `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`
|
||||
- `src/ClaudeDo.Localization/locales/en.json`, `de.json` (button labels)
|
||||
|
||||
**Test**
|
||||
- `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs`
|
||||
- `tests/ClaudeDo.Worker.Tests/Hub/…` (ClearMyDay)
|
||||
- `tests/ClaudeDo.Ui.Tests/…` (DetailsIslandViewModel prep events; TasksIslandViewModel ClearDay) + `StubWorkerClient`
|
||||
|
||||
## Known fragility
|
||||
|
||||
Changing `IWorkerClient` / `WorkerClient` / VM constructors breaks hand-rolled fakes
|
||||
(`StubWorkerClient`, `FakeWorkerClient`) in both test projects — update all of them.
|
||||
114
docs/superpowers/specs/2026-06-03-localization-design.md
Normal file
114
docs/superpowers/specs/2026-06-03-localization-design.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Localization (i18n) Support — Design
|
||||
|
||||
**Date:** 2026-06-03
|
||||
**Status:** Approved (pending spec review)
|
||||
|
||||
## Goal
|
||||
|
||||
Add translation support to ClaudeDo. The user picks a language in the Settings modal and **all** UI text reflects it instantly (no restart). The WPF installer is localized the same way and gets its own language picker. Ship **English only** now, but the system is fully data-driven: adding a new language means dropping one JSON file into a folder — **no code changes, no rebuild**.
|
||||
|
||||
## Decisions (from brainstorming)
|
||||
|
||||
- **Languages:** English only at launch; extensible via translation files.
|
||||
- **Switching:** Live / instant — all bound UI text updates the moment the language changes.
|
||||
- **Storage:** Selected language stored in `~/.todo-app/ui.config.json` (the local UI config that also holds `DbPath`/`SignalRUrl`). Purely a UI concern — does **not** go through the worker/SignalR settings path.
|
||||
- **Installer:** Defaults to existing config language (upgrade) → OS culture → English. Shows a language picker in the wizard, live-switches its own UI, and writes the chosen language into `ui.config.json` so the app launches matching the installer.
|
||||
- **Locale files:** Loose `*.json` files in a `locales/` folder next to the running exe, scanned at startup to discover available languages.
|
||||
- **Code sharing:** A shared `ClaudeDo.Localization` project holds the loading/lookup/language-list logic, referenced by `ClaudeDo.Ui`, `ClaudeDo.App`, and `ClaudeDo.Installer`. Each UI framework keeps its own thin markup-extension binding layer (Avalonia ≠ WPF).
|
||||
|
||||
## Architecture & Components
|
||||
|
||||
### New shared project: `ClaudeDo.Localization`
|
||||
|
||||
- **`LocaleStore`** — discovers and loads `*.json` files from the `locales/` folder next to the running exe. Parses each file's nested JSON, **flattens it into an internal `Dictionary<string,string>`** keyed by dot-path for O(1) lookup, and captures `metadata.code` / `metadata.name`. Exposes the list of available languages for the dropdowns.
|
||||
- **`ILocalizer` / `Localizer`** — singleton holding the *active* language dictionary. Members:
|
||||
- indexer `this[string key]` → translated string (with fallback),
|
||||
- `string Get(string key, params object[] args)` → `string.Format` for parameterized strings,
|
||||
- `void SetLanguage(string code)` → swaps the active dictionary and raises `PropertyChanged` for the indexer so **all live bindings refresh** (this is what enables instant switching),
|
||||
- `AvailableLanguages` (list of `{ code, name }`), `CurrentCode`.
|
||||
- **Fallback chain:** requested key in active language → same key in English → the key path string itself (a missing translation is visible, never a crash).
|
||||
- **OS-culture resolution:** helper that maps the current OS UI culture to an available locale code, falling back to English.
|
||||
|
||||
### Per-framework binding layer (not shared)
|
||||
|
||||
- **Avalonia:** a `{loc:Tr Some.Key}` markup extension that binds to `Localizer[key]` (Source = the singleton `Localizer`, Path = `[key]`). Language change raises the indexer `PropertyChanged`, refreshing every binding.
|
||||
- **WPF installer:** an equivalent markup extension doing the same against the installer's own `Localizer` instance.
|
||||
|
||||
Both consume the **same JSON files and the same `LocaleStore`/`Localizer` logic** from the shared project.
|
||||
|
||||
## Translation File Format
|
||||
|
||||
`locales/en.json` (and future `de.json`, `fr.json`, …) — nested, human-friendly hierarchy:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": { "code": "en", "name": "English" },
|
||||
"settings": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"general": { "model": "Model", "maxParallel": "Max parallel executions" }
|
||||
},
|
||||
"tasks": {
|
||||
"addPlaceholder": "Add a task…",
|
||||
"overdue": "OVERDUE"
|
||||
},
|
||||
"worktrees": { "autoCleanupDays": "{0} days" }
|
||||
}
|
||||
```
|
||||
|
||||
- `metadata.code` is the language id stored in `ui.config.json` and matched to OS culture; `metadata.name` is the dropdown label.
|
||||
- **Lookup by dot-path key** (`"settings.general.model"`). On-disk file stays grouped/nested; the runtime flattens it for fast lookup. Authors edit a clean hierarchy.
|
||||
- **Parameters:** `{0}`, `{1}` placeholders resolved via `Get(key, args)`.
|
||||
- **Encoding:** UTF-8 — non-ASCII languages work out of the box.
|
||||
|
||||
## Data Flow & Wiring
|
||||
|
||||
### App config
|
||||
|
||||
- Add `Language` (string, e.g. `"en"`) to `AppSettings` (`ClaudeDo.Ui/AppSettings.cs`) and to the installer mirror `InstallerAppSettings` (`ClaudeDo.Installer/Core/ConfigModels.cs`).
|
||||
- Add a `Save()` method to `AppSettings` (today the UI only reads it).
|
||||
|
||||
### App startup (`ClaudeDo.App/Program.cs`)
|
||||
|
||||
1. `AppSettings.Load()` reads `Language` (missing/empty → resolve from OS culture, else `"en"`).
|
||||
2. `LocaleStore` scans `locales/` next to the exe; `Localizer` is registered as a singleton and set to the configured language.
|
||||
3. UI renders; every `{loc:Tr ...}` binding pulls from the active dictionary.
|
||||
|
||||
### Changing language in Settings (General tab)
|
||||
|
||||
- New "Language" dropdown bound to `Localizer.AvailableLanguages`; selection bound to current code.
|
||||
- On change → `Localizer.SetLanguage(code)` (instant UI refresh) **and** `AppSettings.Language = code; AppSettings.Save()`. Local UI state only — not routed through worker/SignalR.
|
||||
|
||||
### Installer (`ClaudeDo.Installer`)
|
||||
|
||||
- On launch: default language = existing `ui.config.json` `Language` if present (upgrade), else OS culture, else English.
|
||||
- Wizard gets a language dropdown (same `LocaleStore`, installer's own markup extension) → live-switches the installer UI.
|
||||
- When writing `ui.config.json`, persists the chosen `Language` so the app launches matching the installer.
|
||||
|
||||
### Build wiring
|
||||
|
||||
- `locales/*.json` copied to output (`CopyToOutputDirectory`) for both App and Installer.
|
||||
- Installer packages the `locales/` folder so it lands beside the installed exe.
|
||||
|
||||
## String-Extraction Scope
|
||||
|
||||
Mechanical but large; done screen-by-screen so each commit is reviewable, building one `en.json` as the single source of truth.
|
||||
|
||||
- **22 Avalonia `.axaml` views** — replace inline `Text="..."`, `Content="..."`, `PlaceholderText="..."`, and inline `ComboBoxItem` text with `{loc:Tr key}`.
|
||||
- **ViewModel strings** — user-facing literals built in C# (e.g. `HeaderTitle`, `StatusPill`, status text, parameterized messages) resolve via injected `ILocalizer` (`localizer.Get(...)`). Log messages and non-user-facing strings stay as-is. **Live-switch note:** a VM string resolved once will not refresh on language change. For VM-built user-facing text, either (a) prefer resolving in XAML via `{loc:Tr}` where possible, or (b) have the VM subscribe to the `Localizer` change event and re-raise `PropertyChanged` (or re-resolve) for its localized properties. Decide per-property during extraction.
|
||||
- **10 WPF installer files** — same treatment with the installer's markup extension; VM-driven headings (`Heading`, `NextButtonText`, etc.) go through `ILocalizer`.
|
||||
- **Enum-ish display values** (model names, permission modes, weekday names) — translate the *display* text while keeping the underlying value/binding intact.
|
||||
|
||||
## Testing
|
||||
|
||||
- `ClaudeDo.Localization` unit tests: load/flatten nested JSON, dot-path lookup, fallback chain (active→en→key), `{0}` formatting, OS-culture resolution.
|
||||
- `LocaleStore` discovery test (folder scan → available languages).
|
||||
- **Key-coverage test:** every locale file's flattened key set matches `en.json`; fails the build if `en.json` drifts from other locale files.
|
||||
- Settings round-trip test: `SetLanguage` updates `Localizer` **and** persists to `ui.config.json`.
|
||||
- Manual UI pass (user's visual review): confirm instant switching with a throwaway `de.json` stub during dev, then remove it.
|
||||
|
||||
## Out of Scope (YAGNI)
|
||||
|
||||
- Pluralization rules, RTL layout, per-string gender.
|
||||
- Translating the German weekly-report **body** (generated content — stays as-is).
|
||||
- Localizing log output and non-user-facing strings.
|
||||
226
docs/superpowers/specs/2026-06-03-weekly-report-design.md
Normal file
226
docs/superpowers/specs/2026-06-03-weekly-report-design.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Weekly Report — Design
|
||||
|
||||
**Date:** 2026-06-03
|
||||
**Status:** Approved (pending spec review)
|
||||
|
||||
## Goal
|
||||
|
||||
Generate a short, standup-focused report of what the user did over the past week,
|
||||
for the Wednesday standup. The report is built from the user's Claude Code session
|
||||
history across all repos, distilled and summarized by Claude. Personal repos under a
|
||||
configurable excluded path (default `C:\Private`) are left out. The user can author
|
||||
per-day bullet notes inside ClaudeDo (via the My Day list) that are folded into the
|
||||
report.
|
||||
|
||||
## Decisions (from brainstorming)
|
||||
|
||||
- **Data source:** all Claude Code history in `~/.claude/projects/*/*.jsonl`, both manual
|
||||
sessions and ClaudeDo-run tasks, grouped by repo.
|
||||
- **Exclusion:** a configurable list of path prefixes (default `["C:\\Private"]`). Any
|
||||
session whose `cwd` starts with an excluded prefix is dropped.
|
||||
- **Summarization:** Claude CLI summarizes. The Worker distills the logs, then runs a
|
||||
single one-shot `claude -p` call via the existing `ClaudeProcess` and returns the
|
||||
result markdown. No worktree, no task row, no queue.
|
||||
- **Period:** default "since last Wednesday → today", computed from a configurable
|
||||
standup weekday. The range is adjustable in the modal.
|
||||
- **Signal fed to Claude:** user prompts (intent), assistant closing summaries, and the
|
||||
user's daily notes. No git-commit scanning.
|
||||
- **Report shape:** German, grouped by day, first-person past-tense bullets, ~3-5
|
||||
bullets/day with trivia merged/dropped, notes blended into one deduplicated list per
|
||||
day. See the Report Prompt section.
|
||||
- **Placement:** a "Weekly Report" overlay modal opened from the toolbar, rendering via
|
||||
the existing `MarkdownView`.
|
||||
- **Output:** view-only in-app (no export).
|
||||
- **Notes UI:** authored in the My Day list via a pinned non-task "Notes" pseudo-row that
|
||||
repurposes the Details island into a bullet-notes editor. Per-day bullets with a day
|
||||
navigator (prev/next arrows + date picker + Today).
|
||||
- **Report persistence:** generated reports are stored, keyed by exact date range, and
|
||||
reused. Generation is button-driven (never automatic); a Regenerate button overwrites.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
UI (WeeklyReportModal, Details-island notes mode)
|
||||
│ SignalR
|
||||
▼
|
||||
WorkerHub ── GetWeekReport / GenerateWeekReport / daily-notes CRUD
|
||||
│
|
||||
├── WeekReportService ──► ClaudeHistoryReader (scan ~/.claude/projects)
|
||||
│ │ (distilled activity)
|
||||
│ ├── DailyNoteRepository (notes in window)
|
||||
│ ├── ClaudeProcess (one-shot summarize)
|
||||
│ └── WeekReportRepository (store/reuse)
|
||||
└── DailyNoteRepository (CRUD)
|
||||
|
||||
Data: DailyNoteEntity, WeekReportEntity + repositories + EF migration
|
||||
AppSettingsEntity: ReportExcludedPaths, StandupWeekday
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Data layer (`ClaudeDo.Data`)
|
||||
|
||||
**`DailyNoteEntity`** (table `daily_notes`)
|
||||
- `Id` (GUID string, init-only PK)
|
||||
- `Date` (date-only; the day the bullet belongs to)
|
||||
- `Text` (string, the bullet content)
|
||||
- `SortOrder` (int; ordering within a day)
|
||||
- `CreatedAt` (DateTime)
|
||||
|
||||
**`DailyNoteRepository`** (async, CancellationToken, follows existing repo pattern)
|
||||
- `ListByDayAsync(DateOnly day)` — bullets for one day, ordered by `SortOrder`.
|
||||
- `ListBetweenAsync(DateOnly start, DateOnly end)` — bullets in a window (used by the report).
|
||||
- `AddAsync(DateOnly day, string text)` — appends a bullet (assigns next `SortOrder`).
|
||||
- `UpdateAsync(string id, string text)`
|
||||
- `DeleteAsync(string id)`
|
||||
|
||||
**`WeekReportEntity`** (table `week_reports`)
|
||||
- `Id` (GUID string, init-only PK)
|
||||
- `StartDate`, `EndDate` (date-only; the report window — unique together)
|
||||
- `Markdown` (string; the generated report)
|
||||
- `GeneratedAt` (DateTime)
|
||||
|
||||
**`WeekReportRepository`**
|
||||
- `GetByRangeAsync(DateOnly start, DateOnly end)` — stored report for an exact range, or null.
|
||||
- `UpsertAsync(DateOnly start, DateOnly end, string markdown)` — insert or overwrite by range.
|
||||
|
||||
**`AppSettingsEntity`** — two new columns:
|
||||
- `ReportExcludedPaths` (string, JSON array of path prefixes; default `["C:\\Private"]`)
|
||||
- `StandupWeekday` (int, `DayOfWeek`; default `Wednesday` = 3)
|
||||
|
||||
**Migration** — one EF migration adds `daily_notes`, `week_reports`, and the two
|
||||
`app_settings` columns. Entity configs in `Configuration/` (date-only and enum/JSON
|
||||
conversion via `ValueConverter`, per existing convention).
|
||||
|
||||
### 2. Worker (`ClaudeDo.Worker`) — new `Report/` folder
|
||||
|
||||
**`ClaudeHistoryReader`** (raw → distilled)
|
||||
- Input: date window + excluded path prefixes.
|
||||
- Enumerates `~/.claude/projects/*/*.jsonl`.
|
||||
- Parses each line as JSON; tolerant of malformed lines (skip, never throw).
|
||||
- Drops a session entirely if its `cwd` starts with any excluded prefix
|
||||
(case-insensitive, normalized separators).
|
||||
- Keeps messages whose `timestamp` falls in `[start, end]`.
|
||||
- Extracts, per repo (`cwd`) → per day:
|
||||
- **user prompts**: `type == "user"` text content (string or `content[].text`).
|
||||
Skip tool-result-only user turns and queue/attachment/hook noise.
|
||||
- **assistant closing summaries**: the final assistant text block of each turn/session.
|
||||
- Output: a structured model, e.g.
|
||||
`IReadOnlyList<RepoActivity>` where `RepoActivity { RepoPath, Days: List<DayActivity{ Date, Prompts[], Summaries[] }> }`.
|
||||
|
||||
**`WeekReportService`** (distilled → stored summary)
|
||||
- `GenerateAsync(start, end, ct)`:
|
||||
1. Read settings (excluded paths, standup weekday).
|
||||
2. `ClaudeHistoryReader` → distilled activity.
|
||||
3. `DailyNoteRepository.ListBetweenAsync` → notes grouped by day.
|
||||
4. Pivot the distilled activity (repo→day from the reader) into **day-major**
|
||||
(day→repo) to match the day-grouped report, and build the prompt from the
|
||||
template in the Report Prompt section. Empty window → produce a "no activity"
|
||||
report without calling Claude.
|
||||
5. Run `ClaudeProcess` once (`claude -p`, no worktree/agents; working dir = a neutral
|
||||
dir). Read `RunResult.ResultMarkdown`.
|
||||
6. `WeekReportRepository.UpsertAsync(start, end, markdown)`; return markdown.
|
||||
7. On Claude failure, surface `RunResult.ErrorMarkdown` to the caller (do not store).
|
||||
- `GetStoredAsync(start, end)` → `WeekReportRepository.GetByRangeAsync`.
|
||||
|
||||
Interfaces live in `Report/Interfaces/` per the area convention.
|
||||
|
||||
#### Report Prompt
|
||||
|
||||
`WeekReportService` assembles this prompt. Instructions are in English (more reliable
|
||||
steering); the output is forced to German. `{...}` are filled at build time.
|
||||
|
||||
```
|
||||
You are generating a concise weekly standup report for a software developer.
|
||||
Summarize what they accomplished between {start:dd.MM.yyyy} and {end:dd.MM.yyyy}.
|
||||
|
||||
Rules:
|
||||
- Write the ENTIRE report in German.
|
||||
- Group by day. One "## {Wochentag}, {dd.MM.yyyy}" section per day that has
|
||||
activity (German weekday names). Omit days with no activity entirely.
|
||||
- Within each day: 3–5 first-person, past-tense bullets ("- Habe X umgesetzt",
|
||||
"- Y behoben"). Merge related small work into one bullet.
|
||||
- Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise.
|
||||
- Blend the developer's own notes and the derived activity into ONE deduplicated
|
||||
bullet list per day. The developer's notes are authoritative — never omit or
|
||||
contradict their substance.
|
||||
- Name the project/repo when it adds clarity.
|
||||
- Output ONLY the dated sections. No preamble, no intro, no closing remarks.
|
||||
|
||||
== Activity (from session history) ==
|
||||
{day-major: for each day → for each repo → its prompts + closing summaries}
|
||||
|
||||
== Developer notes ==
|
||||
{day-major: for each day → the bullets}
|
||||
```
|
||||
|
||||
### 3. IPC (Hub + WorkerClient)
|
||||
|
||||
**`WorkerHub`** new methods:
|
||||
- `GetWeekReport(string startIso, string endIso)` → stored markdown or null.
|
||||
- `GenerateWeekReport(string startIso, string endIso)` → generates, stores, returns markdown.
|
||||
- `GetDailyNotes(string dayIso)` → bullets for a day.
|
||||
- `AddDailyNote(string dayIso, string text)` → created bullet.
|
||||
- `UpdateDailyNote(string id, string text)`.
|
||||
- `DeleteDailyNote(string id)`.
|
||||
|
||||
**`WorkerClient`** (UI) mirrors these, following the existing
|
||||
`WorkerPrimeScheduleApi`/AppSettings method pattern.
|
||||
|
||||
### 4. UI (`ClaudeDo.Ui`)
|
||||
|
||||
**Weekly Report modal** (`WeeklyReportModalView` + `WeeklyReportModalViewModel`)
|
||||
- Overlay modal in the `Modals/` pattern (like `WorktreesOverviewModalView`),
|
||||
registered in `IslandsShellViewModel`, opened from a new toolbar button.
|
||||
- Date range: two `ThemedDatePicker`s, default "since last Wednesday → today" computed
|
||||
from `StandupWeekday`.
|
||||
- On open and on range change: call `GetWeekReport`.
|
||||
- Stored report exists → render markdown via `MarkdownView`, show `GeneratedAt`, show
|
||||
a **Regenerate** button.
|
||||
- None → empty state ("Not generated yet") + a **Generate** button.
|
||||
- **Generate**/**Regenerate**: call `GenerateWeekReport` with a busy/spinner state;
|
||||
render the returned markdown. Generation only ever runs from these buttons.
|
||||
- View-only; no export.
|
||||
|
||||
**Notes in My Day**
|
||||
- The My Day smart list (`smart:my-day`) pins a fixed, non-task "Notes" pseudo-row at
|
||||
the top, recognized by the list/selection code (not a `TaskEntity`).
|
||||
- Selecting it puts the **Details island** into **notes mode** (task fields hidden,
|
||||
notes editor shown). The island hosts a dedicated `NotesEditorViewModel` + small view
|
||||
rather than swelling `DetailsIslandViewModel` (already ~978 lines); the bullet logic
|
||||
stays isolated and testable.
|
||||
- **Day navigator** in the editor header: `<` / `>` arrows to step days, a
|
||||
`ThemedDatePicker` to jump to any date, and a "Today" button. Defaults to today; the
|
||||
pinned row's default day rolls over at midnight (no data lost — past days remain
|
||||
reachable via the navigator).
|
||||
- **Bullet editing** for the selected day: list of bullets with add / inline-edit /
|
||||
delete / reorder (`SortOrder`). Each operation goes through the daily-notes hub CRUD.
|
||||
|
||||
### 5. Settings
|
||||
|
||||
- Add the excluded-path list and the standup weekday to the existing Settings modal,
|
||||
persisted via the new `app_settings` columns and the existing
|
||||
`GetAppSettings`/`UpdateAppSettings` path.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Malformed/unreadable JSONL lines are skipped, never fatal.
|
||||
- Empty window → a "no activity" report, no Claude call.
|
||||
- Claude call failure → error surfaced in the modal; nothing stored.
|
||||
- Date ranges normalized to date-only; the stored report key is the exact (start, end).
|
||||
|
||||
## Testing
|
||||
|
||||
- **`ClaudeHistoryReader`** (Worker tests, fixture `.jsonl`): date-window filtering,
|
||||
excluded-prefix dropping (case/separator normalization), prompt/summary extraction,
|
||||
malformed-line tolerance, repo/day grouping.
|
||||
- **`WeekReportService`**: prompt-building from distilled activity + notes; empty-window
|
||||
short-circuit; storage upsert; with a faked `ClaudeProcess`.
|
||||
- **`DailyNoteRepository`** and **`WeekReportRepository`**: CRUD / upsert / range lookup
|
||||
against real SQLite (matches existing test style).
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Report export (clipboard/file) — view-only for now.
|
||||
- Git-commit scanning.
|
||||
- Editing or summarizing full transcripts; only prompts + closing summaries are used.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Approve = Merge → Done, plus Conflict Preview — Design
|
||||
|
||||
**Date:** 2026-06-04
|
||||
**Status:** Approved (autonomous — user on break, authorized to continue)
|
||||
**Author:** brainstormed from issue "Make merge/diff real"
|
||||
|
||||
## Problem
|
||||
|
||||
Approving a `WaitingForReview` task flips it straight to `Done`
|
||||
(`TaskStateService.ApproveReviewAsync`) and **never merges** its worktree — the
|
||||
worktree stays `Active`. The user approved three component tasks expecting them
|
||||
to merge; none did. Separately, there is **no way to see whether a task's
|
||||
worktree merges cleanly** before acting, and a standalone task has no direct
|
||||
**Merge** button (single-task merge is only reachable from inside the Diff
|
||||
modal).
|
||||
|
||||
What is already real (verified): `WorkerHub.MergeTask → TaskMergeService.MergeAsync`
|
||||
performs a real `git merge --no-ff`, aborts on conflict, and marks the worktree
|
||||
`Merged`. **Open Diff** opens a real in-app diff. **Merge All Subtasks**
|
||||
(planning) is real. So the gaps are narrow.
|
||||
|
||||
## Scope decisions (autonomous)
|
||||
|
||||
- **Tab location:** keep the **single "Session" tab** that the recent commit
|
||||
`ac9bae9` deliberately consolidated. All new controls go in its existing
|
||||
`MERGE & WORKTREE` block (`WorkConsole.axaml:196`). Do **not** re-introduce a
|
||||
separate "Actions" tab.
|
||||
- **Approve target:** Approve merges into the UI-selected merge target
|
||||
(`SelectedMergeTarget`); when blank, the worker resolves to the repo's current
|
||||
branch.
|
||||
- **On conflict:** task stays in `WaitingForReview` (no new status). The conflict
|
||||
is surfaced inline. No automatic state change to a "blocked" status.
|
||||
- **Worktree removal on approve:** do **not** remove — merge marks the worktree
|
||||
`Merged` and existing auto-cleanup handles disposal (matches the single-task
|
||||
merge default `removeWorktree:false`).
|
||||
- **Applies to:** standalone leaf tasks with an active worktree. A
|
||||
`WaitingForReview` task with **no** active worktree (e.g. ran in a sandbox, or
|
||||
an improvement parent whose children own the worktrees) is just marked `Done`
|
||||
— current behavior preserved. Planning parents keep "Merge All Subtasks".
|
||||
|
||||
## Acceptance (restated)
|
||||
|
||||
1. Approve a clean-merging task → worktree merged into target, worktree `Merged`,
|
||||
task `Done`.
|
||||
2. Approve a conflicting task → task **not** `Done`, conflict surfaced.
|
||||
3. Opening a Done/WaitingForReview task shows clean/conflict status **without
|
||||
mutating** the tree (use `git merge-tree`, not a real merge).
|
||||
|
||||
## Architecture
|
||||
|
||||
Three layers, each single-purpose; the only new cross-dependency is
|
||||
`TaskMergeService → ITaskStateService` (one-way; verify no DI cycle).
|
||||
|
||||
### 1. GitService — non-destructive conflict probe (`ClaudeDo.Data`)
|
||||
|
||||
New method:
|
||||
|
||||
```csharp
|
||||
public sealed record MergePreview(bool Supported, bool Clean, IReadOnlyList<string> ConflictFiles);
|
||||
|
||||
public async Task<MergePreview> PreviewMergeAsync(
|
||||
string repoDir, string targetBranch, string sourceBranch, CancellationToken ct = default)
|
||||
```
|
||||
|
||||
- Runs `git merge-tree --write-tree --name-only <target> <source>` from `repoDir`.
|
||||
`merge-tree` computes the merge base itself and writes only loose objects — it
|
||||
does **not** touch the working tree, index, or refs.
|
||||
- Exit code `0` → `Clean = true`, no conflict files.
|
||||
- Exit code `1` → `Clean = false`; conflicted paths are the lines after the
|
||||
first (tree-OID) line, up to the first blank line.
|
||||
- Any other outcome (e.g. git too old → "unknown option") → `Supported = false`
|
||||
(UI shows "mergeability unknown").
|
||||
|
||||
New helper for the "· N files" count (clean case):
|
||||
`git diff --name-only <target>...<source>` (three-dot = changes on source since
|
||||
the merge base); count non-empty lines. May reuse/extend existing diff helpers.
|
||||
|
||||
### 2. TaskMergeService — preview + approve orchestration (`ClaudeDo.Worker`)
|
||||
|
||||
Inject `ITaskStateService` (verify `PlanningChainCoordinator` has no back-edge to
|
||||
`TaskMergeService`; if a cycle exists, fall back to orchestrating in the hub).
|
||||
|
||||
```csharp
|
||||
public sealed record MergePreviewResult(string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount);
|
||||
// Status: "clean" | "conflict" | "unavailable"
|
||||
public Task<MergePreviewResult> PreviewAsync(string taskId, string targetBranch, CancellationToken ct);
|
||||
|
||||
public Task<MergeResult> ApproveAndMergeAsync(string taskId, string targetBranch, CancellationToken ct);
|
||||
```
|
||||
|
||||
**PreviewAsync:** load context. If no active worktree → `"unavailable"`. Resolve
|
||||
`targetBranch` (blank → current branch). Call `GitService.PreviewMergeAsync`; map
|
||||
`Supported=false` → `"unavailable"`, else clean/conflict (+ ChangedFileCount on
|
||||
clean).
|
||||
|
||||
**ApproveAndMergeAsync:** load context; require `task.Status == WaitingForReview`
|
||||
(else `Blocked`). Resolve target (blank → current branch).
|
||||
- **No active worktree** → `_state.ApproveReviewAsync(taskId)` → return
|
||||
`MergeResult(StatusMerged, [], null)` ("approved, nothing to merge").
|
||||
- **Active worktree** → `MergeAsync(taskId, target, removeWorktree:false,
|
||||
"Merge {branch}", ct)`. On `StatusMerged` → `_state.ApproveReviewAsync(taskId)`
|
||||
then return the merged result. On `StatusConflict`/`StatusBlocked` → return as-is;
|
||||
**do not** flip status (task stays `WaitingForReview`).
|
||||
|
||||
`TaskStateService.ApproveReviewAsync` is unchanged (still the sole Status writer;
|
||||
still runs `OnChildTerminalAsync`).
|
||||
|
||||
### 3. WorkerHub — signatures (`ClaudeDo.Worker`)
|
||||
|
||||
```csharp
|
||||
public record MergePreviewDto(string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount);
|
||||
|
||||
public Task<MergePreviewDto> PreviewMerge(string taskId, string targetBranch); // new
|
||||
public Task<MergeResultDto> ApproveReview(string taskId, string targetBranch); // CHANGED: was void(taskId)
|
||||
```
|
||||
|
||||
`ApproveReview` returns the orchestration result so the UI can react to conflicts.
|
||||
`MergeTask` / `GetMergeTargets` unchanged.
|
||||
|
||||
### 4. UI (`ClaudeDo.Ui`)
|
||||
|
||||
`IWorkerClient` (+ `WorkerClient` + **both test-project fakes** — see memory:
|
||||
changing `IWorkerClient` breaks hand-rolled fakes):
|
||||
- Change `Task ApproveReviewAsync(string)` → `Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch)`.
|
||||
- Add `Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch)`.
|
||||
- Add `Task<MergeResultDto> MergeTaskAsync(...)` to the **interface** (already on
|
||||
the concrete client) so the single-task Merge button can use `_worker`.
|
||||
|
||||
`DetailsIslandViewModel`:
|
||||
- **Load merge targets whenever a worktree exists.** In `BindAsync`, when
|
||||
`entity.Worktree != null` and the task is not a planning parent, call
|
||||
`GetMergeTargetsAsync(taskId)` and set `SelectedMergeTarget = DefaultBranch`
|
||||
(fixes the standalone-task gap where targets were never loaded).
|
||||
- **Mergeability indicator** properties: `MergePreviewText` (string),
|
||||
`MergeIsClean` / `MergeIsConflict` (bool, for color). Compute via
|
||||
`PreviewMergeAsync` when the merge section is shown for an **Active** worktree;
|
||||
recompute on `SelectedMergeTarget` change. If worktree state is
|
||||
`Merged/Discarded/Kept`, show that label instead of probing. Text examples:
|
||||
"Merges cleanly · 7 files" / "Conflicts in a.cs, b.cs" / "Mergeability unknown".
|
||||
- **Approve** (`ApproveReviewAsync`): pass `SelectedMergeTarget ?? ""`; inspect
|
||||
result — on `"conflict"` set the conflict indicator + a short notice
|
||||
("Approve blocked — resolve conflicts first"); success path relies on the
|
||||
existing `TaskUpdated` broadcast to refresh.
|
||||
- **Single-task Merge** (`MergeCommand`): `MergeTaskAsync(taskId,
|
||||
SelectedMergeTarget ?? "", removeWorktree:false, "Merge task")`; on `"conflict"`
|
||||
show the conflict indicator. Shown for non-planning tasks with an active
|
||||
worktree (planning parents keep "Merge All Subtasks").
|
||||
|
||||
`WorkConsole.axaml` (Session tab, `MERGE & WORKTREE` block):
|
||||
- Add a status line above the button row bound to `MergePreviewText`, colored
|
||||
green (`MossBrush`) when `MergeIsClean`, red (`BloodBrush`) when
|
||||
`MergeIsConflict`, muted otherwise. Use existing tokens/classes only.
|
||||
- Add a **Merge** button (`MergeCommand`) beside **Open Diff** for the
|
||||
single-task path.
|
||||
|
||||
## Testing (git-backed, no real Claude)
|
||||
|
||||
In `ClaudeDo.Worker.Tests` (real temp git repos + real SQLite), and/or
|
||||
`ClaudeDo.Data.Tests` for the pure git probe:
|
||||
- `GitService.PreviewMergeAsync`: clean branches → `Clean=true`; a real
|
||||
edit-conflict on the same lines → `Clean=false` with the expected file in
|
||||
`ConflictFiles`.
|
||||
- `ApproveAndMergeAsync`: clean worktree → returns `merged`, task is `Done`,
|
||||
worktree state `Merged`. Conflicting worktree → returns `conflict`, task still
|
||||
`WaitingForReview`, worktree still `Active`, target branch unmodified
|
||||
(HEAD unchanged, no `MERGE_HEAD`).
|
||||
- No-worktree `WaitingForReview` task → returns `merged`, task `Done`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
External difftools, new task statuses, auto-removing worktrees on approve,
|
||||
re-splitting the console into separate tabs, conflict resolution UI (the existing
|
||||
`ContinueMerge`/`AbortMerge` paths remain as-is for mid-merge cases).
|
||||
@@ -0,0 +1,236 @@
|
||||
# Bundled Prompts Overhaul — Design
|
||||
|
||||
Date: 2026-06-04
|
||||
|
||||
## Goal
|
||||
|
||||
Replace ClaudeDo's bundled prompts with a clean, professional baseline and make
|
||||
every prose prompt a user-editable file with a bundled default. Add a roadblock
|
||||
protocol so an autonomous run can flag problems mid-task without aborting.
|
||||
|
||||
The execution-side defaults (`system.md`) ship as a moderate, **project-agnostic**
|
||||
engineering baseline — ClaudeDo users run tasks against their *own* repos, so no
|
||||
ClaudeDo-specific rules belong there. Everything is in English (tighter
|
||||
tokenization, more reliable instruction-following); the only German output is the
|
||||
weekly report, which a human reads.
|
||||
|
||||
## File layout
|
||||
|
||||
All prompts live under `~/.todo-app/prompts/` as editable files with bundled
|
||||
defaults seeded by `PromptFiles.EnsureExists` (which never overwrites a file the
|
||||
user already has). The `system` + `agent` prompts collapse into one `system.md`;
|
||||
the old `agent`/manual distinction was removed when tags were retired.
|
||||
|
||||
| File | Replaces | Placeholders |
|
||||
|---|---|---|
|
||||
| `system.md` | system + agent (merged) | — |
|
||||
| `planning-system.md` | planning system prompt | — |
|
||||
| `planning-initial.md` | "analyze & break down" kickoff | `{title}`, `{description}` |
|
||||
| `retry.md` | "try again and fix" prompt | — |
|
||||
| `daily-prep.md` | daily-prep prompt | `{date}`, `{maxTasks}` |
|
||||
| `weekly-report.md` | weekly-report instructions | `{start}`, `{end}` |
|
||||
|
||||
The task-execution prompt (title + description + `## Sub-Tasks` checkboxes) stays
|
||||
assembled in code — it is data-shaped, not prose.
|
||||
|
||||
### Templating
|
||||
|
||||
`PromptFiles` gains `Render(PromptKind kind, IReadOnlyDictionary<string,string> values)`
|
||||
that replaces **only** the known named tokens for that kind. Any other `{...}` in
|
||||
the file (e.g. the literal `{Wochentag}` / `{dd.MM.yyyy}` in the German report
|
||||
rules) passes through untouched. Daily-prep tool names are inlined as literals —
|
||||
`--allowedTools` already carries the real names, and inlining keeps the file from
|
||||
silently breaking if a user edits a placeholder.
|
||||
|
||||
### Migration
|
||||
|
||||
`EnsureExists` keeps its current semantics: it seeds a default only when the file
|
||||
is missing, never overwriting user edits. The old `planning.md` and `agent.md`
|
||||
become inert — `TaskRunner` stops reading `agent.md`, and the planning system
|
||||
prompt now reads `planning-system.md`. Old files are harmless to leave or delete.
|
||||
`PromptKind` changes: `Agent` is removed; `Planning` maps to `planning-system.md`;
|
||||
new kinds `PlanningInitial`, `Retry`, `DailyPrep`, `WeeklyReport` are added.
|
||||
|
||||
## Roadblock protocol
|
||||
|
||||
An autonomous run has no human watching, so it must not silently stop or block on
|
||||
a question. Instead the agent emits an inline marker whenever it hits a true
|
||||
blocker, **any number of times**, and keeps working on whatever it still can.
|
||||
|
||||
- **Prompt side** (`system.md`): instruct the agent to write
|
||||
`CLAUDEDO_BLOCKED: <one short sentence>` on its own line whenever something
|
||||
genuinely prevents progress (missing credentials, contradictory requirements, a
|
||||
destructive action it won't take unasked) — then continue with the rest of the
|
||||
task. Reserved for true blockers, not routine decisions it can make itself.
|
||||
- **Detection** (`StreamAnalyzer`): as `assistant` messages stream, scan their
|
||||
text content for lines matching `^CLAUDEDO_BLOCKED:` and collect each reason
|
||||
into an ordered list (`Blocks`). This is live and cumulative — multiple problems
|
||||
across one run are all captured, not just the last.
|
||||
- **Result wiring** (`StreamResult` → `RunResult` → run record): carry the
|
||||
collected `Blocks`. Strip the marker lines from the displayed result text.
|
||||
- **Routing**: a run that finishes with blocks still goes to `WaitingForReview`
|
||||
(standalone tasks) — it is "done as far as the agent could get". The review card
|
||||
shows a ⚠ roadblock hint listing the collected problems. The user answers them
|
||||
via the existing reject-rerun feedback path, which resumes the session with the
|
||||
answers as the next-turn prompt — so the agent continues with the problems
|
||||
resolved rather than restarting.
|
||||
|
||||
## The prompts
|
||||
|
||||
### `system.md`
|
||||
```markdown
|
||||
# Working Agreement
|
||||
|
||||
You are completing one well-defined task autonomously in a git repository.
|
||||
|
||||
## Scope
|
||||
- Do exactly what the task asks — no unrequested refactors, renames, dependency
|
||||
changes, or "while I'm here" cleanup.
|
||||
- If intent is ambiguous, state the assumption you're making and proceed with the
|
||||
most reasonable reading. Stop only if you genuinely cannot move forward.
|
||||
- Prefer three similar lines over a premature abstraction. Don't build for
|
||||
hypothetical future needs.
|
||||
|
||||
## Working in the repo
|
||||
- Read a file before editing it. Match the conventions already in this codebase —
|
||||
they override generic defaults.
|
||||
- Prefer editing existing files to creating new ones. Don't write comments that
|
||||
just restate the code.
|
||||
- Validate only at real boundaries (user input, external APIs).
|
||||
|
||||
## Finishing
|
||||
- Before claiming done, verify: run the build and relevant tests, confirm they
|
||||
pass, and report what you ran. If you couldn't verify something, say so plainly.
|
||||
- Make focused commits using the repository's existing commit-message convention.
|
||||
|
||||
## Safety
|
||||
- Never force-push, hard-reset, or delete branches/files beyond the task's scope
|
||||
without being asked.
|
||||
- Don't introduce injection/XSS/secret-leak issues. Never commit credentials.
|
||||
|
||||
## You are running unattended
|
||||
You run autonomously with no human watching. There is no one to answer mid-task
|
||||
questions, so never stop to ask — make the most reasonable decision, note the
|
||||
assumption, and continue.
|
||||
|
||||
## When you are blocked
|
||||
If something genuinely prevents you from completing part of the task (missing
|
||||
credentials, contradictory requirements, a destructive action you won't take
|
||||
unasked), do NOT silently give up. Write this marker on its own line, then keep
|
||||
working on whatever else you can:
|
||||
|
||||
CLAUDEDO_BLOCKED: <one short sentence describing what blocked you>
|
||||
|
||||
Emit it as many times as needed — once per distinct blocker. Use it only for true
|
||||
blockers, not for routine decisions you can make yourself.
|
||||
```
|
||||
|
||||
> `system.md` also gains an **"Out-of-scope improvements"** section that tells the
|
||||
> agent to file follow-up work via the `SuggestImprovement` tool. That section is
|
||||
> defined in `2026-06-04-child-tasks-and-improvement-loop-design.md` and lands with
|
||||
> that feature.
|
||||
|
||||
### `planning-system.md`
|
||||
```markdown
|
||||
You are the planning assistant for ClaudeDo. Your job is to break a task into
|
||||
smaller, independently executable subtasks — the session ends by creating those
|
||||
subtasks.
|
||||
|
||||
Start every session by invoking the `superpowers:brainstorming` skill (Skill
|
||||
tool) and follow it end to end: clarifying questions one at a time, then 2–3
|
||||
approaches with a recommendation, then a short design. Do not create any subtasks
|
||||
until the user has approved the design.
|
||||
|
||||
You can ONLY shape this task's plan — you cannot edit files or touch other tasks.
|
||||
The tools available to you are: CreateChildTask, ListChildTasks, UpdateChildTask,
|
||||
DeleteChildTask, UpdatePlanningTask, and Finalize. Use nothing else.
|
||||
|
||||
Once the design is approved, create the child tasks with CreateChildTask, then
|
||||
call Finalize. Keep each subtask concrete and self-contained with a clear
|
||||
done-state, ordered so dependencies come first.
|
||||
```
|
||||
|
||||
### `planning-initial.md`
|
||||
```markdown
|
||||
# Task to plan: {title}
|
||||
|
||||
{description}
|
||||
```
|
||||
|
||||
### `retry.md`
|
||||
```markdown
|
||||
The task did not complete on the previous attempt — you may have run out of
|
||||
turns, hit an error, or stopped before finishing.
|
||||
|
||||
Review the work already done in this session and the current state of the
|
||||
repository, identify what is still incomplete or broken, and finish the task.
|
||||
Don't restart from scratch or repeat a failed approach. Verify the result
|
||||
(build + tests) before you stop.
|
||||
```
|
||||
Self-contained — no error injection. The runner appends the captured process
|
||||
output **only when it is a genuine error** (i.e. not the generic
|
||||
`"Claude exited with code N and no result."` fallback), since real session errors
|
||||
are already in the resumed context.
|
||||
|
||||
### `daily-prep.md`
|
||||
```markdown
|
||||
You are preparing my workday for {date}.
|
||||
|
||||
1. Call mcp__claudedo__get_daily_prep_candidates.
|
||||
2. Keep tasks already marked MyDay (currentMyDay) — never remove them.
|
||||
3. Fill MyDay to at most {maxTasks} open tasks TOTAL (currentMyDay counts). Never exceed it.
|
||||
4. Estimate each candidate's effort and pick a feasible mix — not only big items.
|
||||
Prioritize isStarred, due (scheduledFor), and older tasks.
|
||||
5. Place related tasks next to each other using consecutive sortOrder values.
|
||||
6. Apply via mcp__claudedo__set_my_day(taskId, true, sortOrder). Never mark anything
|
||||
outside the candidate list.
|
||||
|
||||
If there are no candidates, do nothing.
|
||||
```
|
||||
|
||||
### `weekly-report.md`
|
||||
```markdown
|
||||
You are generating a concise weekly standup report for a software developer,
|
||||
covering {start} to {end}.
|
||||
|
||||
Rules:
|
||||
- Write the ENTIRE report in German.
|
||||
- Group by day. One "## {Wochentag}, {dd.MM.yyyy}" section per day that has
|
||||
activity (German weekday names). Omit days with no activity.
|
||||
- Within each day: 3–5 first-person, past-tense bullets ("- Habe X umgesetzt",
|
||||
"- Y behoben"). Merge related small work into one bullet.
|
||||
- Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise.
|
||||
- Blend the developer's own notes and the derived activity into ONE deduplicated
|
||||
bullet list per day. The notes are authoritative — never omit or contradict them.
|
||||
- Name the project/repo when it adds clarity.
|
||||
- Output ONLY the dated sections. No preamble, no intro, no closing remarks.
|
||||
|
||||
Two sections follow below: an activity log derived from Claude session history,
|
||||
and the developer's own notes. Base the report on both; the notes are
|
||||
authoritative where they conflict with the derived activity.
|
||||
```
|
||||
|
||||
## Touch points
|
||||
|
||||
- `src/ClaudeDo.Data/PromptFiles.cs` — new `PromptKind` members, new defaults,
|
||||
`Render` helper.
|
||||
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — stop reading `agent.md`; use
|
||||
`retry.md`; conditional stderr append on retry; carry/route `Blocks`.
|
||||
- `src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs` — scan assistant text for
|
||||
`CLAUDEDO_BLOCKED:` markers, collect `Blocks`, strip from result.
|
||||
- `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs` / `RunResult` — carry `Blocks`.
|
||||
- `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` — read
|
||||
`planning-system.md` and `planning-initial.md` via `PromptFiles.Render`.
|
||||
- `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` — read `daily-prep.md`.
|
||||
- `src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs` — read `weekly-report.md`.
|
||||
- UI — review card shows the ⚠ roadblock hint with collected problems.
|
||||
- `src/ClaudeDo.Ui/.../FilesSettingsTabViewModel.cs` — expose the new prompt files.
|
||||
- Tests — `PromptFiles` render/seed; `StreamAnalyzer` marker collection; planning/
|
||||
prep/report builders read from files.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The in-code task-execution assembly (title/description/subtasks) is unchanged.
|
||||
- `ResultSchema` / `--output-schema` remains untouched.
|
||||
- No change to commit-message templating.
|
||||
```
|
||||
@@ -0,0 +1,186 @@
|
||||
# Reusable Child Tasks + Agent Improvement Loop — Design
|
||||
|
||||
Date: 2026-06-04
|
||||
|
||||
## Goal
|
||||
|
||||
Let an executing task agent offload out-of-scope improvements it spots into
|
||||
**child tasks** that run automatically, so ClaudeDo can drive a self-improvement
|
||||
loop. Generalize the parent/child machinery that planning uses today into a
|
||||
reusable subsystem not bound to planning.
|
||||
|
||||
Example: while implementing task X, Claude notices "this module should really be
|
||||
refactored, but that's out of scope" — instead of scope-creeping, it calls a tool
|
||||
that files the refactor as a child of X. The child runs on its own; once all of
|
||||
X's children finish, X surfaces for review with its whole tree visible.
|
||||
|
||||
This builds on the bundled-prompts overhaul (`system.md` gains one instruction to
|
||||
use the offload tool). It is otherwise independent.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
A new task status `WaitingForChildren` is added.
|
||||
|
||||
```
|
||||
Running → WaitingForReview standalone success, no children (existing)
|
||||
Running → WaitingForChildren standalone success, ≥1 child (new)
|
||||
Running → Done planning child success (existing)
|
||||
WaitingForChildren → WaitingForReview all children terminal (new)
|
||||
WaitingForChildren → Cancelled cancel (new)
|
||||
```
|
||||
|
||||
- Improvement-children are created `Idle` **during** the parent's run and stay
|
||||
unqueued until the parent's own run finishes — this avoids the parent and a
|
||||
child working the same repo concurrently.
|
||||
- When the parent's run succeeds and it has ≥1 non-terminal child, the parent goes
|
||||
to `WaitingForChildren` and its children are enqueued (they then run under the
|
||||
normal queue, governed by max-parallel — they are independent, not a forced
|
||||
sequential chain like planning).
|
||||
- Children run automatically and reach `Done` on success without their own review
|
||||
gate (a per-child review would stall the loop). Each child still produces its
|
||||
own worktree/commit; those worktrees are surfaced under the parent for merge.
|
||||
- Children emit `CLAUDEDO_BLOCKED:` markers like any run (see the prompt-overhaul
|
||||
spec). Each child's collected problems roll up onto the **parent's** review card,
|
||||
so a parent in `WaitingForReview` shows "child N reported a problem" alongside
|
||||
its own roadblocks.
|
||||
|
||||
## Worktree topology & merge
|
||||
|
||||
The correctness rule that makes this work:
|
||||
|
||||
- **Children base off the parent's worktree HEAD, not the list's base branch.**
|
||||
The parent's code work lives only on `claudedo/{parentId}` until merged, so a
|
||||
child refactoring code the parent just wrote must branch from the parent's HEAD
|
||||
to see it. (Planning children base off the target branch because a planning
|
||||
parent writes no code — improvement parents do, hence the difference.) The
|
||||
per-run worktree setup takes the base commit from the parent task's recorded
|
||||
worktree HEAD when `ParentTaskId` is set and the parent is a non-planning task.
|
||||
- **Fan-out:** all children branch off the same parent HEAD and run independently
|
||||
(parallel allowed). Parent-dependency is always satisfied; sibling overlaps
|
||||
surface later as merge conflicts.
|
||||
- **Merge reuses the planning orchestrator,** generalized into a shared
|
||||
"tree merge": build an integration branch off the target, then sequentially
|
||||
`merge --no-ff` the **parent's own branch** followed by each child branch,
|
||||
pausing on conflict (continue / abort), exactly as `PlanningMergeOrchestrator`
|
||||
/`PlanningAggregator` do today. Approving the parent triggers this one guided
|
||||
flow, merging parent + all children in as few steps as possible. Because
|
||||
children descend from the parent HEAD, the parent's commits are shared ancestors
|
||||
and merge cleanly ahead of the children.
|
||||
- The parent advances to `WaitingForReview` once **all** children are terminal —
|
||||
counting `Done`, `Failed`, and `Cancelled`, so a failed child can't wedge the
|
||||
parent forever. Failed/cancelled children are flagged on the review card.
|
||||
|
||||
Planning parents keep their existing behavior (parent → `Done` when its chain
|
||||
finishes); they do not use `WaitingForChildren`.
|
||||
|
||||
## Consolidating the child subsystem
|
||||
|
||||
Today child handling is planning-coupled. Generalize:
|
||||
|
||||
- **`TaskRepository.CreateChildAsync`** — drop the `parent.PlanningPhase != None`
|
||||
guard. A child can attach to any existing parent. (Planning callers are
|
||||
unaffected; their parents have a planning phase.) The child sets
|
||||
`ParentTaskId = parentId`; the caller decides `CreatedBy`.
|
||||
- **Child-completion coordinator** — generalize planning's
|
||||
`OnChildFinishedAsync` / `TryCompleteParentAsync` into a single component that,
|
||||
on any child reaching a terminal state, checks the parent and applies a
|
||||
**completion policy**:
|
||||
- *planning parent* → finalize/Done (existing chain advancement stays in the
|
||||
planning layer: unblock the next chained child).
|
||||
- *improvement parent* (in `WaitingForChildren`, all children terminal) →
|
||||
`WaitingForReview`.
|
||||
- `TaskStateService` remains the sole writer of `Status` and owns the new
|
||||
transitions (`SubmitForChildrenAsync`, the `WaitingForChildren → WaitingForReview`
|
||||
advance).
|
||||
|
||||
## The offload tool
|
||||
|
||||
A narrow MCP tool exposed only to task runs (not the general external surface):
|
||||
|
||||
```
|
||||
SuggestImprovement(title, description) → { childTaskId }
|
||||
```
|
||||
|
||||
- The **server** stamps everything — the agent cannot choose the parent, the
|
||||
status, or queue anything directly:
|
||||
- `ParentTaskId = <calling task id>`
|
||||
- `CreatedBy = <calling task id>` (unambiguous "agent-suggested improvement"
|
||||
marker — distinct from `null` user/planning tasks and `"mcp"` external tasks)
|
||||
- `Status = Idle`, same `ListId` as the parent.
|
||||
- **One layer deep:** the tool rejects the call if the calling task already has a
|
||||
`ParentTaskId` (a child cannot spawn children).
|
||||
|
||||
### Knowing the caller's identity
|
||||
|
||||
The always-on external `claudedo` MCP is shared and can't tell which task is
|
||||
calling. So task runs get a **per-run MCP identity**, mirroring planning's
|
||||
per-session token:
|
||||
|
||||
- `TaskRunner` mints a per-run token and writes a run-scoped `.mcp.json` (or
|
||||
reuses the global server with a token header) so the offload tool resolves
|
||||
token → calling task id server-side. A `TaskRunMcpContextAccessor` exposes the
|
||||
current task id to the tool, the same way `PlanningMcpContextAccessor` does.
|
||||
- This is the reliable path for both correct provenance and the one-layer-deep
|
||||
guard — the id is never supplied by the model.
|
||||
|
||||
`system.md` gains a short instruction (from the prompt-overhaul spec):
|
||||
|
||||
```markdown
|
||||
## Out-of-scope improvements
|
||||
If you notice worthwhile work that is genuinely outside this task's scope
|
||||
(a refactor, a follow-up, tech debt), do NOT do it here. File it with
|
||||
SuggestImprovement(title, description) and stay focused on the task at hand.
|
||||
```
|
||||
|
||||
## UI
|
||||
|
||||
- **Collapsible tree:** children group under their parent (by `ParentTaskId`).
|
||||
Improvement-children are visually marked as agent-suggested (via
|
||||
`CreatedBy == parentId`).
|
||||
- **New status chip** for `WaitingForChildren` (e.g. amber "waiting on N
|
||||
improvements") with its own color in `StatusColorConverter`.
|
||||
- **Review card** for a parent in `WaitingForReview` lists child outcomes
|
||||
(done/failed) and their rolled-up `CLAUDEDO_BLOCKED` problems, and drives the
|
||||
shared tree-merge (parent + children) via the planning-style sequential flow
|
||||
with conflict pause/continue/abort.
|
||||
|
||||
## Data / migration
|
||||
|
||||
- Add `WaitingForChildren` to the `TaskStatus` enum and its EF `ValueConverter`.
|
||||
No new columns — `ParentTaskId` and `CreatedBy` already exist. No backfill
|
||||
needed (no existing rows use the new value).
|
||||
|
||||
## Touch points
|
||||
|
||||
- `src/ClaudeDo.Data/Models/TaskStatus` (enum) + `TaskEntityConfiguration` — new value.
|
||||
- `src/ClaudeDo.Data/Repositories/TaskRepository.cs` — generalize `CreateChildAsync`.
|
||||
- `src/ClaudeDo.Worker/State/TaskStateService.cs` — `WaitingForChildren` transitions.
|
||||
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — route to `WaitingForChildren` when
|
||||
children exist; enqueue children on parent finish; mint per-run MCP token.
|
||||
- New: child-completion coordinator (generalized from planning) + the offload tool
|
||||
(e.g. `TaskRunMcpService.SuggestImprovement`) + `TaskRunMcpContextAccessor` +
|
||||
token auth (mirrors `PlanningTokenAuth`).
|
||||
- `src/ClaudeDo.Worker/Planning/*` — refactor planning to consume the shared
|
||||
child-completion coordinator and the shared tree-merge; keep chain-specific
|
||||
advancement local. Generalize `PlanningMergeOrchestrator` / `PlanningAggregator`
|
||||
into a reusable tree-merge that also folds in the parent's own branch.
|
||||
- Worktree setup (`TaskRunner` / `WorktreeManager`) — base an improvement-child's
|
||||
worktree on the parent task's recorded worktree HEAD instead of the list base.
|
||||
- UI — tree grouping, `WaitingForChildren` chip/color, parent review card with
|
||||
child outcomes + rolled-up roadblocks + the merge flow.
|
||||
- Tests — offload tool stamps parent/createdBy + rejects nested calls;
|
||||
parent → `WaitingForChildren` → `WaitingForReview` lifecycle; child worktree
|
||||
bases off parent HEAD; tree-merge folds parent + children; planning regression
|
||||
(still reaches Done).
|
||||
|
||||
## Open questions for review
|
||||
|
||||
1. **Failed child:** parent still advances to `WaitingForReview` with the failure
|
||||
flagged (default), vs. parent → `Failed` if any child failed.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Multi-level nesting (only one layer deep by design).
|
||||
- Per-list "disable improvement offload" toggle (could come later; the tool is
|
||||
always available to top-level runs for now).
|
||||
- Changes to how planning sets up its sequential chain.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Debug Logging & Frontend↔Backend Traceability — Design
|
||||
|
||||
**Date:** 2026-06-04
|
||||
**Status:** Approved (pending spec review)
|
||||
|
||||
## Goal
|
||||
|
||||
Make debug logging rich enough to diagnose problems across the UI↔Worker boundary, while keeping the installed (production) build near-silent. Verbosity is decided by **build configuration, detected at runtime** — no runtime knob, no config field, no `#if DEBUG`:
|
||||
|
||||
- **Debug build** (Rider run button) → verbose, console + file.
|
||||
- **Release build** (installed app) → minimal, file only.
|
||||
|
||||
## Decisions (from brainstorming)
|
||||
|
||||
1. **Mechanism:** runtime build-config detection via the entry assembly's `DebuggableAttribute` (JIT optimizer disabled ⇒ Debug build). A single `BuildConfig.IsDebug` helper drives ordinary `if` branching — no `#if DEBUG` directives. Rider's run button builds `Debug`; the installer ships `-c Release`.
|
||||
2. **Scope:** Worker **and** App/Ui. The desktop side currently has no log sink at all — UI/IPC failures vanish today.
|
||||
3. **Release behavior:** all three log `Warning`+ to file (not silent — capture crashes). Worker drops from its current `Information` to `Warning`.
|
||||
4. **One shared log file** across both processes, unified timeline.
|
||||
5. **Correlation:** TaskId-based (option A). Enrich log lines with `TaskId` when one is in scope. No changes to the SignalR contract (`IWorkerClient`/`WorkerHub` untouched → test fakes untouched).
|
||||
|
||||
## Verbosity matrix
|
||||
|
||||
| Process | Debug build | Release build |
|
||||
|---|---|---|
|
||||
| Worker | `Debug` level, console + shared file | `Warning` level, shared file |
|
||||
| App/Ui | `Debug` level, console + shared file | `Warning` level, shared file |
|
||||
|
||||
## Shared log file
|
||||
|
||||
- Single daily-rolling file: `~/.todo-app/logs/claudedo-.log` (Serilog appends the date).
|
||||
- `shared: true` on both processes' file sinks → Serilog coordinates multi-process writes via a global mutex.
|
||||
- `retainedFileCountLimit: 2`.
|
||||
- Each line is tagged with a `Process` property (`"worker"` / `"app"`) so the two sides are distinguishable in the interleaved timeline.
|
||||
|
||||
> The existing `worker-.log` is replaced by `claudedo-.log`. Task-run NDJSON (`{taskId}_run{n}.ndjson`) and `daily-prep.log` are **out of scope** — they are data streams, not diagnostic logs, and stay exactly as they are.
|
||||
|
||||
## Output template
|
||||
|
||||
```
|
||||
[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Process}/{SourceContext} [{TaskId}] {Message:lj}{NewLine}{Exception}
|
||||
```
|
||||
|
||||
- `{Process}` — `worker` or `app`.
|
||||
- `{SourceContext}` — the `ILogger<T>` category (the logging class), so you see *which* component spoke.
|
||||
- `{TaskId}` — the correlation key, defaulted to `-` when no task is in scope (see enricher below).
|
||||
|
||||
## Traceability (TaskId correlation)
|
||||
|
||||
Use Serilog's `LogContext` (`.Enrich.FromLogContext()` on both processes) plus a small default enricher so `TaskId` is always present (renders `-` when absent — avoids the raw `{TaskId}` token leaking into output).
|
||||
|
||||
Push the property at the entry points where a task is in scope; all nested `ILogger<T>` calls inherit it automatically:
|
||||
|
||||
- **Worker:** wrap per-task execution in `TaskRunner` (the run/continue entry) with `using (LogContext.PushProperty("TaskId", task.Id))`. This covers the bulk of backend activity (runner, state transitions, worktree, planning) for free.
|
||||
- **App/Ui:** push `TaskId` in `WorkerClient` task-targeted calls (e.g. RunNow / Cancel / Continue / review actions) so the UI side of a task action carries the same key.
|
||||
|
||||
Result: grep one `TaskId` in `claudedo-.log` and read the full UI→Worker→UI story in timestamp order.
|
||||
|
||||
This adds **no parameters** to the SignalR surface — correlation rides on the existing `taskId` arguments already present in those calls.
|
||||
|
||||
## Implementation surface
|
||||
|
||||
A single shared helper keeps the two processes' Serilog setup from drifting.
|
||||
|
||||
- **New project:** `ClaudeDo.Logging` — a small library both `ClaudeDo.App` and `ClaudeDo.Worker` reference (keeps `ClaudeDo.Data` free of any Serilog dependency). Contains:
|
||||
- `BuildConfig.IsDebug` — checks the entry assembly's `DebuggableAttribute` (`IsJITOptimizerDisabled` ⇒ Debug build). Cached static.
|
||||
- The output template and the default-TaskId enricher.
|
||||
- `ConfigureLogger(LoggerConfiguration, processTag, logRoot)` — applies level/sink choices by branching on `BuildConfig.IsDebug` (Debug ⇒ `Debug` level + console + file; Release ⇒ `Warning` level + file only). Both processes call it so level/template/retention stay in sync.
|
||||
- **Worker `Program.cs:34`:** replace the inline `UseSerilog` body with a call into the shared helper (`processTag = "worker"`).
|
||||
- **App `Program.cs`:** add Serilog packages; build a logger via the shared helper (`Process = "app"`) and register it with `sc.AddLogging(b => b.AddSerilog(logger, dispose: true))`. App currently registers **no** logging at all, so this also makes `ILogger<T>` injection actually work UI-side. Remove/keep `.LogToTrace()` as appropriate (Avalonia internal trace, separate concern — leave it).
|
||||
- **App shutdown:** flush/close the logger (`Log.CloseAndFlush()` or dispose via the container's existing `finally`).
|
||||
|
||||
### Packages to add (App project)
|
||||
|
||||
- `Serilog.Extensions.Logging` (bridge `ILogger` → Serilog)
|
||||
- `Serilog.Sinks.File`
|
||||
- `Serilog.Sinks.Console`
|
||||
- (Worker already has Serilog + File sink; add `Serilog.Sinks.Console` for the Debug console output.)
|
||||
|
||||
## Testing
|
||||
|
||||
- This is logging wiring; per project policy, no tests that spawn the real Claude CLI and no heavy test scaffolding for log output.
|
||||
- Light verification: a unit-level check that the default enricher yields `-` when no `TaskId` is pushed, and (if practical) that `ConfigureLogger` wires the expected sinks. `BuildConfig.IsDebug` reflects the test assembly's own build config, so it can't be flipped within one run — assert each branch by passing the level/flag explicitly rather than relying on the ambient value, or verify the Release path and smoke-test Debug manually from Rider.
|
||||
- Manual smoke test (documented, not automated): run from Rider, confirm console + `claudedo-.log` show `Debug` lines with `Process`/`SourceContext`; run a task and confirm both `app` and `worker` lines share the same `[TaskId]`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Runtime/config log-level knob.
|
||||
- Per-call correlation IDs for non-task flows (connect, config edits, prep) — TaskId-only for now; revisit if a non-task flow proves to be a black hole.
|
||||
- Changes to task-run NDJSON capture or `daily-prep.log`.
|
||||
- Any change to `IWorkerClient` / `WorkerHub` signatures.
|
||||
@@ -0,0 +1,154 @@
|
||||
# Inherited settings display, per-task overrides, and Turns
|
||||
|
||||
**Date:** 2026-06-04
|
||||
**Status:** Approved (design)
|
||||
|
||||
## Problem
|
||||
|
||||
Config inheritance is three-tier (Task → List → Global app settings). Today the UI
|
||||
only signals inheritance with a placeholder sentinel (`(inherit)` for tasks,
|
||||
`(default)` for lists) and, for tasks, a faint "Effective if inherited: {value}"
|
||||
hint under Model and Agent. Two gaps:
|
||||
|
||||
1. You can't see the *actual resolved value* an inherited field will use, nor where
|
||||
it comes from (List vs Global).
|
||||
2. **Max turns** is global-only (`AppSettingsEntity.DefaultMaxTurns` = 100). It is not
|
||||
overridable per list or per task, unlike Model / SystemPrompt / AgentPath.
|
||||
|
||||
## Goals
|
||||
|
||||
- Show the real inherited value in-place, muted, with a **source-aware marker**
|
||||
(`inherited · List` vs `inherited · Global`). Picking a value turns it into an
|
||||
override; a reset affordance clears it back to inherited.
|
||||
- Add **Turns** (max turns) as an overridable field at both List and Task levels,
|
||||
inheriting from the global default. Numeric box; empty = inherit.
|
||||
- Keep SystemPrompt as-is (it is additive, not override) but show what gets prepended.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No change to SystemPrompt merge semantics (stays additive/concatenated).
|
||||
- No new global settings; `DefaultMaxTurns` already exists.
|
||||
- No change to PermissionMode handling.
|
||||
|
||||
## Inheritance semantics (reference)
|
||||
|
||||
Resolved in `TaskRunner.BuildRunConfig` (~line 388):
|
||||
|
||||
| Field | Semantics | Resolution |
|
||||
|--------------|------------|--------------------------------------------------------|
|
||||
| Model | override | `task.Model ?? listConfig?.Model ?? global.DefaultModel` |
|
||||
| AgentPath | override | `task.AgentPath ?? listConfig?.AgentPath` (no global) |
|
||||
| MaxTurns | override | **new:** `task.MaxTurns ?? listConfig?.MaxTurns ?? global.DefaultMaxTurns` |
|
||||
| SystemPrompt | additive | merged: global + list + task + agent (unchanged) |
|
||||
|
||||
Lists inherit only from Global (no tier above them), so a list's inherited marker is
|
||||
always `inherited · Global`.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. Data layer
|
||||
|
||||
- `ListConfigEntity`: add `int? MaxTurns`.
|
||||
- `TaskEntity`: add `int? MaxTurns` (nullable override).
|
||||
- EF Core migration adding `max_turns` column to `list_config` and `tasks`
|
||||
(nullable, no default — null = inherit).
|
||||
- `TaskRunner` BuildRunConfig: `MaxTurns: task.MaxTurns ?? listConfig?.MaxTurns ?? global.DefaultMaxTurns`.
|
||||
`ClaudeRunConfig.MaxTurns` and `ClaudeArgsBuilder` already accept/emit `--max-turns`
|
||||
when `> 0` — no change needed there.
|
||||
- `ListRepository.SetConfigAsync` (upsert) and `TaskRepository.UpdateAgentSettingsAsync`
|
||||
extend to carry `maxTurns`.
|
||||
|
||||
### 2. DTOs / transport
|
||||
|
||||
Add `int? MaxTurns` to (Worker + Ui copies kept in sync):
|
||||
|
||||
- `UpdateListConfigDto`, `ListConfigDto` (WorkerHub.cs + WorkerClient.cs)
|
||||
- `UpdateTaskAgentSettingsDto` (WorkerHub.cs + WorkerClient.cs)
|
||||
- `TaskConfigDto` (ConfigMcpTools.cs)
|
||||
|
||||
`WorkerHub.UpdateListConfig` / `UpdateTaskAgentSettings` persist the new field via the
|
||||
repositories above. MCP `SetListConfig` / `SetTaskConfig` gain an optional `maxTurns`
|
||||
parameter to keep the agent-facing API at parity with the UI.
|
||||
|
||||
### 3. Resolution helper (Ui)
|
||||
|
||||
A small helper that, given `(taskValue, listValue, globalValue)`, returns
|
||||
`(effectiveValue, source)` where `source ∈ { Override, List, Global }`. Drives the
|
||||
marker text and muted/normal styling for Model, Agent, and Turns so the logic isn't
|
||||
duplicated per field or per editor. Lives in the Ui layer beside its consumers.
|
||||
|
||||
### 4. UI rendering — inherited marker (source-aware)
|
||||
|
||||
For **Model**, **Agent**, **Turns** in both `ListSettingsModalView` and the
|
||||
DetailsIsland "Agent settings (overrides)" expander:
|
||||
|
||||
- Remove the `(inherit)` / `(default)` sentinel *row* from the control's item source.
|
||||
- When no override is set: control shows the **resolved value muted/greyed** (dropdown
|
||||
shows e.g. "sonnet" dimmed; Turns box shows e.g. "100" as a muted placeholder), and a
|
||||
small badge beside the field label reads `inherited · List` or `inherited · Global`.
|
||||
- On picking a value / typing a number: it becomes an override — text returns to normal
|
||||
color, the badge flips to `override` (or hides), and a small **"↺ reset to inherited"**
|
||||
affordance appears that clears the value back to null.
|
||||
- List modal: source is always Global → badge reads `inherited · Global`; reset clears
|
||||
to the global default.
|
||||
- Turns: numeric box, empty = inherit (muted resolved number as placeholder); a typed
|
||||
number is the override.
|
||||
|
||||
**Rendering approach:** a small reusable `InheritedFieldHeader` control (label + badge +
|
||||
reset button), fed by the resolution helper's `source`, wraps each field. Keeps the three
|
||||
fields consistent and avoids per-field XAML duplication. Badge / muted styling uses
|
||||
existing design tokens. Visual polish pass is the user's.
|
||||
|
||||
### 5. SystemPrompt (stays plain)
|
||||
|
||||
SystemPrompt keeps its plain multi-line text box (additive, not override). Below it, a
|
||||
small **read-only, collapsed-by-default** hint shows the inherited prompts that will be
|
||||
prepended (global + list), labeled e.g. "Prepended automatically:". No marker, no reset —
|
||||
it never replaces, only appends.
|
||||
|
||||
### 6. Localization
|
||||
|
||||
New keys in `locales/en.json` + `locales/de.json` (parity enforced by Localization.Tests):
|
||||
marker text (`inherited · List`, `inherited · Global`, `override`), reset affordance
|
||||
label, Turns field label, and the SystemPrompt "prepended automatically" hint. Retire the
|
||||
now-unused `vm.details.effectiveIfInherited` key (and its German counterpart) if nothing
|
||||
else references it.
|
||||
|
||||
## Affected files (indicative)
|
||||
|
||||
- `src/ClaudeDo.Data/Models/ListConfigEntity.cs`, `TaskEntity.cs`
|
||||
- `src/ClaudeDo.Data/Migrations/` (new migration)
|
||||
- `src/ClaudeDo.Data/Repositories/ListRepository.cs`, `TaskRepository.cs`
|
||||
- `src/ClaudeDo.Data/Configuration/` (column mapping for `max_turns`)
|
||||
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs`
|
||||
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||
- `src/ClaudeDo.Worker/External/ConfigMcpTools.cs`
|
||||
- `src/ClaudeDo.Ui/Services/WorkerClient.cs` (+ `Interfaces/IWorkerClient.cs`)
|
||||
- `src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs` + view
|
||||
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` + `DetailsIslandView.axaml`
|
||||
- `src/ClaudeDo.Ui/Views/Controls/` (new `InheritedFieldHeader`)
|
||||
- `src/ClaudeDo.Ui/` resolution helper
|
||||
- `locales/en.json`, `locales/de.json`
|
||||
|
||||
## Testing
|
||||
|
||||
- Data: migration applies; `MaxTurns` round-trips through `ListRepository.SetConfigAsync`
|
||||
and `TaskRepository.UpdateAgentSettingsAsync`.
|
||||
- Worker: `BuildRunConfig` resolves MaxTurns via task → list → global precedence
|
||||
(unit test on the resolution). Existing `ClaudeArgsBuilder` `--max-turns` behavior
|
||||
unchanged.
|
||||
- Ui: resolution helper returns correct `(value, source)` for each of the
|
||||
override / list / global cases across Model, Agent, Turns.
|
||||
- Localization: en/de key parity (existing Localization.Tests).
|
||||
- Test fakes: update hand-rolled `IWorkerClient` fakes in both test projects for the new
|
||||
DTO fields (per known gotcha).
|
||||
- Visual verification of the marker / muted styling: flagged for the user (cannot be
|
||||
asserted programmatically).
|
||||
|
||||
## Open risks
|
||||
|
||||
- DTO/ctor changes ripple into hand-rolled test fakes in Worker.Tests and Ui.Tests —
|
||||
must be updated in the same change.
|
||||
- Removing the sentinel row from dropdowns changes selection binding; ensure null/empty
|
||||
override state is represented without a sentinel item (e.g. dropdown `SelectedItem`
|
||||
null when inherited).
|
||||
200
docs/superpowers/specs/2026-06-04-marketing-website-design.md
Normal file
200
docs/superpowers/specs/2026-06-04-marketing-website-design.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# ClaudeDo distribution website — design
|
||||
|
||||
**Date:** 2026-06-04
|
||||
**Status:** Approved (design), ready for implementation planning
|
||||
**Repo:** new standalone repo `claudedo-web` (not part of the ClaudeDo app solution)
|
||||
**Domain:** `claudedo.kuns.dev` (Coolify on the user's VPS)
|
||||
|
||||
## Purpose
|
||||
|
||||
Give friends a public place to download ClaudeDo and learn what it does, without
|
||||
sending them to the Gitea repo — so the source repo can be made more private. The
|
||||
site also fronts the app's self-updater so the Gitea URL is never exposed in the
|
||||
app or on the page.
|
||||
|
||||
## Goals / non-goals
|
||||
|
||||
**Goals**
|
||||
- Public, no-auth landing page at `claudedo.kuns.dev` that matches the app's visual identity.
|
||||
- A primary download (installer `.exe`) plus the portable `.zip` and checksums.
|
||||
- A release proxy that (a) feeds the page the current version and (b) serves the
|
||||
app's self-updater the same JSON shape Gitea returns, with download URLs rewritten
|
||||
to route through `claudedo.kuns.dev` — hiding Gitea entirely.
|
||||
|
||||
**Non-goals**
|
||||
- No docs site / getting-started page (the app ships an installer that handles setup).
|
||||
- No changelog page (release notes already live on Gitea releases).
|
||||
- No auth, accounts, analytics, or CMS.
|
||||
- No CI/PR tooling for this repo beyond what Coolify needs to deploy.
|
||||
|
||||
## Access & distribution decisions
|
||||
|
||||
- **Access:** fully public. No password/login. Relies on the unadvertised URL.
|
||||
- **Download source:** build-time fetch of the latest Gitea release for the displayed
|
||||
version; actual download links route through the proxy and resolve the latest asset
|
||||
at request time (so a stale page still downloads the current build).
|
||||
- **Self-updater proxy:** in scope for v1 (not deferred).
|
||||
|
||||
## Tech stack
|
||||
|
||||
- **Nuxt 3** (Vue 3) — single framework, single repo, single Coolify deploy.
|
||||
- **Nitro** server routes for the release proxy + asset streaming.
|
||||
- **No DB, no auth, no secrets** (the `releases/ClaudeDo` repo is public).
|
||||
- Fonts: **Inter Tight** (display/body) + **JetBrains Mono** (mono), self-hosted or via
|
||||
Google Fonts. Design tokens ported from `docs/UI Rewrite/design_handoff_claudedo/Tokens.axaml`
|
||||
and `styles.css` (moss/sage/peat palette, dark-first, 14px island radius, grain texture).
|
||||
|
||||
## Concept: "the page IS the app"
|
||||
|
||||
The landing page is a faithful, in-browser rendering of the ClaudeDo desktop — the
|
||||
window chrome + the three islands — rather than a conventional marketing page. This
|
||||
is the chosen direction (over a cinematic single-window scroll and a worklog feed).
|
||||
|
||||
### Layout — three islands on the app "desktop"
|
||||
|
||||
Desktop background = the app's layered moss gradients + 3px grain overlay. A centered
|
||||
app `window` (titlebar + body) holds a 3-column island grid:
|
||||
|
||||
1. **Lists island (left)** — repurposed as page nav.
|
||||
- Header "Lists" + a decorative search box (`Ctrl K` kbd chip).
|
||||
- "Pages" group: Overview · Features (6) · How it works · Screenshots (3) · Download,
|
||||
each with a colored swatch dot; active item gets the accent left-bar.
|
||||
- Footer styled like the app's user footer: avatar, "For friends", `claudedo.kuns.dev`.
|
||||
|
||||
2. **Tasks island (middle)** — features rendered as **task cards**.
|
||||
- Header: date eyebrow, big title (the hero line "Queue the work. Claude does it."),
|
||||
a `running · review` badge, eye/gear icon buttons, and a subtitle.
|
||||
- A decorative "Add a task…" row.
|
||||
- Six **feature cards** (circle check — done cards filled; title; a status chip
|
||||
`done`/`running`/`waiting for review`; a star). The features:
|
||||
1. Isolated worktrees
|
||||
2. The task queue
|
||||
3. Review & merge
|
||||
4. Live session log
|
||||
5. Per-list & per-task config
|
||||
6. Self-updating
|
||||
- A "Ready" group with the final **"↓ Download ClaudeDo"** card.
|
||||
|
||||
3. **Detail island (right)** — faithful to the reworked Task-Detail island.
|
||||
- **Source of truth for the detail visuals:** `docs/superpowers/specs/2026-06-04-task-detail-redesign-design.md`
|
||||
(the app's in-progress rework). The website's detail pane must track that design.
|
||||
- Three-zone stack:
|
||||
- **Task header** — mono id (`#F01…`), title, trash/gear action icons.
|
||||
- **DETAILS bar** — `DETAILS` eyebrow + `Edit` / copy / `⋯`, then a **markdown body**
|
||||
(headings, paragraphs, inline `code`, ordered/unordered lists) describing the feature.
|
||||
- **WorkConsole** docked at the bottom — traffic-light dots, `· N turns · +x −y`,
|
||||
tabs **Output / Actions / Session**, and a `Created …` footer.
|
||||
- Per-feature mapping:
|
||||
- Feature panels: markdown writeup in DETAILS + a short relevant **Output** log.
|
||||
- "Review & merge": opens on the **Actions** tab with `Merge target` + `Open Diff` /
|
||||
`Approve & merge`.
|
||||
- **Download**: DETAILS shows requirements (`.NET 8 Desktop Runtime`, `Claude CLI`,
|
||||
`Git`); the **Actions** tab holds the install controls, with `Merge target`
|
||||
repurposed as a **Build** selector and buttons `↓ Download installer` /
|
||||
`Portable .zip` / `checksums.txt`.
|
||||
|
||||
4. **Statusbar (bottom)** — `● Online · claudedo.kuns.dev · private build`.
|
||||
|
||||
### Interaction
|
||||
|
||||
- Clicking a task card selects it (accent bar + card highlight) and swaps the active
|
||||
detail panel; the WorkConsole tabs are clickable within a panel.
|
||||
- **All panels are server-rendered and present in the DOM**, toggled by class — the
|
||||
page is fully readable and downloadable **without JavaScript** (progressive
|
||||
enhancement). Vue handles the selection state on the client.
|
||||
|
||||
### Responsive
|
||||
|
||||
- ≤ ~1100px: drop the Detail island; show Lists + Tasks.
|
||||
- ≤ ~780px: single column — the Tasks list; tapping a feature pushes to a full-screen
|
||||
Detail view (mirrors the app's narrow-window behavior) with a back affordance.
|
||||
|
||||
## Server: release proxy (Nitro)
|
||||
|
||||
The app's `ReleaseClient` (`src/ClaudeDo.Releases/ReleaseClient.cs`) calls
|
||||
`{apiBase}/releases/latest` and reads `tag_name`, `name`, and
|
||||
`assets[].browser_download_url`; `DownloadAsync` GETs an asset URL directly.
|
||||
|
||||
- **`GET /api/releases/latest`** — fetches `https://git.kuns.dev/api/v1/repos/releases/ClaudeDo/releases/latest`,
|
||||
returns the **same JSON shape**, but every `assets[].browser_download_url` is rewritten
|
||||
from the Gitea URL to `https://claudedo.kuns.dev/api/download/<encoded-asset-path>`.
|
||||
Cached briefly (e.g. 5 min) server-side.
|
||||
- **`GET /api/download/[...path]`** — reconstructs the Gitea asset URL from the path and
|
||||
**streams** the binary back (no redirect to Gitea, so the URL stays hidden). Sets
|
||||
appropriate `Content-Type`/`Content-Disposition`.
|
||||
- **`server/utils/gitea.ts`** — shared base URL (`GITEA_API`, `REPO` from env), fetch
|
||||
helper, and the URL-rewrite/asset-path round-trip.
|
||||
- The page's download buttons point at the same `/api/download/...` routes (with a
|
||||
stable "latest installer" path), so links never go stale between deploys.
|
||||
|
||||
### App-side coordinating change (separate, in the ClaudeDo repo)
|
||||
|
||||
Point `ReleaseClient`'s `apiBase` at `https://claudedo.kuns.dev/api` instead of the
|
||||
Gitea default (one-line DI change where `ReleaseClient`/`UpdateCheckService` are
|
||||
constructed). Tracked as a follow-up; not part of the `claudedo-web` repo. The proxy
|
||||
path (`/api/releases/latest`) is chosen to match the existing
|
||||
`{apiBase}/releases/latest` call so the parser is untouched.
|
||||
|
||||
## Content / assets
|
||||
|
||||
- **Screenshots (provided):** main 3-column view (hero), diff review modal, worktrees
|
||||
panel. Stored in `public/screenshots/`. Placeholders sized for them until dropped in.
|
||||
- Copy: hero "Queue the work. Claude does it."; six feature writeups as above (final
|
||||
wording during implementation).
|
||||
|
||||
## Error handling
|
||||
|
||||
- **Build-time release fetch fails:** render the page with a last-known/placeholder
|
||||
version label; download buttons still work because they resolve via the runtime
|
||||
proxy route.
|
||||
- **Proxy `/api/releases/latest` upstream failure:** return a 502/`null`-equivalent the
|
||||
way Gitea would on miss; the app's `UpdateCheckService` already treats null/exception
|
||||
as `CheckFailed` and degrades gracefully.
|
||||
- **`/api/download` upstream failure:** surface a 502; the button shows an error state.
|
||||
- No retries beyond a single upstream attempt for v1 (low traffic, friends-only).
|
||||
|
||||
## Testing
|
||||
|
||||
- **Vitest** unit tests for `server/utils/gitea.ts`: URL rewrite (Gitea → proxy) and the
|
||||
asset-path round-trip (proxy path → Gitea URL), and release-JSON shape preservation.
|
||||
- A light component smoke test that the page renders the islands and the download
|
||||
controls without JS errors.
|
||||
- No real-network/Gitea calls in tests — mock the upstream fetch.
|
||||
|
||||
## Deployment (Coolify)
|
||||
|
||||
- **Dockerfile**: `node:20-alpine` build → `nuxt build` → run `.output/server/index.mjs`.
|
||||
- Coolify app bound to `claudedo.kuns.dev` with TLS via its reverse proxy.
|
||||
- Env: `GITEA_API` (default `https://git.kuns.dev/api/v1`), `REPO` (`releases/ClaudeDo`),
|
||||
`PUBLIC_BASE_URL` (`https://claudedo.kuns.dev`) for URL rewriting.
|
||||
- Deploy on push to `main`; re-deploy (or a periodic rebuild) refreshes the displayed
|
||||
version. No PR/CI tooling beyond Coolify's build.
|
||||
|
||||
## Open risk
|
||||
|
||||
- The reworked Detail island in the app is still in flux. The website's detail pane
|
||||
must be kept in sync with `2026-06-04-task-detail-redesign-design.md`; expect a
|
||||
visual-polish pass once that rework lands.
|
||||
|
||||
## Repo layout
|
||||
|
||||
```
|
||||
claudedo-web/
|
||||
├── nuxt.config.ts
|
||||
├── app.vue
|
||||
├── pages/index.vue # the one landing page
|
||||
├── components/
|
||||
│ ├── AppWindow.vue # window chrome + statusbar
|
||||
│ ├── ListsIsland.vue # page nav
|
||||
│ ├── TasksIsland.vue # feature cards + download card
|
||||
│ ├── DetailIsland.vue # three-zone detail (header / DETAILS md / WorkConsole)
|
||||
│ ├── WorkConsole.vue # tabs: Output / Actions / Session
|
||||
│ └── content/ # per-feature markdown/blurbs + download panel
|
||||
├── server/
|
||||
│ ├── api/releases/latest.get.ts
|
||||
│ ├── api/download/[...path].get.ts
|
||||
│ └── utils/gitea.ts
|
||||
├── assets/css/tokens.css # palette + type ported from Tokens.axaml/styles.css
|
||||
├── public/screenshots/ # 3 PNGs
|
||||
└── Dockerfile
|
||||
```
|
||||
127
docs/superpowers/specs/2026-06-04-refine-task-design.md
Normal file
127
docs/superpowers/specs/2026-06-04-refine-task-design.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Refine Task — Design
|
||||
|
||||
**Date:** 2026-06-04
|
||||
**Status:** Approved (pending spec review)
|
||||
|
||||
## Goal
|
||||
|
||||
Add a one-click **Refine Task** action to a task card. Clicking it spawns a
|
||||
headless Claude session that reads the task (and the repo), rewrites the task's
|
||||
description to be clearer and runnable autonomously, and — where it helps —
|
||||
breaks the work into subtasks. The user then reviews/hand-edits the result and
|
||||
queues the task manually.
|
||||
|
||||
This is **not** an interactive terminal session. It is a fire-and-forget
|
||||
headless run, structurally similar to the existing daily-prep ("Prime Claude")
|
||||
flow (`PrimeRunner`), not the interactive planning flow.
|
||||
|
||||
## Non-goals / scope
|
||||
|
||||
- No new task status. The task stays `Idle` throughout; refine only mutates the
|
||||
task's `Title`/`Description` and its subtasks.
|
||||
- No worktree, no interactive terminal, no auto-queue.
|
||||
- No per-task refine config (model, turns) — uses the worker's defaults.
|
||||
- Refine does not edit repository files; repo access is read-only.
|
||||
|
||||
## User flow
|
||||
|
||||
1. User clicks the refine icon on an `Idle` task's card.
|
||||
2. UI calls `WorkerHub.RefineTask(taskId)` → `RefineRunner`.
|
||||
3. `RefineRunner` spawns `claude -p` headless in the list's working directory,
|
||||
seeded with a fixed refine prompt + the task's title/description/current
|
||||
subtasks + the task id.
|
||||
4. Claude reads the repo (read-only), then calls:
|
||||
- `mcp__claudedo__update_task` to improve title/description, and
|
||||
- `mcp__claudedo__add_subtask` to add steps where useful.
|
||||
Each MCP call broadcasts `TaskUpdated`, so the description and Steps card
|
||||
update live in the UI.
|
||||
5. Run finishes; the card's refine button returns to its idle state. User
|
||||
reviews, optionally hand-edits the description/steps, then queues manually.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Worker — `RefineRunner`
|
||||
|
||||
- New `Worker/Refine/RefineRunner.cs` implementing `IRefineRunner`
|
||||
(`Worker/Refine/Interfaces/IRefineRunner.cs`). Modeled on `PrimeRunner`.
|
||||
- **Concurrency / single-flight:** an in-flight `HashSet<string>` of task ids
|
||||
guarded by a lock (or `SemaphoreSlim`), so the *same* task cannot refine
|
||||
twice concurrently, but different tasks may refine in parallel. A second
|
||||
click on an already-refining task is a no-op.
|
||||
- **Guards:** only runs when `task.Status == Idle`. Resolves the list's working
|
||||
directory. If the list has **no valid working dir**, fall back to a sandbox
|
||||
directory and run text-only (drop `Read`/`Grep`/`Glob` from the allowlist).
|
||||
- **CLI invocation** (relies on the globally-registered `claudedo` MCP, like
|
||||
daily-prep — no `--mcp-config`):
|
||||
```
|
||||
claude -p --output-format stream-json --verbose
|
||||
--permission-mode acceptEdits
|
||||
--max-turns <N>
|
||||
--allowedTools mcp__claudedo__get_task,mcp__claudedo__update_task,mcp__claudedo__add_subtask,Read,Grep,Glob
|
||||
```
|
||||
`Edit`/`Write`/`Bash` are deliberately **not** whitelisted, so the run is
|
||||
read-only on the repo even under `acceptEdits`. (Chosen over `plan` mode to
|
||||
avoid the headless "exit plan mode to act" friction; the allowlist is the
|
||||
real read-only gate.)
|
||||
- **Logging:** stream stdout to a per-run log at
|
||||
`logs/refine-<taskId[:8]>.log`, truncated at the start of each run.
|
||||
|
||||
### Prompt — `PromptKind.Refine`
|
||||
|
||||
- Add `Refine` to the `PromptKind` enum in `PromptFiles.cs`, file
|
||||
`prompts/refine.md`, with a bundled default.
|
||||
- Default prompt instructs: refine one ClaudeDo task so it is ready to run
|
||||
autonomously; ground the description in the actual code (read-only); keep
|
||||
scope tight (no scope creep into adjacent work); add steps as subtasks only
|
||||
when they genuinely help; use only `get_task`, `update_task`, `add_subtask`
|
||||
and the read-only tools; never edit files.
|
||||
- Rendered via `PromptFiles.Render` with `{taskId}`, `{title}`,
|
||||
`{description}`, and the current subtask list seeded into the prompt so the
|
||||
agent knows which steps already exist.
|
||||
|
||||
### MCP tool — `add_subtask`
|
||||
|
||||
- New `[McpServerTool]` on `ExternalMcpService` (part of the global `claudedo`
|
||||
MCP), signature `add_subtask(taskId, title, orderNum?)`.
|
||||
- Creates a `SubtaskEntity` via `SubtaskRepository`; `orderNum` defaults to
|
||||
append-at-end (max existing + 1). Refuses if the task is `Running`.
|
||||
Broadcasts `TaskUpdated`.
|
||||
- **Append semantics, not replace:** the current subtasks are already in the
|
||||
prompt, so the agent only adds missing steps; re-running refine will not
|
||||
silently wipe steps the user hand-edited.
|
||||
- `update_task` already exists (title/description/commitType) and is reused
|
||||
unchanged.
|
||||
|
||||
### UI — button, icon, feedback
|
||||
|
||||
- **Icon:** add the supplied SVG as an `Icon.Refine` `StreamGeometry` in
|
||||
`IslandStyles.axaml`, rendered as a **stroked `Path`** (`plan-icon` style,
|
||||
fill none) — it is line art, so per the PathIcon-fills-geometry gotcha it
|
||||
must be stroked, not filled.
|
||||
- **Button:** a new `icon-btn` in `TaskRowView.axaml` near the star button,
|
||||
visible only when the task is `Idle`. Bound to a new `RefineTaskCommand` on
|
||||
`TasksIslandViewModel`.
|
||||
- **Feedback:** new broadcaster events `RefineStarted(taskId)` /
|
||||
`RefineFinished(taskId, ok, error?)` drive an `IsRefining` flag on
|
||||
`TaskRowViewModel`; the button shows a busy/disabled state while running. The
|
||||
description and Steps card update live via the existing `TaskUpdated` events
|
||||
fired by the MCP calls.
|
||||
- Wire `RefineTask` through `IWorkerClient` / `WorkerClient`, the `WorkerHub`
|
||||
method, and update the hand-rolled test fakes in both test projects.
|
||||
|
||||
## Testing
|
||||
|
||||
- `add_subtask`: creates the row, appends order correctly, refuses when
|
||||
`Running`, broadcasts `TaskUpdated`.
|
||||
- Refine prompt builder and CLI-args builder produce the expected prompt/flags
|
||||
(including the text-only fallback when no working dir).
|
||||
- `RefineRunner` guards: `Idle`-only, per-task single-flight no-op on a second
|
||||
concurrent call.
|
||||
- **No test spawns the real `claude` CLI** (project rule). The end-to-end run
|
||||
is a manual smoke step.
|
||||
|
||||
## Open implementation calls (decided)
|
||||
|
||||
- **Permission mode:** `acceptEdits` + restricted allowlist for read-only
|
||||
(rather than `plan` mode).
|
||||
- **`add_subtask`:** append-only (rather than replace-all).
|
||||
200
docs/superpowers/specs/2026-06-04-task-detail-redesign-design.md
Normal file
200
docs/superpowers/specs/2026-06-04-task-detail-redesign-design.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Task Detail Island Redesign — Design
|
||||
|
||||
**Date:** 2026-06-04
|
||||
**Status:** Approved (design), pending implementation
|
||||
**Author:** brainstormed with user via visual companion
|
||||
|
||||
## Problem
|
||||
|
||||
The Detail island (`DetailsIslandView`, 413 lines) grew into one long scrolling
|
||||
column as features piled on. The user has to scroll constantly. Specific pains
|
||||
(confirmed by the user):
|
||||
|
||||
- **Everything is always stacked** — Steps, Description, Terminal, and several
|
||||
conditional sections share one scroll column with no way to hide/fold.
|
||||
- **Duplicated info** — `model` shows in the gear flyout *and* the agent strip;
|
||||
the branch line shows in the agent strip *and* as the terminal label.
|
||||
- **Agent strip is a heavy 5-row block** pinned near the bottom even when idle.
|
||||
- **Steps + Description take a lot of room** before the action controls.
|
||||
|
||||
The terminal staying prominent is *fine* — not a pain point.
|
||||
|
||||
## Solution overview
|
||||
|
||||
Replace the linear body with a **fixed-region layout** built from **3 new
|
||||
self-contained components**, plus a roadblock band. Top region (header + details
|
||||
card) stays put; the work console is pinned to the lower third.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ TaskHeaderBar (separated title) │ #T42 · title · 🗑/💀 · ⚙
|
||||
├─────────────────────────────────────┤
|
||||
│ DescriptionStepsCard │ card; text ⇄ steps toggle icon
|
||||
│ (Preview = what Claude gets) │ copy · preview/edit
|
||||
├─────────────────────────────────────┤
|
||||
│ Roadblock band (only when failed) │ ⚠ message · Continue · Reset&Retry
|
||||
├─────────────────────────────────────┤
|
||||
│ WorkConsole (pinned, terminal) │ ●●● · model·turns·diff
|
||||
│ tabs: Output | Actions | Session │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## The 3 components
|
||||
|
||||
Each is a standalone `UserControl` + dedicated `ViewModel` with **design-time
|
||||
sample data** so it renders fully in the Avalonia previewer in isolation. Built
|
||||
in separate worktrees; **none touch `DetailsIslandView.axaml` or
|
||||
`DetailsIslandViewModel.cs`** (that is the wiring session). All visuals use
|
||||
**only** the existing design tokens (`Design/Tokens.axaml`) and style classes
|
||||
(`Design/IslandStyles.axaml`) — no hardcoded colors/sizes.
|
||||
|
||||
New folder: `src/ClaudeDo.Ui/Views/Islands/Detail/`
|
||||
New VMs: `src/ClaudeDo.Ui/ViewModels/Islands/Detail/`
|
||||
|
||||
### 1. TaskHeaderBar
|
||||
|
||||
- **Layout:** one row — `#T42` id badge (mono `meta`, copyable) · editable title
|
||||
`TextBox` (transparent, `FontSizeTaskTitle`, wraps) · **trash/skull button** ·
|
||||
⚙ gear button with the agent-settings flyout.
|
||||
- **Trash → Skull:** when **not** running show `Icon.Trash` (delete task,
|
||||
`BloodBrush`); when **running** show a **skull** glyph (kill session). One
|
||||
button, swaps icon + command on running state. Skull is a *new* filled
|
||||
geometry to add to `IslandStyles.axaml` resources (`Icon.Skull`).
|
||||
- **No done circle. No star** (the star lives on the task card/row already).
|
||||
- **Gear flyout:** keep the existing agent-settings content verbatim — Model
|
||||
combo + `InheritedBadge` + reset; Max Turns `NumericUpDown` + badge + reset;
|
||||
System Prompt `TextBox` + "prepended" hint; Agent File combo + badge + reset.
|
||||
Disabled while running (`IsAgentSectionEnabled`).
|
||||
- **Existing bindings reused:** `TaskIdBadge`, `EditableTitle`, `DeleteTaskCommand`,
|
||||
`StopCommand`, `IsRunning`, `IsAgentSectionEnabled`, all the agent-settings
|
||||
members (`TaskModelOptions`/`TaskModelSelection`/`ModelBadge`/
|
||||
`ResetTaskModelCommand`/`TaskMaxTurns`/`TurnsBadge`/`ResetTaskTurnsCommand`/
|
||||
`TaskSystemPrompt`/`EffectiveSystemPromptHint`/`TaskAgentOptions`/
|
||||
`TaskSelectedAgent`/`AgentBadge`/`ResetTaskAgentCommand`).
|
||||
|
||||
### 2. DescriptionStepsCard
|
||||
|
||||
A `Border.island`-style card. The single explicitly-requested "separate
|
||||
component." Top-right **toggle icon** switches the card between **Description**
|
||||
and **Steps** views; the icon shows the *other* mode (in Description view → steps
|
||||
icon `Icon.MoreHorizontal`/list glyph; in Steps view → text glyph).
|
||||
|
||||
- **Header row:** small `section-label` ("DETAILS" / "STEPS") · spacer · **Copy**
|
||||
icon button (`Icon.Copy`) · **Preview/Edit** toggle button (Description view
|
||||
only) · **toggle icon** (top-right).
|
||||
- **Description view:**
|
||||
- *Preview mode* = renders **what Claude gets** via `MarkdownView`: the
|
||||
canonical composed text (Title + Description + open steps — see below).
|
||||
- *Edit mode* = raw description `TextBox` (mono, `Surface2Brush`, multiline).
|
||||
- **Steps view:** add-step input (Enter to add) + list of step rows (check
|
||||
circle `Ellipse.task-check` + inline-editable title, `subtask-row` style).
|
||||
- **Copy** copies the **formatted** version (Title + Description + open steps),
|
||||
nothing else, to the clipboard.
|
||||
- **Existing bindings reused (when wired):** `EditableDescription`,
|
||||
`IsEditingDescription`/`ToggleEditDescriptionCommand`, `Subtasks`,
|
||||
`NewSubtaskTitle`/`AddSubtaskCommand`, `ToggleSubtaskDoneCommand`,
|
||||
`CommitSubtaskEditCommand`.
|
||||
- **New members (defined on the component VM now, lifted into
|
||||
`DetailsIslandViewModel` at wiring):** `IsStepsView` + `ToggleCardViewCommand`;
|
||||
`ComposedPreview` (string, the canonical format); `CopyFormattedCommand`.
|
||||
|
||||
### 3. WorkConsole
|
||||
|
||||
Terminal-styled card (`Border.terminal`) pinned to the lower third.
|
||||
|
||||
- **Title bar:** three cosmetic traffic-light dots (`Ellipse.dot-red`,
|
||||
`dot-yellow`, `dot-green`) on the left; centered/!right small **info header**:
|
||||
`model · {turns} turns · +adds −dels` (mono `meta`; `diff-add`/`diff-del`
|
||||
classes for the numbers). **No branch line.** LIVE/DONE/FAILED chip
|
||||
(`live-chip`) on the right.
|
||||
- **Tab strip:** `Output` | `Actions` | `Session`.
|
||||
- **Output** — the live log. Reuse `SessionTerminalView` (`Entries`, `Label`,
|
||||
`IsRunning`, `IsDone`, `IsFailed`) for the body, *or* the same
|
||||
timestamp+`SelectableTextBlock` row template.
|
||||
- **Actions** — worktree management: merge-target `ComboBox`, **Open Diff**,
|
||||
**Worktree**, **Merge** (+ planning **Merge All Subtasks** when planning
|
||||
parent). Bindings: `MergeTargetBranches`/`SelectedMergeTarget`,
|
||||
`OpenDiffCommand`, `OpenWorktreeCommand`, `MergeAllCommand`/`CanMergeAll`/
|
||||
`MergeAllDisabledReason`/`MergeAllError`, `ReviewCombinedDiffCommand`.
|
||||
- **Session** — review + outcomes: feedback `TextBox` + Approve/Reject/Park/
|
||||
Cancel (`ReviewFeedback`, `ApproveReviewCommand`, `RejectReviewCommand`,
|
||||
`ParkReviewCommand`, `CancelReviewCommand`, shown when `IsWaitingForReview`)
|
||||
and the child-outcomes list (`ChildOutcomes`, `HasChildOutcomes`).
|
||||
- **Roadblock band** (above the tabs, inside or just above the card): visible on
|
||||
`IsFailed`/`IsCancelled`; shows a warning (`Icon.Warning`, `BloodBrush`) and
|
||||
**Continue** (`ContinueCommand`, `ShowContinue`) + **Reset & Retry**
|
||||
(`ResetAndRetryCommand`, `ShowResetAndRetry`).
|
||||
- **Info-header bindings:** `Model`, `Turns`, `DiffAdditions`, `DiffDeletions`,
|
||||
`IsRunning`/`IsDone`/`IsFailed`.
|
||||
|
||||
## Combined Description + Steps behavior
|
||||
|
||||
Steps are part of the description. When the task runs, the **effective prompt =
|
||||
Title + Description + only the OPEN steps**. Resolved steps are dropped.
|
||||
|
||||
**Canonical composed format** (shared by the Worker prompt, the card's Preview,
|
||||
and Copy):
|
||||
|
||||
```
|
||||
<Title>
|
||||
|
||||
<Description>
|
||||
|
||||
## Sub-Tasks
|
||||
- [ ] <open step 1>
|
||||
- [ ] <open step 2>
|
||||
```
|
||||
|
||||
- Omit the `## Sub-Tasks` section entirely when no open steps remain.
|
||||
- Omit the description paragraph when description is empty.
|
||||
|
||||
**Worker change (wiring session, by Claude):** `TaskRunner.cs:104-113` currently
|
||||
appends *all* subtasks with `[x]`/`[ ]`. Change to append **only incomplete**
|
||||
subtasks as `- [ ]` lines (drop completed). Factor the format into a shared
|
||||
`TaskPromptComposer` in `ClaudeDo.Data` (referenced by both Worker and UI) so the
|
||||
card's Preview and the real prompt never diverge.
|
||||
|
||||
## Color / token guidelines (mandatory)
|
||||
|
||||
- Backgrounds: `IslandBackgroundBrush`, `Surface2Brush`, `Surface3Brush`,
|
||||
`DeepBrush`, `VoidBrush` (terminal). Borders: `LineBrush`/`LineBrightBrush`,
|
||||
`HairlineOverlayBrush`. Text: `TextBrush`/`TextDimBrush`/`TextMuteBrush`/
|
||||
`TextFaintBrush`. Accent: `AccentBrush`/`AccentDimBrush`. Status: blood/peat/
|
||||
moss/sage + the `*TintBrush` pairs.
|
||||
- Radii: `IslandCornerRadius` (14), `ButtonCornerRadius` (6), `InputCornerRadius`
|
||||
(8). Spacing: `SpaceXs..Space2Xl`. Fonts: `SansFont`, `MonoFont`; sizes
|
||||
`FontSizeMono`/`FontSizeBody`/`FontSizeTaskTitle`.
|
||||
- Reuse style classes: `island`, `island-header`, `chip`, `btn`/`btn accent`/
|
||||
`primary`/`danger`, `icon-btn`, `flat`, `terminal`, `dot-red/yellow/green`,
|
||||
`live-chip`, `task-check`, `subtask-row`, `section-label`, `field-label`,
|
||||
`meta`, `diff-add`/`diff-del`, `diff-meter-*`.
|
||||
- **No inline hex, no magic numbers** where a token exists. `PathIcon` fills
|
||||
geometry — line-art must be filled or stroked via `Path`.
|
||||
|
||||
## Build / isolation strategy
|
||||
|
||||
1. Three ClaudeDo tasks (list "Claude do", repo `C:\Private\ClaudeDo`), one per
|
||||
component, run sequentially in their own worktrees.
|
||||
2. Each delivers: `Detail/<Name>.axaml` + `.axaml.cs` + `Detail/<Name>ViewModel.cs`
|
||||
with design-time sample data; the `ClaudeDo.Ui` project **builds green**
|
||||
(`dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj -c Release`).
|
||||
3. Components are visual-only against sample data. Real `DetailsIslandViewModel`
|
||||
binding + the Worker steps→prompt change happen in the **wiring session**
|
||||
(this Claude session, done while the build tasks run).
|
||||
|
||||
## Wiring plan (this session)
|
||||
|
||||
- Implement `TaskPromptComposer` + the `TaskRunner` open-steps change + a unit
|
||||
test in `ClaudeDo.Worker.Tests`/`Data.Tests`.
|
||||
- After the 3 components land: host them in `DetailsIslandView` (header top,
|
||||
card below, roadblock band, work console pinned bottom), lift the new card VM
|
||||
members into `DetailsIslandViewModel`, repoint `x:DataType`, delete the
|
||||
superseded inline sections + `AgentStripView` usage. Update locale parity and
|
||||
the test fakes.
|
||||
|
||||
## Monitoring loop (this session)
|
||||
|
||||
While the build tasks run: poll each via `get_task` / `get_task_log` /
|
||||
`get_task_diff`, summarize progress and anything a session got stuck on, and if a
|
||||
session is blocked on something missing, add a small follow-up task to the
|
||||
"Claude do" list.
|
||||
@@ -0,0 +1,197 @@
|
||||
# Git Tab / Merge & Review Rework — Design
|
||||
|
||||
Date: 2026-06-05
|
||||
Status: Approved
|
||||
|
||||
## Goal
|
||||
|
||||
Make handling merges and reviews as simple as possible in the Terminal component's
|
||||
Git tab, and rework the diff viewers and worktree modals along the way. The work is
|
||||
split into three layers built across separate sessions, with a shared foundation that
|
||||
is built and pushed first so the parallel sessions branch from frozen contracts.
|
||||
|
||||
The user mostly trusts task output but wants the diff one click away for important
|
||||
work, and wants to land several independently-queued worktrees without per-task
|
||||
hopping or hand-resolving conflicts in an external editor.
|
||||
|
||||
## Layers
|
||||
|
||||
- **Layer A — Review/merge cockpit** (this session). Single-task review + merge UX in
|
||||
the Git tab; consolidate the four diff renderers into one `DiffView`.
|
||||
- **Layer B — Multi-worktree merge cockpit** (parallel session). Batch-merge N
|
||||
worktrees into one target, skip-and-continue, conflicts collected for resolution.
|
||||
- **Layer C — Inline conflict resolver** (parallel session). VSCode-style inline hunk
|
||||
resolver plus the worker-side conflict plumbing it needs.
|
||||
|
||||
They stack: A defines the single-task flow, B reuses it for many tasks, both funnel
|
||||
conflicts into C.
|
||||
|
||||
## Shared foundation (built & pushed this session, before B/C branch)
|
||||
|
||||
Everything B and C depend on lands first on `main`. B and C branch from that commit.
|
||||
|
||||
### 1. One diff model + one `DiffView` control
|
||||
|
||||
Today there are four diff renderers and two parallel diff models:
|
||||
|
||||
- `DiffLinesView.axaml` (used by `DiffModalView`)
|
||||
- the inline diff `ItemsControl` in `WorktreeModalView.axaml`
|
||||
- `PlanningDiffView.axaml`
|
||||
- their backing models: `DiffFileViewModel`/`DiffLineViewModel` (+ `UnifiedDiffParser`)
|
||||
vs `WorktreeNodeViewModel`/`WorktreeDiffLineViewModel`
|
||||
|
||||
Collapse into a single canonical diff model + parser + a `DiffView` UserControl. All
|
||||
diff rendering across the app goes through `DiffView`.
|
||||
|
||||
- Model: `DiffFileViewModel { Path, AddCount, DelCount, Lines }`,
|
||||
`DiffLineViewModel { OldNo, NewNo, Kind (Add|Del|Ctx|File|Hunk), Text }`.
|
||||
- Parser: one static `UnifiedDiffParser.Parse(rawUnifiedDiff)` returning the model.
|
||||
- `DiffView` exposes a `Files` styled property (file list + selected-file lines), or a
|
||||
simpler `Lines` property for single-file use — Layer A decides the exact surface
|
||||
while building it, but the type names above are frozen so B and C can bind to them.
|
||||
|
||||
### 2. Frozen worker conflict contract
|
||||
|
||||
Added to `IWorkerClient` (and `WorkerClient` with stub bodies that throw
|
||||
`NotSupportedException`) plus new DTOs, so A and B compile against the interface while
|
||||
C provides the real worker-side implementation.
|
||||
|
||||
```csharp
|
||||
// IWorkerClient additions (signatures frozen this session)
|
||||
Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch);
|
||||
Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId);
|
||||
Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent);
|
||||
Task<MergeResultDto> ContinueMergeAsync(string taskId);
|
||||
Task AbortMergeAsync(string taskId);
|
||||
```
|
||||
|
||||
- `StartConflictMergeAsync` performs the merge with `leaveConflictsInTree: true` (the
|
||||
worker already supports this flag — used today by the planning orchestrator) and
|
||||
returns `MergeResultDto` with `Status="conflict"` and the conflict file list, leaving
|
||||
`.git/MERGE_HEAD` in place in the list's `WorkingDir`.
|
||||
- `GetMergeConflictsAsync` returns each conflicted file with ours/theirs/base content,
|
||||
read via `git show :2:<path>` (ours), `:3:<path>` (theirs), `:1:<path>` (base).
|
||||
- `WriteConflictResolutionAsync` writes resolved content to the file in `WorkingDir`
|
||||
and `git add`s it.
|
||||
- `ContinueMergeAsync` wraps the existing `TaskMergeService.ContinueMergeAsync`
|
||||
(`git add -A` → re-check `git diff --name-only --diff-filter=U` → `git commit`).
|
||||
- `AbortMergeAsync` wraps the existing `TaskMergeService.AbortMergeAsync`
|
||||
(`git merge --abort`).
|
||||
|
||||
New DTOs (defined in the worker hub DTO file, mirrored client-side):
|
||||
|
||||
```csharp
|
||||
public record MergeConflictsDto(string TaskId, IReadOnlyList<ConflictFileDto> Files);
|
||||
public record ConflictFileDto(string Path, IReadOnlyList<ConflictHunkDto> Hunks);
|
||||
public record ConflictHunkDto(string Ours, string Theirs, string? Base);
|
||||
```
|
||||
|
||||
Existing DTOs reused unchanged: `MergeResultDto(Status, ConflictFiles, ErrorMessage)`,
|
||||
`MergePreviewDto`, `MergeTargetsDto`.
|
||||
|
||||
### 3. Conflict data model (UI)
|
||||
|
||||
`ConflictFile { Path, Hunks[] }`, `ConflictHunk { Ours, Theirs, Base, Resolution }`.
|
||||
Shaped so a future 3-way merge pane needs no model change (Layer C is the inline
|
||||
resolver now; the model leaves room for 3-way later).
|
||||
|
||||
### 4. Integration seams (delegates, wired by the integrator at merge)
|
||||
|
||||
A's and B's cockpits hold a `RequestConflictResolution(string taskId)` callback (an
|
||||
`Action<string>` or `Func<string, Task>`). They never reference Layer C's resolver
|
||||
types. The integrator connects these callbacks to C's `ConflictResolverViewModel`
|
||||
factory when merging the three branches together.
|
||||
|
||||
## Parallel boundaries (verified disjoint)
|
||||
|
||||
| Area | A (this session) | B (parallel) | C (parallel) |
|
||||
|---|---|---|---|
|
||||
| `DiffView` + diff model/parser | builds | reuses | reuses |
|
||||
| `WorkConsole.axaml` / `DetailsIslandViewModel` | owns | — | — |
|
||||
| `DiffModalView` + `PlanningDiffView` | migrates to `DiffView` | — | — |
|
||||
| `WorktreesOverviewModalView/VM` + `WorktreeModalView` | — | owns | — |
|
||||
| `WorkerHub` / `TaskMergeService` / `GitService` | — | — | owns |
|
||||
| New `ConflictResolverView/VM` + conflict UI model | — | — | owns |
|
||||
| `IWorkerClient` / `WorkerClient` | adds frozen stubs + DTOs | reuses `MergeTaskAsync` | fills stub bodies |
|
||||
| Test fakes (`IWorkerClient`) in both test projects | adds new no-op methods | — | makes them functional if needed |
|
||||
|
||||
The only file C and A both touch is `WorkerClient.cs` (C replaces the stub bodies A
|
||||
wrote). Contained; reconciled at integration. Everything else is disjoint.
|
||||
|
||||
## Layer A — review/merge cockpit (this session)
|
||||
|
||||
- The Git tab becomes the single Approve + merge surface. `Approve` and the merge
|
||||
target / preview / diff flow together as one block (no separate REVIEW vs
|
||||
MERGE & WORKTREE sections).
|
||||
- `Continue` (reject → requeue with feedback) and `Reset` (reject → idle) **stay** in
|
||||
the Output tab footer — unchanged.
|
||||
- The diff is shown via the unified `DiffView` opened as a modal from the cockpit. No
|
||||
inline diff recap in the tab (the island is too small).
|
||||
- On a single-task **Approve that conflicts**: instead of today's auto-abort, call
|
||||
`StartConflictMergeAsync` and fire `RequestConflictResolution(taskId)`. This leaves
|
||||
the main checkout mid-merge until the user resolves or aborts (behavior change,
|
||||
intended). The callback is inert until Layer C is merged; the integrator wires it.
|
||||
- Migrate `DiffModalView` and `PlanningDiffView` onto the new `DiffView`.
|
||||
|
||||
### Behavior change accepted
|
||||
|
||||
Today `MergeTask`/`ApproveReview` use `leaveConflictsInTree: false` (auto-abort on
|
||||
conflict). Under this design, an Approve that conflicts leaves the merge in progress
|
||||
and opens the resolver. The mid-merge guard (`IsMidMergeAsync`) still prevents a second
|
||||
concurrent merge.
|
||||
|
||||
## Layer B — multi-worktree merge cockpit (parallel)
|
||||
|
||||
- Rework `WorktreesOverviewModalView`/`WorktreesOverviewModalViewModel` into a
|
||||
batch-merge cockpit: list mergeable worktrees, select N, choose one target branch
|
||||
(single target — 99% of the time everything goes to the same branch), "Merge all".
|
||||
- **Skip-and-continue**: client-side loop calling the existing
|
||||
`MergeTaskAsync(taskId, target, removeWorktree, msg)` per selected task. Clean merges
|
||||
apply; conflicting ones are collected (existing `MergeTaskAsync` auto-aborts on
|
||||
conflict, leaving the tree clean) into a "needs resolution" list with live progress.
|
||||
- Each conflict row exposes a **Resolve** action → `RequestConflictResolution(taskId)`
|
||||
(wired to Layer C at integration).
|
||||
- Per-task diff via the shared `DiffView`; migrate `WorktreeModalView`'s inline diff
|
||||
onto it.
|
||||
- B touches no worker files — keeps it parallel-safe.
|
||||
|
||||
## Layer C — inline conflict resolver (parallel)
|
||||
|
||||
### Worker side
|
||||
|
||||
Implement the five frozen contract methods:
|
||||
|
||||
- Add hub methods `StartConflictMerge`, `GetMergeConflicts`, `WriteConflictResolution`,
|
||||
`ContinueMerge`, `AbortMerge` in `WorkerHub`.
|
||||
- `StartConflictMerge` calls the existing `TaskMergeService.MergeAsync` overload with
|
||||
`leaveConflictsInTree: true`.
|
||||
- `ContinueMerge` / `AbortMerge` wrap the existing `TaskMergeService.ContinueMergeAsync`
|
||||
/ `AbortMergeAsync` (currently service-level only, not hub-exposed).
|
||||
- `GetMergeConflicts` reads ours/theirs/base per conflicted file via
|
||||
`git show :2:/:3:/:1:`; add the `GitService` helpers needed.
|
||||
- `WriteConflictResolution` writes the resolved content to `WorkingDir` and stages it.
|
||||
- Fill the `WorkerClient` stub bodies (real SignalR `InvokeAsync` calls).
|
||||
- Update the hand-rolled `IWorkerClient` fakes in both test projects.
|
||||
|
||||
### UI
|
||||
|
||||
- New `ConflictResolverView` + `ConflictResolverViewModel`. Per conflict hunk, show
|
||||
ours vs theirs stacked, with buttons **Accept Current / Accept Incoming / Accept Both
|
||||
/ Edit manually** plus a free-text box for the merged result of that hunk.
|
||||
- When every file's hunks are resolved → `ContinueMergeAsync(taskId)` → `MergeResultDto`
|
||||
(`merged` closes the resolver; `conflict` means not fully resolved, stay open).
|
||||
- `AbortMergeAsync(taskId)` cancels and aborts the merge.
|
||||
- Expose a factory (`Func<string, ConflictResolverViewModel>`) the integrator wires to
|
||||
A's and B's `RequestConflictResolution` callbacks.
|
||||
|
||||
## Build / test
|
||||
|
||||
`.slnx` needs .NET 9; on .NET 8 build individual csproj with `-c Release` (a running
|
||||
Worker locks `Debug`). Run the relevant test projects. No tests that spawn the real
|
||||
`claude` CLI. Keep `en.json`/`de.json` localization keys in parity.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Full 3-way synchronized merge editor (model leaves room; not built now).
|
||||
- Per-task differing merge targets in the batch (single target only).
|
||||
- Any CI/PR tooling (direct push-to-main workflow).
|
||||
99
docs/superpowers/specs/2026-06-05-terminal-review-design.md
Normal file
99
docs/superpowers/specs/2026-06-05-terminal-review-design.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Terminal-style review controls
|
||||
|
||||
**Date:** 2026-06-05
|
||||
**Status:** Approved (design)
|
||||
|
||||
## Problem
|
||||
|
||||
Review feedback today is a multi-line `TextBox` plus four buttons (Approve / Reject /
|
||||
Park / Cancel) tucked into the WorkConsole **Session** tab
|
||||
(`WorkConsole.axaml:169-193`). It feels disconnected from the live terminal. Entering
|
||||
feedback should feel like typing into the terminal, with action buttons docked at the
|
||||
bottom — and merge/approve actions should live in an obvious, dedicated place.
|
||||
|
||||
## Goal
|
||||
|
||||
- Type review feedback directly in the **Output (terminal)** tab, prompt-style.
|
||||
- Bottom-docked action strip on the terminal: `[Retry]` `[Reset]`.
|
||||
- Move all git/merge/worktree actions (including **Approve**) into a new **Git** tab so
|
||||
it is obvious where each action lives.
|
||||
|
||||
## Tab structure
|
||||
|
||||
Three tabs in WorkConsole: **Output** · **Git** · **Session**.
|
||||
|
||||
| Tab | Contents |
|
||||
| --- | --- |
|
||||
| **Output** | Live `Log` (unchanged) + new review footer (below), footer gated on `IsWaitingForReview`. |
|
||||
| **Git** | The current "Merge & worktree" block — merge-target dropdown, mergeability indicator, **Approve**, Open Diff, Merge, Worktree, Review Combined Diff, Merge All Subtasks. Visibility gated on `ShowMergeSection` / `IsWaitingForReview` as today. |
|
||||
| **Session** | Child outcomes + empty-state only. |
|
||||
|
||||
### ViewModel changes (`DetailsIslandViewModel`)
|
||||
|
||||
- Add `public bool IsGitTab => SelectedTab == "git";`
|
||||
- Add `[NotifyPropertyChangedFor(nameof(IsGitTab))]` alongside the existing
|
||||
`IsOutputTab` / `IsSessionTab` notifications on `SelectedTab` (`:139-144`).
|
||||
- `SelectTab` already accepts a string parameter — no change beyond the new `"git"`
|
||||
value wired from XAML.
|
||||
- No command renames (avoids breaking hand-rolled test fakes).
|
||||
|
||||
## Terminal footer (Output tab)
|
||||
|
||||
A `Border` docked `Bottom` inside the Output tab body, visible only when
|
||||
`IsWaitingForReview`:
|
||||
|
||||
- Background `Surface2Brush`, top border `LineBrush` (`BorderThickness="0,1,0,0"`).
|
||||
- A `❯` prompt-prefix `TextBlock` (mono, `TextMuteBrush`) + a borderless mono `TextBox`:
|
||||
- Bound `Text="{Binding ReviewFeedback, Mode=TwoWay}"`.
|
||||
- `AcceptsReturn="True"`, `TextWrapping="Wrap"`, transparent background, no border.
|
||||
- Starts ~1 line tall; grows with content up to `MaxHeight≈160`, then scrolls.
|
||||
- `PlaceholderText` e.g. "Feedback for the next run…".
|
||||
- Right-aligned button strip:
|
||||
- `[Retry]` — `Classes="btn accent"` → `RejectReviewCommand`.
|
||||
- `[Reset]` — `Classes="btn"` → `ParkReviewCommand`.
|
||||
|
||||
`[Accept]` is **not** in the footer; approval happens on the Git tab via
|
||||
`ApproveReviewCommand`. The old `Cancel` review button is dropped from this UI; cancel
|
||||
remains reachable through the task's existing cancel control (`CancelReviewCommand`
|
||||
stays on the ViewModel, just not surfaced here).
|
||||
|
||||
### Enter handling (`WorkConsole.axaml.cs`)
|
||||
|
||||
- Handle `KeyDown` on the input `TextBox`:
|
||||
- **Enter** without Shift → execute `RejectReviewCommand` (if it can execute) and set
|
||||
`e.Handled = true`.
|
||||
- **Shift+Enter** → fall through to default behavior (inserts newline).
|
||||
- `RejectReviewAsync` already returns early on whitespace-only feedback
|
||||
(`DetailsIslandViewModel.cs:1464`), so pressing Enter with an empty prompt is a no-op
|
||||
with no extra guard needed.
|
||||
|
||||
## Command mapping
|
||||
|
||||
| Button | Location | Command | Effect |
|
||||
| --- | --- | --- | --- |
|
||||
| `[Retry]` | Output footer | `RejectReviewCommand` | Reject-to-queue with feedback; resumes the session (Queued). |
|
||||
| `[Reset]` | Output footer | `ParkReviewCommand` | Park back to Idle. |
|
||||
| `[Approve]` | Git tab | `ApproveReviewCommand` | Merge `SelectedMergeTarget` → Done (conflict keeps it in review). |
|
||||
|
||||
## Copy / empty state
|
||||
|
||||
- Update the Session empty-state text (`WorkConsole.axaml:270`) — it currently says
|
||||
"review and merge controls appear here once the run finishes", which is no longer
|
||||
accurate. Reword to reflect that only outcomes live on Session.
|
||||
- Button labels remain literal strings (`Retry`, `Reset`, `Approve`), matching the
|
||||
existing review buttons (no new localization keys).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- No changes to worker-side review/merge logic or `IWorkerClient` signatures.
|
||||
- No merge-target selector duplicated into the terminal footer (Approve uses the Git
|
||||
tab dropdown / default target).
|
||||
- No command renames on the ViewModel.
|
||||
|
||||
## Testing / verification
|
||||
|
||||
- Build `ClaudeDo.App` and `ClaudeDo.Worker` in `-c Release`.
|
||||
- Manual visual verification (must be flagged — cannot be auto-verified):
|
||||
- Footer appears only in `WaitingForReview`, on the Output tab.
|
||||
- Enter sends Retry; Shift+Enter inserts a newline; empty Enter does nothing.
|
||||
- Git tab shows Approve + merge/worktree controls; Session shows only outcomes.
|
||||
@@ -0,0 +1,148 @@
|
||||
# Unify the parent-task model (planning · improvement · normal)
|
||||
|
||||
**Date:** 2026-06-09
|
||||
**Status:** Approved-pending-implementation
|
||||
|
||||
## Problem
|
||||
|
||||
ClaudeDo has three ways a task produces and waits on work, grown as separate
|
||||
mechanisms that represent the *same shape* — "a task runs, may emit children,
|
||||
and once it + its children are terminal it surfaces for review":
|
||||
|
||||
| | children authored | scheduling | parent flow today | merge of children |
|
||||
|---|---|---|---|---|
|
||||
| **Normal** | none | — | `Running → WaitingForReview → Done` | own worktree on approve |
|
||||
| **Improvement** | autonomously *during* run (`suggest_improvement`) | parallel (no blockers) | `Running → WaitingForChildren → WaitingForReview → Done` | separate `MergeAllPlanning` |
|
||||
| **Planning** | interactively *before* run (planning session) | sequential chain (`BlockedByTaskId`) | `Idle →(Active→Finalized)→ Done` (skips review) | separate `MergeAllPlanning` |
|
||||
|
||||
The incidental divergence we want to remove:
|
||||
|
||||
1. **Two "parent is waiting on children" representations** — improvement uses
|
||||
`Status=WaitingForChildren`; planning uses `PlanningPhase=Finalized` with the
|
||||
parent's `Status` jumping `Idle → Done`, never passing through the waiting/review
|
||||
states at all.
|
||||
2. **Two parent-advance methods** doing the same job —
|
||||
`TaskRepository.TryCompleteParentAsync` (planning → `Done`, no review) vs
|
||||
`TaskStateService.TryAdvanceImprovementParentAsync` (improvement → `WaitingForReview`).
|
||||
3. **A separate merge action** — `MergeAllPlanning` / `PlanningMergeOrchestrator`
|
||||
merges children, decoupled from the parent's `approve`. Approving a parent and
|
||||
merging its unit are two clicks.
|
||||
|
||||
What is **genuinely unique and kept**: `PlanningPhase.Active` — the interactive,
|
||||
human-in-the-loop authoring gate where children are drafted and cannot run until
|
||||
finalize. Improvement has no equivalent. The two *authoring* entry points
|
||||
(`PlanningMcpService.CreateChildTask` vs `TaskRunMcpService.SuggestImprovement`)
|
||||
also stay distinct — they already share `CreateChildAsync`; unifying the authoring
|
||||
UX is explicitly out of scope.
|
||||
|
||||
## Decisions (locked)
|
||||
|
||||
- **All parents get review.** A planning parent now surfaces in `WaitingForReview`
|
||||
after its children finish, instead of auto-completing to `Done`.
|
||||
- **Approve merges the whole unit — full UX consolidation.** Approve is the single
|
||||
entry for reviewing *and* merging any task. For a parent with children it drives the
|
||||
existing `PlanningMergeOrchestrator` (unit merge + parent→`Done` + conflict
|
||||
continue/abort, all already implemented); the standalone "Merge All" button is
|
||||
removed and the orchestrator's conflict dialog + combined-diff preview are reused
|
||||
in-place. Childless tasks keep `ApproveAndMergeAsync`.
|
||||
- **Scope = state model + code paths.** Internal refactor; authoring UX and child
|
||||
base-commit resolution are unchanged.
|
||||
|
||||
## Target model
|
||||
|
||||
**One parent-with-children lifecycle, used by every parent regardless of how its
|
||||
children were authored:**
|
||||
|
||||
```
|
||||
┌─ (no children) ──────────────┐
|
||||
Idle → Queued → Running ──┤ ├→ WaitingForReview → Done
|
||||
└─ (has/spawns children) ─┐ │ (approve =
|
||||
│ │ merge unit)
|
||||
WaitingForChildren ─┘ │
|
||||
│ │
|
||||
(all children terminal) ───────┘
|
||||
```
|
||||
|
||||
Planning parent (never runs as an agent — it runs an interactive session):
|
||||
|
||||
```
|
||||
Idle (PlanningPhase None)
|
||||
→[StartPlanning] Idle (PlanningPhase Active) ← authoring gate (KEPT)
|
||||
→[FinalizePlanning] WaitingForChildren (Finalized) ← children chain runs
|
||||
→[all children terminal] WaitingForReview
|
||||
→[approve] merge unit → Done
|
||||
```
|
||||
|
||||
Children (planning **and** improvement) keep going straight to `Done` with no
|
||||
individual review; they accumulate on their branches and merge as a unit when the
|
||||
parent is approved.
|
||||
|
||||
### State machine after the change
|
||||
|
||||
- `WaitingForChildren` is the **single** "parent waiting on children" state, used by
|
||||
both planning and improvement parents.
|
||||
- `WaitingForReview` is reached by every parent before `Done`.
|
||||
- `PlanningPhase`: `None | Active | Finalized` — unchanged; `Active` remains the
|
||||
authoring gate, `Finalized` marks "was a planning parent" and is set together with
|
||||
`Status=WaitingForChildren`.
|
||||
|
||||
## Code changes
|
||||
|
||||
1. **Single parent-advance path.** Rename
|
||||
`TaskStateService.TryAdvanceImprovementParentAsync` →
|
||||
`TryAdvanceParentAsync`; it already only checks `Status==WaitingForChildren` +
|
||||
"all children terminal" → `WaitingForReview` (with the failed/cancelled
|
||||
annotation on `Result`). It becomes the only path for both systems.
|
||||
- Handle **zero children**: a finalized planning parent with no children must go
|
||||
straight to `WaitingForReview` (today `TryComplete`/`TryAdvance` both `return`
|
||||
on `Count == 0`).
|
||||
|
||||
2. **Delete `TaskRepository.TryCompleteParentAsync`** (`TaskRepository.cs:477`) and
|
||||
its invocation in `TaskStateService.OnChildTerminalAsync`. Planning parents now
|
||||
advance via `TryAdvanceParentAsync` to `WaitingForReview` instead of `Done`.
|
||||
- Keep `_chain.OnChildFinishedAsync` (inter-child unblock — planning-only effect).
|
||||
|
||||
3. **`FinalizePlanningAsync`** (`TaskStateService.cs:289`) sets the parent
|
||||
`Status = WaitingForChildren` in the same update that sets
|
||||
`PlanningPhase = Finalized`. This happens before `SetupChainAsync` enqueues
|
||||
child[0], so the parent is in `WaitingForChildren` before any child can finish.
|
||||
|
||||
4. **Approve merges the unit.** `WorkerHub.ApproveReview` (and the MCP
|
||||
`ReviewTask` approve path): when the approved task has children, run
|
||||
`PlanningMergeOrchestrator` (parent worktree if `Active` + each `Done` child in
|
||||
order), then transition the parent to `Done`. On a child merge conflict, the
|
||||
parent stays in `WaitingForReview` (mirrors current single-task approve-conflict
|
||||
behavior). Retire the `MergeAllPlanning` Hub method + UI button.
|
||||
|
||||
5. **Allow cancelling a `WaitingForChildren` parent.** Add `WaitingForChildren` to
|
||||
the `CancelAsync` guard so a parent waiting on children can be cancelled (today it
|
||||
cannot — minor gap).
|
||||
|
||||
6. **Docs.** Fix the `WaitingForChildren`-missing drift in
|
||||
`src/ClaudeDo.Data/CLAUDE.md` and `src/ClaudeDo.Worker/CLAUDE.md`, and update the
|
||||
transition diagram + the root `CLAUDE.md` status-flow line to the unified model.
|
||||
|
||||
## Out of scope (unchanged)
|
||||
|
||||
- Authoring UX: planning session vs `suggest_improvement` stay as two distinct
|
||||
entry points (both already call `CreateChildAsync`).
|
||||
- `WorktreeManager.ResolveBaseCommitAsync` base-commit divergence (planning children
|
||||
branch from list HEAD; improvement children from parent head) — left as-is.
|
||||
- Sequential-vs-parallel scheduling — already shared infrastructure
|
||||
(`BlockedByTaskId`); planning chains, improvement doesn't. No change.
|
||||
|
||||
## Risks / edge cases
|
||||
|
||||
- **Ordering on finalize** — parent must be `WaitingForChildren` before the first
|
||||
child can reach terminal. Guaranteed by setting it inside `FinalizePlanningAsync`,
|
||||
which runs before `SetupChainAsync`.
|
||||
- **Zero-children planning parent** — must advance to `WaitingForReview`, not stick
|
||||
in `WaitingForChildren`. Explicit branch in `TryAdvanceParentAsync` /
|
||||
`FinalizePlanningAsync`.
|
||||
- **Failed/cancelled children** — parent still advances to `WaitingForReview` with
|
||||
the existing `⚠ Children: N failed, M cancelled` annotation; no wedge.
|
||||
- **Approve-merge conflict** — keep parent in `WaitingForReview`; surface the
|
||||
conflicting child like the current merge-conflict path.
|
||||
- **Existing rows** — planning parents currently sitting at `Idle`+`Finalized` with
|
||||
live children: behavior change is forward-only (new finalizes use the new flow);
|
||||
no migration needed since `Status`/`PlanningPhase` columns already exist.
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
@@ -10,7 +11,12 @@ namespace ClaudeDo.App;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public static ServiceProvider Services { get; set; } = null!;
|
||||
private readonly IServiceProvider? _services;
|
||||
|
||||
// Parameterless ctor is required by the XAML previewer / designer.
|
||||
public App() { }
|
||||
|
||||
public App(IServiceProvider services) => _services = services;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -21,14 +27,19 @@ public partial class App : Application
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var services = _services
|
||||
?? throw new InvalidOperationException("App was constructed without a service provider.");
|
||||
|
||||
FocusClearing.Install();
|
||||
|
||||
desktop.MainWindow = new MainWindow
|
||||
{
|
||||
DataContext = Services.GetRequiredService<IslandsShellViewModel>(),
|
||||
DataContext = services.GetRequiredService<IslandsShellViewModel>(),
|
||||
};
|
||||
|
||||
// Kick off the SignalR retry loop — reconnects indefinitely if the worker
|
||||
// is not up yet, or goes down and comes back.
|
||||
_ = Services.GetRequiredService<WorkerClient>().StartAsync();
|
||||
_ = services.GetRequiredService<WorkerClient>().StartAsync();
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 55 KiB |
@@ -28,5 +28,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ClaudeDo.Ui\ClaudeDo.Ui.csproj" />
|
||||
<ProjectReference Include="..\ClaudeDo.Localization\ClaudeDo.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\ClaudeDo.Localization\Locales.targets" />
|
||||
</Project>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using Avalonia;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Releases;
|
||||
using ClaudeDo.Ui;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.Services.Interfaces;
|
||||
using ClaudeDo.Ui.ViewModels;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using ClaudeDo.Ui.ViewModels.Modals;
|
||||
@@ -11,6 +14,9 @@ using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -29,7 +35,6 @@ sealed class Program
|
||||
SetCurrentProcessExplicitAppUserModelID("ClaudeDo.App");
|
||||
|
||||
var services = BuildServices();
|
||||
App.Services = services;
|
||||
|
||||
using (var scope = services.CreateScope())
|
||||
{
|
||||
@@ -39,7 +44,7 @@ sealed class Program
|
||||
|
||||
try
|
||||
{
|
||||
BuildAvaloniaApp()
|
||||
ConfigureAppBuilder(AppBuilder.Configure(() => new App(services)))
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
finally
|
||||
@@ -52,8 +57,12 @@ sealed class Program
|
||||
}
|
||||
}
|
||||
|
||||
// Parameterless entry point required by the XAML previewer / designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
=> ConfigureAppBuilder(AppBuilder.Configure<App>());
|
||||
|
||||
private static AppBuilder ConfigureAppBuilder(AppBuilder builder)
|
||||
=> builder
|
||||
.UsePlatformDetect()
|
||||
#if DEBUG
|
||||
.WithDeveloperTools()
|
||||
@@ -70,6 +79,18 @@ sealed class Program
|
||||
|
||||
// Infrastructure
|
||||
sc.AddSingleton(settings);
|
||||
var localesDir = Path.Combine(AppContext.BaseDirectory, "locales");
|
||||
var localeStore = LocaleStore.Load(localesDir);
|
||||
var initialLang = !string.IsNullOrWhiteSpace(settings.Language)
|
||||
? settings.Language
|
||||
: CultureResolver.Resolve(
|
||||
CultureInfo.CurrentUICulture.Name,
|
||||
localeStore.Available.Select(l => l.Code).ToArray(),
|
||||
fallback: "en");
|
||||
var localizer = new Localizer(localeStore, initialLang);
|
||||
TrExtension.Localizer = localizer;
|
||||
ClaudeDo.Ui.Localization.Loc.Current = localizer;
|
||||
sc.AddSingleton<ILocalizer>(localizer);
|
||||
sc.AddDbContextFactory<ClaudeDoDbContext>(opt =>
|
||||
opt.UseSqlite($"Data Source={dbPath}"));
|
||||
sc.AddScoped<ClaudeDoDbContext>(sp =>
|
||||
@@ -78,6 +99,7 @@ sealed class Program
|
||||
// Services
|
||||
sc.AddSingleton<GitService>();
|
||||
sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService<AppSettings>().SignalRUrl));
|
||||
sc.AddSingleton<IWorkerClient>(sp => sp.GetRequiredService<WorkerClient>());
|
||||
|
||||
// Release check + installer update
|
||||
sc.AddSingleton<HttpClient>(_ => new HttpClient { Timeout = TimeSpan.FromSeconds(10) });
|
||||
@@ -100,6 +122,7 @@ sealed class Program
|
||||
sc.AddTransient<WorktreesOverviewModalViewModel>();
|
||||
sc.AddTransient<Func<WorktreesOverviewModalViewModel>>(sp => () => sp.GetRequiredService<WorktreesOverviewModalViewModel>());
|
||||
sc.AddSingleton<IPrimeScheduleApi, WorkerPrimeScheduleApi>();
|
||||
sc.AddSingleton<INotesApi, WorkerNotesApi>();
|
||||
sc.AddTransient<PrimeClaudeTabViewModel>();
|
||||
sc.AddTransient<SettingsModalViewModel>();
|
||||
sc.AddTransient<MergeModalViewModel>();
|
||||
@@ -107,6 +130,11 @@ sealed class Program
|
||||
sc.AddTransient<ListSettingsModalViewModel>();
|
||||
sc.AddTransient<RepoImportModalViewModel>();
|
||||
sc.AddTransient<Func<RepoImportModalViewModel>>(sp => () => sp.GetRequiredService<RepoImportModalViewModel>());
|
||||
sc.AddTransient<WeeklyReportModalViewModel>();
|
||||
sc.AddTransient<Func<WeeklyReportModalViewModel>>(sp => () => sp.GetRequiredService<WeeklyReportModalViewModel>());
|
||||
sc.AddSingleton<Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>>(sp =>
|
||||
taskId => new ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel(
|
||||
sp.GetRequiredService<WorkerClient>(), taskId));
|
||||
|
||||
// Islands shell VMs
|
||||
sc.AddSingleton<ListsIslandViewModel>(sp =>
|
||||
@@ -122,8 +150,15 @@ sealed class Program
|
||||
new DetailsIslandViewModel(
|
||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||
sp.GetRequiredService<WorkerClient>(),
|
||||
sp));
|
||||
sc.AddSingleton<IslandsShellViewModel>();
|
||||
sp,
|
||||
sp.GetRequiredService<INotesApi>()));
|
||||
sc.AddSingleton<IslandsShellViewModel>(sp =>
|
||||
{
|
||||
var shell = ActivatorUtilities.CreateInstance<IslandsShellViewModel>(sp);
|
||||
shell.ConflictResolverFactory =
|
||||
sp.GetRequiredService<Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>>();
|
||||
return shell;
|
||||
});
|
||||
|
||||
return sc.BuildServiceProvider();
|
||||
}
|
||||
|
||||
@@ -4,21 +4,27 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio
|
||||
|
||||
## Models
|
||||
|
||||
- **TaskEntity** — Id, ListId, Title, Description, Status (`Idle|Queued|Running|Done|Failed|Cancelled`), PlanningPhase (`None|Active|Finalized` — parent-only), BlockedByTaskId (nullable FK to predecessor in a chain), ScheduledFor, Result, LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath (nullable overrides), IsStarred, IsMyDay, Notes, ParentTaskId, PlanningSessionId, PlanningSessionToken, PlanningFinalizedAt, CreatedBy. Legacy values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired; existing rows backfill automatically via the `RetireLegacyTaskStatus` migration.
|
||||
- **TaskEntity** — Id, ListId, Title, Description, Status (`Idle|Queued|Running|WaitingForChildren|WaitingForReview|Done|Failed|Cancelled`), PlanningPhase (`None|Active|Finalized` — parent-only), BlockedByTaskId (nullable FK to predecessor in a chain), ScheduledFor, Result, ReviewFeedback (nullable; reviewer's rejection comment, consumed and cleared by the runner on the next re-run), LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns (nullable overrides), IsStarred, IsMyDay, Notes, ParentTaskId, PlanningSessionId, PlanningSessionToken, PlanningFinalizedAt, CreatedBy. Legacy values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired; existing rows backfill automatically via the `RetireLegacyTaskStatus` migration.
|
||||
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt
|
||||
- **ListConfigEntity** — ListId (PK, 1:1 with list), Model, SystemPrompt, AgentPath (all nullable)
|
||||
- **ListConfigEntity** — ListId (PK, 1:1 with list), Model, SystemPrompt, AgentPath, MaxTurns (all nullable)
|
||||
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, State (Active|Merged|Discarded|Kept)
|
||||
- **TaskRunEntity** — per-run record (session_id, tokens, turns, result, structured output, exit code, log path)
|
||||
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, stored as `days_of_week` int), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on the selected weekdays; no date range.
|
||||
- **DailyNoteEntity** — Id, Date (DateOnly), Text, SortOrder, CreatedAt → table `daily_notes`
|
||||
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → table `week_reports`, unique index on (start_date, end_date)
|
||||
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`), `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`), and `DailyPrepMaxTasks` (int, default 5, column `daily_prep_max_tasks` — hard cap on how many open tasks the daily-prep / "Prime Claude" feature may place in MyDay)
|
||||
- **SubtaskEntity**, **AppSettingsEntity**, **AgentInfo** — existing helpers / settings / record for scanned agent files
|
||||
|
||||
## Repositories
|
||||
|
||||
All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Queued -> Running` claim lives in the Worker's `QueuePicker` (uses `FromSqlRaw`), not here.
|
||||
|
||||
- **TaskRepository** — CRUD, planning helpers (`CreateChildAsync`, `SetPlanningStartedAsync`, `DiscardPlanningAsync`, `TryCompleteParentAsync`, `UpdateChildAsync`), `UpdateAgentSettingsAsync` (model / system-prompt / agent-path overrides). Status-mutation primitives `MarkRunningAsync` / `MarkDoneAsync` / `MarkFailedAsync` / `FlipAllRunningToFailedAsync` are `internal` and called only by `TaskStateService` in the worker. `CreateChildAsync` produces children with `Status=Idle, PlanningPhase=None`; once their parent's `PlanningPhase` becomes `Finalized`, the chain coordinator queues them.
|
||||
- **TaskRepository** — CRUD, planning helpers (`CreateChildAsync`, `SetPlanningStartedAsync`, `DiscardPlanningAsync`, `UpdateChildAsync`), `UpdateAgentSettingsAsync` (model / system-prompt / agent-path overrides). Status-mutation primitives `MarkRunningAsync` / `MarkDoneAsync` / `MarkFailedAsync` / `FlipAllRunningToFailedAsync` are `internal` and called only by `TaskStateService` in the worker. `CreateChildAsync` produces children with `Status=Idle, PlanningPhase=None`; once their parent's `PlanningPhase` becomes `Finalized`, the chain coordinator queues them.
|
||||
- **ListRepository** — CRUD, `GetConfigAsync` / `SetConfigAsync` (upsert) / `DeleteConfigAsync` for `list_config`
|
||||
- **WorktreeRepository** — CRUD, `UpdateHeadAsync`, `SetStateAsync`
|
||||
- **TaskRunRepository**, **SubtaskRepository**, **AppSettingsRepository**
|
||||
- **DailyNoteRepository** — `ListByDayAsync`, `ListBetweenAsync`, `AddAsync`, `UpdateAsync`, `DeleteAsync`
|
||||
- **WeekReportRepository** — `GetByRangeAsync`, `UpsertAsync`
|
||||
|
||||
## Infrastructure
|
||||
|
||||
@@ -29,11 +35,11 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
|
||||
|
||||
## Git
|
||||
|
||||
- **GitService** — async wrapper around git CLI (ProcessStartInfo, no shell). Operations: worktree add/remove, add all, commit (stdin for message), merge ff-only, rev-parse, diff-stat, has-changes, is-git-repo
|
||||
- **GitService** — async wrapper around git CLI (ProcessStartInfo, no shell). Operations: worktree add/remove, add all, commit (stdin for message), merge ff-only, rev-parse, diff-stat, has-changes, is-git-repo, `PreviewMergeAsync` (non-destructive mergeability check via `git merge-tree --write-tree`), `CountChangedFilesAsync`
|
||||
|
||||
## Schema
|
||||
|
||||
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`).
|
||||
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns. Migration `DailyPrepMaxTasks` added the `daily_prep_max_tasks` column to `app_settings` (no new tables).
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -1,15 +1,45 @@
|
||||
using System.Data.Common;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Seeding;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace ClaudeDo.Data;
|
||||
|
||||
public class ClaudeDoDbContext : DbContext
|
||||
{
|
||||
// Runs PRAGMA foreign_keys=ON on every EF-managed connection open so FK
|
||||
// enforcement is active for all IDbContextFactory-created contexts, not
|
||||
// just the single context used in MigrateAndConfigure.
|
||||
private sealed class SqliteForeignKeyInterceptor : DbConnectionInterceptor
|
||||
{
|
||||
internal static readonly SqliteForeignKeyInterceptor Instance = new();
|
||||
|
||||
public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData)
|
||||
=> Apply(connection);
|
||||
|
||||
public override Task ConnectionOpenedAsync(DbConnection connection, ConnectionEndEventData eventData, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Apply(connection);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void Apply(DbConnection connection)
|
||||
{
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA foreign_keys=ON;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public ClaudeDoDbContext(DbContextOptions<ClaudeDoDbContext> options) : base(options) { }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
=> optionsBuilder.AddInterceptors(SqliteForeignKeyInterceptor.Instance);
|
||||
|
||||
public DbSet<TaskEntity> Tasks => Set<TaskEntity>();
|
||||
public DbSet<ListEntity> Lists => Set<ListEntity>();
|
||||
public DbSet<ListConfigEntity> ListConfigs => Set<ListConfigEntity>();
|
||||
@@ -18,10 +48,27 @@ public class ClaudeDoDbContext : DbContext
|
||||
public DbSet<SubtaskEntity> Subtasks => Set<SubtaskEntity>();
|
||||
public DbSet<AppSettingsEntity> AppSettings => Set<AppSettingsEntity>();
|
||||
public DbSet<PrimeScheduleEntity> PrimeSchedules => Set<PrimeScheduleEntity>();
|
||||
public DbSet<DailyNoteEntity> DailyNotes => Set<DailyNoteEntity>();
|
||||
public DbSet<WeekReportEntity> WeekReports => Set<WeekReportEntity>();
|
||||
|
||||
private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
|
||||
new(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
||||
|
||||
private static readonly ValueConverter<DateTime?, DateTime?> UtcNullableConverter =
|
||||
new(v => v, v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : null);
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ClaudeDoDbContext).Assembly);
|
||||
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
foreach (var property in entityType.GetProperties())
|
||||
{
|
||||
if (property.ClrType == typeof(DateTime) && property.GetValueConverter() == null)
|
||||
property.SetValueConverter(UtcConverter);
|
||||
else if (property.ClrType == typeof(DateTime?) && property.GetValueConverter() == null)
|
||||
property.SetValueConverter(UtcNullableConverter);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -22,6 +22,9 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
||||
builder.Property(s => s.DefaultPermissionMode)
|
||||
.HasColumnName("default_permission_mode").IsRequired().HasDefaultValue("bypassPermissions");
|
||||
|
||||
builder.Property(s => s.MaxParallelExecutions)
|
||||
.HasColumnName("max_parallel_executions").IsRequired().HasDefaultValue(1);
|
||||
|
||||
builder.Property(s => s.WorktreeStrategy)
|
||||
.HasColumnName("worktree_strategy").IsRequired().HasDefaultValue("sibling");
|
||||
builder.Property(s => s.CentralWorktreeRoot)
|
||||
@@ -34,6 +37,13 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
||||
builder.Property(s => s.RepoImportFolders)
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
builder.Property(s => s.ReportExcludedPaths).HasColumnName("report_excluded_paths");
|
||||
builder.Property(s => s.StandupWeekday).HasColumnName("standup_weekday")
|
||||
.IsRequired().HasDefaultValue((int)DayOfWeek.Wednesday);
|
||||
|
||||
builder.Property(s => s.DailyPrepMaxTasks)
|
||||
.HasColumnName("daily_prep_max_tasks").IsRequired().HasDefaultValue(5);
|
||||
|
||||
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ClaudeDo.Data.Configuration;
|
||||
|
||||
public class DailyNoteEntityConfiguration : IEntityTypeConfiguration<DailyNoteEntity>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<DailyNoteEntity> builder)
|
||||
{
|
||||
builder.ToTable("daily_notes");
|
||||
builder.HasKey(n => n.Id);
|
||||
builder.Property(n => n.Id).HasColumnName("id").ValueGeneratedNever();
|
||||
builder.Property(n => n.Date).HasColumnName("note_date").IsRequired();
|
||||
builder.Property(n => n.Text).HasColumnName("text").IsRequired();
|
||||
builder.Property(n => n.SortOrder).HasColumnName("sort_order").IsRequired();
|
||||
builder.Property(n => n.CreatedAt).HasColumnName("created_at").IsRequired();
|
||||
builder.HasIndex(n => n.Date);
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,6 @@ public class ListConfigEntityConfiguration : IEntityTypeConfiguration<ListConfig
|
||||
builder.Property(c => c.Model).HasColumnName("model");
|
||||
builder.Property(c => c.SystemPrompt).HasColumnName("system_prompt");
|
||||
builder.Property(c => c.AgentPath).HasColumnName("agent_path");
|
||||
builder.Property(c => c.MaxTurns).HasColumnName("max_turns");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ public class ListEntityConfiguration : IEntityTypeConfiguration<ListEntity>
|
||||
builder.Property(l => l.CreatedAt).HasColumnName("created_at").IsRequired();
|
||||
builder.Property(l => l.WorkingDir).HasColumnName("working_dir");
|
||||
builder.Property(l => l.DefaultCommitType).HasColumnName("default_commit_type").IsRequired().HasDefaultValue("chore");
|
||||
builder.Property(l => l.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
||||
|
||||
builder.HasIndex(l => l.SortOrder).HasDatabaseName("idx_lists_sort");
|
||||
|
||||
builder.HasOne(l => l.Config)
|
||||
.WithOne(c => c.List)
|
||||
|
||||
@@ -13,10 +13,9 @@ public class PrimeScheduleEntityConfiguration : IEntityTypeConfiguration<PrimeSc
|
||||
builder.HasKey(s => s.Id);
|
||||
builder.Property(s => s.Id).HasColumnName("id").ValueGeneratedNever();
|
||||
|
||||
builder.Property(s => s.StartDate).HasColumnName("start_date").IsRequired();
|
||||
builder.Property(s => s.EndDate).HasColumnName("end_date").IsRequired();
|
||||
builder.Property(s => s.Days).HasColumnName("days_of_week")
|
||||
.IsRequired().HasDefaultValue(PrimeDays.Weekdays);
|
||||
builder.Property(s => s.TimeOfDay).HasColumnName("time_of_day").IsRequired();
|
||||
builder.Property(s => s.WorkdaysOnly).HasColumnName("workdays_only").IsRequired().HasDefaultValue(true);
|
||||
builder.Property(s => s.Enabled).HasColumnName("enabled").IsRequired().HasDefaultValue(true);
|
||||
builder.Property(s => s.LastRunAt).HasColumnName("last_run_at");
|
||||
builder.Property(s => s.PromptOverride).HasColumnName("prompt_override");
|
||||
|
||||
@@ -11,24 +11,28 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
||||
private static string StatusToString(TaskStatus v)
|
||||
=> v switch
|
||||
{
|
||||
TaskStatus.Idle => "idle",
|
||||
TaskStatus.Queued => "queued",
|
||||
TaskStatus.Running => "running",
|
||||
TaskStatus.Done => "done",
|
||||
TaskStatus.Failed => "failed",
|
||||
TaskStatus.Cancelled => "cancelled",
|
||||
TaskStatus.Idle => "idle",
|
||||
TaskStatus.Queued => "queued",
|
||||
TaskStatus.Running => "running",
|
||||
TaskStatus.WaitingForReview => "waiting_for_review",
|
||||
TaskStatus.WaitingForChildren => "waiting_for_children",
|
||||
TaskStatus.Done => "done",
|
||||
TaskStatus.Failed => "failed",
|
||||
TaskStatus.Cancelled => "cancelled",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(v)),
|
||||
};
|
||||
|
||||
private static TaskStatus StatusFromString(string v)
|
||||
=> v switch
|
||||
{
|
||||
"idle" => TaskStatus.Idle,
|
||||
"queued" => TaskStatus.Queued,
|
||||
"running" => TaskStatus.Running,
|
||||
"done" => TaskStatus.Done,
|
||||
"failed" => TaskStatus.Failed,
|
||||
"cancelled" => TaskStatus.Cancelled,
|
||||
"idle" => TaskStatus.Idle,
|
||||
"queued" => TaskStatus.Queued,
|
||||
"running" => TaskStatus.Running,
|
||||
"waiting_for_review" => TaskStatus.WaitingForReview,
|
||||
"waiting_for_children" => TaskStatus.WaitingForChildren,
|
||||
"done" => TaskStatus.Done,
|
||||
"failed" => TaskStatus.Failed,
|
||||
"cancelled" => TaskStatus.Cancelled,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(v)),
|
||||
};
|
||||
|
||||
@@ -72,6 +76,8 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
||||
builder.Property(t => t.BlockedByTaskId).HasColumnName("blocked_by_task_id");
|
||||
builder.Property(t => t.ScheduledFor).HasColumnName("scheduled_for");
|
||||
builder.Property(t => t.Result).HasColumnName("result");
|
||||
builder.Property(t => t.ReviewFeedback).HasColumnName("review_feedback");
|
||||
builder.Property(t => t.RoadblockCount).HasColumnName("roadblock_count").HasDefaultValue(0);
|
||||
builder.Property(t => t.LogPath).HasColumnName("log_path");
|
||||
builder.Property(t => t.CreatedAt).HasColumnName("created_at").IsRequired();
|
||||
builder.Property(t => t.StartedAt).HasColumnName("started_at");
|
||||
@@ -80,6 +86,7 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
||||
builder.Property(t => t.Model).HasColumnName("model");
|
||||
builder.Property(t => t.SystemPrompt).HasColumnName("system_prompt");
|
||||
builder.Property(t => t.AgentPath).HasColumnName("agent_path");
|
||||
builder.Property(t => t.MaxTurns).HasColumnName("max_turns");
|
||||
builder.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false);
|
||||
builder.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false);
|
||||
builder.Property(t => t.Notes).HasColumnName("notes");
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ClaudeDo.Data.Configuration;
|
||||
|
||||
public class WeekReportEntityConfiguration : IEntityTypeConfiguration<WeekReportEntity>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<WeekReportEntity> builder)
|
||||
{
|
||||
builder.ToTable("week_reports");
|
||||
builder.HasKey(r => r.Id);
|
||||
builder.Property(r => r.Id).HasColumnName("id").ValueGeneratedNever();
|
||||
builder.Property(r => r.StartDate).HasColumnName("start_date").IsRequired();
|
||||
builder.Property(r => r.EndDate).HasColumnName("end_date").IsRequired();
|
||||
builder.Property(r => r.Markdown).HasColumnName("markdown").IsRequired();
|
||||
builder.Property(r => r.GeneratedAt).HasColumnName("generated_at").IsRequired();
|
||||
builder.HasIndex(r => new { r.StartDate, r.EndDate }).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,7 @@ public sealed class ReviewFilter : ITaskListFilter
|
||||
{
|
||||
public string Id => "virtual:review";
|
||||
public bool Matches(TaskEntity t) =>
|
||||
t.Status == TaskStatus.Done &&
|
||||
t.Worktree is { State: WorktreeState.Active };
|
||||
t.Status == TaskStatus.WaitingForReview;
|
||||
public bool ShouldCount(TaskEntity t) => Matches(t);
|
||||
public bool MatchesAsContext(TaskEntity t, IReadOnlyList<TaskEntity> all) => false;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,15 @@ using System.Text;
|
||||
|
||||
namespace ClaudeDo.Data.Git;
|
||||
|
||||
public sealed record MergePreview(bool Supported, bool Clean, IReadOnlyList<string> ConflictFiles);
|
||||
|
||||
public sealed class GitService
|
||||
{
|
||||
// git mutates shared .git/worktrees/ metadata during `worktree add`; concurrent adds
|
||||
// race and fail with "failed to read .git/worktrees/<other>/commondir". Serialize them
|
||||
// process-wide so parallel task starts don't collide.
|
||||
private static readonly SemaphoreSlim WorktreeAddGate = new(1, 1);
|
||||
|
||||
public async Task<bool> IsGitRepoAsync(string dir, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, _, _) = await RunGitAsync(dir, ["rev-parse", "--git-dir"], ct);
|
||||
@@ -21,10 +28,30 @@ public sealed class GitService
|
||||
|
||||
public async Task WorktreeAddAsync(string repoDir, string branchName, string worktreePath, string baseCommit, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, _, stderr) = await RunGitAsync(repoDir,
|
||||
["worktree", "add", "-b", branchName, worktreePath, baseCommit], ct);
|
||||
if (exitCode != 0)
|
||||
throw new InvalidOperationException($"git worktree add failed (exit {exitCode}): {stderr}");
|
||||
await WorktreeAddGate.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
const int maxAttempts = 3;
|
||||
for (var attempt = 1; ; attempt++)
|
||||
{
|
||||
var (exitCode, _, stderr) = await RunGitAsync(repoDir,
|
||||
["worktree", "add", "-b", branchName, worktreePath, baseCommit], ct);
|
||||
if (exitCode == 0)
|
||||
return;
|
||||
|
||||
// Transient races leave a half-written worktree metadata dir; retry briefly.
|
||||
var transient = stderr.Contains("commondir", StringComparison.OrdinalIgnoreCase)
|
||||
|| stderr.Contains("failed to read", StringComparison.OrdinalIgnoreCase);
|
||||
if (!transient || attempt >= maxAttempts)
|
||||
throw new InvalidOperationException($"git worktree add failed (exit {exitCode}): {stderr}");
|
||||
|
||||
await Task.Delay(150 * attempt, ct);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
WorktreeAddGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetStatusPorcelainAsync(string workingDirectory, CancellationToken ct = default)
|
||||
@@ -97,6 +124,20 @@ public sealed class GitService
|
||||
return await GetDiffAsync(worktreePath, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diff between two commits, run in any repo that can reach them. Used to view a
|
||||
/// task's changes after its worktree has been merged away (the commits survive on
|
||||
/// the target branch even though the worktree directory and branch ref are gone).
|
||||
/// </summary>
|
||||
public async Task<string> GetCommitRangeDiffAsync(string repoDir, string baseCommit, string headCommit, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, stderr) = await RunGitAsync(repoDir,
|
||||
["diff", $"{baseCommit}..{headCommit}"], ct);
|
||||
if (exitCode != 0)
|
||||
throw new InvalidOperationException($"git diff {baseCommit}..{headCommit} failed (exit {exitCode}): {stderr}");
|
||||
return stdout;
|
||||
}
|
||||
|
||||
public async Task<string> DiffStatAsync(string worktreePath, string baseCommit, string headCommit, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath,
|
||||
@@ -236,6 +277,67 @@ public sealed class GitService
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a conflicted file's blob at a merge stage: 1=base, 2=ours, 3=theirs.
|
||||
/// Returns null when the stage doesn't exist (e.g. add/add conflict has no base).
|
||||
/// Output is NOT trimmed so file content round-trips exactly.
|
||||
/// </summary>
|
||||
public async Task<string?> ShowStageAsync(string repoDir, int stage, string path, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["show", $":{stage}:{path}"], ct, trimOutput: false);
|
||||
return exitCode == 0 ? stdout : null;
|
||||
}
|
||||
|
||||
public async Task AddPathAsync(string repoDir, string path, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["add", "--", path], ct);
|
||||
if (exitCode != 0)
|
||||
throw new InvalidOperationException($"git add '{path}' failed (exit {exitCode}): {stderr}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Non-destructive mergeability probe via `git merge-tree --write-tree`. Writes only
|
||||
/// loose objects — the working tree, index, and refs are left untouched.
|
||||
/// </summary>
|
||||
public async Task<MergePreview> PreviewMergeAsync(
|
||||
string repoDir, string targetBranch, string sourceBranch, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, _) = await RunGitAsync(repoDir,
|
||||
["merge-tree", "--write-tree", "--name-only", targetBranch, sourceBranch], ct);
|
||||
|
||||
if (exitCode == 0)
|
||||
return new MergePreview(true, true, Array.Empty<string>());
|
||||
|
||||
if (exitCode == 1)
|
||||
{
|
||||
// stdout: <tree-oid>\n<file>\n...\n\n<informational messages>
|
||||
var lines = stdout.Split('\n');
|
||||
var files = new List<string>();
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i].TrimEnd('\r');
|
||||
if (string.IsNullOrWhiteSpace(line)) break;
|
||||
files.Add(line.Trim());
|
||||
}
|
||||
return new MergePreview(true, false, files);
|
||||
}
|
||||
|
||||
// Any other exit (e.g. git too old: "unknown option --write-tree").
|
||||
return new MergePreview(false, false, Array.Empty<string>());
|
||||
}
|
||||
|
||||
/// <summary>Count of files that differ on <paramref name="sourceBranch"/> since its merge base with the target.</summary>
|
||||
public async Task<int> CountChangedFilesAsync(
|
||||
string repoDir, string targetBranch, string sourceBranch, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, _) = await RunGitAsync(repoDir,
|
||||
["diff", "--name-only", $"{targetBranch}...{sourceBranch}"], ct);
|
||||
if (exitCode != 0) return 0;
|
||||
return stdout
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Count(s => s.Length > 0);
|
||||
}
|
||||
|
||||
public async Task MergeFfOnlyAsync(string repoDir, string branchName, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["merge", "--ff-only", branchName], ct);
|
||||
@@ -244,7 +346,7 @@ public sealed class GitService
|
||||
}
|
||||
|
||||
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunGitAsync(
|
||||
string workDir, IEnumerable<string> args, CancellationToken ct, string? stdinData = null)
|
||||
string workDir, IEnumerable<string> args, CancellationToken ct, string? stdinData = null, bool trimOutput = true)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
@@ -293,6 +395,6 @@ public sealed class GitService
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
return (proc.ExitCode, stdout.TrimEnd(), stderr.TrimEnd());
|
||||
return (proc.ExitCode, trimOutput ? stdout.TrimEnd() : stdout, stderr.TrimEnd());
|
||||
}
|
||||
}
|
||||
|
||||
482
src/ClaudeDo.Data/Migrations/20260416064948_InitialCreate.Designer.cs
generated
Normal file
482
src/ClaudeDo.Data/Migrations/20260416064948_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,482 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260416064948_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("tags", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1L,
|
||||
Name = "agent"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2L,
|
||||
Name = "manual"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.Property<string>("list_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("list_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("list_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.Property<string>("task_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("task_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("task_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("list_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("task_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
498
src/ClaudeDo.Data/Migrations/20260420075929_AddTaskFlagsAndNotes.Designer.cs
generated
Normal file
498
src/ClaudeDo.Data/Migrations/20260420075929_AddTaskFlagsAndNotes.Designer.cs
generated
Normal file
@@ -0,0 +1,498 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260420075929_AddTaskFlagsAndNotes")]
|
||||
partial class AddTaskFlagsAndNotes
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("tags", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1L,
|
||||
Name = "agent"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2L,
|
||||
Name = "manual"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.Property<string>("list_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("list_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("list_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.Property<string>("task_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("task_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("task_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("list_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("task_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
572
src/ClaudeDo.Data/Migrations/20260421113614_AddAppSettings.Designer.cs
generated
Normal file
572
src/ClaudeDo.Data/Migrations/20260421113614_AddAppSettings.Designer.cs
generated
Normal file
@@ -0,0 +1,572 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260421113614_AddAppSettings")]
|
||||
partial class AddAppSettings
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 30,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "bypassPermissions",
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("tags", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1L,
|
||||
Name = "agent"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2L,
|
||||
Name = "manual"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.Property<string>("list_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("list_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("list_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.Property<string>("task_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("task_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("task_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("list_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("task_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
581
src/ClaudeDo.Data/Migrations/20260422120000_AddTaskSortOrder.Designer.cs
generated
Normal file
581
src/ClaudeDo.Data/Migrations/20260422120000_AddTaskSortOrder.Designer.cs
generated
Normal file
@@ -0,0 +1,581 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260422120000_AddTaskSortOrder")]
|
||||
partial class AddTaskSortOrder
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 30,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "bypassPermissions",
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("tags", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1L,
|
||||
Name = "agent"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2L,
|
||||
Name = "manual"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.Property<string>("list_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("list_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("list_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.Property<string>("task_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("task_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("task_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("list_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("task_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
609
src/ClaudeDo.Data/Migrations/20260423154708_AddPlanningSupport.Designer.cs
generated
Normal file
609
src/ClaudeDo.Data/Migrations/20260423154708_AddPlanningSupport.Designer.cs
generated
Normal file
@@ -0,0 +1,609 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260423154708_AddPlanningSupport")]
|
||||
partial class AddPlanningSupport
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 30,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "bypassPermissions",
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("tags", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1L,
|
||||
Name = "agent"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2L,
|
||||
Name = "manual"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.Property<string>("list_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("list_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("list_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.Property<string>("task_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("task_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("task_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("list_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("task_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
613
src/ClaudeDo.Data/Migrations/20260424212250_AddTaskCreatedBy.Designer.cs
generated
Normal file
613
src/ClaudeDo.Data/Migrations/20260424212250_AddTaskCreatedBy.Designer.cs
generated
Normal file
@@ -0,0 +1,613 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260424212250_AddTaskCreatedBy")]
|
||||
partial class AddTaskCreatedBy
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "bypassPermissions",
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("tags", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1L,
|
||||
Name = "agent"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2L,
|
||||
Name = "manual"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.Property<string>("list_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("list_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("list_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.Property<string>("task_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("task_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("task_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("list_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("task_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
632
src/ClaudeDo.Data/Migrations/20260427082248_AddPlanningPhaseAndBlockedBy.Designer.cs
generated
Normal file
632
src/ClaudeDo.Data/Migrations/20260427082248_AddPlanningPhaseAndBlockedBy.Designer.cs
generated
Normal file
@@ -0,0 +1,632 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260427082248_AddPlanningPhaseAndBlockedBy")]
|
||||
partial class AddPlanningPhaseAndBlockedBy
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("tags", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1L,
|
||||
Name = "agent"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2L,
|
||||
Name = "manual"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.Property<string>("list_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("list_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("list_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.Property<string>("task_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("task_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("task_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("list_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("task_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
632
src/ClaudeDo.Data/Migrations/20260427130058_RetireLegacyTaskStatus.Designer.cs
generated
Normal file
632
src/ClaudeDo.Data/Migrations/20260427130058_RetireLegacyTaskStatus.Designer.cs
generated
Normal file
@@ -0,0 +1,632 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260427130058_RetireLegacyTaskStatus")]
|
||||
partial class RetireLegacyTaskStatus
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("tags", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1L,
|
||||
Name = "agent"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2L,
|
||||
Name = "manual"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.Property<string>("list_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("list_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("list_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.Property<string>("task_id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("tag_id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("task_id", "tag_id");
|
||||
|
||||
b.HasIndex("tag_id");
|
||||
|
||||
b.ToTable("task_tags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("list_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("list_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("task_tags", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("task_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
587
src/ClaudeDo.Data/Migrations/20260519044715_RemoveTags.Designer.cs
generated
Normal file
587
src/ClaudeDo.Data/Migrations/20260519044715_RemoveTags.Designer.cs
generated
Normal file
@@ -0,0 +1,587 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260519044715_RemoveTags")]
|
||||
partial class RemoveTags
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
|
||||
b.Property<string>("PromptOverride")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.Property<bool>("WorkdaysOnly")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("workdays_only");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
591
src/ClaudeDo.Data/Migrations/20260529142614_AddRepoImportFolders.Designer.cs
generated
Normal file
591
src/ClaudeDo.Data/Migrations/20260529142614_AddRepoImportFolders.Designer.cs
generated
Normal file
@@ -0,0 +1,591 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260529142614_AddRepoImportFolders")]
|
||||
partial class AddRepoImportFolders
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
|
||||
b.Property<string>("PromptOverride")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.Property<bool>("WorkdaysOnly")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("workdays_only");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
600
src/ClaudeDo.Data/Migrations/20260601114247_AddListSortOrder.Designer.cs
generated
Normal file
600
src/ClaudeDo.Data/Migrations/20260601114247_AddListSortOrder.Designer.cs
generated
Normal file
@@ -0,0 +1,600 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260601114247_AddListSortOrder")]
|
||||
partial class AddListSortOrder
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("idx_lists_sort");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
|
||||
b.Property<string>("PromptOverride")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.Property<bool>("WorkdaysOnly")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("workdays_only");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddListSortOrder : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "sort_order",
|
||||
table: "lists",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
// Backfill existing rows with a dense order (0..N-1) by creation time
|
||||
// so today's sidebar order is preserved after the migration.
|
||||
migrationBuilder.Sql("""
|
||||
WITH ordered AS (
|
||||
SELECT id, (row_number() OVER (ORDER BY created_at) - 1) AS rn
|
||||
FROM lists
|
||||
)
|
||||
UPDATE lists SET sort_order = (SELECT rn FROM ordered WHERE ordered.id = lists.id);
|
||||
""");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "idx_lists_sort",
|
||||
table: "lists",
|
||||
column: "sort_order");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "idx_lists_sort",
|
||||
table: "lists");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "sort_order",
|
||||
table: "lists");
|
||||
}
|
||||
}
|
||||
}
|
||||
607
src/ClaudeDo.Data/Migrations/20260601133737_AddMaxParallelExecutions.Designer.cs
generated
Normal file
607
src/ClaudeDo.Data/Migrations/20260601133737_AddMaxParallelExecutions.Designer.cs
generated
Normal file
@@ -0,0 +1,607 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260601133737_AddMaxParallelExecutions")]
|
||||
partial class AddMaxParallelExecutions
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("MaxParallelExecutions")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("max_parallel_executions");
|
||||
|
||||
b.Property<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("idx_lists_sort");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
|
||||
b.Property<string>("PromptOverride")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.Property<bool>("WorkdaysOnly")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("workdays_only");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMaxParallelExecutions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "max_parallel_executions",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 1);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "app_settings",
|
||||
keyColumn: "id",
|
||||
keyValue: 1,
|
||||
column: "max_parallel_executions",
|
||||
value: 1);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "max_parallel_executions",
|
||||
table: "app_settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
607
src/ClaudeDo.Data/Migrations/20260601140000_NormalizeListIdFormat.Designer.cs
generated
Normal file
607
src/ClaudeDo.Data/Migrations/20260601140000_NormalizeListIdFormat.Designer.cs
generated
Normal file
@@ -0,0 +1,607 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260601140000_NormalizeListIdFormat")]
|
||||
partial class NormalizeListIdFormat
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("MaxParallelExecutions")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("max_parallel_executions");
|
||||
|
||||
b.Property<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("idx_lists_sort");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
|
||||
b.Property<string>("PromptOverride")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.Property<bool>("WorkdaysOnly")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("workdays_only");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class NormalizeListIdFormat : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// SQLite: PRAGMA foreign_keys must run outside a transaction.
|
||||
migrationBuilder.Sql("PRAGMA foreign_keys = OFF;", suppressTransaction: true);
|
||||
|
||||
// Normalize tasks.list_id: 32-char compact hex → 36-char dashed UUID
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE tasks
|
||||
SET list_id = substr(list_id,1,8)||'-'||substr(list_id,9,4)||'-'||substr(list_id,13,4)||'-'||substr(list_id,17,4)||'-'||substr(list_id,21,12)
|
||||
WHERE length(list_id) = 32;
|
||||
""");
|
||||
|
||||
// Normalize list_config.list_id (also the PK of that table)
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE list_config
|
||||
SET list_id = substr(list_id,1,8)||'-'||substr(list_id,9,4)||'-'||substr(list_id,13,4)||'-'||substr(list_id,17,4)||'-'||substr(list_id,21,12)
|
||||
WHERE length(list_id) = 32;
|
||||
""");
|
||||
|
||||
// Normalize lists.id (PK — must come last)
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE lists
|
||||
SET id = substr(id,1,8)||'-'||substr(id,9,4)||'-'||substr(id,13,4)||'-'||substr(id,17,4)||'-'||substr(id,21,12)
|
||||
WHERE length(id) = 32;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("PRAGMA foreign_keys = ON;", suppressTransaction: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("PRAGMA foreign_keys = OFF;", suppressTransaction: true);
|
||||
|
||||
migrationBuilder.Sql("UPDATE tasks SET list_id = replace(list_id,'-','') WHERE length(list_id) = 36;");
|
||||
migrationBuilder.Sql("UPDATE list_config SET list_id = replace(list_id,'-','') WHERE length(list_id) = 36;");
|
||||
migrationBuilder.Sql("UPDATE lists SET id = replace(id,'-','') WHERE length(id) = 36;");
|
||||
|
||||
migrationBuilder.Sql("PRAGMA foreign_keys = ON;", suppressTransaction: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
611
src/ClaudeDo.Data/Migrations/20260601150820_AddReviewFeedback.Designer.cs
generated
Normal file
611
src/ClaudeDo.Data/Migrations/20260601150820_AddReviewFeedback.Designer.cs
generated
Normal file
@@ -0,0 +1,611 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260601150820_AddReviewFeedback")]
|
||||
partial class AddReviewFeedback
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("MaxParallelExecutions")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("max_parallel_executions");
|
||||
|
||||
b.Property<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("idx_lists_sort");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
|
||||
b.Property<string>("PromptOverride")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.Property<bool>("WorkdaysOnly")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("workdays_only");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<string>("ReviewFeedback")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("review_feedback");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddReviewFeedback : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "review_feedback",
|
||||
table: "tasks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "review_feedback",
|
||||
table: "tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
603
src/ClaudeDo.Data/Migrations/20260602060000_PrimeWeekdays.Designer.cs
generated
Normal file
603
src/ClaudeDo.Data/Migrations/20260602060000_PrimeWeekdays.Designer.cs
generated
Normal file
@@ -0,0 +1,603 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260602060000_PrimeWeekdays")]
|
||||
partial class PrimeWeekdays
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("MaxParallelExecutions")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("max_parallel_executions");
|
||||
|
||||
b.Property<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("idx_lists_sort");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("Days")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(31)
|
||||
.HasColumnName("days_of_week");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
|
||||
b.Property<string>("PromptOverride")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<string>("ReviewFeedback")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("review_feedback");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/ClaudeDo.Data/Migrations/20260602060000_PrimeWeekdays.cs
Normal file
48
src/ClaudeDo.Data/Migrations/20260602060000_PrimeWeekdays.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PrimeWeekdays : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "days_of_week",
|
||||
table: "prime_schedules",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 31);
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE prime_schedules SET days_of_week = CASE WHEN workdays_only = 1 THEN 31 ELSE 127 END;");
|
||||
|
||||
migrationBuilder.DropColumn(name: "start_date", table: "prime_schedules");
|
||||
migrationBuilder.DropColumn(name: "end_date", table: "prime_schedules");
|
||||
migrationBuilder.DropColumn(name: "workdays_only", table: "prime_schedules");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
name: "start_date", table: "prime_schedules",
|
||||
type: "TEXT", nullable: false, defaultValue: new DateOnly(2000, 1, 1));
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
name: "end_date", table: "prime_schedules",
|
||||
type: "TEXT", nullable: false, defaultValue: new DateOnly(2099, 12, 31));
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "workdays_only", table: "prime_schedules",
|
||||
type: "INTEGER", nullable: false, defaultValue: true);
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE prime_schedules SET workdays_only = CASE WHEN days_of_week = 127 THEN 0 ELSE 1 END;");
|
||||
|
||||
migrationBuilder.DropColumn(name: "days_of_week", table: "prime_schedules");
|
||||
}
|
||||
}
|
||||
}
|
||||
675
src/ClaudeDo.Data/Migrations/20260603072822_WeeklyReport.Designer.cs
generated
Normal file
675
src/ClaudeDo.Data/Migrations/20260603072822_WeeklyReport.Designer.cs
generated
Normal file
@@ -0,0 +1,675 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260603072822_WeeklyReport")]
|
||||
partial class WeeklyReport
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("MaxParallelExecutions")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("max_parallel_executions");
|
||||
|
||||
b.Property<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
b.Property<string>("ReportExcludedPaths")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("report_excluded_paths");
|
||||
|
||||
b.Property<int>("StandupWeekday")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(3)
|
||||
.HasColumnName("standup_weekday");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("note_date");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.ToTable("daily_notes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("idx_lists_sort");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("Days")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(31)
|
||||
.HasColumnName("days_of_week");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
|
||||
b.Property<string>("PromptOverride")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<string>("ReviewFeedback")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("review_feedback");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTime>("GeneratedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("generated_at");
|
||||
|
||||
b.Property<string>("Markdown")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("markdown");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StartDate", "EndDate")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("week_reports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/ClaudeDo.Data/Migrations/20260603072822_WeeklyReport.cs
Normal file
94
src/ClaudeDo.Data/Migrations/20260603072822_WeeklyReport.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class WeeklyReport : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "report_excluded_paths",
|
||||
table: "app_settings",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "standup_weekday",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 3);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "daily_notes",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
note_date = table.Column<DateOnly>(type: "TEXT", nullable: false),
|
||||
text = table.Column<string>(type: "TEXT", nullable: false),
|
||||
sort_order = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
created_at = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_daily_notes", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "week_reports",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
start_date = table.Column<DateOnly>(type: "TEXT", nullable: false),
|
||||
end_date = table.Column<DateOnly>(type: "TEXT", nullable: false),
|
||||
markdown = table.Column<string>(type: "TEXT", nullable: false),
|
||||
generated_at = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_week_reports", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "app_settings",
|
||||
keyColumn: "id",
|
||||
keyValue: 1,
|
||||
columns: new[] { "report_excluded_paths", "standup_weekday" },
|
||||
values: new object[] { null, 3 });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_daily_notes_note_date",
|
||||
table: "daily_notes",
|
||||
column: "note_date");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_week_reports_start_date_end_date",
|
||||
table: "week_reports",
|
||||
columns: new[] { "start_date", "end_date" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "daily_notes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "week_reports");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "report_excluded_paths",
|
||||
table: "app_settings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "standup_weekday",
|
||||
table: "app_settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
682
src/ClaudeDo.Data/Migrations/20260603141020_DailyPrepMaxTasks.Designer.cs
generated
Normal file
682
src/ClaudeDo.Data/Migrations/20260603141020_DailyPrepMaxTasks.Designer.cs
generated
Normal file
@@ -0,0 +1,682 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260603141020_DailyPrepMaxTasks")]
|
||||
partial class DailyPrepMaxTasks
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<int>("DailyPrepMaxTasks")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(5)
|
||||
.HasColumnName("daily_prep_max_tasks");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("MaxParallelExecutions")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("max_parallel_executions");
|
||||
|
||||
b.Property<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
b.Property<string>("ReportExcludedPaths")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("report_excluded_paths");
|
||||
|
||||
b.Property<int>("StandupWeekday")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(3)
|
||||
.HasColumnName("standup_weekday");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("note_date");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.ToTable("daily_notes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("idx_lists_sort");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("Days")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(31)
|
||||
.HasColumnName("days_of_week");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
|
||||
b.Property<string>("PromptOverride")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<string>("ReviewFeedback")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("review_feedback");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTime>("GeneratedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("generated_at");
|
||||
|
||||
b.Property<string>("Markdown")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("markdown");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StartDate", "EndDate")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("week_reports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class DailyPrepMaxTasks : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "daily_prep_max_tasks",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 5);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "app_settings",
|
||||
keyColumn: "id",
|
||||
keyValue: 1,
|
||||
column: "daily_prep_max_tasks",
|
||||
value: 5);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "daily_prep_max_tasks",
|
||||
table: "app_settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
690
src/ClaudeDo.Data/Migrations/20260604101453_InheritableMaxTurns.Designer.cs
generated
Normal file
690
src/ClaudeDo.Data/Migrations/20260604101453_InheritableMaxTurns.Designer.cs
generated
Normal file
@@ -0,0 +1,690 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260604101453_InheritableMaxTurns")]
|
||||
partial class InheritableMaxTurns
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<int>("DailyPrepMaxTasks")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(5)
|
||||
.HasColumnName("daily_prep_max_tasks");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("MaxParallelExecutions")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("max_parallel_executions");
|
||||
|
||||
b.Property<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
b.Property<string>("ReportExcludedPaths")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("report_excluded_paths");
|
||||
|
||||
b.Property<int>("StandupWeekday")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(3)
|
||||
.HasColumnName("standup_weekday");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("note_date");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.ToTable("daily_notes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<int?>("MaxTurns")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("max_turns");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("idx_lists_sort");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("Days")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(31)
|
||||
.HasColumnName("days_of_week");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
|
||||
b.Property<string>("PromptOverride")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<int?>("MaxTurns")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("max_turns");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<string>("ReviewFeedback")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("review_feedback");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTime>("GeneratedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("generated_at");
|
||||
|
||||
b.Property<string>("Markdown")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("markdown");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StartDate", "EndDate")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("week_reports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InheritableMaxTurns : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "max_turns",
|
||||
table: "tasks",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "max_turns",
|
||||
table: "list_config",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "max_turns",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "max_turns",
|
||||
table: "list_config");
|
||||
}
|
||||
}
|
||||
}
|
||||
696
src/ClaudeDo.Data/Migrations/20260604125720_AddRoadblockCount.Designer.cs
generated
Normal file
696
src/ClaudeDo.Data/Migrations/20260604125720_AddRoadblockCount.Designer.cs
generated
Normal file
@@ -0,0 +1,696 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260604125720_AddRoadblockCount")]
|
||||
partial class AddRoadblockCount
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<int>("DailyPrepMaxTasks")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(5)
|
||||
.HasColumnName("daily_prep_max_tasks");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("MaxParallelExecutions")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("max_parallel_executions");
|
||||
|
||||
b.Property<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
b.Property<string>("ReportExcludedPaths")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("report_excluded_paths");
|
||||
|
||||
b.Property<int>("StandupWeekday")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(3)
|
||||
.HasColumnName("standup_weekday");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("note_date");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.ToTable("daily_notes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<int?>("MaxTurns")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("max_turns");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("idx_lists_sort");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("Days")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(31)
|
||||
.HasColumnName("days_of_week");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
|
||||
b.Property<string>("PromptOverride")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<int?>("MaxTurns")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("max_turns");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<string>("ReviewFeedback")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("review_feedback");
|
||||
|
||||
b.Property<int>("RoadblockCount")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("roadblock_count");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTime>("GeneratedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("generated_at");
|
||||
|
||||
b.Property<string>("Markdown")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("markdown");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StartDate", "EndDate")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("week_reports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddRoadblockCount : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "roadblock_count",
|
||||
table: "tasks",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "roadblock_count",
|
||||
table: "tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
696
src/ClaudeDo.Data/Migrations/20260609000000_UniqueListName.Designer.cs
generated
Normal file
696
src/ClaudeDo.Data/Migrations/20260609000000_UniqueListName.Designer.cs
generated
Normal file
@@ -0,0 +1,696 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ClaudeDoDbContext))]
|
||||
[Migration("20260609000000_UniqueListName")]
|
||||
partial class UniqueListName
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CentralWorktreeRoot")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<int>("DailyPrepMaxTasks")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(5)
|
||||
.HasColumnName("daily_prep_max_tasks");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("")
|
||||
.HasColumnName("default_claude_instructions");
|
||||
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sonnet")
|
||||
.HasColumnName("default_model");
|
||||
|
||||
b.Property<string>("DefaultPermissionMode")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("MaxParallelExecutions")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("max_parallel_executions");
|
||||
|
||||
b.Property<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
b.Property<string>("ReportExcludedPaths")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("report_excluded_paths");
|
||||
|
||||
b.Property<int>("StandupWeekday")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(3)
|
||||
.HasColumnName("standup_weekday");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(7)
|
||||
.HasColumnName("worktree_auto_cleanup_days");
|
||||
|
||||
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||
|
||||
b.Property<string>("WorktreeStrategy")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("sibling")
|
||||
.HasColumnName("worktree_strategy");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("app_settings", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("note_date");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.ToTable("daily_notes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<int?>("MaxTurns")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("max_turns");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DefaultCommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("idx_lists_sort");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("Days")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(31)
|
||||
.HasColumnName("days_of_week");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
|
||||
b.Property<string>("PromptOverride")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("completed");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("OrderNum")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("order_num");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_subtasks_task_id");
|
||||
|
||||
b.ToTable("subtasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AgentPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<string>("BlockedByTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("blocked_by_task_id");
|
||||
|
||||
b.Property<string>("CommitType")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("commit_type");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_by");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_my_day");
|
||||
|
||||
b.Property<bool>("IsStarred")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_starred");
|
||||
|
||||
b.Property<string>("ListId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("list_id");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<int?>("MaxTurns")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("max_turns");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notes");
|
||||
|
||||
b.Property<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_finalized_at");
|
||||
|
||||
b.Property<string>("PlanningPhase")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("none")
|
||||
.HasColumnName("planning_phase");
|
||||
|
||||
b.Property<string>("PlanningSessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_id");
|
||||
|
||||
b.Property<string>("PlanningSessionToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("planning_session_token");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<string>("ReviewFeedback")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("review_feedback");
|
||||
|
||||
b.Property<int>("RoadblockCount")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("roadblock_count");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
b.HasIndex("ParentTaskId")
|
||||
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("idx_tasks_status");
|
||||
|
||||
b.HasIndex("ListId", "SortOrder")
|
||||
.HasDatabaseName("idx_tasks_list_sort");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
b.Property<int?>("ExitCode")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exit_code");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_retry");
|
||||
|
||||
b.Property<string>("LogPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
b.Property<int>("RunNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("run_number");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("StructuredOutputJson")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("structured_output");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int?>("TokensIn")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_in");
|
||||
|
||||
b.Property<int?>("TokensOut")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("tokens_out");
|
||||
|
||||
b.Property<int?>("TurnCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("turn_count");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TaskId")
|
||||
.HasDatabaseName("idx_task_runs_task_id");
|
||||
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTime>("GeneratedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("generated_at");
|
||||
|
||||
b.Property<string>("Markdown")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("markdown");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StartDate", "EndDate")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("week_reports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<string>("BaseCommit")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("base_commit");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("branch_name");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DiffStat")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("diff_stat");
|
||||
|
||||
b.Property<string>("HeadCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("active")
|
||||
.HasColumnName("state");
|
||||
|
||||
b.HasKey("TaskId");
|
||||
|
||||
b.ToTable("worktrees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithOne("Config")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Subtasks")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("BlockedByTaskId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||
.WithMany("Tasks")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("List");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithMany("Runs")
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||
.WithOne("Worktree")
|
||||
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Task");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||
{
|
||||
b.Navigation("Config");
|
||||
|
||||
b.Navigation("Tasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("Runs");
|
||||
|
||||
b.Navigation("Subtasks");
|
||||
|
||||
b.Navigation("Worktree");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UniqueListName : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Remove duplicate list rows that have no tasks — keep the oldest rowid.
|
||||
// This handles the startup-race case where both App and Worker seeded
|
||||
// the same default list names concurrently.
|
||||
migrationBuilder.Sql("""
|
||||
DELETE FROM lists
|
||||
WHERE (SELECT COUNT(*) FROM tasks WHERE list_id = lists.id) = 0
|
||||
AND rowid NOT IN (
|
||||
SELECT MIN(l2.rowid) FROM lists l2 WHERE l2.name = lists.name
|
||||
)
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,12 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("central_worktree_root");
|
||||
|
||||
b.Property<int>("DailyPrepMaxTasks")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(5)
|
||||
.HasColumnName("daily_prep_max_tasks");
|
||||
|
||||
b.Property<string>("DefaultClaudeInstructions")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -54,10 +60,26 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasDefaultValue("bypassPermissions")
|
||||
.HasColumnName("default_permission_mode");
|
||||
|
||||
b.Property<int>("MaxParallelExecutions")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("max_parallel_executions");
|
||||
|
||||
b.Property<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
|
||||
b.Property<string>("ReportExcludedPaths")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("report_excluded_paths");
|
||||
|
||||
b.Property<int>("StandupWeekday")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(3)
|
||||
.HasColumnName("standup_weekday");
|
||||
|
||||
b.Property<int>("WorktreeAutoCleanupDays")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
@@ -85,16 +107,49 @@ namespace ClaudeDo.Data.Migrations
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("note_date");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.ToTable("daily_notes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||
{
|
||||
b.Property<string>("ListId")
|
||||
@@ -105,6 +160,10 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("agent_path");
|
||||
|
||||
b.Property<int?>("MaxTurns")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("max_turns");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
@@ -140,12 +199,21 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("sort_order");
|
||||
|
||||
b.Property<string>("WorkingDir")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("working_dir");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("idx_lists_sort");
|
||||
|
||||
b.ToTable("lists", (string)null);
|
||||
});
|
||||
|
||||
@@ -159,16 +227,18 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("Days")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(31)
|
||||
.HasColumnName("days_of_week");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastRunAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_run_at");
|
||||
@@ -177,20 +247,10 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt_override");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.Property<TimeSpan>("TimeOfDay")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time_of_day");
|
||||
|
||||
b.Property<bool>("WorkdaysOnly")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true)
|
||||
.HasColumnName("workdays_only");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("prime_schedules", (string)null);
|
||||
@@ -292,6 +352,10 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<int?>("MaxTurns")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("max_turns");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
@@ -327,6 +391,16 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result");
|
||||
|
||||
b.Property<string>("ReviewFeedback")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("review_feedback");
|
||||
|
||||
b.Property<int>("RoadblockCount")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("roadblock_count");
|
||||
|
||||
b.Property<DateTime?>("ScheduledFor")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
@@ -453,6 +527,37 @@ namespace ClaudeDo.Data.Migrations
|
||||
b.ToTable("task_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("end_date");
|
||||
|
||||
b.Property<DateTime>("GeneratedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("generated_at");
|
||||
|
||||
b.Property<string>("Markdown")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("markdown");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("start_date");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StartDate", "EndDate")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("week_reports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||
{
|
||||
b.Property<string>("TaskId")
|
||||
|
||||
@@ -11,6 +11,8 @@ public sealed class AppSettingsEntity
|
||||
public int DefaultMaxTurns { get; set; } = 100;
|
||||
public string DefaultPermissionMode { get; set; } = "auto";
|
||||
|
||||
public int MaxParallelExecutions { get; set; } = 1;
|
||||
|
||||
public string WorktreeStrategy { get; set; } = "sibling";
|
||||
public string? CentralWorktreeRoot { get; set; }
|
||||
public bool WorktreeAutoCleanupEnabled { get; set; }
|
||||
@@ -18,4 +20,11 @@ public sealed class AppSettingsEntity
|
||||
|
||||
// JSON array of parent folders remembered by the repo-import modal.
|
||||
public string? RepoImportFolders { get; set; }
|
||||
// JSON array of path prefixes whose sessions are excluded from the weekly report.
|
||||
public string? ReportExcludedPaths { get; set; }
|
||||
// DayOfWeek the standup happens on; default Wednesday. Drives the report's default range.
|
||||
public int StandupWeekday { get; set; } = (int)DayOfWeek.Wednesday;
|
||||
|
||||
// Max number of open tasks the daily prep ("Prime Claude") may place in MyDay.
|
||||
public int DailyPrepMaxTasks { get; set; } = 5;
|
||||
}
|
||||
|
||||
10
src/ClaudeDo.Data/Models/DailyNoteEntity.cs
Normal file
10
src/ClaudeDo.Data/Models/DailyNoteEntity.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace ClaudeDo.Data.Models;
|
||||
|
||||
public sealed class DailyNoteEntity
|
||||
{
|
||||
public string Id { get; init; } = Guid.NewGuid().ToString();
|
||||
public DateOnly Date { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ public sealed class ListConfigEntity
|
||||
public string? Model { get; set; }
|
||||
public string? SystemPrompt { get; set; }
|
||||
public string? AgentPath { get; set; }
|
||||
public int? MaxTurns { get; set; }
|
||||
|
||||
// Navigation property
|
||||
public ListEntity List { get; set; } = null!;
|
||||
|
||||
@@ -7,6 +7,7 @@ public sealed class ListEntity
|
||||
public required DateTime CreatedAt { get; init; }
|
||||
public string? WorkingDir { get; set; }
|
||||
public string DefaultCommitType { get; set; } = CommitTypeRegistry.DefaultType;
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public ListConfigEntity? Config { get; set; }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user