@dreki-gg/taskman 0.2.0 → 0.2.2
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 +85 -0
- package/dist/cli.mjs +11 -1
- package/package.json +8 -3
- package/skills/taskman/core/SKILL.md +165 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @dreki-gg/taskman
|
|
2
|
+
|
|
3
|
+
A standalone task-management engine — and a `taskman` CLI — over a plain
|
|
4
|
+
`.plans/` JSONL ledger. It is the core extracted from
|
|
5
|
+
[`@dreki-gg/pi-plan-mode`](../plan-mode), so any harness (not just pi) can drive
|
|
6
|
+
the same plans, initiatives, and tasks.
|
|
7
|
+
|
|
8
|
+
## Why it exists
|
|
9
|
+
|
|
10
|
+
Planning agents need durable, file-based task state that survives across
|
|
11
|
+
sessions and tools. `taskman` owns that state machine — task status, plan and
|
|
12
|
+
initiative lifecycle, and the projection rules that keep them consistent — with
|
|
13
|
+
**no dependency on any specific agent harness**. Use it from a shell, a CI job,
|
|
14
|
+
a different agent, or as a library.
|
|
15
|
+
|
|
16
|
+
## The ledger (the one durable contract)
|
|
17
|
+
|
|
18
|
+
Everything lives under `.plans/` in the current working directory:
|
|
19
|
+
|
|
20
|
+
- `.plans/plans.jsonl` — the plan registry.
|
|
21
|
+
- `.plans/initiatives.jsonl` — the initiative registry (initiatives group plans).
|
|
22
|
+
- `.plans/<plan>/tasks.jsonl` — one plan's task list (first line is metadata).
|
|
23
|
+
- `.plans/<plan>/HANDOFF.md`, `.plans/<initiative>/INITIATIVE.md` — prose docs.
|
|
24
|
+
|
|
25
|
+
Three invariants are worth knowing; everything else is mechanism:
|
|
26
|
+
|
|
27
|
+
1. **Status is a projection, not a flag.** A plan is `done` when its active
|
|
28
|
+
tasks are all resolved (and no follow-ups remain); an initiative is `done`
|
|
29
|
+
when every member plan is terminal. Writing task state re-derives the
|
|
30
|
+
registry — you do not set plan status by hand for the normal path.
|
|
31
|
+
2. **Plan resolution is stateless.** A command targets a plan via `--plan
|
|
32
|
+
<name>` (accepts `.plans/<name>` too), else the *single* in-progress plan.
|
|
33
|
+
Ambiguous or missing → it exits non-zero and lists the candidates.
|
|
34
|
+
3. **Terminal statuses set manually are never auto-reverted.** `reconcile` only
|
|
35
|
+
moves `in-progress ⇄ done`; it never resurrects a `superseded`/`abandoned`
|
|
36
|
+
plan or regresses a finished one.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
taskman --help # full, always-current command list
|
|
42
|
+
taskman <command> --help # flags + arguments for one command
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`--help` is the source of truth for commands and flags — this README does not
|
|
46
|
+
duplicate it (so it cannot drift). Every command prints human text by default
|
|
47
|
+
and accepts `--json` for machine consumption.
|
|
48
|
+
|
|
49
|
+
A typical execution loop:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
taskman status # what's the active plan and its tasks?
|
|
53
|
+
taskman update-task t-003 done # mark progress (auto-reconciles the plan)
|
|
54
|
+
taskman add-task "handle empty case" --reason "found gap while implementing"
|
|
55
|
+
taskman reconcile --apply # repair safe status drift
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## As a library
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { makePlanRuntime, resolvePlanByName, setTaskStatus } from '@dreki-gg/taskman';
|
|
62
|
+
|
|
63
|
+
const run = makePlanRuntime(); // bridges the Effect programs to the live filesystem
|
|
64
|
+
const { planDir } = await run(resolvePlanByName({ name: 'my-plan' }));
|
|
65
|
+
await run(setTaskStatus(planDir!, 't-001', 'done'));
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The public surface (storage, schema, reconcile, initiative projection,
|
|
69
|
+
resolution, and composite write flows) is exported from the package root. The
|
|
70
|
+
engine is built on [Effect](https://effect.website) with a single `FileSystem`
|
|
71
|
+
seam, so it is straightforward to test and to run against an alternate backend.
|
|
72
|
+
|
|
73
|
+
## Agent skill
|
|
74
|
+
|
|
75
|
+
This package ships a [TanStack Intent](https://tanstack.com/intent) skill
|
|
76
|
+
(`skills/taskman/core`) — versioned guidance that AI coding agents discover from
|
|
77
|
+
`node_modules`. If you use an AI agent, run:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx @tanstack/intent@latest install
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { N as readInitiativesManifest, P as upsertInitiativeEntry, S as initiativeRollup, T as reconcileInitiativeForPlan, V as upsertPlanEntry, X as makePlanRuntime, _ as applyInitiativeReconcile, a as filterPlans, b as collectPlanDrift, d as setTaskStatus, g as resolvePlanByName, i as loadInitiativeListItems, l as sortPlans, m as loadPlanData, n as formatInitiativeList, o as formatPlanList, s as loadPlanListItems, t as filterInitiatives, u as appendDeferredTask, v as applyReconcile, y as collectInitiativeDrift, z as readPlansManifest } from "./initiatives-Ij_teFl_.mjs";
|
|
3
3
|
import { Effect } from "effect";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
//#region src/cli/runtime.ts
|
|
6
7
|
/**
|
|
@@ -259,9 +260,18 @@ async function closeInitiativeCommand(status, name, opts) {
|
|
|
259
260
|
* Thin Commander wiring over the engine: each subcommand delegates to an action
|
|
260
261
|
* module under `cli/commands/`. Human text by default; `--json` for machines.
|
|
261
262
|
*/
|
|
263
|
+
/** Read the shipped package version (dist/cli.mjs → ../package.json). */
|
|
264
|
+
function packageVersion() {
|
|
265
|
+
try {
|
|
266
|
+
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
267
|
+
return JSON.parse(readFileSync(pkgUrl, "utf-8")).version ?? "0.0.0";
|
|
268
|
+
} catch {
|
|
269
|
+
return "0.0.0";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
262
272
|
function buildProgram() {
|
|
263
273
|
const program = new Command();
|
|
264
|
-
program.name("taskman").description("Task-management engine over a .plans/ JSONL ledger").version(
|
|
274
|
+
program.name("taskman").description("Task-management engine over a .plans/ JSONL ledger").version(packageVersion());
|
|
265
275
|
program.command("status").description("Progress + task ids/statuses for the active plan").option("--plan <name>", "plan name (or .plans/<name>) to inspect").option("--json", "machine-readable JSON output").action((opts) => statusCommand(opts));
|
|
266
276
|
program.command("list").description("List plans").option("--status <status>", "all|in-progress|done|superseded|abandoned").option("--sort <field>", "name|date-asc|date-desc|tasks").option("--json", "machine-readable JSON output").action((opts) => listPlansCommand(opts));
|
|
267
277
|
program.command("initiatives").description("List initiatives").option("--status <status>", "all|in-progress|done|superseded|abandoned").option("--json", "machine-readable JSON output").action((opts) => listInitiativesCommand(opts));
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreki-gg/taskman",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Standalone task-management engine + CLI over a .plans/ JSONL ledger — the plan-mode core, usable from any Node harness",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tasks",
|
|
7
7
|
"planning",
|
|
8
|
-
"cli"
|
|
8
|
+
"cli",
|
|
9
|
+
"tanstack-intent"
|
|
9
10
|
],
|
|
10
11
|
"author": "Juan Albarran <jalbarrandev@gmail.com>",
|
|
11
12
|
"license": "MIT",
|
|
@@ -26,11 +27,14 @@
|
|
|
26
27
|
"taskman": "./dist/cli.mjs"
|
|
27
28
|
},
|
|
28
29
|
"files": [
|
|
29
|
-
"dist"
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md",
|
|
32
|
+
"skills"
|
|
30
33
|
],
|
|
31
34
|
"scripts": {
|
|
32
35
|
"build": "tsdown",
|
|
33
36
|
"test": "bun test",
|
|
37
|
+
"intent:validate": "intent validate",
|
|
34
38
|
"typecheck": "tsc --noEmit",
|
|
35
39
|
"lint": "oxlint src",
|
|
36
40
|
"format": "oxfmt --write src",
|
|
@@ -41,6 +45,7 @@
|
|
|
41
45
|
"effect": "^3.21.2"
|
|
42
46
|
},
|
|
43
47
|
"devDependencies": {
|
|
48
|
+
"@tanstack/intent": "^0.1.0",
|
|
44
49
|
"@types/node": "24",
|
|
45
50
|
"bun-types": "latest",
|
|
46
51
|
"oxfmt": "^0.43.0",
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: taskman/core
|
|
3
|
+
description: >
|
|
4
|
+
Drive the taskman CLI and engine over a .plans/ JSONL ledger — plans,
|
|
5
|
+
initiatives, and tasks. Load when running `taskman` commands (status, list,
|
|
6
|
+
update-task, add-task, reconcile, close), tracking plan/task progress across
|
|
7
|
+
sessions or harnesses, or calling @dreki-gg/taskman as a library. Covers the
|
|
8
|
+
status-is-a-projection model, stateless plan resolution, and reconcile.
|
|
9
|
+
type: core
|
|
10
|
+
library: "@dreki-gg/taskman"
|
|
11
|
+
library_version: "0.2.1"
|
|
12
|
+
sources:
|
|
13
|
+
- "dreki-gg/pi-extensions:packages/taskman/README.md"
|
|
14
|
+
- "dreki-gg/pi-extensions:packages/taskman/src/cli.ts"
|
|
15
|
+
- "dreki-gg/pi-extensions:packages/taskman/src/engine.ts"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# taskman
|
|
19
|
+
|
|
20
|
+
`taskman` manages plan/task state on disk under `.plans/` so planning agents
|
|
21
|
+
keep durable progress across sessions and tools. It ships both a CLI (`taskman`)
|
|
22
|
+
and a library (`@dreki-gg/taskman`).
|
|
23
|
+
|
|
24
|
+
The command and flag inventory is **not** reproduced here — it lives in
|
|
25
|
+
`--help`, which is always current for the installed version:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
taskman --help # all commands
|
|
29
|
+
taskman <command> --help # flags + arguments for one command
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This skill teaches the model that `--help` cannot: the data contract and the
|
|
33
|
+
three invariants that decide whether a command does what you expect.
|
|
34
|
+
|
|
35
|
+
## The ledger
|
|
36
|
+
|
|
37
|
+
Everything is plain JSONL/Markdown under `.plans/` in the current directory:
|
|
38
|
+
|
|
39
|
+
- `.plans/plans.jsonl` — plan registry
|
|
40
|
+
- `.plans/initiatives.jsonl` — initiative registry (initiatives group plans)
|
|
41
|
+
- `.plans/<plan>/tasks.jsonl` — one plan's tasks (first line is metadata)
|
|
42
|
+
- `.plans/<plan>/HANDOFF.md`, `.plans/<initiative>/INITIATIVE.md` — prose
|
|
43
|
+
|
|
44
|
+
## Invariants (read before acting)
|
|
45
|
+
|
|
46
|
+
1. **Status is a projection of task state, not a manual flag.** A plan becomes
|
|
47
|
+
`done` when its active tasks are all resolved and no follow-ups remain; an
|
|
48
|
+
initiative becomes `done` when every member plan is terminal. `update-task`
|
|
49
|
+
re-derives the registry automatically.
|
|
50
|
+
2. **Plan resolution is stateless.** A command targets a plan via `--plan
|
|
51
|
+
<name>` (accepts `.plans/<name>`), else the *single* in-progress plan.
|
|
52
|
+
Ambiguous or missing → non-zero exit listing the candidates.
|
|
53
|
+
3. **Manual terminal statuses are never auto-reverted.** `reconcile` only moves
|
|
54
|
+
`in-progress ⇄ done`; it never resurrects a `superseded`/`abandoned` plan or
|
|
55
|
+
regresses a finished one.
|
|
56
|
+
|
|
57
|
+
## Core Patterns
|
|
58
|
+
|
|
59
|
+
### Execution loop
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
taskman status # active plan + task ids/statuses
|
|
63
|
+
taskman update-task t-003 done # mark progress; plan status re-derived
|
|
64
|
+
taskman add-task "handle empty input" --reason "found gap while implementing"
|
|
65
|
+
taskman reconcile --apply # repair safe (in-progress→done) drift
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Machine-readable output
|
|
69
|
+
|
|
70
|
+
Every command prints human text by default and accepts `--json`:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
taskman status --json # { active, plan_name, title, total, counts, task_ids }
|
|
74
|
+
taskman list --json # array of plan items
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Library usage
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { makePlanRuntime, resolvePlanByName, setTaskStatus } from '@dreki-gg/taskman';
|
|
81
|
+
|
|
82
|
+
const run = makePlanRuntime(); // bridges Effect programs to the live filesystem
|
|
83
|
+
const { planName, planDir, candidates } = await run(resolvePlanByName({ name: 'my-plan' }));
|
|
84
|
+
if (!planDir) throw new Error(`Unresolved; candidates: ${candidates.join(', ')}`);
|
|
85
|
+
await run(setTaskStatus(planDir, 't-001', 'done')); // also reconciles the registry
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Common Mistakes
|
|
89
|
+
|
|
90
|
+
### HIGH Setting plan status by hand to mark progress
|
|
91
|
+
|
|
92
|
+
Wrong:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
taskman close done --plan my-plan # to "finish" a plan whose tasks are open
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Correct:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
taskman update-task t-007 done # resolve the tasks; status follows
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`close` forces a lifecycle status and is for `superseded`/`abandoned` (or an
|
|
105
|
+
explicit override). For normal completion, resolve the tasks — the `done`
|
|
106
|
+
projection is derived from task state, so a forced `done` will be re-opened the
|
|
107
|
+
moment another task write reconciles the plan.
|
|
108
|
+
|
|
109
|
+
### HIGH Assuming a bare command finds any plan
|
|
110
|
+
|
|
111
|
+
Wrong:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
taskman status # expecting it to pick "the plan I just finished"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Correct:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
taskman status --plan my-plan # name it explicitly when not uniquely in-progress
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
With no `--plan`, resolution only succeeds when exactly one plan is
|
|
124
|
+
`in-progress`. A just-completed plan is `done`, so it drops out of resolution
|
|
125
|
+
and a bare command exits non-zero — pass `--plan`.
|
|
126
|
+
|
|
127
|
+
### MEDIUM Treating reconcile as a force-sync in both directions
|
|
128
|
+
|
|
129
|
+
Wrong:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
taskman reconcile --apply # expecting it to flip a done plan back to in-progress
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Correct:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# A `done` plan with unfinished tasks is reported as downgrade drift, NOT fixed.
|
|
139
|
+
# Resolve it by marking the tasks instead:
|
|
140
|
+
taskman update-task t-004 done --plan my-plan
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`reconcile --apply` only performs the safe upgrade (`in-progress → done`). A
|
|
144
|
+
`done`→`in-progress` downgrade almost always means work merged without marking
|
|
145
|
+
tasks; it is surfaced for a human, never auto-applied.
|
|
146
|
+
|
|
147
|
+
### MEDIUM Expecting add-task to queue active work
|
|
148
|
+
|
|
149
|
+
Wrong:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
taskman add-task "refactor parser" --reason "messy"
|
|
153
|
+
# ...then expecting it to show as pending work to do now
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Correct:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
taskman add-task "refactor parser" --reason "messy" # captured as a deferred follow-up
|
|
160
|
+
# Review/triage later; it is intentionally kept out of the active queue and
|
|
161
|
+
# keeps the plan non-finalizable until resolved.
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`add-task` records a `deferred`, `discovered` task — a follow-up for later
|
|
165
|
+
triage, not an active to-do. It does not become pending automatically.
|