@dreki-gg/taskman 0.2.1 → 0.3.1
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 +9 -0
- package/dist/cli.mjs +152 -2
- package/package.json +1 -1
- package/skills/taskman/core/SKILL.md +23 -5
package/README.md
CHANGED
|
@@ -46,6 +46,15 @@ taskman <command> --help # flags + arguments for one command
|
|
|
46
46
|
duplicate it (so it cannot drift). Every command prints human text by default
|
|
47
47
|
and accepts `--json` for machine consumption.
|
|
48
48
|
|
|
49
|
+
Create a plan from any harness (handoff/tasks accept an inline value, a
|
|
50
|
+
`--*-file <path>`, or piped stdin):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
echo "$MARKDOWN" | taskman create-plan --name my-plan --title "My Plan" \
|
|
54
|
+
--handoff-file - --tasks '[{"description":"do it"}]'
|
|
55
|
+
taskman create-handoff --plan my-plan --file HANDOFF.md # write/replace prose
|
|
56
|
+
```
|
|
57
|
+
|
|
49
58
|
A typical execution loop:
|
|
50
59
|
|
|
51
60
|
```bash
|
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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";
|
|
2
|
+
import { J as writeTasksJsonl, N as readInitiativesManifest, P as upsertInitiativeEntry, S as initiativeRollup, T as reconcileInitiativeForPlan, V as upsertPlanEntry, W as saveHandoff, X as makePlanRuntime, _ as applyInitiativeReconcile, a as filterPlans, b as collectPlanDrift, d as setTaskStatus, f as nextTaskId, g as resolvePlanByName, i as loadInitiativeListItems, l as sortPlans, m as loadPlanData, n as formatInitiativeList, o as formatPlanList, p as toKebabCase, 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 { readFile } from "node:fs/promises";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
4
6
|
import { Command } from "commander";
|
|
5
7
|
//#region src/cli/runtime.ts
|
|
6
8
|
/**
|
|
@@ -252,6 +254,143 @@ async function closeInitiativeCommand(status, name, opts) {
|
|
|
252
254
|
}, `Initiative ${name} → ${s}${opts.reason ? ` (${opts.reason})` : ""}.`);
|
|
253
255
|
}
|
|
254
256
|
//#endregion
|
|
257
|
+
//#region src/cli/input.ts
|
|
258
|
+
/**
|
|
259
|
+
* Content resolution for CLI commands that accept markdown / JSON payloads from
|
|
260
|
+
* a foreign harness: an inline string, a file path, or piped stdin.
|
|
261
|
+
*/
|
|
262
|
+
/** Read all of stdin as a UTF-8 string (used when neither inline nor file is given). */
|
|
263
|
+
async function readStdin() {
|
|
264
|
+
const chunks = [];
|
|
265
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
266
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Resolve payload content from, in priority order: an inline value, a `--file`
|
|
270
|
+
* path (or `-` for stdin), else piped stdin. Throws `CliError` when nothing is
|
|
271
|
+
* provided and stdin is a TTY (no piped input).
|
|
272
|
+
*/
|
|
273
|
+
async function resolveContent(inline, file, label) {
|
|
274
|
+
if (inline !== void 0) return inline;
|
|
275
|
+
if (file !== void 0 && file !== "-") try {
|
|
276
|
+
return await readFile(file, "utf8");
|
|
277
|
+
} catch {
|
|
278
|
+
throw new CliError(`Could not read ${label} file: ${file}`);
|
|
279
|
+
}
|
|
280
|
+
if (process.stdin.isTTY) throw new CliError(`No ${label} provided. Pass it inline, via --${label}-file, or on stdin.`);
|
|
281
|
+
return readStdin();
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/cli/commands/create-plan.ts
|
|
285
|
+
/**
|
|
286
|
+
* `taskman create-plan` — create a plan from a foreign harness.
|
|
287
|
+
*
|
|
288
|
+
* The CLI sibling of plan-mode's `submit_plan` tool: writes tasks.jsonl,
|
|
289
|
+
* HANDOFF.md, and a plans.jsonl registry entry in one transaction, then
|
|
290
|
+
* re-projects the parent initiative (when linked). Handoff/tasks payloads come
|
|
291
|
+
* from inline values, files, or stdin so any harness can drive it.
|
|
292
|
+
*/
|
|
293
|
+
/** Parse and validate the `--tasks` JSON payload into TaskInput[]. */
|
|
294
|
+
function parseTasks(raw) {
|
|
295
|
+
let parsed;
|
|
296
|
+
try {
|
|
297
|
+
parsed = JSON.parse(raw);
|
|
298
|
+
} catch {
|
|
299
|
+
throw new CliError("--tasks must be a JSON array of { description, ... } objects.");
|
|
300
|
+
}
|
|
301
|
+
if (!Array.isArray(parsed) || parsed.length === 0) throw new CliError("--tasks must be a non-empty JSON array.");
|
|
302
|
+
return parsed.map((entry, i) => {
|
|
303
|
+
if (typeof entry !== "object" || entry === null) throw new CliError(`Task at index ${i} is not an object.`);
|
|
304
|
+
const { description } = entry;
|
|
305
|
+
if (typeof description !== "string" || description.trim() === "") throw new CliError(`Task at index ${i} is missing a "description".`);
|
|
306
|
+
const t = entry;
|
|
307
|
+
return {
|
|
308
|
+
id: t.id,
|
|
309
|
+
description: t.description,
|
|
310
|
+
details: t.details,
|
|
311
|
+
depends_on: t.depends_on
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/** Assign explicit IDs where given, generate t-NNN for the rest. */
|
|
316
|
+
function assignIds(inputs, now) {
|
|
317
|
+
const ids = inputs.map((t) => t.id).filter((id) => Boolean(id));
|
|
318
|
+
const records = [];
|
|
319
|
+
for (const input of inputs) {
|
|
320
|
+
const id = input.id ?? nextTaskId(ids);
|
|
321
|
+
if (!input.id) ids.push(id);
|
|
322
|
+
records.push({
|
|
323
|
+
_type: "task",
|
|
324
|
+
id,
|
|
325
|
+
description: input.description.slice(0, 60),
|
|
326
|
+
details: input.details ?? "",
|
|
327
|
+
status: "pending",
|
|
328
|
+
depends_on: input.depends_on,
|
|
329
|
+
created_at: now,
|
|
330
|
+
updated_at: now
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return records;
|
|
334
|
+
}
|
|
335
|
+
async function createPlanCommand(opts) {
|
|
336
|
+
if (!opts.name) throw new CliError("--name is required.");
|
|
337
|
+
if (!opts.title) throw new CliError("--title is required.");
|
|
338
|
+
const planName = toKebabCase(opts.name);
|
|
339
|
+
const planDir = `.plans/${planName}`;
|
|
340
|
+
const initiative = opts.initiative ? toKebabCase(opts.initiative) : void 0;
|
|
341
|
+
const dependsOnPlans = opts.dependsOn ? opts.dependsOn.split(",").map((s) => toKebabCase(s.trim())).filter(Boolean) : void 0;
|
|
342
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
343
|
+
const handoff = await resolveContent(opts.handoff, opts.handoffFile, "handoff");
|
|
344
|
+
const tasks = assignIds(parseTasks(await resolveContent(opts.tasks, opts.tasksFile, "tasks")), now);
|
|
345
|
+
const meta = {
|
|
346
|
+
_type: "meta",
|
|
347
|
+
title: opts.title,
|
|
348
|
+
plan_name: planName,
|
|
349
|
+
created_at: now
|
|
350
|
+
};
|
|
351
|
+
const unknownInitiative = await runPlanIO(Effect.gen(function* () {
|
|
352
|
+
yield* writeTasksJsonl(planDir, meta, tasks);
|
|
353
|
+
yield* saveHandoff(planDir, handoff);
|
|
354
|
+
yield* upsertPlanEntry(planName, {
|
|
355
|
+
status: "in-progress",
|
|
356
|
+
title: opts.title,
|
|
357
|
+
initiative,
|
|
358
|
+
depends_on: dependsOnPlans
|
|
359
|
+
});
|
|
360
|
+
yield* reconcileInitiativeForPlan(planName);
|
|
361
|
+
if (!initiative) return false;
|
|
362
|
+
return !(yield* readInitiativesManifest()).some((entry) => entry.name === initiative);
|
|
363
|
+
}));
|
|
364
|
+
const linkSuffix = initiative ? ` Linked to initiative "${initiative}"${unknownInitiative ? " (no initiatives.jsonl entry yet — create it with submit_initiative)" : ""}.` : "";
|
|
365
|
+
emit(Boolean(opts.json), {
|
|
366
|
+
plan_name: planName,
|
|
367
|
+
plan_dir: planDir,
|
|
368
|
+
task_count: tasks.length,
|
|
369
|
+
task_ids: tasks.map((t) => t.id),
|
|
370
|
+
initiative: initiative ?? null,
|
|
371
|
+
depends_on: dependsOnPlans ?? null,
|
|
372
|
+
unknown_initiative: unknownInitiative
|
|
373
|
+
}, `Plan "${opts.title}" saved with ${tasks.length} tasks in ${planDir}.${linkSuffix}`);
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/cli/commands/create-handoff.ts
|
|
377
|
+
/**
|
|
378
|
+
* `taskman create-handoff [content]` — write/replace HANDOFF.md for a plan.
|
|
379
|
+
*
|
|
380
|
+
* Lets a foreign harness hand off markdown without going through the plan-mode
|
|
381
|
+
* extension: content comes from an inline argument, `--file <path>`, or stdin.
|
|
382
|
+
*/
|
|
383
|
+
async function createHandoffCommand(content, opts) {
|
|
384
|
+
const { planName, planDir } = await resolvePlanDir(opts.plan);
|
|
385
|
+
const markdown = await resolveContent(content, opts.file, "handoff");
|
|
386
|
+
await runPlanIO(saveHandoff(planDir, markdown));
|
|
387
|
+
emit(Boolean(opts.json), {
|
|
388
|
+
plan_name: planName,
|
|
389
|
+
path: `${planDir}/HANDOFF.md`,
|
|
390
|
+
bytes: Buffer.byteLength(markdown)
|
|
391
|
+
}, `Wrote HANDOFF.md (${Buffer.byteLength(markdown)} bytes) for ${planName}.`);
|
|
392
|
+
}
|
|
393
|
+
//#endregion
|
|
255
394
|
//#region src/cli.ts
|
|
256
395
|
/**
|
|
257
396
|
* `taskman` CLI — drive the `.plans/` task ledger from any Node harness.
|
|
@@ -259,9 +398,18 @@ async function closeInitiativeCommand(status, name, opts) {
|
|
|
259
398
|
* Thin Commander wiring over the engine: each subcommand delegates to an action
|
|
260
399
|
* module under `cli/commands/`. Human text by default; `--json` for machines.
|
|
261
400
|
*/
|
|
401
|
+
/** Read the shipped package version (dist/cli.mjs → ../package.json). */
|
|
402
|
+
function packageVersion() {
|
|
403
|
+
try {
|
|
404
|
+
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
405
|
+
return JSON.parse(readFileSync(pkgUrl, "utf-8")).version ?? "0.0.0";
|
|
406
|
+
} catch {
|
|
407
|
+
return "0.0.0";
|
|
408
|
+
}
|
|
409
|
+
}
|
|
262
410
|
function buildProgram() {
|
|
263
411
|
const program = new Command();
|
|
264
|
-
program.name("taskman").description("Task-management engine over a .plans/ JSONL ledger").version(
|
|
412
|
+
program.name("taskman").description("Task-management engine over a .plans/ JSONL ledger").version(packageVersion());
|
|
265
413
|
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
414
|
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
415
|
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));
|
|
@@ -269,6 +417,8 @@ function buildProgram() {
|
|
|
269
417
|
program.command("update-task").description("Set a task status (done|skipped|blocked|pending)").argument("<id>", "task id, e.g. t-001").argument("<status>", "done|skipped|blocked|pending").option("--plan <name>", "plan to target").option("--notes <text>", "notes recorded on the task").option("--json", "machine-readable JSON output").action((id, status, opts) => updateTaskCommand(id, status, opts));
|
|
270
418
|
program.command("add-task").description("Append a deferred follow-up task").argument("<description>", "short task label").requiredOption("--reason <text>", "why this follow-up matters").option("--plan <name>", "plan to target").option("--details <text>", "fuller implementation notes").option("--json", "machine-readable JSON output").action((description, opts) => addTaskCommand(description, opts));
|
|
271
419
|
program.command("reconcile").description("Detect (and with --apply, repair) status drift").option("--apply", "repair safe in-progress→done drift").option("--json", "machine-readable JSON output").action((opts) => reconcileCommand(opts));
|
|
420
|
+
program.command("create-plan").description("Create a plan (tasks.jsonl + HANDOFF.md + registry entry) from any harness").requiredOption("--name <name>", "short kebab-case plan name").requiredOption("--title <title>", "human-readable plan title").option("--handoff <text>", "HANDOFF.md markdown (inline)").option("--handoff-file <path>", "read HANDOFF.md markdown from a file (\"-\" for stdin)").option("--tasks <json>", "tasks as an inline JSON array of { description, ... }").option("--tasks-file <path>", "read the tasks JSON array from a file (\"-\" for stdin)").option("--initiative <name>", "parent initiative name to link this plan to").option("--depends-on <names>", "comma-separated plan names this plan depends on").option("--json", "machine-readable JSON output").action((opts) => createPlanCommand(opts));
|
|
421
|
+
program.command("create-handoff").description("Write/replace HANDOFF.md for a plan from any harness").argument("[content]", "HANDOFF.md markdown (inline); else use --file or stdin").option("--plan <name>", "plan to target").option("--file <path>", "read markdown from a file (\"-\" for stdin)").option("--json", "machine-readable JSON output").action((content, opts) => createHandoffCommand(content, opts));
|
|
272
422
|
program.command("close").description("Set a plan lifecycle status").argument("<status>", "done|superseded|abandoned|in-progress").option("--plan <name>", "plan to target").option("--reason <text>", "why (recorded in the registry)").option("--json", "machine-readable JSON output").action((status, opts) => closePlanCommand(status, opts));
|
|
273
423
|
program.command("close-initiative").description("Set an initiative lifecycle status").argument("<status>", "done|superseded|abandoned|in-progress").argument("<name>", "initiative name").option("--reason <text>", "why (recorded in the registry)").option("--json", "machine-readable JSON output").action((status, name, opts) => closeInitiativeCommand(status, name, opts));
|
|
274
424
|
return program;
|
package/package.json
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
name: taskman/core
|
|
3
3
|
description: >
|
|
4
4
|
Drive the taskman CLI and engine over a .plans/ JSONL ledger — plans,
|
|
5
|
-
initiatives, and tasks. Load when running `taskman` commands (
|
|
6
|
-
update-task, add-task, reconcile, close),
|
|
7
|
-
sessions or harnesses, or calling
|
|
8
|
-
status-is-a-projection model,
|
|
5
|
+
initiatives, and tasks. Load when running `taskman` commands (create-plan,
|
|
6
|
+
create-handoff, status, list, update-task, add-task, reconcile, close),
|
|
7
|
+
tracking plan/task progress across sessions or harnesses, or calling
|
|
8
|
+
@dreki-gg/taskman as a library. Covers the status-is-a-projection model,
|
|
9
|
+
stateless plan resolution, and reconcile.
|
|
9
10
|
type: core
|
|
10
11
|
library: "@dreki-gg/taskman"
|
|
11
|
-
library_version: "0.
|
|
12
|
+
library_version: "0.3.1"
|
|
12
13
|
sources:
|
|
13
14
|
- "dreki-gg/pi-extensions:packages/taskman/README.md"
|
|
14
15
|
- "dreki-gg/pi-extensions:packages/taskman/src/cli.ts"
|
|
@@ -65,6 +66,23 @@ taskman add-task "handle empty input" --reason "found gap while implementing"
|
|
|
65
66
|
taskman reconcile --apply # repair safe (in-progress→done) drift
|
|
66
67
|
```
|
|
67
68
|
|
|
69
|
+
### Creating plans from any harness
|
|
70
|
+
|
|
71
|
+
Plans don't have to originate in plan-mode's `submit_plan` tool. The CLI creates
|
|
72
|
+
the same durable artifacts (tasks.jsonl + HANDOFF.md + registry entry) so a
|
|
73
|
+
foreign harness can seed the ledger directly. Handoff/task payloads accept an
|
|
74
|
+
inline value, a `--*-file <path>`, or piped stdin.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
echo "$MARKDOWN" | taskman create-plan --name my-plan --title "My Plan" \
|
|
78
|
+
--handoff-file - --tasks '[{"description":"do it"}]' # tasks get t-NNN ids
|
|
79
|
+
taskman create-handoff --plan my-plan --file HANDOFF.md # write/replace prose
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Use `--initiative <name>` to link the plan (create the initiative first) and
|
|
83
|
+
`--depends-on a,b` for plan-level ordering. Task creation here is plan setup —
|
|
84
|
+
distinct from `add-task`, which records a *deferred* follow-up (see below).
|
|
85
|
+
|
|
68
86
|
### Machine-readable output
|
|
69
87
|
|
|
70
88
|
Every command prints human text by default and accepts `--json`:
|