Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b763743bd
|
||
|
|
cf5ef1eda4
|
Generated
+21
-21
@@ -70,11 +70,11 @@
|
|||||||
"uv2nix": "uv2nix"
|
"uv2nix": "uv2nix"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1784633374,
|
"lastModified": 1785270744,
|
||||||
"narHash": "sha256-kaRIIOZwlfLI3dwI5TgnyuLyG5RsuwM6jsuw3s5m5wc=",
|
"narHash": "sha256-8F3atV7WA32b2d2WvljENtW14XxUtJ7fOduU8HQyOAQ=",
|
||||||
"owner": "NousResearch",
|
"owner": "NousResearch",
|
||||||
"repo": "hermes-agent",
|
"repo": "hermes-agent",
|
||||||
"rev": "64702f8f91661149128ca1a721f7a0fd4c22113b",
|
"rev": "28d11ab38c4cbee47f14a1064ada363f7ea93d3f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -90,11 +90,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1783740085,
|
"lastModified": 1785119570,
|
||||||
"narHash": "sha256-qajyHfZY29G2oEQk+uHxmsJcRoBUBXP9maTpFlwP/dI=",
|
"narHash": "sha256-Rgs2xKnGLFWQscxUaXX07oyZeuMDOHEbqDOsgliLFGM=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "3cd22efe6471dc7365c822bd9ad73a21e55f38fb",
|
"rev": "d4fd24667c8cbef124bb70a20380cab75ec8474d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -111,11 +111,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1783823409,
|
"lastModified": 1785269816,
|
||||||
"narHash": "sha256-OI4IkRjRXa1e7hYmCGJDPDq5H/kPwhsyoS80cNUF9fI=",
|
"narHash": "sha256-6JkNDlJ18NY1iAuoSYCPDO7O+yYN/vPwpFmHOHNpVvY=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "7566825d4652a1b885bd4ce65bd9e8def432fec9",
|
"rev": "3d25fa94cf355cf7da64c77bb9c3c15806486329",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -132,11 +132,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1782839684,
|
"lastModified": 1783963347,
|
||||||
"narHash": "sha256-vzs4SBgPsK4aNzlJR2PpFwtARazXMOxZonQnDz0YHxk=",
|
"narHash": "sha256-r376E2XpakiXwModDHIxlvB6qLq4iFVEq730vxOO4JY=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "2a37d71bbe69e1522ddabf03a4cea0374958bdbe",
|
"rev": "a45a7c451455a51ae740ec3bce4024b312809c29",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -163,11 +163,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-deck": {
|
"nixpkgs-deck": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1783776592,
|
"lastModified": 1785090369,
|
||||||
"narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=",
|
"narHash": "sha256-m0pDuRJG7EDo9ri+4Ksu83VsI+PlxNC9lNBfydejce4=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3",
|
"rev": "624af665418d3c65d544145b4d34ad696439570e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -179,11 +179,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1783703440,
|
"lastModified": 1785133411,
|
||||||
"narHash": "sha256-O3/YajjWo001VUIgD8BwaRdSNLUFe7nZ1qV5TwhRBcw=",
|
"narHash": "sha256-Yjv0WEg39KRYS0rBdTbu6Fc/or/ihAKk13W9sQ6VWd0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "8f0500b9660505dc3cb647775fe9a978a74b5283",
|
"rev": "2f5a153c270b70cb0f8c11f46d96d6d3bc39f4e3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -323,11 +323,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1783866614,
|
"lastModified": 1785224333,
|
||||||
"narHash": "sha256-o2h5BfVz9MPIERBPVHtvQGilphtNn5+cCuqqfY5Lkq8=",
|
"narHash": "sha256-8DNJUVM10ktK02V9ZIV064CH+fuPF272VCAZBn6arig=",
|
||||||
"owner": "0xc000022070",
|
"owner": "0xc000022070",
|
||||||
"repo": "zen-browser-flake",
|
"repo": "zen-browser-flake",
|
||||||
"rev": "51602966429e8ccae61324e56b51c37308d1b64e",
|
"rev": "5f1a4be4d280e1ae06d61a75775a234b1714da3f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -27,11 +27,11 @@
|
|||||||
../../modules/home-manager/opencode
|
../../modules/home-manager/opencode
|
||||||
../../modules/home-manager/cedilla
|
../../modules/home-manager/cedilla
|
||||||
../../modules/home-manager/dictation
|
../../modules/home-manager/dictation
|
||||||
|
../../modules/home-manager/gif-recorder
|
||||||
../../modules/home-manager/nvim
|
../../modules/home-manager/nvim
|
||||||
../../modules/home-manager/pi
|
../../modules/home-manager/pi
|
||||||
../../modules/home-manager/tmux
|
../../modules/home-manager/tmux
|
||||||
../../modules/home-manager/openspec
|
../../modules/home-manager/openspec
|
||||||
../../modules/home-manager/dev-orchestrator
|
|
||||||
../../modules/home-manager/power-profiles
|
../../modules/home-manager/power-profiles
|
||||||
../../modules/home-manager/ntfy-notify
|
../../modules/home-manager/ntfy-notify
|
||||||
../../modules/home-manager/pinentry
|
../../modules/home-manager/pinentry
|
||||||
@@ -88,6 +88,7 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
modelHash = "sha256-ZNGCtEC5jVIDxPm9VBVE2ExgUZbE97hF36EfsjWU0eI=";
|
modelHash = "sha256-ZNGCtEC5jVIDxPm9VBVE2ExgUZbE97hF36EfsjWU0eI=";
|
||||||
};
|
};
|
||||||
|
gif-recorder.enable = true;
|
||||||
|
|
||||||
nvim.enable = true;
|
nvim.enable = true;
|
||||||
pi.enable = true;
|
pi.enable = true;
|
||||||
@@ -95,7 +96,6 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
};
|
};
|
||||||
openspec.enable = true;
|
openspec.enable = true;
|
||||||
dev-orchestrator.enable = true;
|
|
||||||
power-profiles.enable = true;
|
power-profiles.enable = true;
|
||||||
ntfy-notify.enable = true;
|
ntfy-notify.enable = true;
|
||||||
bitwarden.enable = true;
|
bitwarden.enable = true;
|
||||||
@@ -117,6 +117,8 @@
|
|||||||
nodejs
|
nodejs
|
||||||
firefox
|
firefox
|
||||||
telegram-desktop
|
telegram-desktop
|
||||||
|
koreader
|
||||||
|
xorg.xauth
|
||||||
];
|
];
|
||||||
|
|
||||||
programs = {
|
programs = {
|
||||||
|
|||||||
@@ -3,16 +3,21 @@
|
|||||||
config,
|
config,
|
||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
}: let
|
}:
|
||||||
|
let
|
||||||
bw = pkgs.writeShellApplication {
|
bw = pkgs.writeShellApplication {
|
||||||
name = "bw";
|
name = "bw";
|
||||||
runtimeInputs = with pkgs; [ rbw fzf wl-clipboard libnotify ];
|
runtimeInputs = with pkgs; [
|
||||||
|
rbw
|
||||||
|
fzf
|
||||||
|
wl-clipboard
|
||||||
|
libnotify
|
||||||
|
];
|
||||||
text = ''
|
text = ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cmd="$(basename "$0")"
|
cmd="$(basename "$0")"
|
||||||
|
|
||||||
entry="$(rbw list --fields=id,name,user --format=tsv \
|
entry="$(rbw list --fields=id,name,user | fzf --with-nth=2.. --delimiter=$'\t')"
|
||||||
| fzf --with-nth=2.. --delimiter=$'\t')"
|
|
||||||
[ -z "$entry" ] && exit 0
|
[ -z "$entry" ] && exit 0
|
||||||
|
|
||||||
id="$(printf '%s' "$entry" | cut -f1)"
|
id="$(printf '%s' "$entry" | cut -f1)"
|
||||||
@@ -28,7 +33,8 @@
|
|||||||
notify-send -t 1200 "bw: copied $cmd"
|
notify-send -t 1200 "bw: copied $cmd"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
options.custom = {
|
options.custom = {
|
||||||
bitwarden.enable = lib.mkEnableOption "enable bitwarden - CLI password manager";
|
bitwarden.enable = lib.mkEnableOption "enable bitwarden - CLI password manager";
|
||||||
};
|
};
|
||||||
@@ -45,14 +51,20 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
home.packages =
|
home.packages =
|
||||||
let mkBin = name:
|
let
|
||||||
pkgs.symlinkJoin {
|
mkBin =
|
||||||
inherit name;
|
name:
|
||||||
paths = [ bw ];
|
pkgs.symlinkJoin {
|
||||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
inherit name;
|
||||||
postBuild = "ln -sf ${bw}/bin/bw $out/bin/${name}";
|
paths = [ bw ];
|
||||||
};
|
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||||
|
postBuild = "ln -sf ${bw}/bin/bw $out/bin/${name}";
|
||||||
|
};
|
||||||
in
|
in
|
||||||
builtins.map mkBin [ "bwp" "bwc" "bwu" ];
|
builtins.map mkBin [
|
||||||
|
"bwp"
|
||||||
|
"bwc"
|
||||||
|
"bwu"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
{ lib, config, pkgs, ... }:
|
|
||||||
{
|
|
||||||
options.custom = {
|
|
||||||
dev-orchestrator.enable = lib.mkEnableOption "enable dev-orchestrator — OpenSpec + OpenCode dev→QA loop with git worktrees";
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf config.custom.dev-orchestrator.enable {
|
|
||||||
home.packages = [
|
|
||||||
(pkgs.writeShellApplication {
|
|
||||||
name = "dev-orchestrator";
|
|
||||||
runtimeInputs = with pkgs; [ git ];
|
|
||||||
text = builtins.readFile ../../../tooling/dev-orchestrator;
|
|
||||||
})
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.custom.gif-recorder;
|
||||||
|
|
||||||
|
gif-recorder = pkgs.writeShellApplication {
|
||||||
|
name = "gif-recorder";
|
||||||
|
runtimeInputs = with pkgs; [
|
||||||
|
coreutils
|
||||||
|
ffmpeg
|
||||||
|
slurp
|
||||||
|
wf-recorder
|
||||||
|
wl-clipboard
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
runtime_dir="''${XDG_RUNTIME_DIR:-/tmp}"
|
||||||
|
state="$runtime_dir/gif-recorder.pid"
|
||||||
|
selection=""
|
||||||
|
video=""
|
||||||
|
gif_tmp=""
|
||||||
|
child_pid=""
|
||||||
|
stopped=0
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$state" "$selection" "$video" "$gif_tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_child() {
|
||||||
|
stopped=1
|
||||||
|
if [ -n "$child_pid" ]; then
|
||||||
|
kill -INT "$child_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
trap stop_child INT TERM
|
||||||
|
|
||||||
|
if [ -f "$state" ]; then
|
||||||
|
active_pid="$(cat "$state" 2>/dev/null || true)"
|
||||||
|
if [ -n "$active_pid" ] && kill -0 "$active_pid" 2>/dev/null; then
|
||||||
|
kill -INT "$active_pid" 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
rm -f "$state"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$$" > "$state"
|
||||||
|
selection="$(mktemp "$runtime_dir/gif-recorder-selection.XXXXXX")"
|
||||||
|
slurp > "$selection" &
|
||||||
|
child_pid="$!"
|
||||||
|
selection_status=0
|
||||||
|
wait "$child_pid" || selection_status="$?"
|
||||||
|
child_pid=""
|
||||||
|
|
||||||
|
if [ "$stopped" -eq 1 ] || [ "$selection_status" -ne 0 ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
geometry="$(cat "$selection")"
|
||||||
|
video="$(mktemp "$runtime_dir/gif-recorder.XXXXXX.mkv")"
|
||||||
|
wf-recorder --geometry "$geometry" -f "$video" &
|
||||||
|
child_pid="$!"
|
||||||
|
recorder_status=0
|
||||||
|
wait "$child_pid" || recorder_status="$?"
|
||||||
|
child_pid=""
|
||||||
|
|
||||||
|
if [ "$stopped" -ne 1 ] || { [ "$recorder_status" -ne 0 ] && [ "$recorder_status" -ne 130 ]; }; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
output_dir="$HOME/Pictures/Screenshots"
|
||||||
|
mkdir -p "$output_dir"
|
||||||
|
gif_tmp="$(mktemp "$runtime_dir/gif-recorder.XXXXXX.gif")"
|
||||||
|
ffmpeg -y -i "$video" \
|
||||||
|
-vf 'fps=15,scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=256[p];[s1][p]paletteuse=dither=sierra2_4a' \
|
||||||
|
"$gif_tmp"
|
||||||
|
|
||||||
|
output="$output_dir/gif-$(date +%Y%m%d-%H%M%S-%N).gif"
|
||||||
|
mv "$gif_tmp" "$output"
|
||||||
|
gif_tmp=""
|
||||||
|
wl-copy --foreground --type image/gif < "$output" &
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.custom.gif-recorder.enable = lib.mkEnableOption "Wayland region GIF recorder";
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
home.packages = [ gif-recorder ];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -240,6 +240,7 @@ in
|
|||||||
hl.bind(mod_shft .. " + 2", hl.dsp.exec_cmd("hyprshot -m output -m DP-1 -o ~/Pictures/Screenshots"))
|
hl.bind(mod_shft .. " + 2", hl.dsp.exec_cmd("hyprshot -m output -m DP-1 -o ~/Pictures/Screenshots"))
|
||||||
hl.bind(mod_shft .. " + 3", hl.dsp.exec_cmd("hyprshot -m window -m active -o ~/Pictures/Screenshots"))
|
hl.bind(mod_shft .. " + 3", hl.dsp.exec_cmd("hyprshot -m window -m active -o ~/Pictures/Screenshots"))
|
||||||
hl.bind(mod_shft .. " + 4", hl.dsp.exec_cmd("hyprshot -m region -o ~/Pictures/Screenshots"))
|
hl.bind(mod_shft .. " + 4", hl.dsp.exec_cmd("hyprshot -m region -o ~/Pictures/Screenshots"))
|
||||||
|
hl.bind(mod_shft .. " + 5", hl.dsp.exec_cmd("gif-recorder"))
|
||||||
|
|
||||||
-- ---------- OBS Recording ----------
|
-- ---------- OBS Recording ----------
|
||||||
hl.bind(mod_shft .. " + 9", hl.dsp.exec_cmd("obs-cmd recording toggle"))
|
hl.bind(mod_shft .. " + 9", hl.dsp.exec_cmd("obs-cmd recording toggle"))
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-07-30
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
The desktop already uses Home Manager modules under `modules/home-manager` and keeps Hyprland keybinds in `modules/home-manager/hyprland/default.nix`. The new capability is a small user-session workflow, not a daemon: select a region with `slurp`, record it with the Wayland-native `wf-recorder`, encode the result with `ffmpeg`, and publish the finished file with `wl-copy`.
|
||||||
|
|
||||||
|
The same Hyprland bind must start and stop the workflow. The first invocation needs to remain alive while recording; a later invocation needs a stable way to find and signal that process. The implementation also needs to avoid leaving partial files in the screenshots directory when selection is cancelled or encoding fails.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Encapsulate packages, script, option, output directory, and runtime state in `modules/home-manager/gif-recorder`.
|
||||||
|
- Use a single toggle command suitable for a Hyprland `bind`.
|
||||||
|
- Select a rectangular region interactively through `slurp`.
|
||||||
|
- Save optimized GIF output in `~/Pictures/Screenshots` and copy it as `image/gif` with `wl-copy`.
|
||||||
|
- Keep host wiring minimal: import/enable in `hosts/jpporta-nixos/home.nix`, bind in the existing Hyprland module.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Full-screen or window recording modes.
|
||||||
|
- A recording history, notification UI, configurable encoder settings, or a separate stop bind.
|
||||||
|
- A new service or background daemon.
|
||||||
|
- Changes to the existing screenshot flow.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D1. One shell script owns the toggle state
|
||||||
|
|
||||||
|
The module installs one `gif-recorder` executable and uses a runtime-directory state file containing the active script PID. If the state file names a live process, a new invocation sends it `SIGINT`; otherwise it runs `slurp` and starts a recording. This keeps the bind to one command and avoids adding a service or IPC layer.
|
||||||
|
|
||||||
|
**Alternative:** use separate start/stop scripts or a systemd user service. Rejected because they add commands and lifecycle machinery for one short-lived action.
|
||||||
|
|
||||||
|
### D2. Use `wf-recorder`, `slurp`, `ffmpeg`, and `wl-copy`
|
||||||
|
|
||||||
|
These tools are small, composable Wayland utilities already suited to the requested flow. `wf-recorder` receives the geometry selected by `slurp`; `ffmpeg` handles palette generation and GIF encoding; `wl-copy --type image/gif` makes the resulting file available to Wayland clipboard consumers.
|
||||||
|
|
||||||
|
**Alternative:** OBS or a screenshot/GIF application. Rejected because a GUI recorder is heavier than the requested Hyprshot-like interaction.
|
||||||
|
|
||||||
|
### D3. Encode through a temporary video file
|
||||||
|
|
||||||
|
Recording first to a temporary Matroska file keeps the output atomic: the final timestamped GIF is moved into `~/Pictures/Screenshots` only after `ffmpeg` succeeds. The encoder uses a palette-generation filter and a bounded frame rate/width to avoid the worst GIF size and color-quality problems without introducing configuration options.
|
||||||
|
|
||||||
|
**Alternative:** record directly to GIF. Rejected because palette generation and interruption handling are worse, and `wf-recorder` is designed primarily for video output.
|
||||||
|
|
||||||
|
### D4. Store state under `XDG_RUNTIME_DIR`
|
||||||
|
|
||||||
|
The state file belongs in `${XDG_RUNTIME_DIR:-/tmp}` and is removed on normal completion or cancellation. It is session-local and avoids polluting the home directory with a lock file.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [GIFs are inherently large and limited to 256 colors] -> Cap output at a modest frame rate and width; keep the original temporary recording out of the screenshots directory.
|
||||||
|
- [A crashed process can leave a stale state file] -> Check whether the recorded PID is alive before treating the workflow as active, and remove stale state before starting.
|
||||||
|
- [Clipboard consumers differ in GIF support] -> Publish with the explicit `image/gif` MIME type; the saved file remains the fallback when a consumer cannot paste animated images.
|
||||||
|
- [The second bind may be pressed while region selection is active] -> Keep the controlling script PID in the state file before invoking `slurp`, and handle interruption by cleaning up without creating output.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Apply the Home Manager configuration for `jpporta-nixos`.
|
||||||
|
2. Press the new Hyprland bind and select a region; press it again to stop and wait for encoding.
|
||||||
|
3. Verify the GIF in `~/Pictures/Screenshots` and paste it into a GIF-capable Wayland application.
|
||||||
|
4. Roll back by disabling/removing the module import and bind; no persistent data migration is required, and existing GIF files are left untouched.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- The exact key combination should follow the existing screenshot bind convention in the Hyprland module; implementation should reuse that convention rather than introduce a new modifier scheme.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
The Hyprland desktop has a screenshot flow but no equally convenient way to capture a short animated region of the screen. A Wayland-native GIF recorder should let the user select a region, press the same bind to stop, and receive the finished GIF in `~/Pictures/Screenshots` and on the clipboard.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Add a new Home Manager module, `modules/home-manager/gif-recorder`, with a single enable option.
|
||||||
|
- Provide a Wayland-native recording script using `wf-recorder` and `slurp` to select and record a screen region.
|
||||||
|
- Toggle recording with one Hyprland bind: the first press starts region selection/recording and the next press stops it.
|
||||||
|
- Convert the recording to an optimized GIF after stopping.
|
||||||
|
- Save completed GIFs under `~/Pictures/Screenshots` with timestamped filenames.
|
||||||
|
- Copy the completed GIF to the Wayland clipboard for immediate pasting.
|
||||||
|
- Enable the module from `hosts/jpporta-nixos/home.nix` and add only the required bind in the existing Hyprland Home Manager module.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `gif-recorder`: Select, toggle-record, save, and clipboard-copy short Wayland screen-region GIFs.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- None.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- New Home Manager module and user-facing recording script.
|
||||||
|
- Existing `modules/home-manager/hyprland/default.nix` gains the toggle bind.
|
||||||
|
- `hosts/jpporta-nixos/home.nix` imports and enables the module.
|
||||||
|
- Adds the runtime packages `wf-recorder`, `slurp`, `ffmpeg`, and a Wayland clipboard utility through the module.
|
||||||
|
- Creates `~/Pictures/Screenshots` when a recording is completed; existing screenshot behavior remains unchanged.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Region recording is toggled by one command
|
||||||
|
The GIF recorder SHALL use one executable that starts an interactive region recording when idle and stops the active recording when invoked again.
|
||||||
|
|
||||||
|
#### Scenario: Start a recording
|
||||||
|
- **WHEN** the recorder is invoked while no recording is active
|
||||||
|
- **THEN** it lets the user select a rectangular Wayland region and starts recording that region
|
||||||
|
|
||||||
|
#### Scenario: Stop a recording
|
||||||
|
- **WHEN** the recorder is invoked while a recording is active
|
||||||
|
- **THEN** it stops the active recording and begins finalization without starting a second recording
|
||||||
|
|
||||||
|
#### Scenario: Cancel region selection
|
||||||
|
- **WHEN** the user cancels region selection before recording starts
|
||||||
|
- **THEN** the recorder exits without creating a GIF and clears its runtime state
|
||||||
|
|
||||||
|
### Requirement: Completed recordings are saved as GIF screenshots
|
||||||
|
The recorder SHALL save each successfully finalized recording as a timestamped `.gif` file below `~/Pictures/Screenshots`.
|
||||||
|
|
||||||
|
#### Scenario: Successful finalization
|
||||||
|
- **WHEN** an active recording is stopped and encoding succeeds
|
||||||
|
- **THEN** the recorder creates the screenshots directory if needed and stores one completed GIF there
|
||||||
|
|
||||||
|
#### Scenario: Failed finalization
|
||||||
|
- **WHEN** recording or GIF encoding fails
|
||||||
|
- **THEN** the recorder does not publish a partial GIF as a completed screenshot and removes temporary recording data
|
||||||
|
|
||||||
|
### Requirement: Completed recordings are copied to the Wayland clipboard
|
||||||
|
After successful GIF encoding, the recorder SHALL copy the resulting file to the Wayland clipboard using the `image/gif` MIME type.
|
||||||
|
|
||||||
|
#### Scenario: Clipboard copy succeeds
|
||||||
|
- **WHEN** a GIF is successfully written
|
||||||
|
- **THEN** the recorder publishes that GIF to the clipboard as `image/gif`
|
||||||
|
|
||||||
|
#### Scenario: Clipboard copy is unavailable
|
||||||
|
- **WHEN** a GIF is successfully written but clipboard publication fails
|
||||||
|
- **THEN** the recorder preserves the GIF in `~/Pictures/Screenshots` and exits without deleting it
|
||||||
|
|
||||||
|
### Requirement: Home Manager integration is opt-in
|
||||||
|
The feature SHALL be exposed as a Home Manager module with an enable option, and enabling it SHALL install all required runtime tools and the recorder executable.
|
||||||
|
|
||||||
|
#### Scenario: Module enabled
|
||||||
|
- **WHEN** `custom.gif-recorder.enable` is true
|
||||||
|
- **THEN** the user environment contains the recorder command and its Wayland recording, selection, encoding, and clipboard dependencies
|
||||||
|
|
||||||
|
#### Scenario: Module disabled
|
||||||
|
- **WHEN** `custom.gif-recorder.enable` is false
|
||||||
|
- **THEN** the module adds no GIF recorder command, packages, or Hyprland bind
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
## 1. Add the Home Manager module
|
||||||
|
|
||||||
|
- [x] 1.1 Create `modules/home-manager/gif-recorder/default.nix` with `custom.gif-recorder.enable` and an enabled-only configuration.
|
||||||
|
- [x] 1.2 Install `wf-recorder`, `slurp`, `ffmpeg`, and `wl-clipboard` through the module and expose one `gif-recorder` script in the user's session path.
|
||||||
|
- [x] 1.3 Implement the toggle state under `XDG_RUNTIME_DIR`: start region selection/recording when idle, signal the active process on the second invocation, clean up on cancellation/interruption, and reject stale state.
|
||||||
|
- [x] 1.4 Finalize successful recordings with palette-based `ffmpeg` GIF encoding, timestamp the output below `~/Pictures/Screenshots`, and copy it with `wl-copy --type image/gif` while preserving the saved file if clipboard copy fails.
|
||||||
|
|
||||||
|
## 2. Wire the feature into the existing host
|
||||||
|
|
||||||
|
- [x] 2.1 Import `../../modules/home-manager/gif-recorder` and set `custom.gif-recorder.enable = true` in `hosts/jpporta-nixos/home.nix`.
|
||||||
|
- [x] 2.2 Add `SUPER + SHIFT + 5` to the existing `Screenshots` section of `modules/home-manager/hyprland/default.nix`, invoking `gif-recorder` so the same bind starts and stops the capture.
|
||||||
|
|
||||||
|
## 3. Verify the workflow
|
||||||
|
|
||||||
|
- [x] 3.1 Evaluate/build the `jpporta-nixos` Home Manager activation package and confirm the module and script parse successfully.
|
||||||
|
- [x] 3.2 Confirm the bind invokes the installed command and that the runtime dependency paths are available in the script.
|
||||||
|
- [ ] 3.3 In a live Hyprland session, select a region, stop with the same bind, and verify a timestamped GIF appears in `~/Pictures/Screenshots` and can be pasted from the Wayland clipboard.
|
||||||
|
- [ ] 3.4 Verify canceling `slurp`, stopping early, and an encoding failure leave no partial GIF and no stale runtime state.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# GIF Recorder
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This capability provides an opt-in Wayland workflow for recording a selected screen region as a GIF, saving it as a screenshot, and copying it to the clipboard.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Region recording is toggled by one command
|
||||||
|
The GIF recorder SHALL use one executable that starts an interactive region recording when idle and stops the active recording when invoked again.
|
||||||
|
|
||||||
|
#### Scenario: Start a recording
|
||||||
|
- **WHEN** the recorder is invoked while no recording is active
|
||||||
|
- **THEN** it lets the user select a rectangular Wayland region and starts recording that region
|
||||||
|
|
||||||
|
#### Scenario: Stop a recording
|
||||||
|
- **WHEN** the recorder is invoked while a recording is active
|
||||||
|
- **THEN** it stops the active recording and begins finalization without starting a second recording
|
||||||
|
|
||||||
|
#### Scenario: Cancel region selection
|
||||||
|
- **WHEN** the user cancels region selection before recording starts
|
||||||
|
- **THEN** the recorder exits without creating a GIF and clears its runtime state
|
||||||
|
|
||||||
|
### Requirement: Completed recordings are saved as GIF screenshots
|
||||||
|
The recorder SHALL save each successfully finalized recording as a timestamped `.gif` file below `~/Pictures/Screenshots`.
|
||||||
|
|
||||||
|
#### Scenario: Successful finalization
|
||||||
|
- **WHEN** an active recording is stopped and encoding succeeds
|
||||||
|
- **THEN** the recorder creates the screenshots directory if needed and stores one completed GIF there
|
||||||
|
|
||||||
|
#### Scenario: Failed finalization
|
||||||
|
- **WHEN** recording or GIF encoding fails
|
||||||
|
- **THEN** the recorder does not publish a partial GIF as a completed screenshot and removes temporary recording data
|
||||||
|
|
||||||
|
### Requirement: Completed recordings are copied to the Wayland clipboard
|
||||||
|
After successful GIF encoding, the recorder SHALL copy the resulting file to the Wayland clipboard using the `image/gif` MIME type.
|
||||||
|
|
||||||
|
#### Scenario: Clipboard copy succeeds
|
||||||
|
- **WHEN** a GIF is successfully written
|
||||||
|
- **THEN** the recorder publishes that GIF to the clipboard as `image/gif`
|
||||||
|
|
||||||
|
#### Scenario: Clipboard copy is unavailable
|
||||||
|
- **WHEN** a GIF is successfully written but clipboard publication fails
|
||||||
|
- **THEN** the recorder preserves the GIF in `~/Pictures/Screenshots` and exits without deleting it
|
||||||
|
|
||||||
|
### Requirement: Home Manager integration is opt-in
|
||||||
|
The feature SHALL be exposed as a Home Manager module with an enable option, and enabling it SHALL install all required runtime tools and the recorder executable.
|
||||||
|
|
||||||
|
#### Scenario: Module enabled
|
||||||
|
- **WHEN** `custom.gif-recorder.enable` is true
|
||||||
|
- **THEN** the user environment contains the recorder command and its Wayland recording, selection, encoding, and clipboard dependencies
|
||||||
|
|
||||||
|
#### Scenario: Module disabled
|
||||||
|
- **WHEN** `custom.gif-recorder.enable` is false
|
||||||
|
- **THEN** the module adds no GIF recorder command, packages, or Hyprland bind
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
# Dev Orchestrator — AI Workflow Guide
|
|
||||||
|
|
||||||
Toolchain de desenvolvimento com IA: **OpenCode** (agente) + **OpenSpec** (spec-driven dev) + **Dev Orchestrator** (loop dev→QA automático).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ferramentas — Quando Usar Cada Uma
|
|
||||||
|
|
||||||
### OpenCode — O Agente
|
|
||||||
|
|
||||||
Executa tarefas de código. Use **diretamente** quando:
|
|
||||||
|
|
||||||
- Explorar uma codebase nova: `opencode` (TUI interativo)
|
|
||||||
- Fazer uma mudança pontual: `opencode run "corrige typo no header"`
|
|
||||||
- Debugar um erro específico: `opencode run "por que esse teste falha?" --thinking`
|
|
||||||
- Revisar um PR manualmente: `opencode pr 42`
|
|
||||||
|
|
||||||
Use via **orquestrador** quando:
|
|
||||||
- Implementar uma feature completa com spec (`dev-orchestrator build`)
|
|
||||||
- Precisar de QA automático depois do dev
|
|
||||||
- Quiser loop dev→QA sem babysitting
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Modos do OpenCode
|
|
||||||
opencode # TUI interativo (exploração)
|
|
||||||
opencode run "tarefa" # one-shot (automação)
|
|
||||||
opencode run "..." --thinking # vê o raciocínio do modelo
|
|
||||||
opencode run "..." -f arquivo.ts # anexa contexto
|
|
||||||
```
|
|
||||||
|
|
||||||
### OpenSpec — O Spec Engine
|
|
||||||
|
|
||||||
Gerencia specs como source of truth. Use **diretamente** quando:
|
|
||||||
|
|
||||||
- Criar spec manualmente: `openspec new change minha-feature`
|
|
||||||
- Validar um spec existente: `openspec validate minha-feature --strict`
|
|
||||||
- Ver status de todos os changes: `openspec list --json`
|
|
||||||
- Arquivar spec concluído: `openspec archive minha-feature --yes`
|
|
||||||
|
|
||||||
Use via **orquestrador** quando:
|
|
||||||
- Quiser que a IA preencha o spec automaticamente (`dev-orchestrator spec`)
|
|
||||||
- O archive deve acontecer automático pós-QA-pass (`dev-orchestrator build`)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Comandos OpenSpec que você mais usa
|
|
||||||
openspec list # o que está ativo?
|
|
||||||
openspec show minha-feature # ler um spec
|
|
||||||
openspec validate minha-feature # check pré-implementação
|
|
||||||
openspec status --change minha-feature # progresso dos artefatos
|
|
||||||
openspec instructions --change minha-feature # o que o agente deve fazer
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dev Orchestrator — O Script
|
|
||||||
|
|
||||||
Automatiza o loop completo. **Substitui** os comandos manuais acima no fluxo principal.
|
|
||||||
|
|
||||||
```
|
|
||||||
você tem ideia → spec → review → build → PR pronto
|
|
||||||
↑ ↑ ↑
|
|
||||||
orquestrador você automático
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pré-requisitos
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g opencode-ai@latest
|
|
||||||
npm install -g @fission-ai/openspec@latest
|
|
||||||
opencode auth login # configura provider (OpenRouter, Anthropic, etc.)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comandos do Orquestrador
|
|
||||||
|
|
||||||
| Comando | O que faz |
|
|
||||||
|---|---|
|
|
||||||
| `dev-orchestrator init` | Configura repo (OpenSpec + .gitignore) — 1x por projeto |
|
|
||||||
| `dev-orchestrator spec <nome>` | Cria spec + worktree isolado + preenche spec via IA |
|
|
||||||
| `dev-orchestrator build <nome>` | Loop dev→QA (3 tentativas), merge automático se passar |
|
|
||||||
| `dev-orchestrator status` | Dashboard de features em andamento |
|
|
||||||
| `dev-orchestrator clean <nome>` | Remove worktree + branch (abortar feature) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fluxo Completo
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐
|
|
||||||
│ init │───→│ spec <nome> │───→│ [review] │───→│ build <nome> │
|
|
||||||
│ (1x/proj)│ │ (orquestrador│ │ (você aprova) │ │ (dev→QA loop)│
|
|
||||||
└──────────┘ │ preenche) │ └──────────────┘ └──────┬──────┘
|
|
||||||
└──────────────┘ │
|
|
||||||
┌────────────────────┘
|
|
||||||
│ QA PASS → merge + archive
|
|
||||||
│ QA FAIL → retry (3x max)
|
|
||||||
└────────────────────────
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1. Inicializar
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/projetos/meu-app
|
|
||||||
dev-orchestrator init
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Especificar feature
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dev-orchestrator spec add-oauth
|
|
||||||
```
|
|
||||||
|
|
||||||
O que acontece internamente:
|
|
||||||
1. `openspec new change add-oauth --json` → cria templates
|
|
||||||
2. `git worktree add -b dev-flow/add-oauth ../.worktrees/add-oauth/ HEAD`
|
|
||||||
3. `npm install` dentro do worktree
|
|
||||||
4. `opencode run "preenche os specs lendo a codebase"` → orquestrador preenche spec.md, design.md, tasks.md
|
|
||||||
|
|
||||||
### 3. Revisar os specs (MOMENTO CRÍTICO)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ler o spec gerado
|
|
||||||
cat openspec/changes/add-oauth/spec.md
|
|
||||||
cat openspec/changes/add-oauth/design.md
|
|
||||||
|
|
||||||
# Validar
|
|
||||||
openspec validate add-oauth --strict
|
|
||||||
|
|
||||||
# Ajustar manualmente se quiser — edite os arquivos
|
|
||||||
vim openspec/changes/add-oauth/spec.md
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Você é o gatekeeper aqui.** Se o spec não está certo, o build vai sair errado.
|
|
||||||
> Gaste tempo revisando os specs — é o investimento mais rentável do fluxo.
|
|
||||||
|
|
||||||
### 4. Build (dev→QA loop)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dev-orchestrator build add-oauth
|
|
||||||
```
|
|
||||||
|
|
||||||
Loop:
|
|
||||||
```
|
|
||||||
Dev Phase ──→ OpenCode implementa + testa + commita
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
QA Phase ──→ OpenCode revisa: spec compliance, testes, regressões
|
|
||||||
│
|
|
||||||
├── PASS → merge no main, archive spec, remove worktree ✅
|
|
||||||
│
|
|
||||||
└── FAIL → volta pro Dev (até 3 tentativas)
|
|
||||||
falhou 3x → worktree mantido pra correção manual
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Status dashboard
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dev-orchestrator status
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quando Sair do Orquestrador
|
|
||||||
|
|
||||||
O script cobre 90% dos casos. Saia dele quando:
|
|
||||||
|
|
||||||
| Situação | O que fazer |
|
|
||||||
|---|---|
|
|
||||||
| Spec ficou ruim e quero reescrever do zero | `openspec new change X` manual, preenche na mão |
|
|
||||||
| QA rejeitou e quero corrigir eu mesmo | `cd ../.worktrees/feature/` → edita → commita → `dev-orchestrator build feature` |
|
|
||||||
| Feature complexa demais pra um spec só | `openspec new change feature-pt1`, `openspec new change feature-pt2` |
|
|
||||||
| Quero iterar rápido sem spec | `opencode` TUI direto (pula o orquestrador) |
|
|
||||||
| Bug fix trivial (1-2 linhas) | `opencode run "fix: ..."` direto, nem cria spec |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Paralelismo
|
|
||||||
|
|
||||||
Rode múltiplos features ao mesmo tempo:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Terminal 1
|
|
||||||
dev-orchestrator build feature-a
|
|
||||||
|
|
||||||
# Terminal 2
|
|
||||||
dev-orchestrator build feature-b
|
|
||||||
```
|
|
||||||
|
|
||||||
Cada um em seu worktree isolado. O merge no main serializa no final — se houver conflito, o script para e avisa.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Estrutura de Diretórios
|
|
||||||
|
|
||||||
```
|
|
||||||
~/projetos/meu-app/
|
|
||||||
├── src/
|
|
||||||
├── openspec/
|
|
||||||
│ ├── specs/ # specs arquivados
|
|
||||||
│ └── changes/ # specs ativos
|
|
||||||
│ └── add-oauth/
|
|
||||||
│ ├── spec.md
|
|
||||||
│ ├── design.md
|
|
||||||
│ └── tasks.md
|
|
||||||
├── .gitignore # inclui .worktrees/
|
|
||||||
└── ...
|
|
||||||
|
|
||||||
../.worktrees/
|
|
||||||
└── add-oauth/ # worktree isolado
|
|
||||||
├── .git # branch: dev-flow/add-oauth
|
|
||||||
├── src/
|
|
||||||
├── node_modules/
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Opcional: Swarm Tools + .hive
|
|
||||||
|
|
||||||
Kanban mais visual, sem servidor:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g opencode-swarm-plugin
|
|
||||||
swarm setup
|
|
||||||
```
|
|
||||||
|
|
||||||
Dentro do OpenCode:
|
|
||||||
- `/swarm "tarefa"` — decompõe e spawna workers paralelos
|
|
||||||
- `/hive` — quadro kanban das tasks
|
|
||||||
- `/inbox` — mensagens entre agentes
|
|
||||||
|
|
||||||
O `.hive/` é uma pasta git-tracked. Independe do `dev-orchestrator`.
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# dev-orchestrator — OpenSpec + OpenCode dev→QA loop with git worktrees
|
|
||||||
# Zero dependencies beyond git, opencode, openspec. No heavy binaries.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
REPO="${DEV_FLOW_REPO:-$(pwd)}"
|
|
||||||
WORKTREE_ROOT="$REPO/../.worktrees"
|
|
||||||
|
|
||||||
# ── helpers ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_wt_path() { echo "$WORKTREE_ROOT/$1"; }
|
|
||||||
_wt_branch(){ echo "dev-flow/$1"; }
|
|
||||||
|
|
||||||
_die() { echo "✗ $*" >&2; exit 1; }
|
|
||||||
_ok() { echo "✓ $*"; }
|
|
||||||
|
|
||||||
# Ensure we're inside a git repo with openspec
|
|
||||||
_guard() {
|
|
||||||
cd "$REPO"
|
|
||||||
git rev-parse --show-toplevel >/dev/null 2>&1 || _die "not a git repo: $REPO"
|
|
||||||
[ -d openspec ] || _die "openspec not initialized. Run: dev-orchestrator init"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── commands ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
cmd_init() {
|
|
||||||
cd "$REPO"
|
|
||||||
git rev-parse --show-toplevel >/dev/null 2>&1 || _die "not a git repo"
|
|
||||||
|
|
||||||
mkdir -p "$WORKTREE_ROOT"
|
|
||||||
[ -d openspec ] && _ok "openspec already initialized" || {
|
|
||||||
openspec init --tools opencode --force
|
|
||||||
_ok "openspec initialized"
|
|
||||||
}
|
|
||||||
|
|
||||||
grep -qxF '.worktrees/' .gitignore 2>/dev/null || {
|
|
||||||
echo '.worktrees/' >> .gitignore
|
|
||||||
_ok "added .worktrees/ to .gitignore"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Repo ready. Next: dev-orchestrator spec <feature-name>"
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_spec() {
|
|
||||||
local name="$1"
|
|
||||||
_guard
|
|
||||||
|
|
||||||
# ── validate name (kebab-case) ──
|
|
||||||
[[ "$name" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]] || _die "name must be kebab-case: add-oauth, fix-login"
|
|
||||||
|
|
||||||
# ── create OpenSpec change ──
|
|
||||||
echo "── Creating OpenSpec change: $name ──"
|
|
||||||
openspec new change "$name" --json 2>/dev/null || {
|
|
||||||
_die "openspec new change failed. Already exists? Run: dev-orchestrator status"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── create worktree ──
|
|
||||||
local branch; branch="$(_wt_branch "$name")"
|
|
||||||
local wt; wt="$(_wt_path "$name")"
|
|
||||||
echo "── Creating worktree: $wt [$branch] ──"
|
|
||||||
git worktree add -b "$branch" "$wt" HEAD
|
|
||||||
|
|
||||||
# ── install deps in worktree (fully isolated, no symlinks) ──
|
|
||||||
if [ -f "$wt/package.json" ]; then
|
|
||||||
echo "── Installing dependencies ──"
|
|
||||||
(cd "$wt" && npm install --silent 2>&1 | tail -1) || true
|
|
||||||
fi
|
|
||||||
if [ -f "$wt/pyproject.toml" ] || [ -f "$wt/setup.py" ]; then
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
(cd "$wt" && [ -d .venv ] || python3 -m venv .venv && . .venv/bin/activate && pip install -e . -q 2>&1 | tail -1) || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
_ok "Spec '$name' scaffolded at openspec/changes/$name/"
|
|
||||||
echo " Worktree: $wt"
|
|
||||||
echo ""
|
|
||||||
echo " ── Next: explore in OpenCode ──"
|
|
||||||
echo " opencode"
|
|
||||||
echo " Then use OpenSpec native commands:"
|
|
||||||
echo " /opsx:explore — discuss the feature, refine requirements"
|
|
||||||
echo " /opsx:continue — fill spec artifacts one by one"
|
|
||||||
echo ""
|
|
||||||
echo " After specs are filled → dev-orchestrator build $name"
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_fill() {
|
|
||||||
local name="$1"
|
|
||||||
_guard
|
|
||||||
|
|
||||||
local wt; wt="$(_wt_path "$name")"
|
|
||||||
[ -d "openspec/changes/$name" ] || _die "spec not found: openspec/changes/$name. Run: dev-orchestrator spec $name"
|
|
||||||
|
|
||||||
echo "── AI filling spec from conversation context ──"
|
|
||||||
opencode run \
|
|
||||||
"You are filling an OpenSpec change for feature: $name.
|
|
||||||
|
|
||||||
STEPS:
|
|
||||||
1. Read the codebase structure first (scan src/, lib/, or equivalent).
|
|
||||||
2. Read openspec/changes/$name/ — note the template files created.
|
|
||||||
3. Read openspec/specs/ for existing specs to avoid conflicts.
|
|
||||||
4. For each template file in the change directory, fill it with:
|
|
||||||
- Clear requirements and acceptance criteria
|
|
||||||
- Files that need to change
|
|
||||||
- Dependencies on other modules
|
|
||||||
- Test plan
|
|
||||||
5. Run: openspec validate $name --strict
|
|
||||||
6. Fix any validation errors, re-run until clean.
|
|
||||||
7. Output a summary: spec files created, key decisions, estimated scope." \
|
|
||||||
--workdir "$REPO"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
_ok "Spec '$name' filled at openspec/changes/$name/"
|
|
||||||
echo " Review the spec, then: dev-orchestrator build $name"
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_build() {
|
|
||||||
local name="$1"
|
|
||||||
_guard
|
|
||||||
|
|
||||||
local wt; wt="$(_wt_path "$name")"
|
|
||||||
[ -d "$wt" ] || _die "worktree not found: $wt. Run: dev-orchestrator spec $name"
|
|
||||||
|
|
||||||
local branch; branch="$(_wt_branch "$name")"
|
|
||||||
local max_retries=3
|
|
||||||
|
|
||||||
for attempt in $(seq 1 $max_retries); do
|
|
||||||
echo ""
|
|
||||||
echo "══════════════════════════════════════════════════"
|
|
||||||
echo " $name — Dev Phase (attempt $attempt/$max_retries)"
|
|
||||||
echo "══════════════════════════════════════════════════"
|
|
||||||
|
|
||||||
opencode run \
|
|
||||||
"IMPLEMENT the spec at openspec/changes/$name/.
|
|
||||||
|
|
||||||
RULES:
|
|
||||||
- Read openspec instructions --change $name first
|
|
||||||
- Implement ALL requirements from the spec
|
|
||||||
- Write tests for every new code path
|
|
||||||
- Run the test suite and ensure it passes
|
|
||||||
- If tests fail, fix them before considering work done
|
|
||||||
- Commit with message: '$name: implement feature (attempt $attempt)'
|
|
||||||
- Do NOT modify openspec/ files — only source code and tests" \
|
|
||||||
--workdir "$wt" || {
|
|
||||||
echo "⚠ Dev phase had errors, proceeding to QA anyway..."
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── QA phase ──
|
|
||||||
echo ""
|
|
||||||
echo "──────────────────────────────────────────────────"
|
|
||||||
echo " $name — QA Review"
|
|
||||||
echo "──────────────────────────────────────────────────"
|
|
||||||
|
|
||||||
local qa_file="/tmp/dev-orchestrator-qa-$$.txt"
|
|
||||||
opencode run \
|
|
||||||
"QA REVIEW for $name.
|
|
||||||
|
|
||||||
CHECKLIST (answer each with PASS or FAIL):
|
|
||||||
1. SPEC COMPLIANCE — Does the code implement everything in openspec/changes/$name/?
|
|
||||||
2. TESTS — Do all tests pass? Run them now.
|
|
||||||
3. REGRESSIONS — Does any existing test break? Check git diff vs main.
|
|
||||||
4. EDGE CASES — Are errors handled? Null/empty inputs? Timeouts?
|
|
||||||
5. CODE QUALITY — Clear naming? No debug leftovers? No commented-out code?
|
|
||||||
|
|
||||||
OUTPUT FORMAT (exactly these 2 lines, nothing else):
|
|
||||||
VERDICT: PASS|FAIL
|
|
||||||
REASON: <one-sentence summary>" \
|
|
||||||
--workdir "$wt" > "$qa_file" 2>&1
|
|
||||||
|
|
||||||
local verdict
|
|
||||||
verdict=$(grep '^VERDICT:' "$qa_file" | head -1 | awk -F': ' '{print $2}')
|
|
||||||
local reason
|
|
||||||
reason=$(grep '^REASON:' "$qa_file" | head -1 | cut -d' ' -f2-)
|
|
||||||
|
|
||||||
if [ "$verdict" = "PASS" ]; then
|
|
||||||
echo ""
|
|
||||||
_ok "QA PASSED — $reason"
|
|
||||||
|
|
||||||
# ── archive + merge ──
|
|
||||||
cd "$REPO"
|
|
||||||
echo "── Archiving spec ──"
|
|
||||||
openspec archive "$name" --yes 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "── Merging to main ──"
|
|
||||||
local current_branch=$(git branch --show-current)
|
|
||||||
git merge "$branch" -m "dev-flow: merge $name" 2>/dev/null || {
|
|
||||||
echo "⚠ Merge conflict. Resolve manually in $wt then run:"
|
|
||||||
echo " cd $wt && git checkout main && git merge $branch"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "── Cleaning up worktree ──"
|
|
||||||
git worktree remove "$wt" 2>/dev/null || true
|
|
||||||
git branch -d "$branch" 2>/dev/null || true
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
_ok "$name — BUILD COMPLETE ✓"
|
|
||||||
rm "$qa_file"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✗ QA FAILED — ${reason:-see $qa_file}"
|
|
||||||
rm "$qa_file"
|
|
||||||
|
|
||||||
if [ "$attempt" -eq "$max_retries" ]; then
|
|
||||||
_die "$name FAILED after $max_retries attempts. Worktree kept at $wt for manual fix."
|
|
||||||
fi
|
|
||||||
echo "↻ Sending back to dev with QA feedback..."
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_status() {
|
|
||||||
_guard 2>/dev/null || true
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
printf "%-4s %-30s %-10s %-10s %-15s\n" "#" "FEATURE" "COMMITS" "SPEC" "WORKTREE"
|
|
||||||
printf "%-4s %-30s %-10s %-10s %-15s\n" "───" "──────────────────────────────" "──────────" "──────────" "──────────────"
|
|
||||||
|
|
||||||
local n=0
|
|
||||||
for wt in "$WORKTREE_ROOT"/*/; do
|
|
||||||
[ -d "$wt" ] || continue
|
|
||||||
local name=$(basename "$wt")
|
|
||||||
local branch; branch="$(_wt_branch "$name")"
|
|
||||||
local commits="?"
|
|
||||||
[ -d "$wt/.git" ] && commits=$(cd "$wt" && git rev-list --count "$branch" -- 2>/dev/null || echo "?")
|
|
||||||
commits="${commits:-0}"
|
|
||||||
|
|
||||||
# spec status
|
|
||||||
local spec="?"
|
|
||||||
[ -d "$REPO/openspec/changes/$name" ] && {
|
|
||||||
spec=$(cd "$REPO" && openspec status --change "$name" --json 2>/dev/null | \
|
|
||||||
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('artifacts_completed','?'),'/',d.get('artifacts_total','?'),sep='')" 2>/dev/null || echo "active")
|
|
||||||
}
|
|
||||||
|
|
||||||
n=$((n+1))
|
|
||||||
printf "%-4s %-30s %-10s %-10s %-15s\n" \
|
|
||||||
"$n." "$name" "$commits" "${spec:-active}" "$wt"
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$n" -eq 0 ]; then
|
|
||||||
echo " (no active features)"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_clean() {
|
|
||||||
local name="$1"
|
|
||||||
cd "$REPO"
|
|
||||||
local wt; wt="$(_wt_path "$name")"
|
|
||||||
local branch; branch="$(_wt_branch "$name")"
|
|
||||||
|
|
||||||
git worktree remove "$wt" 2>/dev/null || true
|
|
||||||
git branch -D "$branch" 2>/dev/null || true
|
|
||||||
if [ ! -d "$wt" ]; then
|
|
||||||
_ok "cleaned $name"
|
|
||||||
else
|
|
||||||
_die "could not remove $wt"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── dispatch ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
case "${1:-}" in
|
|
||||||
init)
|
|
||||||
cmd_init
|
|
||||||
;;
|
|
||||||
spec)
|
|
||||||
[ $# -ge 2 ] || _die "usage: dev-orchestrator spec <feature-name>"
|
|
||||||
cmd_spec "$2"
|
|
||||||
;;
|
|
||||||
fill)
|
|
||||||
[ $# -ge 2 ] || _die "usage: dev-orchestrator fill <feature-name>"
|
|
||||||
cmd_fill "$2"
|
|
||||||
;;
|
|
||||||
build)
|
|
||||||
[ $# -ge 2 ] || _die "usage: dev-orchestrator build <feature-name>"
|
|
||||||
cmd_build "$2"
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
cmd_status
|
|
||||||
;;
|
|
||||||
clean)
|
|
||||||
[ $# -ge 2 ] || _die "usage: dev-orchestrator clean <feature-name>"
|
|
||||||
cmd_clean "$2"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "dev-orchestrator — OpenSpec + OpenCode dev→QA loop"
|
|
||||||
echo ""
|
|
||||||
echo "commands:"
|
|
||||||
echo " init Set up repo (run once per project)"
|
|
||||||
echo " spec <feature-name> Scaffold OpenSpec + worktree (no fill)"
|
|
||||||
echo " fill <feature-name> AI fills spec after exploration"
|
|
||||||
echo " build <feature-name> Dev→QA loop in isolated worktree"
|
|
||||||
echo " status Kanban view of features in flight"
|
|
||||||
echo " clean <feature-name> Remove worktree + branch"
|
|
||||||
echo ""
|
|
||||||
echo "flow: init → spec → [explore in OpenCode] → fill → build → [QA loop]"
|
|
||||||
echo ""
|
|
||||||
echo "worktrees live at: ../.worktrees/<feature>/"
|
|
||||||
echo "branches named: dev-flow/<feature>"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
Reference in New Issue
Block a user