@stylusnexus/work-plan 2026.6.22 → 2026.6.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -314,6 +314,8 @@ Install it from either registry:
314
314
 
315
315
  The extension **shells out to the `work-plan` CLI**, so install the CLI too (npm or any method above). If `work-plan` isn't on your editor's `PATH` — common when VS Code is launched from the Dock/Finder rather than a terminal — set **`workPlan.cliPath`** in Settings to an absolute launcher path (e.g. `/path/to/work-plan-toolkit/bin/work-plan`, or the npm global bin), then reload the window. Extensions auto-update from the registry.
316
316
 
317
+ > **Windows + WSL (and Remote-SSH / dev containers): install the CLI where the extension *runs*, not where you clicked.** When you open a folder in WSL — e.g. `code .` from a WSL shell, or **Reopen in WSL** (the window shows **`WSL: <distro>`** at the bottom-left) — VS Code runs the extension *inside WSL*, so it looks for `work-plan`, `gh`, `python3`, and `yq` on the **WSL** `PATH`. A CLI installed on **Windows-native** is invisible to it, and you'll see a **"work-plan CLI not found"** banner (or, on older builds, a misleading "Not signed in to GitHub" one) even though `gh` is signed in. Fix: open the **WSL** terminal and run `npm install -g @stylusnexus/work-plan` (plus `gh`, `python3`, `yq`) there, then **reload the window**. The same rule applies to Remote-SSH and dev containers — the CLI must live in the remote, not on your local machine. (`gh auth` is likewise per-environment: `gh auth login` inside WSL authenticates the WSL `gh`, which is what the extension reads.)
318
+
317
319
  Useful settings:
318
320
 
319
321
  | Setting | Default | What it does |
