clay-server 4.0.0-beta.15 → 4.0.0-beta.17

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.
Files changed (77) hide show
  1. package/lib/capsule-frame-server.js +26 -0
  2. package/lib/capsule-mate-turn.js +105 -0
  3. package/lib/capsule-pig-logic.js +33 -1
  4. package/lib/capsule-server-runtimes.js +0 -2
  5. package/lib/capsules/pig/display.js +8 -0
  6. package/lib/capsules/pig/manifest.json +1 -1
  7. package/lib/capsules/pig/ui.json +52 -14
  8. package/lib/daemon-projects.js +10 -5
  9. package/lib/daemon.js +69 -10
  10. package/lib/migrate-single-user.js +2 -0
  11. package/lib/project-capsule-turn.js +96 -0
  12. package/lib/project-log-context.js +171 -0
  13. package/lib/project-log-feedback-delivery.js +47 -5
  14. package/lib/project-logs-context-state.js +42 -0
  15. package/lib/project-logs-mcp-server.js +16 -10
  16. package/lib/project-logs-query.js +18 -0
  17. package/lib/project-logs-root.js +8 -0
  18. package/lib/project-logs-service.js +92 -15
  19. package/lib/project-logs-snapshot.js +3 -0
  20. package/lib/project-logs-store.js +29 -32
  21. package/lib/project-logs-versioning.js +6 -2
  22. package/lib/project-logs.js +56 -12
  23. package/lib/project-message-delivery.js +70 -0
  24. package/lib/project-session-pair.js +15 -5
  25. package/lib/project-user-message.js +11 -0
  26. package/lib/project-worker-proposal.js +8 -8
  27. package/lib/project.js +18 -1
  28. package/lib/public/app.js +11 -15
  29. package/lib/public/css/capsule-ui.css +21 -1
  30. package/lib/public/css/home-hub.css +4 -0
  31. package/lib/public/css/input.css +84 -3
  32. package/lib/public/css/project-logs.css +20 -2
  33. package/lib/public/css/session-actions.css +16 -101
  34. package/lib/public/css/sticky-notes.css +3 -7
  35. package/lib/public/index.html +31 -9
  36. package/lib/public/modules/app-connection.js +22 -3
  37. package/lib/public/modules/app-message-cards.js +2 -1
  38. package/lib/public/modules/app-messages.js +39 -3
  39. package/lib/public/modules/app-misc.js +2 -14
  40. package/lib/public/modules/app-panels.js +3 -2
  41. package/lib/public/modules/app-projects.js +6 -3
  42. package/lib/public/modules/app-rendering.js +1 -0
  43. package/lib/public/modules/capsule-preference.js +44 -0
  44. package/lib/public/modules/context-sources.js +0 -3
  45. package/lib/public/modules/home-chat-empty-state.js +39 -0
  46. package/lib/public/modules/home-chat-identity.js +9 -0
  47. package/lib/public/modules/home-mate-chat.js +15 -35
  48. package/lib/public/modules/home-tool-frame.js +17 -0
  49. package/lib/public/modules/home-tools.js +9 -0
  50. package/lib/public/modules/input.js +38 -7
  51. package/lib/public/modules/message-delivery.js +97 -0
  52. package/lib/public/modules/paste-modal.js +84 -0
  53. package/lib/public/modules/project-logs-render.js +48 -2
  54. package/lib/public/modules/project-logs.js +133 -11
  55. package/lib/public/modules/project-removal-target.js +26 -0
  56. package/lib/public/modules/session-actions.js +29 -105
  57. package/lib/public/modules/user-settings.js +4 -0
  58. package/lib/public/modules/worker-pane-lock.js +2 -0
  59. package/lib/sdk-bridge.js +14 -7
  60. package/lib/server-experimental-settings.js +49 -0
  61. package/lib/server-home-chat-events.js +28 -1
  62. package/lib/server-home-chat.js +1 -1
  63. package/lib/server-settings.js +5 -0
  64. package/lib/server-tools.js +30 -0
  65. package/lib/server.js +41 -2
  66. package/lib/session-driver-orchestration.js +31 -0
  67. package/lib/session-pair-prompts.js +24 -5
  68. package/lib/tools-registry.js +46 -1
  69. package/lib/users-experimental-preferences.js +30 -0
  70. package/lib/users.js +6 -0
  71. package/lib/ws-schema.js +8 -2
  72. package/lib/yoke/adapters/codex.js +24 -14
  73. package/lib/yoke/vendor-registry.js +1 -1
  74. package/package.json +2 -2
  75. package/lib/capsule-tictactoe-logic.js +0 -260
  76. package/lib/capsules/tictactoe/manifest.json +0 -9
  77. package/lib/capsules/tictactoe/ui.json +0 -203
