@dreki-gg/taskman 0.2.2 → 0.4.0

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
@@ -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,7 @@
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-AtmFo2DU.mjs";
3
3
  import { Effect } from "effect";
4
+ import { readFile } from "node:fs/promises";
4
5
  import { readFileSync } from "node:fs";
5
6
  import { Command } from "commander";
6
7
  //#region src/cli/runtime.ts
@@ -253,6 +254,143 @@ async function closeInitiativeCommand(status, name, opts) {
253
254
  }, `Initiative ${name} → ${s}${opts.reason ? ` (${opts.reason})` : ""}.`);
254
255
  }
255
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
256
394
  //#region src/cli.ts
257
395
  /**
258
396
  * `taskman` CLI — drive the `.plans/` task ledger from any Node harness.
@@ -279,6 +417,8 @@ function buildProgram() {
279
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));
280
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));
281
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));
282
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));
283
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));
284
424
  return program;
package/dist/index.d.mts CHANGED
@@ -301,12 +301,36 @@ interface FileSystemService {
301
301
  }
302
302
  declare const FileSystem_base: Context.TagClass<FileSystem, "PlanMode/FileSystem", FileSystemService>;
303
303
  declare class FileSystem extends FileSystem_base {}
304
+ /**
305
+ * Build a node-backed filesystem service whose relative paths resolve against
306
+ * `root`. All storage programs use relative `.plans/...` paths, so prefixing a
307
+ * root relocates the entire plan registry coherently (manifests, plan dirs,
308
+ * handoffs) to another working directory.
309
+ *
310
+ * `resolve(root, p)` is a no-op for already-absolute paths and reproduces the
311
+ * default `process.cwd()` behaviour exactly when `root === process.cwd()`, so
312
+ * the common (no-target) path is unchanged.
313
+ */
314
+ declare function makeNodeFileSystemService(root: string): FileSystemService;
315
+ /**
316
+ * Default service: relative paths resolve against the current working directory
317
+ * at call time. `resolve('.', p)` is equivalent to `resolve(process.cwd(), p)`,
318
+ * so this matches the historical behaviour of passing raw relative paths to fs.
319
+ */
304
320
  declare const nodeFileSystemService: FileSystemService;
305
321
  //#endregion
306
322
  //#region src/effects/runtime.d.ts
307
- declare function makeRuntimeLayer(): Layer.Layer<FileSystem>;
308
- /** Build a bridge that runs storage programs against the live filesystem layer. */
309
- declare function makePlanRuntime(): <A, E>(program: Effect.Effect<A, E, FileSystem>) => Promise<A>;
323
+ /**
324
+ * Build the live filesystem layer. Pass `root` to relocate the whole `.plans/`
325
+ * registry under another working directory; omit it for the default (relative
326
+ * paths resolve against the current working directory).
327
+ */
328
+ declare function makeRuntimeLayer(root?: string): Layer.Layer<FileSystem>;
329
+ /**
330
+ * Build a bridge that runs storage programs against the live filesystem layer.
331
+ * Pass `root` to target an external working directory's `.plans/` registry.
332
+ */
333
+ declare function makePlanRuntime(root?: string): <A, E>(program: Effect.Effect<A, E, FileSystem>) => Promise<A>;
310
334
  type RunPlanIO = ReturnType<typeof makePlanRuntime>;
311
335
  //#endregion
312
336
  //#region src/storage/task-storage.d.ts
@@ -679,4 +703,4 @@ declare function toKebabCase(name: string): string;
679
703
  */
680
704
  declare function nextTaskId(existingIds: readonly string[]): string;
681
705
  //#endregion
