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.
- package/dist/api/server.js +154 -3
- package/dist/api/server.test.js +461 -14
- package/dist/copilot/orchestrator.js +70 -3
- package/dist/copilot/orchestrator.test.js +337 -1
- package/dist/copilot/project-resolution.js +73 -0
- package/dist/copilot/project-resolution.test.js +124 -0
- package/dist/copilot/project-rule-warnings.js +73 -0
- package/dist/copilot/project-rule-warnings.test.js +46 -0
- package/dist/copilot/project-rules-injection.js +71 -0
- package/dist/copilot/project-rules-injection.test.js +84 -0
- package/dist/copilot/tools.agent.test.js +214 -0
- package/dist/copilot/tools.js +14 -3
- package/dist/store/db.js +4 -0
- package/dist/store/db.test.js +30 -0
- package/dist/wiki/frontmatter.js +1 -1
- package/dist/wiki/lint.js +37 -10
- package/dist/wiki/lint.test.js +72 -0
- package/dist/wiki/project-registry.js +160 -0
- package/dist/wiki/project-registry.test.js +72 -0
- package/dist/wiki/project-rules.js +155 -0
- package/dist/wiki/project-rules.test.js +217 -0
- package/package.json +1 -1
- package/web/dist/assets/{index-9We9vWBC.js → index-Ch4AYrQP.js} +72 -69
- package/web/dist/assets/index-Ch4AYrQP.js.map +1 -0
- package/web/dist/assets/index-D__tBB0X.css +10 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-9We9vWBC.js.map +0 -1
- package/web/dist/assets/index-DYx2idiH.css +0 -10
package/dist/api/server.js
CHANGED
|
@@ -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(
|
|
915
|
+
res.json(createWikiPagePayload(path, getEmptyWikiWelcomeContent()));
|
|
765
916
|
return;
|
|
766
917
|
}
|
|
767
918
|
throw new NotFoundError("Page not found");
|
|
768
919
|
}
|
|
769
|
-
res.json(
|
|
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));
|