clay-server 4.0.0-beta.8 → 4.0.0

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 (185) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +255 -0
  3. package/lib/capsule-mate-turn.js +105 -0
  4. package/lib/capsule-pig-logic.js +300 -0
  5. package/lib/capsule-server-runtimes.js +39 -0
  6. package/lib/capsules/pig/display.js +198 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +84 -0
  9. package/lib/config.js +25 -1
  10. package/lib/daemon-projects.js +13 -8
  11. package/lib/daemon.js +127 -14
  12. package/lib/git-cli.js +95 -1
  13. package/lib/knowledge-import.js +436 -0
  14. package/lib/knowledge-record-store.js +188 -0
  15. package/lib/knowledge-search.js +240 -0
  16. package/lib/mate-knowledge-mcp-server.js +151 -0
  17. package/lib/mate-knowledge-migration.js +283 -0
  18. package/lib/mate-knowledge-service.js +488 -0
  19. package/lib/mate-knowledge-sync.js +482 -0
  20. package/lib/migrate-single-user.js +2 -0
  21. package/lib/notes-lifecycle.js +149 -0
  22. package/lib/notes.js +60 -3
  23. package/lib/os-users.js +3 -1
  24. package/lib/project-capsule-catalog.js +9 -1
  25. package/lib/project-capsule-turn.js +96 -0
  26. package/lib/project-connection.js +15 -1
  27. package/lib/project-file-history.js +223 -0
  28. package/lib/project-file-watch.js +77 -49
  29. package/lib/project-filesystem.js +31 -220
  30. package/lib/project-http.js +15 -1
  31. package/lib/project-knowledge.js +22 -0
  32. package/lib/project-log-context.js +172 -0
  33. package/lib/project-log-feedback-delivery.js +118 -0
  34. package/lib/project-logs-comments.js +137 -0
  35. package/lib/project-logs-context-state.js +42 -0
  36. package/lib/project-logs-mcp-server.js +328 -0
  37. package/lib/project-logs-query.js +185 -0
  38. package/lib/project-logs-root.js +75 -0
  39. package/lib/project-logs-schema.js +259 -0
  40. package/lib/project-logs-service.js +463 -0
  41. package/lib/project-logs-snapshot.js +205 -0
  42. package/lib/project-logs-store.js +495 -0
  43. package/lib/project-logs-versioning.js +132 -0
  44. package/lib/project-logs.js +400 -0
  45. package/lib/project-mate-interaction.js +14 -0
  46. package/lib/project-mate-knowledge.js +110 -0
  47. package/lib/project-memory.js +22 -0
  48. package/lib/project-message-delivery.js +70 -0
  49. package/lib/project-pair-lifecycle.js +460 -0
  50. package/lib/project-request-access.js +66 -0
  51. package/lib/project-session-handoff.js +1 -1
  52. package/lib/project-session-notes.js +64 -13
  53. package/lib/project-session-pair.js +159 -146
  54. package/lib/project-session-spawn.js +1 -1
  55. package/lib/project-sessions.js +58 -13
  56. package/lib/project-user-message.js +53 -4
  57. package/lib/project-worker-permission.js +430 -0
  58. package/lib/project-worker-proposal.js +208 -62
  59. package/lib/project.js +185 -6
  60. package/lib/public/app.js +63 -35
  61. package/lib/public/css/admin.css +2 -2
  62. package/lib/public/css/capsule-ui.css +38 -0
  63. package/lib/public/css/command-palette.css +19 -9
  64. package/lib/public/css/git-placard.css +154 -0
  65. package/lib/public/css/home-hub.css +4 -0
  66. package/lib/public/css/home-session-actions.css +66 -0
  67. package/lib/public/css/home-sidebar.css +66 -0
  68. package/lib/public/css/input.css +84 -3
  69. package/lib/public/css/messages.css +40 -1
  70. package/lib/public/css/mobile-nav.css +80 -0
  71. package/lib/public/css/notifications-center.css +67 -0
  72. package/lib/public/css/pane.css +97 -10
  73. package/lib/public/css/project-logs.css +198 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/session-actions.css +16 -101
  76. package/lib/public/css/sidebar.css +103 -0
  77. package/lib/public/css/sticky-notes.css +237 -229
  78. package/lib/public/css/worker-proposal.css +36 -3
  79. package/lib/public/index.html +43 -11
  80. package/lib/public/modules/admin.js +8 -8
  81. package/lib/public/modules/app-connection.js +124 -6
  82. package/lib/public/modules/app-home-hub.js +12 -8
  83. package/lib/public/modules/app-message-cards.js +2 -1
  84. package/lib/public/modules/app-messages.js +92 -11
  85. package/lib/public/modules/app-misc.js +2 -14
  86. package/lib/public/modules/app-notifications.js +11 -7
  87. package/lib/public/modules/app-panels.js +3 -2
  88. package/lib/public/modules/app-projects.js +56 -18
  89. package/lib/public/modules/app-rendering.js +1 -0
  90. package/lib/public/modules/capsule-preference.js +44 -0
  91. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  92. package/lib/public/modules/clay-log-links.js +70 -0
  93. package/lib/public/modules/command-palette.js +52 -15
  94. package/lib/public/modules/context-sources.js +0 -3
  95. package/lib/public/modules/git-agent-sessions.js +85 -0
  96. package/lib/public/modules/git-panel.js +111 -84
  97. package/lib/public/modules/git-placard.js +161 -0
  98. package/lib/public/modules/home-chat-empty-state.js +39 -0
  99. package/lib/public/modules/home-chat-identity.js +9 -0
  100. package/lib/public/modules/home-conversations-sheet.js +102 -48
  101. package/lib/public/modules/home-mate-chat.js +15 -35
  102. package/lib/public/modules/home-session-actions.js +6 -0
  103. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  104. package/lib/public/modules/home-sidebar.js +7 -0
  105. package/lib/public/modules/home-surface-boot.js +7 -3
  106. package/lib/public/modules/home-tool-frame.js +182 -0
  107. package/lib/public/modules/home-tools.js +138 -1
  108. package/lib/public/modules/input.js +38 -7
  109. package/lib/public/modules/markdown.js +2 -0
  110. package/lib/public/modules/mate-sidebar.js +0 -8
  111. package/lib/public/modules/message-delivery.js +97 -0
  112. package/lib/public/modules/paste-modal.js +84 -0
  113. package/lib/public/modules/project-activation.js +45 -0
  114. package/lib/public/modules/project-logs-render.js +405 -0
  115. package/lib/public/modules/project-logs.js +461 -0
  116. package/lib/public/modules/project-removal-target.js +26 -0
  117. package/lib/public/modules/scheduler.js +17 -3
  118. package/lib/public/modules/session-actions.js +29 -105
  119. package/lib/public/modules/session-hierarchy.js +54 -0
  120. package/lib/public/modules/sidebar-mobile.js +40 -15
  121. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  122. package/lib/public/modules/sidebar-sessions.js +77 -11
  123. package/lib/public/modules/sidebar.js +17 -5
  124. package/lib/public/modules/split-group-helpers.js +8 -0
  125. package/lib/public/modules/split-pair-ui.js +15 -4
  126. package/lib/public/modules/split-view.js +5 -2
  127. package/lib/public/modules/split-worker-runtime.js +24 -0
  128. package/lib/public/modules/sticky-note-markdown.js +5 -0
  129. package/lib/public/modules/sticky-notes-browser.js +326 -0
  130. package/lib/public/modules/sticky-notes-card.js +360 -0
  131. package/lib/public/modules/sticky-notes-editor.js +291 -0
  132. package/lib/public/modules/sticky-notes-shared.js +90 -0
  133. package/lib/public/modules/sticky-notes.js +170 -946
  134. package/lib/public/modules/thinking-lifecycle.js +175 -0
  135. package/lib/public/modules/thinking-summary.js +28 -0
  136. package/lib/public/modules/thinking-view.js +75 -0
  137. package/lib/public/modules/tool-palette-order.js +143 -0
  138. package/lib/public/modules/tool-palette-overlays.js +159 -0
  139. package/lib/public/modules/tool-palette.js +21 -188
  140. package/lib/public/modules/tools.js +31 -123
  141. package/lib/public/modules/update-snooze.js +169 -0
  142. package/lib/public/modules/user-settings.js +4 -0
  143. package/lib/public/modules/worker-pane-lock.js +176 -0
  144. package/lib/public/modules/worker-proposal-state.js +16 -0
  145. package/lib/public/modules/worker-proposal.js +49 -20
  146. package/lib/public/style.css +3 -1
  147. package/lib/sdk-bridge.js +135 -46
  148. package/lib/sdk-message-processor.js +36 -19
  149. package/lib/server-experimental-settings.js +49 -0
  150. package/lib/server-global-ws.js +11 -0
  151. package/lib/server-home-chat-events.js +31 -1
  152. package/lib/server-home-chat.js +27 -4
  153. package/lib/server-home-clay-entry.js +1 -1
  154. package/lib/server-home-clay-session-links.js +34 -5
  155. package/lib/server-settings.js +5 -0
  156. package/lib/server-tools.js +127 -9
  157. package/lib/server.js +137 -22
  158. package/lib/session-driver-eligibility.js +38 -0
  159. package/lib/session-driver-orchestration.js +31 -0
  160. package/lib/session-hygiene.js +3 -0
  161. package/lib/session-notes-mcp-server.js +25 -5
  162. package/lib/session-pair-factory.js +247 -0
  163. package/lib/session-pair-mcp-server.js +46 -8
  164. package/lib/session-pair-prompts.js +79 -0
  165. package/lib/session-pair-turn-control.js +153 -0
  166. package/lib/session-provenance.js +119 -0
  167. package/lib/session-spawn-mcp-server.js +1 -1
  168. package/lib/session-split-groups.js +4 -1
  169. package/lib/session-title-policy.js +60 -0
  170. package/lib/session-visibility.js +41 -0
  171. package/lib/sessions.js +57 -10
  172. package/lib/tools-registry.js +89 -10
  173. package/lib/update-snooze.js +437 -0
  174. package/lib/users-experimental-preferences.js +30 -0
  175. package/lib/users-permissions.js +6 -6
  176. package/lib/users.js +22 -16
  177. package/lib/workspace-query-access.js +102 -0
  178. package/lib/workspace-query-service.js +24 -17
  179. package/lib/worktree.js +25 -38
  180. package/lib/ws-schema.js +38 -7
  181. package/lib/yoke/adapters/claude-worker.js +16 -6
  182. package/lib/yoke/adapters/claude.js +12 -0
  183. package/lib/yoke/adapters/codex.js +24 -14
  184. package/lib/yoke/vendor-registry.js +1 -1
  185. package/package.json +2 -2
