Design — jacked 0.51.0: install/upgrade change-summary + /recover goal-loop recovery

Status:approved-by-user
Date:2026-06-17
Branch:feat/install-summary-and-recover-goalloop
Release:0.51.0

Goal

Two user-facing improvements, shipped together as 0.51.0 on one branch:

Part A — install/upgrade change-summary

A1. The manifest

The root problem: jacked keeps no record of what it previously installed — no manifest, and the prior version isn't durably stored. A small manifest fixes this and powers everything in Part A.

New module jacked/install_manifest.py (pure, no Click/rich, fully unit-testable). Manifest file at ~/.claude/jacked-manifest.json:

{
  "version": "0.51.0",                // the jacked version that wrote this manifest
  "written_at": "2026-06-17T16:40:00Z",
  "artifacts": {
    "skills":    {"recover": "sha256:…", "whats-next": "sha256:…", …},
    "commands":  {"dc": "sha256:…", …},
    "agents":    {"readme-maintainer": "sha256:…", …},
    "lenses":    {…},
    "templates": {"plan-template.html": "sha256:…", …}
  }
}

Public API:

FunctionResponsibility
CATEGORIESOrdered list describing each artifact category: key (skills/commands/agents/lenses/templates), source glob + name-extraction, destination dir + dest-path-for-name. Single source of truth shared by install, prune, and diff.
hash_source(data_root) -> dictWalk the package source for every category; return {category: {name: "sha256:…"}} of the current shipped artifacts (hash of file bytes).
load(path=DEFAULT) -> dict | NoneRead + JSON-parse the manifest; None if missing/corrupt (corrupt → treat as first install, logged).
diff(prior, current_hashes) -> ManifestDiffPer category: added (name in current, not in prior), changed (in both, hash differs), removed (in prior, not in current), unchanged. prior=None → everything is added (first manifested install).
write(path, version, current_hashes)Atomic write (temp + rename) of the new manifest.

Diffing by content hash of the source (current-source vs prior-manifest) is correct in both copy installs (PyPI) and editable/symlink dev installs — it compares "what we ship now" against "what we shipped last time," never the lossy dest-vs-source comparison.

A2. install: diff, prune, summary, --json

The install command (jacked/cli.py) is refactored to drive the manifest:

  1. Load prior manifest → capture prior_version (or None).
  2. Hash current sourcediff(prior, current).
  3. Install added + changed artifacts via the existing link/copy path (unchanged behavior, including editable symlinks).
  4. Prune removed: for each removed name (present in the prior manifest, so jacked-owned), delete its destination (skills: rmtree the skill dir; others: unlink the file). The manifest is the ownership proof — the user's own skills/commands are never in it, so never touched.
  5. Write the new manifest (current version + current hashes).
  6. Write ~/.claude/jacked-last-install.json (see A3) — the structured summary record.
  7. Render the concise terminal summary (below), replacing the old banner.

Terminal summary (rich, slate/blue-agnostic — uses the existing [green]/[yellow]/ vocabulary). Upgrade example:

Jacked upgraded   0.50.0 → 0.51.0

  Skills      + recover            new
  Commands    ~ whats-next         updated
  Agents      − legacy-helper      removed
              34 unchanged

→ Restart Claude Code to load changes.

First install (no prior manifest): header reads Jacked installed — 0.51.0 and every artifact is listed as new (per the chosen "concise always" output style). If nothing changed on a re-run: Jacked 0.51.0 — already up to date (N artifacts unchanged).

--json flag on install: emit the structured summary to stdout (and suppress the human render), shape identical to jacked-last-install.json below. Lets any caller capture the diff programmatically.

A3. Dashboard surface

Rather than scrape install's stdout from two different update paths, install self-records: it always writes ~/.claude/jacked-last-install.json:

