DOT'T CHANGE, DELETE OR CREATE ANY FILES OR FOLDERS OUTSIDE OF THE PROJECT REPO!
DON'T RUN COMMANDS THAT USE PATHS FROM OUTSIDE THE PROJECT REPO!
RALPH PHILOSOPHY: When the agent fails, don't just fix the code — fix the prompt. Erect signs so the next loop doesn't have to rediscover fire.
"Ralph is deterministically bad in an undeterministic world."
- Ralph Wiggum Methodology
- Project Context
- Quality Gates
- Requirements
- Signs (Lessons Learned)
- Completion Checklist
- Iteration > Perfection: Don't aim for perfect on first try. Let the loop refine the work.
- Failures Are Data: Deterministically bad means failures are predictable and informative.
- Operator Skill Matters: Success depends on writing good prompts, not just having a good model.
- Persistence Wins: Keep trying until success. The loop handles retry logic.
Each requirement (REQ-XXX) is designed to be executed as a single Ralph loop:
/ralph-wiggum:ralph-loop "<requirement prompt>" --completion-promise "COMPLETE" --max-iterations 30Requirements are structured with:
- Clear completion criteria (exact conditions for success)
- Self-correction patterns (what to do when tests fail)
- Verification commands (automated checks)
- Signs (guardrails from past failures)
1. Read the requirement
2. Write failing test (TDD)
3. Implement minimal code to pass
4. Run verification commands
5. If any fail → fix and retry from step 3
6. If all pass → output <promise>COMPLETE</promise>
Deferred Apps creates lightweight wrapper scripts (~5KB) that appear as installed applications in your desktop launcher, but only download the actual package on first launch. Perfect for apps you rarely use but want available.
- Core library (
package.nix) withmkDeferredApp,mkDeferredPackages - NixOS and Home Manager modules
- Two modes: pname (nix shell) and package (nix-store -r)
- Icon resolution with Papirus theme (icons COPIED, not referenced)
- Collision detection for terminal commands
- GC root support
- Nested package support (python313Packages.numpy)
- Comprehensive test suite
- @arianvp: Use
nix-store --realise→ Implemented as package mode - Closure optimization → Icons copied (~5KB) not referenced (~1GB)
- CLI tools support → REQ-001
- Status tracking → REQ-002
- Preload for offline use → REQ-004
# ALL of these must pass before marking any task complete
nix flake check # All tests pass
statix check . # No linter warnings
deadnix --fail -L . # No dead code
nix fmt -- --check # Proper formattingIF any verification fails:
1. Fix the issue
2. Re-run ALL verifications
3. Repeat until ALL pass
4. Only then proceed to next step
DO NOT skip verification steps.
DO NOT mark tasks complete with failing checks.
Priority: HIGH
Estimated Iterations: 15-25
Dependencies: None
Users want deferred apps for CLI tools (ripgrep, fd, jq), not just GUI applications. Currently, all deferred apps create .desktop files and resolve icons, which is unnecessary overhead for terminal-only tools.
- As a user, I want to defer CLI tools without creating desktop files
- As a user, I want faster builds when deferring CLI-only tools (no icon lookup)
- As a user, I want a convenient module option for listing CLI apps
-
mkDeferredApp { package = pkgs.ripgrep; cliOnly = true; }creates wrapper WITHOUT .desktop file -
mkDeferredApp { pname = "fd"; cliOnly = true; }works the same way - CLI-only wrappers have terminal command symlink in
$out/bin/ - CLI-only wrappers do NOT have
$out/share/applications/directory - CLI-only wrappers do NOT have
$out/share/icons/directory - Build time is measurably faster (no icon theme scanning)
- Module option
programs.deferredApps.cliPackagesaccepts list of packages - Module option
programs.deferredApps.cliAppsaccepts list of pnames - All existing tests continue to pass
- New tests cover all CLI-only scenarios
Create tests/cli-mode.nix with these test cases:
# Test 1: CLI-only has bin symlink
cli-mode-has-bin = mkBuildCheck "cli-mode-has-bin"
(deferredAppsLib.mkDeferredApp { package = pkgs.ripgrep; cliOnly = true; })
''
test -L "$out/bin/rg" || { echo "FAIL: missing bin symlink"; exit 1; }
echo "PASS: bin symlink exists"
'';
# Test 2: CLI-only has NO desktop file
cli-mode-no-desktop = mkBuildCheck "cli-mode-no-desktop"
(deferredAppsLib.mkDeferredApp { package = pkgs.ripgrep; cliOnly = true; })
''
if [ -d "$out/share/applications" ]; then
echo "FAIL: cli-only should not have share/applications"
exit 1
fi
echo "PASS: no desktop directory"
'';
# Test 3: CLI-only has NO icons
cli-mode-no-icons = mkBuildCheck "cli-mode-no-icons"
(deferredAppsLib.mkDeferredApp { package = pkgs.ripgrep; cliOnly = true; })
''
if [ -d "$out/share/icons" ]; then
echo "FAIL: cli-only should not have share/icons"
exit 1
fi
echo "PASS: no icons directory"
'';
# Test 4: CLI-only with pname mode
cli-mode-pname = mkBuildCheck "cli-mode-pname"
(deferredAppsLib.mkDeferredApp { pname = "fd"; cliOnly = true; })
''
test -L "$out/bin/fd" || { echo "FAIL: missing bin symlink"; exit 1; }
test ! -d "$out/share/applications" || { echo "FAIL: has desktop dir"; exit 1; }
echo "PASS: pname cli-only works"
'';
# Test 5: Module cliPackages option
cli-mode-module-packages = let
eval = evalNixosModule {
programs.deferredApps = {
enable = true;
cliPackages = [ pkgs.ripgrep pkgs.fd ];
};
};
in mkCheck "cli-mode-module-packages" (
builtins.length eval.config.environment.systemPackages >= 2
);
# Test 6: Default cliOnly is false (backward compatibility)
cli-mode-default-false = mkBuildCheck "cli-mode-default-false"
(deferredAppsLib.mkDeferredApp { package = pkgs.hello; })
''
test -d "$out/share/applications" || { echo "FAIL: default should have desktop"; exit 1; }
echo "PASS: default has desktop file"
'';-
Add
cliOnlyparameter tomkDeferredAppinpackage.nix:- Default:
false - When
true: skip desktop file creation, skip icon resolution
- Default:
-
Modify derivation build script to conditionally create directories:
if [ -z "$cliOnly" ]; then # Create desktop file and icons mkdir -p "$out/share/applications" "$out/share/icons" # ... existing icon/desktop logic fi
-
Add module options in
modules/shared.nix:cliPackages = lib.mkOption { type = lib.types.listOf lib.types.package; default = []; description = "CLI tools to defer (no desktop files created)"; }; cliApps = lib.mkOption { type = lib.types.listOf lib.types.str; default = []; description = "CLI tool names to defer (no desktop files created)"; };
-
Update
buildDeferredPackagesto handle CLI apps withcliOnly = true -
Add tests to
tests/cli-mode.nix -
Update README with CLI-only documentation
# Run after implementation
nix flake check
nix build .#checks.x86_64-linux.cli-mode-has-bin
nix build .#checks.x86_64-linux.cli-mode-no-desktop
nix build .#checks.x86_64-linux.cli-mode-no-icons
nix build .#checks.x86_64-linux.cli-mode-pname
nix build .#checks.x86_64-linux.cli-mode-module-packages
nix build .#checks.x86_64-linux.cli-mode-default-false
statix check .
deadnix --fail -L .
nix fmt -- --check- DO NOT break backward compatibility -
cliOnlydefaults tofalse - DO NOT skip icon resolution for non-cliOnly apps
- DO NOT forget to update collision detection for CLI apps
- DO test both package mode AND pname mode with cliOnly
- DO update the module's
buildDeferredPackagesto pass cliOnly
When ALL of the following are true:
- All 6 test cases pass
- All existing tests still pass
nix flake checkpassesstatix check .has no warningsdeadnix --fail -L .finds no dead codenix fmt -- --checkpasses
Output: <promise>COMPLETE</promise>
Priority: HIGH
Estimated Iterations: 20-30
Dependencies: None
Users want to know which deferred apps have been downloaded vs which are still pending. This is especially useful for:
- Checking disk usage of deferred apps
- Knowing which apps will work offline
- Debugging download issues
- As a user, I want to see a list of all my deferred apps
- As a user, I want to know which apps are downloaded vs pending
- As a user, I want to see how much disk space downloaded apps use
- As a user, I want JSON output for scripting
-
deferred-apps statusshows all deferred apps with download status - Status shows: app name, status (downloaded/pending), size (if downloaded)
-
deferred-apps status --jsonoutputs machine-readable JSON -
deferred-apps status <app>shows status of specific app - Tool is automatically available when module is enabled
- Works for both package mode and pname mode apps
- Exit code 0 on success, non-zero on error
Create tests in tests/status-cli.nix:
# Test 1: Status CLI exists
status-cli-exists = let
statusPkg = pkgs.callPackage ../cli/status.nix {
inherit (deferredAppsLib) getPnameFromPackage;
};
in mkBuildCheck "status-cli-exists" statusPkg ''
test -x "$out/bin/deferred-apps" || { echo "FAIL: missing binary"; exit 1; }
"$out/bin/deferred-apps" --help | grep -q "status" || { echo "FAIL: no status cmd"; exit 1; }
echo "PASS: status CLI exists"
'';
# Test 2: Status CLI has subcommands
status-cli-subcommands = let
statusPkg = pkgs.callPackage ../cli/status.nix {};
in mkBuildCheck "status-cli-subcommands" statusPkg ''
"$out/bin/deferred-apps" status --help | grep -q "json" || { echo "FAIL: no --json"; exit 1; }
echo "PASS: has expected options"
'';
# Test 3: Module includes status CLI
status-cli-in-module = let
eval = evalNixosModule {
programs.deferredApps = {
enable = true;
packages = [ pkgs.hello ];
};
};
hasStatusCli = builtins.any (p:
(p.pname or p.name or "") == "deferred-apps-cli"
) eval.config.environment.systemPackages;
in mkCheck "status-cli-in-module" hasStatusCli;
# Test 4: JSON output is valid
status-cli-json-valid = let
statusPkg = pkgs.callPackage ../cli/status.nix {};
in mkBuildCheck "status-cli-json-valid" statusPkg ''
# Create mock registry for testing
mkdir -p $TMPDIR/deferred-apps
echo '{"apps":{"hello":{"outPath":"/nix/store/xxx-hello"}}}' > $TMPDIR/deferred-apps/registry.json
output=$("$out/bin/deferred-apps" status --json --registry "$TMPDIR/deferred-apps/registry.json" 2>/dev/null || true)
echo "$output" | ${pkgs.jq}/bin/jq . > /dev/null || { echo "FAIL: invalid JSON"; exit 1; }
echo "PASS: valid JSON output"
'';-
Create CLI package at
cli/status.nix:{ pkgs, lib, ... }: pkgs.writeShellApplication { name = "deferred-apps"; runtimeInputs = with pkgs; [ jq coreutils nix ]; text = builtins.readFile ./deferred-apps.sh; }
-
Create CLI script at
cli/deferred-apps.sh:- Subcommand:
status [--json] [app-name] - Read registry from
~/.local/share/deferred-apps/registry.json - Check store paths existence with
nix path-info - Calculate sizes with
nix path-info --size
- Subcommand:
-
Create registry mechanism:
- Module generates registry JSON at build time
- Contains: app name → store path mapping
- Registry placed in
$out/share/deferred-apps/registry.json
-
Update module to include CLI and registry:
environment.systemPackages = [ statusCli ]; environment.etc."deferred-apps/registry.json".source = registryJson;
-
Add tests to flake checks
Text format (default):
Deferred Apps Status
====================
App Status Size
----------------------------------------
spotify downloaded 245 MB
discord pending -
obs-studio downloaded 189 MB
----------------------------------------
Total downloaded: 2/3 (434 MB)
JSON format (--json):
{
"apps": [
{"name": "spotify", "status": "downloaded", "size_bytes": 257000000, "path": "/nix/store/..."},
{"name": "discord", "status": "pending", "size_bytes": null, "path": "/nix/store/..."},
{"name": "obs-studio", "status": "downloaded", "size_bytes": 198000000, "path": "/nix/store/..."}
],
"summary": {
"total": 3,
"downloaded": 2,
"pending": 1,
"total_size_bytes": 455000000
}
}nix flake check
nix build .#checks.x86_64-linux.status-cli-exists
nix build .#checks.x86_64-linux.status-cli-subcommands
nix build .#checks.x86_64-linux.status-cli-in-module
nix build .#checks.x86_64-linux.status-cli-json-valid
statix check .
deadnix --fail -L .
nix fmt -- --check- DO NOT hardcode paths - use XDG_DATA_HOME with fallback
- DO NOT assume apps are always in nix store - check existence
- DO handle missing registry gracefully (empty list, not error)
- DO use
nix path-infonot manual store inspection - DO make JSON output parseable by jq
When ALL of the following are true:
- CLI tool builds and has
statussubcommand - Text and JSON output formats work
- Module automatically includes the CLI
- All tests pass
- All quality gates pass
Output: <promise>COMPLETE</promise>
Priority: MEDIUM
Estimated Iterations: 15-25
Dependencies: REQ-002 (shares CLI infrastructure)
When gcRoot = true, downloaded packages are protected from garbage collection. Users need a way to:
- See what's protected
- Clean up when disk space is needed
- Understand disk usage impact
- As a user, I want to list all GC-protected deferred apps
- As a user, I want to see total size of GC roots
- As a user, I want to remove all GC roots to reclaim space
- As a user, I want to remove GC root for specific app
-
deferred-apps gc listshows all GC roots -
deferred-apps gc statusshows total size -
deferred-apps gc cleanremoves all GC roots -
deferred-apps gc clean <app>removes specific app's GC root -
deferred-apps gc clean --dry-runshows what would be removed - Confirmation prompt before destructive operations (unless
--yes) - Works with existing GC root directory structure
# Test 1: GC list command exists
gc-cli-list = let
cliPkg = pkgs.callPackage ../cli/status.nix {};
in mkBuildCheck "gc-cli-list" cliPkg ''
"$out/bin/deferred-apps" gc --help | grep -q "list" || { echo "FAIL: no list"; exit 1; }
echo "PASS: gc list exists"
'';
# Test 2: GC clean has dry-run
gc-cli-dry-run = let
cliPkg = pkgs.callPackage ../cli/status.nix {};
in mkBuildCheck "gc-cli-dry-run" cliPkg ''
"$out/bin/deferred-apps" gc clean --help | grep -q "dry-run" || { echo "FAIL: no dry-run"; exit 1; }
echo "PASS: has dry-run option"
'';
# Test 3: GC clean requires confirmation
gc-cli-confirmation = let
cliPkg = pkgs.callPackage ../cli/status.nix {};
in mkBuildCheck "gc-cli-confirmation" cliPkg ''
# Should fail without --yes when no tty
if echo "" | "$out/bin/deferred-apps" gc clean 2>/dev/null; then
echo "FAIL: should require confirmation"
exit 1
fi
echo "PASS: requires confirmation"
'';-
Add
gcsubcommand to CLI:gc list- enumerate~/.local/share/deferred-apps/gcroots/gc status- sum sizes of all rootsgc clean [--yes] [--dry-run] [app]- remove roots
-
GC root directory structure:
~/.local/share/deferred-apps/gcroots/ ├── spotify -> /nix/store/xxx-spotify ├── discord -> /nix/store/xxx-discord └── obs-studio -> /nix/store/xxx-obs-studio -
Safety features:
- Confirmation prompt (bypass with
--yes) - Dry-run mode
- Clear error messages
- Confirmation prompt (bypass with
nix flake check
nix build .#checks.x86_64-linux.gc-cli-list
nix build .#checks.x86_64-linux.gc-cli-dry-run
nix build .#checks.x86_64-linux.gc-cli-confirmation
statix check .
deadnix --fail -L .
nix fmt -- --check- DO NOT delete without confirmation (unless --yes)
- DO NOT fail if gcroots directory doesn't exist (just show empty)
- DO use
readlink -fto resolve symlinks for size calculation - DO handle broken symlinks gracefully
When ALL tests pass and quality gates pass:
Output: <promise>COMPLETE</promise>
Priority: MEDIUM
Estimated Iterations: 20-30
Dependencies: REQ-002 (shares CLI infrastructure)
Users want to pre-download apps before going offline (travel, conferences, etc.). This is a common request from the community.
- As a user, I want to download all deferred apps at once
- As a user, I want to download specific apps by name
- As a user, I want to see progress during downloads
- As a user, I want downloads to continue if one fails
-
deferred-apps preloaddownloads all pending apps -
deferred-apps preload spotify discorddownloads specific apps - Shows progress: "Downloading 1/5: spotify..."
- Continues on failure, reports failures at end
-
--parallel Noption for concurrent downloads (default: 1) - Exit code indicates success (0) or partial failure (1)
# Test 1: Preload command exists
preload-cli-exists = let
cliPkg = pkgs.callPackage ../cli/status.nix {};
in mkBuildCheck "preload-cli-exists" cliPkg ''
"$out/bin/deferred-apps" preload --help || { echo "FAIL: no preload"; exit 1; }
echo "PASS: preload exists"
'';
# Test 2: Preload has parallel option
preload-cli-parallel = let
cliPkg = pkgs.callPackage ../cli/status.nix {};
in mkBuildCheck "preload-cli-parallel" cliPkg ''
"$out/bin/deferred-apps" preload --help | grep -q "parallel" || { echo "FAIL: no parallel"; exit 1; }
echo "PASS: has parallel option"
'';-
Add
preloadsubcommand:- Read registry for app → store path mapping
- For each app: run
nix-store -r <path> - Track success/failure
- Report summary
-
Progress display:
Preloading deferred apps... [1/5] spotify: downloading... done (245 MB) [2/5] discord: downloading... done (312 MB) [3/5] obs-studio: downloading... FAILED (network error) [4/5] blender: downloading... done (1.2 GB) [5/5] gimp: downloading... done (89 MB) Summary: 4/5 succeeded, 1 failed Failed: obs-studio -
Parallel downloads (optional enhancement):
- Use
xargs -Por GNU parallel - Default to sequential for safety
- Use
nix flake check
nix build .#checks.x86_64-linux.preload-cli-exists
nix build .#checks.x86_64-linux.preload-cli-parallel
statix check .
deadnix --fail -L .
nix fmt -- --check- DO NOT exit early on first failure - complete all downloads
- DO report which apps failed at the end
- DO use appropriate exit codes (0 = all success, 1 = partial failure)
- DO NOT require root/sudo for downloads
When ALL tests pass and quality gates pass:
Output: <promise>COMPLETE</promise>
Priority: MEDIUM
Estimated Iterations: 15-20
Dependencies: None
Different users want different feedback during downloads:
- GUI users: desktop notifications
- Terminal users: progress bars
- Scripts: no output (just exit codes)
- As a terminal user, I want to see download progress in my terminal
- As a script author, I want silent operation
- As a GUI user, I want notifications (current default)
-
progressMode = "notification"(default) - uses notify-send -
progressMode = "terminal"- prints progress to stderr -
progressMode = "none"- silent operation - Per-app override via extraApps
- Module option
programs.deferredApps.progressMode
# Test 1: Terminal progress mode
progress-mode-terminal = mkBuildCheck "progress-mode-terminal"
(deferredAppsLib.mkDeferredApp { package = pkgs.hello; progressMode = "terminal"; })
''
grep -q 'PROGRESS_MODE="terminal"' "$out/libexec/deferred-hello" || \
{ echo "FAIL: terminal mode not set"; exit 1; }
echo "PASS: terminal mode set"
'';
# Test 2: None progress mode
progress-mode-none = mkBuildCheck "progress-mode-none"
(deferredAppsLib.mkDeferredApp { package = pkgs.hello; progressMode = "none"; })
''
grep -q 'PROGRESS_MODE="none"' "$out/libexec/deferred-hello" || \
{ echo "FAIL: none mode not set"; exit 1; }
# Should not have notify-send call when mode is none
if grep -q 'notify-send' "$out/libexec/deferred-hello" | grep -v 'PROGRESS_MODE'; then
: # notify-send might still be referenced but guarded
fi
echo "PASS: none mode set"
'';
# Test 3: Default is notification
progress-mode-default = mkBuildCheck "progress-mode-default"
(deferredAppsLib.mkDeferredApp { package = pkgs.hello; })
''
grep -q 'PROGRESS_MODE="notification"' "$out/libexec/deferred-hello" || \
{ echo "FAIL: default should be notification"; exit 1; }
echo "PASS: default is notification"
'';
# Test 4: Module option works
progress-mode-module = let
eval = evalNixosModule {
programs.deferredApps = {
enable = true;
packages = [ pkgs.hello ];
progressMode = "terminal";
};
};
in mkCheck "progress-mode-module" (eval.config.programs.deferredApps.progressMode == "terminal");-
Add
progressModeparameter tomkDeferredApp:- Type: enum ["notification" "terminal" "none"]
- Default: "notification"
-
Update wrapper script to respect mode:
PROGRESS_MODE="@progressMode@" show_progress() { case "$PROGRESS_MODE" in notification) notify-send --app-name="Deferred Apps" "$1" "$2" & ;; terminal) echo "[deferred-apps] $1: $2" >&2 ;; none) : # silent ;; esac }
-
Add module option:
progressMode = lib.mkOption { type = lib.types.enum [ "notification" "terminal" "none" ]; default = "notification"; description = "How to show download progress"; };
-
Add per-app override in extraApps submodule
nix flake check
nix build .#checks.x86_64-linux.progress-mode-terminal
nix build .#checks.x86_64-linux.progress-mode-none
nix build .#checks.x86_64-linux.progress-mode-default
nix build .#checks.x86_64-linux.progress-mode-module
statix check .
deadnix --fail -L .
nix fmt -- --check- DO NOT break existing notification behavior (it's the default)
- DO use stderr for terminal output (not stdout)
- DO keep notify-send optional (graceful degradation)
When ALL tests pass and quality gates pass:
Output: <promise>COMPLETE</promise>
Priority: MEDIUM
Estimated Iterations: 25-35
Dependencies: None
Users often want to defer entire categories of apps with shared settings:
- "Multimedia apps I rarely use" (all with gcRoot=true)
- "Dev tools" (all from unstable channel)
- "Games" (all unfree)
- As a user, I want to group apps by category
- As a user, I want to apply shared settings to a group
- As a user, I want to enable/disable entire groups
-
programs.deferredApps.groups.<name>.packagesaccepts list of packages -
programs.deferredApps.groups.<name>.appsaccepts list of pnames - Groups can set:
gcRoot,flakeRef,allowUnfree,progressMode -
programs.deferredApps.groups.<name>.enabledefaults to true - Setting
enable = falsedisables entire group - Group settings are overridable per-app via extraApps
- Collision detection works across groups
# Test 1: Basic group with packages
groups-basic-packages = let
eval = evalNixosModule {
programs.deferredApps = {
enable = true;
groups.multimedia = {
packages = [ pkgs.vlc pkgs.mpv ];
gcRoot = true;
};
};
};
in mkCheck "groups-basic-packages" (
builtins.length eval.config.environment.systemPackages >= 2
);
# Test 2: Group with apps (pnames)
groups-basic-apps = let
eval = evalNixosModule {
programs.deferredApps = {
enable = true;
groups.tools = {
apps = [ "hello" "cowsay" ];
};
};
};
in mkCheck "groups-basic-apps" (
builtins.length eval.config.environment.systemPackages >= 2
);
# Test 3: Disabled group produces no packages
groups-disabled = let
eval = evalNixosModule {
programs.deferredApps = {
enable = true;
groups.unused = {
enable = false;
packages = [ pkgs.hello pkgs.cowsay ];
};
};
};
# Should only have libnotify, not hello/cowsay wrappers
in mkCheck "groups-disabled" (
builtins.length eval.config.environment.systemPackages <= 2
);
# Test 4: Group settings inherited
groups-settings-inherited = let
eval = evalNixosModule {
programs.deferredApps = {
enable = true;
groups.protected = {
packages = [ pkgs.hello ];
gcRoot = true;
progressMode = "terminal";
};
};
};
in mkCheck "groups-settings-inherited" (
eval.config.programs.deferredApps.groups.protected.gcRoot == true
);
# Test 5: Collision detection across groups
groups-collision = let
result = builtins.tryEval (evalNixosModule {
programs.deferredApps = {
enable = true;
groups.a = { packages = [ pkgs.hello ]; };
groups.b = { packages = [ pkgs.hello ]; }; # Collision!
};
});
in mkCheck "groups-collision" (!result.success);-
Define group submodule in
shared.nix:groupModule = lib.types.submodule { options = { enable = lib.mkEnableOption "this app group" // { default = true; }; packages = lib.mkOption { type = lib.types.listOf lib.types.package; default = []; }; apps = lib.mkOption { type = lib.types.listOf lib.types.str; default = []; }; gcRoot = lib.mkOption { type = lib.types.bool; default = false; }; flakeRef = lib.mkOption { type = lib.types.str; default = "nixpkgs"; }; allowUnfree = lib.mkOption { type = lib.types.bool; default = false; }; progressMode = lib.mkOption { type = lib.types.enum [ "notification" "terminal" "none" ]; default = "notification"; }; }; };
-
Add groups option:
groups = lib.mkOption { type = lib.types.attrsOf groupModule; default = {}; description = "Named groups of deferred apps with shared settings"; };
-
Update
buildDeferredPackagesto iterate over enabled groups -
Extend collision detection to include all group packages
nix flake check
nix build .#checks.x86_64-linux.groups-basic-packages
nix build .#checks.x86_64-linux.groups-basic-apps
nix build .#checks.x86_64-linux.groups-disabled
nix build .#checks.x86_64-linux.groups-settings-inherited
nix build .#checks.x86_64-linux.groups-collision
statix check .
deadnix --fail -L .
nix fmt -- --check- DO NOT forget to check collisions across ALL groups
- DO NOT ignore disabled groups in collision check (they should be excluded)
- DO propagate group settings to individual apps
- DO allow extraApps to override group settings
When ALL tests pass and quality gates pass:
Output: <promise>COMPLETE</promise>
Priority: LOW Estimated Iterations: 10-15 Dependencies: None Status: ✅ COMPLETE
Users want to customize notification text for branding or localization.
-
notificationTitleoption overrides "Starting {app}..." -
notificationBodyoption overrides "Downloading application..." -
showNotification = falsedisables notifications entirely - Per-app override via extraApps
# Test 1: Custom title
notification-custom-title = mkBuildCheck "notification-custom-title"
(deferredAppsLib.mkDeferredApp {
package = pkgs.hello;
notificationTitle = "Loading {app}";
})
''
grep -q 'Loading {app}' "$out/libexec/deferred-hello" || \
{ echo "FAIL: custom title not used"; exit 1; }
echo "PASS: custom title works"
'';
# Test 2: Show notification false
notification-disabled = mkBuildCheck "notification-disabled"
(deferredAppsLib.mkDeferredApp {
package = pkgs.hello;
showNotification = false;
})
''
grep -q 'SHOW_NOTIFICATION="0"' "$out/libexec/deferred-hello" || \
{ echo "FAIL: notification should be disabled"; exit 1; }
echo "PASS: notification disabled"
'';- Add options:
notificationTitle,notificationBody,showNotification - Update wrapper script template
- Add to module options and extraApps submodule
nix flake check- DO NOT break existing notification behavior (it's the default)
- DO use stderr for notifications (not stdout)
- DO keep notify-send optional (graceful degradation)
When ALL tests pass and quality gates pass:
Output: <promise>COMPLETE</promise>
Priority: HIGH
Estimated Iterations: 30-40
Dependencies: REQ-001 through REQ-004 (optional, but recommended)
Some behaviors can only be verified in a full NixOS system:
- Desktop files linked into system profile
- Icons available to desktop environment
- Wrapper scripts executable with correct paths
- Module integration with environment.systemPackages
- VM test boots NixOS with deferred-apps module
- Verifies desktop files in
/run/current-system/sw/share/applications/ - Verifies icons in
/run/current-system/sw/share/icons/ - Verifies wrapper in
/run/current-system/sw/libexec/ - Verifies terminal command in
/run/current-system/sw/bin/ - Test runs as part of
nix flake check
Create tests/vm.nix:
{ pkgs, lib, self, system }:
let
nixosTest = pkgs.nixosTest or pkgs.testers.nixosTest;
in {
vm-integration = nixosTest {
name = "deferred-apps-integration";
nodes.machine = { pkgs, ... }: {
imports = [ self.nixosModules.default ];
programs.deferredApps = {
enable = true;
apps = [ "hello" ];
packages = [ pkgs.cowsay ];
};
# Minimal VM config
virtualisation.memorySize = 1024;
};
testScript = ''
machine.wait_for_unit("multi-user.target")
# Test 1: Desktop file exists for pname mode
machine.succeed("test -f /run/current-system/sw/share/applications/hello.desktop")
# Test 2: Desktop file exists for package mode
machine.succeed("test -f /run/current-system/sw/share/applications/cowsay.desktop")
# Test 3: Wrapper scripts exist
machine.succeed("test -x /run/current-system/sw/libexec/deferred-hello")
machine.succeed("test -x /run/current-system/sw/libexec/deferred-cowsay")
# Test 4: Terminal commands exist
machine.succeed("test -L /run/current-system/sw/bin/hello")
machine.succeed("test -L /run/current-system/sw/bin/cowsay")
# Test 5: Icons exist (Papirus theme)
machine.succeed("test -d /run/current-system/sw/share/icons")
# Test 6: Desktop file content is correct
machine.succeed("grep -q 'Exec=.*/libexec/deferred-hello' /run/current-system/sw/share/applications/hello.desktop")
# Test 7: Wrapper is executable and runs (will fail to download, but should start)
result = machine.execute("timeout 5 /run/current-system/sw/libexec/deferred-hello --version 2>&1 || true")
# Should either succeed or fail with network/nix error, not permission error
assert "Permission denied" not in result[1], f"Permission error: {result[1]}"
'';
};
}- Create
tests/vm.nixwith nixosTest definition - Import in
tests/default.nix:vmTests = import ./vm.nix { inherit pkgs lib self system; };
- Add to flake checks (conditional on x86_64-linux due to VM support)
- Ensure test runs in CI (may need KVM or QEMU)
# Build and run VM test
nix build .#checks.x86_64-linux.vm-integration
# Or run interactively for debugging
nix build .#checks.x86_64-linux.vm-integration.driverInteractive
./result/bin/nixos-test-driver- DO NOT require network access in VM tests (use local store)
- DO set reasonable timeouts (VMs are slow)
- DO use minimal VM config (less memory = faster CI)
- DO test both pname AND package mode
When VM test passes and all quality gates pass:
Output: <promise>COMPLETE</promise>
Priority: CRITICAL
Estimated Iterations: 40-60
Dependencies: REQ-001 through REQ-008
The ultimate goal is to submit deferred-apps to nixpkgs for community benefit and long-term maintenance.
- All functions have documentation comments
- All module options have type, default, description, example
- No
builtins.currentSystemusage - No hardcoded paths
- License is nixpkgs-compatible (GPL-3.0-or-later is fine)
- README documents all features
- ARCHITECTURE.md explains design decisions
- CONTRIBUTING.md exists
- Security model documented
- Benchmarks documented
- All tests pass on x86_64-linux and aarch64-linux
Code Quality:
-
statix check .- zero warnings -
deadnix --fail -L .- zero findings -
nix fmt -- --check- all formatted - All public functions documented
- All options have examples
Documentation:
- README.md complete with all features
- ARCHITECTURE.md explains:
- Why wrappers are in
/libexec - Why icons are copied not referenced
- Package mode vs pname mode trade-offs
- Security model
- Why wrappers are in
- CONTRIBUTING.md with:
- Development setup
- Test running instructions
- Code style guidelines
Security:
- No shell injection vulnerabilities
- No arbitrary code execution paths
-
--impureusage documented and justified - GC root security implications documented
Testing:
- Unit tests for all library functions
- Integration tests for modules
- VM tests for full system
- Tests pass on both x86_64 and aarch64
# Full verification suite
nix flake check
statix check .
deadnix --fail -L .
nix fmt -- --check
# Multi-arch (if available)
nix build .#checks.x86_64-linux --all
nix build .#checks.aarch64-linux --all # Requires aarch64 builderWhen ALL checklist items are complete and ALL verification passes:
Output: <promise>COMPLETE</promise>
RALPH PHILOSOPHY: When Ralph falls off the slide, add a sign saying "SLIDE DOWN, DON'T JUMP"
This section accumulates lessons learned during development. Update after each failure.
SIGN: Wrappers go in /libexec, symlinks in /bin
WHY: Prevents PATH pollution, follows FHS conventions
SIGN: Icons are COPIED not referenced
WHY: Referencing pulls ~1GB Papirus theme into closure
SIGN: Use unsafeDiscardStringContext for store paths
WHY: Prevents .drv files from polluting system closure
SIGN: Package mode uses nix-store -r, then --realise fallback
WHY: Cache fetch is fast, local build is fallback
SIGN: Pname mode uses nix shell with flake ref
WHY: Dynamic resolution at runtime, no build-time dependency
SIGN: Use mkCheck for pure assertions
WHY: No build required, fast evaluation
SIGN: Use mkBuildCheck for derivation inspection
WHY: Need to examine build output
SIGN: Use mkClosureCheck for size verification
WHY: Closure size is critical metric
SIGN: Test names: prefix with category (lib-, pkg-, module-)
WHY: Easy to identify test scope in output
SIGN: Don't use builtins.currentSystem
WHY: Breaks flake purity, use system from args
SIGN: Don't reference icon theme package in closure
WHY: ~1GB dependency for ~5KB icon
SIGN: Always check terminal command collisions
WHY: Multiple packages can have same mainProgram
SIGN: Default cliOnly to false
WHY: Breaking change if default is true
SIGN: Use lib.mkDefault for environment variables
WHY: Allow user overrides
SIGN: Include libnotify in systemPackages
WHY: Notifications need notify-send
SIGN: pathsToLink includes share/applications and share/icons
WHY: Desktop files and icons must be in system profile
- REQ-001: CLI-Only Mode
- REQ-002: Status CLI Tool
- REQ-003: GC Management CLI
- REQ-004: Preload CLI Tool
- REQ-005: Progress Mode
- REQ-006: Application Groups
- REQ-007: Notification Customization
- REQ-008: NixOS VM Integration Tests
- REQ-009: nixpkgs Submission Readiness
# Run this when ALL requirements are complete
nix flake check && \
statix check . && \
deadnix --fail -L . && \
nix fmt -- --check && \
echo "ALL QUALITY GATES PASSED"When all checkboxes are checked and final verification passes: PROJECT COMPLETE - Ready for nixpkgs submission!
When working on any requirement:
- READ THE REQUIREMENT COMPLETELY before starting
- WRITE TESTS FIRST (TDD) - verify they fail before implementing
- IMPLEMENT MINIMAL CODE to pass tests
- RUN ALL VERIFICATIONS after every change
- FIX FAILURES IMMEDIATELY - do not accumulate debt
- UPDATE SIGNS if you discover new pitfalls
- MARK CHECKBOXES as complete only when verified
# After EVERY change:
nix flake check && statix check . && deadnix --fail -L . && nix fmt -- --checkWhen all verifications pass for a requirement:
<promise>COMPLETE</promise>
If stuck after 10+ iterations:
- Document what's blocking in Signs section
- List attempted approaches
- Suggest alternative solutions
- Output:
<promise>BLOCKED</promise>
Remember: Ralph is deterministically bad in an undeterministic world. Embrace the loop.