682
- export { AddTaskInput, type ExecPendingConfig, ExecPendingConfigSchema, FileSystem, type FileSystemService, InitiativeDriftRow, initiatives_d_exports as InitiativeListing, type InitiativeManifestEntry, InitiativeManifestEntrySchema, InitiativeMemberRow, InitiativeRollup, type InitiativeStatus, InitiativeStatusSchema, type InitiativeUpsert, JsonlParseError, JsonlValidationError, MissingMetaRecord, type PlanData, PlanDriftRow, plans_d_exports as PlanListing, type PlanManifestEntry, PlanManifestEntrySchema, PlanReadError, PlanReadiness, type PlanStatus, PlanStatusSchema, PlanStorageError, type PlanUpsert, PlanWriteError, ResolvedPlanName, type RunPlanIO, type TaskMeta, TaskMetaSchema, TaskNotFound, type TaskOrigin, TaskOriginSchema, type TaskRecord, TaskRecordSchema, type TaskStatus, TaskStatusSchema, TasksFileNotFound, TasksLineSchema, TasksSnapshot, type ThinkingLevel, UpdatedTaskResult, activeTasksResolved, appendDeferredTask, applyInitiativeReconcile, applyInitiativeUpsert, applyPlanUpsert, applyReconcile, causeMessage, collectInitiativeDrift, collectPlanDrift, computePlanReadiness, decodeExecPendingConfig, decodeInitiativeManifestEntry, decodePlanManifestEntry, decodeTaskMeta, decodeTaskRecord, decodeTasksLine, deferredTasks, errorMessage, initiativeRollup, isInitiativeFinalizable, isPlanFinalizable, isTerminalStatus, loadHandoff, loadPlanData, makePlanRuntime, makeRuntimeLayer, membersOf, mutateInitiativesManifest, mutatePlansManifest, nextTaskId, nodeFileSystemService, normalizePlanName, reactivateForExecution, readInitiativesManifest, readPlansManifest, readTasksJsonl, reconcileInitiativeForPlan, reconcileInitiativeStatus, reconcilePlanStatus, resolvePlanByName, saveHandoff, saveInitiative, setTaskStatus, toKebabCase, toNativeError, updateTask, upsertInitiativeEntry, upsertPlanEntry, withFileLock, writeFileAtomic, writeInitiativesManifest, writePlansManifest, writeTasksJsonl };
706
+ export { AddTaskInput, type ExecPendingConfig, ExecPendingConfigSchema, FileSystem, type FileSystemService, InitiativeDriftRow, initiatives_d_exports as InitiativeListing, type InitiativeManifestEntry, InitiativeManifestEntrySchema, InitiativeMemberRow, InitiativeRollup, type InitiativeStatus, InitiativeStatusSchema, type InitiativeUpsert, JsonlParseError, JsonlValidationError, MissingMetaRecord, type PlanData, PlanDriftRow, plans_d_exports as PlanListing, type PlanManifestEntry, PlanManifestEntrySchema, PlanReadError, PlanReadiness, type PlanStatus, PlanStatusSchema, PlanStorageError, type PlanUpsert, PlanWriteError, ResolvedPlanName, type RunPlanIO, type TaskMeta, TaskMetaSchema, TaskNotFound, type TaskOrigin, TaskOriginSchema, type TaskRecord, TaskRecordSchema, type TaskStatus, TaskStatusSchema, TasksFileNotFound, TasksLineSchema, TasksSnapshot, type ThinkingLevel, UpdatedTaskResult, activeTasksResolved, appendDeferredTask, applyInitiativeReconcile, applyInitiativeUpsert, applyPlanUpsert, applyReconcile, causeMessage, collectInitiativeDrift, collectPlanDrift, computePlanReadiness, decodeExecPendingConfig, decodeInitiativeManifestEntry, decodePlanManifestEntry, decodeTaskMeta, decodeTaskRecord, decodeTasksLine, deferredTasks, errorMessage, initiativeRollup, isInitiativeFinalizable, isPlanFinalizable, isTerminalStatus, loadHandoff, loadPlanData, makeNodeFileSystemService, makePlanRuntime, makeRuntimeLayer, membersOf, mutateInitiativesManifest, mutatePlansManifest, nextTaskId, nodeFileSystemService, normalizePlanName, reactivateForExecution, readInitiativesManifest, readPlansManifest, readTasksJsonl, reconcileInitiativeForPlan, reconcileInitiativeStatus, reconcilePlanStatus, resolvePlanByName, saveHandoff, saveInitiative, setTaskStatus, toKebabCase, toNativeError, updateTask, upsertInitiativeEntry, upsertPlanEntry, withFileLock, writeFileAtomic, writeInitiativesManifest, writePlansManifest, writeTasksJsonl };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { $ as nodeFileSystemService, A as reactivateForExecution, B as reconcilePlanStatus, C as isInitiativeFinalizable, Ct as TasksFileNotFound, D as activeTasksResolved, E as reconcileInitiativeStatus, Et as toNativeError, F as writeInitiativesManifest, G as saveInitiative, H as writePlansManifest, I as applyPlanUpsert, J as writeTasksJsonl, K as readTasksJsonl, L as isTerminalStatus, M as mutateInitiativesManifest, N as readInitiativesManifest, O as deferredTasks, P as upsertInitiativeEntry, Q as FileSystem, R as mutatePlansManifest, S as initiativeRollup, St as TaskNotFound, T as reconcileInitiativeForPlan, Tt as errorMessage, U as loadHandoff, V as upsertPlanEntry, W as saveHandoff, X as makePlanRuntime, Y as withFileLock, Z as makeRuntimeLayer, _ as applyInitiativeReconcile, _t as JsonlParseError, at as PlanStatusSchema, b as collectPlanDrift, bt as PlanReadError, c as plans_exports, ct as TaskRecordSchema, d as setTaskStatus, dt as decodeExecPendingConfig, et as writeFileAtomic, f as nextTaskId, ft as decodeInitiativeManifestEntry, g as resolvePlanByName, gt as decodeTasksLine, h as normalizePlanName, ht as decodeTaskRecord, it as PlanManifestEntrySchema, j as applyInitiativeUpsert, k as isPlanFinalizable, lt as TaskStatusSchema, m as loadPlanData, mt as decodeTaskMeta, nt as InitiativeManifestEntrySchema, ot as TaskMetaSchema, p as toKebabCase, pt as decodePlanManifestEntry, q as updateTask, r as initiatives_exports, rt as InitiativeStatusSchema, st as TaskOriginSchema, tt as ExecPendingConfigSchema, u as appendDeferredTask, ut as TasksLineSchema, v as applyReconcile, vt as JsonlValidationError, w as membersOf, wt as causeMessage, x as computePlanReadiness, xt as PlanWriteError, y as collectInitiativeDrift, yt as MissingMetaRecord, z as readPlansManifest } from "./initiatives-Ij_teFl_.mjs";
2
- export { ExecPendingConfigSchema, FileSystem, initiatives_exports as InitiativeListing, InitiativeManifestEntrySchema, InitiativeStatusSchema, JsonlParseError, JsonlValidationError, MissingMetaRecord, plans_exports as PlanListing, PlanManifestEntrySchema, PlanReadError, PlanStatusSchema, PlanWriteError, TaskMetaSchema, TaskNotFound, TaskOriginSchema, TaskRecordSchema, TaskStatusSchema, TasksFileNotFound, TasksLineSchema, activeTasksResolved, appendDeferredTask, applyInitiativeReconcile, applyInitiativeUpsert, applyPlanUpsert, applyReconcile, causeMessage, collectInitiativeDrift, collectPlanDrift, computePlanReadiness, decodeExecPendingConfig, decodeInitiativeManifestEntry, decodePlanManifestEntry, decodeTaskMeta, decodeTaskRecord, decodeTasksLine, deferredTasks, errorMessage, initiativeRollup, isInitiativeFinalizable, isPlanFinalizable, isTerminalStatus, loadHandoff, loadPlanData, makePlanRuntime, makeRuntimeLayer, membersOf, mutateInitiativesManifest, mutatePlansManifest, nextTaskId, nodeFileSystemService, normalizePlanName, reactivateForExecution, readInitiativesManifest, readPlansManifest, readTasksJsonl, reconcileInitiativeForPlan, reconcileInitiativeStatus, reconcilePlanStatus, resolvePlanByName, saveHandoff, saveInitiative, setTaskStatus, toKebabCase, toNativeError, updateTask, upsertInitiativeEntry, upsertPlanEntry, withFileLock, writeFileAtomic, writeInitiativesManifest, writePlansManifest, writeTasksJsonl };
1
+ import { $ as makeNodeFileSystemService, A as reactivateForExecution, B as reconcilePlanStatus, C as isInitiativeFinalizable, Ct as TaskNotFound, D as activeTasksResolved, Dt as toNativeError, E as reconcileInitiativeStatus, Et as errorMessage, F as writeInitiativesManifest, G as saveInitiative, H as writePlansManifest, I as applyPlanUpsert, J as writeTasksJsonl, K as readTasksJsonl, L as isTerminalStatus, M as mutateInitiativesManifest, N as readInitiativesManifest, O as deferredTasks, P as upsertInitiativeEntry, Q as FileSystem, R as mutatePlansManifest, S as initiativeRollup, St as PlanWriteError, T as reconcileInitiativeForPlan, Tt as causeMessage, U as loadHandoff, V as upsertPlanEntry, W as saveHandoff, X as makePlanRuntime, Y as withFileLock, Z as makeRuntimeLayer, _ as applyInitiativeReconcile, _t as decodeTasksLine, at as PlanManifestEntrySchema, b as collectPlanDrift, bt as MissingMetaRecord, c as plans_exports, ct as TaskOriginSchema, d as setTaskStatus, dt as TasksLineSchema, et as nodeFileSystemService, f as nextTaskId, ft as decodeExecPendingConfig, g as resolvePlanByName, gt as decodeTaskRecord, h as normalizePlanName, ht as decodeTaskMeta, it as InitiativeStatusSchema, j as applyInitiativeUpsert, k as isPlanFinalizable, lt as TaskRecordSchema, m as loadPlanData, mt as decodePlanManifestEntry, nt as ExecPendingConfigSchema, ot as PlanStatusSchema, p as toKebabCase, pt as decodeInitiativeManifestEntry, q as updateTask, r as initiatives_exports, rt as InitiativeManifestEntrySchema, st as TaskMetaSchema, tt as writeFileAtomic, u as appendDeferredTask, ut as TaskStatusSchema, v as applyReconcile, vt as JsonlParseError, w as membersOf, wt as TasksFileNotFound, x as computePlanReadiness, xt as PlanReadError, y as collectInitiativeDrift, yt as JsonlValidationError, z as readPlansManifest } from "./initiatives-AtmFo2DU.mjs";
2
+ export { ExecPendingConfigSchema, FileSystem, initiatives_exports as InitiativeListing, InitiativeManifestEntrySchema, InitiativeStatusSchema, JsonlParseError, JsonlValidationError, MissingMetaRecord, plans_exports as PlanListing, PlanManifestEntrySchema, PlanReadError, PlanStatusSchema, PlanWriteError, TaskMetaSchema, TaskNotFound, TaskOriginSchema, TaskRecordSchema, TaskStatusSchema, TasksFileNotFound, TasksLineSchema, activeTasksResolved, appendDeferredTask, applyInitiativeReconcile, applyInitiativeUpsert, applyPlanUpsert, applyReconcile, causeMessage, collectInitiativeDrift, collectPlanDrift, computePlanReadiness, decodeExecPendingConfig, decodeInitiativeManifestEntry, decodePlanManifestEntry, decodeTaskMeta, decodeTaskRecord, decodeTasksLine, deferredTasks, errorMessage, initiativeRollup, isInitiativeFinalizable, isPlanFinalizable, isTerminalStatus, loadHandoff, loadPlanData, makeNodeFileSystemService, makePlanRuntime, makeRuntimeLayer, membersOf, mutateInitiativesManifest, mutatePlansManifest, nextTaskId, nodeFileSystemService, normalizePlanName, reactivateForExecution, readInitiativesManifest, readPlansManifest, readTasksJsonl, reconcileInitiativeForPlan, reconcileInitiativeStatus, reconcilePlanStatus, resolvePlanByName, saveHandoff, saveInitiative, setTaskStatus, toKebabCase, toNativeError, updateTask, upsertInitiativeEntry, upsertPlanEntry, withFileLock, writeFileAtomic, writeInitiativesManifest, writePlansManifest, writeTasksJsonl };
@@ -1,8 +1,8 @@
1
1
  import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
