mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-27 06:12:05 -07:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4d95de51a0
|
|||
|
ed32f985c6
|
|||
|
854179b9c1
|
|||
|
a3ddfa0641
|
|||
|
49a582b4fc
|
|||
|
a92631bf52
|
|||
|
ac857e932e
|
|||
|
8c633f7e48
|
|||
|
d2cfa1b871
|
|||
|
3fe63a6afa
|
|||
|
5dd8bb7fbf
|
|||
|
5b06579e65
|
|||
|
416942ff2d
|
|||
|
2d4f2d1139
|
|||
|
77e632276b
|
|||
|
4c95b57885
|
|||
|
242402b253
|
|||
|
61d15f9431
|
|||
|
508864bcbb
|
|||
|
23c54bb01e
|
|||
|
ec667c64e8
|
|||
|
39b2ccad8e
|
|||
|
23815945bf
|
|||
|
9dca83acd9
|
|||
|
55300e2d8c
|
|||
|
28afd15134
|
|||
|
58304757aa
|
|||
|
c95518e94a
|
|||
| 5ee4617607 | |||
|
842008b089
|
|||
|
6f56a0bcf6
|
|||
| 5feed360ca | |||
| c17f0a4080 | |||
| 0317c7f011 | |||
|
13797b5005
|
|||
|
b24d9d7487
|
|||
| 3a01cffc6b | |||
|
eddf6f0456
|
|||
|
f6c024d61e
|
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
|
|
||||||
|
|||||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -334,6 +334,14 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Build changelog artifacts for release
|
||||||
|
run: |
|
||||||
|
if find changes -maxdepth 1 -name '*.md' -not -name README.md -print -quit | grep -q .; then
|
||||||
|
bun run changelog:build --version "${{ steps.version.outputs.VERSION }}"
|
||||||
|
else
|
||||||
|
echo "No pending changelog fragments found."
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Verify changelog is ready for tagged release
|
- name: Verify changelog is ready for tagged release
|
||||||
run: bun run changelog:check --version "${{ steps.version.outputs.VERSION }}"
|
run: bun run changelog:check --version "${{ steps.version.outputs.VERSION }}"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
194
Backlog.md
Normal file
194
Backlog.md
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
|
||||||
|
## Icebox
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Ticket Details
|
||||||
|
|
||||||
|
### SM-001
|
||||||
|
|
||||||
|
Title: Add tests for CLI parser and args normalizer
|
||||||
|
Priority: P1
|
||||||
|
Status: todo
|
||||||
|
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
|
||||||
70
CHANGELOG.md
70
CHANGELOG.md
@@ -1,5 +1,75 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
### 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
|
||||||
|
- 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
|
||||||
|
- Overlay: Kept sidebar cue tracking stable across playback transitions and timing edge cases.
|
||||||
|
- Overlay: Improved sidebar resume/start behavior to jump directly to the first resolved active cue.
|
||||||
|
- Overlay: Stopped stale subtitle refreshes from regressing active-cue and text state.
|
||||||
|
|
||||||
## v0.7.0 (2026-03-19)
|
## v0.7.0 (2026-03-19)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
226
README.md
226
README.md
@@ -1,60 +1,168 @@
|
|||||||
<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">
|
|
||||||
|
|
||||||
[](./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>
|
||||||
|
|
||||||
|
### Integrations
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><b>YouTube</b></td>
|
||||||
|
<td>Auto-loaded yt-dlp subtitle tracks at startup with 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 +196,63 @@ 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 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
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user