taskmanager-exe 0.3.1__py3-none-any.whl

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.
taskman/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """TaskManager package."""
taskman/cli.py ADDED
@@ -0,0 +1,93 @@
1
+ import argparse
2
+
3
+ from taskman import core
4
+
5
+
6
+ def _get_version() -> str:
7
+ try:
8
+ from importlib.metadata import version
9
+ return version("taskmanager-exe")
10
+ except Exception:
11
+ return "dev"
12
+
13
+
14
+ def main() -> None:
15
+ parser = argparse.ArgumentParser(prog="taskman")
16
+ parser.add_argument("--version", action="version", version=f"%(prog)s {_get_version()}")
17
+ subparsers = parser.add_subparsers(dest="command")
18
+
19
+ # Setup commands
20
+ subparsers.add_parser("init")
21
+ install_mcp = subparsers.add_parser("install-mcp")
22
+ install_mcp.add_argument("agent", choices=["claude", "cursor", "codex"])
23
+ install_skills = subparsers.add_parser("install-skills")
24
+ install_skills.add_argument("agent", choices=["claude", "codex"])
25
+ uninstall_mcp = subparsers.add_parser("uninstall-mcp")
26
+ uninstall_mcp.add_argument("agent", choices=["claude", "cursor", "codex"])
27
+ uninstall_skills = subparsers.add_parser("uninstall-skills")
28
+ uninstall_skills.add_argument("agent", choices=["claude", "codex"])
29
+ subparsers.add_parser("stdio")
30
+
31
+ wt_parser = subparsers.add_parser("wt")
32
+ wt_parser.add_argument("name", nargs="?", default=None,
33
+ help="worktree name (omit to just clone .agent-files in current dir)")
34
+ wt_parser.add_argument("--new", dest="new_branch", action="store_true",
35
+ help="create new branch instead of using existing one")
36
+
37
+ # Operation commands
38
+ desc = subparsers.add_parser("describe")
39
+ desc.add_argument("reason")
40
+
41
+ sy = subparsers.add_parser("sync")
42
+ sy.add_argument("reason")
43
+
44
+ hd = subparsers.add_parser("history-diffs")
45
+ hd.add_argument("file")
46
+ hd.add_argument("start_rev")
47
+ hd.add_argument("end_rev", nargs="?", default="@")
48
+
49
+ hb = subparsers.add_parser("history-batch")
50
+ hb.add_argument("file")
51
+ hb.add_argument("start_rev")
52
+ hb.add_argument("end_rev", nargs="?", default="@")
53
+
54
+ hs = subparsers.add_parser("history-search")
55
+ hs.add_argument("pattern")
56
+ hs.add_argument("--file", default=None)
57
+ hs.add_argument("--limit", type=int, default=20)
58
+
59
+ args = parser.parse_args()
60
+
61
+ if args.command == "init":
62
+ print(core.init())
63
+ elif args.command == "wt":
64
+ print(core.wt(args.name, new_branch=args.new_branch))
65
+ elif args.command == "install-mcp":
66
+ print(core.install_mcp(args.agent))
67
+ elif args.command == "install-skills":
68
+ print(core.install_skills(args.agent))
69
+ elif args.command == "uninstall-mcp":
70
+ print(core.uninstall_mcp(args.agent))
71
+ elif args.command == "uninstall-skills":
72
+ print(core.uninstall_skills(args.agent))
73
+ elif args.command == "stdio":
74
+ from taskman.server import main as server_main
75
+
76
+ server_main()
77
+ elif args.command == "describe":
78
+ print(core.describe(args.reason))
79
+ elif args.command == "sync":
80
+ print(core.sync(args.reason))
81
+ elif args.command == "history-diffs":
82
+ print(core.history_diffs(args.file, args.start_rev, args.end_rev))
83
+ elif args.command == "history-batch":
84
+ print(core.history_batch(args.file, args.start_rev, args.end_rev))
85
+ elif args.command == "history-search":
86
+ print(core.history_search(args.pattern, args.file, args.limit))
87
+ else:
88
+ parser.print_help()
89
+ raise SystemExit(1)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
taskman/core.py ADDED
@@ -0,0 +1,579 @@
1
+ import json
2
+ import re
3
+ import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+ import tomllib
7
+
8
+ from taskman.jj import run_jj, find_agent_files_dir
9
+
10
+
11
+ def _run_cmd(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
12
+ proc = subprocess.run(
13
+ args,
14
+ cwd=str(cwd) if cwd is not None else None,
15
+ text=True,
16
+ capture_output=True,
17
+ )
18
+ return proc.returncode, proc.stdout, proc.stderr
19
+
20
+
21
+ def _run_cmd_check(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
22
+ code, out, err = _run_cmd(args, cwd=cwd)
23
+ if code != 0:
24
+ cmd_str = " ".join(args)
25
+ raise RuntimeError(
26
+ f"command failed ({code}): {cmd_str}\nstdout:\n{out}\nstderr:\n{err}"
27
+ )
28
+ return code, out, err
29
+
30
+
31
+ def _agent_files_cwd() -> Path:
32
+ return find_agent_files_dir()
33
+
34
+
35
+ def _current_rev_id(cwd: Path) -> str:
36
+ _, out, _ = run_jj(
37
+ ["log", "--no-graph", "-r", "@", "-T", "change_id.short()"],
38
+ cwd,
39
+ )
40
+ return out.strip()
41
+
42
+
43
+ def _has_remote_main(cwd: Path) -> bool:
44
+ try:
45
+ run_jj(["log", "-r", "main@origin", "--no-graph", "-T", "commit_id"], cwd)
46
+ except RuntimeError:
47
+ return False
48
+ return True
49
+
50
+
51
+ def _is_main_tracked(cwd: Path) -> bool:
52
+ """Check if main@origin is tracked (linked to local main)."""
53
+ _, out, _ = run_jj(["bookmark", "list", "--all"], cwd)
54
+ # Tracked: "main: xyz abc" with "main@origin" on separate line
55
+ # Untracked: "main@origin [new] untracked"
56
+ for line in out.splitlines():
57
+ if "main@origin" in line and "untracked" in line:
58
+ return False
59
+ return True
60
+
61
+
62
+ def _setup_main_bookmark(cwd: Path) -> None:
63
+ """Ensure main bookmark exists, tracks main@origin, and points to @."""
64
+ # Track main@origin if exists and untracked
65
+ if _has_remote_main(cwd) and not _is_main_tracked(cwd):
66
+ run_jj(["bookmark", "track", "main@origin"], cwd)
67
+
68
+ # Set main bookmark to current revision (creates if doesn't exist)
69
+ try:
70
+ run_jj(["bookmark", "set", "main", "-r", "@"], cwd)
71
+ except RuntimeError as exc:
72
+ # If bookmark doesn't exist, create it
73
+ if "no such bookmark" in str(exc).lower():
74
+ run_jj(["bookmark", "create", "main", "-r", "@"], cwd)
75
+ else:
76
+ raise
77
+
78
+
79
+ def _status_has_conflicts(status_out: str) -> bool:
80
+ return bool(re.search(r"(?im)^(conflicts|conflicted)\b", status_out))
81
+
82
+
83
+ def _rev_list_for_revset(revset: str, cwd: Path) -> list[str]:
84
+ _, out, _ = run_jj(
85
+ ["log", "--no-graph", "-r", revset, "-T", 'change_id.short() ++ "\\n"'],
86
+ cwd,
87
+ )
88
+ return [line.strip() for line in out.splitlines() if line.strip()]
89
+
90
+
91
+ def _revset_has_revs(revset: str, cwd: Path) -> bool:
92
+ return bool(_rev_list_for_revset(revset, cwd))
93
+
94
+
95
+ def _rev_list(start_rev: str, end_rev: str, cwd: Path) -> list[str]:
96
+ if _revset_has_revs(start_rev, cwd):
97
+ revset = f"{start_rev}::{end_rev}"
98
+ else:
99
+ revset = f"::{end_rev}"
100
+ return _rev_list_for_revset(revset, cwd)
101
+
102
+
103
+ def _escape_revset_value(value: str) -> str:
104
+ return value.replace("\\", "\\\\").replace('"', "\\\"")
105
+
106
+
107
+ def describe(reason: str) -> str:
108
+ """Create named checkpoint.
109
+
110
+ 1. jj status (trigger snapshot)
111
+ 2. jj describe -m "<reason>"
112
+
113
+ Returns: Revision ID and confirmation
114
+ """
115
+ cwd = _agent_files_cwd()
116
+ run_jj(["status"], cwd)
117
+ run_jj(["describe", "-m", reason], cwd)
118
+ rev = _current_rev_id(cwd)
119
+ return f"described {rev}: {reason}"
120
+
121
+
122
+ def sync(reason: str) -> str:
123
+ """Full sync: describe, fetch, rebase, push.
124
+
125
+ 1. jj describe -m "<reason>"
126
+ 2. jj git fetch
127
+ 3. jj rebase -d main@origin (skip if no remote branch)
128
+ 4. Check jj status for conflicts -> return if conflicts
129
+ 5. jj git push -> return error if rejected
130
+
131
+ Returns: Step-by-step status or conflict info
132
+ """
133
+ cwd = _agent_files_cwd()
134
+ steps: list[str] = []
135
+
136
+ run_jj(["describe", "-m", reason], cwd)
137
+ rev = _current_rev_id(cwd)
138
+ steps.append(f"rev: {rev}")
139
+
140
+ run_jj(["git", "fetch"], cwd)
141
+ steps.append("git fetch: ok")
142
+
143
+ has_remote = _has_remote_main(cwd)
144
+ if has_remote:
145
+ run_jj(["rebase", "-d", "main@origin"], cwd)
146
+ steps.append("rebase: main@origin")
147
+ else:
148
+ steps.append("rebase: skipped (no main@origin)")
149
+
150
+ _, status_out, _ = run_jj(["status"], cwd)
151
+ if _status_has_conflicts(status_out):
152
+ return "conflicts detected:\n" + status_out
153
+
154
+ _setup_main_bookmark(cwd)
155
+
156
+ try:
157
+ # Use --all for first push (no remote yet), regular push otherwise
158
+ push_cmd = ["git", "push"] if has_remote else ["git", "push", "--all"]
159
+ run_jj(push_cmd, cwd)
160
+ steps.append("git push: ok")
161
+ except RuntimeError as exc:
162
+ err_msg = str(exc)
163
+ steps.append("git push: FAILED")
164
+ # Extract useful info from jj error
165
+ if "no author" in err_msg.lower() or "no committer" in err_msg.lower():
166
+ steps.append("Error: commit has no author/committer set")
167
+ steps.append("Fix: jj config set --user user.name 'Your Name'")
168
+ steps.append(" jj config set --user user.email 'you@example.com'")
169
+ elif "rejected" in err_msg.lower() or "non-fast-forward" in err_msg.lower():
170
+ steps.append("Error: push rejected (remote changed)")
171
+ steps.append("Recovery: run 'taskman sync' again to rebase and retry")
172
+ else:
173
+ steps.append(err_msg)
174
+ return "\n".join(steps)
175
+
176
+ return "\n".join(steps)
177
+
178
+
179
+ def history_diffs(file: str, start_rev: str, end_rev: str = "@") -> str:
180
+ """Get all diffs for file across revision range.
181
+
182
+ 1. Get revisions: jj log --no-graph -r "{start}::{end}" -T 'change_id.short()'
183
+ 2. For each: jj diff -r {rev} -- {file}
184
+ 3. Concatenate with === {rev} === headers
185
+ """
186
+ cwd = _agent_files_cwd()
187
+ revs = _rev_list(start_rev, end_rev, cwd)
188
+ if not revs:
189
+ return "No revisions found in range."
190
+
191
+ sections: list[str] = []
192
+ for rev in revs:
193
+ sections.append(f"=== {rev} ===")
194
+ _, out, _ = run_jj(["diff", "-r", rev, "--", file], cwd)
195
+ sections.append(out.rstrip())
196
+
197
+ return "\n".join(sections).rstrip()
198
+
199
+
200
+ def history_batch(file: str, start_rev: str, end_rev: str = "@") -> str:
201
+ """Fetch file content at all revisions in range.
202
+
203
+ 1. Get revisions (same as history_diffs)
204
+ 2. For each: jj file show -r {rev} {file}
205
+ 3. Concatenate with === {rev} === headers
206
+ """
207
+ cwd = _agent_files_cwd()
208
+ revs = _rev_list(start_rev, end_rev, cwd)
209
+ if not revs:
210
+ return "No revisions found in range."
211
+
212
+ sections: list[str] = []
213
+ for rev in revs:
214
+ try:
215
+ _, out, _ = run_jj(["file", "show", "-r", rev, file], cwd)
216
+ except RuntimeError as exc:
217
+ if "no such path" in str(exc).lower():
218
+ sections.append(f"=== {rev} ===")
219
+ sections.append("(file does not exist at this revision)")
220
+ continue
221
+ raise
222
+ sections.append(f"=== {rev} ===")
223
+ sections.append(out.rstrip())
224
+
225
+ return "\n".join(sections).rstrip()
226
+
227
+
228
+ def history_search(pattern: str, file: str | None = None, limit: int = 20) -> str:
229
+ """Search history for pattern in diffs using jj's diff_contains().
230
+
231
+ Uses: jj log -r 'diff_contains("{pattern}")' --limit {limit}
232
+ Or with file: jj log -r 'diff_contains("{pattern}", "{file}")' --limit {limit}
233
+
234
+ Supports jj pattern syntax: exact:, glob:, regex:, substring:
235
+ Examples:
236
+ history_search("TODO") # glob (default)
237
+ history_search("regex:fix.*bug") # regex
238
+ history_search("exact:FIXME", "src/") # exact match in src/
239
+
240
+ Returns: Matching revisions with commit info
241
+ """
242
+ cwd = _agent_files_cwd()
243
+ escaped_pattern = _escape_revset_value(pattern)
244
+ if file is None:
245
+ revset = f'diff_contains("{escaped_pattern}")'
246
+ else:
247
+ escaped_file = _escape_revset_value(file)
248
+ revset = f'diff_contains("{escaped_pattern}", "{escaped_file}")'
249
+ _, out, _ = run_jj(["log", "-r", revset, "--limit", str(limit)], cwd)
250
+ return out.rstrip()
251
+
252
+
253
+ # Setup functions
254
+
255
+ def init() -> str:
256
+ """Create .agent-files.git/ (bare) + .agent-files/ (clone)
257
+
258
+ 1. git init --bare .agent-files.git
259
+ 2. jj git clone .agent-files.git .agent-files
260
+ 3. Create initial files: STATUS.md, LONGTERM_MEM.md, MEDIUMTERM_MEM.md, tasks/
261
+ 4. jj describe -m "initial setup" && jj git push
262
+ """
263
+ cwd = Path.cwd()
264
+ bare = cwd / ".agent-files.git"
265
+ clone = cwd / ".agent-files"
266
+
267
+ if bare.exists() or clone.exists():
268
+ raise FileExistsError(".agent-files.git or .agent-files already exists")
269
+
270
+ _run_cmd_check(["git", "init", "--bare", str(bare)], cwd=cwd)
271
+ run_jj(["git", "clone", str(bare), str(clone)], cwd)
272
+
273
+ # Set default author for agent commits
274
+ run_jj(["config", "set", "--repo", "user.name", "Agent"], clone)
275
+ run_jj(["config", "set", "--repo", "user.email", "agent@localhost"], clone)
276
+
277
+ (clone / "tasks").mkdir(parents=True, exist_ok=True)
278
+ for filename in ["STATUS.md", "LONGTERM_MEM.md", "MEDIUMTERM_MEM.md"]:
279
+ path = clone / filename
280
+ path.touch(exist_ok=True)
281
+
282
+ run_jj(["describe", "-m", "initial setup"], clone)
283
+ run_jj(["bookmark", "create", "main", "-r", "@"], clone)
284
+ run_jj(["git", "push", "--all"], clone)
285
+
286
+ return "Initialized .agent-files.git and .agent-files"
287
+
288
+
289
+ def _find_agent_files_git_dir(start: Path | None = None) -> Path:
290
+ current = Path.cwd() if start is None else Path(start)
291
+ if current.is_file():
292
+ current = current.parent
293
+
294
+ while True:
295
+ candidate = current / ".agent-files.git"
296
+ if candidate.is_dir():
297
+ return candidate
298
+ if current.parent == current:
299
+ break
300
+ current = current.parent
301
+
302
+ raise FileNotFoundError(".agent-files.git directory not found")
303
+
304
+
305
+ def wt(name: str | None = None, *, new_branch: bool = False) -> str:
306
+ """Create git worktree and/or clone .agent-files
307
+
308
+ If name is provided (from main repo):
309
+ 1. Create worktrees/<name>/ via git worktree add
310
+ 2. Clone .agent-files into worktrees/<name>/
311
+
312
+ If name is None (recovery for existing worktree):
313
+ 1. Clone .agent-files into current directory
314
+
315
+ By default uses existing branch. If new_branch=True, creates new branch.
316
+ """
317
+ cwd = Path.cwd()
318
+ origin = _find_agent_files_git_dir(cwd)
319
+ in_main_repo = (cwd / ".agent-files.git").exists()
320
+
321
+ if name:
322
+ if not in_main_repo:
323
+ raise ValueError(
324
+ f"Run 'taskman wt {name}' from main repo ({origin.parent})"
325
+ )
326
+ worktree_dir = cwd / "worktrees" / name
327
+ if worktree_dir.exists():
328
+ raise FileExistsError(f"worktrees/{name} already exists")
329
+
330
+ cmd = ["git", "worktree", "add", str(worktree_dir)]
331
+ if not new_branch:
332
+ cmd.append(name)
333
+ _run_cmd_check(cmd, cwd=cwd)
334
+
335
+ clone = worktree_dir / ".agent-files"
336
+ run_jj(["git", "clone", str(origin), str(clone)], worktree_dir)
337
+ run_jj(["config", "set", "--repo", "user.name", "Agent"], clone)
338
+ run_jj(["config", "set", "--repo", "user.email", "agent@localhost"], clone)
339
+
340
+ return f"Created worktree at worktrees/{name}/"
341
+ else:
342
+ if in_main_repo:
343
+ raise ValueError("Use 'taskman wt <name>' to create a worktree")
344
+ clone = cwd / ".agent-files"
345
+ if clone.exists():
346
+ raise FileExistsError(".agent-files already exists")
347
+ run_jj(["git", "clone", str(origin), str(clone)], cwd)
348
+ run_jj(["config", "set", "--repo", "user.name", "Agent"], clone)
349
+ run_jj(["config", "set", "--repo", "user.email", "agent@localhost"], clone)
350
+ return f"Cloned .agent-files from {origin}"
351
+
352
+
353
+ def _load_json(path: Path) -> dict:
354
+ if not path.exists():
355
+ return {}
356
+ text = path.read_text(encoding="utf-8")
357
+ if not text.strip():
358
+ return {}
359
+ return json.loads(text)
360
+
361
+
362
+ def _write_json(path: Path, data: dict) -> None:
363
+ path.parent.mkdir(parents=True, exist_ok=True)
364
+ path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
365
+
366
+
367
+ def _load_toml(path: Path) -> dict:
368
+ if not path.exists():
369
+ return {}
370
+ text = path.read_text(encoding="utf-8")
371
+ if not text.strip():
372
+ return {}
373
+ return tomllib.loads(text)
374
+
375
+
376
+ def _toml_format_value(value) -> str:
377
+ if isinstance(value, str):
378
+ escaped = value.replace("\\", "\\\\").replace('"', "\\\"")
379
+ return f"\"{escaped}\""
380
+ if isinstance(value, bool):
381
+ return "true" if value else "false"
382
+ if isinstance(value, int):
383
+ return str(value)
384
+ if isinstance(value, float):
385
+ return repr(value)
386
+ if isinstance(value, list):
387
+ return "[" + ", ".join(_toml_format_value(v) for v in value) + "]"
388
+ raise TypeError(f"Unsupported TOML value: {value!r}")
389
+
390
+
391
+ def _toml_dump_table(lines: list[str], prefix: list[str], table: dict) -> None:
392
+ lines.append(f"[{'.'.join(prefix)}]")
393
+ for key in sorted(table.keys()):
394
+ value = table[key]
395
+ if isinstance(value, dict):
396
+ continue
397
+ lines.append(f"{key} = {_toml_format_value(value)}")
398
+
399
+ for key in sorted(table.keys()):
400
+ value = table[key]
401
+ if isinstance(value, dict):
402
+ lines.append("")
403
+ _toml_dump_table(lines, prefix + [key], value)
404
+
405
+
406
+ def _toml_dumps(data: dict) -> str:
407
+ lines: list[str] = []
408
+
409
+ for key in sorted(data.keys()):
410
+ value = data[key]
411
+ if isinstance(value, dict):
412
+ continue
413
+ lines.append(f"{key} = {_toml_format_value(value)}")
414
+
415
+ for key in sorted(data.keys()):
416
+ value = data[key]
417
+ if isinstance(value, dict):
418
+ if lines:
419
+ lines.append("")
420
+ _toml_dump_table(lines, [key], value)
421
+
422
+ if not lines:
423
+ return ""
424
+ return "\n".join(lines).rstrip() + "\n"
425
+
426
+
427
+ def install_mcp(agent: str) -> str:
428
+ """Install MCP config for agent (claude, cursor, codex).
429
+
430
+ Config locations:
431
+ - claude: ~/.claude.json or .mcp.json (adds to mcpServers)
432
+ - cursor: ~/.cursor/mcp.json (adds to mcpServers)
433
+ - codex: ~/.codex/config.toml (adds to mcp_servers)
434
+ """
435
+ home = Path.home()
436
+ if agent == "claude":
437
+ project_config = Path(".mcp.json")
438
+ path = project_config if project_config.exists() else home / ".claude.json"
439
+ data = _load_json(path)
440
+ data.setdefault("mcpServers", {})
441
+ data["mcpServers"]["taskman"] = {
442
+ "type": "stdio",
443
+ "command": "taskman",
444
+ "args": ["stdio"],
445
+ }
446
+ _write_json(path, data)
447
+ return f"Installed taskman MCP server in {path}"
448
+
449
+ if agent == "cursor":
450
+ project_config = Path(".cursor") / "mcp.json"
451
+ path = project_config if project_config.exists() else home / ".cursor" / "mcp.json"
452
+ data = _load_json(path)
453
+ data.setdefault("mcpServers", {})
454
+ data["mcpServers"]["taskman"] = {
455
+ "type": "stdio",
456
+ "command": "taskman",
457
+ "args": ["stdio"],
458
+ }
459
+ _write_json(path, data)
460
+ return f"Installed taskman MCP server in {path}"
461
+
462
+ if agent == "codex":
463
+ path = home / ".codex" / "config.toml"
464
+ data = _load_toml(path)
465
+ data.setdefault("mcp_servers", {})
466
+ data["mcp_servers"]["taskman"] = {
467
+ "command": "taskman",
468
+ "args": ["stdio"],
469
+ }
470
+ path.parent.mkdir(parents=True, exist_ok=True)
471
+ path.write_text(_toml_dumps(data), encoding="utf-8")
472
+ return f"Installed taskman MCP server in {path}"
473
+
474
+ raise ValueError(f"Unknown agent: {agent}")
475
+
476
+
477
+ def install_skills(agent: str) -> str:
478
+ """Copy skill files to agent's skills directory."""
479
+ skills_dir = Path(__file__).resolve().parent / "skills"
480
+ if not skills_dir.is_dir():
481
+ raise FileNotFoundError(f"skills directory not found: {skills_dir}")
482
+
483
+ home = Path.home()
484
+ if agent == "claude":
485
+ dest_dir = home / ".claude" / "skills" / "taskman"
486
+ elif agent == "codex":
487
+ dest_dir = home / ".codex" / "skills" / "taskman"
488
+ else:
489
+ raise ValueError(f"Unknown agent: {agent}")
490
+
491
+ dest_dir.mkdir(parents=True, exist_ok=True)
492
+
493
+ count = 0
494
+ for path in skills_dir.glob("*.md"):
495
+ shutil.copy2(path, dest_dir / path.name)
496
+ count += 1
497
+
498
+ return f"Installed {count} skills to {dest_dir}"
499
+
500
+
501
+ def uninstall_mcp(agent: str) -> str:
502
+ """Remove MCP config for agent (claude, cursor, codex)."""
503
+ home = Path.home()
504
+ if agent == "claude":
505
+ project_config = Path(".mcp.json")
506
+ path = project_config if project_config.exists() else home / ".claude.json"
507
+ if not path.exists():
508
+ return f"No MCP config found at {path}"
509
+ data = _load_json(path)
510
+ servers = data.get("mcpServers")
511
+ if isinstance(servers, dict) and "taskman" in servers:
512
+ servers.pop("taskman", None)
513
+ if servers:
514
+ data["mcpServers"] = servers
515
+ else:
516
+ data.pop("mcpServers", None)
517
+ _write_json(path, data)
518
+ return f"Removed taskman MCP server from {path}"
519
+ return f"No taskman MCP server entry found in {path}"
520
+
521
+ if agent == "cursor":
522
+ project_config = Path(".cursor") / "mcp.json"
523
+ path = project_config if project_config.exists() else home / ".cursor" / "mcp.json"
524
+ if not path.exists():
525
+ return f"No MCP config found at {path}"
526
+ data = _load_json(path)
527
+ servers = data.get("mcpServers")
528
+ if isinstance(servers, dict) and "taskman" in servers:
529
+ servers.pop("taskman", None)
530
+ if servers:
531
+ data["mcpServers"] = servers
532
+ else:
533
+ data.pop("mcpServers", None)
534
+ _write_json(path, data)
535
+ return f"Removed taskman MCP server from {path}"
536
+ return f"No taskman MCP server entry found in {path}"
537
+
538
+ if agent == "codex":
539
+ path = home / ".codex" / "config.toml"
540
+ if not path.exists():
541
+ return f"No MCP config found at {path}"
542
+ data = _load_toml(path)
543
+ servers = data.get("mcp_servers")
544
+ if isinstance(servers, dict) and "taskman" in servers:
545
+ servers.pop("taskman", None)
546
+ if servers:
547
+ data["mcp_servers"] = servers
548
+ else:
549
+ data.pop("mcp_servers", None)
550
+ path.write_text(_toml_dumps(data), encoding="utf-8")
551
+ return f"Removed taskman MCP server from {path}"
552
+ return f"No taskman MCP server entry found in {path}"
553
+
554
+ raise ValueError(f"Unknown agent: {agent}")
555
+
556
+
557
+ def uninstall_skills(agent: str) -> str:
558
+ """Remove taskman skill files from agent's skills directory."""
559
+ home = Path.home()
560
+ if agent == "claude":
561
+ dest_dir = home / ".claude" / "skills" / "taskman"
562
+ elif agent == "codex":
563
+ dest_dir = home / ".codex" / "skills" / "taskman"
564
+ else:
565
+ raise ValueError(f"Unknown agent: {agent}")
566
+
567
+ if not dest_dir.is_dir():
568
+ return f"No skills directory found at {dest_dir}"
569
+
570
+ count = 0
571
+ for path in dest_dir.glob("*.md"):
572
+ path.unlink()
573
+ count += 1
574
+
575
+ # Remove the taskman directory if empty
576
+ if dest_dir.is_dir() and not any(dest_dir.iterdir()):
577
+ dest_dir.rmdir()
578
+
579
+ return f"Removed {count} skills from {dest_dir}"
taskman/jj.py ADDED
@@ -0,0 +1,76 @@
1
+ import shlex
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+
6
+ def run_jj(args: list[str], cwd: Path) -> tuple[int, str, str]:
7
+ """Run jj command with git conflict style.
8
+
9
+ Uses --config-toml when supported, otherwise falls back to --config.
10
+ Uses subprocess.run() - no async needed for sequential CLI commands.
11
+
12
+ Returns: (returncode, stdout, stderr)
13
+ Raises: RuntimeError if returncode != 0
14
+ """
15
+ cmd_toml = [
16
+ "jj",
17
+ "--config-toml",
18
+ 'ui.conflict-marker-style = "git"',
19
+ *args,
20
+ ]
21
+ proc = subprocess.run(
22
+ cmd_toml,
23
+ cwd=str(cwd),
24
+ text=True,
25
+ capture_output=True,
26
+ )
27
+ if proc.returncode != 0 and "unexpected argument '--config-toml'" in proc.stderr:
28
+ cmd_legacy = [
29
+ "jj",
30
+ "--config",
31
+ "ui.conflict-marker-style=git",
32
+ *args,
33
+ ]
34
+ proc = subprocess.run(
35
+ cmd_legacy,
36
+ cwd=str(cwd),
37
+ text=True,
38
+ capture_output=True,
39
+ )
40
+ if proc.returncode != 0:
41
+ message = (
42
+ f"jj command failed ({proc.returncode}): {shlex.join(cmd_legacy)}\n"
43
+ f"stdout:\n{proc.stdout}\n"
44
+ f"stderr:\n{proc.stderr}"
45
+ )
46
+ raise RuntimeError(message)
47
+ return proc.returncode, proc.stdout, proc.stderr
48
+
49
+ if proc.returncode != 0:
50
+ message = (
51
+ f"jj command failed ({proc.returncode}): {shlex.join(cmd_toml)}\n"
52
+ f"stdout:\n{proc.stdout}\n"
53
+ f"stderr:\n{proc.stderr}"
54
+ )
55
+ raise RuntimeError(message)
56
+ return proc.returncode, proc.stdout, proc.stderr
57
+
58
+
59
+ def find_agent_files_dir(start: Path | None = None) -> Path:
60
+ """Search upward from start (default: cwd) to find .agent-files/
61
+
62
+ Returns: Path to .agent-files/ or raises FileNotFoundError
63
+ """
64
+ current = Path.cwd() if start is None else Path(start)
65
+ if current.is_file():
66
+ current = current.parent
67
+
68
+ while True:
69
+ candidate = current / ".agent-files"
70
+ if candidate.is_dir():
71
+ return candidate
72
+ if current.parent == current:
73
+ break
74
+ current = current.parent
75
+
76
+ raise FileNotFoundError(".agent-files directory not found")
taskman/server.py ADDED
@@ -0,0 +1,56 @@
1
+ import asyncio
2
+ from mcp.server.fastmcp import FastMCP
3
+
4
+ from taskman import core
5
+
6
+
7
+ class _SyncMCP:
8
+ def __init__(self, inner: FastMCP) -> None:
9
+ self._inner = inner
10
+
11
+ def list_tools(self):
12
+ return asyncio.run(self._inner.list_tools())
13
+
14
+ def __getattr__(self, name):
15
+ return getattr(self._inner, name)
16
+
17
+
18
+ mcp = _SyncMCP(FastMCP("taskman"))
19
+
20
+
21
+ @mcp.tool()
22
+ def describe(reason: str) -> str:
23
+ """Create named checkpoint."""
24
+ return core.describe(reason)
25
+
26
+
27
+ @mcp.tool()
28
+ def sync(reason: str) -> str:
29
+ """Full sync: describe, fetch, rebase, push."""
30
+ return core.sync(reason)
31
+
32
+
33
+ @mcp.tool()
34
+ def history_diffs(file: str, start_rev: str, end_rev: str = "@") -> str:
35
+ """Get all diffs for file across revision range."""
36
+ return core.history_diffs(file, start_rev, end_rev)
37
+
38
+
39
+ @mcp.tool()
40
+ def history_batch(file: str, start_rev: str, end_rev: str = "@") -> str:
41
+ """Fetch file content at all revisions in range."""
42
+ return core.history_batch(file, start_rev, end_rev)
43
+
44
+
45
+ @mcp.tool()
46
+ def history_search(pattern: str, file: str | None = None, limit: int = 20) -> str:
47
+ """Search history for pattern in diffs."""
48
+ return core.history_search(pattern, file, limit)
49
+
50
+
51
+ def main() -> None:
52
+ mcp.run()
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: taskman
3
+ description: Agent memory and task management CLI. Use this skill when you need to persist context across sessions, track tasks, hand off work, or store temporary agent scratch data. Provides the `taskman` CLI for init, sync, describe, and history operations.
4
+ ---
5
+
6
+ # Taskman
7
+
8
+ Version-controlled agent memory and task management. The `.agent-files/` directory is scratch space for ANY agent work that should persist across sessions - task tracking, memory, handoffs, notes, or temporary files.
9
+
10
+ ## Structure
11
+
12
+ ```
13
+ .agent-files/
14
+ STATUS.md # Task index, current session state
15
+ LONGTERM_MEM.md # Architecture knowledge (months+)
16
+ MEDIUMTERM_MEM.md # Patterns, gotchas (weeks)
17
+ tasks/
18
+ TASK_<slug>.md # Active tasks
19
+ _archive/ # Completed tasks
20
+ (any other scratch files)
21
+ ```
22
+
23
+ **STATUS.md**: Operational state - task index, current focus, blockers, next steps. Update, don't overwrite.
24
+
25
+ **LONGTERM_MEM.md**: System architecture, component relationships. Rarely changes.
26
+
27
+ **MEDIUMTERM_MEM.md**: Reusable patterns and gotchas. NOT session logs.
28
+
29
+ **Task files**: One per user-facing work unit. Format:
30
+
31
+ ```markdown
32
+ # TASK: <title>
33
+
34
+ ## Meta
35
+ Status: planned|in_progress|blocked|complete
36
+ Priority: P0|P1|P2
37
+ Created: YYYY-MM-DD
38
+ Completed: YYYY-MM-DD
39
+
40
+ ## Problem
41
+ <what, why>
42
+
43
+ ## Design
44
+ <decisions, alternatives rejected>
45
+
46
+ ## Checklist
47
+ - [ ] item
48
+ - [x] completed item
49
+
50
+ ## Attempts
51
+ ### Attempt N (YYYY-MM-DD HH:MM)
52
+ Approach: ...
53
+ Result: ...
54
+
55
+ ## Summary
56
+ Current state: ...
57
+ Key learnings: ...
58
+ Next steps: ...
59
+
60
+ ## Notes
61
+ <breadcrumbs - pointers to recoverable info>
62
+
63
+ ## Budget (optional)
64
+ Estimate: <tokens> (planning: X, impl: Y, validation: Z)
65
+ Variance: low|med|high
66
+ Intervention: autonomous|checkpoints|steering|collaborative
67
+ Spent: <tokens>
68
+ ```
69
+
70
+ Budget uses tokens (measurable) not time. Variance = estimate spread (low=tight, high=wide). Intervention = human engagement pattern, not duration.
71
+
72
+ **Scratch space**: Store any temporary agent work here - it's version-controlled separately from the main repo.
73
+
74
+ ## Progressive Disclosure
75
+
76
+ Store breadcrumbs (pointers), not content. Recover on-demand via Read/Bash/WebFetch.
77
+
78
+ ```
79
+ <slug>: <recovery-instruction>
80
+ ```
81
+
82
+ Examples: `auth-flow: src/auth/login.ts:45-80` | `build-status: run `make build`` | `prev-attempt: jj diff -r @--`
83
+
84
+ Store inline only: decisions, key insights, non-reproducible errors.
85
+
86
+ See `/handoff` for writing breadcrumbs, `/continue` for expanding them.
87
+
88
+ ## Commands
89
+
90
+ | Command | Use when |
91
+ |---------|----------|
92
+ | /continue | Resuming work from a previous session |
93
+ | /handoff | Saving context mid-task for next session |
94
+ | /remember | Persisting learnings to memory files |
95
+ | /complete | Finishing and archiving a task |
96
+ | /sync | Syncing .agent-files with origin |
97
+ | /describe | Creating a named checkpoint |
98
+ | /history-search | Searching history for patterns |
99
+ | /history-diffs | Viewing diffs across revisions |
100
+ | /history-batch | Fetching file content at revisions |
101
+ | /wt | Setting up .agent-files in a git worktree |
102
+
103
+ When a command is invoked, read the corresponding `.md` file in this skill directory for detailed instructions.
104
+
105
+ ## jj Snapshotting
106
+
107
+ jj does NOT auto-snapshot on file changes alone. A jj command must be run to trigger a snapshot. Run `jj st` periodically (after edits or batches of edits) to capture history. Without this, intermediate states are lost.
108
+
109
+ ## Important
110
+
111
+ `.agent-files/` should never be committed. Add it to `.gitignore`.
@@ -0,0 +1,16 @@
1
+ Complete a task and archive it.
2
+
3
+ 1. Update the task file:
4
+ - Set Status: complete
5
+ - Add Completed: date
6
+ - Fill Notes with any gotchas or uncompleted work
7
+
8
+ 2. Move task to _archive/
9
+
10
+ 3. Update STATUS.md:
11
+ - Remove from active tasks
12
+ - Add pointer to next task (if any)
13
+
14
+ 4. Run: taskman sync "complete: $ARGUMENTS"
15
+
16
+ Keep it brief - the task is done.
@@ -0,0 +1,28 @@
1
+ Resume work from a previous session.
2
+
3
+ 1. Run: taskman sync "continue"
4
+
5
+ 2. Read STATUS.md - current focus, blockers, task index
6
+
7
+ 3. Read the active task file(s) - focus on Summary and Notes sections
8
+
9
+ 4. **Expand breadcrumbs selectively** (see below)
10
+
11
+ 5. Ultrathink about your approach before continuing.
12
+
13
+ ## Expanding Breadcrumbs
14
+
15
+ Task files contain pointers, not content. Expand only what's needed for your next step:
16
+
17
+ | Breadcrumb | Recovery |
18
+ |------------|----------|
19
+ | `src/auth.ts:45-80` | Read tool (those lines only) |
20
+ | run \`pytest -v\` | Bash tool (current state) |
21
+ | `jj diff -r @--` | Bash tool (last changes) |
22
+ | `issue: github.com/...` | WebFetch if needed |
23
+
24
+ **Order:** Read summary → identify next step → expand only what's needed → work → repeat.
25
+
26
+ Don't preload all references upfront. The previous session left good pointers - trust them and expand lazily.
27
+
28
+ **Ultrathink vs preloading:** Think deeply about *approach*, not by dumping all content into context. Expand breadcrumbs to answer specific questions, not "just in case".
@@ -0,0 +1,5 @@
1
+ Create a named checkpoint in .agent-files.
2
+
3
+ Run: taskman describe "$ARGUMENTS"
4
+
5
+ Report the revision ID.
@@ -0,0 +1,43 @@
1
+ Mid-task handoff - save detailed context for next session.
2
+
3
+ 1. Update the current task file with:
4
+ - Attempts: what was tried, what failed (brief - just approach + outcome)
5
+ - Summary: current state, key learnings, next steps
6
+ - Notes: **breadcrumbs only** - pointers to recoverable information
7
+ - Budget: update Spent tokens if tracking
8
+
9
+ 2. Run: taskman sync "handoff: $ARGUMENTS"
10
+
11
+ 3. Update STATUS.md with handoff context (brief pointer to task file)
12
+
13
+ ## Breadcrumb Principle
14
+
15
+ **Store pointers, not content.** The next session can recover information on-demand.
16
+
17
+ Bad (context pollution):
18
+ ```markdown
19
+ ## Notes
20
+ The authentication flow works like this:
21
+ [50 lines of code]
22
+ The error message was:
23
+ [20 lines of stack trace]
24
+ ```
25
+
26
+ Good (progressive disclosure):
27
+ ```markdown
28
+ ## Notes
29
+ auth-flow: src/auth/login.ts:45-80
30
+ error-repro: run `make test-auth` (fails on line 23)
31
+ prev-diff: jj diff -r @--
32
+ related-issue: github.com/org/repo/issues/123
33
+ ```
34
+
35
+ ## Writing Breadcrumbs
36
+
37
+ Format: `<slug>: <recovery-instruction> [(context)]`
38
+
39
+ Recovery: file→Read, command→Bash, url→WebFetch
40
+
41
+ **Store inline** (not as breadcrumbs): decisions, key insights, non-reproducible errors.
42
+
43
+ Goal: next session reconstructs context in 2-3 tool calls, not by reading walls of text.
@@ -0,0 +1,7 @@
1
+ Fetch file content at all revisions in range.
2
+
3
+ Arguments: <file> <start_rev> [end_rev]
4
+
5
+ Run: taskman history-batch $ARGUMENTS
6
+
7
+ Display the output.
@@ -0,0 +1,7 @@
1
+ Get all diffs for a file across revisions.
2
+
3
+ Arguments: <file> <start_rev> [end_rev]
4
+
5
+ Run: taskman history-diffs $ARGUMENTS
6
+
7
+ Display the output.
@@ -0,0 +1,13 @@
1
+ Search history for a pattern in diffs.
2
+
3
+ Arguments: <pattern> [--file <file>] [--limit N]
4
+
5
+ Pattern syntax (jj native):
6
+ - Default: glob match
7
+ - regex:pattern - regex match
8
+ - exact:pattern - exact match
9
+ - substring:pattern - substring match
10
+
11
+ Run: taskman history-search $ARGUMENTS
12
+
13
+ Display matching revisions.
@@ -0,0 +1,8 @@
1
+ Persist context to memory files.
2
+
3
+ 1. Update appropriate file based on $ARGUMENTS:
4
+ - MEDIUMTERM_MEM.md: patterns, gotchas, recent learnings
5
+ - LONGTERM_MEM.md: architecture, component relationships
6
+ - STATUS.md: current state, blockers, next steps
7
+
8
+ 2. Run: taskman sync "remember: $ARGUMENTS"
taskman/skills/sync.md ADDED
@@ -0,0 +1,5 @@
1
+ Sync .agent-files with origin.
2
+
3
+ Run: taskman sync "$ARGUMENTS"
4
+
5
+ If conflicts are reported, help the user resolve them with Edit, then run sync again.
taskman/skills/wt.md ADDED
@@ -0,0 +1,9 @@
1
+ Set up .agent-files in a git worktree.
2
+
3
+ Run: taskman wt $ARGUMENTS
4
+
5
+ - No arguments: clone .agent-files into current directory (for existing worktrees)
6
+ - `taskman wt <name>`: create worktree for existing branch <name>
7
+ - `taskman wt <name> --new`: create worktree + new branch at worktrees/<name>/
8
+
9
+ Use when working in a git worktree that doesn't have .agent-files yet.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: taskmanager-exe
3
+ Version: 0.3.1
4
+ Summary: Version-controlled task management for AI agents
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: mcp
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest>=8.0; extra == 'dev'
10
+ Description-Content-Type: text/markdown
11
+
12
+ # TaskManager.exe
13
+
14
+ Version-controlled task management for AI agents. Agents use familiar file editing tools; versioning and sync happen transparently via [jj (jujutsu)](https://martinvonz.github.io/jj/).
15
+
16
+ ## Problem
17
+
18
+ AI agents using file-based task systems lose work when:
19
+ - Multiple agents edit the same file
20
+ - Context resets mid-task and overwrites with stale state
21
+ - No history to recover from
22
+ - **Agents go in circles** - after context reset, they repeat mistakes because they don't know what was already tried
23
+
24
+ ## Installation
25
+
26
+ Requires Python 3.11+ and [jj](https://martinvonz.github.io/jj/latest/install/).
27
+
28
+ ```bash
29
+ pipx install taskmanager-exe
30
+ # or
31
+ uvx taskmanager-exe
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ```bash
37
+ # Initialize in your repo
38
+ taskman init
39
+
40
+ # Install MCP server config
41
+ taskman install-mcp claude # or: cursor, codex
42
+
43
+ # Install Claude Code skills (optional)
44
+ taskman install-skills
45
+ ```
46
+
47
+ To create a worktree (from main repo):
48
+ ```bash
49
+ taskman wt my-feature # creates worktrees/my-feature/ + clones .agent-files
50
+ ```
51
+
52
+ To add .agent-files to an existing worktree (recovery):
53
+ ```bash
54
+ taskman wt # clones .agent-files into current directory
55
+ ```
56
+
57
+ ## How It Works
58
+
59
+ ```
60
+ Agent
61
+
62
+ ├── Edit tool ────────► .agent-files/ (jj repo)
63
+ │ (file ops) │
64
+ │ push/pull
65
+ ├── MCP Server ───────────────┼──────────────────►
66
+ │ (batch/sync) ▼
67
+ │ .agent-files.git/ (bare origin)
68
+ └── Skills ───────────────────┘
69
+ (CLI wrapper)
70
+ ```
71
+
72
+ - Agents edit files with their normal Edit tool
73
+ - jj auto-snapshots every change (no explicit commit needed)
74
+ - MCP tools or Skills handle sync and history queries
75
+ - Bare git origin serializes concurrent access across worktrees
76
+
77
+ ## CLI Commands
78
+
79
+ ```bash
80
+ taskman init # create .agent-files.git/ + .agent-files/
81
+ taskman wt <name> # create worktree (from main repo)
82
+ taskman wt # add .agent-files to existing worktree
83
+ taskman install-mcp <agent> # install MCP config (claude, cursor, codex)
84
+ taskman install-skills # install skill files to ~/.claude/commands/
85
+ taskman uninstall-mcp <agent> # remove MCP config
86
+ taskman uninstall-skills # remove skill files
87
+
88
+ taskman describe <reason> # create named checkpoint
89
+ taskman sync <reason> # full sync: describe + fetch + rebase + push
90
+ taskman history-diffs <file> <start> [end] # diffs across revision range
91
+ taskman history-batch <file> <start> [end] # file content at each revision
92
+ taskman history-search <pattern> [file] [limit] # search history
93
+
94
+ taskman stdio # run MCP server (stdio transport)
95
+ ```
96
+
97
+ ## MCP Tools
98
+
99
+ When installed via `taskman install-mcp`, these tools are available:
100
+
101
+ | Tool | Description |
102
+ |------|-------------|
103
+ | `describe(reason)` | Create named checkpoint |
104
+ | `sync(reason)` | Full sync workflow |
105
+ | `history_diffs(file, start, end)` | Aggregate diffs across range |
106
+ | `history_batch(file, start, end)` | File content at all revisions |
107
+ | `history_search(pattern, file, limit)` | Search history for pattern |
108
+
109
+ ## Skills
110
+
111
+ When installed via `taskman install-skills`, these Claude Code skills are available:
112
+
113
+ | Skill | Description |
114
+ |-------|-------------|
115
+ | `/continue` | Resume work - pull + read STATUS.md |
116
+ | `/handoff` | Mid-task handoff - sync + detailed context |
117
+ | `/complete` | Task done - sync + archive |
118
+ | `/describe <reason>` | Create named checkpoint |
119
+ | `/sync <reason>` | Full sync workflow |
120
+ | `/history-diffs <file> <start> [end]` | Diffs across range |
121
+ | `/history-batch <file> <start> [end]` | File content at revisions |
122
+ | `/history-search <pattern> [--file] [--limit]` | Search history |
123
+
124
+ Skills wrap the CLI and work without MCP support.
125
+
126
+ ## Direct jj Commands
127
+
128
+ Agents can also use jj directly for simple operations:
129
+
130
+ ```bash
131
+ jj status # current state
132
+ jj log # view history
133
+ jj diff # see changes
134
+ jj restore --from <rev> <file> # restore file from revision
135
+ ```
136
+
137
+ ## Task File Structure
138
+
139
+ ```
140
+ .agent-files/
141
+ STATUS.md # Task index, session state
142
+ LONGTERM_MEM.md # Architecture (months+)
143
+ MEDIUMTERM_MEM.md # Patterns, gotchas (weeks)
144
+ tasks/
145
+ TASK_<slug>.md # Individual tasks
146
+ _archive/ # Completed tasks
147
+ ```
148
+
149
+ ## Sync Model
150
+
151
+ Sync at task boundaries:
152
+ - `/continue` - session start, pull latest state
153
+ - `/handoff` - mid-task, push with detailed context
154
+ - `/complete` - task done, push and archive
155
+
156
+ On conflict, agent resolves with Edit tool, then syncs again.
157
+
158
+ ## License
159
+
160
+ MIT
@@ -0,0 +1,21 @@
1
+ taskman/__init__.py,sha256=_TflS09UJqi5AVigsi628jsaSPinpUxYDMBsis-kYaU,27
2
+ taskman/cli.py,sha256=NsadNr9OnngqOH_7nkR-6kDpYJ6kecjmbOWt9XF4eEw,3364
3
+ taskman/core.py,sha256=du1dhUipOqV7ook5H6fEFRM_g1XebBHBtzaSnJFPB_0,19422
4
+ taskman/jj.py,sha256=aF7W8MtxqbfRqVcmIAHVqEAW8S5B__u29QUOvlURipE,2267
5
+ taskman/server.py,sha256=5ACffRaSZRUuPpoaheiBsVqaMLacYZeRPc9erGJCyoE,1279
6
+ taskman/skills/SKILL.md,sha256=lvTofHeN4GwCsMmhjX5wZ_ZzanYhW1S4NtXFJB-MHpc,3467
7
+ taskman/skills/complete.md,sha256=2A5yZ2VowIvfdYkV0vSmYLa94HZJneTyQzmDmdjEG-U,360
8
+ taskman/skills/continue.md,sha256=aDXGWK9nP4xr9qiHzsvwuvS0osW90rMdM0GeMxL5M3s,1055
9
+ taskman/skills/describe.md,sha256=CICVO3o0C7Fj2Pdw_Y2RVwHc11JMtevExrGtY7uTSTI,104
10
+ taskman/skills/handoff.md,sha256=9LdtnLrSzVCkBNVYkawyUNzsoOMkh_c7N4UX1Kve9Fs,1262
11
+ taskman/skills/history-batch.md,sha256=_X_VS7mvsx6UZchXk1LaLBcu8O1OqNxH6RPBBtB_dTY,147
12
+ taskman/skills/history-diffs.md,sha256=3AiwkCJVnRJ-pdtbzY6uqxveW4eGjZNGOQfU6VgF5Tc,144
13
+ taskman/skills/history-search.md,sha256=StXlsDSEOZp4Z4sJzBhUcSqJz2kNPtEgvwRMwlF0i0k,307
14
+ taskman/skills/remember.md,sha256=47FLqmxR1nFKN6R7nlvV3ZyTd78acAQs-ZMqEHzF_JI,299
15
+ taskman/skills/sync.md,sha256=WYLiTzl_Rb4_2aWpmaW8sHYNWwOkMOTTPAQjGcFIt0M,150
16
+ taskman/skills/wt.md,sha256=N0-mCOl3z44q3R3XsXqmO6Mrm_CSSWuLu-WU1dOpSLs,368
17
+ taskmanager_exe-0.3.1.dist-info/METADATA,sha256=i8qsRndysDYwoHYKC7wIOJL7akcb-clXpCXKCEC82xQ,5034
18
+ taskmanager_exe-0.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
19
+ taskmanager_exe-0.3.1.dist-info/entry_points.txt,sha256=_S_GgMwhqTBXOPeevUXw0_Y7iOauQLIsLaC9bECTIy4,45
20
+ taskmanager_exe-0.3.1.dist-info/licenses/LICENSE,sha256=GrA0izm2N5Ifdndiu7vcBjKqIE4varKiDt3LLqY5wPA,164
21
+ taskmanager_exe-0.3.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ taskman = taskman.cli:main
@@ -0,0 +1,3 @@
1
+ This is 100% (or maybe 99.5%) AI-generated code. It is free! You may do with it what you want!
2
+
3
+ I TAKE ZERO RESPONSIBILITY FOR ANYTHING THIS CODE DOES! IT IS SLOP!