chapterhouse 0.3.19 → 0.3.21

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.
@@ -0,0 +1,73 @@
1
+ const WARNING_CAPABLE_FIELDS = [
2
+ "auto_pr",
3
+ "require_worktree",
4
+ "pr_draft_default",
5
+ "default_branch",
6
+ "require_clean_worktree",
7
+ ];
8
+ const PR_CREATION_RE = /\b(?:open|create|raise|submit|start)\s+(?:a\s+|the\s+)?(?:(?:non[-\s]?draft|draft|ready[-\s]+for[-\s]+review)\s+)?(?:pull request|pr)\b/i;
9
+ const PR_NEGATION_RE = /\b(?:do not|don't|dont|not|avoid)\s+(?:open|create|raise|submit|start)\s+(?:a\s+|the\s+)?(?:pull request|pr)\b/i;
10
+ const NON_DRAFT_PR_RE = /\b(?:non[-\s]?draft|ready[-\s]+for[-\s]+review)\s+(?:pull request|pr)\b|\b(?:pull request|pr)\b[^.\n]{0,40}\b(?:ready[-\s]+for[-\s]+review|non[-\s]?draft)\b/i;
11
+ const MAIN_CHECKOUT_RE = /\b(?:directly\s+in|from|using|on)\s+the\s+main\s+checkout\b|\bwithout\s+(?:a\s+)?worktree\b|\bskip\s+the\s+worktree\b/i;
12
+ const DIRTY_WORKTREE_RE = /\bdirty\s+(?:worktree|tree)\b|\buncommitted changes\b|\bunclean\s+(?:worktree|tree)\b/i;
13
+ const RISKY_OPERATION_RE = /\b(?:release|ship|publish|deploy|tag|version|bump version|cut a release)\b/i;
14
+ const BRANCH_TARGET_RE = /\b(?:pull request|pr)\b[^.\n]{0,60}\b(?:against|into|onto|target(?:ing)?|base(?:d)? on)\s+([a-z0-9._/-]+)\b/gi;
15
+ const BRANCH_SOURCE_RE = /\b(?:branch|rebase|merge)\b[^.\n]{0,60}\b(?:from|against|into|onto)\s+([a-z0-9._/-]+)\b/gi;
16
+ export function detectProjectRuleWarnings(taskText, hardRules) {
17
+ const warnings = [];
18
+ const normalizedTask = normalize(taskText);
19
+ for (const field of WARNING_CAPABLE_FIELDS) {
20
+ if (!isRuleViolated(field, normalizedTask, hardRules)) {
21
+ continue;
22
+ }
23
+ warnings.push(formatProjectRuleWarning(field, hardRules[field]));
24
+ }
25
+ return warnings;
26
+ }
27
+ function isRuleViolated(field, normalizedTask, hardRules) {
28
+ switch (field) {
29
+ case "auto_pr":
30
+ return hardRules.auto_pr === false
31
+ && PR_CREATION_RE.test(normalizedTask)
32
+ && !PR_NEGATION_RE.test(normalizedTask);
33
+ case "require_worktree":
34
+ return hardRules.require_worktree === true && MAIN_CHECKOUT_RE.test(normalizedTask);
35
+ case "pr_draft_default":
36
+ return hardRules.pr_draft_default === true && NON_DRAFT_PR_RE.test(normalizedTask);
37
+ case "default_branch":
38
+ return hasNonDefaultBranchTarget(normalizedTask, hardRules.default_branch);
39
+ case "require_clean_worktree":
40
+ return hardRules.require_clean_worktree === true
41
+ && DIRTY_WORKTREE_RE.test(normalizedTask)
42
+ && RISKY_OPERATION_RE.test(normalizedTask);
43
+ default:
44
+ return false;
45
+ }
46
+ }
47
+ function hasNonDefaultBranchTarget(taskText, defaultBranch) {
48
+ const normalizedDefault = defaultBranch.trim().toLowerCase();
49
+ if (!normalizedDefault) {
50
+ return false;
51
+ }
52
+ return extractExplicitBranches(taskText).some((branch) => branch !== normalizedDefault);
53
+ }
54
+ function extractExplicitBranches(taskText) {
55
+ const branches = new Set();
56
+ for (const regex of [BRANCH_TARGET_RE, BRANCH_SOURCE_RE]) {
57
+ regex.lastIndex = 0;
58
+ for (const match of taskText.matchAll(regex)) {
59
+ const branch = match[1]?.trim().toLowerCase();
60
+ if (branch) {
61
+ branches.add(branch);
62
+ }
63
+ }
64
+ }
65
+ return [...branches];
66
+ }
67
+ function formatProjectRuleWarning(field, value) {
68
+ return `⚠️ Project rule warning: this task may violate \`${field}: ${String(value)}\` — proceeding anyway.`;
69
+ }
70
+ function normalize(taskText) {
71
+ return taskText.toLowerCase().replace(/\s+/g, " ").trim();
72
+ }
73
+ //# sourceMappingURL=project-rule-warnings.js.map
@@ -0,0 +1,46 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ function createHardRules(overrides = {}) {
4
+ return {
5
+ auto_pr: false,
6
+ require_worktree: true,
7
+ pr_draft_default: true,
8
+ default_branch: "main",
9
+ commit_co_author: "Copilot <223556219+Copilot@users.noreply.github.com>",
10
+ test_command: "npm test",
11
+ build_command: "npm run build",
12
+ lint_command: "npm run lint:md",
13
+ require_clean_worktree: true,
14
+ ...overrides,
15
+ };
16
+ }
17
+ async function loadModule() {
18
+ const nonce = `${Date.now()}-${Math.random()}`;
19
+ return import(new URL(`./project-rule-warnings.js?case=${nonce}`, import.meta.url).href);
20
+ }
21
+ test("detectProjectRuleWarnings returns ordered warnings for explicit hard-rule conflicts", async () => {
22
+ const module = await loadModule();
23
+ assert.deepEqual(module.detectProjectRuleWarnings("Start the refactor directly in the main checkout, open a non-draft PR against develop, and release from the dirty worktree.", createHardRules()), [
24
+ "⚠️ Project rule warning: this task may violate `auto_pr: false` — proceeding anyway.",
25
+ "⚠️ Project rule warning: this task may violate `require_worktree: true` — proceeding anyway.",
26
+ "⚠️ Project rule warning: this task may violate `pr_draft_default: true` — proceeding anyway.",
27
+ "⚠️ Project rule warning: this task may violate `default_branch: main` — proceeding anyway.",
28
+ "⚠️ Project rule warning: this task may violate `require_clean_worktree: true` — proceeding anyway.",
29
+ ]);
30
+ });
31
+ test("detectProjectRuleWarnings warns for an explicit non-default branch target only", async () => {
32
+ const module = await loadModule();
33
+ assert.deepEqual(module.detectProjectRuleWarnings("Create the release branch from develop and target the PR at develop.", createHardRules({
34
+ auto_pr: true,
35
+ pr_draft_default: false,
36
+ require_worktree: false,
37
+ require_clean_worktree: false,
38
+ })), [
39
+ "⚠️ Project rule warning: this task may violate `default_branch: main` — proceeding anyway.",
40
+ ]);
41
+ });
42
+ test("detectProjectRuleWarnings stays quiet for compliant or ambiguous wording", async () => {
43
+ const module = await loadModule();
44
+ assert.deepEqual(module.detectProjectRuleWarnings("Do not open a PR yet. If you change code, use a fresh worktree on main and confirm the tree is clean before any release step.", createHardRules()), []);
45
+ });
46
+ //# sourceMappingURL=project-rule-warnings.test.js.map
@@ -0,0 +1,71 @@
1
+ import { loadRegistry } from "../wiki/project-registry.js";
2
+ import { loadProjectRules, } from "../wiki/project-rules.js";
3
+ import { resolveProject } from "./project-resolution.js";
4
+ const PROJECT_RULE_HARD_FIELD_ORDER = [
5
+ "auto_pr",
6
+ "require_worktree",
7
+ "pr_draft_default",
8
+ "default_branch",
9
+ "commit_co_author",
10
+ "test_command",
11
+ "build_command",
12
+ "lint_command",
13
+ "require_clean_worktree",
14
+ ];
15
+ export class ActiveProjectRulesLoadError extends Error {
16
+ slug;
17
+ constructor(slug, cause) {
18
+ const message = cause instanceof Error ? cause.message : String(cause);
19
+ super(message);
20
+ this.slug = slug;
21
+ this.name = "ActiveProjectRulesLoadError";
22
+ }
23
+ }
24
+ export function resolveActiveProjectRules(prompt, projectPath, options) {
25
+ const registry = (options?.loadRegistry ?? loadRegistry)();
26
+ const project = resolveProject(prompt, { projectPath }, registry);
27
+ if (!project) {
28
+ return null;
29
+ }
30
+ let rules;
31
+ try {
32
+ rules = (options?.loadProjectRules ?? loadProjectRules)(project.slug);
33
+ }
34
+ catch (error) {
35
+ throw new ActiveProjectRulesLoadError(project.slug, error);
36
+ }
37
+ if (!rules.found) {
38
+ return null;
39
+ }
40
+ return {
41
+ project,
42
+ rules,
43
+ };
44
+ }
45
+ export function renderActiveProjectRulesBlock(slug, registryPath, rulesPath, hardRules, softRules) {
46
+ return renderProjectRulesBlock("## Active Project Rules", "These rules are advisory. If a planned action may violate a hard rule, emit a visible warning and proceed unless the user says otherwise.", slug, registryPath, rulesPath, hardRules, softRules);
47
+ }
48
+ export function renderDelegatedProjectRulesPreamble(slug, registryPath, rulesPath, hardRules, softRules) {
49
+ return renderProjectRulesBlock("Project rules for this task:", "These rules are advisory. If this task may violate a hard rule, include a visible warning in your preamble and continue.", slug, registryPath, rulesPath, hardRules, softRules);
50
+ }
51
+ function renderProjectRulesBlock(heading, advisoryLine, slug, registryPath, rulesPath, hardRules, softRules) {
52
+ const lines = [
53
+ heading,
54
+ "",
55
+ `Project: ${slug}`,
56
+ `Registry path: ${registryPath}`,
57
+ `Rules page: ${rulesPath}`,
58
+ "",
59
+ "Hard rules:",
60
+ ...PROJECT_RULE_HARD_FIELD_ORDER.map((field) => `- ${field}: ${String(hardRules[field])}`),
61
+ "",
62
+ "Soft rules:",
63
+ ];
64
+ const normalizedSoftRules = softRules.replace(/\r\n/g, "\n").replace(/\n+$/, "");
65
+ if (normalizedSoftRules) {
66
+ lines.push(...normalizedSoftRules.split("\n"));
67
+ }
68
+ lines.push("", advisoryLine);
69
+ return lines.join("\n");
70
+ }
71
+ //# sourceMappingURL=project-rules-injection.js.map
@@ -0,0 +1,84 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ function createLoadedProjectRules() {
4
+ return {
5
+ found: true,
6
+ path: "pages/projects/chapterhouse/rules.md",
7
+ hard: {
8
+ auto_pr: false,
9
+ require_worktree: true,
10
+ pr_draft_default: true,
11
+ default_branch: "main",
12
+ commit_co_author: "Copilot <223556219+Copilot@users.noreply.github.com>",
13
+ test_command: "npm test",
14
+ build_command: "npm run build",
15
+ lint_command: "npm run lint:md",
16
+ require_clean_worktree: true,
17
+ },
18
+ soft: "- Keep tests green.\n- Prefer explicit names.\n",
19
+ warnings: [],
20
+ metadata: {},
21
+ };
22
+ }
23
+ async function loadModule() {
24
+ const nonce = `${Date.now()}-${Math.random()}`;
25
+ return import(new URL(`./project-rules-injection.js?case=${nonce}`, import.meta.url).href);
26
+ }
27
+ test("resolveActiveProjectRules prefers an explicit @project mention", async () => {
28
+ const module = await loadModule();
29
+ const activeRules = module.resolveActiveProjectRules("@project:chapterhouse inspect the worker task prompt", "/work/docs-site", {
30
+ loadRegistry: () => ({
31
+ chapterhouse: "/work/chapterhouse",
32
+ "docs-site": "/work/docs-site",
33
+ }),
34
+ loadProjectRules: () => createLoadedProjectRules(),
35
+ });
36
+ assert.deepEqual(activeRules, {
37
+ project: {
38
+ slug: "chapterhouse",
39
+ path: "/work/chapterhouse",
40
+ source: "mention",
41
+ },
42
+ rules: createLoadedProjectRules(),
43
+ });
44
+ });
45
+ test("resolveActiveProjectRules falls back to the current project path", async () => {
46
+ const module = await loadModule();
47
+ const activeRules = module.resolveActiveProjectRules("inspect the worker task prompt", "/work/chapterhouse/src/copilot", {
48
+ loadRegistry: () => ({
49
+ chapterhouse: "/work/chapterhouse",
50
+ }),
51
+ loadProjectRules: () => createLoadedProjectRules(),
52
+ });
53
+ assert.deepEqual(activeRules, {
54
+ project: {
55
+ slug: "chapterhouse",
56
+ path: "/work/chapterhouse",
57
+ source: "selectedProjectPath",
58
+ },
59
+ rules: createLoadedProjectRules(),
60
+ });
61
+ });
62
+ test("renderDelegatedProjectRulesPreamble matches the PRD wording and field order", async () => {
63
+ const module = await loadModule();
64
+ const rules = createLoadedProjectRules();
65
+ assert.equal(module.renderDelegatedProjectRulesPreamble("chapterhouse", "/home/bjk/projects/chapterhouse", rules.path, rules.hard, rules.soft), "Project rules for this task:\n\n"
66
+ + "Project: chapterhouse\n"
67
+ + "Registry path: /home/bjk/projects/chapterhouse\n"
68
+ + "Rules page: pages/projects/chapterhouse/rules.md\n\n"
69
+ + "Hard rules:\n"
70
+ + "- auto_pr: false\n"
71
+ + "- require_worktree: true\n"
72
+ + "- pr_draft_default: true\n"
73
+ + "- default_branch: main\n"
74
+ + "- commit_co_author: Copilot <223556219+Copilot@users.noreply.github.com>\n"
75
+ + "- test_command: npm test\n"
76
+ + "- build_command: npm run build\n"
77
+ + "- lint_command: npm run lint:md\n"
78
+ + "- require_clean_worktree: true\n\n"
79
+ + "Soft rules:\n"
80
+ + "- Keep tests green.\n"
81
+ + "- Prefer explicit names.\n\n"
82
+ + "These rules are advisory. If this task may violate a hard rule, include a visible warning in your preamble and continue.");
83
+ });
84
+ //# sourceMappingURL=project-rules-injection.test.js.map
@@ -0,0 +1,214 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import test from "node:test";
6
+ function createActiveProjectRules() {
7
+ return {
8
+ project: {
9
+ slug: "chapterhouse",
10
+ path: "/home/bjk/projects/chapterhouse",
11
+ source: "mention",
12
+ },
13
+ rules: {
14
+ found: true,
15
+ path: "pages/projects/chapterhouse/rules.md",
16
+ hard: {
17
+ auto_pr: false,
18
+ require_worktree: true,
19
+ pr_draft_default: true,
20
+ default_branch: "main",
21
+ commit_co_author: "Copilot <223556219+Copilot@users.noreply.github.com>",
22
+ test_command: "npm test",
23
+ build_command: "npm run build",
24
+ lint_command: "npm run lint:md",
25
+ require_clean_worktree: true,
26
+ },
27
+ soft: "- Keep tests green.\n- Prefer explicit names.\n",
28
+ warnings: [],
29
+ metadata: {},
30
+ },
31
+ };
32
+ }
33
+ function expectedWarningLines() {
34
+ return [
35
+ "⚠️ Project rule warning: this task may violate `auto_pr: false` — proceeding anyway.",
36
+ "⚠️ Project rule warning: this task may violate `require_worktree: true` — proceeding anyway.",
37
+ "⚠️ Project rule warning: this task may violate `pr_draft_default: true` — proceeding anyway.",
38
+ "⚠️ Project rule warning: this task may violate `default_branch: main` — proceeding anyway.",
39
+ "⚠️ Project rule warning: this task may violate `require_clean_worktree: true` — proceeding anyway.",
40
+ ];
41
+ }
42
+ function expectedDelegatedPrompt(task, warningLines = []) {
43
+ const warningBlock = warningLines.length > 0 ? `${warningLines.join("\n")}\n\n` : "";
44
+ return warningBlock
45
+ + "Project rules for this task:\n\n"
46
+ + "Project: chapterhouse\n"
47
+ + "Registry path: /home/bjk/projects/chapterhouse\n"
48
+ + "Rules page: pages/projects/chapterhouse/rules.md\n\n"
49
+ + "Hard rules:\n"
50
+ + "- auto_pr: false\n"
51
+ + "- require_worktree: true\n"
52
+ + "- pr_draft_default: true\n"
53
+ + "- default_branch: main\n"
54
+ + "- commit_co_author: Copilot <223556219+Copilot@users.noreply.github.com>\n"
55
+ + "- test_command: npm test\n"
56
+ + "- build_command: npm run build\n"
57
+ + "- lint_command: npm run lint:md\n"
58
+ + "- require_clean_worktree: true\n\n"
59
+ + "Soft rules:\n"
60
+ + "- Keep tests green.\n"
61
+ + "- Prefer explicit names.\n\n"
62
+ + "These rules are advisory. If this task may violate a hard rule, include a visible warning in your preamble and continue.\n\n"
63
+ + task;
64
+ }
65
+ async function loadToolsModule(t, options) {
66
+ const sentPrompts = [];
67
+ const taskId = options?.taskId ?? `delegated-task-${Date.now()}-${Math.random()}`;
68
+ const fakeSession = {
69
+ on: () => () => { },
70
+ async sendAndWait(request) {
71
+ sentPrompts.push(request.prompt);
72
+ return { data: { content: `handled: ${request.prompt}` } };
73
+ },
74
+ async destroy() { },
75
+ };
76
+ t.mock.module("./orchestrator.js", {
77
+ namedExports: {
78
+ getCurrentSourceChannel: () => options?.sourceChannel ?? "web",
79
+ getCurrentActivityCallback: () => undefined,
80
+ getCurrentAuthenticatedUser: () => undefined,
81
+ getLastAuthenticatedUser: () => undefined,
82
+ getCurrentAuthorizationHeader: () => undefined,
83
+ getCurrentSessionKey: () => "session-test",
84
+ getCurrentActiveProjectRules: () => options?.activeProjectRules ?? null,
85
+ switchSessionModel: async () => { },
86
+ },
87
+ });
88
+ t.mock.module("./agents.js", {
89
+ namedExports: {
90
+ getAgentRegistry: () => [{ slug: "coder", name: "Coder", model: "claude-sonnet-4.6" }],
91
+ getAgent: (name) => name === "coder"
92
+ ? { slug: "coder", name: "Coder", model: "claude-sonnet-4.6" }
93
+ : undefined,
94
+ createEphemeralAgentSession: async () => fakeSession,
95
+ getAgentSessionStatus: () => ({ tasks: [] }),
96
+ getActiveTasks: () => [],
97
+ getTask: () => undefined,
98
+ registerTask: () => ({
99
+ taskId,
100
+ agentSlug: "coder",
101
+ description: "Show dispatched prompt",
102
+ status: "running",
103
+ startedAt: Date.now(),
104
+ originChannel: options?.sourceChannel ?? "web",
105
+ }),
106
+ completeTask: () => { },
107
+ failTask: () => { },
108
+ createAgentFile: () => "",
109
+ removeAgentFile: () => { },
110
+ loadAgents: () => [],
111
+ },
112
+ });
113
+ const module = await import(new URL(`./tools.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
114
+ return { module, sentPrompts, taskId };
115
+ }
116
+ test.beforeEach(() => {
117
+ process.env.CHAPTERHOUSE_HOME = mkdtempSync(join(tmpdir(), "chapterhouse-tools-agent-"));
118
+ });
119
+ test.afterEach(async () => {
120
+ const home = process.env.CHAPTERHOUSE_HOME;
121
+ if (home) {
122
+ const dbModule = await import("../store/db.js");
123
+ dbModule.closeDb();
124
+ rmSync(home, { recursive: true, force: true });
125
+ }
126
+ });
127
+ test("delegate_to_agent persists the full dispatched task prompt", async (t) => {
128
+ const promptText = [
129
+ "Inspect the /workers route detail pane.",
130
+ "Return the exact prompt sent to the worker.",
131
+ ].join("\n");
132
+ const { module, taskId } = await loadToolsModule(t, {
133
+ sourceChannel: "chat",
134
+ taskId: "delegated-task-001",
135
+ });
136
+ const tools = module.createTools({
137
+ client: { async listModels() { return []; } },
138
+ onAgentTaskComplete: () => { },
139
+ });
140
+ const tool = tools.find((entry) => entry.name === "delegate_to_agent");
141
+ assert.ok(tool, "delegate_to_agent tool should be registered");
142
+ await tool.handler({
143
+ agent_name: "coder",
144
+ summary: "Show dispatched prompt",
145
+ task: promptText,
146
+ }, {});
147
+ const dbModule = await import("../store/db.js");
148
+ const db = dbModule.getDb();
149
+ const row = db.prepare(`SELECT description, prompt, session_key, source FROM agent_tasks WHERE task_id = ?`).get(taskId);
150
+ assert.deepEqual(row, {
151
+ description: "Show dispatched prompt",
152
+ prompt: promptText,
153
+ session_key: "session-test",
154
+ source: "adhoc",
155
+ });
156
+ });
157
+ test("delegate_to_agent prepends active project rules to the dispatched task prompt", async (t) => {
158
+ const { module, sentPrompts } = await loadToolsModule(t, {
159
+ activeProjectRules: createActiveProjectRules(),
160
+ });
161
+ const tools = module.createTools({
162
+ client: { async listModels() { return []; } },
163
+ onAgentTaskComplete: () => { },
164
+ });
165
+ const tool = tools.find((entry) => entry.name === "delegate_to_agent");
166
+ assert.ok(tool, "delegate_to_agent tool should be registered");
167
+ const task = "Inspect the worker task prompt.\nDo not re-resolve @project:docs-site from this text.";
168
+ await tool.handler({
169
+ agent_name: "coder",
170
+ summary: "Show dispatched prompt",
171
+ task,
172
+ }, {});
173
+ await new Promise((resolve) => setTimeout(resolve, 0));
174
+ assert.deepEqual(sentPrompts, [expectedDelegatedPrompt(task)]);
175
+ });
176
+ test("delegate_to_agent prepends project rule warnings above the dispatched task prompt", async (t) => {
177
+ const { module, sentPrompts } = await loadToolsModule(t, {
178
+ activeProjectRules: createActiveProjectRules(),
179
+ });
180
+ const tools = module.createTools({
181
+ client: { async listModels() { return []; } },
182
+ onAgentTaskComplete: () => { },
183
+ });
184
+ const tool = tools.find((entry) => entry.name === "delegate_to_agent");
185
+ assert.ok(tool, "delegate_to_agent tool should be registered");
186
+ const task = "Start the refactor directly in the main checkout, open a non-draft PR against develop, and release from the dirty worktree.";
187
+ await tool.handler({
188
+ agent_name: "coder",
189
+ summary: "Show warning preamble",
190
+ task,
191
+ }, {});
192
+ await new Promise((resolve) => setTimeout(resolve, 0));
193
+ assert.deepEqual(sentPrompts, [expectedDelegatedPrompt(task, expectedWarningLines())]);
194
+ });
195
+ test("delegate_to_agent leaves the prompt unchanged when no active project is resolved", async (t) => {
196
+ const { module, sentPrompts } = await loadToolsModule(t, {
197
+ activeProjectRules: null,
198
+ });
199
+ const tools = module.createTools({
200
+ client: { async listModels() { return []; } },
201
+ onAgentTaskComplete: () => { },
202
+ });
203
+ const tool = tools.find((entry) => entry.name === "delegate_to_agent");
204
+ assert.ok(tool, "delegate_to_agent tool should be registered");
205
+ const task = "Inspect the worker task prompt without project context.";
206
+ await tool.handler({
207
+ agent_name: "coder",
208
+ summary: "Show dispatched prompt",
209
+ task,
210
+ }, {});
211
+ await new Promise((resolve) => setTimeout(resolve, 0));
212
+ assert.deepEqual(sentPrompts, [task]);
213
+ });
214
+ //# sourceMappingURL=tools.agent.test.js.map
@@ -6,7 +6,7 @@ import { join } from "path";
6
6
  import { homedir } from "os";
7
7
  import { listSkills, createSkill, removeSkill } from "./skills.js";
8
8
  import { config, persistModel } from "../config.js";
9
- import { getCurrentSourceChannel, getCurrentActivityCallback, getCurrentAuthenticatedUser, getLastAuthenticatedUser, getCurrentAuthorizationHeader, getCurrentSessionKey, switchSessionModel, } from "./orchestrator.js";
9
+ import { getCurrentSourceChannel, getCurrentActivityCallback, getCurrentActiveProjectRules, getCurrentAuthenticatedUser, getLastAuthenticatedUser, getCurrentAuthorizationHeader, getCurrentSessionKey, switchSessionModel, } from "./orchestrator.js";
10
10
  import { getRouterConfig, updateRouterConfig } from "./router.js";
11
11
  import { ensureWikiStructure, readPage, writePage, deletePage, writeRawSource, assertPagePath } from "../wiki/fs.js";
12
12
  import { searchIndex, addToIndex, removeFromIndex, buildIndexEntryForPage } from "../wiki/index-manager.js";
@@ -20,6 +20,8 @@ import { getCategoryDir, topicPagePath, slugify, entityCategories, FLAT_CATEGORI
20
20
  import { withWikiWrite } from "../wiki/lock.js";
21
21
  import { readWikiPage, teamWikiSync } from "../wiki/team-sync.js";
22
22
  import { getAgentRegistry, getAgent, createEphemeralAgentSession, getAgentSessionStatus, getTask, registerTask, completeTask, failTask, createAgentFile, removeAgentFile, loadAgents, } from "./agents.js";
23
+ import { detectProjectRuleWarnings } from "./project-rule-warnings.js";
24
+ import { renderDelegatedProjectRulesPreamble } from "./project-rules-injection.js";
23
25
  import { adoGetOkrs, adoOkrSummary, adoUpdateKr } from "../integrations/ado-skill.js";
24
26
  import { TeamsNotifier } from "../integrations/teams-notify.js";
25
27
  import { TeamPushClient } from "../integrations/team-push.js";
@@ -117,9 +119,18 @@ export function createTools(deps) {
117
119
  return `Failed to create session for @${delegatedSlug}: ${msg}`;
118
120
  }
119
121
  const task = registerTask(delegatedSlug, args.summary, getCurrentSourceChannel());
122
+ const activeProjectRules = getCurrentActiveProjectRules();
123
+ const warningLines = activeProjectRules
124
+ ? detectProjectRuleWarnings(args.task, activeProjectRules.rules.hard)
125
+ : [];
126
+ const warningBlock = warningLines.length > 0 ? `${warningLines.join("\n")}\n\n` : "";
127
+ const taskPrompt = activeProjectRules
128
+ ? `${warningBlock}${renderDelegatedProjectRulesPreamble(activeProjectRules.project.slug, activeProjectRules.project.path, activeProjectRules.rules.path, activeProjectRules.rules.hard, activeProjectRules.rules.soft)}\n\n${args.task}`
129
+ : args.task;
120
130
  // Persist task to DB
121
131
  const db = getDb();
122
- db.prepare(`INSERT INTO agent_tasks (task_id, agent_slug, description, status, origin_channel, session_key, source) VALUES (?, ?, ?, 'running', ?, ?, 'adhoc')`).run(task.taskId, delegatedSlug, args.summary, task.originChannel || null, getCurrentSessionKey());
132
+ db.prepare(`INSERT INTO agent_tasks (task_id, agent_slug, description, prompt, status, origin_channel, session_key, source)
133
+ VALUES (?, ?, ?, ?, 'running', ?, ?, 'adhoc')`).run(task.taskId, delegatedSlug, args.summary, args.task, task.originChannel || null, getCurrentSessionKey());
123
134
  // Capture the parent's activity callback so the child session can stream
124
135
  // its events back to the originating SSE connection. This survives past
125
136
  // the parent assistant turn — the child runs long after the parent's
@@ -167,7 +178,7 @@ export function createTools(deps) {
167
178
  // Non-blocking: dispatch and return immediately. Session is always destroyed after.
168
179
  (async () => {
169
180
  try {
170
- const result = await session.sendAndWait({ prompt: args.task }, timeoutMs);
181
+ const result = await session.sendAndWait({ prompt: taskPrompt }, timeoutMs);
171
182
  const output = result?.data?.content || "No response";
172
183
  completeTask(task.taskId, output);
173
184
  db.prepare(`UPDATE agent_tasks SET status = 'completed', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(output.slice(0, 10000), task.taskId);
package/dist/store/db.js CHANGED
@@ -37,6 +37,7 @@ export function getDb() {
37
37
  task_id TEXT PRIMARY KEY,
38
38
  agent_slug TEXT NOT NULL,
39
39
  description TEXT NOT NULL,
40
+ prompt TEXT,
40
41
  status TEXT NOT NULL DEFAULT 'running',
41
42
  result TEXT,
42
43
  origin_channel TEXT,
@@ -114,6 +115,9 @@ export function getDb() {
114
115
  if (!taskCols.some((c) => c.name === 'source')) {
115
116
  db.exec(`ALTER TABLE agent_tasks ADD COLUMN source TEXT NOT NULL DEFAULT 'adhoc'`);
116
117
  }
118
+ if (!taskCols.some((c) => c.name === "prompt")) {
119
+ db.exec(`ALTER TABLE agent_tasks ADD COLUMN prompt TEXT`);
120
+ }
117
121
  // agent_task_events: append-only per-task tool-call activity log for /workers streaming
118
122
  db.exec(`
119
123
  CREATE TABLE IF NOT EXISTS agent_task_events (
@@ -86,6 +86,36 @@ test("getDb migrates legacy conversation_log tables to allow system messages", a
86
86
  dbModule.closeDb();
87
87
  }
88
88
  });
89
+ test("getDb migrates legacy agent_tasks tables to add a nullable prompt column", async () => {
90
+ const seedDb = new Database(dbPath);
91
+ seedDb.exec(`
92
+ CREATE TABLE agent_tasks (
93
+ task_id TEXT PRIMARY KEY,
94
+ agent_slug TEXT NOT NULL,
95
+ description TEXT NOT NULL,
96
+ status TEXT NOT NULL DEFAULT 'running',
97
+ result TEXT,
98
+ origin_channel TEXT,
99
+ started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
100
+ completed_at DATETIME
101
+ )
102
+ `);
103
+ seedDb.close();
104
+ const dbModule = await loadDbModule();
105
+ try {
106
+ const db = dbModule.getDb();
107
+ const cols = db.prepare(`PRAGMA table_info(agent_tasks)`).all();
108
+ assert.ok(cols.some((col) => col.name === "prompt"), "agent_tasks.prompt column must be added by migration");
109
+ const longPrompt = "Investigate worker prompt capture.\n".repeat(30);
110
+ db.prepare(`INSERT INTO agent_tasks (task_id, agent_slug, description, prompt, status)
111
+ VALUES (?, ?, ?, ?, ?)`).run("task-prompt-001", "coder", "Investigate workers", longPrompt, "running");
112
+ const row = db.prepare(`SELECT prompt FROM agent_tasks WHERE task_id = ?`).get("task-prompt-001");
113
+ assert.equal(row.prompt, longPrompt, "prompt text should round-trip without truncation");
114
+ }
115
+ finally {
116
+ dbModule.closeDb();
117
+ }
118
+ });
89
119
  test("getDb prunes oversized conversation logs on startup and during inserts", async () => {
90
120
  const seedDb = new Database(dbPath);
91
121
  seedDb.exec(`
@@ -23,7 +23,7 @@ const KNOWN_WIKI_FRONTMATTER_FIELDS = new Set([
23
23
  "contradictions",
24
24
  "related",
25
25
  ]);
26
- const PROJECT_RULE_HARD_FIELDS = [
26
+ export const PROJECT_RULE_HARD_FIELDS = [
27
27
  "auto_pr",
28
28
  "require_worktree",
29
29
  "pr_draft_default",