@@ -0,0 +1,96 @@
1
+ // Delivers a Capsule turn prompt into this Mate project.
2
+ //
3
+ // A Capsule game against a Mate needs the Mate to actually take its turns:
4
+ // when Logic passes the turn to the mate seat, this module wakes the Mate
5
+ // with a prompt telling it to snapshot the game and act. One session hosts
6
+ // the whole game (found again by session.capsuleGame.toolId), so the user
7
+ // watches a single running conversation of the Mate playing rather than a
8
+ // new session per turn. The prompt is recorded as an internal user message,
9
+ // the same shape the debate engine uses, so the transcript shows the nudge
10
+ // without pretending the human typed it.
11
+ //
12
+ // This module only carries words to the Mate. Every actual game mutation the
13
+ // Mate then makes goes through the same clay_tool_act pipeline as always.
14
+
15
+ function attachCapsuleTurn(ctx) {
16
+ var sm = ctx.sm;
17
+
18
+ function ownsSession(session, principal) {
19
+ if (ctx.isMultiUser()) return !!principal.userId && session.ownerId === principal.userId;
20
+ return !session.ownerId;
21
+ }
22
+
23
+ function findGameSession(principal, toolId) {
24
+ var found = null;
25
+ sm.sessions.forEach(function (session) {
26
+ if (found || !session || session.hidden || session.destroying) return;
27
+ if (!session.capsuleGame || session.capsuleGame.toolId !== toolId) return;
28
+ if (!ownsSession(session, principal)) return;
29
+ found = session;
30
+ });
31
+ return found;
32
+ }
33
+
34
+ async function createGameSession(principal, options) {
35
+ var selection = await ctx.resolveModel(principal);
36
+ if (!selection || selection.status !== "ready" || !selection.vendor || !selection.model) {
37
+ throw new Error(selection && selection.error ? selection.error : "No configured model is available for the Mate.");
38
+ }
39
+ var create = typeof sm.createSessionRaw === "function" ? sm.createSessionRaw : sm.createSession;
40
+ var session = create.call(sm, {
41
+ ownerId: principal.userId || null,
42
+ vendor: selection.vendor,
43
+ model: selection.model,
44
+ });
45
+ session.title = (options.toolName || options.toolId) + " game";
46
+ session.capsuleGame = { toolId: options.toolId };
47
+ return session;
48
+ }
49
+
50
+ async function deliverCapsuleTurn(principal, options) {
51
+ if (!principal || !options || !options.toolId || !options.text) {
52
+ throw new Error("A bound owner, toolId, and turn text are required.");
53
+ }
54
+ var sdk = ctx.getSdk();
55
+ if (!sdk || typeof sdk.startQuery !== "function") throw new Error("Mate project runtime is unavailable.");
56
+ var session = findGameSession(principal, options.toolId);
57
+ var created = false;
58
+ if (!session) {
59
+ session = await createGameSession(principal, options);
60
+ created = true;
61
+ }
62
+ // Recorded as a dedicated event, not a user message: the transcript shows
63
+ // a short system note (see historyToHomeChat) instead of pretending the
64
+ // human typed the delivery prompt. The full prompt still reaches the
65
+ // model through the query below.
66
+ sm.sendAndRecord(session, {
67
+ type: "capsule_turn",
68
+ text: options.text,
69
+ toolId: options.toolId,
70
+ toolName: options.toolName || options.toolId,
71
+ kind: options.kind || "turn",
72
+ });
73
+ session.isProcessing = true;
74
+ session.lastActivity = Date.now();
75
+ sm.broadcastSessionList();
76
+ try {
77
+ var delivered = typeof sdk.pushMessage === "function" ? sdk.pushMessage(session, options.text) : false;
78
+ if (!delivered) await Promise.resolve(sdk.startQuery(session, options.text, undefined, ctx.getLinuxUserForSession(session)));
79
+ } catch (error) {
80
+ session.isProcessing = false;
81
+ sm.sendAndRecord(session, { type: "error", text: "Capsule turn could not start: " + (error && error.message ? error.message : String(error)) });
82
+ sm.broadcastSessionList();
83
+ throw error;
84
+ }
85
+ // The reference shape the home chat surface opens sessions by.
86
+ return {
87
+ session: session,
88
+ created: created,
89
+ reference: (typeof session.cliSessionId === "string" && session.cliSessionId) || "local:" + session.localId,
90
+ };
91
+ }
92
+
93
+ return { deliverCapsuleTurn: deliverCapsuleTurn };
94
+ }
95
+
96
+ module.exports = { attachCapsuleTurn: attachCapsuleTurn };
@@ -0,0 +1,171 @@
1
+ // Stable project knowledge and Git worktree change-set identities.
2
+ //
3
+ // Existing projects freeze their current path-derived scope so every opaque
4
+ // log ref remains valid. New projects receive a path-independent id. A linked
5
+ // worktree stores its generated change-set marker in Git's administrative
6
+ // directory, which survives daemon restarts and branch renames but disappears
7
+ // with the worktree itself; the durable log records retain the id afterward.
8
+
9
+ var crypto = require("crypto");
10
+ var fs = require("fs");
11
+ var path = require("path");
12
+ var execFileSync = require("child_process").execFileSync;
13
+ var logsRoot = require("./project-logs-root");
14
+ var utils = require("./utils");
15
+
16
+ var ID_PATTERN = /^[A-Za-z0-9_-]{1,120}$/;
17
+
18
+ function newId(prefix) {
19
+ return prefix + "_" + crypto.randomBytes(16).toString("base64url");
20
+ }
21
+
22
+ function validId(value, prefix) {
23
+ return typeof value === "string" && ID_PATTERN.test(value) && (!prefix || value.indexOf(prefix + "_") === 0);
24
+ }
25
+
26
+ function legacyKnowledgeId(cwd) {
27
+ return utils.encodeCwd(logsRoot.resolveProjectRoot(cwd));
28
+ }
29
+
30
+ function ensureProjectKnowledgeId(project) {
31
+ if (!project || typeof project !== "object") return false;
32
+ if (validId(project.projectKnowledgeId)) return false;
33
+ project.projectKnowledgeId = project.path ? legacyKnowledgeId(project.path) : newId("pk");
34
+ return true;
35
+ }
36
+
37
+ function initializeProjectKnowledge(config) {
38
+ var changed = false;
39
+ var projects = config && Array.isArray(config.projects) ? config.projects : [];
40
+ for (var i = 0; i < projects.length; i++) {
41
+ if (ensureProjectKnowledgeId(projects[i])) changed = true;
42
+ }
43
+ return changed;
44
+ }
45
+
46
+ function createProjectKnowledgeId() {
47
+ return newId("pk");
48
+ }
49
+
50
+ function git(cwd, args) {
51
+ return execFileSync("git", args, {
52
+ cwd: cwd,
53
+ encoding: "utf8",
54
+ timeout: 5000,
55
+ stdio: ["pipe", "pipe", "pipe"],
56
+ }).trim();
57
+ }
58
+
59
+ function commit(cwd, ref) {
60
+ try { return git(cwd, ["rev-parse", ref || "HEAD"]); }
61
+ catch (e) { return null; }
62
+ }
63
+
64
+ function markerPath(cwd) {
65
+ try {
66
+ var gitDir = git(cwd, ["rev-parse", "--git-dir"]);
67
+ return path.join(path.resolve(cwd, gitDir), "clay-change-set.json");
68
+ } catch (e) {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function readMarker(filePath, knowledgeId) {
74
+ if (!filePath) return null;
75
+ try {
76
+ var parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
77
+ if (!parsed || parsed.projectKnowledgeId !== knowledgeId || !validId(parsed.changeSetId, "cs")) return null;
78
+ return parsed;
79
+ } catch (e) {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ function deterministicFallback(knowledgeId, cwd) {
85
+ var digest = crypto.createHash("sha256").update(knowledgeId + "\u0000" + logsRoot.canonicalPath(cwd)).digest("base64url");
86
+ return "cs_" + digest.substring(0, 22);
87
+ }
88
+
89
+ function resolveWorktreeContext(cwd, knowledgeId, details) {
90
+ var input = details || {};
91
+ var filePath = markerPath(cwd);
92
+ var marker = readMarker(filePath, knowledgeId);
93
+ if (!marker) {
94
+ marker = {
95
+ projectKnowledgeId: knowledgeId,
96
+ changeSetId: newId("cs"),
97
+ baseCommit: input.baseCommit || null,
98
+ createdAt: Date.now(),
99
+ };
100
+ if (filePath) {
101
+ try {
102
+ fs.writeFileSync(filePath, JSON.stringify(marker, null, 2) + "\n", { flag: "wx", mode: 0o600 });
103
+ } catch (e) {
104
+ marker = readMarker(filePath, knowledgeId) || marker;
105
+ }
106
+ }
107
+ }
108
+ if (!filePath || !readMarker(filePath, knowledgeId)) {
109
+ marker.changeSetId = deterministicFallback(knowledgeId, cwd);
110
+ }
111
+ return {
112
+ kind: "worktree",
113
+ changeSetId: marker.changeSetId,
114
+ branch: input.branch || null,
115
+ baseCommit: marker.baseCommit || input.baseCommit || null,
116
+ headCommit: commit(cwd, "HEAD"),
117
+ status: "active",
118
+ };
119
+ }
120
+
121
+ function isMerged(parentCwd, worktreeCwd) {
122
+ var head = commit(worktreeCwd, "HEAD");
123
+ if (!head) return false;
124
+ try {
125
+ execFileSync("git", ["merge-base", "--is-ancestor", head, "HEAD"], {
126
+ cwd: parentCwd,
127
+ timeout: 5000,
128
+ stdio: ["pipe", "pipe", "pipe"],
129
+ });
130
+ return true;
131
+ } catch (e) {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ function cleanCommit(value) {
137
+ return typeof value === "string" && /^[0-9a-f]{7,64}$/i.test(value) ? value : null;
138
+ }
139
+
140
+ function cleanBranch(value) {
141
+ if (typeof value !== "string") return null;
142
+ var cleaned = value.trim().substring(0, 200);
143
+ return cleaned && cleaned.indexOf("\n") === -1 && cleaned.indexOf("\r") === -1 ? cleaned : null;
144
+ }
145
+
146
+ function normalizeRecordContext(value) {
147
+ if (!value || value.kind !== "worktree" || !validId(value.changeSetId, "cs")) {
148
+ return { kind: "project", changeSetId: null, branch: null, baseCommit: null, headCommit: null, status: "active" };
149
+ }
150
+ return {
151
+ kind: "worktree",
152
+ changeSetId: value.changeSetId,
153
+ branch: cleanBranch(value.branch),
154
+ baseCommit: cleanCommit(value.baseCommit),
155
+ headCommit: cleanCommit(value.headCommit),
156
+ status: value.status === "merged" || value.status === "archived" ? value.status : "active",
157
+ };
158
+ }
159
+
160
+ module.exports = {
161
+ ID_PATTERN: ID_PATTERN,
162
+ validId: validId,
163
+ legacyKnowledgeId: legacyKnowledgeId,
164
+ ensureProjectKnowledgeId: ensureProjectKnowledgeId,
165
+ initializeProjectKnowledge: initializeProjectKnowledge,
166
+ createProjectKnowledgeId: createProjectKnowledgeId,
167
+ resolveWorktreeContext: resolveWorktreeContext,
168
+ commit: commit,
169
+ isMerged: isMerged,
170
+ normalizeRecordContext: normalizeRecordContext,
171
+ };
@@ -1,6 +1,6 @@
1
- // Deliver new Project Log comments to the exact live session responsible for
2
- // the canonical entry. Comments remain durable in the ledger when that session
3
- // is gone; this module never guesses another session or creates one.
1
+ // Deliver new Project Log comments to the session responsible for the
2
+ // canonical entry. A busy Driver gets an isolated hidden reviewer so feedback
3
+ // never waits behind unrelated work in the visible conversation.
4
4
 
5
5
  function attachProjectLogFeedbackDelivery(ctx) {
6
6
  var sm = ctx.sm;
@@ -8,8 +8,7 @@ function attachProjectLogFeedbackDelivery(ctx) {
8
8
  var onProcessingChanged = ctx.onProcessingChanged || function () {};
9
9
  var getLinuxUserForSession = ctx.getLinuxUserForSession || function () { return null; };
10
10
 
11
- function findAuthorSession(entry) {
12
- var author = entry && entry.updatedBy;
11
+ function sessionForAuthor(author) {
13
12
  if (!author || author.type !== "session" || !author.sessionKey) return null;
14
13
  var match = null;
15
14
  sm.sessions.forEach(function (session) {
@@ -22,6 +21,48 @@ function attachProjectLogFeedbackDelivery(ctx) {
22
21
  return match;
23
22
  }
24
23
 
24
+ function findAuthorSession(entry) {
25
+ return sessionForAuthor(entry && entry.updatedBy) || sessionForAuthor(entry && entry.createdBy);
26
+ }
27
+
28
+ function startImmediateReviewer(source, entry, text, sdk) {
29
+ if (!sm.createSessionRaw || !sm.deleteSessionQuiet) return false;
30
+ var reviewer = sm.createSessionRaw({
31
+ ownerId: source.ownerId || null,
32
+ sessionVisibility: "private",
33
+ vendor: source.vendor || null,
34
+ model: source.model || null,
35
+ effort: source.effort || null,
36
+ permissionMode: source.permissionMode || null,
37
+ });
38
+ reviewer.hidden = true;
39
+ reviewer.title = "Project Log review";
40
+ reviewer.singleTurn = true;
41
+ reviewer.projectLogReview = { ref: entry.ref };
42
+ reviewer.isProcessing = true;
43
+ reviewer.sentToolResults = {};
44
+ reviewer.onQueryComplete = function () {
45
+ setImmediate(function () { sm.deleteSessionQuiet(reviewer.localId); });
46
+ };
47
+ sm.sendAndRecord(reviewer, {
48
+ type: "user_message",
49
+ text: text,
50
+ _internal: true,
51
+ projectLogFeedback: true,
52
+ projectLogRef: entry.ref,
53
+ });
54
+ Promise.resolve(sdk.startQuery(reviewer, text, null, getLinuxUserForSession(reviewer))).catch(function (error) {
55
+ reviewer.isProcessing = false;
56
+ sm.sendAndRecord(reviewer, {
57
+ type: "error",
58
+ text: "Could not review Project Log feedback: " + (error.message || String(error)),
59
+ _internal: true,
60
+ });
61
+ sm.deleteSessionQuiet(reviewer.localId);
62
+ });
63
+ return true;
64
+ }
65
+
25
66
  function feedbackPrompt(entry) {
26
67
  return "[Project Log feedback]\n" +
27
68
  "A user added a comment to " + entry.ref + ". Review the pending feedback now with list_log_feedback, " +
@@ -37,6 +78,7 @@ function attachProjectLogFeedbackDelivery(ctx) {
37
78
  var sdk = getSdk();
38
79
  if (!session || !sdk) return false;
39
80
  var text = feedbackPrompt(entry);
81
+ if (session.isProcessing && startImmediateReviewer(session, entry, text, sdk)) return true;
40
82
  sm.sendAndRecord(session, {
41
83
  type: "user_message",
42
84
  text: text,
@@ -0,0 +1,42 @@
1
+ // Append-only lifecycle projection for worktree-scoped Project Log entries.
2
+
3
+ var recordStore = require("./knowledge-record-store");
4
+ var logContext = require("./project-log-context");
5
+
6
+ function attachContextState(store, scopeId) {
7
+ function states() {
8
+ var out = {};
9
+ var records = store.all();
10
+ for (var i = 0; i < records.length; i++) {
11
+ var record = records[i];
12
+ if (record.op !== "context-state" || !record.changeSetId) continue;
13
+ if (record.status !== "archived" && record.status !== "merged" && record.status !== "active") continue;
14
+ out[record.changeSetId] = record.status;
15
+ }
16
+ return out;
17
+ }
18
+
19
+ function set(context, status) {
20
+ var normalized = logContext.normalizeRecordContext(context);
21
+ if (!normalized.changeSetId) return false;
22
+ if (status !== "active" && status !== "archived" && status !== "merged") {
23
+ throw new Error("Invalid Project Log change-set status.");
24
+ }
25
+ if (states()[normalized.changeSetId] === status) return false;
26
+ store.append({
27
+ id: recordStore.newRecordId(),
28
+ rootId: "context-" + normalized.changeSetId,
29
+ op: "context-state",
30
+ scope: scopeId,
31
+ changeSetId: normalized.changeSetId,
32
+ status: status,
33
+ context: normalized,
34
+ at: Date.now(),
35
+ });
36
+ return true;
37
+ }
38
+
39
+ return { states: states, set: set };
40
+ }
41
+
42
+ module.exports = { attachContextState: attachContextState };
@@ -12,14 +12,15 @@ var logsSchema = require("./project-logs-schema");
12
12
  var logsComments = require("./project-logs-comments");
13
13
 
14
14
  var LOGS_CONTRACT =
15
- "Project Logs are this project's durable record of decisions and work, written the way a good Git history reads. " +
15
+ "Project Logs are this project's durable work-continuity record, written so a newly created Driver can understand what the user asked for, what happened, and what remains without reading the previous chat. " +
16
16
  "You are the only author: connected people read the log and may add comments, but they cannot create or revise entries. That makes accuracy your responsibility. " +
17
- "Write an entry when the project gains something worth knowing weeks from now: a decision and the reasoning behind it, a security finding, an investigation's outcome, an incident and its resolution, an operational runbook, a durable reference, or real progress. " +
18
- "Every entry needs a concise meaningful title, a one or two sentence summary explaining what was decided or done and why, and a category. Set priority when an entry genuinely outranks routine work. " +
17
+ "Log every concrete user work instruction that changes, diagnoses, designs, or verifies project state, not only unusually important work. Create or identify one entry for the coherent task, then revise that same entry as work progresses instead of creating one entry per turn. " +
18
+ "The entry must preserve the user's requested outcome and material constraints, followed by what was changed, discovered, or decided, the affected area, verification, and the current result. If work is incomplete or blocked, state the remaining work and next action explicitly. For a small task, one completed entry is enough; for longer work, create it when the task starts and update it at meaningful milestones and completion. " +
19
+ "Every entry needs a concise meaningful title, a one or two sentence summary that combines the request with the current outcome, and a category. Set priority when an entry genuinely outranks routine work; routine work still belongs in the ledger at normal priority. " +
19
20
  "Categories are this project's own evolving vocabulary rather than a fixed list: list or search first, reuse an established category when one fits, and coin a new concise one only when the project needs a durable distinction it lacks. " +
20
21
  "Prefer updating an existing entry over creating a near-duplicate: when a decision supersedes an earlier one, revise that entry so its history shows the change. " +
21
- "Do not log conversation summaries, task narration, restatements of the request, completed-work announcements, speculation, or anything the repository and its history already record. " +
22
- "Every write is attributed and permanently revision-tracked, so keep entries short, concrete, and true.";
22
+ "Do not paste raw conversation transcripts, log command-by-command narration, trivial confirmations, or speculation. Repository history may show the code change but usually does not preserve the user's intent, constraints, verification, or unfinished state, so it is not a substitute for the work log. " +
23
+ "Every write is attributed and permanently revision-tracked, so keep entries concise, concrete, and true while retaining enough context for a clean Driver handoff.";
23
24
 
24
25
  // User learning moments are a durable project asset, so capturing them is a
25
26
  // default rather than an option. This category is about a change in the user's
@@ -75,8 +76,9 @@ var CATEGORY_DESCRIPTION = "Record category: a short lowercase hyphen-separated
75
76
  "Common starting points are " + logsSchema.SEED_CATEGORIES.join(", ") + ". A category is dry metadata, never a persona or an identifier. " +
76
77
  "Use `learning` only for a user learning moment described by the learning contract, never for knowledge or lessons acquired by the Driver.";
77
78
  var PRIORITY_DESCRIPTION = "How much this outranks routine work: " + logsSchema.PRIORITIES.join(", ") + ". Defaults to normal. Priority is independent of category, so an urgent decision is both.";
78
- var SUMMARY_DESCRIPTION = "One or two sentences explaining what was decided or done and why. This is what a reader sees in the ledger, so it must stand alone. For a learning entry, identify the concept the user engaged with plainly enough that the row itself teaches it.";
79
+ var SUMMARY_DESCRIPTION = "One or two sentences combining the user's requested outcome with the current result or status. This is what a new Driver sees in the ledger, so it must stand alone. For a learning entry, identify the concept the user engaged with plainly enough that the row itself teaches it.";
79
80
  var REF_DESCRIPTION = "Opaque log reference returned by list_logs, search_logs, or create_log.";
81
+ var CONTEXT_DESCRIPTION = "Optional logical scope: current includes project-wide entries plus this worktree's change set, project shows only project-wide entries, and all includes archived and merged worktree changes. Defaults to current in a worktree and project otherwise.";
80
82
 
81
83
  function textResult(value) {
82
84
  return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
@@ -113,6 +115,7 @@ function projectTools(bound) {
113
115
  kind: { type: "string", description: "Optional category filter, matched exactly against this project's vocabulary. The response lists the categories currently in use." },
114
116
  priority: { type: "string", enum: logsSchema.PRIORITIES, description: "Optional priority filter." },
115
117
  tag: { type: "string", description: "Optional single tag filter." },
118
+ contextMode: { type: "string", enum: ["current", "project", "all"], description: CONTEXT_DESCRIPTION },
116
119
  cursor: { type: "string", description: "Opaque pagination cursor from a previous response." },
117
120
  limit: { type: "number", description: "Page size, from 1 to " + logsStore.MAX_PAGE + "." },
118
121
  }),
@@ -125,6 +128,7 @@ function projectTools(bound) {
125
128
  query: { type: "string", description: "Search query." },
126
129
  kind: { type: "string", description: "Optional category filter, matched exactly against this project's vocabulary. The response lists the categories currently in use." },
127
130
  priority: { type: "string", enum: logsSchema.PRIORITIES, description: "Optional priority filter." },
131
+ contextMode: { type: "string", enum: ["current", "project", "all"], description: CONTEXT_DESCRIPTION },
128
132
  cursor: { type: "string", description: "Opaque pagination cursor from a previous response." },
129
133
  limit: { type: "number", description: "Page size, from 1 to " + logsStore.MAX_PAGE + "." },
130
134
  }, ["query"]),
@@ -148,27 +152,27 @@ function projectTools(bound) {
148
152
  },
149
153
  {
150
154
  name: "create_log",
151
- description: LOGS_CONTRACT + " " + LEARNING_CONTRACT + " Create a log entry in this project. Apply the test first: will someone need this fact weeks from now, and is it not already recorded? If it supersedes an existing entry, revise that entry instead of adding a new one.",
155
+ description: LOGS_CONTRACT + " " + LEARNING_CONTRACT + " Create a log entry for a new coherent user-directed task or durable project record. Search first; if the task already has an entry, revise it instead of adding a duplicate.",
152
156
  inputSchema: buildShape({
153
157
  kind: { type: "string", description: CATEGORY_DESCRIPTION },
154
158
  priority: { type: "string", enum: logsSchema.PRIORITIES, description: PRIORITY_DESCRIPTION },
155
159
  title: { type: "string", description: "Short factual title, plain text, written like a good commit subject." },
156
160
  summary: { type: "string", description: SUMMARY_DESCRIPTION },
157
- body: { type: "string", description: "The durable facts, context, and outcome in Markdown. No narration of your own activity. For a learning entry, cover the user's original wording or mental model, the precise concept, how it applies here, and any boundary or misconception." },
161
+ body: { type: "string", description: "The concise continuity record in Markdown: requested outcome and constraints, work/result, affected area, verification, current status, and next action when unfinished. Omit raw transcripts and command-by-command narration. For a learning entry, cover the user's original wording or mental model, the precise concept, how it applies here, and any boundary or misconception." },
158
162
  tags: { type: "string", description: "Optional JSON array of short tag strings." },
159
163
  }, ["kind", "title", "summary"]),
160
164
  handler: handler(bound, "createLog"),
161
165
  },
162
166
  {
163
167
  name: "update_log",
164
- description: LOGS_CONTRACT + " " + LEARNING_CONTRACT + " Revise an existing log entry, including when a later decision supersedes it, or when new learning refines an existing learning entry. The previous revision, its title, and its summary are all retained in the entry history.",
168
+ description: LOGS_CONTRACT + " " + LEARNING_CONTRACT + " Revise the coherent task entry when work progresses, completes, becomes blocked, or a later decision supersedes it. Also revise when new learning refines an existing learning entry. The previous revision, its title, and its summary are all retained in the entry history.",
165
169
  inputSchema: buildShape({
166
170
  ref: { type: "string", description: REF_DESCRIPTION },
167
171
  kind: { type: "string", description: CATEGORY_DESCRIPTION },
168
172
  priority: { type: "string", enum: logsSchema.PRIORITIES, description: PRIORITY_DESCRIPTION },
169
173
  title: { type: "string", description: "Replacement title. The previous title stays in the entry's history." },
170
174
  summary: { type: "string", description: "Replacement summary. The previous summary stays in the entry's history." },
171
- body: { type: "string", description: "Replacement record body in Markdown." },
175
+ body: { type: "string", description: "Replacement continuity record body in Markdown, including the request, current result, verification, and any remaining next action." },
172
176
  tags: { type: "string", description: "Optional JSON array of short tag strings, replacing the current tags." },
173
177
  }, ["ref"]),
174
178
  handler: handler(bound, "updateLog"),
@@ -238,6 +242,7 @@ function globalTools(bound) {
238
242
  kind: { type: "string", description: "Optional category filter, matched exactly against this project's vocabulary. The response lists the categories currently in use." },
239
243
  priority: { type: "string", enum: logsSchema.PRIORITIES, description: "Optional priority filter." },
240
244
  tag: { type: "string", description: "Optional single tag filter." },
245
+ contextMode: { type: "string", enum: ["project", "all"], description: CONTEXT_DESCRIPTION },
241
246
  cursor: { type: "string", description: "Opaque pagination cursor from a previous response." },
242
247
  limit: { type: "number", description: "Page size, from 1 to " + logsStore.MAX_PAGE + "." },
243
248
  }, ["projectSlug"]),
@@ -251,6 +256,7 @@ function globalTools(bound) {
251
256
  query: { type: "string", description: "Search query." },
252
257
  kind: { type: "string", description: "Optional category filter, matched exactly against this project's vocabulary. The response lists the categories currently in use." },
253
258
  priority: { type: "string", enum: logsSchema.PRIORITIES, description: "Optional priority filter." },
259
+ contextMode: { type: "string", enum: ["project", "all"], description: CONTEXT_DESCRIPTION },
254
260
  cursor: { type: "string", description: "Opaque pagination cursor from a previous response." },
255
261
  limit: { type: "number", description: "Page size, from 1 to " + logsStore.MAX_PAGE + "." },
256
262
  }, ["projectSlug", "query"]),
@@ -62,6 +62,19 @@ function page(items, args) {
62
62
  };
63
63
  }
64
64
 
65
+ function contextMode(options) {
66
+ var value = options && options.contextMode;
67
+ return value === "all" || value === "current" || value === "project" ? value : "all";
68
+ }
69
+
70
+ function matchesContext(entry, options) {
71
+ var mode = contextMode(options);
72
+ var changeSetId = entry && entry.context && entry.context.changeSetId;
73
+ if (mode === "all") return true;
74
+ if (mode === "project") return !changeSetId;
75
+ return !changeSetId || changeSetId === options.currentChangeSetId;
76
+ }
77
+
65
78
  function listEntries(entries, args, cleanLine, validateKind) {
66
79
  var options = args || {};
67
80
  var kind = options.kind ? validateKind(options.kind, null) : null;
@@ -73,6 +86,7 @@ function listEntries(entries, args, cleanLine, validateKind) {
73
86
  if (kind && entry.category !== kind) return false;
74
87
  if (priority && entry.priority !== priority) return false;
75
88
  if (tag && entry.tags.indexOf(tag) === -1) return false;
89
+ if (!matchesContext(entry, options)) return false;
76
90
  return true;
77
91
  });
78
92
  var result = page(filtered, options);
@@ -116,6 +130,7 @@ function summarize(entry) {
116
130
  // How many comments still await Driver review. A count only: the ledger
117
131
  // never carries comment bodies.
118
132
  pendingFeedbackCount: entry.pendingFeedbackCount || 0,
133
+ context: entry.context || null,
119
134
  };
120
135
  }
121
136
 
@@ -128,6 +143,7 @@ function searchEntries(entries, args, cleanLine, validateKind) {
128
143
  var candidates = entries.filter(function (entry) {
129
144
  if (kind && entry.category !== kind) return false;
130
145
  if (priority && entry.priority !== priority) return false;
146
+ if (!matchesContext(entry, options)) return false;
131
147
  return true;
132
148
  });
133
149
 
@@ -161,6 +177,8 @@ module.exports = {
161
177
  encodeCursor: encodeCursor,
162
178
  decodeCursor: decodeCursor,
163
179
  page: page,
180
+ contextMode: contextMode,
181
+ matchesContext: matchesContext,
164
182
  summarize: summarize,
165
183
  listEntries: listEntries,
166
184
  searchEntries: searchEntries,
@@ -60,8 +60,16 @@ function scopeIdForRoot(root) {
60
60
  return "project/" + utils.encodeCwd(root);
61
61
  }
62
62
 
63
+ function scopeIdForKnowledgeId(knowledgeId) {
64
+ if (typeof knowledgeId !== "string" || !/^[A-Za-z0-9_-]{1,120}$/.test(knowledgeId)) {
65
+ throw new Error("Invalid project knowledge id.");
66
+ }
67
+ return "project/" + knowledgeId;
68
+ }
69
+
63
70
  module.exports = {
64
71
  resolveProjectRoot: resolveProjectRoot,
65
72
  scopeIdForRoot: scopeIdForRoot,
73
+ scopeIdForKnowledgeId: scopeIdForKnowledgeId,
66
74
  canonicalPath: canonicalPath,
67
75
  };