@@ -0,0 +1,172 @@
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 { runGitSync } = require("./git-cli");
13
+ var { fsAsUser } = require("./os-users");
14
+ var logsRoot = require("./project-logs-root");
15
+ var utils = require("./utils");
16
+
17
+ var ID_PATTERN = /^[A-Za-z0-9_-]{1,120}$/;
18
+
19
+ function newId(prefix) {
20
+ return prefix + "_" + crypto.randomBytes(16).toString("base64url");
21
+ }
22
+
23
+ function validId(value, prefix) {
24
+ return typeof value === "string" && ID_PATTERN.test(value) && (!prefix || value.indexOf(prefix + "_") === 0);
25
+ }
26
+
27
+ function legacyKnowledgeId(cwd) {
28
+ return utils.encodeCwd(logsRoot.resolveProjectRoot(cwd));
29
+ }
30
+
31
+ function ensureProjectKnowledgeId(project) {
32
+ if (!project || typeof project !== "object") return false;
33
+ if (validId(project.projectKnowledgeId)) return false;
34
+ project.projectKnowledgeId = project.path ? legacyKnowledgeId(project.path) : newId("pk");
35
+ return true;
36
+ }
37
+
38
+ function initializeProjectKnowledge(config) {
39
+ var changed = false;
40
+ var projects = config && Array.isArray(config.projects) ? config.projects : [];
41
+ for (var i = 0; i < projects.length; i++) {
42
+ if (ensureProjectKnowledgeId(projects[i])) changed = true;
43
+ }
44
+ return changed;
45
+ }
46
+
47
+ function createProjectKnowledgeId() {
48
+ return newId("pk");
49
+ }
50
+
51
+ function git(cwd, args, osUserInfo) {
52
+ return runGitSync(cwd, args, null, osUserInfo).trim();
53
+ }
54
+
55
+ function commit(cwd, ref, osUserInfo) {
56
+ try { return git(cwd, ["rev-parse", ref || "HEAD"], osUserInfo); }
57
+ catch (e) { return null; }
58
+ }
59
+
60
+ function markerPath(cwd, osUserInfo) {
61
+ try {
62
+ var gitDir = git(cwd, ["rev-parse", "--git-dir"], osUserInfo);
63
+ return path.join(path.resolve(cwd, gitDir), "clay-change-set.json");
64
+ } catch (e) {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ function readMarker(filePath, knowledgeId) {
70
+ if (!filePath) return null;
71
+ try {
72
+ var parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
73
+ if (!parsed || parsed.projectKnowledgeId !== knowledgeId || !validId(parsed.changeSetId, "cs")) return null;
74
+ return parsed;
75
+ } catch (e) {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function deterministicFallback(knowledgeId, cwd) {
81
+ var digest = crypto.createHash("sha256").update(knowledgeId + "\u0000" + logsRoot.canonicalPath(cwd)).digest("base64url");
82
+ return "cs_" + digest.substring(0, 22);
83
+ }
84
+
85
+ function writeMarker(filePath, marker, osUserInfo) {
86
+ var content = JSON.stringify(marker, null, 2) + "\n";
87
+ if (osUserInfo) {
88
+ fsAsUser("write", { file: filePath, content: content, flag: "wx", mode: 0o600 }, osUserInfo);
89
+ return;
90
+ }
91
+ fs.writeFileSync(filePath, content, { flag: "wx", mode: 0o600 });
92
+ }
93
+
94
+ function resolveWorktreeContext(cwd, knowledgeId, details, osUserInfo) {
95
+ var input = details || {};
96
+ var filePath = markerPath(cwd, osUserInfo);
97
+ var marker = readMarker(filePath, knowledgeId);
98
+ if (!marker) {
99
+ marker = {
100
+ projectKnowledgeId: knowledgeId,
101
+ changeSetId: newId("cs"),
102
+ baseCommit: input.baseCommit || null,
103
+ createdAt: Date.now(),
104
+ };
105
+ if (filePath) {
106
+ try {
107
+ writeMarker(filePath, marker, osUserInfo);
108
+ } catch (e) {
109
+ marker = readMarker(filePath, knowledgeId) || marker;
110
+ }
111
+ }
112
+ }
113
+ if (!filePath || !readMarker(filePath, knowledgeId)) {
114
+ marker.changeSetId = deterministicFallback(knowledgeId, cwd);
115
+ }
116
+ return {
117
+ kind: "worktree",
118
+ changeSetId: marker.changeSetId,
119
+ branch: input.branch || null,
120
+ baseCommit: marker.baseCommit || input.baseCommit || null,
121
+ headCommit: commit(cwd, "HEAD", osUserInfo),
122
+ status: "active",
123
+ };
124
+ }
125
+
126
+ function isMerged(parentCwd, worktreeCwd, osUserInfo) {
127
+ var head = commit(worktreeCwd, "HEAD", osUserInfo);
128
+ if (!head) return false;
129
+ try {
130
+ runGitSync(parentCwd, ["merge-base", "--is-ancestor", head, "HEAD"], null, osUserInfo);
131
+ return true;
132
+ } catch (e) {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ function cleanCommit(value) {
138
+ return typeof value === "string" && /^[0-9a-f]{7,64}$/i.test(value) ? value : null;
139
+ }
140
+
141
+ function cleanBranch(value) {
142
+ if (typeof value !== "string") return null;
143
+ var cleaned = value.trim().substring(0, 200);
144
+ return cleaned && cleaned.indexOf("\n") === -1 && cleaned.indexOf("\r") === -1 ? cleaned : null;
145
+ }
146
+
147
+ function normalizeRecordContext(value) {
148
+ if (!value || value.kind !== "worktree" || !validId(value.changeSetId, "cs")) {
149
+ return { kind: "project", changeSetId: null, branch: null, baseCommit: null, headCommit: null, status: "active" };
150
+ }
151
+ return {
152
+ kind: "worktree",
153
+ changeSetId: value.changeSetId,
154
+ branch: cleanBranch(value.branch),
155
+ baseCommit: cleanCommit(value.baseCommit),
156
+ headCommit: cleanCommit(value.headCommit),
157
+ status: value.status === "merged" || value.status === "archived" ? value.status : "active",
158
+ };
159
+ }
160
+
161
+ module.exports = {
162
+ ID_PATTERN: ID_PATTERN,
163
+ validId: validId,
164
+ legacyKnowledgeId: legacyKnowledgeId,
165
+ ensureProjectKnowledgeId: ensureProjectKnowledgeId,
166
+ initializeProjectKnowledge: initializeProjectKnowledge,
167
+ createProjectKnowledgeId: createProjectKnowledgeId,
168
+ resolveWorktreeContext: resolveWorktreeContext,
169
+ commit: commit,
170
+ isMerged: isMerged,
171
+ normalizeRecordContext: normalizeRecordContext,
172
+ };
@@ -0,0 +1,118 @@
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
+
5
+ function attachProjectLogFeedbackDelivery(ctx) {
6
+ var sm = ctx.sm;
7
+ var getSdk = ctx.getSdk;
8
+ var onProcessingChanged = ctx.onProcessingChanged || function () {};
9
+ var getLinuxUserForSession = ctx.getLinuxUserForSession || function () { return null; };
10
+
11
+ function sessionForAuthor(author) {
12
+ if (!author || author.type !== "session" || !author.sessionKey) return null;
13
+ var match = null;
14
+ sm.sessions.forEach(function (session) {
15
+ if (match || !session || session.destroying || session.hidden) return;
16
+ if (session.cliSessionId !== author.sessionKey && "local:" + session.localId !== author.sessionKey) return;
17
+ if ((session.ownerId || null) !== (author.userId || null)) return;
18
+ if (sm.sessions.get(session.localId) !== session) return;
19
+ match = session;
20
+ });
21
+ return match;
22
+ }
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
+
66
+ function feedbackPrompt(entry) {
67
+ return "[Project Log feedback]\n" +
68
+ "A user added a comment to " + entry.ref + ". Review the pending feedback now with list_log_feedback, " +
69
+ "then resolve this specific comment through review_log_comment. Apply supported corrections to the canonical " +
70
+ "entry; otherwise clarify or decline with a concise reason. Do not treat this internal notification as user " +
71
+ "chat or answer it only in the conversation.\n" +
72
+ "[End Project Log feedback]";
73
+ }
74
+
75
+ function deliver(entry) {
76
+ if (!entry || !entry.ref || !entry.pendingFeedbackCount) return false;
77
+ var session = findAuthorSession(entry);
78
+ var sdk = getSdk();
79
+ if (!session || !sdk) return false;
80
+ var text = feedbackPrompt(entry);
81
+ if (session.isProcessing && startImmediateReviewer(session, entry, text, sdk)) return true;
82
+ sm.sendAndRecord(session, {
83
+ type: "user_message",
84
+ text: text,
85
+ _internal: true,
86
+ projectLogFeedback: true,
87
+ projectLogRef: entry.ref,
88
+ });
89
+ session.lastActivity = Date.now();
90
+ if (!session.isProcessing) {
91
+ session.isProcessing = true;
92
+ session.sentToolResults = {};
93
+ onProcessingChanged();
94
+ sm.sendToSession(session, { type: "status", status: "processing" });
95
+ }
96
+ if (!sdk.pushMessage(session, text)) {
97
+ session._queryStartTs = Date.now();
98
+ Promise.resolve(sdk.startQuery(session, text, null, getLinuxUserForSession(session))).catch(function (error) {
99
+ session.isProcessing = false;
100
+ sm.sendAndRecord(session, {
101
+ type: "error",
102
+ text: "Could not deliver Project Log feedback: " + (error.message || String(error)),
103
+ _internal: true,
104
+ });
105
+ onProcessingChanged();
106
+ });
107
+ }
108
+ sm.broadcastSessionList();
109
+ return true;
110
+ }
111
+
112
+ return {
113
+ deliver: deliver,
114
+ findAuthorSession: findAuthorSession,
115
+ };
116
+ }
117
+
118
+ module.exports = { attachProjectLogFeedbackDelivery: attachProjectLogFeedbackDelivery };
@@ -0,0 +1,137 @@
1
+ // Comment and review projection for Project Logs.
2
+ //
3
+ // Split from project-logs-store.js so the store stays inside the module size
4
+ // limit.
5
+ //
6
+ // A user comment is a revision proposal, never an automatic canonical
7
+ // mutation. It starts `pending` and stays that way until the project's own
8
+ // Driver session reviews it. Reviews are append-only records that reference a
9
+ // comment id; nothing is ever edited in place, and a comment written before
10
+ // reviews existed simply projects as `pending`.
11
+
12
+ var STATUS_PENDING = "pending";
13
+ var STATUS_CLARIFICATION = "clarification-needed";
14
+ var STATUS_INCORPORATED = "incorporated";
15
+ var STATUS_DECLINED = "declined";
16
+
17
+ var STATUSES = [STATUS_PENDING, STATUS_CLARIFICATION, STATUS_INCORPORATED, STATUS_DECLINED];
18
+
19
+ // Actions the Driver may take. `incorporate` is the only one that produces a
20
+ // canonical revision.
21
+ var ACTIONS = ["incorporate", "clarify", "decline"];
22
+
23
+ var ACTION_STATUS = {
24
+ incorporate: STATUS_INCORPORATED,
25
+ clarify: STATUS_CLARIFICATION,
26
+ decline: STATUS_DECLINED,
27
+ };
28
+
29
+ // Waiting on the Driver. Only an unreviewed comment qualifies.
30
+ //
31
+ // A clarification is deliberately NOT Driver work: the Driver has already
32
+ // answered, and the ball is with the user. Because a comment is settled once
33
+ // and never re-reviewed, counting a clarification as pending would leave it in
34
+ // the queue forever with no way out. The user's reply is a new comment, which
35
+ // is pending in its own right and can be incorporated or declined.
36
+ function isPendingReview(status) {
37
+ return status === STATUS_PENDING;
38
+ }
39
+
40
+ // Answered by the Driver and now waiting on the person who commented. Shown
41
+ // permanently on the original comment; never counted as Driver work.
42
+ function isAwaitingUser(status) {
43
+ return status === STATUS_CLARIFICATION;
44
+ }
45
+
46
+ function isAction(value) {
47
+ return ACTIONS.indexOf(value) !== -1;
48
+ }
49
+
50
+ // Project the comment thread of one entry, folding review records onto the
51
+ // comments they resolve. `revisionForRecordId` maps an incorporating review
52
+ // record id to the canonical revision number it produced, so an incorporated
53
+ // comment can point at its result.
54
+ function comments(chain, options) {
55
+ var settings = options || {};
56
+ var maxComments = settings.maxComments || 200;
57
+ var revisionForRecordId = settings.revisionForRecordId || {};
58
+ var byId = {};
59
+ var order = [];
60
+
61
+ var i;
62
+ for (i = 0; i < chain.length; i++) {
63
+ var record = chain[i];
64
+ if (record.op !== "comment") continue;
65
+ if (order.length >= maxComments) break;
66
+ var comment = {
67
+ id: record.id,
68
+ body: record.body || "",
69
+ at: record.at || 0,
70
+ author: record.author || null,
71
+ // A comment with no review yet is pending, which is also how every
72
+ // comment written before reviews existed projects.
73
+ status: STATUS_PENDING,
74
+ review: null,
75
+ };
76
+ byId[record.id] = comment;
77
+ order.push(comment);
78
+ }
79
+
80
+ for (i = 0; i < chain.length; i++) {
81
+ var review = chain[i];
82
+ if (review.op !== "review") continue;
83
+ var target = byId[review.commentId];
84
+ if (!target) continue;
85
+ if (!isAction(review.action)) continue;
86
+ // Append-only: the first review of a comment settles it. A later review of
87
+ // the same comment is ignored rather than silently overwriting the record
88
+ // of what was decided.
89
+ if (target.review) continue;
90
+ target.status = ACTION_STATUS[review.action];
91
+ target.review = {
92
+ action: review.action,
93
+ response: typeof review.response === "string" ? review.response : "",
94
+ at: review.at || 0,
95
+ reviewer: review.author || null,
96
+ revision: review.action === "incorporate"
97
+ ? (revisionForRecordId[review.id] || null)
98
+ : null,
99
+ };
100
+ }
101
+
102
+ return order;
103
+ }
104
+
105
+ // How many comments are still waiting on the Driver. Used for the ledger row
106
+ // count and for the system-prompt signal, neither of which carries any body.
107
+ function pendingReviewCount(list) {
108
+ var count = 0;
109
+ for (var i = 0; i < (list || []).length; i++) {
110
+ if (isPendingReview(list[i].status)) count++;
111
+ }
112
+ return count;
113
+ }
114
+
115
+ function pendingReviewComments(list) {
116
+ var out = [];
117
+ for (var i = 0; i < (list || []).length; i++) {
118
+ if (isPendingReview(list[i].status)) out.push(list[i]);
119
+ }
120
+ return out;
121
+ }
122
+
123
+ module.exports = {
124
+ STATUS_PENDING: STATUS_PENDING,
125
+ STATUS_CLARIFICATION: STATUS_CLARIFICATION,
126
+ STATUS_INCORPORATED: STATUS_INCORPORATED,
127
+ STATUS_DECLINED: STATUS_DECLINED,
128
+ STATUSES: STATUSES,
129
+ ACTIONS: ACTIONS,
130
+ ACTION_STATUS: ACTION_STATUS,
131
+ isPendingReview: isPendingReview,
132
+ isAwaitingUser: isAwaitingUser,
133
+ isAction: isAction,
134
+ comments: comments,
135
+ pendingReviewCount: pendingReviewCount,
136
+ pendingReviewComments: pendingReviewComments,
137
+ };
@@ -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 };