@docyrus/docyrus 0.0.43 → 0.0.45
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 +116 -83
- package/agent-loader.js +30 -21
- package/agent-loader.js.map +2 -2
- package/main.js +1238 -1189
- package/main.js.map +4 -4
- package/package.json +1 -1
- package/resources/pi-agent/extensions/tasks.ts +41 -12
- package/resources/pi-agent/prompts/agent-system.md +2 -1
- package/resources/pi-agent/prompts/coder-system.md +3 -2
- package/server-loader.js +675 -494
- package/server-loader.js.map +3 -3
package/server-loader.js
CHANGED
|
@@ -22018,6 +22018,23 @@ function normalizeDeploymentName(params) {
|
|
|
22018
22018
|
return params.defaultModel;
|
|
22019
22019
|
}
|
|
22020
22020
|
async function resolveKnowledgeProviderFromAgentRuntime(params) {
|
|
22021
|
+
if (params.docyrusApi) {
|
|
22022
|
+
const { apiBaseUrl, accessToken } = params.docyrusApi;
|
|
22023
|
+
const gatewayBase = apiBaseUrl.replace(/\/+$/u, "") + "/ai/gateway";
|
|
22024
|
+
return {
|
|
22025
|
+
provider: {
|
|
22026
|
+
providerId: "docyrus-api",
|
|
22027
|
+
apiBase: gatewayBase,
|
|
22028
|
+
model: DEFAULT_EMBEDDING_MODEL,
|
|
22029
|
+
dimensions: DEFAULT_DIMENSIONS,
|
|
22030
|
+
key: accessToken,
|
|
22031
|
+
headers: (key) => ({
|
|
22032
|
+
Authorization: `Bearer ${key}`,
|
|
22033
|
+
"Content-Type": "application/json"
|
|
22034
|
+
})
|
|
22035
|
+
}
|
|
22036
|
+
};
|
|
22037
|
+
}
|
|
22021
22038
|
const agentRootPath = resolveDocyrusPiAgentRootPath(params.settingsRootPath);
|
|
22022
22039
|
const authState = readJsonFile((0, import_node_path10.join)(agentRootPath, "auth.json")) || {};
|
|
22023
22040
|
const envStore = new AgentEnvStore((0, import_node_path10.join)(agentRootPath, "env.json"));
|
|
@@ -22560,9 +22577,9 @@ async function checkKnowledgeSections(knowledgeDir) {
|
|
|
22560
22577
|
}
|
|
22561
22578
|
|
|
22562
22579
|
// src/knowledge/init.ts
|
|
22563
|
-
var
|
|
22564
|
-
var
|
|
22565
|
-
var
|
|
22580
|
+
var import_node_fs7 = require("node:fs");
|
|
22581
|
+
var import_promises9 = require("node:fs/promises");
|
|
22582
|
+
var import_node_path13 = require("node:path");
|
|
22566
22583
|
init_graph();
|
|
22567
22584
|
|
|
22568
22585
|
// src/knowledge/bootstrap.ts
|
|
@@ -22974,29 +22991,265 @@ async function generateInitialKnowledge(params) {
|
|
|
22974
22991
|
};
|
|
22975
22992
|
}
|
|
22976
22993
|
|
|
22994
|
+
// src/knowledge/init.ts
|
|
22995
|
+
var MARKER_BEGIN = "%% docyrus-knowledge:begin %%";
|
|
22996
|
+
var MARKER_END = "%% docyrus-knowledge:end %%";
|
|
22997
|
+
var HOOK_MARKER_BEGIN = "# docyrus-knowledge:begin";
|
|
22998
|
+
var HOOK_MARKER_END = "# docyrus-knowledge:end";
|
|
22999
|
+
function wrapManagedBlock(content3) {
|
|
23000
|
+
return `${MARKER_BEGIN}
|
|
23001
|
+
${content3}${content3.endsWith("\n") ? "" : "\n"}${MARKER_END}
|
|
23002
|
+
`;
|
|
23003
|
+
}
|
|
23004
|
+
async function ensureDirectory(pathValue) {
|
|
23005
|
+
await (0, import_promises9.mkdir)(pathValue, {
|
|
23006
|
+
recursive: true,
|
|
23007
|
+
mode: 493
|
|
23008
|
+
});
|
|
23009
|
+
}
|
|
23010
|
+
async function writeIfMissing(filePath, content3) {
|
|
23011
|
+
if ((0, import_node_fs7.existsSync)(filePath)) {
|
|
23012
|
+
return "kept";
|
|
23013
|
+
}
|
|
23014
|
+
await ensureDirectory((0, import_node_path13.dirname)(filePath));
|
|
23015
|
+
await (0, import_promises9.writeFile)(filePath, content3, "utf8");
|
|
23016
|
+
return "created";
|
|
23017
|
+
}
|
|
23018
|
+
async function writeOrUpdate(filePath, content3) {
|
|
23019
|
+
if (!(0, import_node_fs7.existsSync)(filePath)) {
|
|
23020
|
+
await ensureDirectory((0, import_node_path13.dirname)(filePath));
|
|
23021
|
+
await (0, import_promises9.writeFile)(filePath, content3, "utf8");
|
|
23022
|
+
return "created";
|
|
23023
|
+
}
|
|
23024
|
+
const current = await (0, import_promises9.readFile)(filePath, "utf8");
|
|
23025
|
+
if (current === content3) {
|
|
23026
|
+
return "kept";
|
|
23027
|
+
}
|
|
23028
|
+
await ensureDirectory((0, import_node_path13.dirname)(filePath));
|
|
23029
|
+
await (0, import_promises9.writeFile)(filePath, content3, "utf8");
|
|
23030
|
+
return "updated";
|
|
23031
|
+
}
|
|
23032
|
+
async function upsertManagedBlock(filePath, content3) {
|
|
23033
|
+
const wrapped = wrapManagedBlock(content3);
|
|
23034
|
+
if (!(0, import_node_fs7.existsSync)(filePath)) {
|
|
23035
|
+
await ensureDirectory((0, import_node_path13.dirname)(filePath));
|
|
23036
|
+
await (0, import_promises9.writeFile)(filePath, wrapped, "utf8");
|
|
23037
|
+
return "created";
|
|
23038
|
+
}
|
|
23039
|
+
const current = await (0, import_promises9.readFile)(filePath, "utf8");
|
|
23040
|
+
const beginIndex = current.indexOf(MARKER_BEGIN);
|
|
23041
|
+
const endIndex = current.indexOf(MARKER_END);
|
|
23042
|
+
if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
|
|
23043
|
+
const replaceEnd = current[endIndex + MARKER_END.length] === "\n" ? endIndex + MARKER_END.length + 1 : endIndex + MARKER_END.length;
|
|
23044
|
+
const updated = `${current.slice(0, beginIndex)}${wrapped}${current.slice(replaceEnd)}`;
|
|
23045
|
+
await (0, import_promises9.writeFile)(filePath, updated, "utf8");
|
|
23046
|
+
return "updated";
|
|
23047
|
+
}
|
|
23048
|
+
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
23049
|
+
await (0, import_promises9.writeFile)(filePath, `${current}${separator}${wrapped}`, "utf8");
|
|
23050
|
+
return "appended";
|
|
23051
|
+
}
|
|
23052
|
+
function normalizeObject(value2) {
|
|
23053
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) ? { ...value2 } : {};
|
|
23054
|
+
}
|
|
23055
|
+
async function updateJsonFile(filePath, updater) {
|
|
23056
|
+
const current = (0, import_node_fs7.existsSync)(filePath) ? normalizeObject(JSON.parse((0, import_node_fs7.readFileSync)(filePath, "utf8"))) : {};
|
|
23057
|
+
await ensureDirectory((0, import_node_path13.dirname)(filePath));
|
|
23058
|
+
await (0, import_promises9.writeFile)(filePath, `${JSON.stringify(updater(current), null, 2)}
|
|
23059
|
+
`, "utf8");
|
|
23060
|
+
}
|
|
23061
|
+
function wrapHookBlock(content3) {
|
|
23062
|
+
return `${HOOK_MARKER_BEGIN}
|
|
23063
|
+
${content3}${content3.endsWith("\n") ? "" : "\n"}${HOOK_MARKER_END}
|
|
23064
|
+
`;
|
|
23065
|
+
}
|
|
23066
|
+
async function upsertShellHook(filePath, content3, shellHeader) {
|
|
23067
|
+
const wrapped = wrapHookBlock(content3);
|
|
23068
|
+
if (!(0, import_node_fs7.existsSync)(filePath)) {
|
|
23069
|
+
await ensureDirectory((0, import_node_path13.dirname)(filePath));
|
|
23070
|
+
const initial = `${shellHeader || ""}${wrapped}`;
|
|
23071
|
+
await (0, import_promises9.writeFile)(filePath, initial, {
|
|
23072
|
+
encoding: "utf8",
|
|
23073
|
+
mode: 493
|
|
23074
|
+
});
|
|
23075
|
+
await (0, import_promises9.chmod)(filePath, 493);
|
|
23076
|
+
return "created";
|
|
23077
|
+
}
|
|
23078
|
+
const current = await (0, import_promises9.readFile)(filePath, "utf8");
|
|
23079
|
+
const beginIndex = current.indexOf(HOOK_MARKER_BEGIN);
|
|
23080
|
+
const endIndex = current.indexOf(HOOK_MARKER_END);
|
|
23081
|
+
if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
|
|
23082
|
+
const replaceEnd = current[endIndex + HOOK_MARKER_END.length] === "\n" ? endIndex + HOOK_MARKER_END.length + 1 : endIndex + HOOK_MARKER_END.length;
|
|
23083
|
+
const nextContent = `${current.slice(0, beginIndex)}${wrapped}${current.slice(replaceEnd)}`;
|
|
23084
|
+
if (nextContent === current) {
|
|
23085
|
+
return "kept";
|
|
23086
|
+
}
|
|
23087
|
+
await (0, import_promises9.writeFile)(filePath, nextContent, "utf8");
|
|
23088
|
+
await (0, import_promises9.chmod)(filePath, 493);
|
|
23089
|
+
return "updated";
|
|
23090
|
+
}
|
|
23091
|
+
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
23092
|
+
await (0, import_promises9.writeFile)(filePath, `${current}${separator}${wrapped}`, "utf8");
|
|
23093
|
+
await (0, import_promises9.chmod)(filePath, 493);
|
|
23094
|
+
return "appended";
|
|
23095
|
+
}
|
|
23096
|
+
function buildKnowledgeDocumentTemplate() {
|
|
23097
|
+
return [
|
|
23098
|
+
"# Knowledge",
|
|
23099
|
+
"",
|
|
23100
|
+
"This directory captures the architecture, constraints, workflows, and test expectations that coding agents should read before changing the repo.",
|
|
23101
|
+
"",
|
|
23102
|
+
"## Working Agreement",
|
|
23103
|
+
"",
|
|
23104
|
+
"Search this knowledge graph before coding, keep it updated when behavior changes, and use \\[\\[wiki links\\]\\] plus `@docyrus` backlinks to connect decisions to implementation.",
|
|
23105
|
+
"",
|
|
23106
|
+
"## Backlinks",
|
|
23107
|
+
"",
|
|
23108
|
+
"Use `// @docyrus: \\[\\[knowledge#Section Name\\]\\]` or `# @docyrus: \\[\\[knowledge#Section Name\\]\\]` near the code or tests that implement a documented behavior.",
|
|
23109
|
+
""
|
|
23110
|
+
].join("\n");
|
|
23111
|
+
}
|
|
23112
|
+
function buildAgentsTemplate(commandPrefix) {
|
|
23113
|
+
return [
|
|
23114
|
+
"# Knowledge Graph Workflow",
|
|
23115
|
+
"",
|
|
23116
|
+
`- If \`docyrus/knowledge/\` exists, run \`${commandPrefix} knowledge search\` before coding so you start from documented intent instead of rediscovering it from source files.`,
|
|
23117
|
+
`- Use \`${commandPrefix} knowledge expand\` when prompts contain \`[[refs]]\` so section names resolve to real file locations and summaries.`,
|
|
23118
|
+
"- Keep `docyrus/knowledge/` in sync whenever you change functionality, architecture, tests, or behavior.",
|
|
23119
|
+
`- Before finishing, run \`${commandPrefix} knowledge check\` and fix any broken links, stale references, or missing \`@docyrus\` backlinks.`,
|
|
23120
|
+
""
|
|
23121
|
+
].join("\n");
|
|
23122
|
+
}
|
|
23123
|
+
function buildCursorRulesTemplate(commandPrefix) {
|
|
23124
|
+
return [
|
|
23125
|
+
"# Knowledge Graph Workflow",
|
|
23126
|
+
"",
|
|
23127
|
+
`- Use \`${commandPrefix} knowledge search\` to find relevant documentation before reading or editing code.`,
|
|
23128
|
+
`- Use \`${commandPrefix} knowledge section\` to read the full section once you have a relevant section id.`,
|
|
23129
|
+
`- Use \`${commandPrefix} knowledge expand\` whenever the prompt includes \`[[refs]]\`.`,
|
|
23130
|
+
`- Keep \`docyrus/knowledge/\` updated and finish with \`${commandPrefix} knowledge check\`.`,
|
|
23131
|
+
""
|
|
23132
|
+
].join("\n");
|
|
23133
|
+
}
|
|
23134
|
+
function buildClaudeHookCommand(commandPrefix, event) {
|
|
23135
|
+
return `${commandPrefix} knowledge hook claude ${event}`;
|
|
23136
|
+
}
|
|
23137
|
+
function syncClaudeHooks(settings, commandPrefix) {
|
|
23138
|
+
const hooks = normalizeObject(settings.hooks);
|
|
23139
|
+
const nextHooks = { ...hooks };
|
|
23140
|
+
for (const [eventName, entries] of Object.entries(nextHooks)) {
|
|
23141
|
+
if (!Array.isArray(entries)) {
|
|
23142
|
+
continue;
|
|
23143
|
+
}
|
|
23144
|
+
nextHooks[eventName] = entries.filter((entry) => {
|
|
23145
|
+
const hooksList = Array.isArray(entry.hooks) ? entry.hooks || [] : [];
|
|
23146
|
+
return !hooksList.some((hook) => typeof hook.command === "string" && hook.command.includes("knowledge hook claude"));
|
|
23147
|
+
});
|
|
23148
|
+
}
|
|
23149
|
+
const addHook = (eventName) => {
|
|
23150
|
+
const current = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : [];
|
|
23151
|
+
current.push({
|
|
23152
|
+
hooks: [
|
|
23153
|
+
{
|
|
23154
|
+
type: "command",
|
|
23155
|
+
command: buildClaudeHookCommand(commandPrefix, eventName)
|
|
23156
|
+
}
|
|
23157
|
+
]
|
|
23158
|
+
});
|
|
23159
|
+
nextHooks[eventName] = current;
|
|
23160
|
+
};
|
|
23161
|
+
addHook("UserPromptSubmit");
|
|
23162
|
+
addHook("Stop");
|
|
23163
|
+
return {
|
|
23164
|
+
...settings,
|
|
23165
|
+
hooks: nextHooks
|
|
23166
|
+
};
|
|
23167
|
+
}
|
|
23168
|
+
function syncCursorHooks(settings, commandPrefix) {
|
|
23169
|
+
const hooks = normalizeObject(settings.hooks);
|
|
23170
|
+
const stopEntries = Array.isArray(hooks.stop) ? hooks.stop : [];
|
|
23171
|
+
const filtered = stopEntries.filter((entry) => typeof entry.command !== "string" || !entry.command.includes("knowledge hook cursor"));
|
|
23172
|
+
return {
|
|
23173
|
+
...settings.version ? settings : { version: 1 },
|
|
23174
|
+
...settings,
|
|
23175
|
+
hooks: {
|
|
23176
|
+
...hooks,
|
|
23177
|
+
stop: [
|
|
23178
|
+
...filtered,
|
|
23179
|
+
{
|
|
23180
|
+
command: `${commandPrefix} knowledge hook cursor stop`
|
|
23181
|
+
}
|
|
23182
|
+
]
|
|
23183
|
+
}
|
|
23184
|
+
};
|
|
23185
|
+
}
|
|
23186
|
+
async function initializeKnowledgeRepo(params) {
|
|
23187
|
+
const root = (0, import_node_path13.resolve)(params.root || process.cwd());
|
|
23188
|
+
const knowledgeDir = getKnowledgeDirectory(root);
|
|
23189
|
+
const knowledgeFile = (0, import_node_path13.join)(knowledgeDir, "knowledge.md");
|
|
23190
|
+
await ensureDirectory(knowledgeDir);
|
|
23191
|
+
const knowledgeFileStatus = await writeIfMissing(knowledgeFile, `${buildKnowledgeDocumentTemplate()}
|
|
23192
|
+
`);
|
|
23193
|
+
const agentsStatus = await upsertManagedBlock((0, import_node_path13.join)(root, "AGENTS.md"), buildAgentsTemplate(params.commandPrefix));
|
|
23194
|
+
const claudeStatus = await upsertManagedBlock((0, import_node_path13.join)(root, "CLAUDE.md"), buildAgentsTemplate(params.commandPrefix));
|
|
23195
|
+
const cursorRulesStatus = await writeOrUpdate((0, import_node_path13.join)(root, ".cursor", "rules", "docyrus-knowledge.md"), `${buildCursorRulesTemplate(params.commandPrefix)}
|
|
23196
|
+
`);
|
|
23197
|
+
const bootstrap = (await generateInitialKnowledge({
|
|
23198
|
+
root,
|
|
23199
|
+
brief: params.brief
|
|
23200
|
+
})).files;
|
|
23201
|
+
await updateJsonFile((0, import_node_path13.join)(root, ".claude", "settings.json"), (value2) => syncClaudeHooks(value2, params.commandPrefix));
|
|
23202
|
+
await updateJsonFile((0, import_node_path13.join)(root, ".cursor", "hooks.json"), (value2) => syncCursorHooks(value2, params.commandPrefix));
|
|
23203
|
+
let huskyHookStatus;
|
|
23204
|
+
const huskyDir = (0, import_node_path13.join)(root, ".husky");
|
|
23205
|
+
if ((0, import_node_fs7.existsSync)(huskyDir)) {
|
|
23206
|
+
const huskyHookPath = (0, import_node_path13.join)(huskyDir, "pre-commit");
|
|
23207
|
+
const huskyHeader = (0, import_node_fs7.existsSync)((0, import_node_path13.join)(huskyDir, "_", "h")) ? '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/h"\n\n' : void 0;
|
|
23208
|
+
huskyHookStatus = await upsertShellHook(huskyHookPath, `${params.commandPrefix} knowledge pre-commit
|
|
23209
|
+
`, huskyHeader);
|
|
23210
|
+
}
|
|
23211
|
+
let gitHookStatus;
|
|
23212
|
+
if (!huskyHookStatus && params.installGitHook) {
|
|
23213
|
+
const gitHookPath = (0, import_node_path13.join)(root, ".git", "hooks", "pre-commit");
|
|
23214
|
+
gitHookStatus = await upsertShellHook(gitHookPath, `${params.commandPrefix} knowledge pre-commit
|
|
23215
|
+
`, "#!/usr/bin/env sh\n\n");
|
|
23216
|
+
}
|
|
23217
|
+
return {
|
|
23218
|
+
root,
|
|
23219
|
+
knowledgeDir,
|
|
23220
|
+
knowledgeFile,
|
|
23221
|
+
agentsStatus,
|
|
23222
|
+
claudeStatus,
|
|
23223
|
+
cursorRulesStatus,
|
|
23224
|
+
knowledgeFileStatus,
|
|
23225
|
+
bootstrap,
|
|
23226
|
+
huskyHookStatus,
|
|
23227
|
+
gitHookStatus
|
|
23228
|
+
};
|
|
23229
|
+
}
|
|
23230
|
+
|
|
22977
23231
|
// src/project-plan/graph.ts
|
|
22978
23232
|
var import_node_crypto3 = require("node:crypto");
|
|
22979
|
-
var
|
|
22980
|
-
var
|
|
22981
|
-
var
|
|
22982
|
-
init_graph();
|
|
23233
|
+
var import_node_fs9 = require("node:fs");
|
|
23234
|
+
var import_promises11 = __toESM(require("node:fs/promises"));
|
|
23235
|
+
var import_node_path15 = __toESM(require("node:path"));
|
|
22983
23236
|
|
|
22984
23237
|
// src/project-plan/localTodos.ts
|
|
22985
23238
|
var import_node_crypto2 = __toESM(require("node:crypto"));
|
|
22986
|
-
var
|
|
22987
|
-
var
|
|
22988
|
-
var
|
|
23239
|
+
var import_node_fs8 = require("node:fs");
|
|
23240
|
+
var import_promises10 = __toESM(require("node:fs/promises"));
|
|
23241
|
+
var import_node_path14 = __toESM(require("node:path"));
|
|
22989
23242
|
var TODO_DIR_NAME = ".pi/todos";
|
|
22990
23243
|
var TODO_PATH_ENV = "PI_TODO_PATH";
|
|
22991
23244
|
function resolveTodosDirectory(root) {
|
|
22992
23245
|
const overridePath = process.env[TODO_PATH_ENV]?.trim();
|
|
22993
23246
|
if (overridePath) {
|
|
22994
|
-
return
|
|
23247
|
+
return import_node_path14.default.resolve(root, overridePath);
|
|
22995
23248
|
}
|
|
22996
|
-
return
|
|
23249
|
+
return import_node_path14.default.resolve(root, TODO_DIR_NAME);
|
|
22997
23250
|
}
|
|
22998
23251
|
function getTodoFilePath(todosDir, id) {
|
|
22999
|
-
return
|
|
23252
|
+
return import_node_path14.default.join(todosDir, `${id}.md`);
|
|
23000
23253
|
}
|
|
23001
23254
|
function splitFrontMatter(content3) {
|
|
23002
23255
|
if (!content3.startsWith("{")) {
|
|
@@ -23102,7 +23355,7 @@ ${trimmedBody}
|
|
|
23102
23355
|
async function generateTodoId(todosDir) {
|
|
23103
23356
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
23104
23357
|
const id = import_node_crypto2.default.randomBytes(4).toString("hex");
|
|
23105
|
-
if (!(0,
|
|
23358
|
+
if (!(0, import_node_fs8.existsSync)(getTodoFilePath(todosDir, id))) {
|
|
23106
23359
|
return id;
|
|
23107
23360
|
}
|
|
23108
23361
|
}
|
|
@@ -23113,7 +23366,7 @@ async function listLinkedTodosByTask(root) {
|
|
|
23113
23366
|
const result = /* @__PURE__ */ new Map();
|
|
23114
23367
|
let entries = [];
|
|
23115
23368
|
try {
|
|
23116
|
-
entries = await
|
|
23369
|
+
entries = await import_promises10.default.readdir(todosDir);
|
|
23117
23370
|
} catch {
|
|
23118
23371
|
return result;
|
|
23119
23372
|
}
|
|
@@ -23124,7 +23377,7 @@ async function listLinkedTodosByTask(root) {
|
|
|
23124
23377
|
const id = entry.slice(0, -3);
|
|
23125
23378
|
const filePath = getTodoFilePath(todosDir, id);
|
|
23126
23379
|
try {
|
|
23127
|
-
const raw2 = await
|
|
23380
|
+
const raw2 = await import_promises10.default.readFile(filePath, "utf8");
|
|
23128
23381
|
const { frontMatter } = splitFrontMatter(raw2);
|
|
23129
23382
|
const parsed = parseLocalTodoFrontMatter(frontMatter, id);
|
|
23130
23383
|
if (!parsed.parent_task_id) {
|
|
@@ -23135,7 +23388,7 @@ async function listLinkedTodosByTask(root) {
|
|
|
23135
23388
|
id,
|
|
23136
23389
|
title: parsed.title,
|
|
23137
23390
|
status: parsed.status,
|
|
23138
|
-
filePath:
|
|
23391
|
+
filePath: import_node_path14.default.relative(root, filePath),
|
|
23139
23392
|
parentTaskId: parsed.parent_task_id
|
|
23140
23393
|
});
|
|
23141
23394
|
result.set(parsed.parent_task_id, existing);
|
|
@@ -23153,7 +23406,7 @@ async function listAllLinkedTodos(root) {
|
|
|
23153
23406
|
}
|
|
23154
23407
|
async function createLinkedTodo(params) {
|
|
23155
23408
|
const todosDir = resolveTodosDirectory(params.root);
|
|
23156
|
-
await
|
|
23409
|
+
await import_promises10.default.mkdir(todosDir, {
|
|
23157
23410
|
recursive: true
|
|
23158
23411
|
});
|
|
23159
23412
|
const id = await generateTodoId(todosDir);
|
|
@@ -23167,12 +23420,12 @@ async function createLinkedTodo(params) {
|
|
|
23167
23420
|
parent_task_id: params.parentTaskId,
|
|
23168
23421
|
body: params.body ?? ""
|
|
23169
23422
|
};
|
|
23170
|
-
await
|
|
23423
|
+
await import_promises10.default.writeFile(filePath, serializeLocalTodo(todo), "utf8");
|
|
23171
23424
|
return {
|
|
23172
23425
|
id,
|
|
23173
23426
|
title: todo.title,
|
|
23174
23427
|
status: todo.status,
|
|
23175
|
-
filePath:
|
|
23428
|
+
filePath: import_node_path14.default.relative(params.root, filePath),
|
|
23176
23429
|
parentTaskId: params.parentTaskId
|
|
23177
23430
|
};
|
|
23178
23431
|
}
|
|
@@ -23200,6 +23453,9 @@ var PROJECT_TASK_STATUSES = [
|
|
|
23200
23453
|
var PROJECT_PLAN_DIR_SEGMENTS = ["docyrus", "project-plan"];
|
|
23201
23454
|
var PROJECT_PLAN_JSON_FILE_NAME = "project-plan.json";
|
|
23202
23455
|
var PROJECT_PLAN_MARKDOWN_FILE_NAME = "PROJECT_PLAN.md";
|
|
23456
|
+
var KNOWLEDGE_FEATURES_PATH_SEGMENTS = ["docyrus", "knowledge", "features", "features.md"];
|
|
23457
|
+
var FEATURES_MARKER_BEGIN = "<!-- docyrus-project-plan:features:begin -->";
|
|
23458
|
+
var FEATURES_MARKER_END = "<!-- docyrus-project-plan:features:end -->";
|
|
23203
23459
|
function isRecord3(value2) {
|
|
23204
23460
|
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
23205
23461
|
}
|
|
@@ -23219,6 +23475,9 @@ function slugify(value2) {
|
|
|
23219
23475
|
function hashParts(parts2) {
|
|
23220
23476
|
return (0, import_node_crypto3.createHash)("sha1").update(parts2.join("::")).digest("hex").slice(0, 12);
|
|
23221
23477
|
}
|
|
23478
|
+
function normalizeSectionId(slug) {
|
|
23479
|
+
return `section-${hashParts([slug])}`;
|
|
23480
|
+
}
|
|
23222
23481
|
function normalizeFeatureId(sectionId, slug) {
|
|
23223
23482
|
return `feature-${hashParts([sectionId, slug])}`;
|
|
23224
23483
|
}
|
|
@@ -23231,7 +23490,7 @@ function compareStrings(left, right) {
|
|
|
23231
23490
|
function sortGraph(graph) {
|
|
23232
23491
|
return {
|
|
23233
23492
|
version: 1,
|
|
23234
|
-
sections: [...graph.sections].sort((left, right) => compareStrings(left.
|
|
23493
|
+
sections: [...graph.sections].sort((left, right) => compareStrings(left.id, right.id)),
|
|
23235
23494
|
features: [...graph.features].sort((left, right) => {
|
|
23236
23495
|
return compareStrings(left.sectionId, right.sectionId) || compareStrings(left.title, right.title) || compareStrings(left.id, right.id);
|
|
23237
23496
|
}),
|
|
@@ -23241,13 +23500,13 @@ function sortGraph(graph) {
|
|
|
23241
23500
|
};
|
|
23242
23501
|
}
|
|
23243
23502
|
function resolveProjectPlanDirectory(root) {
|
|
23244
|
-
return
|
|
23503
|
+
return import_node_path15.default.join(root, ...PROJECT_PLAN_DIR_SEGMENTS);
|
|
23245
23504
|
}
|
|
23246
23505
|
function resolveProjectPlanGraphPath(root) {
|
|
23247
|
-
return
|
|
23506
|
+
return import_node_path15.default.join(resolveProjectPlanDirectory(root), PROJECT_PLAN_JSON_FILE_NAME);
|
|
23248
23507
|
}
|
|
23249
23508
|
function resolveProjectPlanMarkdownPath(root) {
|
|
23250
|
-
return
|
|
23509
|
+
return import_node_path15.default.join(resolveProjectPlanDirectory(root), PROJECT_PLAN_MARKDOWN_FILE_NAME);
|
|
23251
23510
|
}
|
|
23252
23511
|
function createEmptyProjectPlanGraph() {
|
|
23253
23512
|
return {
|
|
@@ -23257,20 +23516,16 @@ function createEmptyProjectPlanGraph() {
|
|
|
23257
23516
|
tasks: []
|
|
23258
23517
|
};
|
|
23259
23518
|
}
|
|
23260
|
-
async function readKnowledgeSections(root) {
|
|
23261
|
-
const knowledgeDir = import_node_path14.default.join(root, "docyrus", "knowledge");
|
|
23262
|
-
if (!(0, import_node_fs8.existsSync)(knowledgeDir)) {
|
|
23263
|
-
return [];
|
|
23264
|
-
}
|
|
23265
|
-
return flattenSections(await loadAllSections(knowledgeDir));
|
|
23266
|
-
}
|
|
23267
23519
|
function parseProjectPlanGraph(rawValue) {
|
|
23268
23520
|
if (!isRecord3(rawValue)) {
|
|
23269
23521
|
return createEmptyProjectPlanGraph();
|
|
23270
23522
|
}
|
|
23271
23523
|
const sections = Array.isArray(rawValue.sections) ? rawValue.sections.filter((value2) => isRecord3(value2)).map((value2) => ({
|
|
23272
|
-
|
|
23273
|
-
|
|
23524
|
+
id: isNonEmptyString2(value2.id) ? value2.id.trim() : "",
|
|
23525
|
+
title: isNonEmptyString2(value2.title) ? value2.title.trim() : "",
|
|
23526
|
+
slug: isNonEmptyString2(value2.slug) ? value2.slug.trim() : "",
|
|
23527
|
+
summary: typeof value2.summary === "string" ? value2.summary.trim() : ""
|
|
23528
|
+
})).filter((value2) => value2.id.length > 0) : [];
|
|
23274
23529
|
const features = Array.isArray(rawValue.features) ? rawValue.features.filter((value2) => isRecord3(value2)).map((value2) => ({
|
|
23275
23530
|
id: isNonEmptyString2(value2.id) ? value2.id.trim() : "",
|
|
23276
23531
|
title: isNonEmptyString2(value2.title) ? value2.title.trim() : "",
|
|
@@ -23298,10 +23553,10 @@ function parseProjectPlanGraph(rawValue) {
|
|
|
23298
23553
|
}
|
|
23299
23554
|
async function readProjectPlanGraph(root) {
|
|
23300
23555
|
const graphPath = resolveProjectPlanGraphPath(root);
|
|
23301
|
-
if (!(0,
|
|
23556
|
+
if (!(0, import_node_fs9.existsSync)(graphPath)) {
|
|
23302
23557
|
return createEmptyProjectPlanGraph();
|
|
23303
23558
|
}
|
|
23304
|
-
const raw2 = await
|
|
23559
|
+
const raw2 = await import_promises11.default.readFile(graphPath, "utf8");
|
|
23305
23560
|
return parseProjectPlanGraph(JSON.parse(raw2));
|
|
23306
23561
|
}
|
|
23307
23562
|
function deriveFeatureStatus(tasks) {
|
|
@@ -23334,21 +23589,7 @@ function deriveSectionStatus(features) {
|
|
|
23334
23589
|
}
|
|
23335
23590
|
return "planned";
|
|
23336
23591
|
}
|
|
23337
|
-
function findKnowledgeSectionTitle(knowledgeSections, sectionId) {
|
|
23338
|
-
const section = knowledgeSections.find((item) => item.id === sectionId);
|
|
23339
|
-
if (!section) {
|
|
23340
|
-
return {
|
|
23341
|
-
heading: sectionId,
|
|
23342
|
-
filePath: ""
|
|
23343
|
-
};
|
|
23344
|
-
}
|
|
23345
|
-
return {
|
|
23346
|
-
heading: section.heading,
|
|
23347
|
-
filePath: section.filePath
|
|
23348
|
-
};
|
|
23349
|
-
}
|
|
23350
23592
|
async function buildProjectPlanHierarchy(root, graph) {
|
|
23351
|
-
const knowledgeSections = await readKnowledgeSections(root);
|
|
23352
23593
|
const currentGraph = graph ?? await readProjectPlanGraph(root);
|
|
23353
23594
|
const linkedTodos = await listLinkedTodosByTask(root);
|
|
23354
23595
|
const featureMap = /* @__PURE__ */ new Map();
|
|
@@ -23369,13 +23610,13 @@ async function buildProjectPlanHierarchy(root, graph) {
|
|
|
23369
23610
|
});
|
|
23370
23611
|
}
|
|
23371
23612
|
const sections = currentGraph.sections.map((section) => {
|
|
23372
|
-
const features = currentGraph.features.filter((feature) => feature.sectionId === section.
|
|
23373
|
-
const knowledgeMetadata = findKnowledgeSectionTitle(knowledgeSections, section.sectionId);
|
|
23613
|
+
const features = currentGraph.features.filter((feature) => feature.sectionId === section.id).map((feature) => featureMap.get(feature.id)).filter((feature) => Boolean(feature));
|
|
23374
23614
|
const taskCount = features.reduce((count, feature) => count + feature.taskCount, 0);
|
|
23375
23615
|
return {
|
|
23376
|
-
|
|
23377
|
-
|
|
23378
|
-
|
|
23616
|
+
id: section.id,
|
|
23617
|
+
title: section.title,
|
|
23618
|
+
slug: section.slug,
|
|
23619
|
+
summary: section.summary,
|
|
23379
23620
|
status: deriveSectionStatus(features),
|
|
23380
23621
|
featureCount: features.length,
|
|
23381
23622
|
taskCount,
|
|
@@ -23407,9 +23648,9 @@ async function renderProjectPlanMarkdown(root, graph) {
|
|
|
23407
23648
|
return `${lines.join("\n")}`;
|
|
23408
23649
|
}
|
|
23409
23650
|
for (const section of populatedSections) {
|
|
23410
|
-
lines.push(`## ${section.
|
|
23651
|
+
lines.push(`## ${section.title}`);
|
|
23411
23652
|
lines.push("");
|
|
23412
|
-
lines.push(`- Section ID: \`${section.
|
|
23653
|
+
lines.push(`- Section ID: \`${section.id}\``);
|
|
23413
23654
|
lines.push(`- Status: \`${section.status}\``);
|
|
23414
23655
|
lines.push(`- Features: ${section.featureCount}`);
|
|
23415
23656
|
lines.push(`- Tasks: ${section.taskCount}`);
|
|
@@ -23443,53 +23684,118 @@ async function renderProjectPlanMarkdown(root, graph) {
|
|
|
23443
23684
|
return `${lines.join("\n").trimEnd()}
|
|
23444
23685
|
`;
|
|
23445
23686
|
}
|
|
23446
|
-
|
|
23447
|
-
const
|
|
23448
|
-
const
|
|
23449
|
-
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
|
|
23454
|
-
|
|
23455
|
-
|
|
23456
|
-
|
|
23457
|
-
|
|
23458
|
-
|
|
23459
|
-
|
|
23460
|
-
|
|
23461
|
-
|
|
23462
|
-
|
|
23463
|
-
|
|
23464
|
-
|
|
23465
|
-
|
|
23466
|
-
|
|
23467
|
-
|
|
23468
|
-
|
|
23469
|
-
|
|
23470
|
-
|
|
23687
|
+
function buildKnowledgeFeaturesSection(graph) {
|
|
23688
|
+
const sectionMap = new Map(graph.sections.map((section) => [section.id, section]));
|
|
23689
|
+
const featuresBySection = /* @__PURE__ */ new Map();
|
|
23690
|
+
for (const feature of graph.features) {
|
|
23691
|
+
const list2 = featuresBySection.get(feature.sectionId) || [];
|
|
23692
|
+
list2.push(feature);
|
|
23693
|
+
featuresBySection.set(feature.sectionId, list2);
|
|
23694
|
+
}
|
|
23695
|
+
const lines = [
|
|
23696
|
+
"## Planned Features",
|
|
23697
|
+
"",
|
|
23698
|
+
"Features tracked in the project plan.",
|
|
23699
|
+
""
|
|
23700
|
+
];
|
|
23701
|
+
if (graph.features.length === 0) {
|
|
23702
|
+
lines.push("No planned features yet.");
|
|
23703
|
+
lines.push("");
|
|
23704
|
+
return lines.join("\n");
|
|
23705
|
+
}
|
|
23706
|
+
for (const section of graph.sections) {
|
|
23707
|
+
const features = featuresBySection.get(section.id);
|
|
23708
|
+
if (!features || features.length === 0) {
|
|
23709
|
+
continue;
|
|
23710
|
+
}
|
|
23711
|
+
lines.push(`### ${section.title}`);
|
|
23712
|
+
lines.push("");
|
|
23713
|
+
for (const feature of features) {
|
|
23714
|
+
const summary = feature.summary ? ` \u2014 ${feature.summary}` : "";
|
|
23715
|
+
lines.push(`- **${feature.title}**${summary}`);
|
|
23716
|
+
}
|
|
23717
|
+
lines.push("");
|
|
23718
|
+
}
|
|
23719
|
+
const ungrouped = graph.features.filter((feature) => !sectionMap.has(feature.sectionId));
|
|
23720
|
+
if (ungrouped.length > 0) {
|
|
23721
|
+
for (const feature of ungrouped) {
|
|
23722
|
+
const summary = feature.summary ? ` \u2014 ${feature.summary}` : "";
|
|
23723
|
+
lines.push(`- **${feature.title}**${summary}`);
|
|
23724
|
+
}
|
|
23725
|
+
lines.push("");
|
|
23726
|
+
}
|
|
23727
|
+
return lines.join("\n");
|
|
23728
|
+
}
|
|
23729
|
+
async function syncFeaturesToKnowledge(root, graph) {
|
|
23730
|
+
const featuresPath = import_node_path15.default.join(root, ...KNOWLEDGE_FEATURES_PATH_SEGMENTS);
|
|
23731
|
+
if (!(0, import_node_fs9.existsSync)(featuresPath)) {
|
|
23732
|
+
return;
|
|
23733
|
+
}
|
|
23734
|
+
const content3 = buildKnowledgeFeaturesSection(graph);
|
|
23735
|
+
const wrapped = `${FEATURES_MARKER_BEGIN}
|
|
23736
|
+
${content3.trimEnd()}
|
|
23737
|
+
|
|
23738
|
+
${FEATURES_MARKER_END}
|
|
23739
|
+
`;
|
|
23740
|
+
const current = await import_promises11.default.readFile(featuresPath, "utf8");
|
|
23741
|
+
const beginIndex = current.indexOf(FEATURES_MARKER_BEGIN);
|
|
23742
|
+
const endIndex = current.indexOf(FEATURES_MARKER_END);
|
|
23743
|
+
if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
|
|
23744
|
+
const replaceEnd = current[endIndex + FEATURES_MARKER_END.length] === "\n" ? endIndex + FEATURES_MARKER_END.length + 1 : endIndex + FEATURES_MARKER_END.length;
|
|
23745
|
+
const next = `${current.slice(0, beginIndex)}${wrapped}${current.slice(replaceEnd)}`;
|
|
23746
|
+
if (next !== current) {
|
|
23747
|
+
await import_promises11.default.writeFile(featuresPath, next, "utf8");
|
|
23748
|
+
}
|
|
23749
|
+
return;
|
|
23750
|
+
}
|
|
23751
|
+
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
23752
|
+
await import_promises11.default.writeFile(featuresPath, `${current}${separator}${wrapped}`, "utf8");
|
|
23753
|
+
}
|
|
23754
|
+
async function writeProjectPlanFiles(root, graph) {
|
|
23755
|
+
const graphPath = resolveProjectPlanGraphPath(root);
|
|
23756
|
+
const markdownPath = resolveProjectPlanMarkdownPath(root);
|
|
23757
|
+
await import_promises11.default.mkdir(import_node_path15.default.dirname(graphPath), {
|
|
23758
|
+
recursive: true
|
|
23759
|
+
});
|
|
23760
|
+
const sortedGraph = sortGraph(graph);
|
|
23761
|
+
await import_promises11.default.writeFile(graphPath, `${JSON.stringify(sortedGraph, null, 2)}
|
|
23762
|
+
`, "utf8");
|
|
23763
|
+
await import_promises11.default.writeFile(markdownPath, await renderProjectPlanMarkdown(root, sortedGraph), "utf8");
|
|
23764
|
+
await syncFeaturesToKnowledge(root, sortedGraph);
|
|
23765
|
+
return {
|
|
23766
|
+
graph: sortedGraph,
|
|
23767
|
+
graphPath,
|
|
23768
|
+
markdownPath
|
|
23769
|
+
};
|
|
23770
|
+
}
|
|
23771
|
+
function buildProjectPlanCheckError(code, message, target) {
|
|
23772
|
+
return {
|
|
23773
|
+
code,
|
|
23774
|
+
message,
|
|
23775
|
+
target
|
|
23776
|
+
};
|
|
23777
|
+
}
|
|
23778
|
+
async function validateProjectPlanGraph(root, graph) {
|
|
23779
|
+
const currentGraph = graph ?? await readProjectPlanGraph(root);
|
|
23471
23780
|
const errors = [];
|
|
23472
|
-
const knowledgeSections = await readKnowledgeSections(root);
|
|
23473
|
-
const knowledgeSectionIds = new Set(knowledgeSections.map((section) => section.id));
|
|
23474
23781
|
const sectionIds = /* @__PURE__ */ new Set();
|
|
23475
23782
|
const featureIds = /* @__PURE__ */ new Set();
|
|
23476
23783
|
const featureMap = /* @__PURE__ */ new Map();
|
|
23477
23784
|
for (const section of currentGraph.sections) {
|
|
23478
|
-
if (!section.
|
|
23479
|
-
errors.push(buildProjectPlanCheckError("section_missing_id", "Project plan section is missing `
|
|
23785
|
+
if (!section.id) {
|
|
23786
|
+
errors.push(buildProjectPlanCheckError("section_missing_id", "Project plan section is missing `id`."));
|
|
23480
23787
|
continue;
|
|
23481
23788
|
}
|
|
23482
|
-
if (sectionIds.has(section.
|
|
23483
|
-
errors.push(buildProjectPlanCheckError("duplicate_section", `Duplicate section id "${section.
|
|
23789
|
+
if (sectionIds.has(section.id)) {
|
|
23790
|
+
errors.push(buildProjectPlanCheckError("duplicate_section", `Duplicate section id "${section.id}"`, section.id));
|
|
23484
23791
|
continue;
|
|
23485
23792
|
}
|
|
23486
|
-
sectionIds.add(section.
|
|
23487
|
-
if (!
|
|
23488
|
-
errors.push(buildProjectPlanCheckError(
|
|
23489
|
-
|
|
23490
|
-
|
|
23491
|
-
|
|
23492
|
-
));
|
|
23793
|
+
sectionIds.add(section.id);
|
|
23794
|
+
if (!section.title.trim()) {
|
|
23795
|
+
errors.push(buildProjectPlanCheckError("section_missing_title", `Section "${section.id}" is missing a title.`, section.id));
|
|
23796
|
+
}
|
|
23797
|
+
if (!section.slug.trim()) {
|
|
23798
|
+
errors.push(buildProjectPlanCheckError("section_missing_slug", `Section "${section.id}" is missing a slug.`, section.id));
|
|
23493
23799
|
}
|
|
23494
23800
|
}
|
|
23495
23801
|
const allowedTypes = new Set(PROJECT_TASK_TYPES);
|
|
@@ -23539,395 +23845,159 @@ async function validateProjectPlanGraph(root, graph) {
|
|
|
23539
23845
|
if (!feature) {
|
|
23540
23846
|
errors.push(buildProjectPlanCheckError(
|
|
23541
23847
|
"task_unknown_feature",
|
|
23542
|
-
`Task "${task.id}" references unknown feature "${task.featureId}".`,
|
|
23543
|
-
task.id
|
|
23544
|
-
));
|
|
23545
|
-
continue;
|
|
23546
|
-
}
|
|
23547
|
-
if (!task.title.trim()) {
|
|
23548
|
-
errors.push(buildProjectPlanCheckError("task_missing_title", `Task "${task.id}" is missing a title.`, task.id));
|
|
23549
|
-
}
|
|
23550
|
-
if (!allowedTypes.has(task.type)) {
|
|
23551
|
-
errors.push(buildProjectPlanCheckError(
|
|
23552
|
-
"task_invalid_type",
|
|
23553
|
-
`Task "${task.id}" has invalid type "${task.type}".`,
|
|
23554
|
-
task.id
|
|
23555
|
-
));
|
|
23556
|
-
}
|
|
23557
|
-
if (!allowedAssignees.has(task.assignee)) {
|
|
23558
|
-
errors.push(buildProjectPlanCheckError(
|
|
23559
|
-
"task_invalid_assignee",
|
|
23560
|
-
`Task "${task.id}" has invalid assignee "${task.assignee}".`,
|
|
23561
|
-
task.id
|
|
23562
|
-
));
|
|
23563
|
-
}
|
|
23564
|
-
if (!allowedStatuses.has(task.status)) {
|
|
23565
|
-
errors.push(buildProjectPlanCheckError(
|
|
23566
|
-
"task_invalid_status",
|
|
23567
|
-
`Task "${task.id}" has invalid status "${task.status}".`,
|
|
23568
|
-
task.id
|
|
23569
|
-
));
|
|
23570
|
-
}
|
|
23571
|
-
if (!sectionIds.has(task.sectionId)) {
|
|
23572
|
-
errors.push(buildProjectPlanCheckError(
|
|
23573
|
-
"task_unknown_section",
|
|
23574
|
-
`Task "${task.id}" references unknown section "${task.sectionId}".`,
|
|
23575
|
-
task.id
|
|
23576
|
-
));
|
|
23577
|
-
}
|
|
23578
|
-
if (task.sectionId !== feature.sectionId) {
|
|
23579
|
-
errors.push(buildProjectPlanCheckError(
|
|
23580
|
-
"task_section_mismatch",
|
|
23581
|
-
`Task "${task.id}" section "${task.sectionId}" does not match feature "${feature.id}" section "${feature.sectionId}".`,
|
|
23582
|
-
task.id
|
|
23583
|
-
));
|
|
23584
|
-
}
|
|
23585
|
-
}
|
|
23586
|
-
return {
|
|
23587
|
-
ok: errors.length === 0,
|
|
23588
|
-
errors
|
|
23589
|
-
};
|
|
23590
|
-
}
|
|
23591
|
-
async function resolveKnowledgeSectionIds(root) {
|
|
23592
|
-
const sections = await readKnowledgeSections(root);
|
|
23593
|
-
return sections.map((section) => section.id).sort(compareStrings);
|
|
23594
|
-
}
|
|
23595
|
-
async function syncProjectPlanSectionsFromKnowledge(root) {
|
|
23596
|
-
const currentGraph = await readProjectPlanGraph(root);
|
|
23597
|
-
const knowledgeSectionIds = await resolveKnowledgeSectionIds(root);
|
|
23598
|
-
const syncedGraph = {
|
|
23599
|
-
...currentGraph,
|
|
23600
|
-
sections: knowledgeSectionIds.map((sectionId) => ({
|
|
23601
|
-
sectionId
|
|
23602
|
-
}))
|
|
23603
|
-
};
|
|
23604
|
-
return writeProjectPlanFiles(root, syncedGraph);
|
|
23605
|
-
}
|
|
23606
|
-
async function ensureProjectPlanGraph(root) {
|
|
23607
|
-
const graphPath = resolveProjectPlanGraphPath(root);
|
|
23608
|
-
if (!(0, import_node_fs8.existsSync)(graphPath)) {
|
|
23609
|
-
return syncProjectPlanSectionsFromKnowledge(root);
|
|
23610
|
-
}
|
|
23611
|
-
const graph = await readProjectPlanGraph(root);
|
|
23612
|
-
return writeProjectPlanFiles(root, graph);
|
|
23613
|
-
}
|
|
23614
|
-
async function upsertProjectPlanFeature(params) {
|
|
23615
|
-
const state = await ensureProjectPlanGraph(params.root);
|
|
23616
|
-
const slug = slugify(params.slug?.trim() || params.title);
|
|
23617
|
-
const featureId = params.featureId?.trim() || normalizeFeatureId(params.sectionId, slug);
|
|
23618
|
-
const graph = state.graph;
|
|
23619
|
-
const existing = graph.features.find((feature) => feature.id === featureId) || graph.features.find((feature) => feature.sectionId === params.sectionId && feature.slug === slug);
|
|
23620
|
-
const nextFeature = {
|
|
23621
|
-
id: existing?.id || featureId,
|
|
23622
|
-
title: params.title.trim(),
|
|
23623
|
-
slug,
|
|
23624
|
-
summary: params.summary?.trim() || "",
|
|
23625
|
-
sectionId: params.sectionId
|
|
23626
|
-
};
|
|
23627
|
-
const nextFeatures = existing ? graph.features.map((feature) => feature.id === existing.id ? nextFeature : feature) : [...graph.features, nextFeature];
|
|
23628
|
-
await writeProjectPlanFiles(params.root, {
|
|
23629
|
-
...graph,
|
|
23630
|
-
features: nextFeatures
|
|
23631
|
-
});
|
|
23632
|
-
return nextFeature;
|
|
23633
|
-
}
|
|
23634
|
-
async function getProjectPlanTask(params) {
|
|
23635
|
-
const graph = await readProjectPlanGraph(params.root);
|
|
23636
|
-
const linkedTodos = await listLinkedTodosByTask(params.root);
|
|
23637
|
-
const task = graph.tasks.find((item) => item.id === params.taskId);
|
|
23638
|
-
if (!task) {
|
|
23639
|
-
return null;
|
|
23640
|
-
}
|
|
23641
|
-
const taskLinkedTodos = linkedTodos.get(task.id) || [];
|
|
23642
|
-
return {
|
|
23643
|
-
...task,
|
|
23644
|
-
linkedTodos: taskLinkedTodos,
|
|
23645
|
-
linkedTodoCount: taskLinkedTodos.length
|
|
23646
|
-
};
|
|
23647
|
-
}
|
|
23648
|
-
async function upsertProjectPlanTask(params) {
|
|
23649
|
-
const state = await ensureProjectPlanGraph(params.root);
|
|
23650
|
-
const graph = state.graph;
|
|
23651
|
-
const feature = graph.features.find((item) => item.id === params.featureId);
|
|
23652
|
-
if (!feature) {
|
|
23653
|
-
throw new Error(`Feature "${params.featureId}" not found.`);
|
|
23654
|
-
}
|
|
23655
|
-
const sectionId = params.sectionId?.trim() || feature.sectionId;
|
|
23656
|
-
const taskId = params.taskId?.trim() || normalizeTaskId(feature.id, params.type, params.title);
|
|
23657
|
-
const existing = graph.tasks.find((task) => task.id === taskId) || graph.tasks.find((task) => task.featureId === feature.id && task.type === params.type && task.title === params.title.trim());
|
|
23658
|
-
const nextTask = {
|
|
23659
|
-
id: existing?.id || taskId,
|
|
23660
|
-
title: params.title.trim(),
|
|
23661
|
-
summary: params.summary?.trim() || "",
|
|
23662
|
-
type: params.type,
|
|
23663
|
-
assignee: params.assignee,
|
|
23664
|
-
status: params.status || "planned",
|
|
23665
|
-
acceptanceCriteria: [...new Set((params.acceptanceCriteria || []).map((item) => item.trim()).filter(Boolean))],
|
|
23666
|
-
featureId: feature.id,
|
|
23667
|
-
sectionId
|
|
23668
|
-
};
|
|
23669
|
-
const nextTasks = existing ? graph.tasks.map((task) => task.id === existing.id ? nextTask : task) : [...graph.tasks, nextTask];
|
|
23670
|
-
await writeProjectPlanFiles(params.root, {
|
|
23671
|
-
...graph,
|
|
23672
|
-
tasks: nextTasks
|
|
23673
|
-
});
|
|
23674
|
-
return nextTask;
|
|
23675
|
-
}
|
|
23676
|
-
async function updateProjectPlanTaskStatus(params) {
|
|
23677
|
-
const graph = await readProjectPlanGraph(params.root);
|
|
23678
|
-
const task = graph.tasks.find((item) => item.id === params.taskId);
|
|
23679
|
-
if (!task) {
|
|
23680
|
-
throw new Error(`Task "${params.taskId}" not found.`);
|
|
23681
|
-
}
|
|
23682
|
-
const nextTask = {
|
|
23683
|
-
...task,
|
|
23684
|
-
status: params.status
|
|
23685
|
-
};
|
|
23686
|
-
await writeProjectPlanFiles(params.root, {
|
|
23687
|
-
...graph,
|
|
23688
|
-
tasks: graph.tasks.map((item) => item.id === task.id ? nextTask : item)
|
|
23689
|
-
});
|
|
23690
|
-
return nextTask;
|
|
23691
|
-
}
|
|
23692
|
-
|
|
23693
|
-
// src/knowledge/init.ts
|
|
23694
|
-
var MARKER_BEGIN = "%% docyrus-knowledge:begin %%";
|
|
23695
|
-
var MARKER_END = "%% docyrus-knowledge:end %%";
|
|
23696
|
-
var HOOK_MARKER_BEGIN = "# docyrus-knowledge:begin";
|
|
23697
|
-
var HOOK_MARKER_END = "# docyrus-knowledge:end";
|
|
23698
|
-
function wrapManagedBlock(content3) {
|
|
23699
|
-
return `${MARKER_BEGIN}
|
|
23700
|
-
${content3}${content3.endsWith("\n") ? "" : "\n"}${MARKER_END}
|
|
23701
|
-
`;
|
|
23702
|
-
}
|
|
23703
|
-
async function ensureDirectory(pathValue) {
|
|
23704
|
-
await (0, import_promises11.mkdir)(pathValue, {
|
|
23705
|
-
recursive: true,
|
|
23706
|
-
mode: 493
|
|
23707
|
-
});
|
|
23708
|
-
}
|
|
23709
|
-
async function writeIfMissing(filePath, content3) {
|
|
23710
|
-
if ((0, import_node_fs9.existsSync)(filePath)) {
|
|
23711
|
-
return "kept";
|
|
23712
|
-
}
|
|
23713
|
-
await ensureDirectory((0, import_node_path15.dirname)(filePath));
|
|
23714
|
-
await (0, import_promises11.writeFile)(filePath, content3, "utf8");
|
|
23715
|
-
return "created";
|
|
23716
|
-
}
|
|
23717
|
-
async function writeOrUpdate(filePath, content3) {
|
|
23718
|
-
if (!(0, import_node_fs9.existsSync)(filePath)) {
|
|
23719
|
-
await ensureDirectory((0, import_node_path15.dirname)(filePath));
|
|
23720
|
-
await (0, import_promises11.writeFile)(filePath, content3, "utf8");
|
|
23721
|
-
return "created";
|
|
23722
|
-
}
|
|
23723
|
-
const current = await (0, import_promises11.readFile)(filePath, "utf8");
|
|
23724
|
-
if (current === content3) {
|
|
23725
|
-
return "kept";
|
|
23726
|
-
}
|
|
23727
|
-
await ensureDirectory((0, import_node_path15.dirname)(filePath));
|
|
23728
|
-
await (0, import_promises11.writeFile)(filePath, content3, "utf8");
|
|
23729
|
-
return "updated";
|
|
23730
|
-
}
|
|
23731
|
-
async function upsertManagedBlock(filePath, content3) {
|
|
23732
|
-
const wrapped = wrapManagedBlock(content3);
|
|
23733
|
-
if (!(0, import_node_fs9.existsSync)(filePath)) {
|
|
23734
|
-
await ensureDirectory((0, import_node_path15.dirname)(filePath));
|
|
23735
|
-
await (0, import_promises11.writeFile)(filePath, wrapped, "utf8");
|
|
23736
|
-
return "created";
|
|
23737
|
-
}
|
|
23738
|
-
const current = await (0, import_promises11.readFile)(filePath, "utf8");
|
|
23739
|
-
const beginIndex = current.indexOf(MARKER_BEGIN);
|
|
23740
|
-
const endIndex = current.indexOf(MARKER_END);
|
|
23741
|
-
if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
|
|
23742
|
-
const replaceEnd = current[endIndex + MARKER_END.length] === "\n" ? endIndex + MARKER_END.length + 1 : endIndex + MARKER_END.length;
|
|
23743
|
-
const updated = `${current.slice(0, beginIndex)}${wrapped}${current.slice(replaceEnd)}`;
|
|
23744
|
-
await (0, import_promises11.writeFile)(filePath, updated, "utf8");
|
|
23745
|
-
return "updated";
|
|
23746
|
-
}
|
|
23747
|
-
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
23748
|
-
await (0, import_promises11.writeFile)(filePath, `${current}${separator}${wrapped}`, "utf8");
|
|
23749
|
-
return "appended";
|
|
23750
|
-
}
|
|
23751
|
-
function normalizeObject(value2) {
|
|
23752
|
-
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) ? { ...value2 } : {};
|
|
23753
|
-
}
|
|
23754
|
-
async function updateJsonFile(filePath, updater) {
|
|
23755
|
-
const current = (0, import_node_fs9.existsSync)(filePath) ? normalizeObject(JSON.parse((0, import_node_fs9.readFileSync)(filePath, "utf8"))) : {};
|
|
23756
|
-
await ensureDirectory((0, import_node_path15.dirname)(filePath));
|
|
23757
|
-
await (0, import_promises11.writeFile)(filePath, `${JSON.stringify(updater(current), null, 2)}
|
|
23758
|
-
`, "utf8");
|
|
23759
|
-
}
|
|
23760
|
-
function wrapHookBlock(content3) {
|
|
23761
|
-
return `${HOOK_MARKER_BEGIN}
|
|
23762
|
-
${content3}${content3.endsWith("\n") ? "" : "\n"}${HOOK_MARKER_END}
|
|
23763
|
-
`;
|
|
23764
|
-
}
|
|
23765
|
-
async function upsertShellHook(filePath, content3, shellHeader) {
|
|
23766
|
-
const wrapped = wrapHookBlock(content3);
|
|
23767
|
-
if (!(0, import_node_fs9.existsSync)(filePath)) {
|
|
23768
|
-
await ensureDirectory((0, import_node_path15.dirname)(filePath));
|
|
23769
|
-
const initial = `${shellHeader || ""}${wrapped}`;
|
|
23770
|
-
await (0, import_promises11.writeFile)(filePath, initial, {
|
|
23771
|
-
encoding: "utf8",
|
|
23772
|
-
mode: 493
|
|
23773
|
-
});
|
|
23774
|
-
await (0, import_promises11.chmod)(filePath, 493);
|
|
23775
|
-
return "created";
|
|
23776
|
-
}
|
|
23777
|
-
const current = await (0, import_promises11.readFile)(filePath, "utf8");
|
|
23778
|
-
const beginIndex = current.indexOf(HOOK_MARKER_BEGIN);
|
|
23779
|
-
const endIndex = current.indexOf(HOOK_MARKER_END);
|
|
23780
|
-
if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
|
|
23781
|
-
const replaceEnd = current[endIndex + HOOK_MARKER_END.length] === "\n" ? endIndex + HOOK_MARKER_END.length + 1 : endIndex + HOOK_MARKER_END.length;
|
|
23782
|
-
const nextContent = `${current.slice(0, beginIndex)}${wrapped}${current.slice(replaceEnd)}`;
|
|
23783
|
-
if (nextContent === current) {
|
|
23784
|
-
return "kept";
|
|
23785
|
-
}
|
|
23786
|
-
await (0, import_promises11.writeFile)(filePath, nextContent, "utf8");
|
|
23787
|
-
await (0, import_promises11.chmod)(filePath, 493);
|
|
23788
|
-
return "updated";
|
|
23789
|
-
}
|
|
23790
|
-
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
23791
|
-
await (0, import_promises11.writeFile)(filePath, `${current}${separator}${wrapped}`, "utf8");
|
|
23792
|
-
await (0, import_promises11.chmod)(filePath, 493);
|
|
23793
|
-
return "appended";
|
|
23794
|
-
}
|
|
23795
|
-
function buildKnowledgeDocumentTemplate() {
|
|
23796
|
-
return [
|
|
23797
|
-
"# Knowledge",
|
|
23798
|
-
"",
|
|
23799
|
-
"This directory captures the architecture, constraints, workflows, and test expectations that coding agents should read before changing the repo.",
|
|
23800
|
-
"",
|
|
23801
|
-
"## Working Agreement",
|
|
23802
|
-
"",
|
|
23803
|
-
"Search this knowledge graph before coding, keep it updated when behavior changes, and use \\[\\[wiki links\\]\\] plus `@docyrus` backlinks to connect decisions to implementation.",
|
|
23804
|
-
"",
|
|
23805
|
-
"## Backlinks",
|
|
23806
|
-
"",
|
|
23807
|
-
"Use `// @docyrus: \\[\\[knowledge#Section Name\\]\\]` or `# @docyrus: \\[\\[knowledge#Section Name\\]\\]` near the code or tests that implement a documented behavior.",
|
|
23808
|
-
""
|
|
23809
|
-
].join("\n");
|
|
23810
|
-
}
|
|
23811
|
-
function buildAgentsTemplate(commandPrefix) {
|
|
23812
|
-
return [
|
|
23813
|
-
"# Knowledge Graph Workflow",
|
|
23814
|
-
"",
|
|
23815
|
-
`- If \`docyrus/knowledge/\` exists, run \`${commandPrefix} knowledge search\` before coding so you start from documented intent instead of rediscovering it from source files.`,
|
|
23816
|
-
`- Use \`${commandPrefix} knowledge expand\` when prompts contain \`[[refs]]\` so section names resolve to real file locations and summaries.`,
|
|
23817
|
-
"- Keep `docyrus/knowledge/` in sync whenever you change functionality, architecture, tests, or behavior.",
|
|
23818
|
-
`- Before finishing, run \`${commandPrefix} knowledge check\` and fix any broken links, stale references, or missing \`@docyrus\` backlinks.`,
|
|
23819
|
-
""
|
|
23820
|
-
].join("\n");
|
|
23821
|
-
}
|
|
23822
|
-
function buildCursorRulesTemplate(commandPrefix) {
|
|
23823
|
-
return [
|
|
23824
|
-
"# Knowledge Graph Workflow",
|
|
23825
|
-
"",
|
|
23826
|
-
`- Use \`${commandPrefix} knowledge search\` to find relevant documentation before reading or editing code.`,
|
|
23827
|
-
`- Use \`${commandPrefix} knowledge section\` to read the full section once you have a relevant section id.`,
|
|
23828
|
-
`- Use \`${commandPrefix} knowledge expand\` whenever the prompt includes \`[[refs]]\`.`,
|
|
23829
|
-
`- Keep \`docyrus/knowledge/\` updated and finish with \`${commandPrefix} knowledge check\`.`,
|
|
23830
|
-
""
|
|
23831
|
-
].join("\n");
|
|
23832
|
-
}
|
|
23833
|
-
function buildClaudeHookCommand(commandPrefix, event) {
|
|
23834
|
-
return `${commandPrefix} knowledge hook claude ${event}`;
|
|
23835
|
-
}
|
|
23836
|
-
function syncClaudeHooks(settings, commandPrefix) {
|
|
23837
|
-
const hooks = normalizeObject(settings.hooks);
|
|
23838
|
-
const nextHooks = { ...hooks };
|
|
23839
|
-
for (const [eventName, entries] of Object.entries(nextHooks)) {
|
|
23840
|
-
if (!Array.isArray(entries)) {
|
|
23848
|
+
`Task "${task.id}" references unknown feature "${task.featureId}".`,
|
|
23849
|
+
task.id
|
|
23850
|
+
));
|
|
23841
23851
|
continue;
|
|
23842
23852
|
}
|
|
23843
|
-
|
|
23844
|
-
|
|
23845
|
-
|
|
23846
|
-
|
|
23853
|
+
if (!task.title.trim()) {
|
|
23854
|
+
errors.push(buildProjectPlanCheckError("task_missing_title", `Task "${task.id}" is missing a title.`, task.id));
|
|
23855
|
+
}
|
|
23856
|
+
if (!allowedTypes.has(task.type)) {
|
|
23857
|
+
errors.push(buildProjectPlanCheckError(
|
|
23858
|
+
"task_invalid_type",
|
|
23859
|
+
`Task "${task.id}" has invalid type "${task.type}".`,
|
|
23860
|
+
task.id
|
|
23861
|
+
));
|
|
23862
|
+
}
|
|
23863
|
+
if (!allowedAssignees.has(task.assignee)) {
|
|
23864
|
+
errors.push(buildProjectPlanCheckError(
|
|
23865
|
+
"task_invalid_assignee",
|
|
23866
|
+
`Task "${task.id}" has invalid assignee "${task.assignee}".`,
|
|
23867
|
+
task.id
|
|
23868
|
+
));
|
|
23869
|
+
}
|
|
23870
|
+
if (!allowedStatuses.has(task.status)) {
|
|
23871
|
+
errors.push(buildProjectPlanCheckError(
|
|
23872
|
+
"task_invalid_status",
|
|
23873
|
+
`Task "${task.id}" has invalid status "${task.status}".`,
|
|
23874
|
+
task.id
|
|
23875
|
+
));
|
|
23876
|
+
}
|
|
23877
|
+
if (!sectionIds.has(task.sectionId)) {
|
|
23878
|
+
errors.push(buildProjectPlanCheckError(
|
|
23879
|
+
"task_unknown_section",
|
|
23880
|
+
`Task "${task.id}" references unknown section "${task.sectionId}".`,
|
|
23881
|
+
task.id
|
|
23882
|
+
));
|
|
23883
|
+
}
|
|
23884
|
+
if (task.sectionId !== feature.sectionId) {
|
|
23885
|
+
errors.push(buildProjectPlanCheckError(
|
|
23886
|
+
"task_section_mismatch",
|
|
23887
|
+
`Task "${task.id}" section "${task.sectionId}" does not match feature "${feature.id}" section "${feature.sectionId}".`,
|
|
23888
|
+
task.id
|
|
23889
|
+
));
|
|
23890
|
+
}
|
|
23847
23891
|
}
|
|
23848
|
-
const addHook = (eventName) => {
|
|
23849
|
-
const current = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : [];
|
|
23850
|
-
current.push({
|
|
23851
|
-
hooks: [
|
|
23852
|
-
{
|
|
23853
|
-
type: "command",
|
|
23854
|
-
command: buildClaudeHookCommand(commandPrefix, eventName)
|
|
23855
|
-
}
|
|
23856
|
-
]
|
|
23857
|
-
});
|
|
23858
|
-
nextHooks[eventName] = current;
|
|
23859
|
-
};
|
|
23860
|
-
addHook("UserPromptSubmit");
|
|
23861
|
-
addHook("Stop");
|
|
23862
23892
|
return {
|
|
23863
|
-
|
|
23864
|
-
|
|
23893
|
+
ok: errors.length === 0,
|
|
23894
|
+
errors
|
|
23865
23895
|
};
|
|
23866
23896
|
}
|
|
23867
|
-
function
|
|
23868
|
-
const
|
|
23869
|
-
|
|
23870
|
-
|
|
23897
|
+
async function ensureProjectPlanGraph(root) {
|
|
23898
|
+
const graphPath = resolveProjectPlanGraphPath(root);
|
|
23899
|
+
if (!(0, import_node_fs9.existsSync)(graphPath)) {
|
|
23900
|
+
return writeProjectPlanFiles(root, createEmptyProjectPlanGraph());
|
|
23901
|
+
}
|
|
23902
|
+
const graph = await readProjectPlanGraph(root);
|
|
23903
|
+
return writeProjectPlanFiles(root, graph);
|
|
23904
|
+
}
|
|
23905
|
+
async function upsertProjectPlanSection(params) {
|
|
23906
|
+
const state = await ensureProjectPlanGraph(params.root);
|
|
23907
|
+
const slug = slugify(params.slug?.trim() || params.title);
|
|
23908
|
+
const sectionId = params.id?.trim() || normalizeSectionId(slug);
|
|
23909
|
+
const graph = state.graph;
|
|
23910
|
+
const existing = graph.sections.find((section) => section.id === sectionId) || graph.sections.find((section) => section.slug === slug);
|
|
23911
|
+
const nextSection = {
|
|
23912
|
+
id: existing?.id || sectionId,
|
|
23913
|
+
title: params.title.trim(),
|
|
23914
|
+
slug,
|
|
23915
|
+
summary: params.summary?.trim() || ""
|
|
23916
|
+
};
|
|
23917
|
+
const nextSections = existing ? graph.sections.map((section) => section.id === existing.id ? nextSection : section) : [...graph.sections, nextSection];
|
|
23918
|
+
await writeProjectPlanFiles(params.root, {
|
|
23919
|
+
...graph,
|
|
23920
|
+
sections: nextSections
|
|
23921
|
+
});
|
|
23922
|
+
return nextSection;
|
|
23923
|
+
}
|
|
23924
|
+
async function upsertProjectPlanFeature(params) {
|
|
23925
|
+
const state = await ensureProjectPlanGraph(params.root);
|
|
23926
|
+
const slug = slugify(params.slug?.trim() || params.title);
|
|
23927
|
+
const featureId = params.featureId?.trim() || normalizeFeatureId(params.sectionId, slug);
|
|
23928
|
+
const graph = state.graph;
|
|
23929
|
+
const existing = graph.features.find((feature) => feature.id === featureId) || graph.features.find((feature) => feature.sectionId === params.sectionId && feature.slug === slug);
|
|
23930
|
+
const nextFeature = {
|
|
23931
|
+
id: existing?.id || featureId,
|
|
23932
|
+
title: params.title.trim(),
|
|
23933
|
+
slug,
|
|
23934
|
+
summary: params.summary?.trim() || "",
|
|
23935
|
+
sectionId: params.sectionId
|
|
23936
|
+
};
|
|
23937
|
+
const nextFeatures = existing ? graph.features.map((feature) => feature.id === existing.id ? nextFeature : feature) : [...graph.features, nextFeature];
|
|
23938
|
+
await writeProjectPlanFiles(params.root, {
|
|
23939
|
+
...graph,
|
|
23940
|
+
features: nextFeatures
|
|
23941
|
+
});
|
|
23942
|
+
return nextFeature;
|
|
23943
|
+
}
|
|
23944
|
+
async function getProjectPlanTask(params) {
|
|
23945
|
+
const graph = await readProjectPlanGraph(params.root);
|
|
23946
|
+
const linkedTodos = await listLinkedTodosByTask(params.root);
|
|
23947
|
+
const task = graph.tasks.find((item) => item.id === params.taskId);
|
|
23948
|
+
if (!task) {
|
|
23949
|
+
return null;
|
|
23950
|
+
}
|
|
23951
|
+
const taskLinkedTodos = linkedTodos.get(task.id) || [];
|
|
23871
23952
|
return {
|
|
23872
|
-
...
|
|
23873
|
-
|
|
23874
|
-
|
|
23875
|
-
...hooks,
|
|
23876
|
-
stop: [
|
|
23877
|
-
...filtered,
|
|
23878
|
-
{
|
|
23879
|
-
command: `${commandPrefix} knowledge hook cursor stop`
|
|
23880
|
-
}
|
|
23881
|
-
]
|
|
23882
|
-
}
|
|
23953
|
+
...task,
|
|
23954
|
+
linkedTodos: taskLinkedTodos,
|
|
23955
|
+
linkedTodoCount: taskLinkedTodos.length
|
|
23883
23956
|
};
|
|
23884
23957
|
}
|
|
23885
|
-
async function
|
|
23886
|
-
const
|
|
23887
|
-
const
|
|
23888
|
-
const
|
|
23889
|
-
|
|
23890
|
-
|
|
23891
|
-
`);
|
|
23892
|
-
const agentsStatus = await upsertManagedBlock((0, import_node_path15.join)(root, "AGENTS.md"), buildAgentsTemplate(params.commandPrefix));
|
|
23893
|
-
const claudeStatus = await upsertManagedBlock((0, import_node_path15.join)(root, "CLAUDE.md"), buildAgentsTemplate(params.commandPrefix));
|
|
23894
|
-
const cursorRulesStatus = await writeOrUpdate((0, import_node_path15.join)(root, ".cursor", "rules", "docyrus-knowledge.md"), `${buildCursorRulesTemplate(params.commandPrefix)}
|
|
23895
|
-
`);
|
|
23896
|
-
const bootstrap = (await generateInitialKnowledge({
|
|
23897
|
-
root,
|
|
23898
|
-
brief: params.brief
|
|
23899
|
-
})).files;
|
|
23900
|
-
const projectPlan = await syncProjectPlanSectionsFromKnowledge(root);
|
|
23901
|
-
await updateJsonFile((0, import_node_path15.join)(root, ".claude", "settings.json"), (value2) => syncClaudeHooks(value2, params.commandPrefix));
|
|
23902
|
-
await updateJsonFile((0, import_node_path15.join)(root, ".cursor", "hooks.json"), (value2) => syncCursorHooks(value2, params.commandPrefix));
|
|
23903
|
-
let huskyHookStatus;
|
|
23904
|
-
const huskyDir = (0, import_node_path15.join)(root, ".husky");
|
|
23905
|
-
if ((0, import_node_fs9.existsSync)(huskyDir)) {
|
|
23906
|
-
const huskyHookPath = (0, import_node_path15.join)(huskyDir, "pre-commit");
|
|
23907
|
-
const huskyHeader = (0, import_node_fs9.existsSync)((0, import_node_path15.join)(huskyDir, "_", "h")) ? '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/h"\n\n' : void 0;
|
|
23908
|
-
huskyHookStatus = await upsertShellHook(huskyHookPath, `${params.commandPrefix} knowledge pre-commit
|
|
23909
|
-
`, huskyHeader);
|
|
23958
|
+
async function upsertProjectPlanTask(params) {
|
|
23959
|
+
const state = await ensureProjectPlanGraph(params.root);
|
|
23960
|
+
const graph = state.graph;
|
|
23961
|
+
const feature = graph.features.find((item) => item.id === params.featureId);
|
|
23962
|
+
if (!feature) {
|
|
23963
|
+
throw new Error(`Feature "${params.featureId}" not found.`);
|
|
23910
23964
|
}
|
|
23911
|
-
|
|
23912
|
-
|
|
23913
|
-
|
|
23914
|
-
|
|
23915
|
-
|
|
23965
|
+
const sectionId = params.sectionId?.trim() || feature.sectionId;
|
|
23966
|
+
const taskId = params.taskId?.trim() || normalizeTaskId(feature.id, params.type, params.title);
|
|
23967
|
+
const existing = graph.tasks.find((task) => task.id === taskId) || graph.tasks.find((task) => task.featureId === feature.id && task.type === params.type && task.title === params.title.trim());
|
|
23968
|
+
const nextTask = {
|
|
23969
|
+
id: existing?.id || taskId,
|
|
23970
|
+
title: params.title.trim(),
|
|
23971
|
+
summary: params.summary?.trim() || "",
|
|
23972
|
+
type: params.type,
|
|
23973
|
+
assignee: params.assignee,
|
|
23974
|
+
status: params.status || "planned",
|
|
23975
|
+
acceptanceCriteria: [...new Set((params.acceptanceCriteria || []).map((item) => item.trim()).filter(Boolean))],
|
|
23976
|
+
featureId: feature.id,
|
|
23977
|
+
sectionId
|
|
23978
|
+
};
|
|
23979
|
+
const nextTasks = existing ? graph.tasks.map((task) => task.id === existing.id ? nextTask : task) : [...graph.tasks, nextTask];
|
|
23980
|
+
await writeProjectPlanFiles(params.root, {
|
|
23981
|
+
...graph,
|
|
23982
|
+
tasks: nextTasks
|
|
23983
|
+
});
|
|
23984
|
+
return nextTask;
|
|
23985
|
+
}
|
|
23986
|
+
async function updateProjectPlanTaskStatus(params) {
|
|
23987
|
+
const graph = await readProjectPlanGraph(params.root);
|
|
23988
|
+
const task = graph.tasks.find((item) => item.id === params.taskId);
|
|
23989
|
+
if (!task) {
|
|
23990
|
+
throw new Error(`Task "${params.taskId}" not found.`);
|
|
23916
23991
|
}
|
|
23917
|
-
|
|
23918
|
-
|
|
23919
|
-
|
|
23920
|
-
knowledgeFile,
|
|
23921
|
-
projectPlanGraphPath: projectPlan.graphPath,
|
|
23922
|
-
projectPlanMarkdownPath: projectPlan.markdownPath,
|
|
23923
|
-
agentsStatus,
|
|
23924
|
-
claudeStatus,
|
|
23925
|
-
cursorRulesStatus,
|
|
23926
|
-
knowledgeFileStatus,
|
|
23927
|
-
bootstrap,
|
|
23928
|
-
huskyHookStatus,
|
|
23929
|
-
gitHookStatus
|
|
23992
|
+
const nextTask = {
|
|
23993
|
+
...task,
|
|
23994
|
+
status: params.status
|
|
23930
23995
|
};
|
|
23996
|
+
await writeProjectPlanFiles(params.root, {
|
|
23997
|
+
...graph,
|
|
23998
|
+
tasks: graph.tasks.map((item) => item.id === task.id ? nextTask : item)
|
|
23999
|
+
});
|
|
24000
|
+
return nextTask;
|
|
23931
24001
|
}
|
|
23932
24002
|
|
|
23933
24003
|
// src/agent/authFlows.ts
|
|
@@ -25366,6 +25436,49 @@ async function waitForIdle(session, timeoutMs = 3e4) {
|
|
|
25366
25436
|
function resolveServerSettingsRootPath(agentDir) {
|
|
25367
25437
|
return (0, import_node_path18.resolve)(agentDir, "..", "..");
|
|
25368
25438
|
}
|
|
25439
|
+
var SESSION_MODE_COMMANDS = {
|
|
25440
|
+
"read-only": "read-only",
|
|
25441
|
+
"end-read-only": "normal"
|
|
25442
|
+
};
|
|
25443
|
+
var SESSION_MODE_COMMAND_LABELS = {
|
|
25444
|
+
"read-only": "Read-only mode activated. Write and edit tools are disabled. Use /end-read-only to resume normal operation.",
|
|
25445
|
+
"end-read-only": "Read-only mode deactivated. Normal operation resumed."
|
|
25446
|
+
};
|
|
25447
|
+
function parseModeSlashCommand(text3) {
|
|
25448
|
+
const match2 = /^\/([a-z][a-z0-9-]*)(?:\s+([\s\S]+))?$/u.exec(text3.trim());
|
|
25449
|
+
if (!match2) {
|
|
25450
|
+
return null;
|
|
25451
|
+
}
|
|
25452
|
+
const command = match2[1];
|
|
25453
|
+
if (!(command in SESSION_MODE_COMMANDS)) {
|
|
25454
|
+
return null;
|
|
25455
|
+
}
|
|
25456
|
+
return {
|
|
25457
|
+
command,
|
|
25458
|
+
remainder: match2[2]?.trim() ?? ""
|
|
25459
|
+
};
|
|
25460
|
+
}
|
|
25461
|
+
function makeModeNotificationStream(params) {
|
|
25462
|
+
const { command, messageId, encoder } = params;
|
|
25463
|
+
const label = SESSION_MODE_COMMAND_LABELS[command] ?? `/${command} executed.`;
|
|
25464
|
+
return new ReadableStream({
|
|
25465
|
+
start(controller) {
|
|
25466
|
+
function write(chunk) {
|
|
25467
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}
|
|
25468
|
+
|
|
25469
|
+
`));
|
|
25470
|
+
}
|
|
25471
|
+
write({ type: "start" });
|
|
25472
|
+
write({ type: "start-step" });
|
|
25473
|
+
write({ type: "text-start", id: messageId });
|
|
25474
|
+
write({ type: "text-delta", id: messageId, delta: label });
|
|
25475
|
+
write({ type: "text-end", id: messageId });
|
|
25476
|
+
write({ type: "finish-step" });
|
|
25477
|
+
write({ type: "finish" });
|
|
25478
|
+
controller.close();
|
|
25479
|
+
}
|
|
25480
|
+
});
|
|
25481
|
+
}
|
|
25369
25482
|
function serializeKnowledgeMatch(match2) {
|
|
25370
25483
|
return {
|
|
25371
25484
|
id: match2.section.id,
|
|
@@ -25660,6 +25773,7 @@ async function walkFiles(params) {
|
|
|
25660
25773
|
async function createAgentServer(params) {
|
|
25661
25774
|
const { port, sessionManager, modelRegistry, authRuntime, context, onCreateSession, onResumeSession, authToken } = params;
|
|
25662
25775
|
let activeSession = params.session;
|
|
25776
|
+
let sessionMode = "normal";
|
|
25663
25777
|
const pendingAskUserRequests = /* @__PURE__ */ new Map();
|
|
25664
25778
|
const pendingDocyrusWebBrowserRequests = /* @__PURE__ */ new Map();
|
|
25665
25779
|
const oauthFlowManager = new OAuthFlowManager();
|
|
@@ -25700,7 +25814,8 @@ async function createAgentServer(params) {
|
|
|
25700
25814
|
app.get("/api/status", (c) => {
|
|
25701
25815
|
return c.json({
|
|
25702
25816
|
isStreaming: activeSession.isStreaming,
|
|
25703
|
-
model: activeSession.model ? { provider: activeSession.model.provider, id: activeSession.model.id } : null
|
|
25817
|
+
model: activeSession.model ? { provider: activeSession.model.provider, id: activeSession.model.id } : null,
|
|
25818
|
+
mode: sessionMode
|
|
25704
25819
|
});
|
|
25705
25820
|
});
|
|
25706
25821
|
app.post("/api/chat", async (c) => {
|
|
@@ -25709,6 +25824,7 @@ async function createAgentServer(params) {
|
|
|
25709
25824
|
if (body2.sessionId && body2.sessionId.trim() !== activeSession.id?.trim()) {
|
|
25710
25825
|
try {
|
|
25711
25826
|
activeSession = await onResumeSession(body2.sessionId);
|
|
25827
|
+
sessionMode = "normal";
|
|
25712
25828
|
} catch (error) {
|
|
25713
25829
|
const msg = error instanceof Error ? error.message : String(error);
|
|
25714
25830
|
return c.json({ error: `Failed to resume session: ${msg}` }, 400);
|
|
@@ -25745,6 +25861,34 @@ async function createAgentServer(params) {
|
|
|
25745
25861
|
pendingDocyrusWebBrowserRequests.delete(sessionId);
|
|
25746
25862
|
}
|
|
25747
25863
|
}
|
|
25864
|
+
let promptText = userMessage;
|
|
25865
|
+
if (!askUserResponse && !docyrusWebBrowserResponse) {
|
|
25866
|
+
const modeCmd = parseModeSlashCommand(userMessage);
|
|
25867
|
+
if (modeCmd) {
|
|
25868
|
+
if (activeSession.isStreaming) {
|
|
25869
|
+
await activeSession.abort();
|
|
25870
|
+
await waitForIdle(activeSession);
|
|
25871
|
+
}
|
|
25872
|
+
await activeSession.prompt(`/${modeCmd.command}`);
|
|
25873
|
+
sessionMode = SESSION_MODE_COMMANDS[modeCmd.command] ?? sessionMode;
|
|
25874
|
+
if (!modeCmd.remainder) {
|
|
25875
|
+
const notifEncoder = new TextEncoder();
|
|
25876
|
+
const notifMessageId = generateMessageId();
|
|
25877
|
+
return c.body(
|
|
25878
|
+
makeModeNotificationStream({ command: modeCmd.command, messageId: notifMessageId, encoder: notifEncoder }),
|
|
25879
|
+
{
|
|
25880
|
+
headers: {
|
|
25881
|
+
"Content-Type": "text/event-stream",
|
|
25882
|
+
"Cache-Control": "no-cache",
|
|
25883
|
+
"Connection": "keep-alive",
|
|
25884
|
+
"x-vercel-ai-ui-message-stream": "v1"
|
|
25885
|
+
}
|
|
25886
|
+
}
|
|
25887
|
+
);
|
|
25888
|
+
}
|
|
25889
|
+
promptText = modeCmd.remainder;
|
|
25890
|
+
}
|
|
25891
|
+
}
|
|
25748
25892
|
if (activeSession.isStreaming) {
|
|
25749
25893
|
await activeSession.abort();
|
|
25750
25894
|
await waitForIdle(activeSession);
|
|
@@ -25789,7 +25933,7 @@ async function createAgentServer(params) {
|
|
|
25789
25933
|
unsubscribe();
|
|
25790
25934
|
}
|
|
25791
25935
|
});
|
|
25792
|
-
activeSession.prompt(
|
|
25936
|
+
activeSession.prompt(promptText).then(() => {
|
|
25793
25937
|
if (!activeSession.isStreaming) {
|
|
25794
25938
|
unsubscribe();
|
|
25795
25939
|
writeChunk({ type: "finish-step" });
|
|
@@ -25815,6 +25959,19 @@ async function createAgentServer(params) {
|
|
|
25815
25959
|
}
|
|
25816
25960
|
});
|
|
25817
25961
|
});
|
|
25962
|
+
app.post("/api/chat/abort", async (c) => {
|
|
25963
|
+
if (!activeSession.isStreaming) {
|
|
25964
|
+
return c.json({ ok: true, wasStreaming: false });
|
|
25965
|
+
}
|
|
25966
|
+
try {
|
|
25967
|
+
await activeSession.abort();
|
|
25968
|
+
await waitForIdle(activeSession);
|
|
25969
|
+
return c.json({ ok: true, wasStreaming: true });
|
|
25970
|
+
} catch (error) {
|
|
25971
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25972
|
+
return c.json({ error: message }, 500);
|
|
25973
|
+
}
|
|
25974
|
+
});
|
|
25818
25975
|
app.get("/api/sessions", async (c) => {
|
|
25819
25976
|
try {
|
|
25820
25977
|
const sessions = await sessionManager.list();
|
|
@@ -25857,6 +26014,7 @@ async function createAgentServer(params) {
|
|
|
25857
26014
|
await waitForIdle(activeSession);
|
|
25858
26015
|
}
|
|
25859
26016
|
activeSession = await onCreateSession();
|
|
26017
|
+
sessionMode = "normal";
|
|
25860
26018
|
const sessionId = activeSession.id?.trim();
|
|
25861
26019
|
if (!sessionId) {
|
|
25862
26020
|
return c.json({ error: "Created session is missing an id" }, 500);
|
|
@@ -26064,8 +26222,7 @@ async function createAgentServer(params) {
|
|
|
26064
26222
|
const codeRefs = await checkKnowledgeCodeRefs(knowledgeDir);
|
|
26065
26223
|
const index2 = await checkKnowledgeIndex(knowledgeDir);
|
|
26066
26224
|
const sections = await checkKnowledgeSections(knowledgeDir);
|
|
26067
|
-
const
|
|
26068
|
-
const totalErrors = markdown.errors.length + codeRefs.errors.length + index2.length + sections.length + projectPlan.errors.length;
|
|
26225
|
+
const totalErrors = markdown.errors.length + codeRefs.errors.length + index2.length + sections.length;
|
|
26069
26226
|
return c.json({
|
|
26070
26227
|
ok: totalErrors === 0,
|
|
26071
26228
|
knowledgeDir,
|
|
@@ -26073,8 +26230,7 @@ async function createAgentServer(params) {
|
|
|
26073
26230
|
markdown: markdown.errors,
|
|
26074
26231
|
codeRefs: codeRefs.errors,
|
|
26075
26232
|
index: index2,
|
|
26076
|
-
sections
|
|
26077
|
-
projectPlan: projectPlan.errors
|
|
26233
|
+
sections
|
|
26078
26234
|
},
|
|
26079
26235
|
files: {
|
|
26080
26236
|
markdown: markdown.files,
|
|
@@ -26110,12 +26266,7 @@ async function createAgentServer(params) {
|
|
|
26110
26266
|
brief: body2.brief?.trim() || void 0,
|
|
26111
26267
|
targetSectionIds: Array.isArray(body2.sectionIds) ? body2.sectionIds : void 0
|
|
26112
26268
|
});
|
|
26113
|
-
|
|
26114
|
-
return c.json({
|
|
26115
|
-
...result,
|
|
26116
|
-
projectPlanGraphPath: projectPlan.graphPath,
|
|
26117
|
-
projectPlanMarkdownPath: projectPlan.markdownPath
|
|
26118
|
-
});
|
|
26269
|
+
return c.json(result);
|
|
26119
26270
|
} catch (error) {
|
|
26120
26271
|
const message = error instanceof Error ? error.message : String(error);
|
|
26121
26272
|
return c.json({ error: message }, 500);
|
|
@@ -26159,17 +26310,23 @@ async function createAgentServer(params) {
|
|
|
26159
26310
|
return c.json({ error: message }, 500);
|
|
26160
26311
|
}
|
|
26161
26312
|
});
|
|
26162
|
-
app.post("/api/project-plan/
|
|
26313
|
+
app.post("/api/project-plan/sections", async (c) => {
|
|
26314
|
+
const body2 = await c.req.json().catch(() => ({}));
|
|
26315
|
+
if (!body2.title?.trim()) {
|
|
26316
|
+
return c.json({ error: "Missing required field: title" }, 400);
|
|
26317
|
+
}
|
|
26163
26318
|
try {
|
|
26164
|
-
const
|
|
26165
|
-
|
|
26166
|
-
|
|
26167
|
-
|
|
26168
|
-
|
|
26319
|
+
const section = await upsertProjectPlanSection({
|
|
26320
|
+
root: context.cwd,
|
|
26321
|
+
id: body2.id?.trim() || void 0,
|
|
26322
|
+
title: body2.title.trim(),
|
|
26323
|
+
slug: body2.slug?.trim() || void 0,
|
|
26324
|
+
summary: body2.summary?.trim() || void 0
|
|
26169
26325
|
});
|
|
26326
|
+
return c.json(section);
|
|
26170
26327
|
} catch (error) {
|
|
26171
26328
|
const message = error instanceof Error ? error.message : String(error);
|
|
26172
|
-
return c.json({ error: message },
|
|
26329
|
+
return c.json({ error: message }, 400);
|
|
26173
26330
|
}
|
|
26174
26331
|
});
|
|
26175
26332
|
app.get("/api/project-plan/tasks/:taskId", async (c) => {
|
|
@@ -26466,6 +26623,20 @@ async function createAgentServer(params) {
|
|
|
26466
26623
|
return c.json({ error: message }, 500);
|
|
26467
26624
|
}
|
|
26468
26625
|
});
|
|
26626
|
+
app.post("/api/sessions/rename", async (c) => {
|
|
26627
|
+
const body2 = await c.req.json();
|
|
26628
|
+
const name2 = typeof body2.name === "string" ? body2.name.trim() : "";
|
|
26629
|
+
if (!name2) {
|
|
26630
|
+
return c.json({ error: "Missing required field: name" }, 400);
|
|
26631
|
+
}
|
|
26632
|
+
try {
|
|
26633
|
+
activeSession.setSessionName(name2);
|
|
26634
|
+
return c.json({ ok: true, name: name2 });
|
|
26635
|
+
} catch (error) {
|
|
26636
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26637
|
+
return c.json({ error: message }, 500);
|
|
26638
|
+
}
|
|
26639
|
+
});
|
|
26469
26640
|
app.get("/api/fs/tree", async (c) => {
|
|
26470
26641
|
const requestPath = c.req.query("path") ?? ".";
|
|
26471
26642
|
const maxDepth = Math.min(Number(c.req.query("depth") ?? 4), 10);
|
|
@@ -27276,6 +27447,8 @@ async function createAgentServer(params) {
|
|
|
27276
27447
|
|
|
27277
27448
|
`);
|
|
27278
27449
|
process.stderr.write(` POST /api/chat \u2014 send chat messages (SSE UIMessage stream)
|
|
27450
|
+
`);
|
|
27451
|
+
process.stderr.write(` POST /api/chat/abort \u2014 abort active chat stream
|
|
27279
27452
|
`);
|
|
27280
27453
|
process.stderr.write(` GET /api/health \u2014 health check
|
|
27281
27454
|
`);
|
|
@@ -27288,6 +27461,8 @@ async function createAgentServer(params) {
|
|
|
27288
27461
|
process.stderr.write(` GET /api/sessions/:sessionId/messages \u2014 session messages
|
|
27289
27462
|
`);
|
|
27290
27463
|
process.stderr.write(` POST /api/sessions/:sessionId/resume \u2014 resume a session
|
|
27464
|
+
`);
|
|
27465
|
+
process.stderr.write(` POST /api/sessions/rename \u2014 rename active session
|
|
27291
27466
|
`);
|
|
27292
27467
|
process.stderr.write(` GET /api/context \u2014 server context
|
|
27293
27468
|
`);
|
|
@@ -27303,11 +27478,11 @@ async function createAgentServer(params) {
|
|
|
27303
27478
|
`);
|
|
27304
27479
|
process.stderr.write(` GET /api/knowledge/search \u2014 semantic knowledge search
|
|
27305
27480
|
`);
|
|
27306
|
-
process.stderr.write(` GET /api/knowledge/check \u2014 knowledge
|
|
27481
|
+
process.stderr.write(` GET /api/knowledge/check \u2014 knowledge validation
|
|
27307
27482
|
`);
|
|
27308
|
-
process.stderr.write(` POST /api/knowledge/init \u2014 initialize knowledge graph
|
|
27483
|
+
process.stderr.write(` POST /api/knowledge/init \u2014 initialize knowledge graph
|
|
27309
27484
|
`);
|
|
27310
|
-
process.stderr.write(` POST /api/knowledge/refresh \u2014 refresh managed knowledge files
|
|
27485
|
+
process.stderr.write(` POST /api/knowledge/refresh \u2014 refresh managed knowledge files
|
|
27311
27486
|
`);
|
|
27312
27487
|
process.stderr.write(` GET /api/project-plan \u2014 canonical project-plan graph and hierarchy
|
|
27313
27488
|
`);
|
|
@@ -27315,7 +27490,7 @@ async function createAgentServer(params) {
|
|
|
27315
27490
|
`);
|
|
27316
27491
|
process.stderr.write(` POST /api/project-plan/ensure \u2014 ensure project-plan files exist
|
|
27317
27492
|
`);
|
|
27318
|
-
process.stderr.write(` POST /api/project-plan/
|
|
27493
|
+
process.stderr.write(` POST /api/project-plan/sections \u2014 upsert project-plan section
|
|
27319
27494
|
`);
|
|
27320
27495
|
process.stderr.write(` GET /api/project-plan/tasks/:taskId \u2014 project task detail
|
|
27321
27496
|
`);
|
|
@@ -27552,6 +27727,12 @@ function createServerSessionAdapter(params) {
|
|
|
27552
27727
|
supportsThinking() {
|
|
27553
27728
|
return params.session.supportsThinking();
|
|
27554
27729
|
},
|
|
27730
|
+
get sessionName() {
|
|
27731
|
+
return params.session.sessionName;
|
|
27732
|
+
},
|
|
27733
|
+
setSessionName(name2) {
|
|
27734
|
+
params.session.setSessionName?.(name2);
|
|
27735
|
+
},
|
|
27555
27736
|
listCommands() {
|
|
27556
27737
|
const getCommands = params.extensionsResult.runtime.getCommands;
|
|
27557
27738
|
return typeof getCommands === "function" ? getCommands() : [];
|