{
  "at": "2026-06-17T16:40:01Z",
  "from_version": "0.50.0",          // null on first install
  "to_version": "0.51.0",
  "changes": {
    "skills":   {"added": ["recover"], "changed": [], "removed": []},
    "commands": {"added": [], "changed": ["whats-next"], "removed": []},
    "agents":   {"added": [], "changed": [], "removed": ["legacy-helper"]},
    "lenses":   {"added": [], "changed": [], "removed": []},
    "templates":{"added": [], "changed": [], "removed": []}
  },
  "unchanged_count": 34
}

Both update paths get the summary for free because the record is produced by install itself, which both paths run.

Part B — /recover goal-loop + near-empty pick

Two focused additions to jacked/recover.py + a skill update. No change to the CLI's two-phase shape.

B1. Near-empty recommendation

Candidates are already ranked newest-first by last-timestamped line. Add recommend_index(candidates, min_msgs=MIN_SUBSTANCE_MSGS): return the index of the first candidate whose msg_count >= min_msgs (default MIN_SUBSTANCE_MSGS = 4); if none qualify, fall back to index 0 (absolute newest). The CLI's chosen becomes candidates[recommend_index(...)]; the full candidate list (including any skipped near-empty newest) is still returned as alternates. This matches "newest unless the very newest is nearly empty."

B2. Goal/loop kickoff recovery (only if it looks active)

A /goal or /loop run cannot be auto-resumed — it only continues when the user pastes the command into a live Claude Code session. So when the recommended session was driving one and it looks like it was still active at the crash, /recover surfaces the exact kickoff verbatim for copy-paste.

B3. Skill update

jacked/data/skills/recover/SKILL.md gains: (a) a note that the recommended pick skips a near-empty newest session (and the user can still choose it as an alternate); (b) a step instructing the agent that if the digest contains a "Manual restart required" block, present that /goal or /loop command to the user to copy-paste, and explicitly state it cannot be auto-run.

File changes

FileChange
jacked/install_manifest.pyNew. Manifest schema, CATEGORIES, hash_source, load, diff, write, ManifestDiff. Pure/stdlib.
jacked/cli.pyinstall refactor: manifest-driven diff + prune + manifest write + jacked-last-install.json write + concise summary render + --json flag; remove the verbose banner (keep required-plugin blocker line); move full recommendations behind jacked doctor/--recommend. Make uninstall manifest-aware (clean pruned artifacts too).
jacked/api/routes/system.pyNew route GET /api/install/summary reading jacked-last-install.json.
jacked/data/web/js/components/*.js, app.js, header.jsNew one-shot "What changed" panel + wiring on post-upgrade reload and app load (slate/blue, localStorage seen-marker).
jacked/data/web/update.htmlRender added/changed/removed on succeeded via /api/install/summary.
jacked/data/web/css/style.cssStyles for the change panel if not covered by existing .stat-card/.badge.
jacked/recover.pyrecommend_index (near-empty skip); goal/loop extraction + active-gate; Digest.resumable_commands + render block.
jacked/data/skills/recover/SKILL.mdNote the near-empty skip + the "Manual restart required" copy-paste step.
tests/test_install_manifest.py, tests/test_recover.py, tests/…api…New/extended. Unit tests (run via uv run python -m pytest).
jacked/__init__.py, README.mdVersion → 0.51.0; document the change-summary + goal-loop recovery.

Testing

Scope boundaries (YAGNI)

Failure modes

ScenarioBehavior
No prior manifest (first run after this ships)Treat as first install: everything listed as new, write baseline manifest. No pruning (nothing recorded to prune).
Corrupt manifest JSONload returns None (logged); treated as first install. Never crash the install.
Prune target already gone / not writableSkip with a logged warning; never abort install.
Editable/dev install (symlinks)Hash-of-source diff still works (compares source-now vs manifest); version arrow still correct.
jacked-last-install.json unreadable in UI/api/install/summary returns {summary: null}; UI shows nothing (no error toast).
Session has a /goal//loop early but lots of later unrelated workActive-gate fails → not surfaced (treated as finished).
Recommended session is near-empty and so are all othersrecommend_index falls back to absolute newest; all candidates still listed.

Open questions


Generated with the jacked HTML artifact template.