mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-03 06:12:07 -07:00
Compare commits
38 Commits
v0.8.0
...
codex/wind
| Author | SHA1 | Date | |
|---|---|---|---|
|
aa0385904e
|
|||
|
bf06463bb3
|
|||
|
61ab1b76fc
|
|||
| 8a5805550f | |||
| 78d0da03dd | |||
| 8b9ac99f3d | |||
| 85e3aa4c6b | |||
|
640c8acd7c
|
|||
| d6c72806bb | |||
|
3502cdc607
|
|||
| d51e7fe401 | |||
|
f9a4039ad2
|
|||
|
8e5c21b443
|
|||
|
55b350c3a2
|
|||
|
54324df3be
|
|||
| 35adf8299c | |||
|
2d4f2d1139
|
|||
|
77e632276b
|
|||
|
4c95b57885
|
|||
|
242402b253
|
|||
|
61d15f9431
|
|||
|
508864bcbb
|
|||
|
23c54bb01e
|
|||
|
ec667c64e8
|
|||
|
39b2ccad8e
|
|||
|
23815945bf
|
|||
|
9dca83acd9
|
|||
|
55300e2d8c
|
|||
|
28afd15134
|
|||
|
58304757aa
|
|||
|
c95518e94a
|
|||
| 5ee4617607 | |||
|
842008b089
|
|||
|
6f56a0bcf6
|
|||
| 5feed360ca | |||
| c17f0a4080 | |||
| 0317c7f011 | |||
|
13797b5005
|
20
.agents/plugins/marketplace.json
Normal file
20
.agents/plugins/marketplace.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "subminer-local",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "SubMiner Local"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "subminer-workflow",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./plugins/subminer-workflow"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "Productivity"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,127 +1,22 @@
|
|||||||
---
|
---
|
||||||
name: "subminer-change-verification"
|
name: 'subminer-change-verification'
|
||||||
description: "Use when working in the SubMiner repo and you need to verify code changes actually work. Covers targeted regression checks during debugging and pre-handoff verification, with cheap-first lane selection for config, docs, launcher/plugin, runtime-compat, and optional real-runtime escalation."
|
description: 'Compatibility shim. Canonical SubMiner change verification workflow now lives in the repo-local subminer-workflow plugin.'
|
||||||
---
|
---
|
||||||
|
|
||||||
# SubMiner Change Verification
|
# Compatibility Shim
|
||||||
|
|
||||||
Use this skill for SubMiner code changes. Default to cheap, repo-native verification first. Escalate only when the changed behavior actually depends on Electron, mpv, overlay/window tracking, or other GUI-sensitive runtime behavior.
|
Canonical source:
|
||||||
|
|
||||||
## Scripts
|
- `plugins/subminer-workflow/skills/subminer-change-verification/SKILL.md`
|
||||||
|
|
||||||
- `scripts/classify_subminer_diff.sh`
|
Canonical helper scripts:
|
||||||
- Emits suggested lanes and flags from explicit paths or current git changes.
|
|
||||||
- `scripts/verify_subminer_change.sh`
|
|
||||||
- Runs selected lanes, captures artifacts, and writes a compact summary.
|
|
||||||
|
|
||||||
If you need an explicit installed path, use the directory that contains this `SKILL.md`. The helper scripts live under:
|
- `plugins/subminer-workflow/skills/subminer-change-verification/scripts/classify_subminer_diff.sh`
|
||||||
|
- `plugins/subminer-workflow/skills/subminer-change-verification/scripts/verify_subminer_change.sh`
|
||||||
|
|
||||||
```bash
|
When this shim is invoked:
|
||||||
export SUBMINER_VERIFY_SKILL="<path-to-skill>"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Default workflow
|
1. Read the canonical plugin-owned skill.
|
||||||
|
2. Follow the plugin-owned skill as the source of truth.
|
||||||
1. Inspect the changed files or user-requested area.
|
3. Use the wrapper scripts in this shim directory only for compatibility with existing commands, docs, and backlog history.
|
||||||
2. Run the classifier unless you already know the right lane.
|
4. Do not duplicate workflow changes here; update the plugin-owned skill and scripts instead.
|
||||||
3. Run the verifier with the cheapest sufficient lane set.
|
|
||||||
4. If the classifier emits `flag:real-runtime-candidate`, do not jump straight to runtime verification. First run the non-runtime lanes.
|
|
||||||
5. Escalate to explicit `--lane real-runtime --allow-real-runtime` only when cheaper lanes cannot validate the behavior claim.
|
|
||||||
6. Return:
|
|
||||||
- verification summary
|
|
||||||
- exact commands run
|
|
||||||
- artifact paths
|
|
||||||
- skipped lanes and blockers
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
Repo-source quick start:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash .agents/skills/subminer-change-verification/scripts/classify_subminer_diff.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Installed-skill quick start:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash "$SUBMINER_VERIFY_SKILL/scripts/classify_subminer_diff.sh"
|
|
||||||
```
|
|
||||||
|
|
||||||
Classify explicit files:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash .agents/skills/subminer-change-verification/scripts/classify_subminer_diff.sh \
|
|
||||||
launcher/main.ts \
|
|
||||||
plugin/subminer/lifecycle.lua \
|
|
||||||
src/main/runtime/mpv-client-runtime-service.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Run automatic lane selection:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash .agents/skills/subminer-change-verification/scripts/verify_subminer_change.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Installed-skill form:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash "$SUBMINER_VERIFY_SKILL/scripts/verify_subminer_change.sh"
|
|
||||||
```
|
|
||||||
|
|
||||||
Run targeted lanes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash .agents/skills/subminer-change-verification/scripts/verify_subminer_change.sh \
|
|
||||||
--lane launcher-plugin \
|
|
||||||
--lane runtime-compat
|
|
||||||
```
|
|
||||||
|
|
||||||
Dry-run to inspect planned commands and artifact layout:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash .agents/skills/subminer-change-verification/scripts/verify_subminer_change.sh \
|
|
||||||
--dry-run \
|
|
||||||
launcher/main.ts \
|
|
||||||
src/main.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Lane guidance
|
|
||||||
|
|
||||||
- `docs`
|
|
||||||
- For `docs-site/`, `docs/`, and doc-only edits.
|
|
||||||
- `config`
|
|
||||||
- For `src/config/` and config-template-sensitive edits.
|
|
||||||
- `core`
|
|
||||||
- For general source changes where `typecheck` + `test:fast` is the best cheap signal.
|
|
||||||
- `launcher-plugin`
|
|
||||||
- For `launcher/`, `plugin/subminer/`, plugin gating scripts, and wrapper/mpv routing work.
|
|
||||||
- `runtime-compat`
|
|
||||||
- For `src/main*`, runtime/composer wiring, mpv/overlay services, window trackers, and dist-sensitive behavior.
|
|
||||||
- `real-runtime`
|
|
||||||
- Only after deliberate escalation.
|
|
||||||
|
|
||||||
## Real Runtime Escalation
|
|
||||||
|
|
||||||
Escalate only when the change claim depends on actual runtime behavior, for example:
|
|
||||||
|
|
||||||
- overlay appears, hides, or tracks a real mpv window
|
|
||||||
- mpv launch flags or pause-until-ready behavior
|
|
||||||
- plugin/socket/auto-start handshake under a real player
|
|
||||||
- macOS/window-tracker/focus-sensitive behavior
|
|
||||||
|
|
||||||
If the environment cannot support authoritative runtime verification, report the blocker explicitly. Do not silently downgrade a runtime-required claim to a pass.
|
|
||||||
|
|
||||||
## Artifact contract
|
|
||||||
|
|
||||||
The verifier writes under `.tmp/skill-verification/<timestamp>/`:
|
|
||||||
|
|
||||||
- `summary.json`
|
|
||||||
- `summary.txt`
|
|
||||||
- `classification.txt`
|
|
||||||
- `env.txt`
|
|
||||||
- `lanes.txt`
|
|
||||||
- `steps.tsv`
|
|
||||||
- `steps/*.stdout.log`
|
|
||||||
- `steps/*.stderr.log`
|
|
||||||
|
|
||||||
On failure, quote the exact failing command and point at the artifact directory.
|
|
||||||
|
|||||||
@@ -1,163 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
usage() {
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
cat <<'EOF'
|
REPO_ROOT=$(cd "$SCRIPT_DIR/../../../.." && pwd)
|
||||||
Usage: classify_subminer_diff.sh [path ...]
|
TARGET="$REPO_ROOT/plugins/subminer-workflow/skills/subminer-change-verification/scripts/classify_subminer_diff.sh"
|
||||||
|
|
||||||
Emit suggested verification lanes for explicit paths or current local git changes.
|
if [[ ! -x "$TARGET" ]]; then
|
||||||
|
echo "Missing canonical script: $TARGET" >&2
|
||||||
Output format:
|
exit 1
|
||||||
lane:<name>
|
|
||||||
flag:<name>
|
|
||||||
reason:<text>
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
has_item() {
|
|
||||||
local needle=$1
|
|
||||||
shift || true
|
|
||||||
local item
|
|
||||||
for item in "$@"; do
|
|
||||||
if [[ "$item" == "$needle" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
add_lane() {
|
|
||||||
local lane=$1
|
|
||||||
if ! has_item "$lane" "${LANES[@]:-}"; then
|
|
||||||
LANES+=("$lane")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
add_flag() {
|
|
||||||
local flag=$1
|
|
||||||
if ! has_item "$flag" "${FLAGS[@]:-}"; then
|
|
||||||
FLAGS+=("$flag")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
add_reason() {
|
|
||||||
REASONS+=("$1")
|
|
||||||
}
|
|
||||||
|
|
||||||
collect_git_paths() {
|
|
||||||
local top_level
|
|
||||||
if ! top_level=$(git rev-parse --show-toplevel 2>/dev/null); then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
(
|
|
||||||
cd "$top_level"
|
|
||||||
if git rev-parse --verify HEAD >/dev/null 2>&1; then
|
|
||||||
git diff --name-only --relative HEAD --
|
|
||||||
git diff --name-only --relative --cached --
|
|
||||||
else
|
|
||||||
git diff --name-only --relative --
|
|
||||||
git diff --name-only --relative --cached --
|
|
||||||
fi
|
|
||||||
git ls-files --others --exclude-standard
|
|
||||||
) | awk 'NF' | sort -u
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
declare -a PATHS=()
|
exec "$TARGET" "$@"
|
||||||
declare -a LANES=()
|
|
||||||
declare -a FLAGS=()
|
|
||||||
declare -a REASONS=()
|
|
||||||
|
|
||||||
if [[ $# -gt 0 ]]; then
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
PATHS+=("$1")
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
else
|
|
||||||
while IFS= read -r line; do
|
|
||||||
[[ -n "$line" ]] && PATHS+=("$line")
|
|
||||||
done < <(collect_git_paths)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ${#PATHS[@]} -eq 0 ]]; then
|
|
||||||
add_lane "core"
|
|
||||||
add_reason "no changed paths detected -> default to core"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for path in "${PATHS[@]}"; do
|
|
||||||
specialized=0
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
docs-site/*|docs/*|changes/*|README.md)
|
|
||||||
add_lane "docs"
|
|
||||||
add_reason "$path -> docs"
|
|
||||||
specialized=1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
src/config/*|src/generate-config-example.ts|src/verify-config-example.ts|docs-site/public/config.example.jsonc|config.example.jsonc)
|
|
||||||
add_lane "config"
|
|
||||||
add_reason "$path -> config"
|
|
||||||
specialized=1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
launcher/*|plugin/subminer/*|plugin/subminer.conf|scripts/test-plugin-*|scripts/get-mpv-window-*|scripts/configure-plugin-binary-path.mjs)
|
|
||||||
add_lane "launcher-plugin"
|
|
||||||
add_reason "$path -> launcher-plugin"
|
|
||||||
add_flag "real-runtime-candidate"
|
|
||||||
add_reason "$path -> real-runtime-candidate"
|
|
||||||
specialized=1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
src/main.ts|src/main-entry.ts|src/preload.ts|src/main/*|src/core/services/mpv*|src/core/services/overlay*|src/renderer/*|src/window-trackers/*|scripts/prepare-build-assets.mjs)
|
|
||||||
add_lane "runtime-compat"
|
|
||||||
add_reason "$path -> runtime-compat"
|
|
||||||
add_flag "real-runtime-candidate"
|
|
||||||
add_reason "$path -> real-runtime-candidate"
|
|
||||||
specialized=1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [[ "$specialized" == "0" ]]; then
|
|
||||||
case "$path" in
|
|
||||||
src/*|package.json|tsconfig*.json|scripts/*|Makefile)
|
|
||||||
add_lane "core"
|
|
||||||
add_reason "$path -> core"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
package.json|src/main.ts|src/main-entry.ts|src/preload.ts)
|
|
||||||
add_flag "broad-impact"
|
|
||||||
add_reason "$path -> broad-impact"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ ${#LANES[@]} -eq 0 ]]; then
|
|
||||||
add_lane "core"
|
|
||||||
add_reason "no lane-specific matches -> default to core"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for lane in "${LANES[@]}"; do
|
|
||||||
printf 'lane:%s\n' "$lane"
|
|
||||||
done
|
|
||||||
|
|
||||||
for flag in "${FLAGS[@]}"; do
|
|
||||||
printf 'flag:%s\n' "$flag"
|
|
||||||
done
|
|
||||||
|
|
||||||
for reason in "${REASONS[@]}"; do
|
|
||||||
printf 'reason:%s\n' "$reason"
|
|
||||||
done
|
|
||||||
|
|||||||
@@ -1,566 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: verify_subminer_change.sh [options] [path ...]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--lane <name> Force a verification lane. Repeatable.
|
|
||||||
--artifact-dir <dir> Use an explicit artifact directory.
|
|
||||||
--allow-real-runtime Allow explicit real-runtime execution.
|
|
||||||
--allow-real-gui Deprecated alias for --allow-real-runtime.
|
|
||||||
--dry-run Record planned steps without executing commands.
|
|
||||||
--help Show this help text.
|
|
||||||
|
|
||||||
If no lanes are supplied, the script classifies the provided paths. If no paths are
|
|
||||||
provided, it classifies the current local git changes.
|
|
||||||
|
|
||||||
Authoritative real-runtime verification should be requested with explicit path
|
|
||||||
arguments instead of relying on inferred local git changes.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
timestamp() {
|
|
||||||
date +%Y%m%d-%H%M%S
|
|
||||||
}
|
|
||||||
|
|
||||||
timestamp_iso() {
|
|
||||||
date -u +%Y-%m-%dT%H:%M:%SZ
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_session_id() {
|
|
||||||
local tmp_dir
|
|
||||||
tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/subminer-verify-$(timestamp)-XXXXXX")
|
|
||||||
basename "$tmp_dir"
|
|
||||||
rmdir "$tmp_dir"
|
|
||||||
}
|
|
||||||
|
|
||||||
has_item() {
|
|
||||||
local needle=$1
|
|
||||||
shift || true
|
|
||||||
local item
|
|
||||||
for item in "$@"; do
|
|
||||||
if [[ "$item" == "$needle" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize_lane_name() {
|
|
||||||
case "$1" in
|
|
||||||
real-gui)
|
|
||||||
printf '%s' "real-runtime"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
printf '%s' "$1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
add_lane() {
|
|
||||||
local lane
|
|
||||||
lane=$(normalize_lane_name "$1")
|
|
||||||
if ! has_item "$lane" "${SELECTED_LANES[@]:-}"; then
|
|
||||||
SELECTED_LANES+=("$lane")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
add_blocker() {
|
|
||||||
BLOCKERS+=("$1")
|
|
||||||
BLOCKED=1
|
|
||||||
}
|
|
||||||
|
|
||||||
append_step_record() {
|
|
||||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
||||||
"$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" >>"$STEPS_TSV"
|
|
||||||
}
|
|
||||||
|
|
||||||
record_env() {
|
|
||||||
{
|
|
||||||
printf 'repo_root=%s\n' "$REPO_ROOT"
|
|
||||||
printf 'session_id=%s\n' "$SESSION_ID"
|
|
||||||
printf 'artifact_dir=%s\n' "$ARTIFACT_DIR"
|
|
||||||
printf 'path_selection_mode=%s\n' "$PATH_SELECTION_MODE"
|
|
||||||
printf 'dry_run=%s\n' "$DRY_RUN"
|
|
||||||
printf 'allow_real_runtime=%s\n' "$ALLOW_REAL_RUNTIME"
|
|
||||||
printf 'session_home=%s\n' "$SESSION_HOME"
|
|
||||||
printf 'session_xdg_config_home=%s\n' "$SESSION_XDG_CONFIG_HOME"
|
|
||||||
printf 'session_mpv_dir=%s\n' "$SESSION_MPV_DIR"
|
|
||||||
printf 'session_logs_dir=%s\n' "$SESSION_LOGS_DIR"
|
|
||||||
printf 'session_mpv_log=%s\n' "$SESSION_MPV_LOG"
|
|
||||||
printf 'pwd=%s\n' "$(pwd)"
|
|
||||||
git rev-parse --short HEAD 2>/dev/null | sed 's/^/git_head=/' || true
|
|
||||||
git status --short 2>/dev/null || true
|
|
||||||
if [[ ${#PATH_ARGS[@]} -gt 0 ]]; then
|
|
||||||
printf 'requested_paths=\n'
|
|
||||||
printf ' %s\n' "${PATH_ARGS[@]}"
|
|
||||||
fi
|
|
||||||
} >"$ARTIFACT_DIR/env.txt"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_step() {
|
|
||||||
local lane=$1
|
|
||||||
local name=$2
|
|
||||||
local command=$3
|
|
||||||
local note=${4:-}
|
|
||||||
local slug=${name//[^a-zA-Z0-9_-]/-}
|
|
||||||
local stdout_rel="steps/${slug}.stdout.log"
|
|
||||||
local stderr_rel="steps/${slug}.stderr.log"
|
|
||||||
local stdout_path="$ARTIFACT_DIR/$stdout_rel"
|
|
||||||
local stderr_path="$ARTIFACT_DIR/$stderr_rel"
|
|
||||||
local status exit_code
|
|
||||||
|
|
||||||
COMMANDS_RUN+=("$command")
|
|
||||||
printf '%s\n' "$command" >"$ARTIFACT_DIR/steps/${slug}.command.txt"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "1" ]]; then
|
|
||||||
printf '[dry-run] %s\n' "$command" >"$stdout_path"
|
|
||||||
: >"$stderr_path"
|
|
||||||
status="dry-run"
|
|
||||||
exit_code=0
|
|
||||||
else
|
|
||||||
if bash -lc "cd \"$REPO_ROOT\" && $command" >"$stdout_path" 2>"$stderr_path"; then
|
|
||||||
status="passed"
|
|
||||||
exit_code=0
|
|
||||||
EXECUTED_REAL_STEPS=1
|
|
||||||
else
|
|
||||||
exit_code=$?
|
|
||||||
status="failed"
|
|
||||||
FAILED=1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
append_step_record "$lane" "$name" "$status" "$exit_code" "$command" "$stdout_rel" "$stderr_rel" "$note"
|
|
||||||
printf '%s\t%s\t%s\n' "$lane" "$name" "$status"
|
|
||||||
|
|
||||||
if [[ "$status" == "failed" ]]; then
|
|
||||||
FAILURE_STEP="$name"
|
|
||||||
FAILURE_COMMAND="$command"
|
|
||||||
FAILURE_STDOUT="$stdout_rel"
|
|
||||||
FAILURE_STDERR="$stderr_rel"
|
|
||||||
return "$exit_code"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
record_nonpassing_step() {
|
|
||||||
local lane=$1
|
|
||||||
local name=$2
|
|
||||||
local status=$3
|
|
||||||
local note=$4
|
|
||||||
local slug=${name//[^a-zA-Z0-9_-]/-}
|
|
||||||
local stdout_rel="steps/${slug}.stdout.log"
|
|
||||||
local stderr_rel="steps/${slug}.stderr.log"
|
|
||||||
printf '%s\n' "$note" >"$ARTIFACT_DIR/$stdout_rel"
|
|
||||||
: >"$ARTIFACT_DIR/$stderr_rel"
|
|
||||||
append_step_record "$lane" "$name" "$status" "0" "" "$stdout_rel" "$stderr_rel" "$note"
|
|
||||||
printf '%s\t%s\t%s\n' "$lane" "$name" "$status"
|
|
||||||
}
|
|
||||||
|
|
||||||
record_skipped_step() {
|
|
||||||
record_nonpassing_step "$1" "$2" "skipped" "$3"
|
|
||||||
}
|
|
||||||
|
|
||||||
record_blocked_step() {
|
|
||||||
add_blocker "$3"
|
|
||||||
record_nonpassing_step "$1" "$2" "blocked" "$3"
|
|
||||||
}
|
|
||||||
|
|
||||||
record_failed_step() {
|
|
||||||
FAILED=1
|
|
||||||
FAILURE_STEP=$2
|
|
||||||
FAILURE_COMMAND=${FAILURE_COMMAND:-"(validation)"}
|
|
||||||
FAILURE_STDOUT="steps/${2//[^a-zA-Z0-9_-]/-}.stdout.log"
|
|
||||||
FAILURE_STDERR="steps/${2//[^a-zA-Z0-9_-]/-}.stderr.log"
|
|
||||||
add_blocker "$3"
|
|
||||||
record_nonpassing_step "$1" "$2" "failed" "$3"
|
|
||||||
}
|
|
||||||
|
|
||||||
find_real_runtime_helper() {
|
|
||||||
local candidate
|
|
||||||
for candidate in \
|
|
||||||
"$SCRIPT_DIR/run_real_runtime_smoke.sh" \
|
|
||||||
"$SCRIPT_DIR/run_real_mpv_smoke.sh"; do
|
|
||||||
if [[ -x "$candidate" ]]; then
|
|
||||||
printf '%s' "$candidate"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
acquire_real_runtime_lease() {
|
|
||||||
local lease_root="$REPO_ROOT/.tmp/skill-verification/locks"
|
|
||||||
local lease_dir="$lease_root/exclusive-real-runtime"
|
|
||||||
mkdir -p "$lease_root"
|
|
||||||
if mkdir "$lease_dir" 2>/dev/null; then
|
|
||||||
REAL_RUNTIME_LEASE_DIR="$lease_dir"
|
|
||||||
printf '%s\n' "$SESSION_ID" >"$lease_dir/session_id"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local owner=""
|
|
||||||
if [[ -f "$lease_dir/session_id" ]]; then
|
|
||||||
owner=$(cat "$lease_dir/session_id")
|
|
||||||
fi
|
|
||||||
add_blocker "real-runtime lease already held${owner:+ by $owner}"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
release_real_runtime_lease() {
|
|
||||||
if [[ -n "$REAL_RUNTIME_LEASE_DIR" && -d "$REAL_RUNTIME_LEASE_DIR" ]]; then
|
|
||||||
if [[ -f "$REAL_RUNTIME_LEASE_DIR/session_id" ]]; then
|
|
||||||
local owner
|
|
||||||
owner=$(cat "$REAL_RUNTIME_LEASE_DIR/session_id")
|
|
||||||
if [[ "$owner" != "$SESSION_ID" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
rm -rf "$REAL_RUNTIME_LEASE_DIR"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
compute_final_status() {
|
|
||||||
if [[ "$FAILED" == "1" ]]; then
|
|
||||||
FINAL_STATUS="failed"
|
|
||||||
elif [[ "$BLOCKED" == "1" ]]; then
|
|
||||||
FINAL_STATUS="blocked"
|
|
||||||
elif [[ "$EXECUTED_REAL_STEPS" == "1" ]]; then
|
|
||||||
FINAL_STATUS="passed"
|
|
||||||
else
|
|
||||||
FINAL_STATUS="skipped"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
write_summary_files() {
|
|
||||||
local lane_lines
|
|
||||||
lane_lines=$(printf '%s\n' "${SELECTED_LANES[@]}")
|
|
||||||
printf '%s\n' "$lane_lines" >"$ARTIFACT_DIR/lanes.txt"
|
|
||||||
printf '%s\n' "${BLOCKERS[@]}" >"$ARTIFACT_DIR/blockers.txt"
|
|
||||||
printf '%s\n' "${PATH_ARGS[@]}" >"$ARTIFACT_DIR/requested-paths.txt"
|
|
||||||
|
|
||||||
ARTIFACT_DIR_ENV="$ARTIFACT_DIR" \
|
|
||||||
SESSION_ID_ENV="$SESSION_ID" \
|
|
||||||
FINAL_STATUS_ENV="$FINAL_STATUS" \
|
|
||||||
PATH_SELECTION_MODE_ENV="$PATH_SELECTION_MODE" \
|
|
||||||
ALLOW_REAL_RUNTIME_ENV="$ALLOW_REAL_RUNTIME" \
|
|
||||||
SESSION_HOME_ENV="$SESSION_HOME" \
|
|
||||||
SESSION_XDG_CONFIG_HOME_ENV="$SESSION_XDG_CONFIG_HOME" \
|
|
||||||
SESSION_MPV_DIR_ENV="$SESSION_MPV_DIR" \
|
|
||||||
SESSION_LOGS_DIR_ENV="$SESSION_LOGS_DIR" \
|
|
||||||
SESSION_MPV_LOG_ENV="$SESSION_MPV_LOG" \
|
|
||||||
STARTED_AT_ENV="$STARTED_AT" \
|
|
||||||
FINISHED_AT_ENV="$FINISHED_AT" \
|
|
||||||
FAILED_ENV="$FAILED" \
|
|
||||||
FAILURE_COMMAND_ENV="${FAILURE_COMMAND:-}" \
|
|
||||||
FAILURE_STDOUT_ENV="${FAILURE_STDOUT:-}" \
|
|
||||||
FAILURE_STDERR_ENV="${FAILURE_STDERR:-}" \
|
|
||||||
bun -e '
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
function readLines(filePath) {
|
|
||||||
if (!fs.existsSync(filePath)) return [];
|
|
||||||
return fs.readFileSync(filePath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
const artifactDir = process.env.ARTIFACT_DIR_ENV;
|
|
||||||
const reportsDir = path.join(artifactDir, "reports");
|
|
||||||
const lanes = readLines(path.join(artifactDir, "lanes.txt"));
|
|
||||||
const blockers = readLines(path.join(artifactDir, "blockers.txt"));
|
|
||||||
const requestedPaths = readLines(path.join(artifactDir, "requested-paths.txt"));
|
|
||||||
const steps = readLines(path.join(artifactDir, "steps.tsv")).map((line) => {
|
|
||||||
const [lane, name, status, exitCode, command, stdout, stderr, note] = line.split("\t");
|
|
||||||
return {
|
|
||||||
lane,
|
|
||||||
name,
|
|
||||||
status,
|
|
||||||
exitCode: Number(exitCode || 0),
|
|
||||||
command,
|
|
||||||
stdout,
|
|
||||||
stderr,
|
|
||||||
note,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const summary = {
|
|
||||||
sessionId: process.env.SESSION_ID_ENV || "",
|
|
||||||
artifactDir,
|
|
||||||
reportsDir,
|
|
||||||
status: process.env.FINAL_STATUS_ENV || "failed",
|
|
||||||
selectedLanes: lanes,
|
|
||||||
failed: process.env.FAILED_ENV === "1",
|
|
||||||
failure:
|
|
||||||
process.env.FAILED_ENV === "1"
|
|
||||||
? {
|
|
||||||
command: process.env.FAILURE_COMMAND_ENV || "",
|
|
||||||
stdout: process.env.FAILURE_STDOUT_ENV || "",
|
|
||||||
stderr: process.env.FAILURE_STDERR_ENV || "",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
blockers,
|
|
||||||
pathSelectionMode: process.env.PATH_SELECTION_MODE_ENV || "git-inferred",
|
|
||||||
requestedPaths,
|
|
||||||
allowRealRuntime: process.env.ALLOW_REAL_RUNTIME_ENV === "1",
|
|
||||||
startedAt: process.env.STARTED_AT_ENV || "",
|
|
||||||
finishedAt: process.env.FINISHED_AT_ENV || "",
|
|
||||||
env: {
|
|
||||||
home: process.env.SESSION_HOME_ENV || "",
|
|
||||||
xdgConfigHome: process.env.SESSION_XDG_CONFIG_HOME_ENV || "",
|
|
||||||
mpvDir: process.env.SESSION_MPV_DIR_ENV || "",
|
|
||||||
logsDir: process.env.SESSION_LOGS_DIR_ENV || "",
|
|
||||||
mpvLog: process.env.SESSION_MPV_LOG_ENV || "",
|
|
||||||
},
|
|
||||||
steps,
|
|
||||||
};
|
|
||||||
|
|
||||||
const summaryJson = JSON.stringify(summary, null, 2) + "\n";
|
|
||||||
fs.writeFileSync(path.join(artifactDir, "summary.json"), summaryJson);
|
|
||||||
fs.writeFileSync(path.join(reportsDir, "summary.json"), summaryJson);
|
|
||||||
|
|
||||||
const lines = [];
|
|
||||||
lines.push(`session_id: ${summary.sessionId}`);
|
|
||||||
lines.push(`artifact_dir: ${artifactDir}`);
|
|
||||||
lines.push(`selected_lanes: ${lanes.join(", ") || "(none)"}`);
|
|
||||||
lines.push(`status: ${summary.status}`);
|
|
||||||
lines.push(`path_selection_mode: ${summary.pathSelectionMode}`);
|
|
||||||
if (requestedPaths.length > 0) {
|
|
||||||
lines.push(`requested_paths: ${requestedPaths.join(", ")}`);
|
|
||||||
}
|
|
||||||
if (blockers.length > 0) {
|
|
||||||
lines.push(`blockers: ${blockers.join(" | ")}`);
|
|
||||||
}
|
|
||||||
for (const step of steps) {
|
|
||||||
lines.push(`${step.lane}/${step.name}: ${step.status}`);
|
|
||||||
if (step.command) lines.push(` command: ${step.command}`);
|
|
||||||
lines.push(` stdout: ${step.stdout}`);
|
|
||||||
lines.push(` stderr: ${step.stderr}`);
|
|
||||||
if (step.note) lines.push(` note: ${step.note}`);
|
|
||||||
}
|
|
||||||
if (summary.failed) {
|
|
||||||
lines.push(`failure_command: ${process.env.FAILURE_COMMAND_ENV || ""}`);
|
|
||||||
}
|
|
||||||
const summaryText = lines.join("\n") + "\n";
|
|
||||||
fs.writeFileSync(path.join(artifactDir, "summary.txt"), summaryText);
|
|
||||||
fs.writeFileSync(path.join(reportsDir, "summary.txt"), summaryText);
|
|
||||||
'
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
release_real_runtime_lease
|
|
||||||
}
|
|
||||||
|
|
||||||
CLASSIFIER_OUTPUT=""
|
|
||||||
ARTIFACT_DIR=""
|
|
||||||
ALLOW_REAL_RUNTIME=0
|
|
||||||
DRY_RUN=0
|
|
||||||
FAILED=0
|
|
||||||
BLOCKED=0
|
|
||||||
EXECUTED_REAL_STEPS=0
|
|
||||||
FINAL_STATUS=""
|
|
||||||
FAILURE_STEP=""
|
|
||||||
FAILURE_COMMAND=""
|
|
||||||
FAILURE_STDOUT=""
|
|
||||||
FAILURE_STDERR=""
|
|
||||||
REAL_RUNTIME_LEASE_DIR=""
|
|
||||||
STARTED_AT=""
|
|
||||||
FINISHED_AT=""
|
|
||||||
|
|
||||||
declare -a EXPLICIT_LANES=()
|
|
||||||
declare -a SELECTED_LANES=()
|
|
||||||
declare -a PATH_ARGS=()
|
|
||||||
declare -a COMMANDS_RUN=()
|
|
||||||
declare -a BLOCKERS=()
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--lane)
|
|
||||||
EXPLICIT_LANES+=("$(normalize_lane_name "$2")")
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--artifact-dir)
|
|
||||||
ARTIFACT_DIR=$2
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--allow-real-runtime|--allow-real-gui)
|
|
||||||
ALLOW_REAL_RUNTIME=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--dry-run)
|
|
||||||
DRY_RUN=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--help|-h)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
--)
|
|
||||||
shift
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
PATH_ARGS+=("$1")
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
PATH_ARGS+=("$1")
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
REPO_ROOT=$(cd "$SCRIPT_DIR/../../../.." && pwd)
|
||||||
SESSION_ID=$(generate_session_id)
|
TARGET="$REPO_ROOT/plugins/subminer-workflow/skills/subminer-change-verification/scripts/verify_subminer_change.sh"
|
||||||
PATH_SELECTION_MODE="git-inferred"
|
|
||||||
if [[ ${#PATH_ARGS[@]} -gt 0 ]]; then
|
|
||||||
PATH_SELECTION_MODE="explicit"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$ARTIFACT_DIR" ]]; then
|
if [[ ! -x "$TARGET" ]]; then
|
||||||
mkdir -p "$REPO_ROOT/.tmp/skill-verification"
|
echo "Missing canonical script: $TARGET" >&2
|
||||||
ARTIFACT_DIR="$REPO_ROOT/.tmp/skill-verification/$SESSION_ID"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SESSION_HOME="$ARTIFACT_DIR/home"
|
|
||||||
SESSION_XDG_CONFIG_HOME="$ARTIFACT_DIR/xdg"
|
|
||||||
SESSION_MPV_DIR="$ARTIFACT_DIR/mpv"
|
|
||||||
SESSION_LOGS_DIR="$ARTIFACT_DIR/logs"
|
|
||||||
SESSION_MPV_LOG="$SESSION_LOGS_DIR/mpv.log"
|
|
||||||
|
|
||||||
mkdir -p "$ARTIFACT_DIR/steps" "$ARTIFACT_DIR/reports" "$SESSION_HOME" "$SESSION_XDG_CONFIG_HOME" "$SESSION_MPV_DIR" "$SESSION_LOGS_DIR"
|
|
||||||
STEPS_TSV="$ARTIFACT_DIR/steps.tsv"
|
|
||||||
: >"$STEPS_TSV"
|
|
||||||
|
|
||||||
trap cleanup EXIT
|
|
||||||
STARTED_AT=$(timestamp_iso)
|
|
||||||
|
|
||||||
if [[ ${#EXPLICIT_LANES[@]} -gt 0 ]]; then
|
|
||||||
local_lane=""
|
|
||||||
for local_lane in "${EXPLICIT_LANES[@]}"; do
|
|
||||||
add_lane "$local_lane"
|
|
||||||
done
|
|
||||||
printf 'reason:explicit lanes supplied\n' >"$ARTIFACT_DIR/classification.txt"
|
|
||||||
else
|
|
||||||
if [[ ${#PATH_ARGS[@]} -gt 0 ]]; then
|
|
||||||
CLASSIFIER_OUTPUT=$(bash "$SCRIPT_DIR/classify_subminer_diff.sh" "${PATH_ARGS[@]}")
|
|
||||||
else
|
|
||||||
CLASSIFIER_OUTPUT=$(bash "$SCRIPT_DIR/classify_subminer_diff.sh")
|
|
||||||
fi
|
|
||||||
printf '%s\n' "$CLASSIFIER_OUTPUT" >"$ARTIFACT_DIR/classification.txt"
|
|
||||||
while IFS= read -r line; do
|
|
||||||
case "$line" in
|
|
||||||
lane:*)
|
|
||||||
add_lane "${line#lane:}"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done <<<"$CLASSIFIER_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
record_env
|
|
||||||
|
|
||||||
printf 'artifact_dir=%s\n' "$ARTIFACT_DIR"
|
|
||||||
printf 'selected_lanes=%s\n' "$(IFS=,; echo "${SELECTED_LANES[*]}")"
|
|
||||||
|
|
||||||
for lane in "${SELECTED_LANES[@]}"; do
|
|
||||||
case "$lane" in
|
|
||||||
docs)
|
|
||||||
run_step "$lane" "docs-test" "bun run docs:test" || break
|
|
||||||
[[ "$FAILED" == "1" ]] && break
|
|
||||||
run_step "$lane" "docs-build" "bun run docs:build" || break
|
|
||||||
;;
|
|
||||||
config)
|
|
||||||
run_step "$lane" "test-config" "bun run test:config" || break
|
|
||||||
;;
|
|
||||||
core)
|
|
||||||
run_step "$lane" "typecheck" "bun run typecheck" || break
|
|
||||||
[[ "$FAILED" == "1" ]] && break
|
|
||||||
run_step "$lane" "test-fast" "bun run test:fast" || break
|
|
||||||
;;
|
|
||||||
launcher-plugin)
|
|
||||||
run_step "$lane" "launcher-smoke-src" "bun run test:launcher:smoke:src" || break
|
|
||||||
[[ "$FAILED" == "1" ]] && break
|
|
||||||
run_step "$lane" "plugin-src" "bun run test:plugin:src" || break
|
|
||||||
;;
|
|
||||||
runtime-compat)
|
|
||||||
run_step "$lane" "build" "bun run build" || break
|
|
||||||
[[ "$FAILED" == "1" ]] && break
|
|
||||||
run_step "$lane" "test-runtime-compat" "bun run test:runtime:compat" || break
|
|
||||||
[[ "$FAILED" == "1" ]] && break
|
|
||||||
run_step "$lane" "test-smoke-dist" "bun run test:smoke:dist" || break
|
|
||||||
;;
|
|
||||||
real-runtime)
|
|
||||||
if [[ "$PATH_SELECTION_MODE" != "explicit" ]]; then
|
|
||||||
record_blocked_step \
|
|
||||||
"$lane" \
|
|
||||||
"real-runtime-guard" \
|
|
||||||
"real-runtime lane requires explicit paths; inferred local git changes are non-authoritative"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$ALLOW_REAL_RUNTIME" != "1" ]]; then
|
|
||||||
record_blocked_step \
|
|
||||||
"$lane" \
|
|
||||||
"real-runtime-guard" \
|
|
||||||
"real-runtime lane requested but --allow-real-runtime was not supplied"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! acquire_real_runtime_lease; then
|
|
||||||
record_blocked_step \
|
|
||||||
"$lane" \
|
|
||||||
"real-runtime-lease" \
|
|
||||||
"real-runtime lease already held; rerun after the active runtime verification finishes"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! REAL_RUNTIME_HELPER=$(find_real_runtime_helper); then
|
|
||||||
record_blocked_step \
|
|
||||||
"$lane" \
|
|
||||||
"real-runtime-helper" \
|
|
||||||
"real-runtime helper not implemented yet"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf -v REAL_RUNTIME_COMMAND \
|
|
||||||
'SESSION_ID=%q HOME=%q XDG_CONFIG_HOME=%q SUBMINER_MPV_LOG=%q bash %q' \
|
|
||||||
"$SESSION_ID" \
|
|
||||||
"$SESSION_HOME" \
|
|
||||||
"$SESSION_XDG_CONFIG_HOME" \
|
|
||||||
"$SESSION_MPV_LOG" \
|
|
||||||
"$REAL_RUNTIME_HELPER"
|
|
||||||
|
|
||||||
run_step "$lane" "real-runtime-smoke" "$REAL_RUNTIME_COMMAND" || break
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
record_failed_step "$lane" "lane-validation" "unknown lane: $lane"
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [[ "$FAILED" == "1" || "$BLOCKED" == "1" ]]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
FINISHED_AT=$(timestamp_iso)
|
|
||||||
compute_final_status
|
|
||||||
write_summary_files
|
|
||||||
|
|
||||||
printf 'status=%s\n' "$FINAL_STATUS"
|
|
||||||
printf 'artifact_dir=%s\n' "$ARTIFACT_DIR"
|
|
||||||
|
|
||||||
case "$FINAL_STATUS" in
|
|
||||||
failed)
|
|
||||||
printf 'result=failed\n'
|
|
||||||
printf 'failure_command=%s\n' "$FAILURE_COMMAND"
|
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
fi
|
||||||
blocked)
|
|
||||||
printf 'result=blocked\n'
|
exec "$TARGET" "$@"
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
printf 'result=ok\n'
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|||||||
@@ -1,146 +1,18 @@
|
|||||||
---
|
---
|
||||||
name: "subminer-scrum-master"
|
name: 'subminer-scrum-master'
|
||||||
description: "Use in the SubMiner repo when a request should be turned into planned work and driven through execution. Assesses whether backlog tracking is warranted, creates or updates tasks when needed, records a plan, dispatches one or more subagents, and requires verification before handoff."
|
description: 'Compatibility shim. Canonical SubMiner scrum-master workflow now lives in the repo-local subminer-workflow plugin.'
|
||||||
---
|
---
|
||||||
|
|
||||||
# SubMiner Scrum Master
|
# Compatibility Shim
|
||||||
|
|
||||||
Own workflow, not code by default.
|
Canonical source:
|
||||||
|
|
||||||
Use this skill when the user gives a feature request, bug report, issue, refactor, or implementation ask and the agent should manage intake, planning, backlog hygiene, worker dispatch, and verification through completion.
|
- `plugins/subminer-workflow/skills/subminer-scrum-master/SKILL.md`
|
||||||
|
|
||||||
## Core Rules
|
When this shim is invoked:
|
||||||
|
|
||||||
1. Decide first whether backlog tracking is warranted.
|
1. Read the canonical plugin-owned skill.
|
||||||
2. If backlog is needed, search first. Update existing work when it clearly matches.
|
2. Follow the plugin-owned skill as the source of truth.
|
||||||
3. If backlog is not needed, keep the process light. Do not invent ticket ceremony.
|
3. Do not duplicate workflow changes here; update the plugin-owned skill instead.
|
||||||
4. Record a plan before dispatching coding work.
|
|
||||||
5. Use parent + subtasks for multi-part work when backlog is used.
|
|
||||||
6. Dispatch conservatively. Parallelize only disjoint write scopes.
|
|
||||||
7. Require verification before handoff, typically via `subminer-change-verification`.
|
|
||||||
8. Report backlog actions, dispatched workers, verification, blockers, and remaining risks.
|
|
||||||
|
|
||||||
## Backlog Decision
|
This shim exists so existing repo references and prompts keep resolving during the migration to the repo-local plugin workflow.
|
||||||
|
|
||||||
Skip backlog when the request is:
|
|
||||||
- question only
|
|
||||||
- obvious mechanical edit
|
|
||||||
- tiny isolated change with no real planning
|
|
||||||
|
|
||||||
Use backlog when the work:
|
|
||||||
- needs planning or scope decisions
|
|
||||||
- spans multiple phases or subsystems
|
|
||||||
- is likely to need subagent dispatch
|
|
||||||
- should remain traceable for handoff/resume
|
|
||||||
|
|
||||||
If backlog is used:
|
|
||||||
- search existing tasks first
|
|
||||||
- create/update a standalone task for one focused deliverable
|
|
||||||
- create/update a parent task plus subtasks for multi-part work
|
|
||||||
- record the implementation plan in the task before implementation begins
|
|
||||||
|
|
||||||
## Intake Workflow
|
|
||||||
|
|
||||||
1. Parse the request.
|
|
||||||
Classify it as question, mechanical edit, bugfix, feature, refactor, investigation, or follow-up.
|
|
||||||
2. Decide whether backlog is needed.
|
|
||||||
3. If backlog is needed:
|
|
||||||
- search first
|
|
||||||
- update existing task if clearly relevant
|
|
||||||
- otherwise create the right structure
|
|
||||||
- write the implementation plan before dispatch
|
|
||||||
4. If backlog is skipped:
|
|
||||||
- write a short working plan in-thread
|
|
||||||
- proceed without fake ticketing
|
|
||||||
5. Choose execution mode:
|
|
||||||
- no subagents for trivial work
|
|
||||||
- one worker for focused work
|
|
||||||
- parallel workers only for disjoint scopes
|
|
||||||
6. Run verification before handoff.
|
|
||||||
|
|
||||||
## Dispatch Rules
|
|
||||||
|
|
||||||
The scrum master orchestrates. Workers implement.
|
|
||||||
|
|
||||||
- Do not become the default implementer unless delegation is unnecessary.
|
|
||||||
- Do not parallelize overlapping files or tightly coupled runtime work.
|
|
||||||
- Give every worker explicit ownership of files/modules.
|
|
||||||
- Tell every worker other agents may be active and they must not revert unrelated edits.
|
|
||||||
- Require each worker to report:
|
|
||||||
- changed files
|
|
||||||
- tests run
|
|
||||||
- blockers
|
|
||||||
|
|
||||||
Use worker agents for implementation and explorer agents only for bounded codebase questions.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
Every nontrivial code task gets verification.
|
|
||||||
|
|
||||||
Preferred flow:
|
|
||||||
1. use `subminer-change-verification`
|
|
||||||
2. start with the cheapest sufficient lane
|
|
||||||
3. escalate only when needed
|
|
||||||
4. if worker verification is sufficient, accept it or run one final consolidating pass
|
|
||||||
|
|
||||||
Never hand off nontrivial work without stating what was verified and what was skipped.
|
|
||||||
|
|
||||||
## Pre-Handoff Policy Checks (Required)
|
|
||||||
|
|
||||||
Before handoff, always ask and answer both of these questions explicitly:
|
|
||||||
|
|
||||||
1. **Docs update required?**
|
|
||||||
2. **Changelog fragment required?**
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
- Do not assume silence implies "no." Record an explicit yes/no decision for each item.
|
|
||||||
- If the answer is yes, either complete the update or report the blocker before handoff.
|
|
||||||
- Include the final answers in the handoff summary even when both answers are "no."
|
|
||||||
|
|
||||||
## Failure / Scope Handling
|
|
||||||
|
|
||||||
- If a worker hits ambiguity, pause and ask the user.
|
|
||||||
- If verification fails, either:
|
|
||||||
- send the worker back with exact failure context, or
|
|
||||||
- fix it directly if it is tiny and clearly in scope
|
|
||||||
- If new scope appears, revisit backlog structure before silently expanding work.
|
|
||||||
|
|
||||||
## Representative Flows
|
|
||||||
|
|
||||||
### Trivial no-ticket work
|
|
||||||
|
|
||||||
- decide backlog is unnecessary
|
|
||||||
- keep a short plan
|
|
||||||
- implement directly or with one worker if helpful
|
|
||||||
- run targeted verification
|
|
||||||
- report outcome concisely
|
|
||||||
|
|
||||||
### Single-task implementation
|
|
||||||
|
|
||||||
- search/create/update one task
|
|
||||||
- record plan
|
|
||||||
- dispatch one worker
|
|
||||||
- integrate
|
|
||||||
- verify
|
|
||||||
- update task and report outcome
|
|
||||||
|
|
||||||
### Parent + subtasks execution
|
|
||||||
|
|
||||||
- search/create/update parent task
|
|
||||||
- create subtasks for distinct deliverables/phases
|
|
||||||
- record sequencing in the plan
|
|
||||||
- dispatch workers only where scopes are disjoint
|
|
||||||
- integrate
|
|
||||||
- run consolidated verification
|
|
||||||
- update task state and report outcome
|
|
||||||
|
|
||||||
## Output Expectations
|
|
||||||
|
|
||||||
At the end, report:
|
|
||||||
- whether backlog was used and what changed
|
|
||||||
- which workers were dispatched and what they owned
|
|
||||||
- what verification ran
|
|
||||||
- explicit answers to:
|
|
||||||
- docs update required?
|
|
||||||
- changelog fragment required?
|
|
||||||
- blockers, skips, and risks
|
|
||||||
|
|||||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -61,6 +61,16 @@ jobs:
|
|||||||
- name: Test suite (source)
|
- name: Test suite (source)
|
||||||
run: bun run test:fast
|
run: bun run test:fast
|
||||||
|
|
||||||
|
- name: Coverage suite (maintained source lane)
|
||||||
|
run: bun run test:coverage:src
|
||||||
|
|
||||||
|
- name: Upload coverage artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: coverage-test-src
|
||||||
|
path: coverage/test-src/lcov.info
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Launcher smoke suite (source)
|
- name: Launcher smoke suite (source)
|
||||||
run: bun run test:launcher:smoke:src
|
run: bun run test:launcher:smoke:src
|
||||||
|
|
||||||
|
|||||||
75
.github/workflows/release.yml
vendored
75
.github/workflows/release.yml
vendored
@@ -49,6 +49,16 @@ jobs:
|
|||||||
- name: Test suite (source)
|
- name: Test suite (source)
|
||||||
run: bun run test:fast
|
run: bun run test:fast
|
||||||
|
|
||||||
|
- name: Coverage suite (maintained source lane)
|
||||||
|
run: bun run test:coverage:src
|
||||||
|
|
||||||
|
- name: Upload coverage artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: coverage-test-src
|
||||||
|
path: coverage/test-src/lcov.info
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Launcher smoke suite (source)
|
- name: Launcher smoke suite (source)
|
||||||
run: bun run test:launcher:smoke:src
|
run: bun run test:launcher:smoke:src
|
||||||
|
|
||||||
@@ -399,33 +409,64 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Validate AUR SSH secret
|
- name: Check AUR publish prerequisites
|
||||||
|
id: aur_prereqs
|
||||||
env:
|
env:
|
||||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
if [ -z "${AUR_SSH_PRIVATE_KEY}" ]; then
|
if [ -z "${AUR_SSH_PRIVATE_KEY}" ]; then
|
||||||
echo "Missing required secret: AUR_SSH_PRIVATE_KEY"
|
echo "::warning::Missing AUR_SSH_PRIVATE_KEY; skipping automated AUR publish."
|
||||||
exit 1
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Configure SSH for AUR
|
- name: Configure SSH for AUR
|
||||||
|
id: aur_ssh
|
||||||
|
if: steps.aur_prereqs.outputs.skip != 'true'
|
||||||
env:
|
env:
|
||||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
install -dm700 ~/.ssh
|
if install -dm700 ~/.ssh \
|
||||||
printf '%s\n' "${AUR_SSH_PRIVATE_KEY}" > ~/.ssh/aur
|
&& printf '%s\n' "${AUR_SSH_PRIVATE_KEY}" > ~/.ssh/aur \
|
||||||
chmod 600 ~/.ssh/aur
|
&& chmod 600 ~/.ssh/aur \
|
||||||
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
|
&& ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts \
|
||||||
chmod 644 ~/.ssh/known_hosts
|
&& chmod 644 ~/.ssh/known_hosts; then
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "::warning::Unable to configure SSH for AUR; skipping automated AUR publish."
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Clone AUR repo
|
- name: Clone AUR repo
|
||||||
|
id: aur_clone
|
||||||
|
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true'
|
||||||
env:
|
env:
|
||||||
GIT_SSH_COMMAND: ssh -i ~/.ssh/aur -o IdentitiesOnly=yes
|
GIT_SSH_COMMAND: ssh -i ~/.ssh/aur -o IdentitiesOnly=yes
|
||||||
run: git clone ssh://aur@aur.archlinux.org/subminer-bin.git aur-subminer-bin
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
attempts=3
|
||||||
|
for attempt in $(seq 1 "$attempts"); do
|
||||||
|
if git clone ssh://aur@aur.archlinux.org/subminer-bin.git aur-subminer-bin; then
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf aur-subminer-bin
|
||||||
|
|
||||||
|
if [ "$attempt" -lt "$attempts" ]; then
|
||||||
|
sleep $((attempt * 15))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "::warning::Unable to clone subminer-bin from AUR after ${attempts} attempts; skipping automated AUR publish."
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Download release assets for AUR
|
- name: Download release assets for AUR
|
||||||
|
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
@@ -439,6 +480,7 @@ jobs:
|
|||||||
--pattern "subminer-assets.tar.gz"
|
--pattern "subminer-assets.tar.gz"
|
||||||
|
|
||||||
- name: Update AUR packaging metadata
|
- name: Update AUR packaging metadata
|
||||||
|
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true'
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
version_no_v="${{ steps.version.outputs.VERSION }}"
|
version_no_v="${{ steps.version.outputs.VERSION }}"
|
||||||
@@ -453,6 +495,7 @@ jobs:
|
|||||||
--assets ".tmp/aur-release-assets/subminer-assets.tar.gz"
|
--assets ".tmp/aur-release-assets/subminer-assets.tar.gz"
|
||||||
|
|
||||||
- name: Commit and push AUR update
|
- name: Commit and push AUR update
|
||||||
|
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true'
|
||||||
working-directory: aur-subminer-bin
|
working-directory: aur-subminer-bin
|
||||||
env:
|
env:
|
||||||
GIT_SSH_COMMAND: ssh -i ~/.ssh/aur -o IdentitiesOnly=yes
|
GIT_SSH_COMMAND: ssh -i ~/.ssh/aur -o IdentitiesOnly=yes
|
||||||
@@ -466,4 +509,16 @@ jobs:
|
|||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
git add PKGBUILD .SRCINFO
|
git add PKGBUILD .SRCINFO
|
||||||
git commit -m "Update to ${{ steps.version.outputs.VERSION }}"
|
git commit -m "Update to ${{ steps.version.outputs.VERSION }}"
|
||||||
git push origin HEAD:master
|
|
||||||
|
attempts=3
|
||||||
|
for attempt in $(seq 1 "$attempts"); do
|
||||||
|
if git push origin HEAD:master; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$attempt" -lt "$attempts" ]; then
|
||||||
|
sleep $((attempt * 15))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "::warning::Unable to push the AUR update after ${attempts} attempts; GitHub release is published, but subminer-bin needs manual follow-up."
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ out/
|
|||||||
dist/
|
dist/
|
||||||
release/
|
release/
|
||||||
build/yomitan/
|
build/yomitan/
|
||||||
|
coverage/
|
||||||
|
|
||||||
# Launcher build artifact (produced by make build-launcher)
|
# Launcher build artifact (produced by make build-launcher)
|
||||||
/subminer
|
/subminer
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ This project uses Backlog.md MCP for all task and project management activities.
|
|||||||
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work
|
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work
|
||||||
|
|
||||||
These guides cover:
|
These guides cover:
|
||||||
|
|
||||||
- Decision framework for when to create tasks
|
- Decision framework for when to create tasks
|
||||||
- Search-first workflow to avoid duplicates
|
- Search-first workflow to avoid duplicates
|
||||||
- Links to detailed guides for task creation, execution, and finalization
|
- Links to detailed guides for task creation, execution, and finalization
|
||||||
|
|||||||
266
Backlog.md
Normal file
266
Backlog.md
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
# Backlog
|
||||||
|
|
||||||
|
Purpose: lightweight repo-local task board. Seeded with current testing / coverage work.
|
||||||
|
|
||||||
|
Status keys:
|
||||||
|
|
||||||
|
- `todo`: not started
|
||||||
|
- `doing`: in progress
|
||||||
|
- `blocked`: waiting
|
||||||
|
- `done`: shipped
|
||||||
|
|
||||||
|
Priority keys:
|
||||||
|
|
||||||
|
- `P0`: urgent / release-risk
|
||||||
|
- `P1`: high value
|
||||||
|
- `P2`: useful cleanup
|
||||||
|
- `P3`: nice-to-have
|
||||||
|
|
||||||
|
## Active
|
||||||
|
|
||||||
|
| ID | Pri | Status | Area | Title |
|
||||||
|
| ------ | --- | ------ | -------------- | --------------------------------------------------- |
|
||||||
|
| SM-013 | P1 | done | review-followup | Address PR #36 CodeRabbit action items |
|
||||||
|
|
||||||
|
## Ready
|
||||||
|
|
||||||
|
| ID | Pri | Status | Area | Title |
|
||||||
|
| ------ | --- | ------ | ----------------- | ---------------------------------------------------------------- |
|
||||||
|
| SM-001 | P1 | todo | launcher | Add tests for CLI parser and args normalizer |
|
||||||
|
| SM-002 | P1 | todo | immersion-tracker | Backfill tests for uncovered query exports |
|
||||||
|
| SM-003 | P1 | todo | anki | Add focused field-grouping service + merge edge-case tests |
|
||||||
|
| SM-004 | P2 | todo | tests | Extract shared test utils for deps factories and polling helpers |
|
||||||
|
| SM-005 | P2 | todo | tests | Strengthen weak assertions in app-ready and IPC tests |
|
||||||
|
| SM-006 | P2 | todo | tests | Break up monolithic youtube-flow and subtitle-sidebar tests |
|
||||||
|
| SM-007 | P2 | todo | anilist | Add tests for AniList rate limiter |
|
||||||
|
| SM-008 | P3 | todo | subtitles | Add core subtitle-position persistence/path tests |
|
||||||
|
| SM-009 | P3 | todo | tokenizer | Add tests for JLPT token filter |
|
||||||
|
| SM-010 | P1 | todo | immersion-tracker | Refactor storage + immersion-tracker service into focused modules |
|
||||||
|
| SM-011 | P1 | done | tests | Add coverage reporting for maintained test lanes |
|
||||||
|
| SM-012 | P2 | done | config/runtime | Replace JSON serialize-clone helpers with structured cloning |
|
||||||
|
|
||||||
|
## Icebox
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Ticket Details
|
||||||
|
|
||||||
|
### SM-001
|
||||||
|
|
||||||
|
Title: Add tests for CLI parser and args normalizer
|
||||||
|
Priority: P1
|
||||||
|
Status: done
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- `launcher/config/cli-parser-builder.ts`
|
||||||
|
- `launcher/config/args-normalizer.ts`
|
||||||
|
Acceptance:
|
||||||
|
- root options parsing covered
|
||||||
|
- subcommand routing covered
|
||||||
|
- invalid action / invalid log level / invalid backend cases covered
|
||||||
|
- target classification covered: file, directory, URL, invalid
|
||||||
|
|
||||||
|
### SM-002
|
||||||
|
|
||||||
|
Title: Backfill tests for uncovered query exports
|
||||||
|
Priority: P1
|
||||||
|
Status: todo
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- `src/core/services/immersion-tracker/query-*.ts`
|
||||||
|
Targets:
|
||||||
|
- headword helpers
|
||||||
|
- anime/media detail helpers not covered by existing wrapper tests
|
||||||
|
- lexical detail / appearance helpers
|
||||||
|
- maintenance helpers beyond `deleteSession` and `upsertCoverArt`
|
||||||
|
Acceptance:
|
||||||
|
- every exported query helper either directly tested or explicitly justified as covered elsewhere
|
||||||
|
- at least one focused regression per complex SQL branch / aggregation branch
|
||||||
|
|
||||||
|
### SM-003
|
||||||
|
|
||||||
|
Title: Add focused field-grouping service + merge edge-case tests
|
||||||
|
Priority: P1
|
||||||
|
Status: todo
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- `src/anki-integration/field-grouping.ts`
|
||||||
|
- `src/anki-integration/field-grouping-merge.ts`
|
||||||
|
Acceptance:
|
||||||
|
- auto/manual/disabled flow branches covered
|
||||||
|
- duplicate-card preview failure path covered
|
||||||
|
- merge edge cases covered: empty fields, generated media fallback, strict grouped spans, audio synchronization
|
||||||
|
|
||||||
|
### SM-004
|
||||||
|
|
||||||
|
Title: Extract shared test utils for deps factories and polling helpers
|
||||||
|
Priority: P2
|
||||||
|
Status: todo
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- common `makeDeps` / `createDeps` helpers
|
||||||
|
- common `waitForCondition`
|
||||||
|
Acceptance:
|
||||||
|
- shared helper module added
|
||||||
|
- at least 3 duplicated polling helpers removed
|
||||||
|
- at least 5 duplicated deps factories consolidated or clearly prepared for follow-up migration
|
||||||
|
|
||||||
|
### SM-005
|
||||||
|
|
||||||
|
Title: Strengthen weak assertions in app-ready and IPC tests
|
||||||
|
Priority: P2
|
||||||
|
Status: todo
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- `src/core/services/app-ready.test.ts`
|
||||||
|
- `src/core/services/ipc.test.ts`
|
||||||
|
Acceptance:
|
||||||
|
- replace broad `assert.ok(...)` presence checks with exact value / order assertions where expected value known
|
||||||
|
- handler registration tests assert channel-specific behavior, not only existence
|
||||||
|
|
||||||
|
### SM-006
|
||||||
|
|
||||||
|
Title: Break up monolithic youtube-flow and subtitle-sidebar tests
|
||||||
|
Priority: P2
|
||||||
|
Status: todo
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- `src/main/runtime/youtube-flow.test.ts`
|
||||||
|
- `src/renderer/modals/subtitle-sidebar.test.ts`
|
||||||
|
Acceptance:
|
||||||
|
- reduce single-test breadth
|
||||||
|
- split largest tests into focused cases by behavior
|
||||||
|
- keep semantics unchanged
|
||||||
|
|
||||||
|
### SM-007
|
||||||
|
|
||||||
|
Title: Add tests for AniList rate limiter
|
||||||
|
Priority: P2
|
||||||
|
Status: todo
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- `src/core/services/anilist/rate-limiter.ts`
|
||||||
|
Acceptance:
|
||||||
|
- capacity-window wait behavior covered
|
||||||
|
- `x-ratelimit-remaining` + reset handling covered
|
||||||
|
- `retry-after` handling covered
|
||||||
|
|
||||||
|
### SM-008
|
||||||
|
|
||||||
|
Title: Add core subtitle-position persistence/path tests
|
||||||
|
Priority: P3
|
||||||
|
Status: todo
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- `src/core/services/subtitle-position.ts`
|
||||||
|
Acceptance:
|
||||||
|
- save/load persistence covered
|
||||||
|
- fallback behavior covered
|
||||||
|
- path normalization behavior covered for URL vs local target
|
||||||
|
|
||||||
|
### SM-009
|
||||||
|
|
||||||
|
Title: Add tests for JLPT token filter
|
||||||
|
Priority: P3
|
||||||
|
Status: todo
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- `src/core/services/jlpt-token-filter.ts`
|
||||||
|
Acceptance:
|
||||||
|
- excluded term membership covered
|
||||||
|
- ignored POS1 membership covered
|
||||||
|
- exported list / entry consistency covered
|
||||||
|
|
||||||
|
### SM-010
|
||||||
|
|
||||||
|
Title: Refactor storage + immersion-tracker service into focused layers without API changes
|
||||||
|
Priority: P1
|
||||||
|
Status: todo
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- `src/core/database/storage/storage.ts`
|
||||||
|
- `src/core/database/storage/schema.ts`
|
||||||
|
- `src/core/database/storage/cover-blob.ts`
|
||||||
|
- `src/core/database/storage/records.ts`
|
||||||
|
- `src/core/database/storage/write-path.ts`
|
||||||
|
- `src/core/services/immersion-tracker/youtube.ts`
|
||||||
|
- `src/core/services/immersion-tracker/youtube-manager.ts`
|
||||||
|
- `src/core/services/immersion-tracker/write-queue.ts`
|
||||||
|
- `src/core/services/immersion-tracker/immersion-tracker-service.ts`
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- behavior and public API remain unchanged for all callers
|
||||||
|
- `storage.ts` responsibilities split into DDL/migrations, cover blob helpers, record CRUD, and write-path execution
|
||||||
|
- `immersion-tracker-service.ts` reduces to session state, media change orchestration, query proxies, and lifecycle
|
||||||
|
- YouTube code split into pure utilities, a stateful manager (`YouTubeManager`), and a dedicated write queue (`WriteQueue`)
|
||||||
|
- removed `storage.ts` is replaced with focused modules and updated imports
|
||||||
|
- no API or migration regressions; existing tests for trackers/storage coverage remain green or receive focused updates
|
||||||
|
|
||||||
|
### SM-011
|
||||||
|
|
||||||
|
Title: Add coverage reporting for maintained test lanes
|
||||||
|
Priority: P1
|
||||||
|
Status: done
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- `package.json`
|
||||||
|
- CI workflow files under `.github/`
|
||||||
|
- `docs/workflow/verification.md`
|
||||||
|
Acceptance:
|
||||||
|
- at least one maintained test lane emits machine-readable coverage output
|
||||||
|
- CI surfaces coverage as an artifact, summary, or check output
|
||||||
|
- local contributor path for coverage is documented
|
||||||
|
- chosen coverage path works with Bun/TypeScript lanes already maintained by the repo
|
||||||
|
Implementation note:
|
||||||
|
- Added `bun run test:coverage:src` for the maintained source lane via a sharded coverage runner, with merged LCOV output at `coverage/test-src/lcov.info` and CI/release artifact upload as `coverage-test-src`.
|
||||||
|
|
||||||
|
### SM-012
|
||||||
|
|
||||||
|
Title: Replace JSON serialize-clone helpers with structured cloning
|
||||||
|
Priority: P2
|
||||||
|
Status: todo
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- `src/runtime-options.ts`
|
||||||
|
- `src/config/definitions.ts`
|
||||||
|
- `src/config/service.ts`
|
||||||
|
- `src/main/controller-config-update.ts`
|
||||||
|
Acceptance:
|
||||||
|
- runtime/config clone helpers stop using `JSON.parse(JSON.stringify(...))`
|
||||||
|
- replacement preserves current behavior for plain config/runtime objects
|
||||||
|
- focused tests cover clone/merge behavior that could regress during the swap
|
||||||
|
- no new clone helper is introduced in these paths without a documented reason
|
||||||
|
|
||||||
|
Done:
|
||||||
|
|
||||||
|
- replaced JSON serialize-clone call sites in runtime/config/controller update paths with `structuredClone`
|
||||||
|
- updated focused tests and fixtures to cover detached clone behavior and guard against regressions
|
||||||
|
|
||||||
|
### SM-013
|
||||||
|
|
||||||
|
Title: Address PR #36 CodeRabbit action items
|
||||||
|
Priority: P1
|
||||||
|
Status: done
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- `plugins/subminer-workflow/skills/subminer-change-verification/scripts/verify_subminer_change.sh`
|
||||||
|
- `scripts/subminer-change-verification.test.ts`
|
||||||
|
- `src/core/services/immersion-tracker/query-sessions.ts`
|
||||||
|
- `src/core/services/immersion-tracker/query-trends.ts`
|
||||||
|
- `src/core/services/immersion-tracker/maintenance.ts`
|
||||||
|
- `src/main/boot/services.ts`
|
||||||
|
- `src/main/character-dictionary-runtime/zip.test.ts`
|
||||||
|
Acceptance:
|
||||||
|
- fix valid open CodeRabbit findings on PR #36
|
||||||
|
- add focused regression coverage for behavior changes where practical
|
||||||
|
- verify touched tests plus typecheck stay green
|
||||||
|
|
||||||
|
Done:
|
||||||
|
|
||||||
|
- hardened `--artifact-dir` validation in the verification script
|
||||||
|
- fixed trend aggregation rounding and monthly ratio bucketing
|
||||||
|
- preserved unwatched anime episodes in episode queries
|
||||||
|
- restored seconds-based aggregate timestamps in shared maintenance
|
||||||
|
- fixed the startup refactor compile break by making the predicates local at the call site
|
||||||
|
- verified with `bun test src/core/services/immersion-tracker/__tests__/query.test.ts src/core/services/immersion-tracker/__tests__/query-split-modules.test.ts` and `bun run typecheck`
|
||||||
102
CHANGELOG.md
102
CHANGELOG.md
@@ -1,20 +1,102 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- AniList: Stopped post-watch tracking from sending a second progress update when the current episode was already satisfied by a ready retry item in the same watch-completion pass.
|
||||||
|
- Playback: Fixed managed local playback so duplicate startup-ready retries no longer unpause media after a later manual pause on the same file.
|
||||||
|
- Playback: Fixed managed local subtitle auto-selection so local files reuse configured primary/secondary subtitle language priorities instead of staying on mpv's initial `sid=auto` guess.
|
||||||
|
|
||||||
|
## v0.10.0 (2026-03-29)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Integrations: Replaced the deprecated Discord Rich Presence wrapper with the maintained `@xhayper/discord-rpc` package.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Stats: Fixed stats startup so the immersion tracker can run when `Bun.serve` is unavailable.
|
||||||
|
- Stats: Stats server now falls back to a Node `http` listener in Electron/runtime paths that do not expose Bun.
|
||||||
|
- Overlay: Fixed the macOS visible-overlay toggle path so manual hides stay hidden and the plugin uses the explicit visible-overlay toggle command.
|
||||||
|
- Subtitle Sidebar: Restored macOS mpv passthrough while the overlay subtitle sidebar is open so clicks outside the sidebar can refocus mpv and keep native keybindings working.
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
- Release: Added a maintained source coverage lane that shards Bun coverage one test file at a time and merges LCOV output into `coverage/test-src/lcov.info`.
|
||||||
|
- Release: CI and release quality-gate now upload the merged source-lane LCOV artifact for inspection.
|
||||||
|
- Runtime: Extracted remaining inline runtime logic from `src/main.ts` into dedicated runtime modules and composer helpers.
|
||||||
|
- Runtime: Added focused regression tests for the extracted runtime/composer boundaries.
|
||||||
|
- Runtime: Updated task tracking notes to mark TASK-238.6 complete and confirm follow-on boot-phase split can be deferred.
|
||||||
|
- Runtime: Split `src/main.ts` boot wiring into dedicated `src/main/boot/services.ts`, `src/main/boot/runtimes.ts`, and `src/main/boot/handlers.ts` modules.
|
||||||
|
- Runtime: Added focused tests for the new boot-phase seams and kept the startup/typecheck/build verification lanes green.
|
||||||
|
- Runtime: Updated internal architecture/task docs to record the boot-phase split and new ownership boundary.
|
||||||
|
|
||||||
|
## v0.9.3 (2026-03-25)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Launcher: Moved YouTube primary subtitle language defaults to `youtube.primarySubLanguages`.
|
||||||
|
- Launcher: Removed the placeholder YouTube subtitle retime step and now uses downloaded primary subtitle tracks directly, so there is no fake path rewrite before playback/sidebar loading.
|
||||||
|
- YouTube: Removed the `src/core/services/youtube/retime` helper and its tests after retiring the internal retime strategy.
|
||||||
|
- Docs: Clarified optional `alass` / `ffsubsync` subtitle-sync requirements and setup steps, including fallback behavior when sync tools are absent.
|
||||||
|
- Launcher: Removed the old `youtubeSubgen.primarySubLanguages` config path from the generated config and docs.
|
||||||
|
|
||||||
|
## v0.9.2 (2026-03-25)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Overlay: Fixed overlay pointer tracking so Windows click-through toggles immediately when the cursor enters or leaves subtitle regions, without waiting for a later hover resync.
|
||||||
|
- Overlay: Fixed Windows overlay window tracking on scaled displays by converting native tracked window bounds to Electron DIP coordinates before applying overlay bounds.
|
||||||
|
- Launcher: Fixed Windows direct `--youtube-play` startup so MPV boots reliably, stays paused until the app-owned subtitle flow is ready, and reuses an already-running SubMiner instance when available.
|
||||||
|
- Launcher: Fixed standalone Windows `--youtube-play` sessions so closing MPV fully exits SubMiner instead of leaving hidden overlay windows or a background process behind.
|
||||||
|
- Overlay: Fixed `subminer <youtube-url>` on Linux so the YouTube playback flow waits for Yomitan to load before creating the overlay window, avoiding the broken lookup popup state that previously required a manual overlay refresh.
|
||||||
|
|
||||||
|
## v0.9.1 (2026-03-24)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Release: Reduced packaged release size by excluding duplicate `extraResources` payload and pruning docs, tests, sourcemaps, and other source-only files from Electron bundles.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Overlay: Restored controller navigation and lookup/mining controls while the subtitle sidebar is open, while keeping true modal dialogs blocking controller actions.
|
||||||
|
- Tokenizer: Fixed subtitle annotation clearing so explanatory contrast endings like `んですけど` are excluded consistently across the shared tokenizer filter and annotation stage.
|
||||||
|
|
||||||
|
## v0.9.0 (2026-03-23)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Docs: Added a new WebSocket / Texthooker API and integration guide covering WebSocket payloads, custom client patterns, mpv plugin automation, and webhook-style relay examples. Linked from configuration and mining workflow docs for easier discovery.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Launcher: Added an app-owned YouTube subtitle flow that pauses mpv, uses absPlayer-style YouTube timedtext parsing/conversion to download subtitle tracks, and injects them as external files before playback resumes.
|
||||||
|
- Launcher: Changed YouTube subtitle startup to auto-load the best-available primary and secondary subtitle tracks at launch instead of forcing the picker modal first. Secondary subtitle failures no longer block playback resume.
|
||||||
|
- Launcher: Added `Ctrl+Alt+C` as the default keybinding to manually open the YouTube subtitle picker during active YouTube playback.
|
||||||
|
- Launcher: Added yt-dlp metadata probing so YouTube playback and immersion tracking record canonical video title and channel metadata.
|
||||||
|
- Launcher: Stopped forcing `--ytdl-raw-options=` before user-provided mpv options so existing YouTube cookie integrations in user `--args` are no longer clobbered.
|
||||||
|
- Launcher: Disabled mpv native YouTube subtitle auto-loading for the app-owned flow so injected external subtitle files remain authoritative.
|
||||||
|
- Launcher: Added OSD status messages for YouTube playback startup, subtitle acquisition, and subtitle loading so the flow stays visible before and during the picker.
|
||||||
|
- Subtitle Sidebar: Added startup-auto-open controls and resume positioning improvements so the sidebar jumps directly to the first resolved active cue.
|
||||||
|
- Subtitle Sidebar: Improved subtitle prefetch and embedded overlay passthrough sync so sidebar and overlay subtitle states stay consistent across media transitions.
|
||||||
|
- Subtitle Sidebar: Updated scroll handling, embedded layout styling, and active-cue visual behavior.
|
||||||
|
- Stats: Stats Library tab now displays YouTube video title, channel name, and channel thumbnail for YouTube media entries, with retry logic to fill in metadata that arrives after initial load.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Launcher: Fixed Anki media mining for mpv YouTube streams by unwrapping the stream URL so audio and screenshot capture work correctly for YouTube playback sessions.
|
||||||
|
- Immersion: Fixed YouTube media path handling in the immersion runtime and tracking so YouTube sessions record correct media references, AniList guessing skips YouTube URLs, and post-watch state transitions do not fire for YouTube media.
|
||||||
|
- Launcher: Fixed startup-launched YouTube playback so primary subtitle overlay updates continue after auto-load completes.
|
||||||
|
- Launcher: Fixed auto-loaded YouTube primary subtitles so parsed cues appear in the subtitle sidebar without needing a manual picker retry.
|
||||||
|
- Launcher: Fixed the YouTube picker to guard against duplicate subtitle submissions and tightened YouTube URL detection so follow-up runtime flows only treat real YouTube hosts as YouTube playback.
|
||||||
|
- Launcher: Fixed primary subtitle failure notifications being shown while app-owned YouTube subtitle probing and downloads are still in flight.
|
||||||
|
- Launcher: Preserved existing authoritative YouTube subtitle tracks when available; downloaded tracks are used only to fill missing sides, and native mpv secondary subtitle rendering is hidden so the overlay remains the sole secondary display.
|
||||||
|
|
||||||
## v0.8.0 (2026-03-22)
|
## v0.8.0 (2026-03-22)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Overlay: Added the subtitle sidebar feature with a new `subtitleSidebar` configuration surface and rendered sidebar modal with cue list rendering, click-to-seek, active-cue highlighting, and embedded layout support.
|
||||||
|
- IPC: Added sidebar snapshot plumbing between renderer and main process for overlay/sidebar synchronization.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Docs: Refreshed the vendored Texthooker docs/index.html bundle to match the latest local build artifacts.
|
- Config: Added hot-reloadable sidebar options for enablement, layout, visibility, typography, opacity, sizing, and interaction behavior (`autoOpen`, `pauseOnHover`, `autoScroll`, toggle key).
|
||||||
|
- Docs: Added full `subtitleSidebar` documentation coverage, including sample config, option table, and toggle shortcut notes.
|
||||||
|
- Runtime: Improved subtitle prefetch/rendering flow so sidebar and overlay subtitle states stay in sync across media transitions.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Anki: Known-word cache refreshes now reconcile Anki changes incrementally instead of wiping and rebuilding on startup, mined cards can append their word into the cache immediately through a new default-enabled config flag, and explicit refreshes now run through `subminer doctor --refresh-known-words`.
|
- Overlay: Kept sidebar cue tracking stable across playback transitions and timing edge cases.
|
||||||
- Subtitle: Restored known-word coloring and JLPT underlines for subtitle tokens like `大体` when the subtitle token is kanji but the known-word cache only matches the kana reading.
|
- Overlay: Improved sidebar resume/start behavior to jump directly to the first resolved active cue.
|
||||||
- Stats: Episode progress in the anime page now uses the last ended playback position instead of cumulative active watch time, avoiding distorted percentages after rewatches or repeated sessions.
|
- Overlay: Stopped stale subtitle refreshes from regressing active-cue and text state.
|
||||||
- Stats: Anime episode progress now keeps the latest known playback position through active-session checkpoints and stale-session recovery, so recently watched episodes no longer lose their progress percentage.
|
|
||||||
- Stats: Anime episode progress now falls back to the latest retained subtitle/event timing when a session is missing a persisted playback-position checkpoint, so older watch sessions no longer get stuck at `0%` progress.
|
|
||||||
- Overlay: Kept subtitle sidebar cue tracking stable across transitions by avoiding cue-line regression on subtitle timing edge cases and stale text updates.
|
|
||||||
- Overlay: Improved sidebar config by documenting and exposing layout mode and typography options (`layout`, `fontFamily`, `fontSize`) in the generated documentation flow.
|
|
||||||
- Overlay: Added `subtitleSidebar.autoOpen` (default `false`) to open the subtitle sidebar once during overlay startup when the sidebar feature is enabled.
|
|
||||||
- Overlay: Made subtitle sidebar resume/start positioning jump directly to the first resolved active cue instead of smooth-scrolling through the full list, while keeping smooth auto-follow for later cue changes.
|
|
||||||
|
|
||||||
## v0.7.0 (2026-03-19)
|
## v0.7.0 (2026-03-19)
|
||||||
|
|
||||||
|
|||||||
236
README.md
236
README.md
@@ -1,60 +1,178 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="assets/SubMiner.png" width="140" alt="SubMiner logo">
|
|
||||||
|
<img src="assets/SubMiner.png" width="160" alt="SubMiner logo">
|
||||||
|
|
||||||
# SubMiner
|
# SubMiner
|
||||||
|
|
||||||
**Sentence-mine from mpv — look up words, one-key Anki export, immersion tracking.**
|
## Turn mpv into a sentence-mining workstation.
|
||||||
|
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
Look up words with Yomitan, export to Anki in one key, track your immersion — all without leaving mpv.
|
||||||
[](https://github.com/ksyasuda/SubMiner)
|
|
||||||
[](https://docs.subminer.moe)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://aur.archlinux.org/packages/subminer-bin)
|
[](https://github.com/ksyasuda/SubMiner)
|
||||||
|
[](https://docs.subminer.moe)
|
||||||
|
[](https://aur.archlinux.org/packages/subminer-bin)
|
||||||
|
|
||||||
|
[](./assets/minecard.mp4)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
## How It Works
|
||||||
|
|
||||||
SubMiner is an Electron overlay for [mpv](https://mpv.io) that turns video into a sentence-mining workstation. Look up any word with [Yomitan](https://github.com/yomidevs/yomitan), mine it to Anki with one key, and track your immersion over time.
|
SubMiner runs as an invisible Electron overlay on top of mpv. Subtitles render as an interactive layer. Move your cursor over any word and trigger a [Yomitan](https://github.com/yomidevs/yomitan) lookup. Press one key to snapshot the sentence, audio, and screenshot into Anki via AnkiConnect.
|
||||||
|
|
||||||
<div align="center">
|
First-run setup requires the mpv plugin before it can finish. On Windows, the optional `SubMiner mpv` shortcut created during setup is the recommended playback entry point because it launches `mpv` with SubMiner's defaults directly, so you do not need an `mpv.conf` profile just to use it.
|
||||||
|
|
||||||
[](./assets/minecard.mp4)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
**Dictionary lookups** — Yomitan runs inside the overlay. Hover or navigate to any word for full dictionary popups without leaving mpv.
|
### Dictionary Lookups
|
||||||
|
|
||||||
**One-key Anki mining** — Press one key to create a card with the sentence, audio clip, screenshot, and machine translation from the exact playback moment.
|
Yomitan runs inside the overlay. Trigger a lookup on any word for full dictionary popups — definitions, pitch accent, frequency data — without ever leaving mpv.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="docs-site/public/screenshots/yomitan-lookup.png" width="800" alt="Yomitan popup with dictionary entry and mine button over annotated subtitles in mpv">
|
<img src="docs-site/public/screenshots/yomitan-lookup.png" width="800" alt="Yomitan dictionary popup over annotated subtitles in mpv">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Reading annotations** — Real-time subtitle annotations with N+1 targeting, frequency highlighting, JLPT tags, and a character name dictionary. Grammar-only tokens render as plain text.
|
<br>
|
||||||
|
|
||||||
|
### Instant Anki Mining
|
||||||
|
|
||||||
|
Create an Anki card with the sentence, audio clip, screenshot, and machine translation from the exact playback moment with one key press, click, or controller input.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="docs-site/public/screenshots/annotations.png" width="800" alt="Annotated subtitles with frequency highlighting, JLPT underlines, known words, and N+1 targets">
|
<img src="docs-site/public/screenshots/one-key-mining.png" width="800" alt="Anki card created from SubMiner with sentence, audio, and screenshot">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Immersion dashboard** — Local stats dashboard with watch time, anime progress, vocabulary growth, mining throughput, and session history.
|
<br>
|
||||||
|
|
||||||
|
### Reading Annotations
|
||||||
|
|
||||||
|
Real-time subtitle annotations with frequency highlighting, JLPT tags, N+1 targeting, and a character name dictionary. Known words fade back; new words stand out. Grammar-only tokens render as plain text so you focus on what matters.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="docs-site/public/screenshots/stats-overview.png" width="800" alt="Stats dashboard with watch time, cards mined, streaks, and tracking snapshot">
|
<img src="docs-site/public/screenshots/annotations.png" width="800" alt="Annotated subtitles with frequency coloring, JLPT underlines, and N+1 targets">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Integrations** — AniList episode tracking, Jellyfin remote playback, Jimaku subtitle downloads, alass/ffsubsync, and an annotated websocket feed for external clients.
|
<br>
|
||||||
|
|
||||||
|
### Immersion Dashboard
|
||||||
|
|
||||||
|
Local stats dashboard — watch time, anime library, vocabulary growth, mining throughput, session history, and trends. All stored locally, no third-party tracking.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="docs-site/public/screenshots/texthooker.png" width="800" alt="Texthooker page with annotated subtitle lines and frequency highlighting">
|
<img src="docs-site/public/screenshots/stats-overview.png" width="800" alt="Stats dashboard showing watch time, cards mined, streaks, and tracking data">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Playlist Browser
|
||||||
|
|
||||||
|
Browse sibling episode files and the active mpv queue in one overlay modal. Open it with `Ctrl+Alt+P` to append episodes from the current directory, jump to queued items, remove entries, or reorder the playlist without leaving playback.
|
||||||
|
|
||||||
|
Managed local playback now reapplies your configured subtitle language priorities after mpv loads track metadata, so mixed subtitle sets can settle onto the expected primary and secondary tracks instead of staying on mpv's initial `sid=auto` guess.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Integrations
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><b>YouTube</b></td>
|
||||||
|
<td>Auto-loaded yt-dlp subtitle tracks at startup with config-driven primary/secondary language priorities and a manual overlay picker on demand (<code>Ctrl+Alt+C</code>)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>AniList</b></td>
|
||||||
|
<td>Automatic episode tracking and progress sync</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>Jellyfin</b></td>
|
||||||
|
<td>Browse and launch media from your Jellyfin server</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>Jimaku</b></td>
|
||||||
|
<td>Search and download Japanese subtitles</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>alass / ffsubsync</b></td>
|
||||||
|
<td>Automatic subtitle retiming — requires <code>alass</code> or <code>ffsubsync</code> on your <code>PATH</code> (optional; subtitle syncing is disabled without them)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>WebSocket</b></td>
|
||||||
|
<td>Annotated subtitle feed for external clients (texthooker pages, custom tools)</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs-site/public/screenshots/texthooker.png" width="800" alt="Texthooker page receiving annotated subtitle lines via WebSocket">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
| | Required | Optional |
|
||||||
|
| -------------- | --------------------------------------- | ---------------------------------------------------------- |
|
||||||
|
| **Player** | [`mpv`](https://mpv.io) with IPC socket | — |
|
||||||
|
| **Processing** | `ffmpeg`, `mecab` + `mecab-ipadic` | `guessit` (AniSkip), `alass` / `ffsubsync` (subtitle sync) |
|
||||||
|
| **Media** | — | `yt-dlp`, `chafa`, `ffmpegthumbnailer` |
|
||||||
|
| **Selection** | — | `fzf` / `rofi` |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> [`bun`](https://bun.sh) is required if building from source or using the CLI wrapper: `subminer`. Pre-built releases (AppImage, DMG, installer) do not require it.
|
||||||
|
|
||||||
|
**Platform-specific:**
|
||||||
|
|
||||||
|
| Linux | macOS | Windows |
|
||||||
|
| ----------------------------------- | ------------------------ | ------------- |
|
||||||
|
| `hyprctl` or `xdotool` + `xwininfo` | Accessibility permission | No extra deps |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Arch Linux</b></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
paru -S --needed mpv ffmpeg mecab-git mecab-ipadic
|
||||||
|
# Optional
|
||||||
|
paru -S --needed yt-dlp fzf rofi chafa ffmpegthumbnailer xdotool xorg-xwininfo
|
||||||
|
# Optional: subtitle sync (install at least one for subtitle syncing to work)
|
||||||
|
paru -S --needed alass python-ffsubsync
|
||||||
|
# X11 / XWAYLAND
|
||||||
|
paru -S --needed xdotool xorg-xwininfo
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>macOS</b></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install mpv ffmpeg mecab mecab-ipadic
|
||||||
|
# Optional
|
||||||
|
brew install yt-dlp fzf rofi chafa ffmpegthumbnailer
|
||||||
|
# Optional: subtitle sync (install at least one for subtitle syncing to work)
|
||||||
|
brew install alass
|
||||||
|
pip install ffsubsync
|
||||||
|
```
|
||||||
|
|
||||||
|
Grant Accessibility permission to SubMiner in **System Settings > Privacy & Security > Accessibility**.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Windows</b></summary>
|
||||||
|
|
||||||
|
Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on your `PATH`.
|
||||||
|
|
||||||
|
For MeCab, install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Install
|
### 1. Install
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Arch Linux (AUR)</b></summary>
|
<summary><b>Arch Linux (AUR)</b></summary>
|
||||||
@@ -88,53 +206,61 @@ wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>macOS / Windows / From source</b></summary>
|
<summary><b>macOS</b></summary>
|
||||||
|
|
||||||
**macOS** — Download the latest DMG/ZIP from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
|
Download the latest DMG or ZIP from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
|
||||||
|
|
||||||
**Windows** — Download the latest installer or portable `.zip` from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Keep `mpv` on `PATH`.
|
|
||||||
|
|
||||||
**From source** — See [docs.subminer.moe/installation#from-source](https://docs.subminer.moe/installation#from-source).
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### First Launch
|
<details>
|
||||||
|
<summary><b>Windows</b></summary>
|
||||||
|
|
||||||
Run `SubMiner.AppImage` (Linux), `SubMiner.app` (macOS), or `SubMiner.exe` (Windows). On first launch, SubMiner starts in the tray, creates a default config, and opens a setup popup where you can install the mpv plugin and configure Yomitan dictionaries.
|
Download the latest installer or portable `.zip` from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Make sure `mpv` is on your `PATH`.
|
||||||
|
|
||||||
### Mine
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>From source</b></summary>
|
||||||
|
|
||||||
|
See the [build-from-source guide](https://docs.subminer.moe/installation#from-source).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 2. First Launch
|
||||||
|
|
||||||
|
Run the app. On first launch SubMiner starts in the system tray, creates a default config, and opens a setup popup to finish config, install the mpv plugin, and configure Yomitan dictionaries.
|
||||||
|
|
||||||
|
### 3. Mine
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
subminer video.mkv # auto-starts overlay + resumes playback
|
subminer video.mkv # play video with overlay
|
||||||
subminer --start video.mkv # explicit overlay start (if plugin auto_start=no)
|
subminer --start video.mkv # explicit overlay start
|
||||||
subminer stats # open the immersion dashboard
|
subminer stats # open immersion dashboard
|
||||||
subminer stats -b # keep the stats daemon running in background
|
subminer stats -b # stats daemon in background
|
||||||
subminer stats -s # stop the dedicated stats daemon
|
subminer stats -s # stop background stats daemon
|
||||||
subminer stats cleanup # repair/prune stored stats vocabulary rows
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
| Required | Optional |
|
|
||||||
| ------------------------------------------------------ | ----------------------------- |
|
|
||||||
| [`mpv`](https://mpv.io) with IPC socket | `yt-dlp` |
|
|
||||||
| `ffmpeg` | `guessit` (AniSkip detection) |
|
|
||||||
| `mecab` + `mecab-ipadic` | `fzf` / `rofi` |
|
|
||||||
| [`bun`](https://bun.sh) (source builds, Linux wrapper) | `chafa`, `ffmpegthumbnailer` |
|
|
||||||
| Linux: `hyprctl` or `xdotool` + `xwininfo` | |
|
|
||||||
| macOS: Accessibility permission | |
|
|
||||||
|
|
||||||
Windows uses native window tracking and does not need the Linux compositor tools.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Full guides on configuration, Anki, Jellyfin, immersion tracking, and more at **[docs.subminer.moe](https://docs.subminer.moe)**.
|
Full guides on configuration, Anki setup, Jellyfin, immersion tracking, and more: **[docs.subminer.moe](https://docs.subminer.moe)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
Built on [GameSentenceMiner](https://github.com/bpwhelan/GameSentenceMiner), [Renji's Texthooker Page](https://github.com/Renji-XD/texthooker-ui), [Anacreon-Script](https://github.com/friedrich-de/Anacreon-Script), and [Bee's Character Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary). Subtitles from [Jimaku.cc](https://jimaku.cc). Lookups via [Yomitan](https://github.com/yomidevs/yomitan). JLPT tags from [yomitan-jlpt-vocab](https://github.com/stephenmk/yomitan-jlpt-vocab).
|
SubMiner builds on the work of these open-source projects:
|
||||||
|
|
||||||
|
| Project | Role |
|
||||||
|
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||||
|
| [Anacreon-Script](https://github.com/friedrich-de/Anacreon-Script) | Inspiration for the mining workflow |
|
||||||
|
| [asbplayer](https://github.com/killergerbah/asbplayer) | Inspiration for subtitle sidebar and logic for YouTube subtitle parsing |
|
||||||
|
| [Bee's Character Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) | Character name recognition in subtitles |
|
||||||
|
| [GameSentenceMiner](https://github.com/bpwhelan/GameSentenceMiner) | Inspiration for Electron overlay with Yomitan integration |
|
||||||
|
| [jellyfin-mpv-shim](https://github.com/jellyfin/jellyfin-mpv-shim) | Jellyfin integration |
|
||||||
|
| [Jimaku.cc](https://jimaku.cc) | Japanese subtitle search and downloads |
|
||||||
|
| [Renji's Texthooker Page](https://github.com/Renji-XD/texthooker-ui) | Base for the WebSocket texthooker integration |
|
||||||
|
| [Yomitan](https://github.com/yomidevs/yomitan) | Dictionary engine powering all lookups and the morphological parser |
|
||||||
|
| [yomitan-jlpt-vocab](https://github.com/stephenmk/yomitan-jlpt-vocab) | JLPT level tags for vocabulary |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
id: TASK-143
|
id: TASK-143
|
||||||
title: Keep character dictionary auto-sync non-blocking during startup
|
title: Keep character dictionary auto-sync non-blocking during startup
|
||||||
status: In Progress
|
status: Done
|
||||||
assignee:
|
assignee:
|
||||||
- codex
|
- codex
|
||||||
created_date: '2026-03-09 01:45'
|
created_date: '2026-03-09 01:45'
|
||||||
updated_date: '2026-03-20 09:22'
|
updated_date: '2026-03-23 03:22'
|
||||||
labels:
|
labels:
|
||||||
- dictionary
|
- dictionary
|
||||||
- startup
|
- startup
|
||||||
@@ -18,7 +18,7 @@ references:
|
|||||||
- >-
|
- >-
|
||||||
/home/sudacode/projects/japanese/SubMiner/src/main/runtime/current-media-tokenization-gate.ts
|
/home/sudacode/projects/japanese/SubMiner/src/main/runtime/current-media-tokenization-gate.ts
|
||||||
priority: high
|
priority: high
|
||||||
ordinal: 38500
|
ordinal: 144500
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
@@ -5,7 +5,7 @@ status: Done
|
|||||||
assignee:
|
assignee:
|
||||||
- '@codex'
|
- '@codex'
|
||||||
created_date: '2026-03-19 17:46'
|
created_date: '2026-03-19 17:46'
|
||||||
updated_date: '2026-03-19 17:54'
|
updated_date: '2026-03-23 03:22'
|
||||||
labels:
|
labels:
|
||||||
- stats
|
- stats
|
||||||
- immersion-tracking
|
- immersion-tracking
|
||||||
@@ -19,6 +19,7 @@ references:
|
|||||||
- src/core/services/stats-server.ts
|
- src/core/services/stats-server.ts
|
||||||
parent_task_id: TASK-177
|
parent_task_id: TASK-177
|
||||||
priority: medium
|
priority: medium
|
||||||
|
ordinal: 132500
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
@@ -5,7 +5,7 @@ status: Done
|
|||||||
assignee:
|
assignee:
|
||||||
- '@codex'
|
- '@codex'
|
||||||
created_date: '2026-03-19 19:38'
|
created_date: '2026-03-19 19:38'
|
||||||
updated_date: '2026-03-19 19:40'
|
updated_date: '2026-03-23 03:22'
|
||||||
labels:
|
labels:
|
||||||
- stats
|
- stats
|
||||||
- immersion-tracking
|
- immersion-tracking
|
||||||
@@ -17,6 +17,7 @@ references:
|
|||||||
- stats/src/lib/dashboard-data.ts
|
- stats/src/lib/dashboard-data.ts
|
||||||
parent_task_id: TASK-177
|
parent_task_id: TASK-177
|
||||||
priority: medium
|
priority: medium
|
||||||
|
ordinal: 130500
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
@@ -5,7 +5,7 @@ status: Done
|
|||||||
assignee:
|
assignee:
|
||||||
- '@codex'
|
- '@codex'
|
||||||
created_date: '2026-03-19 20:15'
|
created_date: '2026-03-19 20:15'
|
||||||
updated_date: '2026-03-19 20:17'
|
updated_date: '2026-03-23 03:22'
|
||||||
labels:
|
labels:
|
||||||
- launcher
|
- launcher
|
||||||
- stats
|
- stats
|
||||||
@@ -19,6 +19,7 @@ references:
|
|||||||
- src/main/runtime/stats-cli-command.test.ts
|
- src/main/runtime/stats-cli-command.test.ts
|
||||||
parent_task_id: TASK-177
|
parent_task_id: TASK-177
|
||||||
priority: medium
|
priority: medium
|
||||||
|
ordinal: 129500
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
@@ -5,7 +5,7 @@ status: Done
|
|||||||
assignee:
|
assignee:
|
||||||
- codex
|
- codex
|
||||||
created_date: '2026-03-19 20:31'
|
created_date: '2026-03-19 20:31'
|
||||||
updated_date: '2026-03-19 20:52'
|
updated_date: '2026-03-23 03:22'
|
||||||
labels:
|
labels:
|
||||||
- bug
|
- bug
|
||||||
- stats
|
- stats
|
||||||
@@ -17,6 +17,7 @@ references:
|
|||||||
- >-
|
- >-
|
||||||
/Users/sudacode/projects/japanese/SubMiner/stats/src/lib/session-detail.test.tsx
|
/Users/sudacode/projects/japanese/SubMiner/stats/src/lib/session-detail.test.tsx
|
||||||
parent_task_id: TASK-182
|
parent_task_id: TASK-182
|
||||||
|
ordinal: 128500
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
@@ -5,7 +5,7 @@ status: Done
|
|||||||
assignee:
|
assignee:
|
||||||
- codex
|
- codex
|
||||||
created_date: '2026-03-18 00:29'
|
created_date: '2026-03-18 00:29'
|
||||||
updated_date: '2026-03-18 00:55'
|
updated_date: '2026-03-23 03:22'
|
||||||
labels:
|
labels:
|
||||||
- stats
|
- stats
|
||||||
- performance
|
- performance
|
||||||
@@ -22,6 +22,7 @@ references:
|
|||||||
- stats/src/types/stats.ts
|
- stats/src/types/stats.ts
|
||||||
- stats/src/lib/dashboard-data.ts
|
- stats/src/lib/dashboard-data.ts
|
||||||
priority: medium
|
priority: medium
|
||||||
|
ordinal: 138500
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user