clay-server 4.1.0-beta.5 → 4.1.0-beta.7
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/LICENSE +661 -21
- package/LICENSES/MIT-legacy.txt +21 -0
- package/LICENSES/README.md +7 -0
- package/README.md +2 -2
- package/bin/cli.js +0 -2
- package/lib/autonomous-run-record.js +32 -0
- package/lib/autonomous-run-scheduler.js +92 -0
- package/lib/daemon.js +0 -2
- package/lib/default-ai-runtime.js +117 -0
- package/lib/durable-scheduler-record.js +61 -0
- package/lib/durable-scheduler-store.js +185 -0
- package/lib/durable-scheduler.js +495 -0
- package/lib/git-cli.js +1 -0
- package/lib/issue-work-session.js +10 -0
- package/lib/issues-comments.js +40 -0
- package/lib/issues-commit-verification.js +30 -0
- package/lib/issues-mcp-server.js +27 -0
- package/lib/issues-schema.js +119 -0
- package/lib/issues-service.js +339 -0
- package/lib/issues-store.js +210 -0
- package/lib/loop-cron.js +47 -0
- package/lib/loop-durable-schedule.js +159 -0
- package/lib/loop-guidance.js +35 -0
- package/lib/loop-registry-occurrence.js +76 -0
- package/lib/model-entry.js +15 -0
- package/lib/project-autonomous-run.js +498 -0
- package/lib/project-capsule-turn.js +1 -0
- package/lib/project-connection.js +6 -1
- package/lib/project-delegated-session.js +1 -0
- package/lib/project-filesystem.js +10 -10
- package/lib/project-full-access.js +86 -0
- package/lib/project-http.js +3 -1
- package/lib/project-issue-launch.js +154 -0
- package/lib/project-issues.js +58 -0
- package/lib/project-logs-mcp-server.js +8 -12
- package/lib/project-loop-interview.js +163 -0
- package/lib/project-loop.js +150 -64
- package/lib/project-message-delivery.js +4 -2
- package/lib/project-models.js +71 -3
- package/lib/project-pair-autonomous-stop.js +16 -0
- package/lib/project-pair-owned-stop.js +18 -0
- package/lib/project-pair-task-control.js +14 -1
- package/lib/project-pending-message-queue.js +264 -0
- package/lib/project-scheduled-messages.js +316 -0
- package/lib/project-scheduled-task-execution.js +236 -0
- package/lib/project-scheduled-tasks.js +481 -0
- package/lib/project-session-pair.js +16 -19
- package/lib/project-sessions.js +114 -31
- package/lib/project-tool-llm-runtime.js +48 -0
- package/lib/project-user-message.js +202 -60
- package/lib/project-workspace-query.js +25 -0
- package/lib/project.js +361 -69
- package/lib/public/app.js +85 -22
- package/lib/public/css/default-ai.css +33 -0
- package/lib/public/css/home-hub.css +15 -2
- package/lib/public/css/home-sidebar.css +50 -2
- package/lib/public/css/input.css +160 -5
- package/lib/public/css/issues.css +101 -0
- package/lib/public/css/messages.css +19 -0
- package/lib/public/css/pane.css +58 -7
- package/lib/public/css/pending-message-queue.css +187 -0
- package/lib/public/css/rewind.css +11 -2
- package/lib/public/css/scheduled-tasks.css +122 -0
- package/lib/public/index.html +42 -150
- package/lib/public/modules/app-connection.js +3 -0
- package/lib/public/modules/app-cursors.js +29 -12
- package/lib/public/modules/app-loop-ui.js +4 -8
- package/lib/public/modules/app-message-router.js +8 -0
- package/lib/public/modules/app-messages.js +34 -9
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/app-rate-limit.js +8 -5
- package/lib/public/modules/autonomous-run.js +285 -0
- package/lib/public/modules/clay-file-links.js +111 -0
- package/lib/public/modules/clay-issue-links.js +131 -0
- package/lib/public/modules/clay-log-links.js +2 -0
- package/lib/public/modules/cursor-sharing-preference.js +38 -0
- package/lib/public/modules/default-ai.js +377 -0
- package/lib/public/modules/filebrowser.js +43 -2
- package/lib/public/modules/home-sidebar.js +16 -8
- package/lib/public/modules/home-surface.js +5 -0
- package/lib/public/modules/input.js +11 -20
- package/lib/public/modules/issues-context.js +117 -0
- package/lib/public/modules/issues-render.js +29 -0
- package/lib/public/modules/issues.js +270 -0
- package/lib/public/modules/loop-interview.js +151 -0
- package/lib/public/modules/markdown.js +4 -0
- package/lib/public/modules/message-delivery.js +5 -3
- package/lib/public/modules/pending-message-image-editor.js +143 -0
- package/lib/public/modules/pending-message-queue-model.js +22 -0
- package/lib/public/modules/pending-message-queue.js +449 -0
- package/lib/public/modules/project-logs.js +2 -2
- package/lib/public/modules/scheduled-message-state.js +46 -0
- package/lib/public/modules/scheduled-task-edit-state.js +7 -0
- package/lib/public/modules/scheduled-task-engine.js +48 -0
- package/lib/public/modules/scheduled-task-form.js +122 -0
- package/lib/public/modules/scheduled-task-recurrence.js +46 -0
- package/lib/public/modules/scheduled-task-schedule-rule.js +115 -0
- package/lib/public/modules/scheduled-tasks.js +459 -0
- package/lib/public/modules/scheduler-config.js +6 -52
- package/lib/public/modules/scheduler.js +9 -21
- package/lib/public/modules/sidebar-mobile.js +3 -0
- package/lib/public/modules/sidebar.js +0 -11
- package/lib/public/modules/split-view.js +2 -0
- package/lib/public/modules/sticky-note-markdown.js +1 -0
- package/lib/public/modules/terminal.js +2 -0
- package/lib/public/modules/tool-palette-order.js +3 -3
- package/lib/public/modules/tools.js +5 -2
- package/lib/public/style.css +3 -0
- package/lib/record-revision.js +11 -0
- package/lib/schedule-rule.js +151 -0
- package/lib/schedule-validation.js +124 -0
- package/lib/scheduled-run-state.js +87 -0
- package/lib/scheduled-task-records.js +127 -0
- package/lib/scheduler.js +179 -196
- package/lib/sdk-bridge.js +157 -22
- package/lib/sdk-message-processor.js +3 -0
- package/lib/server-cursor-sharing.js +65 -0
- package/lib/server-default-ai.js +265 -0
- package/lib/server-durable-scheduler.js +26 -0
- package/lib/server-home-chat.js +29 -9
- package/lib/server-home-clay-entry.js +4 -1
- package/lib/server-home-debate-planning.js +32 -14
- package/lib/server-home-mate-creation.js +30 -11
- package/lib/server-home-models.js +14 -6
- package/lib/server-issue-links.js +18 -0
- package/lib/server.js +63 -0
- package/lib/session-notes-mcp-server.js +1 -1
- package/lib/session-pair-factory.js +42 -4
- package/lib/session-pair-query-start.js +22 -0
- package/lib/session-query-tool-bridge.js +52 -0
- package/lib/session-search.js +4 -1
- package/lib/session-split-groups.js +35 -1
- package/lib/sessions.js +51 -3
- package/lib/tool-llm.js +1 -0
- package/lib/users-default-ai-preferences.js +80 -0
- package/lib/users-home-surface-preferences.js +2 -1
- package/lib/users-preferences.js +35 -0
- package/lib/users-scheduled-task-preferences.js +44 -0
- package/lib/users.js +16 -0
- package/lib/workspace-query-projection.js +29 -0
- package/lib/workspace-query-service.js +19 -29
- package/lib/ws-schema.js +61 -1
- package/lib/yoke/adapters/antigravity.js +1 -0
- package/lib/yoke/adapters/claude-worker.js +11 -6
- package/lib/yoke/adapters/claude.js +35 -1
- package/lib/yoke/adapters/codex.js +3 -0
- package/lib/yoke/claude-user-input.js +2 -0
- package/lib/yoke/index.js +2 -0
- package/lib/yoke/mcp-bridge-server.js +7 -1
- package/lib/yoke/package.json +1 -1
- package/lib/yoke/session-tool-transport.js +53 -0
- package/lib/yoke/user-input.js +21 -7
- package/package.json +5 -6
- package/bin/claude-relay.js +0 -6
- package/lib/public/modules/app-loop-wizard.js +0 -439
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Validation and bounded projections for Issues.
|
|
2
|
+
|
|
3
|
+
var TYPES = ["bug", "feature", "plan"];
|
|
4
|
+
var PRIORITIES = ["normal", "important", "urgent"];
|
|
5
|
+
var STATUSES = ["open", "in_progress", "resolved", "closed"];
|
|
6
|
+
|
|
7
|
+
function text(value, name, max, required) {
|
|
8
|
+
if (typeof value !== "string") {
|
|
9
|
+
throw new Error(name + " must be a string.");
|
|
10
|
+
}
|
|
11
|
+
var result = value.trim();
|
|
12
|
+
if (required && !result) throw new Error(name + " is required.");
|
|
13
|
+
if (result.length > max) throw new Error(name + " exceeds " + max + " characters.");
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function choice(value, name, allowed, fallback) {
|
|
18
|
+
var result = value === undefined ? fallback : value;
|
|
19
|
+
if (typeof result !== "string" || allowed.indexOf(result) === -1) {
|
|
20
|
+
throw new Error("Invalid " + name + ".");
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function clone(value) {
|
|
26
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeVerifiedCommit(value) {
|
|
30
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid verified commit evidence.");
|
|
31
|
+
var sha = text(value.sha, "verified commit SHA", 64, true).toLowerCase();
|
|
32
|
+
if (!/^[0-9a-f]{40,64}$/.test(sha)) throw new Error("Verified commit evidence requires a full SHA.");
|
|
33
|
+
return {
|
|
34
|
+
sha: sha,
|
|
35
|
+
subject: text(value.subject, "verified commit subject", 400, true),
|
|
36
|
+
verifiedAt: Number(value.verifiedAt) || Date.now(),
|
|
37
|
+
repository: text(value.repository, "verified commit repository", 120, true),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeContext(value) {
|
|
42
|
+
if (value == null) return null;
|
|
43
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid issue context.");
|
|
44
|
+
var result = {};
|
|
45
|
+
if (value.kind !== undefined) result.kind = text(value.kind, "context.kind", 32, true);
|
|
46
|
+
if (value.changeSetId !== undefined) result.changeSetId = text(value.changeSetId, "context.changeSetId", 120, true);
|
|
47
|
+
if (value.branch != null) result.branch = text(value.branch, "context.branch", 200, false) || null;
|
|
48
|
+
if (value.baseCommit != null) result.baseCommit = text(value.baseCommit, "context.baseCommit", 64, false) || null;
|
|
49
|
+
if (value.headCommit != null) result.headCommit = text(value.headCommit, "context.headCommit", 64, false) || null;
|
|
50
|
+
return Object.keys(result).length ? result : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeSessions(value) {
|
|
54
|
+
if (value === undefined) return [];
|
|
55
|
+
if (!Array.isArray(value) || value.length > 40) throw new Error("linked work sessions must be an array of at most 40 items.");
|
|
56
|
+
var out = [];
|
|
57
|
+
for (var i = 0; i < value.length; i++) {
|
|
58
|
+
if (!value[i] || typeof value[i] !== "object") throw new Error("Invalid linked work session.");
|
|
59
|
+
var session = {
|
|
60
|
+
sessionId: text(value[i].sessionId, "linked work session id", 120, true),
|
|
61
|
+
role: text(value[i].role, "linked work session role", 32, true),
|
|
62
|
+
};
|
|
63
|
+
if (value[i].issueRevision !== undefined) {
|
|
64
|
+
if (!Number.isInteger(value[i].issueRevision) || value[i].issueRevision < 1) throw new Error("Invalid linked issue revision.");
|
|
65
|
+
session.issueRevision = value[i].issueRevision;
|
|
66
|
+
}
|
|
67
|
+
if (value[i].requestId !== undefined) session.requestId = text(value[i].requestId, "linked work request id", 200, true);
|
|
68
|
+
out.push(session);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function snapshot(input, current) {
|
|
74
|
+
var data = input || {};
|
|
75
|
+
var base = current || {};
|
|
76
|
+
var allowed = { title: true, summary: true, body: true, resolutionSummary: true, type: true, priority: true, status: true,
|
|
77
|
+
context: true, linkedWorkSessions: true, resolutionHistory: true, verifiedCommit: true, closeReason: true };
|
|
78
|
+
var keys = Object.keys(data);
|
|
79
|
+
for (var ki = 0; ki < keys.length; ki++) if (!Object.prototype.hasOwnProperty.call(allowed, keys[ki])) throw new Error("Unknown issue field: " + keys[ki] + ".");
|
|
80
|
+
var result = {
|
|
81
|
+
title: data.title === undefined ? base.title || "" : text(data.title, "title", 200, true),
|
|
82
|
+
summary: data.summary === undefined ? base.summary || "" : text(data.summary, "summary", 400, false),
|
|
83
|
+
body: data.body === undefined ? base.body || "" : text(data.body, "body", 20000, false),
|
|
84
|
+
resolutionSummary: data.resolutionSummary === undefined ? base.resolutionSummary || "" : text(data.resolutionSummary, "resolutionSummary", 400, false),
|
|
85
|
+
type: data.type === undefined ? base.type || "bug" : choice(data.type, "type", TYPES),
|
|
86
|
+
priority: data.priority === undefined ? base.priority || "normal" : choice(data.priority, "priority", PRIORITIES),
|
|
87
|
+
status: data.status === undefined ? base.status || "open" : choice(data.status, "status", STATUSES),
|
|
88
|
+
context: data.context === undefined ? clone(base.context || null) : normalizeContext(data.context),
|
|
89
|
+
linkedWorkSessions: data.linkedWorkSessions === undefined ? clone(base.linkedWorkSessions || []) : normalizeSessions(data.linkedWorkSessions),
|
|
90
|
+
resolutionHistory: clone(base.resolutionHistory || []),
|
|
91
|
+
};
|
|
92
|
+
if (!result.title) throw new Error("title is required.");
|
|
93
|
+
if (data.verifiedCommit !== undefined) result.verifiedCommit = normalizeVerifiedCommit(data.verifiedCommit);
|
|
94
|
+
else if (base.verifiedCommit) result.verifiedCommit = normalizeVerifiedCommit(base.verifiedCommit);
|
|
95
|
+
if (result.status === "resolved" && (!result.resolutionSummary || !result.verifiedCommit)) {
|
|
96
|
+
throw new Error("resolved issues require a resolutionSummary and verified commit evidence.");
|
|
97
|
+
}
|
|
98
|
+
if (result.status !== "resolved" && result.status !== "closed") delete result.verifiedCommit;
|
|
99
|
+
if (result.status !== "resolved" && result.status !== "closed") delete result.resolutionSummary;
|
|
100
|
+
if (result.status === "closed") {
|
|
101
|
+
var reason = data.closeReason === undefined ? base.closeReason : data.closeReason;
|
|
102
|
+
result.closeReason = choice(reason, "close reason", ["declined", "duplicate", "non_code_decision"]);
|
|
103
|
+
} else if (data.closeReason !== undefined) {
|
|
104
|
+
throw new Error("closeReason is only valid when closing an issue.");
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
TYPES: TYPES,
|
|
111
|
+
PRIORITIES: PRIORITIES,
|
|
112
|
+
STATUSES: STATUSES,
|
|
113
|
+
clone: clone,
|
|
114
|
+
normalizeContext: normalizeContext,
|
|
115
|
+
normalizeSessions: normalizeSessions,
|
|
116
|
+
normalizeVerifiedCommit: normalizeVerifiedCommit,
|
|
117
|
+
snapshot: snapshot,
|
|
118
|
+
text: text,
|
|
119
|
+
};
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// Authorization and capability bindings for the Phase 1 Issues backend.
|
|
2
|
+
|
|
3
|
+
var issuesStore = require("./issues-store");
|
|
4
|
+
var schema = require("./issues-schema");
|
|
5
|
+
var provenance = require("./session-provenance");
|
|
6
|
+
var commitVerification = require("./issues-commit-verification");
|
|
7
|
+
var accessScope = require("./project-logs-access-scope").attachProjectLogsAccessScope;
|
|
8
|
+
|
|
9
|
+
var PUBLIC_FIELDS = { title: true, summary: true, body: true, resolutionSummary: true,
|
|
10
|
+
type: true, priority: true, status: true, closeReason: true };
|
|
11
|
+
|
|
12
|
+
function clone(value) { return value == null ? value : JSON.parse(JSON.stringify(value)); }
|
|
13
|
+
|
|
14
|
+
function attachIssuesService(ctx) {
|
|
15
|
+
var context = ctx || {};
|
|
16
|
+
var projects = context.getProjects;
|
|
17
|
+
var multiUser = context.isMultiUser || function () { return false; };
|
|
18
|
+
var findUserById = context.findUserById || function () { return null; };
|
|
19
|
+
var stores = new Map();
|
|
20
|
+
var verify = commitVerification.injectedVerifier(context.verifyCommit);
|
|
21
|
+
var scope;
|
|
22
|
+
|
|
23
|
+
function projectFor(slug) {
|
|
24
|
+
var list = typeof projects === "function" ? projects() : null;
|
|
25
|
+
return typeof slug === "string" && list && typeof list.get === "function" ? list.get(slug) || null : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function statusFor(project) {
|
|
29
|
+
var status;
|
|
30
|
+
if (!project || typeof project.getStatus !== "function") return null;
|
|
31
|
+
try { status = project.getStatus(); } catch (e) { return null; }
|
|
32
|
+
if (!status || typeof status !== "object" || Array.isArray(status)) return null;
|
|
33
|
+
if (typeof status.slug !== "string" || !status.slug || typeof status.path !== "string" || !status.path) return null;
|
|
34
|
+
if (typeof status.projectKnowledgeId !== "string" || !status.projectKnowledgeId) return null;
|
|
35
|
+
if (status.projectOwnerId != null && typeof status.projectOwnerId !== "string") return null;
|
|
36
|
+
if (status.isWorktree === true && (typeof status.changeSetId !== "string" || !status.changeSetId || typeof status.parentSlug !== "string" || !status.parentSlug)) return null;
|
|
37
|
+
return status;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function governingStatus(status) {
|
|
41
|
+
if (!status || status.isWorktree !== true) return status;
|
|
42
|
+
return statusFor(projectFor(status.parentSlug));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ownerKnown(status) {
|
|
46
|
+
return !multiUser() || !!status.projectOwnerId && !!findUserById(status.projectOwnerId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function openStore(status) {
|
|
50
|
+
var governing = governingStatus(status);
|
|
51
|
+
if (!governing || !governing.path || !governing.projectKnowledgeId) throw new Error("Issues are unavailable for this project.");
|
|
52
|
+
var key = governing.projectKnowledgeId;
|
|
53
|
+
if (!stores.has(key)) stores.set(key, issuesStore.createIssuesStore({ projectKnowledgeId: key, baseDir: context.baseDir }));
|
|
54
|
+
return stores.get(key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function issueContext(status) {
|
|
58
|
+
if (!status || status.isWorktree !== true || !status.changeSetId) return null;
|
|
59
|
+
return { kind: "worktree", changeSetId: status.changeSetId, branch: status.branch || null,
|
|
60
|
+
baseCommit: status.baseCommit || null, headCommit: status.headCommit || null };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function author(user, type, session) {
|
|
64
|
+
return { type: type, userId: user ? user.id || null : null,
|
|
65
|
+
displayName: user && (user.displayName || user.username) || null,
|
|
66
|
+
sessionKey: session ? session.cliSessionId || "local:" + session.localId : null,
|
|
67
|
+
sessionOriginId: session ? session.sessionOriginId || null : null,
|
|
68
|
+
vendor: session ? session.vendor || null : null, at: Date.now() };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cleanPublic(input, controls, allowCloseReason) {
|
|
72
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Issue arguments are required.");
|
|
73
|
+
var keys = Object.keys(input);
|
|
74
|
+
var result = {};
|
|
75
|
+
for (var i = 0; i < keys.length; i++) {
|
|
76
|
+
var key = keys[i];
|
|
77
|
+
if (Object.prototype.hasOwnProperty.call(controls, key)) continue;
|
|
78
|
+
if (!Object.prototype.hasOwnProperty.call(PUBLIC_FIELDS, key) || (key === "closeReason" && !allowCloseReason)) throw new Error("Unknown or server-owned issue field: " + key + ".");
|
|
79
|
+
result[key] = input[key];
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function osUsersEnabled() {
|
|
85
|
+
return typeof context.osUsersEnabled === "function" ? context.osUsersEnabled() === true : context.osUsersEnabled === true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function osIdentity(state) {
|
|
89
|
+
if (!osUsersEnabled()) return null;
|
|
90
|
+
var identity = typeof context.getOsUserInfoForActor === "function" ? context.getOsUserInfoForActor(state.user) : null;
|
|
91
|
+
if (!identity || typeof identity !== "object") throw new Error("An OS identity is required for commit verification.");
|
|
92
|
+
return clone(identity);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
scope = accessScope({ projectFor: projectFor, isMultiUser: multiUser,
|
|
96
|
+
canAccessProject: context.canAccessProject, hasFullProjectAccess: context.hasFullProjectAccess,
|
|
97
|
+
openStore: function () { return null; } });
|
|
98
|
+
|
|
99
|
+
function bindingState(project, session, userId, originId) {
|
|
100
|
+
var status = statusFor(project);
|
|
101
|
+
if (!status || status.isMate === true) return null;
|
|
102
|
+
var governing = governingStatus(status);
|
|
103
|
+
if (!governing || !ownerKnown(governing)) return null;
|
|
104
|
+
var user = null;
|
|
105
|
+
if (multiUser()) {
|
|
106
|
+
if (!userId || !(user = findUserById(userId))) return null;
|
|
107
|
+
} else {
|
|
108
|
+
user = userId ? findUserById(userId) : null;
|
|
109
|
+
if (!user) user = { id: userId || status.projectOwnerId || null };
|
|
110
|
+
}
|
|
111
|
+
if (session) {
|
|
112
|
+
var manager = project.getSessionManager ? project.getSessionManager() : project.sm;
|
|
113
|
+
if (!manager || !manager.sessions || manager.sessions.get(session.localId) !== session) return null;
|
|
114
|
+
if ((session.ownerId || null) !== (userId || null) || (session.sessionOriginId || null) !== originId) return null;
|
|
115
|
+
if (multiUser() && !session.ownerId) return null;
|
|
116
|
+
if (provenance.isWorker(session) || session.hidden === true || session.delegated === true || session._pairDelegation || session._delegatedBy) return null;
|
|
117
|
+
}
|
|
118
|
+
var principal = multiUser() ? { userId: user.id, singleUserOwnerId: null }
|
|
119
|
+
: { userId: null, singleUserOwnerId: user.id || status.projectOwnerId || null };
|
|
120
|
+
var authorization;
|
|
121
|
+
try { authorization = scope.projectAuthorization(principal, status); } catch (e) { return null; }
|
|
122
|
+
if (!authorization.allowed || (authorization.worktreeOnly && !status.changeSetId)) return null;
|
|
123
|
+
principal.isProjectOwner = scope.ownsProject(principal, governing);
|
|
124
|
+
return { project: project, status: status, session: session, user: user,
|
|
125
|
+
principal: principal, worktreeOnly: authorization.worktreeOnly };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function bind(source, isSession) {
|
|
129
|
+
if (!source || typeof source.projectSlug !== "string") return null;
|
|
130
|
+
var initialSlug = source.projectSlug;
|
|
131
|
+
var initialProject = projectFor(initialSlug);
|
|
132
|
+
var initialSession = isSession ? source.session : null;
|
|
133
|
+
if (!initialProject || (isSession && !initialSession)) return null;
|
|
134
|
+
if (initialSession) provenance.ensureOrigin(initialSession);
|
|
135
|
+
var initialUserId = initialSession ? initialSession.ownerId || null : source.userId || null;
|
|
136
|
+
var initialOrigin = initialSession ? initialSession.sessionOriginId || null : null;
|
|
137
|
+
var initial = bindingState(initialProject, initialSession, initialUserId, initialOrigin);
|
|
138
|
+
if (!initial) return null;
|
|
139
|
+
|
|
140
|
+
function current() {
|
|
141
|
+
if (projectFor(initialSlug) !== initialProject) throw new Error("This Issues binding is no longer valid.");
|
|
142
|
+
var live = bindingState(initialProject, initialSession, initialUserId, initialOrigin);
|
|
143
|
+
if (!live) throw new Error("This Issues binding is no longer valid.");
|
|
144
|
+
return live;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function visibleIssue(store, ref, state) {
|
|
148
|
+
var result = store.read(ref);
|
|
149
|
+
if (state.worktreeOnly && (!result.context || result.context.changeSetId !== state.status.changeSetId)) throw new Error("Issue not found.");
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
function scopedIssue(store, ref, state) {
|
|
153
|
+
var result = store.read(ref, true);
|
|
154
|
+
if (state.worktreeOnly && (!result.context || result.context.changeSetId !== state.status.changeSetId)) throw new Error("Issue not found.");
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function scopedOptions(state, args) {
|
|
159
|
+
var result = Object.assign({}, args || {});
|
|
160
|
+
if (state.worktreeOnly) {
|
|
161
|
+
if (!state.status.changeSetId) throw new Error("Issues are unavailable without a change set.");
|
|
162
|
+
result.changeSetId = state.status.changeSetId;
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function operation(state, needsIdentity) {
|
|
168
|
+
var governing = governingStatus(state.status);
|
|
169
|
+
return { project: state.project, session: state.session, userId: state.user.id || null,
|
|
170
|
+
path: state.status.path, knowledgeId: governing.projectKnowledgeId,
|
|
171
|
+
changeSetId: state.status.changeSetId || null, worktreeOnly: state.worktreeOnly,
|
|
172
|
+
recordContext: clone(issueContext(state.status)),
|
|
173
|
+
actor: clone(author(state.user, state.session ? "session" : "user", state.session)),
|
|
174
|
+
osIdentity: needsIdentity ? osIdentity(state) : null };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function recheck(before, needsIdentity) {
|
|
178
|
+
var fresh = current();
|
|
179
|
+
var governing = governingStatus(fresh.status);
|
|
180
|
+
var liveIdentity = needsIdentity ? osIdentity(fresh) : null;
|
|
181
|
+
if (fresh.project !== before.project || fresh.session !== before.session || (fresh.user.id || null) !== before.userId ||
|
|
182
|
+
fresh.status.path !== before.path || !governing || governing.projectKnowledgeId !== before.knowledgeId ||
|
|
183
|
+
(fresh.status.changeSetId || null) !== before.changeSetId || fresh.worktreeOnly !== before.worktreeOnly ||
|
|
184
|
+
JSON.stringify(liveIdentity) !== JSON.stringify(before.osIdentity)) throw new Error("Issue authorization changed during the operation.");
|
|
185
|
+
return fresh;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function evidence(before, sha) {
|
|
189
|
+
return verify(before.path, sha, before.knowledgeId, before.osIdentity).then(function (proof) {
|
|
190
|
+
var normalized = schema.normalizeVerifiedCommit(proof);
|
|
191
|
+
if (normalized.repository !== before.knowledgeId) throw new Error("Verified commit evidence belongs to a different repository.");
|
|
192
|
+
return normalized;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
linkWorkSession: function (args, session) {
|
|
198
|
+
var state = current();
|
|
199
|
+
var manager = state.project.getSessionManager();
|
|
200
|
+
if (!args || !Number.isInteger(args.expectedRevision)) throw new Error("expectedRevision is required.");
|
|
201
|
+
if (!session || manager.sessions.get(session.localId) !== session || provenance.isWorker(session) || session.hidden === true ||
|
|
202
|
+
session.delegated === true || session._pairDelegation || session._delegatedBy ||
|
|
203
|
+
(session.ownerId || null) !== (state.user.id || null)) throw new Error("Invalid work session.");
|
|
204
|
+
provenance.ensureOrigin(session);
|
|
205
|
+
var target = openStore(state.status);
|
|
206
|
+
var entry = visibleIssue(target, args.ref, state);
|
|
207
|
+
if (entry.revision !== args.expectedRevision) throw new Error("Issue revision conflict.");
|
|
208
|
+
if (entry.status === "resolved" || entry.status === "closed") throw new Error("Reopen this issue before starting work.");
|
|
209
|
+
var links = entry.linkedWorkSessions || [];
|
|
210
|
+
if (links.some(function (link) { return link.sessionId === session.sessionOriginId; })) return entry;
|
|
211
|
+
var link = { sessionId: session.sessionOriginId, role: "driver", issueRevision: args.expectedRevision };
|
|
212
|
+
if (args.requestId !== undefined) link.requestId = schema.text(args.requestId, "work request id", 200, true);
|
|
213
|
+
return target.update(args.ref, { linkedWorkSessions: links.concat([link]), status: "in_progress" }, args.expectedRevision, author(state.user, "user", null));
|
|
214
|
+
},
|
|
215
|
+
resolveWorkSession: function (args) {
|
|
216
|
+
var state = current();
|
|
217
|
+
var entry = visibleIssue(openStore(state.status), args.ref, state);
|
|
218
|
+
if (!(entry.linkedWorkSessions || []).some(function (link) { return link.sessionId === args.sessionOriginId; })) throw new Error("Work session unavailable.");
|
|
219
|
+
var found = null;
|
|
220
|
+
state.project.getSessionManager().sessions.forEach(function (session) {
|
|
221
|
+
if (session.sessionOriginId === args.sessionOriginId && (session.ownerId || null) === (state.user.id || null) &&
|
|
222
|
+
!provenance.isWorker(session) && session.hidden !== true && session.delegated !== true && !session._pairDelegation && !session._delegatedBy) found = session;
|
|
223
|
+
});
|
|
224
|
+
if (!found) throw new Error("Work session unavailable.");
|
|
225
|
+
return found;
|
|
226
|
+
},
|
|
227
|
+
scopeId: "project/" + governingStatus(initial.status).projectKnowledgeId + "/issues",
|
|
228
|
+
canComment: true,
|
|
229
|
+
canDelete: initial.principal.isProjectOwner === true,
|
|
230
|
+
listIssues: function (args) { var state = current(); return openStore(state.status).list(scopedOptions(state, args)); },
|
|
231
|
+
searchIssues: function (args) { var state = current(); return openStore(state.status).search(scopedOptions(state, args)); },
|
|
232
|
+
readIssue: function (args) { var state = current(); return visibleIssue(openStore(state.status), args && args.ref, state); },
|
|
233
|
+
readIssueTombstone: function (args) { var state = current(); return scopedIssue(openStore(state.status), args && args.ref, state); },
|
|
234
|
+
issueHistory: function (args) { var state = current(); var store = openStore(state.status); var history = store.history(args && args.ref); if (state.worktreeOnly) { var historical = scopedIssue(store, args && args.ref, state); if (!historical.context || historical.context.changeSetId !== state.status.changeSetId) throw new Error("Issue not found."); } return history; },
|
|
235
|
+
readIssueRevision: function (args) { var state = current(); var store = openStore(state.status); visibleIssue(store, args && args.ref, state); return store.readRevision(args.ref, args.revision); },
|
|
236
|
+
commentIssue: function (args) { var state = current(); var target = openStore(state.status); visibleIssue(target, args && args.ref, state); return target.comment(args.ref, args.body, author(state.user, "user", null)); },
|
|
237
|
+
listIssueFeedback: function (args) {
|
|
238
|
+
if (!isSession) throw new Error("Only the Project Driver can review issue comments.");
|
|
239
|
+
var state = current(); var target = openStore(state.status); var out = []; var feedbackLimit = args && args.limit === undefined ? 200 : Number(args.limit);
|
|
240
|
+
if (!Number.isInteger(feedbackLimit) || feedbackLimit < 1 || feedbackLimit > 200) throw new Error("Feedback limit must be between 1 and 200.");
|
|
241
|
+
var position = { issueCursor: null, issueIndex: 0, commentOffset: 0 };
|
|
242
|
+
if (args && args.cursor) {
|
|
243
|
+
try {
|
|
244
|
+
if (/^\d+$/.test(String(args.cursor))) position.issueCursor = String(args.cursor);
|
|
245
|
+
else position = JSON.parse(Buffer.from(String(args.cursor), "base64url").toString("utf8"));
|
|
246
|
+
} catch (e) { throw new Error("Invalid issue feedback cursor."); }
|
|
247
|
+
if (!position || (position.issueCursor !== null && typeof position.issueCursor !== "string") || !Number.isInteger(position.issueIndex) || position.issueIndex < 0 || !Number.isInteger(position.commentOffset) || position.commentOffset < 0) throw new Error("Invalid issue feedback cursor.");
|
|
248
|
+
}
|
|
249
|
+
var nextCursor = null; var scanned = 0;
|
|
250
|
+
while (out.length < feedbackLimit && scanned < 500) {
|
|
251
|
+
var result = target.list(scopedOptions(state, { limit: 50, cursor: position.issueCursor }));
|
|
252
|
+
if (!result.issues.length) break;
|
|
253
|
+
scanned += result.issues.length;
|
|
254
|
+
var firstIssueIndex = position.issueIndex;
|
|
255
|
+
if (firstIssueIndex >= result.issues.length) throw new Error("Invalid issue feedback cursor.");
|
|
256
|
+
for (var i = firstIssueIndex; i < result.issues.length && out.length < feedbackLimit; i++) {
|
|
257
|
+
var entry = visibleIssue(target, result.issues[i].ref, state);
|
|
258
|
+
var waiting = (entry.comments || []).filter(function (item) { return item.status === "pending"; });
|
|
259
|
+
var start = i === position.issueIndex ? position.commentOffset : 0;
|
|
260
|
+
var j = start;
|
|
261
|
+
for (; j < waiting.length && out.length < feedbackLimit; j++) out.push({ ref: entry.ref, title: entry.title, commentId: waiting[j].id, body: waiting[j].body, author: waiting[j].author, at: waiting[j].at });
|
|
262
|
+
if (out.length >= feedbackLimit) {
|
|
263
|
+
if (j < waiting.length) nextCursor = Buffer.from(JSON.stringify({ issueCursor: position.issueCursor, issueIndex: i, commentOffset: j })).toString("base64url");
|
|
264
|
+
else if (i + 1 < result.issues.length) nextCursor = Buffer.from(JSON.stringify({ issueCursor: position.issueCursor, issueIndex: i + 1, commentOffset: 0 })).toString("base64url");
|
|
265
|
+
else if (result.nextCursor) nextCursor = Buffer.from(JSON.stringify({ issueCursor: result.nextCursor, issueIndex: 0, commentOffset: 0 })).toString("base64url");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (nextCursor || !result.nextCursor) break;
|
|
269
|
+
if (scanned >= 500) {
|
|
270
|
+
nextCursor = Buffer.from(JSON.stringify({ issueCursor: result.nextCursor, issueIndex: 0, commentOffset: 0 })).toString("base64url");
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
position.issueCursor = result.nextCursor; position.issueIndex = 0; position.commentOffset = 0;
|
|
274
|
+
}
|
|
275
|
+
return { feedback: out, total: out.length, nextCursor: nextCursor, truncated: !!nextCursor };
|
|
276
|
+
},
|
|
277
|
+
reviewIssueComment: function (args) {
|
|
278
|
+
if (!isSession) throw new Error("Only the Project Driver can review issue comments.");
|
|
279
|
+
var state = current(); var target = openStore(state.status); var entry = visibleIssue(target, args && args.ref, state);
|
|
280
|
+
if (args.action === "incorporate") {
|
|
281
|
+
if (!Number.isInteger(args.expectedRevision)) throw new Error("expectedRevision is required to incorporate a comment.");
|
|
282
|
+
var data = cleanPublic(args, { ref: true, expectedRevision: true, commentId: true, action: true, response: true, commitSha: true }, true);
|
|
283
|
+
if (data.status === "resolved") throw new Error("Use update_issue with verified commit evidence before resolving this Issue.");
|
|
284
|
+
return target.incorporate(entry.ref, args.commentId, data, args.expectedRevision, args.response, author(state.user, "session", state.session));
|
|
285
|
+
}
|
|
286
|
+
return target.review(entry.ref, args.commentId, args.action, args.response, author(state.user, "session", state.session));
|
|
287
|
+
},
|
|
288
|
+
removeIssue: function (args) {
|
|
289
|
+
var state = current();
|
|
290
|
+
if (!state.principal.isProjectOwner) throw new Error("Only the project owner can delete Issues.");
|
|
291
|
+
var target = openStore(state.status); var entry = visibleIssue(target, args && args.ref, state);
|
|
292
|
+
if (!Number.isInteger(args && args.expectedRevision) || entry.revision !== args.expectedRevision) throw new Error("Issue revision conflict.");
|
|
293
|
+
return target.remove(entry.ref, author(state.user, "user", null), args.expectedRevision);
|
|
294
|
+
},
|
|
295
|
+
createIssue: function (args) {
|
|
296
|
+
if (!isSession) throw new Error("People cannot create issues directly. Ask the Project Driver to create one.");
|
|
297
|
+
var data = cleanPublic(args, { commitSha: true }, true);
|
|
298
|
+
var status = data.status === undefined ? "open" : data.status;
|
|
299
|
+
if (schema.STATUSES.indexOf(status) === -1) throw new Error("Invalid status.");
|
|
300
|
+
var state = current();
|
|
301
|
+
if (status !== "resolved") return Promise.resolve(openStore(state.status).create(data,
|
|
302
|
+
author(state.user, state.session ? "session" : "user", state.session), issueContext(state.status)));
|
|
303
|
+
if (typeof data.resolutionSummary !== "string" || !data.resolutionSummary.trim()) throw new Error("resolutionSummary is required when resolving an issue.");
|
|
304
|
+
var before = operation(state, true);
|
|
305
|
+
return evidence(before, args.commitSha).then(function (proof) {
|
|
306
|
+
var after = recheck(before, true);
|
|
307
|
+
return openStore(after.status).create(data, before.actor, before.recordContext, proof);
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
updateIssue: function (args) {
|
|
311
|
+
var data = cleanPublic(args, { ref: true, expectedRevision: true, commitSha: true }, true);
|
|
312
|
+
if (!Number.isInteger(args.expectedRevision)) throw new Error("expectedRevision is required.");
|
|
313
|
+
if (data.status !== undefined && schema.STATUSES.indexOf(data.status) === -1) throw new Error("Invalid status.");
|
|
314
|
+
var ref = args.ref;
|
|
315
|
+
var expectedRevision = args.expectedRevision;
|
|
316
|
+
var state = current();
|
|
317
|
+
var target = openStore(state.status);
|
|
318
|
+
var existing = visibleIssue(target, ref, state);
|
|
319
|
+
if (existing.revision !== expectedRevision) throw new Error("Issue revision conflict.");
|
|
320
|
+
if (data.status !== "resolved") return Promise.resolve(target.update(ref, data, expectedRevision,
|
|
321
|
+
author(state.user, state.session ? "session" : "user", state.session)));
|
|
322
|
+
if (typeof data.resolutionSummary !== "string" || !data.resolutionSummary.trim()) throw new Error("resolutionSummary is required when resolving an issue.");
|
|
323
|
+
var before = operation(state, true);
|
|
324
|
+
return evidence(before, args.commitSha).then(function (proof) {
|
|
325
|
+
var after = recheck(before, true);
|
|
326
|
+
var afterStore = openStore(after.status);
|
|
327
|
+
visibleIssue(afterStore, ref, after);
|
|
328
|
+
return afterStore.update(ref, data, expectedRevision, before.actor, null, proof);
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { bindUser: function (source) { return bind(source, false); },
|
|
335
|
+
bindProjectSession: function (source) { return bind(source, true); },
|
|
336
|
+
issueScopeIdForKnowledgeId: issuesStore.issueScopeIdForKnowledgeId };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = { attachIssuesService: attachIssuesService };
|