@docyrus/docyrus 0.0.42 → 0.0.44

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.42",
3
+ "version": "0.0.44",
4
4
  "private": false,
5
5
  "description": "Docyrus API CLI",
6
6
  "main": "./main.js",
@@ -746,8 +746,8 @@ async function architectHandler(pi: ExtensionAPI, ctx: ExtensionCommandContext,
746
746
  }
747
747
 
748
748
  export default function architectExtension(pi: ExtensionAPI) {
749
- pi.registerCommand("architect", {
750
- description: "Analyze tenant data sources and plan Docyrus schema artifacts for an app idea",
749
+ pi.registerCommand("plan", {
750
+ description: "Discover tenant state and start a planning-only branch for an app idea or task",
751
751
  handler: async(args, ctx) => {
752
752
  await architectHandler(pi, ctx, args);
753
753
  },
@@ -2,16 +2,6 @@
2
2
  "name": "pi-mcp-adapter",
3
3
  "version": "2.2.0",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
- "type": "module",
6
- "license": "MIT",
7
- "author": "Nico Bailon",
8
- "bin": {
9
- "pi-mcp-adapter": "./cli.js"
10
- },
11
- "repository": {
12
- "type": "git",
13
- "url": "https://github.com/nicobailon/pi-mcp-adapter"
14
- },
15
5
  "keywords": [
16
6
  "pi-package",
17
7
  "pi",
@@ -23,16 +13,15 @@
23
13
  "claude",
24
14
  "llm"
25
15
  ],
26
- "scripts": {
27
- "test": "vitest run",
28
- "test:watch": "vitest",
29
- "test:coverage": "vitest run --coverage"
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/nicobailon/pi-mcp-adapter"
30
19
  },
31
- "pi": {
32
- "extensions": [
33
- "./index.ts"
34
- ],
35
- "video": "https://github.com/nicobailon/pi-mcp-adapter/raw/refs/heads/main/pi-mcp.mp4"
20
+ "license": "MIT",
21
+ "author": "Nico Bailon",
22
+ "type": "module",
23
+ "bin": {
24
+ "pi-mcp-adapter": "./cli.js"
36
25
  },
37
26
  "files": [
38
27
  "cli.js",
@@ -68,18 +57,29 @@
68
57
  "CHANGELOG.md",
69
58
  "LICENSE"
70
59
  ],
60
+ "scripts": {
61
+ "test": "vitest run",
62
+ "test:coverage": "vitest run --coverage",
63
+ "test:watch": "vitest"
64
+ },
71
65
  "dependencies": {
72
66
  "@modelcontextprotocol/ext-apps": "^1.2.2",
73
67
  "@modelcontextprotocol/sdk": "^1.25.1",
74
68
  "@sinclair/typebox": "^0.32.0",
75
69
  "zod": "^3.25.0 || ^4.0.0"
76
70
  },
77
- "peerDependencies": {
78
- "zod": "^3.25.0 || ^4.0.0"
79
- },
80
71
  "devDependencies": {
81
72
  "@types/node": "^20.0.0",
82
73
  "typescript": "^5.0.0",
83
74
  "vitest": "^3.0.0"
75
+ },
76
+ "peerDependencies": {
77
+ "zod": "^3.25.0 || ^4.0.0"
78
+ },
79
+ "pi": {
80
+ "extensions": [
81
+ "./index.ts"
82
+ ],
83
+ "video": "https://github.com/nicobailon/pi-mcp-adapter/raw/refs/heads/main/pi-mcp.mp4"
84
84
  }
85
85
  }
@@ -511,7 +511,7 @@ function buildPlanPromptOverlay(state: IPlanSessionState): string {
511
511
  const task = state.task?.trim() || "(unknown task)";
512
512
  const model = state.planModel ? `Planning model: ${state.planModel}` : "Planning model: current session model";
513
513
  const artifact = state.artifactPath ? `Artifact path: ${state.artifactPath}` : "Artifact path: unavailable";
514
- const modeLabel = state.mode === "architect" ? "Docyrus `/architect` planning mode." : "Docyrus `/plan` mode.";
514
+ const modeLabel = "Docyrus `/plan` mode.";
515
515
  return [
516
516
  modeLabel,
517
517
  PLAN_MODE_SYSTEM_PROMPT,
@@ -735,7 +735,115 @@ export function isPotentiallyMutatingShellCommand(command: string): boolean {
735
735
  }
736
736
 
737
737
  function buildBlockedToolReason(detail: string): string {
738
- return `A planning session is active. ${detail} Exit with /end-plan or /end-architect before implementing.`;
738
+ return `A planning session is active. ${detail} Exit with /end-plan before implementing.`;
739
+ }
740
+
741
+ const READONLY_STATE_TYPE = "readonly-session";
742
+ const READONLY_WIDGET_KEY = "readonly-mode";
743
+
744
+ interface IReadOnlySessionState {
745
+ active: boolean;
746
+ startedAt?: string;
747
+ }
748
+
749
+ let currentReadOnlyState: IReadOnlySessionState | undefined;
750
+
751
+ const READONLY_MODE_SYSTEM_PROMPT = [
752
+ "You are in Docyrus `/read-only` mode.",
753
+ "",
754
+ "You MUST NOT modify any files or remote state.",
755
+ "You can read files, search code, analyze, summarize, and answer questions.",
756
+ "Write and edit tools are disabled.",
757
+ "",
758
+ "Exit read-only mode with /end-read-only to resume normal operation.",
759
+ ].join("\n");
760
+
761
+ function getReadOnlyState(ctx: ExtensionContext): IReadOnlySessionState | undefined {
762
+ let state: IReadOnlySessionState | undefined;
763
+ for (const entry of ctx.sessionManager.getBranch()) {
764
+ if (entry.type === "custom" && entry.customType === READONLY_STATE_TYPE) {
765
+ state = entry.data as IReadOnlySessionState | undefined;
766
+ }
767
+ }
768
+
769
+ if (state?.active) {
770
+ return state;
771
+ }
772
+
773
+ return undefined;
774
+ }
775
+
776
+ function setReadOnlyWidget(ctx: ExtensionContext, state: IReadOnlySessionState | undefined): void {
777
+ if (!ctx.hasUI) {
778
+ return;
779
+ }
780
+
781
+ if (!state?.active) {
782
+ ctx.ui.setWidget(READONLY_WIDGET_KEY, undefined);
783
+ return;
784
+ }
785
+
786
+ ctx.ui.setWidget(READONLY_WIDGET_KEY, (_tui, theme) => {
787
+ const lines = [
788
+ theme.fg("accent", theme.bold("Read-only mode active")),
789
+ theme.fg("muted", "Write & edit tools disabled"),
790
+ theme.fg("warning", "Exit with /end-read-only"),
791
+ ];
792
+
793
+ const container = new Container();
794
+ container.addChild(new DynamicBorder((value: string) => theme.fg("accent", value)));
795
+ container.addChild(new Text(lines.join("\n"), 0, 0));
796
+ return container;
797
+ });
798
+ }
799
+
800
+ function buildBlockedReadOnlyReason(detail: string): string {
801
+ return `Read-only mode is active. ${detail} Exit with /end-read-only to resume editing.`;
802
+ }
803
+
804
+ function handleToolCallDuringReadOnly(event: ToolCallEvent, ctx: ExtensionContext): { block: boolean; reason: string } | undefined {
805
+ if (!getReadOnlyState(ctx)?.active) {
806
+ return undefined;
807
+ }
808
+
809
+ if (event.toolName === "edit" || event.toolName === "write") {
810
+ return {
811
+ block: true,
812
+ reason: buildBlockedReadOnlyReason(`The ${event.toolName} tool is disabled in read-only mode.`),
813
+ };
814
+ }
815
+
816
+ if (event.toolName === "bash" && isRecord(event.input) && typeof event.input.command === "string") {
817
+ if (isPotentiallyMutatingShellCommand(event.input.command)) {
818
+ return {
819
+ block: true,
820
+ reason: buildBlockedReadOnlyReason(`Mutating bash commands are disabled in read-only mode (${event.input.command}).`),
821
+ };
822
+ }
823
+ }
824
+
825
+ if (event.toolName === "todo" && isRecord(event.input) && typeof event.input.action === "string") {
826
+ if (shouldBlockTodoAction(event.input.action)) {
827
+ return {
828
+ block: true,
829
+ reason: buildBlockedReadOnlyReason(`Todo action "${event.input.action}" is disabled in read-only mode.`),
830
+ };
831
+ }
832
+ }
833
+
834
+ return undefined;
835
+ }
836
+
837
+ function handleUserBashDuringReadOnly(event: UserBashEvent, ctx: ExtensionContext) {
838
+ if (!getReadOnlyState(ctx)?.active) {
839
+ return undefined;
840
+ }
841
+
842
+ if (!isPotentiallyMutatingShellCommand(event.command)) {
843
+ return undefined;
844
+ }
845
+
846
+ return buildBlockedUserBashResult(`Mutating shell commands are disabled in read-only mode (${event.command}).`);
739
847
  }
740
848
 
741
849
  function buildBlockedUserBashResult(detail: string) {
@@ -900,7 +1008,11 @@ export async function startPlanningWorkflow(params: IPlanningStartParams): Promi
900
1008
  }
901
1009
 
902
1010
  try {
903
- await ensureArtifactFile(artifactPath, params.task ?? mode);
1011
+ if (mode === "architect") {
1012
+ await fs.mkdir(artifactPath, { recursive: true });
1013
+ } else {
1014
+ await ensureArtifactFile(artifactPath, params.task ?? mode);
1015
+ }
904
1016
  } catch (error) {
905
1017
  if (ctx.hasUI) {
906
1018
  const message = error instanceof Error ? error.message : String(error);
@@ -968,32 +1080,13 @@ export async function startPlanningWorkflow(params: IPlanningStartParams): Promi
968
1080
 
969
1081
  if (ctx.hasUI) {
970
1082
  const profileHint = profileName ? ` (profile: ${profileName})` : "";
971
- const modeLabel = mode === "architect" ? "Architect mode" : "Plan mode";
972
- ctx.ui.notify(`${modeLabel} active${profileHint}. Writing updates to ${artifactPath}`, "info");
1083
+ ctx.ui.notify(`Plan mode active${profileHint}. Writing updates to ${artifactPath}`, "info");
973
1084
  }
974
1085
 
975
1086
  pi.sendUserMessage(initialPrompt);
976
1087
  return true;
977
1088
  }
978
1089
 
979
- async function planHandler(pi: ExtensionAPI, ctx: ExtensionCommandContext, args: string): Promise<void> {
980
- const task = parsePlanTask(args) ?? undefined;
981
- const artifactPath = createPlanArtifactPath({
982
- cwd: ctx.cwd,
983
- task: task ?? "plan",
984
- date: new Date(),
985
- });
986
-
987
- await startPlanningWorkflow({
988
- pi,
989
- ctx,
990
- mode: "plan",
991
- task,
992
- artifactPath,
993
- initialPrompt: buildInitialPlanPrompt(task),
994
- });
995
- }
996
-
997
1090
  export async function endPlanningWorkflow(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
998
1091
  const state = getPlanState(ctx);
999
1092
  if (!state?.active || !state.originId) {
@@ -1208,13 +1301,6 @@ function handleUserBashDuringPlan(event: UserBashEvent, ctx: ExtensionContext) {
1208
1301
  }
1209
1302
 
1210
1303
  export default function planExtension(pi: ExtensionAPI) {
1211
- pi.registerCommand("plan", {
1212
- description: "Start a planning-only branch with an optional dedicated planning model",
1213
- handler: async(args, ctx) => {
1214
- await planHandler(pi, ctx, args);
1215
- },
1216
- });
1217
-
1218
1304
  pi.registerCommand("end-plan", {
1219
1305
  description: "Leave plan mode, summarize the plan branch, and return to the original branch",
1220
1306
  handler: async(_args, ctx) => {
@@ -1222,37 +1308,85 @@ export default function planExtension(pi: ExtensionAPI) {
1222
1308
  },
1223
1309
  });
1224
1310
 
1225
- pi.registerCommand("end-architect", {
1226
- description: "Leave architect mode, summarize the architect branch, and return to the original branch",
1311
+ pi.registerCommand("plan-policy", {
1312
+ description: "Show effective planning-model policy and the resolved planning model",
1313
+ handler: async(_args, ctx) => {
1314
+ await planPolicyHandler(pi, ctx);
1315
+ },
1316
+ });
1317
+
1318
+ pi.registerCommand("read-only", {
1319
+ description: "Enter read-only mode — write and edit tools are disabled, analysis and search remain available",
1227
1320
  handler: async(_args, ctx) => {
1228
- await endPlanningWorkflow(pi, ctx);
1321
+ if (getReadOnlyState(ctx)?.active) {
1322
+ if (ctx.hasUI) {
1323
+ ctx.ui.notify("Read-only mode is already active. Exit with /end-read-only first.", "warning");
1324
+ }
1325
+ return;
1326
+ }
1327
+
1328
+ const state: IReadOnlySessionState = {
1329
+ active: true,
1330
+ startedAt: new Date().toISOString(),
1331
+ };
1332
+ pi.appendEntry(READONLY_STATE_TYPE, state);
1333
+ currentReadOnlyState = state;
1334
+ setReadOnlyWidget(ctx, state);
1335
+
1336
+ if (ctx.hasUI) {
1337
+ ctx.ui.notify("Read-only mode active. Write & edit tools disabled. Exit with /end-read-only.", "info");
1338
+ }
1229
1339
  },
1230
1340
  });
1231
1341
 
1232
- pi.registerCommand("plan-policy", {
1233
- description: "Show effective planning-model policy and the resolved planning model",
1342
+ pi.registerCommand("end-read-only", {
1343
+ description: "Exit read-only mode and resume normal operation",
1234
1344
  handler: async(_args, ctx) => {
1235
- await planPolicyHandler(pi, ctx);
1345
+ if (!getReadOnlyState(ctx)?.active) {
1346
+ if (ctx.hasUI) {
1347
+ ctx.ui.notify("No active read-only session was found.", "info");
1348
+ }
1349
+ return;
1350
+ }
1351
+
1352
+ pi.appendEntry(READONLY_STATE_TYPE, { active: false });
1353
+ currentReadOnlyState = undefined;
1354
+ setReadOnlyWidget(ctx, undefined);
1355
+
1356
+ if (ctx.hasUI) {
1357
+ ctx.ui.notify("Read-only mode ended. Normal operation resumed.", "info");
1358
+ }
1236
1359
  },
1237
1360
  });
1238
1361
 
1239
1362
  pi.on("before_agent_start", (event, ctx) => {
1240
- const state = getPlanState(ctx);
1241
- if (!state?.active) {
1363
+ const planState = getPlanState(ctx);
1364
+ const readOnlyState = getReadOnlyState(ctx);
1365
+ const overlays: string[] = [];
1366
+
1367
+ if (planState?.active) {
1368
+ overlays.push(buildPlanPromptOverlay(planState));
1369
+ }
1370
+
1371
+ if (readOnlyState?.active) {
1372
+ overlays.push(READONLY_MODE_SYSTEM_PROMPT);
1373
+ }
1374
+
1375
+ if (overlays.length === 0) {
1242
1376
  return;
1243
1377
  }
1244
1378
 
1245
1379
  return {
1246
- systemPrompt: [event.systemPrompt, buildPlanPromptOverlay(state)].filter(Boolean).join("\n\n"),
1380
+ systemPrompt: [event.systemPrompt, ...overlays].filter(Boolean).join("\n\n"),
1247
1381
  };
1248
1382
  });
1249
1383
 
1250
1384
  pi.on("tool_call", (event, ctx) => {
1251
- return handleToolCallDuringPlan(event, ctx);
1385
+ return handleToolCallDuringPlan(event, ctx) ?? handleToolCallDuringReadOnly(event, ctx);
1252
1386
  });
1253
1387
 
1254
1388
  pi.on("user_bash", (event, ctx) => {
1255
- return handleUserBashDuringPlan(event, ctx);
1389
+ return handleUserBashDuringPlan(event, ctx) ?? handleUserBashDuringReadOnly(event, ctx);
1256
1390
  });
1257
1391
 
1258
1392
  pi.on("agent_end", async(event, ctx) => {
@@ -1275,14 +1409,22 @@ export default function planExtension(pi: ExtensionAPI) {
1275
1409
 
1276
1410
  pi.on("session_start", async(_event, ctx) => {
1277
1411
  await syncPlanState(pi, ctx);
1412
+ const readOnlyState = getReadOnlyState(ctx);
1413
+ currentReadOnlyState = readOnlyState;
1414
+ setReadOnlyWidget(ctx, readOnlyState);
1278
1415
  });
1279
1416
 
1280
1417
  pi.on("session_tree", async(_event, ctx) => {
1281
1418
  await syncPlanState(pi, ctx);
1419
+ const readOnlyState = getReadOnlyState(ctx);
1420
+ currentReadOnlyState = readOnlyState;
1421
+ setReadOnlyWidget(ctx, readOnlyState);
1282
1422
  });
1283
1423
 
1284
1424
  pi.on("session_shutdown", async(_event, ctx) => {
1285
1425
  currentPlanState = undefined;
1286
1426
  setPlanWidget(ctx, undefined);
1427
+ currentReadOnlyState = undefined;
1428
+ setReadOnlyWidget(ctx, undefined);
1287
1429
  });
1288
1430
  }
@@ -11,6 +11,7 @@ import { Text } from "@mariozechner/pi-tui";
11
11
  type ITasksAction =
12
12
  | "show"
13
13
  | "get"
14
+ | "create-section"
14
15
  | "create-feature"
15
16
  | "create-task"
16
17
  | "set-status"
@@ -53,9 +54,10 @@ interface IProjectFeatureSummary {
53
54
  }
54
55
 
55
56
  interface IProjectSectionSummary {
56
- sectionId: string;
57
- heading: string;
58
- filePath: string;
57
+ id: string;
58
+ title: string;
59
+ slug: string;
60
+ summary: string;
59
61
  status: string;
60
62
  featureCount: number;
61
63
  taskCount: number;
@@ -72,6 +74,7 @@ const TaskParams = Type.Object({
72
74
  action: StringEnum([
73
75
  "show",
74
76
  "get",
77
+ "create-section",
75
78
  "create-feature",
76
79
  "create-task",
77
80
  "set-status",
@@ -79,10 +82,10 @@ const TaskParams = Type.Object({
79
82
  ] as const),
80
83
  taskId: Type.Optional(Type.String({ description: "Canonical task id" })),
81
84
  featureId: Type.Optional(Type.String({ description: "Canonical feature id" })),
82
- sectionId: Type.Optional(Type.String({ description: "Knowledge section id" })),
83
- title: Type.Optional(Type.String({ description: "Feature or task title" })),
84
- summary: Type.Optional(Type.String({ description: "Feature or task summary" })),
85
- slug: Type.Optional(Type.String({ description: "Feature slug" })),
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" })),
86
89
  type: Type.Optional(Type.String({ description: "Task type" })),
87
90
  assignee: Type.Optional(Type.String({ description: "Task assignee" })),
88
91
  status: Type.Optional(Type.String({ description: "Task status" })),
@@ -175,7 +178,7 @@ function formatHierarchySummary(payload: IProjectPlanShowPayload): string {
175
178
 
176
179
  const lines: string[] = [];
177
180
  for (const section of populatedSections) {
178
- lines.push(`${section.heading} (${section.status})`);
181
+ lines.push(`${section.title} (${section.status})`);
179
182
  for (const feature of section.features) {
180
183
  lines.push(` - ${feature.title} (${feature.status})`);
181
184
  for (const task of feature.tasks) {
@@ -212,21 +215,21 @@ async function tasksCommandHandler(pi: ExtensionAPI, ctx: ExtensionCommandContex
212
215
 
213
216
  const query = rawArgs.trim().toLowerCase();
214
217
  const sections = payload.hierarchy.sections.filter((section) => section.features.length > 0)
215
- .filter((section) => !query || section.heading.toLowerCase().includes(query) || section.sectionId.toLowerCase().includes(query));
218
+ .filter((section) => !query || section.title.toLowerCase().includes(query) || section.id.toLowerCase().includes(query));
216
219
  if (sections.length === 0) {
217
220
  ctx.ui.notify("No project-plan sections with tasks were found.", "info");
218
221
  return;
219
222
  }
220
223
 
221
224
  const selectedSectionId = await selectFromOptions(ctx, "Project Sections", sections.map((section) => ({
222
- label: `${section.heading} (${section.status})`,
223
- value: section.sectionId,
225
+ label: `${section.title} (${section.status})`,
226
+ value: section.id,
224
227
  })));
225
228
  if (!selectedSectionId) {
226
229
  return;
227
230
  }
228
231
 
229
- const section = sections.find((item) => item.sectionId === selectedSectionId);
232
+ const section = sections.find((item) => item.id === selectedSectionId);
230
233
  if (!section) {
231
234
  return;
232
235
  }
@@ -361,6 +364,32 @@ export default function tasksExtension(pi: ExtensionAPI) {
361
364
  };
362
365
  }
363
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
+
364
393
  case "create-feature": {
365
394
  if (!params.sectionId || !params.title) {
366
395
  return {
@@ -9,6 +9,7 @@ Core behavior:
9
9
  - Start by inspecting real tenant state before making claims about apps, data sources, users, environments, auth state, or API shape.
10
10
  - If the repository contains `docyrus/knowledge/`, use `docyrus knowledge search`, `docyrus knowledge section`, and `docyrus knowledge expand` before broader repo exploration.
11
11
  - When `docyrus/knowledge/` exists, keep it updated and finish local work with `docyrus knowledge check`.
12
+ - If the repository contains `docyrus/project-plan/project-plan.json`, use `docyrus project-plan show` to understand current work scope and `docyrus project-plan get-task` to inspect individual tasks before starting work. Update task status with `docyrus project-plan set-task-status` as work progresses.
12
13
  - Use the installed Docyrus skills as your command and workflow reference.
13
14
  - Be careful with tenant-scoped mutations. Confirm real identifiers and current context before changing state.
14
15
  - Treat auth files, tokens, and `.docyrus` contents as sensitive.
@@ -24,4 +25,16 @@ Docyrus concepts you should understand and use accurately:
24
25
  - discover commands and tenant OpenAPI inspection
25
26
  - raw API access via the CLI when needed
26
27
 
28
+ Project plan system:
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.
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
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
37
+
38
+ 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
+
27
40
  You are not a generic shell assistant first. You are a Docyrus-first operator that happens to have local tools.
@@ -20,6 +20,7 @@ Core behavior:
20
20
  - Use the local `docyrus` CLI for Docyrus platform state, tenant context, schema inspection, app and data-source operations, API discovery, and data verification.
21
21
  - If the repository contains `docyrus/knowledge/`, use `docyrus knowledge search`, `docyrus knowledge section`, and `docyrus knowledge expand` before coding so you start from documented repo intent rather than only source inspection.
22
22
  - When `docyrus/knowledge/` exists, keep it in sync with behavior changes and finish with `docyrus knowledge check`.
23
+ - If the repository contains `docyrus/project-plan/project-plan.json`, read it at session start with `docyrus project-plan show` to understand current priorities and work scope. Update task status with `docyrus project-plan set-task-status` as work begins and finishes. Use `docyrus project-plan create-linked-todo` to break agent-assigned tasks into local subtasks when needed.
23
24
  - Prefer `--json` whenever command output needs to be parsed, compared, or fed back into reasoning.
24
25
  - Start from real state before making claims about apps, data sources, users, fields, enums, environments, auth state, API shape, or deployment context.
25
26
  - Distinguish clearly between local code changes and remote Docyrus platform mutations.
@@ -69,6 +70,24 @@ Schema-first workflow for new Docyrus-backed apps and major features:
69
70
  - After schema changes, verify the resulting metadata and runtime behavior with `docyrus ds get`, `docyrus ds list`, and `docyrus discover`.
70
71
  - When the project uses generated collections or codegen from the OpenAPI spec, resync or regenerate those artifacts after schema changes if the repo workflow requires it.
71
72
 
73
+ Project plan system:
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.
76
+
77
+ Key commands:
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
86
+ - `docyrus project-plan check` — validate section references and graph integrity
87
+ - `docyrus project-plan ensure` — initialize an empty project-plan graph if it does not yet exist
88
+
89
+ Workflow: read the project plan at session start → set relevant tasks to `in_progress` before beginning → create linked todos for complex implementation tasks → set tasks to `done` after work is verified.
90
+
72
91
  Docyrus CLI workflows you should rely on:
73
92
 
74
93
  - Use `docyrus auth who`, `docyrus auth accounts ...`, `docyrus auth tenants ...`, and `docyrus env ...` to confirm active identity, tenant, and environment.
@@ -24,7 +24,7 @@ Use this skill when you are:
24
24
  - Setting up authentication with `@docyrus/signin`
25
25
  - Bootstrapping tenant-aware runtime utilities with `@docyrus/app-utils`
26
26
  - Fetching or mutating data with generated collections or `@docyrus/api-client`
27
- - Persisting app-level config or saved grid views with `AppConfig` and `DataViews`
27
+ - Persisting app-level config or user-level config or saved grid views with `AppConfig`, `UserAppConfig`, and `DataViews`
28
28
  - Building record sharing, role management, or ACL-driven UI flows
29
29
  - Designing feature UIs such as dashboards, forms, tables, layouts, dialogs, analytics, or detail pages
30
30
  - Selecting between shadcn, diceui, animate-ui, docyrus-ui, and reui components
@@ -36,7 +36,7 @@ Use this skill when you are:
36
36
  2. Bootstrap `TenantPreferences`, date/number utilities, and shared app runtime helpers from `@docyrus/app-utils`.
37
37
  3. Use generated Docyrus collection hooks or the REST client for data access.
38
38
  4. Define `columns`, filters, formulas, child queries, and mutations correctly.
39
- 5. Use `AppConfig` for per-app persisted settings and `DataViews` for saved grid views.
39
+ 5. Use `AppConfig` for per-app persisted settings, `UserAppConfig` for per-user per-app settings, and `DataViews` for saved grid views.
40
40
  6. Check preferred UI components before building anything custom.
41
41
  7. Use Docyrus form and detail patterns for create, edit, item detail, and editable grid flows.
42
42
  8. Connect UI actions to TanStack Query mutations and invalidate relevant queries.
@@ -81,6 +81,7 @@ Use `@docyrus/app-utils` as the default runtime layer for tenant-level formattin
81
81
  ```tsx
82
82
  import {
83
83
  createAppConfigClient,
84
+ createUserAppConfigClient,
84
85
  createDataViewClient,
85
86
  createDateUtils,
86
87
  createNumberUtils,
@@ -109,6 +110,7 @@ function useAppRuntime(appId: string) {
109
110
  }),
110
111
  numberUtils: createNumberUtils({ preferences }),
111
112
  appConfig: createAppConfigClient(client!, appId),
113
+ userConfig: createUserAppConfigClient(client!, appId),
112
114
  dataViews: createDataViewClient(client!, appId),
113
115
  }
114
116
  },
@@ -121,6 +123,7 @@ Use this runtime to:
121
123
  - Format dates and datetimes with tenant format strings and the user's timezone.
122
124
  - Format numbers, currency-like values, and decimals using tenant separators and precision.
123
125
  - Read and upsert the app's single persisted `AppConfig` document.
126
+ - Read and upsert the current user's `UserAppConfig` document (per-user per-app settings).
124
127
  - Read and persist saved grid views through `DataViews`.
125
128
 
126
129
  ### Data fetching with generated collections
@@ -187,17 +190,18 @@ Use `DataGridViewSelect` as the default saved-view UI for Docyrus grids, and per
187
190
  8. **Initialize `TenantPreferences` once per app runtime** and create shared `dateUtils` / `numberUtils` instances from `@docyrus/app-utils`.
188
191
  9. **Formatting functions from `@docyrus/app-utils` are regionalized** — do not hardcode locale, date format, decimal separator, thousand separator, or decimal precision when tenant preferences should drive them.
189
192
  10. **Use `createAppConfigClient(client, appId)`** for the app's single persisted config document; `upsert` is the default write path.
190
- 11. **Use `createDataViewClient(client, appId)`** for saved grid-view CRUD.
191
- 12. **Use `DataViews` with `DataGridViewSelect`** to show, create, edit, reorder, hide, unhide, soft-delete, and hard-delete saved data grid views.
192
- 13. **`DataGridViewSelect` needs a TanStack table instance** and should receive `fields` when you want the built-in filter builder/editor experience.
193
- 14. **Data view creation requires `name` and `tenant_data_source_id`**.
194
- 15. **Use `dataViews.update(viewId, { archived: true })` for soft-delete** and `dataViews.remove(viewId)` only for irreversible hard-delete.
195
- 16. **Regenerate collections after schema changes** by rebuilding the tenant OpenAPI spec, downloading the latest `openapi.json`, and re-running the collection generator.
196
- 17. **ACL endpoints are usually raw-client integrations** use `useDocyrusClient()` or `RestApiClient` for roles, user-role assignments, role queries, record sharing, and ownership transfer.
197
- 18. **Prefer role `uid` values** for ACL role writes, user-role `roleIds`, and role-query `roleIds`.
198
- 19. **Treat `PUT /v1/users/acl/users/:userId/roles` as full replacement** and `POST /v1/users/acl/users/:userId/roles` as additive.
199
- 20. **Send role-query `query` as raw JSON** and omit `tenantAppId` when `dataSourceId` is present; backend derives it.
200
- 21. **After deleting a role, invalidate dependent app queries** for role lists, user-role lists, role-query lists, and any UI that renders primary-role labels.
193
+ 11. **Use `createUserAppConfigClient(client, appId)`** for the current user's persisted config document scoped to an app (e.g. theme, layout preferences, sidebar state); `upsert` is the default write path.
194
+ 12. **Use `createDataViewClient(client, appId)`** for saved grid-view CRUD.
195
+ 13. **Use `DataViews` with `DataGridViewSelect`** to show, create, edit, reorder, hide, unhide, soft-delete, and hard-delete saved data grid views.
196
+ 14. **`DataGridViewSelect` needs a TanStack table instance** and should receive `fields` when you want the built-in filter builder/editor experience.
197
+ 15. **Data view creation requires `name` and `tenant_data_source_id`**.
198
+ 16. **Use `dataViews.update(viewId, { archived: true })` for soft-delete** and `dataViews.remove(viewId)` only for irreversible hard-delete.
199
+ 17. **Regenerate collections after schema changes** by rebuilding the tenant OpenAPI spec, downloading the latest `openapi.json`, and re-running the collection generator.
200
+ 18. **ACL endpoints are usually raw-client integrations** — use `useDocyrusClient()` or `RestApiClient` for roles, user-role assignments, role queries, record sharing, and ownership transfer.
201
+ 19. **Prefer role `uid` values** for ACL role writes, user-role `roleIds`, and role-query `roleIds`.
202
+ 20. **Treat `PUT /v1/users/acl/users/:userId/roles` as full replacement** and `POST /v1/users/acl/users/:userId/roles` as additive.
203
+ 21. **Send role-query `query` as raw JSON** and omit `tenantAppId` when `dataSourceId` is present; backend derives it.
204
+ 22. **After deleting a role, invalidate dependent app queries** for role lists, user-role lists, role-query lists, and any UI that renders primary-role labels.
201
205
 
202
206
  ## Critical UI/UX Rules
203
207