@docyrus/docyrus 0.0.45 → 0.0.48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docyrus/docyrus",
3
- "version": "0.0.45",
3
+ "version": "0.0.48",
4
4
  "private": false,
5
5
  "description": "Docyrus API CLI",
6
6
  "main": "./main.js",
@@ -1,7 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { dirname, join, relative, resolve as resolvePath } from "node:path";
4
- import { Type } from "@sinclair/typebox";
5
4
  import type {
6
5
  ExtensionAPI,
7
6
  ExtensionCommandContext,
@@ -10,17 +9,14 @@ import type {
10
9
  TurnEndEvent,
11
10
  } from "@mariozechner/pi-coding-agent";
12
11
  import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
13
- import type { Theme } from "@mariozechner/pi-tui";
14
12
  import { Box, Markdown, Text } from "@mariozechner/pi-tui";
15
13
 
16
- const PREVIEW_LINES = 4;
17
14
  const KNOWLEDGE_STATE_TYPE = "knowledge-session";
18
15
  const KNOWLEDGE_WIDGET_KEY = "knowledge";
19
16
  const KNOWLEDGE_REMINDER_INTERVAL_MS = 10 * 60 * 1000;
20
17
 
21
18
  type Scope = "local" | "global";
22
19
  type KnowledgeSeverity = "ok" | "watch" | "drift" | "critical";
23
- type KnowledgeToolName = "read" | "write" | "edit";
24
20
 
25
21
  interface ICliEnvironment {
26
22
  executable: string;
@@ -156,34 +152,6 @@ async function runCliJson<TValue>(environment: ICliEnvironment, args: string[],
156
152
  };
157
153
  }
158
154
 
159
- function collapsibleResult(
160
- result: { content: Array<{ type: string; text?: string }> },
161
- options: { expanded: boolean; isPartial: boolean },
162
- theme: Theme,
163
- ) {
164
- const text = result.content?.[0]?.type === "text" ? (result.content[0] as { type: "text"; text: string }).text : "";
165
- if (!text) {
166
- return new Text(theme.fg("dim", "(empty)"), 0, 0);
167
- }
168
- if (options.isPartial) {
169
- return new Text(theme.fg("dim", "…"), 0, 0);
170
- }
171
-
172
- const markdownTheme = getMarkdownTheme();
173
- if (options.expanded) {
174
- return new Markdown(text, 0, 0, markdownTheme);
175
- }
176
-
177
- const lines = text.split("\n");
178
- if (lines.length <= PREVIEW_LINES) {
179
- return new Markdown(text, 0, 0, markdownTheme);
180
- }
181
-
182
- const preview = lines.slice(0, PREVIEW_LINES).join("\n");
183
- const remaining = lines.length - PREVIEW_LINES;
184
- const hint = keyHint("expandTools", "to expand");
185
- return new Text(`${preview}\n${theme.fg("dim", `… ${remaining} more lines (${hint})`)}`, 0, 0);
186
- }
187
155
 
188
156
  function normalizeTrackedPath(pathValue: string, cwd: string): string {
189
157
  const trimmed = pathValue.trim();
@@ -419,36 +387,6 @@ function trackToolResult(pi: ExtensionAPI, ctx: ExtensionContext, event: ToolRes
419
387
  updateKnowledgeWidget(ctx, next);
420
388
  }
421
389
 
422
- function registerKnowledgeTool(
423
- pi: ExtensionAPI,
424
- name: string,
425
- label: string,
426
- description: string,
427
- argsBuilder: (params: Record<string, unknown>) => string[],
428
- schema: object,
429
- preview: (args: Record<string, unknown>, theme: Theme) => string,
430
- ) {
431
- pi.registerTool({
432
- name,
433
- label,
434
- description,
435
- promptSnippet: description,
436
- parameters: schema,
437
- async execute(_id, params) {
438
- const environment = readCliEnvironment();
439
- const result = await runCli(environment, argsBuilder(params as Record<string, unknown>), process.cwd(), false);
440
- const text = result.stdout.trim() || summarizeFailure(result);
441
- return {
442
- content: [{ type: "text", text }],
443
- ...(result.code && result.code !== 0 ? { isError: true } : {}),
444
- };
445
- },
446
- renderCall(args, theme) {
447
- return new Text(preview(args as Record<string, unknown>, theme), 0, 0);
448
- },
449
- renderResult: collapsibleResult,
450
- });
451
- }
452
390
 
453
391
  function parseCommandArgs(rawArgs: string): string | null {
454
392
  const trimmed = rawArgs.trim();
@@ -456,97 +394,6 @@ function parseCommandArgs(rawArgs: string): string | null {
456
394
  }
457
395
 
458
396
  export default function(pi: ExtensionAPI) {
459
- registerKnowledgeTool(
460
- pi,
461
- "docyrus_knowledge_search",
462
- "knowledge search",
463
- "Semantic search across docyrus/knowledge sections",
464
- (params) => {
465
- const args = ["knowledge", "search"];
466
- if (typeof params.query === "string" && params.query.trim().length > 0) {
467
- args.push(params.query);
468
- }
469
- if (typeof params.limit === "number") {
470
- args.push("--limit", String(params.limit));
471
- }
472
- return args;
473
- },
474
- Type.Object({
475
- query: Type.String({ description: "Search query in natural language" }),
476
- limit: Type.Optional(Type.Number({ description: "Maximum matches", default: 5 })),
477
- }),
478
- (args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge search "))}${theme.fg("dim", `"${String(args.query || "")}"`)}`,
479
- );
480
-
481
- registerKnowledgeTool(
482
- pi,
483
- "docyrus_knowledge_section",
484
- "knowledge section",
485
- "Show a full knowledge section with refs and backlinks",
486
- (params) => ["knowledge", "section", String(params.query || "")],
487
- Type.Object({
488
- query: Type.String({ description: "Section id or heading query" }),
489
- }),
490
- (args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge section "))}${theme.fg("dim", `"${String(args.query || "")}"`)}`,
491
- );
492
-
493
- registerKnowledgeTool(
494
- pi,
495
- "docyrus_knowledge_locate",
496
- "knowledge locate",
497
- "Find knowledge sections by exact, short, or fuzzy match",
498
- (params) => ["knowledge", "locate", String(params.query || "")],
499
- Type.Object({
500
- query: Type.String({ description: "Section id or heading query" }),
501
- }),
502
- (args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge locate "))}${theme.fg("dim", `"${String(args.query || "")}"`)}`,
503
- );
504
-
505
- registerKnowledgeTool(
506
- pi,
507
- "docyrus_knowledge_refs",
508
- "knowledge refs",
509
- "Find markdown and code references to a knowledge section or source symbol",
510
- (params) => {
511
- const args = ["knowledge", "refs", String(params.query || "")];
512
- if (typeof params.scope === "string" && params.scope.length > 0) {
513
- args.push("--scope", params.scope);
514
- }
515
- return args;
516
- },
517
- Type.Object({
518
- query: Type.String({ description: "Section id, source file, or source symbol query" }),
519
- scope: Type.Optional(Type.String({ description: "md, code, or md+code" })),
520
- }),
521
- (args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge refs "))}${theme.fg("dim", `"${String(args.query || "")}"`)}`,
522
- );
523
-
524
- registerKnowledgeTool(
525
- pi,
526
- "docyrus_knowledge_expand",
527
- "knowledge expand",
528
- "Expand [[refs]] into resolved section context",
529
- (params) => ["knowledge", "expand", String(params.text || "")],
530
- Type.Object({
531
- text: Type.String({ description: "Text containing [[refs]]" }),
532
- }),
533
- (args, theme) => {
534
- const text = String(args.text || "");
535
- const preview = text.length > 60 ? `${text.slice(0, 60)}…` : text;
536
- return `${theme.fg("toolTitle", theme.bold("knowledge expand "))}${theme.fg("dim", `"${preview}"`)}`;
537
- },
538
- );
539
-
540
- registerKnowledgeTool(
541
- pi,
542
- "docyrus_knowledge_check",
543
- "knowledge check",
544
- "Validate the repo knowledge graph",
545
- () => ["knowledge", "check"],
546
- Type.Object({}),
547
- (_args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge check"))}`,
548
- );
549
-
550
397
  pi.registerCommand("knowledge-init", {
551
398
  description: "Bootstrap docyrus/knowledge from the current repo and install hook guidance",
552
399
  handler: async(rawArgs, ctx: ExtensionCommandContext) => {
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Server Auto-Commit Extension (server-only)
3
+ *
4
+ * Automatically commits and pushes all changes after each agent turn
5
+ * when auto-commit is enabled for the session.
6
+ *
7
+ * Enable at session creation:
8
+ * POST /api/sessions { "autoCommit": true }
9
+ *
10
+ * Toggle on an active session:
11
+ * PATCH /api/sessions/:sessionId/config { "autoCommit": true|false }
12
+ *
13
+ * Check current config:
14
+ * GET /api/sessions/:sessionId/config
15
+ *
16
+ * Config is stored at: <agentDir>/session-config/<sessionId>.json
17
+ */
18
+
19
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
20
+ import { execFile as execFileCb } from "node:child_process";
21
+ import { promises as fs } from "node:fs";
22
+ import * as os from "node:os";
23
+ import * as path from "node:path";
24
+ import { promisify } from "node:util";
25
+
26
+ const execFile = promisify(execFileCb);
27
+
28
+ interface IExecResult {
29
+ stdout: string;
30
+ stderr: string;
31
+ code: number;
32
+ }
33
+
34
+ async function runGit(args: string[], cwd: string): Promise<IExecResult> {
35
+ try {
36
+ const result = await execFile("git", args, { cwd });
37
+ return { stdout: result.stdout, stderr: result.stderr, code: 0 };
38
+ } catch (error: unknown) {
39
+ const err = error as { stdout?: string; stderr?: string; code?: number };
40
+ return {
41
+ stdout: err.stdout ?? "",
42
+ stderr: err.stderr ?? "",
43
+ code: err.code ?? 1,
44
+ };
45
+ }
46
+ }
47
+
48
+ function expandUserPath(p: string): string {
49
+ if (p === "~") {return os.homedir();}
50
+ if (p.startsWith("~/")) {return path.join(os.homedir(), p.slice(2));}
51
+ return p;
52
+ }
53
+
54
+ function getScopedAgentDir(): string {
55
+ for (const key of ["PI_CODING_AGENT_DIR", "TAU_CODING_AGENT_DIR"]) {
56
+ const value = process.env[key]?.trim();
57
+ if (value) {return expandUserPath(value);}
58
+ }
59
+ return path.join(os.homedir(), ".pi", "agent");
60
+ }
61
+
62
+ async function isAutoCommitEnabled(sessionId: string): Promise<boolean> {
63
+ try {
64
+ const configFile = path.join(getScopedAgentDir(), "session-config", `${sessionId}.json`);
65
+ const raw = await fs.readFile(configFile, "utf-8");
66
+ return (JSON.parse(raw) as { autoCommit?: boolean }).autoCommit === true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ export default function(_pi: ExtensionAPI) {
73
+ _pi.on("agent_end", async(_event, ctx) => {
74
+ const sessionId = ctx.sessionManager.getSessionId();
75
+ if (!(await isAutoCommitEnabled(sessionId))) {return;}
76
+
77
+ const status = await runGit(["status", "--porcelain"], ctx.cwd);
78
+ if (status.code !== 0 || !status.stdout.trim()) {return;}
79
+
80
+ const timestamp = new Date().toISOString();
81
+
82
+ const add = await runGit(["add", "-A"], ctx.cwd);
83
+ if (add.code !== 0) {
84
+ process.stderr.write(`[server-auto-commit] git add failed: ${add.stderr}\n`);
85
+ return;
86
+ }
87
+
88
+ const commit = await runGit(
89
+ ["commit", "--no-verify", "-m", `auto: agent turn ${timestamp}`],
90
+ ctx.cwd,
91
+ );
92
+ if (commit.code !== 0) {
93
+ process.stderr.write(`[server-auto-commit] git commit failed: ${commit.stderr}\n`);
94
+ return;
95
+ }
96
+
97
+ const push = await runGit(["push", "--no-verify"], ctx.cwd);
98
+ if (push.code !== 0) {
99
+ process.stderr.write(`[server-auto-commit] git push failed: ${push.stderr}\n`);
100
+ return;
101
+ }
102
+
103
+ process.stderr.write(`[server-auto-commit] committed and pushed at ${timestamp}\n`);
104
+ });
105
+ }
@@ -1,21 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
- import { StringEnum } from "@mariozechner/pi-ai";
3
- import { Type } from "@sinclair/typebox";
4
2
  import type {
5
3
  ExtensionAPI,
6
4
  ExtensionCommandContext,
7
5
  } from "@mariozechner/pi-coding-agent";
8
- import type { Theme } from "@mariozechner/pi-tui";
9
- import { Text } from "@mariozechner/pi-tui";
10
-
11
- type ITasksAction =
12
- | "show"
13
- | "get"
14
- | "create-section"
15
- | "create-feature"
16
- | "create-task"
17
- | "set-status"
18
- | "create-linked-todo";
19
6
 
20
7
  interface ICliEnvironment {
21
8
  executable: string;
@@ -70,28 +57,6 @@ interface IProjectPlanShowPayload {
70
57
  };
71
58
  }
72
59
 
73
- const TaskParams = Type.Object({
74
- action: StringEnum([
75
- "show",
76
- "get",
77
- "create-section",
78
- "create-feature",
79
- "create-task",
80
- "set-status",
81
- "create-linked-todo",
82
- ] as const),
83
- taskId: Type.Optional(Type.String({ description: "Canonical task id" })),
84
- featureId: Type.Optional(Type.String({ description: "Canonical feature id" })),
85
- sectionId: Type.Optional(Type.String({ description: "Project plan section id" })),
86
- title: Type.Optional(Type.String({ description: "Section, feature, or task title" })),
87
- summary: Type.Optional(Type.String({ description: "Section, feature, or task summary" })),
88
- slug: Type.Optional(Type.String({ description: "Section or feature slug" })),
89
- type: Type.Optional(Type.String({ description: "Task type" })),
90
- assignee: Type.Optional(Type.String({ description: "Task assignee" })),
91
- status: Type.Optional(Type.String({ description: "Task status" })),
92
- acceptanceCriteria: Type.Optional(Type.Array(Type.String({ description: "Acceptance criterion" }))),
93
- });
94
-
95
60
  function readCliEnvironment(env: NodeJS.ProcessEnv = process.env): ICliEnvironment {
96
61
  const executable = env.DOCYRUS_CLI_EXECUTABLE?.trim();
97
62
  const entryPath = env.DOCYRUS_CLI_ENTRY?.trim();
@@ -326,197 +291,6 @@ async function tasksCommandHandler(pi: ExtensionAPI, ctx: ExtensionCommandContex
326
291
  }
327
292
 
328
293
  export default function tasksExtension(pi: ExtensionAPI) {
329
- pi.registerTool({
330
- name: "project_task",
331
- label: "Project Task",
332
- description: "Manage the canonical repo-tracked project-plan graph and linked local subtasks.",
333
- parameters: TaskParams,
334
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
335
- const environment = readCliEnvironment();
336
- const action = params.action as ITasksAction;
337
-
338
- switch (action) {
339
- case "show": {
340
- const payload = await runCliJson<IProjectPlanShowPayload>(environment, ["project-plan", "show"], ctx.cwd);
341
- const text = formatHierarchySummary(payload);
342
- return {
343
- content: [{ type: "text", text }],
344
- details: payload,
345
- };
346
- }
347
-
348
- case "get": {
349
- if (!params.taskId) {
350
- return {
351
- content: [{ type: "text", text: "taskId required" }],
352
- isError: true,
353
- };
354
- }
355
- const payload = await runCliJson<Record<string, unknown>>(environment, [
356
- "project-plan",
357
- "get-task",
358
- "--taskId",
359
- String(params.taskId),
360
- ], ctx.cwd);
361
- return {
362
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
363
- details: payload,
364
- };
365
- }
366
-
367
- case "create-section": {
368
- if (!params.title) {
369
- return {
370
- content: [{ type: "text", text: "title required" }],
371
- isError: true,
372
- };
373
- }
374
- const args = [
375
- "project-plan",
376
- "upsert-section",
377
- "--title",
378
- String(params.title),
379
- ];
380
- if (params.slug) {
381
- args.push("--slug", String(params.slug));
382
- }
383
- if (params.summary) {
384
- args.push("--summary", String(params.summary));
385
- }
386
- const payload = await runCliJson<Record<string, unknown>>(environment, args, ctx.cwd);
387
- return {
388
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
389
- details: payload,
390
- };
391
- }
392
-
393
- case "create-feature": {
394
- if (!params.sectionId || !params.title) {
395
- return {
396
- content: [{ type: "text", text: "sectionId and title required" }],
397
- isError: true,
398
- };
399
- }
400
- const args = [
401
- "project-plan",
402
- "upsert-feature",
403
- "--sectionId",
404
- String(params.sectionId),
405
- "--title",
406
- String(params.title),
407
- ];
408
- if (params.slug) {
409
- args.push("--slug", String(params.slug));
410
- }
411
- if (params.summary) {
412
- args.push("--summary", String(params.summary));
413
- }
414
- const payload = await runCliJson<Record<string, unknown>>(environment, args, ctx.cwd);
415
- return {
416
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
417
- details: payload,
418
- };
419
- }
420
-
421
- case "create-task": {
422
- if (!params.featureId || !params.title || !params.type || !params.assignee) {
423
- return {
424
- content: [{ type: "text", text: "featureId, title, type, and assignee required" }],
425
- isError: true,
426
- };
427
- }
428
- const args = [
429
- "project-plan",
430
- "upsert-task",
431
- "--featureId",
432
- String(params.featureId),
433
- "--title",
434
- String(params.title),
435
- "--type",
436
- String(params.type),
437
- "--assignee",
438
- String(params.assignee),
439
- ];
440
- if (params.sectionId) {
441
- args.push("--sectionId", String(params.sectionId));
442
- }
443
- if (params.summary) {
444
- args.push("--summary", String(params.summary));
445
- }
446
- if (params.status) {
447
- args.push("--status", String(params.status));
448
- }
449
- if (Array.isArray(params.acceptanceCriteria) && params.acceptanceCriteria.length > 0) {
450
- args.push("--acceptanceCriteria", JSON.stringify(params.acceptanceCriteria));
451
- }
452
- const payload = await runCliJson<Record<string, unknown>>(environment, args, ctx.cwd);
453
- return {
454
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
455
- details: payload,
456
- };
457
- }
458
-
459
- case "set-status": {
460
- if (!params.taskId || !params.status) {
461
- return {
462
- content: [{ type: "text", text: "taskId and status required" }],
463
- isError: true,
464
- };
465
- }
466
- const payload = await runCliJson<Record<string, unknown>>(environment, [
467
- "project-plan",
468
- "set-task-status",
469
- "--taskId",
470
- String(params.taskId),
471
- "--status",
472
- String(params.status),
473
- ], ctx.cwd);
474
- return {
475
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
476
- details: payload,
477
- };
478
- }
479
-
480
- case "create-linked-todo": {
481
- if (!params.taskId) {
482
- return {
483
- content: [{ type: "text", text: "taskId required" }],
484
- isError: true,
485
- };
486
- }
487
- const args = [
488
- "project-plan",
489
- "create-linked-todo",
490
- "--taskId",
491
- String(params.taskId),
492
- ];
493
- if (params.title) {
494
- args.push("--title", String(params.title));
495
- }
496
- if (params.summary) {
497
- args.push("--body", String(params.summary));
498
- }
499
- const payload = await runCliJson<Record<string, unknown>>(environment, args, ctx.cwd);
500
- return {
501
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
502
- details: payload,
503
- };
504
- }
505
- }
506
- },
507
- renderCall(args, theme: Theme) {
508
- return new Text(
509
- `${theme.fg("toolTitle", theme.bold("project task "))}${theme.fg("muted", String(args.action || "show"))}`,
510
- 0,
511
- 0,
512
- );
513
- },
514
- renderResult(result, _options, _theme) {
515
- const text = result.content[0];
516
- return new Text(text?.type === "text" ? text.text : "", 0, 0);
517
- },
518
- });
519
-
520
294
  pi.registerCommand("tasks", {
521
295
  description: "Browse project-plan sections, features, and tasks",
522
296
  handler: async(args, ctx) => {
@@ -27,13 +27,18 @@ Docyrus concepts you should understand and use accurately:
27
27
 
28
28
  Project plan system:
29
29
 
30
- The project-plan is a repo-tracked work graph stored at `docyrus/project-plan/project-plan.json`. It organizes work into sections (standalone groupings like phases or feature areas), features, and tasks. A derived `PROJECT_PLAN.md` is always kept in sync. Features are also synced into the knowledge base features document when it exists.
30
+ The project-plan is a repo-tracked work graph stored at `docyrus/project-plan/project-plan.json`. It organizes work into sections (standalone groupings like phases or feature areas), features, and tasks. A derived `PROJECT_PLAN.md` is always kept in sync. Features are also synced into the knowledge base features document when it exists. Sections, features, and tasks each support an optional integer `order` field that controls display sequence (lower = first; entities without an order sort after ordered ones).
31
31
 
32
- - `docyrus project-plan show` — view the full hierarchy with statuses
33
- - `docyrus project-plan get-task --taskId <id>` — inspect a task and its linked local subtasks
32
+ - `docyrus project-plan show --json` — view the full hierarchy with statuses; returns `{ graph, hierarchy }`
33
+ - `docyrus project-plan get-task --taskId <id> --json` — inspect a task and its linked local subtasks
34
34
  - `docyrus project-plan set-task-status --taskId <id> --status <status>` — advance task status (`planned` → `in_progress` → `done`, or `blocked`)
35
- - `docyrus project-plan upsert-section --title <title>` — create or update a section
36
- - `docyrus project-plan check` validate that all section references and task fields are consistent
35
+ - `docyrus project-plan create-linked-todo --taskId <id> [--title <title>] [--body <body>]` — create a local `.pi/todos` subtask linked to an agent-assigned canonical task
36
+ - `docyrus project-plan upsert-section --title <title> [--id <id>] [--slug <slug>] [--summary <summary>] [--order <n>]` — create or update a section
37
+ - `docyrus project-plan upsert-feature --sectionId <id> --title <title> [--featureId <id>] [--slug <slug>] [--summary <summary>] [--order <n>]` — create or update a feature
38
+ - `docyrus project-plan upsert-task --featureId <id> --title <title> --type <type> --assignee <assignee> [--taskId <id>] [--status <status>] [--summary <summary>] [--acceptanceCriteria <json-array>] [--order <n>]` — create or update a task
39
+ - `docyrus project-plan set-order --sectionId|--featureId|--taskId <id> --order <n>` — set display order on an existing entity (lower = first; unordered items sort last)
40
+ - `docyrus project-plan check` — validate section references and graph integrity
41
+ - `docyrus project-plan ensure` — initialize an empty project-plan graph if it does not yet exist
37
42
 
38
43
  When working on a repo that has a project plan, read it at the start of a session to understand scope and priorities. Update task status as work progresses and after it completes.
39
44
 
@@ -72,17 +72,18 @@ Schema-first workflow for new Docyrus-backed apps and major features:
72
72
 
73
73
  Project plan system:
74
74
 
75
- The project-plan is a repo-tracked work graph stored at `docyrus/project-plan/project-plan.json` with a derived `PROJECT_PLAN.md` always kept in sync. Work is organized into sections (standalone groupings like phases or feature areas), features, and tasks. Features are also synced into the knowledge base features document when it exists. Tasks have a type (`new-implementation`, `bug-fix`, `api-test`, `browser-automation-test`, `work`), an assignee (`agent` or `user`), a status (`planned`, `in_progress`, `blocked`, `done`), and optional acceptance criteria.
75
+ The project-plan is a repo-tracked work graph stored at `docyrus/project-plan/project-plan.json` with a derived `PROJECT_PLAN.md` always kept in sync. Work is organized into sections (standalone groupings like phases or feature areas), features, and tasks. Features are also synced into the knowledge base features document when it exists. Tasks have a type (`new-implementation`, `bug-fix`, `api-test`, `browser-automation-test`, `work`), an assignee (`agent` or `user`), a status (`planned`, `in_progress`, `blocked`, `done`), and optional acceptance criteria. Sections, features, and tasks each support an optional integer `order` field that controls display sequence (lower = first; entities without an order sort after ordered ones).
76
76
 
77
77
  Key commands:
78
78
 
79
- - `docyrus project-plan show` — view the full hierarchy with feature and task statuses
80
- - `docyrus project-plan get-task --taskId <id>` — inspect a specific task and its linked local subtasks
81
- - `docyrus project-plan set-task-status --taskId <id> --status <status>` — advance task status
82
- - `docyrus project-plan create-linked-todo --taskId <id> --title <title> --body <body>` — create a local `.pi/todos` subtask linked to an agent-assigned canonical task
83
- - `docyrus project-plan upsert-section --title <title> --slug <slug> --summary <summary>` — create or update a section
84
- - `docyrus project-plan upsert-feature --sectionId <id> --title <title> --slug <slug>` — create or update a feature
85
- - `docyrus project-plan upsert-task --featureId <id> --title <title> --type <type> --assignee <assignee>` — create or update a task
79
+ - `docyrus project-plan show --json` — view the full hierarchy with feature and task statuses; returns `{ graph, hierarchy }`
80
+ - `docyrus project-plan get-task --taskId <id> --json` — inspect a specific task and its linked local subtasks
81
+ - `docyrus project-plan set-task-status --taskId <id> --status <status>` — advance task status (`planned` → `in_progress` → `done`, or `blocked`)
82
+ - `docyrus project-plan create-linked-todo --taskId <id> [--title <title>] [--body <body>]` — create a local `.pi/todos` subtask linked to an agent-assigned canonical task
83
+ - `docyrus project-plan upsert-section --title <title> [--id <id>] [--slug <slug>] [--summary <summary>] [--order <n>]` — create or update a section
84
+ - `docyrus project-plan upsert-feature --sectionId <id> --title <title> [--featureId <id>] [--slug <slug>] [--summary <summary>] [--order <n>]` — create or update a feature
85
+ - `docyrus project-plan upsert-task --featureId <id> --title <title> --type <type> --assignee <assignee> [--taskId <id>] [--status <status>] [--summary <summary>] [--acceptanceCriteria <json-array>] [--order <n>]` — create or update a task
86
+ - `docyrus project-plan set-order --sectionId|--featureId|--taskId <id> --order <n>` — set display order on an existing entity (lower = first; unordered items sort last)
86
87
  - `docyrus project-plan check` — validate section references and graph integrity
87
88
  - `docyrus project-plan ensure` — initialize an empty project-plan graph if it does not yet exist
88
89