2
  import { Context, Data, Effect, Either, Layer, Option, Schema } from "effect";
3
3
  import { mkdir, open, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
4
+ import { dirname, join, resolve } from "node:path";
4
5
  import { createWriteStream } from "node:fs";
5
- import { dirname, join } from "node:path";
6
6
  import { randomUUID } from "node:crypto";
7
7
  //#region src/errors.ts
8
8
  /**
@@ -220,48 +220,67 @@ async function syncDirectory(dir) {
220
220
  * all failure modes typed (`PlanReadError` / `PlanWriteError`).
221
221
  */
222
222
  var FileSystem = class extends Context.Tag("PlanMode/FileSystem")() {};
223
- const nodeFileSystemService = {
224
- readFileString: (path) => Effect.tryPromise({
225
- try: () => readFile(path, "utf-8"),
226
- catch: (cause) => new PlanReadError({
227
- path,
228
- cause
229
- })
230
- }),
231
- writeFileString: (path, data) => Effect.tryPromise({
232
- try: () => writeFile(path, data, "utf-8"),
233
- catch: (cause) => new PlanWriteError({
234
- path,
235
- cause
236
- })
237
- }),
238
- writeFileAtomic: (path, data) => writeFileAtomic(path, data),
239
- makeDir: (path) => Effect.tryPromise({
240
- try: async () => {
241
- await mkdir(path, { recursive: true });
242
- },
243
- catch: (cause) => new PlanWriteError({
244
- path,
245
- cause
246
- })
247
- }),
248
- listDirectories: (path) => Effect.tryPromise({
249
- try: async () => {
250
- return (await readdir(path, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
251
- },
252
- catch: (cause) => new PlanReadError({
253
- path,
254
- cause
255
- })
256
- }),
257
- removeFile: (path) => Effect.tryPromise({
258
- try: () => unlink(path),
259
- catch: (cause) => new PlanWriteError({
260
- path,
261
- cause
223
+ /**
224
+ * Build a node-backed filesystem service whose relative paths resolve against
225
+ * `root`. All storage programs use relative `.plans/...` paths, so prefixing a
226
+ * root relocates the entire plan registry coherently (manifests, plan dirs,
227
+ * handoffs) to another working directory.
228
+ *
229
+ * `resolve(root, p)` is a no-op for already-absolute paths and reproduces the
230
+ * default `process.cwd()` behaviour exactly when `root === process.cwd()`, so
231
+ * the common (no-target) path is unchanged.
232
+ */
233
+ function makeNodeFileSystemService(root) {
234
+ const at = (path) => resolve(root, path);
235
+ return {
236
+ readFileString: (path) => Effect.tryPromise({
237
+ try: () => readFile(at(path), "utf-8"),
238
+ catch: (cause) => new PlanReadError({
239
+ path,
240
+ cause
241
+ })
242
+ }),
243
+ writeFileString: (path, data) => Effect.tryPromise({
244
+ try: () => writeFile(at(path), data, "utf-8"),
245
+ catch: (cause) => new PlanWriteError({
246
+ path,
247
+ cause
248
+ })
249
+ }),
250
+ writeFileAtomic: (path, data) => writeFileAtomic(at(path), data),
251
+ makeDir: (path) => Effect.tryPromise({
252
+ try: async () => {
253
+ await mkdir(at(path), { recursive: true });
254
+ },
255
+ catch: (cause) => new PlanWriteError({
256
+ path,
257
+ cause
258
+ })
259
+ }),
260
+ listDirectories: (path) => Effect.tryPromise({
261
+ try: async () => {
262
+ return (await readdir(at(path), { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
263
+ },
264
+ catch: (cause) => new PlanReadError({
265
+ path,
266
+ cause
267
+ })
268
+ }),
269
+ removeFile: (path) => Effect.tryPromise({
270
+ try: () => unlink(at(path)),
271
+ catch: (cause) => new PlanWriteError({
272
+ path,
273
+ cause
274
+ })
262
275
  })
263
- })
264
- };
276
+ };
277
+ }
278
+ /**
279
+ * Default service: relative paths resolve against the current working directory
280
+ * at call time. `resolve('.', p)` is equivalent to `resolve(process.cwd(), p)`,
281
+ * so this matches the historical behaviour of passing raw relative paths to fs.
282
+ */
283
+ const nodeFileSystemService = makeNodeFileSystemService(".");
265
284
  //#endregion
266
285
  //#region src/effects/runtime.ts
267
286
  /**
@@ -271,12 +290,21 @@ const nodeFileSystemService = {
271
290
  * through the `runPlanIO` bridge so the imperative pi event handlers keep their
272
291
  * `await fn(...)` shape.
273
292
  */
274
- function makeRuntimeLayer() {
275
- return Layer.succeed(FileSystem, nodeFileSystemService);
293
+ /**
294
+ * Build the live filesystem layer. Pass `root` to relocate the whole `.plans/`
295
+ * registry under another working directory; omit it for the default (relative
296
+ * paths resolve against the current working directory).
297
+ */
298
+ function makeRuntimeLayer(root) {
299
+ const service = root === void 0 ? nodeFileSystemService : makeNodeFileSystemService(root);
300
+ return Layer.succeed(FileSystem, service);
276
301
  }
277
- /** Build a bridge that runs storage programs against the live filesystem layer. */
278
- function makePlanRuntime() {
279
- const layer = makeRuntimeLayer();
302
+ /**
303
+ * Build a bridge that runs storage programs against the live filesystem layer.
304
+ * Pass `root` to target an external working directory's `.plans/` registry.
305
+ */
306
+ function makePlanRuntime(root) {
307
+ const layer = makeRuntimeLayer(root);
280
308
  return function runPlanIO(program) {
281
309
  return Effect.runPromise(program.pipe(Effect.provide(layer)));
282
310
  };
@@ -1250,4 +1278,4 @@ function parseInitiativeFilter(raw) {
1250
1278
  return "all";
1251
1279
  }
1252
1280
  //#endregion
1253
- export { nodeFileSystemService as $, reactivateForExecution as A, reconcilePlanStatus as B, isInitiativeFinalizable as C, TasksFileNotFound as Ct, activeTasksResolved as D, reconcileInitiativeStatus as E, toNativeError as Et, writeInitiativesManifest as F, saveInitiative as G, writePlansManifest as H, applyPlanUpsert as I, writeTasksJsonl as J, readTasksJsonl as K, isTerminalStatus$1 as L, mutateInitiativesManifest as M, readInitiativesManifest as N, deferredTasks as O, upsertInitiativeEntry as P, FileSystem as Q, mutatePlansManifest as R, initiativeRollup as S, TaskNotFound as St, reconcileInitiativeForPlan as T, errorMessage as Tt, loadHandoff as U, upsertPlanEntry as V, saveHandoff as W, makePlanRuntime as X, withFileLock as Y, makeRuntimeLayer as Z, applyInitiativeReconcile as _, JsonlParseError as _t, filterPlans as a, PlanStatusSchema as at, collectPlanDrift as b, PlanReadError as bt, plans_exports as c, TaskRecordSchema as ct, setTaskStatus as d, decodeExecPendingConfig as dt, writeFileAtomic as et, nextTaskId as f, decodeInitiativeManifestEntry as ft, resolvePlanByName as g, decodeTasksLine as gt, normalizePlanName as h, decodeTaskRecord as ht, loadInitiativeListItems as i, PlanManifestEntrySchema as it, applyInitiativeUpsert as j, isPlanFinalizable as k, sortPlans as l, TaskStatusSchema as lt, loadPlanData as m, decodeTaskMeta as mt, formatInitiativeList as n, InitiativeManifestEntrySchema as nt, formatPlanList as o, TaskMetaSchema as ot, toKebabCase as p, decodePlanManifestEntry as pt, updateTask as q, initiatives_exports as r, InitiativeStatusSchema as rt, loadPlanListItems as s, TaskOriginSchema as st, filterInitiatives as t, ExecPendingConfigSchema as tt, appendDeferredTask as u, TasksLineSchema as ut, applyReconcile as v, JsonlValidationError as vt, membersOf as w, causeMessage as wt, computePlanReadiness as x, PlanWriteError as xt, collectInitiativeDrift as y, MissingMetaRecord as yt, readPlansManifest as z };
1281
+ export { makeNodeFileSystemService as $, reactivateForExecution as A, reconcilePlanStatus as B, isInitiativeFinalizable as C, TaskNotFound as Ct, activeTasksResolved as D, toNativeError as Dt, reconcileInitiativeStatus as E, errorMessage as Et, writeInitiativesManifest as F, saveInitiative as G, writePlansManifest as H, applyPlanUpsert as I, writeTasksJsonl as J, readTasksJsonl as K, isTerminalStatus$1 as L, mutateInitiativesManifest as M, readInitiativesManifest as N, deferredTasks as O, upsertInitiativeEntry as P, FileSystem as Q, mutatePlansManifest as R, initiativeRollup as S, PlanWriteError as St, reconcileInitiativeForPlan as T, causeMessage as Tt, loadHandoff as U, upsertPlanEntry as V, saveHandoff as W, makePlanRuntime as X, withFileLock as Y, makeRuntimeLayer as Z, applyInitiativeReconcile as _, decodeTasksLine as _t, filterPlans as a, PlanManifestEntrySchema as at, collectPlanDrift as b, MissingMetaRecord as bt, plans_exports as c, TaskOriginSchema as ct, setTaskStatus as d, TasksLineSchema as dt, nodeFileSystemService as et, nextTaskId as f, decodeExecPendingConfig as ft, resolvePlanByName as g, decodeTaskRecord as gt, normalizePlanName as h, decodeTaskMeta as ht, loadInitiativeListItems as i, InitiativeStatusSchema as it, applyInitiativeUpsert as j, isPlanFinalizable as k, sortPlans as l, TaskRecordSchema as lt, loadPlanData as m, decodePlanManifestEntry as mt, formatInitiativeList as n, ExecPendingConfigSchema as nt, formatPlanList as o, PlanStatusSchema as ot, toKebabCase as p, decodeInitiativeManifestEntry as pt, updateTask as q, initiatives_exports as r, InitiativeManifestEntrySchema as rt, loadPlanListItems as s, TaskMetaSchema as st, filterInitiatives as t, writeFileAtomic as tt, appendDeferredTask as u, TaskStatusSchema as ut, applyReconcile as v, JsonlParseError as vt, membersOf as w, TasksFileNotFound as wt, computePlanReadiness as x, PlanReadError as xt, collectInitiativeDrift as y, JsonlValidationError as yt, readPlansManifest as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreki-gg/taskman",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
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",
@@ -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 (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.
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.2.1"
12
+ library_version: "0.4.0"
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`: