chapterhouse 0.3.20 → 0.3.22

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.
@@ -15,6 +15,9 @@ import { createAuthMiddleware, getBootstrapAuthResponse } from "./auth.js";
15
15
  import { createConcurrentConnectionLimiter, createFixedWindowRateLimiter } from "./rate-limit.js";
16
16
  import { createTeamRouter } from "./team.js";
17
17
  import { writePage, deletePage, pageExists, listPages, ensureWikiStructure, assertPagePath, } from "../wiki/fs.js";
18
+ import { parseWikiFrontmatter } from "../wiki/frontmatter.js";
19
+ import { loadRegistry, saveRegistry } from "../wiki/project-registry.js";
20
+ import { getProjectRulesPath, listTopLevelSoftRules, loadProjectRules, loadProjectRuleSummary, renderInitialProjectRulesPage, saveProjectRulesHardFields, saveProjectRulesSoftRules, } from "../wiki/project-rules.js";
18
21
  import { readWikiPage, teamWikiSync } from "../wiki/team-sync.js";
19
22
  import { withWikiWrite } from "../wiki/lock.js";
20
23
  import { listSkills, removeSkill } from "../copilot/skills.js";
@@ -71,6 +74,56 @@ const autoRequestSchema = z.object({
71
74
  const wikiWriteSchema = z.object({
72
75
  content: z.string({ error: "Missing 'content' string in request body" }),
73
76
  }).strict();
77
+ const projectCreateSchema = z.object({
78
+ slug: requiredString("Missing 'slug' in request body")
79
+ .regex(/^[a-z0-9][a-z0-9-]*$/, "Project slug must be a lowercase slug"),
80
+ cwd: requiredString("Missing 'cwd' in request body")
81
+ .refine((value) => value.startsWith("/"), "Project cwd must be an absolute path"),
82
+ }).strict();
83
+ const projectHardRulesSchema = z.object({
84
+ hardRules: z.object({
85
+ auto_pr: z.boolean({ error: "hardRules.auto_pr must be a boolean" }),
86
+ require_worktree: z.boolean({ error: "hardRules.require_worktree must be a boolean" }),
87
+ pr_draft_default: z.boolean({ error: "hardRules.pr_draft_default must be a boolean" }),
88
+ default_branch: requiredString("hardRules.default_branch must be a non-empty string"),
89
+ commit_co_author: requiredString("hardRules.commit_co_author must be a non-empty string"),
90
+ test_command: z.string({ error: "hardRules.test_command must be a string" }),
91
+ build_command: z.string({ error: "hardRules.build_command must be a string" }),
92
+ lint_command: z.string({ error: "hardRules.lint_command must be a string" }),
93
+ require_clean_worktree: z.boolean({ error: "hardRules.require_clean_worktree must be a boolean" }),
94
+ }).strict(),
95
+ }).strict();
96
+ const projectSoftRulesSchema = z.object({
97
+ softRules: z.array(requiredString("softRules entries must be non-empty strings")),
98
+ }).strict();
99
+ function createWikiPagePayload(path, content) {
100
+ const { parsed: frontmatter, body: renderedContent } = parseWikiFrontmatter(content);
101
+ return {
102
+ path,
103
+ content,
104
+ renderedContent,
105
+ frontmatter,
106
+ };
107
+ }
108
+ function createProjectDetailPayload(slug, cwd) {
109
+ const rules = loadProjectRules(slug);
110
+ if (!rules.found) {
111
+ return {
112
+ slug,
113
+ cwd,
114
+ rulesFound: false,
115
+ hardRules: null,
116
+ softRules: [],
117
+ };
118
+ }
119
+ return {
120
+ slug,
121
+ cwd,
122
+ rulesFound: true,
123
+ hardRules: rules.hard,
124
+ softRules: listTopLevelSoftRules(rules.soft),
125
+ };
126
+ }
74
127
  // Load a configured API token when present; startup validation below enforces auth.
75
128
  let apiToken = null;
76
129
  try {
@@ -288,7 +341,7 @@ app.get("/api/workers", (_req, res) => {
288
341
  app.get("/api/workers/:taskId", (req, res) => {
289
342
  const taskId = req.params.taskId;
290
343
  const row = getDb()
291
- .prepare(`SELECT task_id, agent_slug, description, status, result, started_at, completed_at
344
+ .prepare(`SELECT task_id, agent_slug, description, prompt, status, result, started_at, completed_at
292
345
  FROM agent_tasks WHERE task_id = ?`)
293
346
  .get(taskId);
294
347
  if (!row) {
@@ -301,6 +354,7 @@ app.get("/api/workers/:taskId", (req, res) => {
301
354
  agentSlug: row.agent_slug,
302
355
  name: agent?.name || row.agent_slug,
303
356
  description: row.description,
357
+ prompt: row.prompt,
304
358
  status: row.status,
305
359
  result: row.result,
306
360
  startedAt: row.started_at,
@@ -711,6 +765,103 @@ app.post("/api/auto", (req, res) => {
711
765
  log.info({ enabled: updated.enabled }, "Auto-routing updated");
712
766
  res.json(updated);
713
767
  });
768
+ app.get("/api/projects", (_req, res) => {
769
+ ensureWikiStructure();
770
+ const projects = Object.entries(loadRegistry())
771
+ .sort(([left], [right]) => left.localeCompare(right))
772
+ .map(([slug, cwd]) => {
773
+ const summary = loadProjectRuleSummary(slug);
774
+ return {
775
+ slug,
776
+ cwd,
777
+ hardRuleCount: summary.hardRuleCount,
778
+ softRuleCount: summary.softRuleCount,
779
+ };
780
+ });
781
+ res.json(projects);
782
+ });
783
+ app.get("/api/projects/:slug", (req, res) => {
784
+ ensureWikiStructure();
785
+ const slugParam = req.params.slug;
786
+ const slug = Array.isArray(slugParam) ? (slugParam[0] ?? "") : (slugParam ?? "");
787
+ const cwd = loadRegistry()[slug];
788
+ if (!cwd) {
789
+ res.status(404).json({ error: "Project not found" });
790
+ return;
791
+ }
792
+ res.json(createProjectDetailPayload(slug, cwd));
793
+ });
794
+ app.post("/api/projects", async (req, res) => {
795
+ ensureWikiStructure();
796
+ const { slug, cwd } = parseRequest(projectCreateSchema, req.body ?? {});
797
+ const rulesPath = getProjectRulesPath(slug);
798
+ await withWikiWrite(() => {
799
+ const registry = loadRegistry();
800
+ if (registry[slug]) {
801
+ throw new BadRequestError(`Project '${slug}' already exists`);
802
+ }
803
+ if (pageExists(rulesPath)) {
804
+ throw new BadRequestError(`Project rules page '${rulesPath}' already exists`);
805
+ }
806
+ saveRegistry({
807
+ ...registry,
808
+ [slug]: cwd,
809
+ });
810
+ writePage(rulesPath, renderInitialProjectRulesPage(slug));
811
+ });
812
+ res.status(201).json(createProjectDetailPayload(slug, cwd));
813
+ });
814
+ app.delete("/api/projects/:slug", async (req, res) => {
815
+ ensureWikiStructure();
816
+ const slugParam = req.params.slug;
817
+ const slug = Array.isArray(slugParam) ? (slugParam[0] ?? "") : (slugParam ?? "");
818
+ const registry = loadRegistry();
819
+ if (!registry[slug]) {
820
+ throw new NotFoundError("Project not found");
821
+ }
822
+ const rulesPath = getProjectRulesPath(slug);
823
+ await withWikiWrite(() => {
824
+ const nextRegistry = { ...loadRegistry() };
825
+ delete nextRegistry[slug];
826
+ saveRegistry(nextRegistry);
827
+ deletePage(rulesPath);
828
+ });
829
+ res.json({ ok: true, slug });
830
+ });
831
+ app.put("/api/projects/:slug/rules/hard", async (req, res) => {
832
+ ensureWikiStructure();
833
+ const slugParam = req.params.slug;
834
+ const slug = Array.isArray(slugParam) ? (slugParam[0] ?? "") : (slugParam ?? "");
835
+ const cwd = loadRegistry()[slug];
836
+ if (!cwd) {
837
+ throw new NotFoundError("Project not found");
838
+ }
839
+ if (!pageExists(getProjectRulesPath(slug))) {
840
+ throw new NotFoundError("Project rules not found");
841
+ }
842
+ const { hardRules } = parseRequest(projectHardRulesSchema, req.body ?? {});
843
+ await withWikiWrite(() => {
844
+ saveProjectRulesHardFields(slug, hardRules);
845
+ });
846
+ res.json(createProjectDetailPayload(slug, cwd));
847
+ });
848
+ app.put("/api/projects/:slug/rules/soft", async (req, res) => {
849
+ ensureWikiStructure();
850
+ const slugParam = req.params.slug;
851
+ const slug = Array.isArray(slugParam) ? (slugParam[0] ?? "") : (slugParam ?? "");
852
+ const cwd = loadRegistry()[slug];
853
+ if (!cwd) {
854
+ throw new NotFoundError("Project not found");
855
+ }
856
+ if (!pageExists(getProjectRulesPath(slug))) {
857
+ throw new NotFoundError("Project rules not found");
858
+ }
859
+ const { softRules } = parseRequest(projectSoftRulesSchema, req.body ?? {});
860
+ await withWikiWrite(() => {
861
+ saveProjectRulesSoftRules(slug, softRules);
862
+ });
863
+ res.json(createProjectDetailPayload(slug, cwd));
864
+ });
714
865
  // ---------------------------------------------------------------------------
715
866
  // Wiki: list, read, write, delete
716
867
  // ---------------------------------------------------------------------------
@@ -761,12 +912,12 @@ app.get("/api/wiki/page", async (req, res) => {
761
912
  const content = await readWikiPage(path, { authorizationHeader });
762
913
  if (content === undefined) {
763
914
  if (path === "pages/index.md") {
764
- res.json({ path, content: getEmptyWikiWelcomeContent() });
915
+ res.json(createWikiPagePayload(path, getEmptyWikiWelcomeContent()));
765
916
  return;
766
917
  }
767
918
  throw new NotFoundError("Page not found");
768
919
  }
769
- res.json({ path, content });
920
+ res.json(createWikiPagePayload(path, content));
770
921
  });
771
922
  app.put("/api/wiki/page", async (req, res) => {
772
923
  const path = assertValidPagePath(readPathParam(req));