@@ -541,7 +543,7 @@ See `docs/usage-examples.md` for end-to-end scenarios (morning brief, mid-work h
541
543
  | `canonicalize <track>` | Add a canonical issue table to a track file (so `refresh-md` knows where to update). The table carries a `Milestone` column and is ordered active-milestone-first — issues in the track's `milestone_alignment` milestone, then other milestones grouped (blank divider row between groups), then no-milestone last — so "what's next" sits above "someday" (#101). It's one table (not per-milestone sub-tables) so `refresh-md` re-derives it cleanly. |
542
544
  | `plan-status [--repo=<key>] [--json] [--stamp [--draft]] [--llm [--apply]] [--archive \| --issues] [--draft]` | Reach a verdict on every plan/spec doc in a repo by correlating its declared file-manifest against git + the filesystem: ✅ shipped / 🟡 partial / 💀 dead / 👻 manifest-less / 🧳 foreign. Read-only by default. `--stamp` writes an idempotent status header into each doc (`--draft` previews); `--llm` runs a two-step AI verdict on prose/ambiguous docs; `--archive` moves dead plans to `archive/abandoned/` and `--issues` opens issues for partial plans (both gated, both honor `--draft`); `--json` for machine output. Add `--archive-shipped` to batch-archive every clean shipped doc (lie-gap excluded unless `--include-lie-gap`); `--include-archived` also lists archived docs (tagged `archived`/`archive_kind`) in `--json`. |
543
545
  | `plan-confirm --repo=<key> --verdict=shipped\|partial\|dead [--clear] [--confirm=<token>] -- <rel>` | Affirm a **human** verdict on ONE plan/spec doc by writing `verdict_override` into its YAML **frontmatter only** (never the body, checkboxes, manifest, or status banner). `plan-status` then pins that verdict over the mechanical one and silences the "shipped but boxes unchecked" lie-gap. Use when a genuinely-shipped plan is flagged only because its phase checkboxes were never ticked. `<rel>` is the repo-relative doc path. Public-repo gated (`--confirm=<token>`); `--clear` removes the override. |
544
- | `plan-archive --repo=<key> [--draft] [--yes] [--json] -- <rel>` | Archive ONE plan/spec doc whose effective verdict is **shipped**: history-preserving `git mv` into `archive/shipped/`. Refuses non-shipped docs; skips (never overwrites) a name collision. `--draft` previews; `--yes` skips the prompt for non-interactive callers (the VS Code viewer); `--json` emits a single `{action,rel,outcome,dest}` object. |
546
+ | `plan-archive --repo=<key> [--draft] [--yes] [--json] -- <rel>` | Archive ONE plan/spec doc whose effective verdict is **shipped**: history-preserving `git mv` into `archive/shipped/`. Refuses non-shipped docs; skips (never overwrites) a name collision. `--draft` previews; `--yes` skips the prompt for non-interactive callers (the VS Code viewer); `--json` emits a single `{action,rel,outcome,dest}` object. The `outcome` distinguishes a **tracked** doc (`archived` — a staged history-preserving `git mv`; commit & push to share it) from a **gitignored/untracked** doc (`archived_local` — a plain filesystem move, not git-tracked), so archive never silently fails on an untracked path (#399). |
545
547
  | `plan-ack --repo=<key> [--clear] [--confirm=<token>] -- <rel>` | Persist a **durable acknowledgment** into ONE plan/spec doc's YAML **frontmatter only** (`acknowledged: true`) — a "stop flagging this" that's committed with the repo and shared with teammates, unlike the VS Code viewer's per-machine `workspaceState` ack. `plan-status` reads it back (emits `acknowledged`) and demotes the doc. `<rel>` is the repo-relative doc path. Public-repo gated (`--confirm=<token>`); `--clear` removes it. |
546
548
  | `plan-baseline --repo=<key> [--clear] [--confirm=<token>] -- <rel>` | Stamp the **current computed verdict** into ONE plan/spec doc's YAML **frontmatter only** (`verdict_baseline`) as a drift tripwire. `plan-status` then flags **drift** (emits `verdict_drift`) when the live verdict later diverges from the baseline — catching a once-shipped plan that silently **regressed** (its declared files were deleted/moved), the third "started, then drifted off" signal beyond stalled + lie-gap. The value is computed authoritatively (not taken from the caller); a human `verdict_override` suppresses drift. Public-repo gated; `--clear` removes it. |
547
549
  | `close-issue --repo=<key\|slug> [--reason=completed\|not_planned] [--comment=<text>] -- <number>` | ⚠️ A GitHub-mutating command — closes a GitHub issue via `gh issue close`. PRs merged to `dev` don't auto-close issues (GitHub auto-closes only from the default branch), so done-but-OPEN issues pile up; this closes one explicitly. `--reason` maps to GitHub's completed/not-planned; `--comment` posts a closing note. The VS Code viewer gates this behind a mandatory "Close on GitHub? — cannot be undone" modal. |
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2026.06.22+f62c4cc
1
+ 2026.06.24+b047035
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stylusnexus/work-plan",
3
- "version": "2026.6.22",
3
+ "version": "2026.6.24",
4
4
  "description": "Track-aware daily work planning over GitHub issues. Shared tracks (git-synced .work-plan/ in each repo), AI clustering (group/auto-triage), VS Code viewer, Claude Code + Codex plugins. Pure Python stdlib.",
5
5
  "bin": {
6
6
  "work-plan": "bin/work-plan"
@@ -30,7 +30,7 @@ Track-aware daily planner. Each "track" is a YAML-frontmattered markdown file th
30
30
  | `/work-plan duplicates [--min-similarity=0.7]` | Find likely-duplicate issues by title similarity (stdlib difflib). |
31
31
  | `/work-plan plan-status [--repo=<key>] [--stamp [--draft]] [--type=plan\|spec]` | **Doc/plan liveness.** "Which of my plan/spec docs actually shipped, half-shipped, or died?" Correlates each plan's declared file-manifest (Create/Modify/Test paths) against git + filesystem — not the unreliable checkboxes. Reports ✅ shipped / 🟡 partial / 💀 dead / 👻 manifest-less. Read-only by default; `--stamp` writes an idempotent status header into each doc (`--draft` previews, writes nothing). Natural-language triggers: "what's done vs unfinished in `<repo>`", "stamp the plan statuses", "which plans are stale/dead". |
32
32
  | `/work-plan plan-confirm --repo=<key> --verdict=shipped\|partial\|dead [--clear] -- <rel>` | **Affirm a human verdict** on one plan doc by writing `verdict_override` into its **frontmatter only** (never body/checkboxes/manifest). `plan-status` then pins that verdict and silences the "shipped but boxes unchecked" lie-gap. Use when a genuinely-shipped plan is flagged red only because nobody ticked its phase checkboxes — confirm it instead of hand-ticking boxes. Public-repo gated (prints `needs_confirm` + token; re-run with `--confirm=<token>`). Natural-language triggers: "that plan really did ship, stop flagging it", "mark `<plan>` as shipped/dead". |
33
- | `/work-plan plan-archive --repo=<key> [--draft] -- <rel>` | **Archive a shipped plan.** Moves a doc whose effective verdict is shipped into `archive/shipped/` (`git mv`). Refuses non-shipped; skips collisions. Natural-language triggers: "archive this finished plan", "file away the shipped plans". Bulk: `plan-status --archive-shipped`. |
33
+ | `/work-plan plan-archive --repo=<key> [--draft] -- <rel>` | **Archive a shipped plan.** Moves a doc whose effective verdict is shipped into `archive/shipped/` — a staged `git mv` for a tracked doc (commit & push to share), or a plain filesystem move for a gitignored/untracked one (#399). Refuses non-shipped; skips collisions. Natural-language triggers: "archive this finished plan", "file away the shipped plans". Bulk: `plan-status --archive-shipped`. |
34
34
  | `/work-plan plan-ack --repo=<key> [--clear] -- <rel>` | **Durably acknowledge** one plan doc by writing `acknowledged: true` into its **frontmatter only** — a "stop flagging this" that's committed + shared (vs the viewer's per-machine ack). `plan-status` reads it back and demotes the doc. Public-repo gated. Natural-language triggers: "stop flagging this plan for everyone", "acknowledge `<plan>` for good". |
35
35
  | `/work-plan plan-baseline --repo=<key> [--clear] -- <rel>` | **Stamp a drift baseline** on one plan doc by writing its current computed verdict to `verdict_baseline` in **frontmatter only**. `plan-status` then flags **drift** when the live verdict diverges — catching a once-shipped plan that silently regressed (files deleted/moved). Distinct from `plan-confirm` (human pin); an override suppresses drift. Public-repo gated. Natural-language triggers: "watch this plan for regressions", "alert me if `<plan>` stops being shipped". |
36
36
 
@@ -76,11 +76,16 @@ def run(args: list) -> int:
76
76
 
77
77
  outcome = archive_lib.move_to_archive(rel, repo_root, "shipped")
78
78
  if outcome is None:
79
- print(f"ERROR: git mv failed for {rel}", file=sys.stderr)
79
+ print(f"ERROR: archive move failed for {rel}", file=sys.stderr)
80
80
  return 1
81
81
  if outcome == "skipped_collision":
82
82
  _emit(as_json, rel, "skipped_collision", dest,
83
83
  f"destination already exists: {dest} — skipped")
84
84
  return 0
85
- _emit(as_json, rel, "archived", dest, f"✓ archived {rel} -> {dest}")
85
+ if outcome == "archived_local":
86
+ _emit(as_json, rel, "archived_local", dest,
87
+ f"✓ archived {rel} -> {dest} (moved on disk; not git-tracked)")
88
+ return 0
89
+ _emit(as_json, rel, "archived", dest,
90
+ f"✓ archived {rel} -> {dest} (staged rename — commit & push to share)")
86
91
  return 0
@@ -356,11 +356,14 @@ def _archive_dead(docs, rows, repo_root, draft: bool) -> int:
356
356
  moved = 0
357
357
  for r in dead:
358
358
  dest = reconcile_actions.archive_dest(r["rel"])
359
- if git_state.git_mv(r["rel"], dest, repo_root):
359
+ outcome = archive_lib.move_to_archive(r["rel"], repo_root, "abandoned")
360
+ if outcome in ("archived", "archived_local"):
360
361
  moved += 1
361
- print(f" {r['rel']}")
362
+ suffix = " (moved on disk; not git-tracked)" if outcome == "archived_local" else ""
363
+ print(f" ✓ {r['rel']}{suffix}")
362
364
  else:
363
- print(f" {r['rel']} (git mv failed)")
365
+ reason = "already exists at destination" if outcome == "skipped_collision" else "move failed"
366
+ print(f" ✗ {r['rel']} ({reason})")
364
367
  print(f"Archived {moved}/{len(dead)}.")
365
368
  return 0
366
369
 
@@ -379,8 +382,9 @@ def _archive_shipped(rows, repo_root, draft, as_json, include_lie_gap, yes=False
379
382
  for r in targets:
380
383
  outcome = archive_lib.move_to_archive(r["rel"], repo_root, "shipped")
381
384
  dest = reconcile_actions.archive_dest(r["rel"], "shipped")
382
- if outcome == "archived":
383
- archived.append({"rel": r["rel"], "dest": dest})
385
+ if outcome in ("archived", "archived_local"):
386
+ archived.append({"rel": r["rel"], "dest": dest,
387
+ "outcome": outcome})
384
388
  elif outcome == "skipped_collision":
385
389
  skipped.append({"rel": r["rel"], "reason": "collision"})
386
390
  print(json.dumps({
@@ -413,10 +417,13 @@ def _archive_shipped(rows, repo_root, draft, as_json, include_lie_gap, yes=False
413
417
  outcome = archive_lib.move_to_archive(r["rel"], repo_root, "shipped")
414
418
  if outcome == "archived":
415
419
  archived += 1
416
- print(f" ✓ {r['rel']}")
420
+ print(f" ✓ {r['rel']} (staged rename — commit & push to share)")
421
+ elif outcome == "archived_local":
422
+ archived += 1
423
+ print(f" ✓ {r['rel']} (moved on disk; not git-tracked)")
417
424
  else:
418
425
  skipped += 1
419
- print(f" ✗ {r['rel']} ({outcome or 'git mv failed'})")
426
+ print(f" ✗ {r['rel']} ({outcome or 'move failed'})")
420
427
  print(f"Archived {archived}, skipped {skipped}.")
421
428
  return 0
422
429
 
@@ -1,6 +1,9 @@
1
- """The archive move primitive: collision-checked, history-preserving `git mv`
2
- of a doc into archive/<kind>/. Eligibility (is-this-shipped) is the caller's
3
- job this only moves files."""
1
+ """The archive move primitive: collision-checked move of a doc into
2
+ archive/<kind>/. For tracked files the move is a history-preserving `git mv`
3
+ (staged rename); for untracked/gitignored files it falls back to a plain
4
+ filesystem move. Eligibility (is-this-shipped) is the caller's job — this
5
+ only moves files."""
6
+ import shutil
4
7
  from pathlib import Path
5
8
 
6
9
  from lib import git_state
@@ -8,14 +11,26 @@ from lib import reconcile_actions
8
11
 
9
12
 
10
13
  def move_to_archive(rel: str, repo_root, kind: str):
11
- """git-mv `rel` into archive/<kind>/. Returns:
12
- "archived" on a clean move,
13
- "skipped_collision" if the destination already exists (never overwrite),
14
- None if git_mv failed for any other reason (hard error).
14
+ """Move `rel` into archive/<kind>/. Returns:
15
+ "archived" tracked file: history-preserving `git mv` (staged rename),
16
+ "archived_local" untracked/gitignored file: plain filesystem move,
17
+ "skipped_collision" destination already exists (never overwrite),
18
+ None hard failure (git mv failed on a tracked file, or OSError
19
+ on an untracked file).
15
20
  """
16
21
  dest = reconcile_actions.archive_dest(rel, kind)
17
22
  if (Path(repo_root) / dest).exists():
18
23
  return "skipped_collision"
19
- if git_state.git_mv(rel, dest, repo_root):
20
- return "archived"
21
- return None
24
+ if git_state.is_tracked(rel, Path(repo_root)):
25
+ if git_state.git_mv(rel, dest, repo_root):
26
+ return "archived"
27
+ return None
28
+ # Untracked / gitignored — plain filesystem move.
29
+ src_path = Path(repo_root) / rel
30
+ dst_path = Path(repo_root) / dest
31
+ try:
32
+ dst_path.parent.mkdir(parents=True, exist_ok=True)
33
+ shutil.move(str(src_path), str(dst_path))
34
+ return "archived_local"
35
+ except OSError:
36
+ return None
@@ -358,6 +358,18 @@ def path_committed_since(rel_path: str, since: date, repo_path: Path) -> bool:
358
358
  return proc is not None and proc.returncode == 0 and bool(proc.stdout.strip())
359
359
 
360
360
 
361
+ def is_tracked(rel_path: str, repo_path: Path) -> bool:
362
+ """True iff `rel_path` is tracked by git (git ls-files --error-unmatch).
363
+
364
+ False for untracked/gitignored paths AND when repo_path isn't a git repo
365
+ or doesn't exist. Never raises.
366
+ """
367
+ if not repo_path or not Path(repo_path).exists():
368
+ return False
369
+ proc = _git(repo_path, "ls-files", "--error-unmatch", rel_path)
370
+ return proc is not None and proc.returncode == 0
371
+
372
+
361
373
  def git_mv(src_rel: str, dst_rel: str, repo_path: Path) -> bool:
362
374
  """git-mv `src_rel` -> `dst_rel` (both repo-relative), creating the dest
363
375
  directory first. Returns True on success. History-preserving."""
@@ -1,12 +1,15 @@
1
1
  """Pure archive helpers: destination path + shipped-row selection (offline)."""
2
2
  import sys
3
+ import tempfile
3
4
  import unittest
4
5
  from pathlib import Path
6
+ from unittest import mock
5
7
 
6
8
  SKILL_ROOT = Path(__file__).resolve().parents[1]
7
9
  sys.path.insert(0, str(SKILL_ROOT))
8
10
 
9
11
  from lib import reconcile_actions as ra
12
+ from lib import archive as archive_lib
10
13
 
11
14
 
12
15
  class ArchiveDestTest(unittest.TestCase):
@@ -46,11 +49,6 @@ class ShippedRowsTest(unittest.TestCase):
46
49
  self.assertEqual(rels, ["a.md", "b.md"])
47
50
 
48
51
 
49
- import tempfile
50
- from unittest import mock
51
- from lib import archive as archive_lib
52
-
53
-
54
52
  class MoveToArchiveTest(unittest.TestCase):
55
53
  def _repo(self, d):
56
54
  root = Path(d)
@@ -58,33 +56,77 @@ class MoveToArchiveTest(unittest.TestCase):
58
56
  (root / "docs/plans/x.md").write_text("# x")
59
57
  return root
60
58
 
61
- def test_archived_calls_git_mv_to_shipped_dir(self):
59
+ # ------------------------------------------------------------------
60
+ # tracked path: branches through git_mv
61
+ # ------------------------------------------------------------------
62
+
63
+ def test_tracked_archived_calls_git_mv(self):
64
+ """Tracked file: is_tracked=True -> git_mv called -> 'archived'."""
62
65
  with tempfile.TemporaryDirectory() as d:
63
66
  root = self._repo(d)
64
- with mock.patch("lib.archive.git_state.git_mv", return_value=True) as mv:
67
+ with mock.patch("lib.archive.git_state.is_tracked", return_value=True), \
68
+ mock.patch("lib.archive.git_state.git_mv", return_value=True) as mv:
65
69
  outcome = archive_lib.move_to_archive("docs/plans/x.md", root, "shipped")
66
70
  self.assertEqual(outcome, "archived")
67
71
  mv.assert_called_once_with(
68
72
  "docs/plans/x.md", "docs/plans/archive/shipped/x.md", root)
69
73
 
70
- def test_collision_skips_without_calling_git_mv(self):
74
+ def test_tracked_git_mv_failure_returns_none(self):
75
+ """Tracked file where git_mv fails -> None (hard error)."""
71
76
  with tempfile.TemporaryDirectory() as d:
72
77
  root = self._repo(d)
73
- dest = root / "docs/plans/archive/shipped"
74
- dest.mkdir(parents=True)
75
- (dest / "x.md").write_text("# already here")
76
- with mock.patch("lib.archive.git_state.git_mv") as mv:
78
+ with mock.patch("lib.archive.git_state.is_tracked", return_value=True), \
79
+ mock.patch("lib.archive.git_state.git_mv", return_value=False):
77
80
  outcome = archive_lib.move_to_archive("docs/plans/x.md", root, "shipped")
78
- self.assertEqual(outcome, "skipped_collision")
81
+ self.assertIsNone(outcome)
82
+
83
+ # ------------------------------------------------------------------
84
+ # untracked/gitignored path: plain filesystem move
85
+ # ------------------------------------------------------------------
86
+
87
+ def test_untracked_archived_local_via_filesystem(self):
88
+ """Untracked file: is_tracked=False -> shutil.move called -> 'archived_local'.
89
+
90
+ The file must actually move on disk (src gone, dest exists)."""
91
+ with tempfile.TemporaryDirectory() as d:
92
+ root = self._repo(d)
93
+ with mock.patch("lib.archive.git_state.is_tracked", return_value=False), \
94
+ mock.patch("lib.archive.git_state.git_mv") as mv:
95
+ outcome = archive_lib.move_to_archive("docs/plans/x.md", root, "shipped")
96
+ self.assertEqual(outcome, "archived_local")
79
97
  mv.assert_not_called()
98
+ # Source is gone; dest exists.
99
+ self.assertFalse((root / "docs/plans/x.md").exists())
100
+ self.assertTrue((root / "docs/plans/archive/shipped/x.md").exists())
80
101
 
81
- def test_git_mv_failure_returns_none(self):
102
+ def test_untracked_oserror_returns_none(self):
103
+ """Untracked file where shutil.move raises OSError -> None."""
82
104
  with tempfile.TemporaryDirectory() as d:
83
105
  root = self._repo(d)
84
- with mock.patch("lib.archive.git_state.git_mv", return_value=False):
106
+ with mock.patch("lib.archive.git_state.is_tracked", return_value=False), \
107
+ mock.patch("lib.archive.shutil.move",
108
+ side_effect=OSError("permission denied")):
85
109
  outcome = archive_lib.move_to_archive("docs/plans/x.md", root, "shipped")
86
110
  self.assertIsNone(outcome)
87
111
 
112
+ # ------------------------------------------------------------------
113
+ # collision: checked before tracking branch, git_mv never called
114
+ # ------------------------------------------------------------------
115
+
116
+ def test_collision_skips_without_any_move(self):
117
+ """Destination already exists -> 'skipped_collision', no move attempted."""
118
+ with tempfile.TemporaryDirectory() as d:
119
+ root = self._repo(d)
120
+ dest = root / "docs/plans/archive/shipped"
121
+ dest.mkdir(parents=True)
122
+ (dest / "x.md").write_text("# already here")
123
+ with mock.patch("lib.archive.git_state.is_tracked") as is_tracked, \
124
+ mock.patch("lib.archive.git_state.git_mv") as mv:
125
+ outcome = archive_lib.move_to_archive("docs/plans/x.md", root, "shipped")
126
+ self.assertEqual(outcome, "skipped_collision")
127
+ is_tracked.assert_not_called()
128
+ mv.assert_not_called()
129
+
88
130
 
89
131
  if __name__ == "__main__":
90
132
  unittest.main()
@@ -43,7 +43,9 @@ class PlanArchiveTest(unittest.TestCase):
43
43
  def test_archives_shipped_with_yes_json(self):
44
44
  with tempfile.TemporaryDirectory() as d:
45
45
  root = _repo(d, SHIPPED_BODY)
46
- with mock.patch("commands.plan_archive.archive_lib.git_state.git_mv",
46
+ with mock.patch("commands.plan_archive.archive_lib.git_state.is_tracked",
47
+ return_value=True), \
48
+ mock.patch("commands.plan_archive.archive_lib.git_state.git_mv",
47
49
  return_value=True) as mv:
48
50
  rc, out = _run(root, ["--repo=x", "--yes", "--json",
49
51
  "--", "docs/plans/p.md"])
@@ -29,6 +29,9 @@ class ArchiveTest(unittest.TestCase):
29
29
  # run() now reads the batched paths_last_commit_dates map (#391); mock its
30
30
  # .get to return the stale date for every doc (path_last_commit_date is the
31
31
  # fallback path, mocked too).
32
+ #
33
+ # _archive_dead now routes through archive_lib.move_to_archive, so patch
34
+ # is_tracked + git_mv on lib.archive.git_state (not plan_status.git_state).
32
35
  stale = datetime(2026, 1, 1)
33
36
  batched = mock.MagicMock()
34
37
  batched.get.return_value = stale
@@ -37,7 +40,8 @@ class ArchiveTest(unittest.TestCase):
37
40
  mock.patch("commands.plan_status.git_state.paths_last_commit_dates",
38
41
  return_value=batched), \
39
42
  mock.patch("commands.plan_status.Path.cwd", return_value=root), \
40
- mock.patch("commands.plan_status.git_state.git_mv",
43
+ mock.patch("lib.archive.git_state.is_tracked", return_value=True), \
44
+ mock.patch("lib.archive.git_state.git_mv",
41
45
  return_value=mv_ok) as mv, \
42
46
  mock.patch("commands.plan_status.prompt_yes_no", return_value=True):
43
47
  buf = io.StringIO()
@@ -107,7 +107,9 @@ class ArchiveShippedBatchTest(unittest.TestCase):
107
107
  # to "no" on non-TTY stdin) and archive, mirroring per-doc plan-archive.
108
108
  with tempfile.TemporaryDirectory() as d:
109
109
  root = _repo_batch(d)
110
- with mock.patch("commands.plan_status.archive_lib.git_state.git_mv",
110
+ with mock.patch("commands.plan_status.archive_lib.git_state.is_tracked",
111
+ return_value=True), \
112
+ mock.patch("commands.plan_status.archive_lib.git_state.git_mv",
111
113
  return_value=True) as mv:
112
114
  rc, out = _run(root, ["--archive-shipped", "--yes"])
113
115
  self.assertEqual(rc, 0)