@deftai/directive-core 0.84.0 → 0.86.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 (79) hide show
  1. package/dist/cache/fetch.d.ts +17 -0
  2. package/dist/cache/fetch.js +50 -0
  3. package/dist/doctor/checks.js +16 -1
  4. package/dist/doctor/constants.d.ts +4 -2
  5. package/dist/doctor/constants.js +10 -6
  6. package/dist/doctor/index.d.ts +1 -0
  7. package/dist/doctor/index.js +1 -0
  8. package/dist/doctor/main.d.ts +2 -1
  9. package/dist/doctor/main.js +83 -22
  10. package/dist/doctor/npm-registry.d.ts +23 -0
  11. package/dist/doctor/npm-registry.js +128 -0
  12. package/dist/doctor/npm-view.d.ts +10 -0
  13. package/dist/doctor/npm-view.js +31 -0
  14. package/dist/doctor/payload-staleness.js +2 -9
  15. package/dist/doctor/signpost-checks.d.ts +2 -1
  16. package/dist/doctor/signpost-checks.js +2 -0
  17. package/dist/doctor/types.d.ts +12 -0
  18. package/dist/eval-health-relocation/evaluate.js +15 -1
  19. package/dist/fs/projection-containment.js +1 -1
  20. package/dist/hooks/cursor-hooks.d.ts +6 -20
  21. package/dist/hooks/cursor-hooks.js +7 -92
  22. package/dist/init-deposit/agent-hooks.d.ts +2 -2
  23. package/dist/init-deposit/agent-hooks.js +24 -38
  24. package/dist/init-deposit/gitignore.js +5 -0
  25. package/dist/init-deposit/hygiene.d.ts +22 -0
  26. package/dist/init-deposit/hygiene.js +136 -4
  27. package/dist/init-deposit/legacy-detect.d.ts +25 -1
  28. package/dist/init-deposit/legacy-detect.js +143 -11
  29. package/dist/init-deposit/refresh.d.ts +2 -0
  30. package/dist/init-deposit/refresh.js +18 -5
  31. package/dist/policy/index.d.ts +2 -0
  32. package/dist/policy/index.js +2 -0
  33. package/dist/policy/org-force-on-migration.d.ts +44 -0
  34. package/dist/policy/org-force-on-migration.js +242 -0
  35. package/dist/policy/product-signal.d.ts +3 -1
  36. package/dist/policy/product-signal.js +15 -4
  37. package/dist/policy/value-feedback.d.ts +18 -1
  38. package/dist/policy/value-feedback.js +78 -5
  39. package/dist/preflight-cache/evaluate.js +12 -1
  40. package/dist/product-signal/consent-prompt.d.ts +13 -0
  41. package/dist/product-signal/consent-prompt.js +30 -0
  42. package/dist/product-signal/submit.js +22 -1
  43. package/dist/render/framework-commands.d.ts +1 -1
  44. package/dist/render/framework-commands.js +6 -6
  45. package/dist/render/index.d.ts +1 -0
  46. package/dist/render/index.js +1 -0
  47. package/dist/render/roadmap-render.d.ts +5 -1
  48. package/dist/render/roadmap-render.js +20 -2
  49. package/dist/render/rule-map-template.d.ts +2 -0
  50. package/dist/render/rule-map-template.js +407 -0
  51. package/dist/render/rule-map.d.ts +2 -0
  52. package/dist/render/rule-map.js +708 -0
  53. package/dist/review-monitor/constants.d.ts +7 -1
  54. package/dist/review-monitor/constants.js +40 -14
  55. package/dist/review-monitor/github-lease.d.ts +45 -0
  56. package/dist/review-monitor/github-lease.js +106 -0
  57. package/dist/review-monitor/index.d.ts +2 -0
  58. package/dist/review-monitor/index.js +2 -0
  59. package/dist/review-monitor/lease-comment.d.ts +36 -0
  60. package/dist/review-monitor/lease-comment.js +152 -0
  61. package/dist/review-monitor/record.d.ts +61 -13
  62. package/dist/review-monitor/record.js +379 -132
  63. package/dist/review-monitor/verify.d.ts +2 -0
  64. package/dist/review-monitor/verify.js +32 -13
  65. package/dist/scm/gh-rest.d.ts +7 -1
  66. package/dist/scm/gh-rest.js +35 -0
  67. package/dist/session/session-start.js +5 -0
  68. package/dist/task-surface/index.js +53 -13
  69. package/dist/verify-env/agent-hooks-live-probe.d.ts +32 -0
  70. package/dist/verify-env/agent-hooks-live-probe.js +216 -0
  71. package/dist/verify-env/index.d.ts +1 -0
  72. package/dist/verify-env/index.js +1 -0
  73. package/dist/xbrief-migrate/constants.d.ts +6 -0
  74. package/dist/xbrief-migrate/constants.js +6 -0
  75. package/dist/xbrief-migrate/index.d.ts +2 -2
  76. package/dist/xbrief-migrate/index.js +2 -2
  77. package/dist/xbrief-migrate/migrate-project.d.ts +7 -0
  78. package/dist/xbrief-migrate/migrate-project.js +44 -18
  79. package/package.json +3 -3
@@ -1,163 +1,384 @@
1
- import { execFileSync } from "node:child_process";
2
- import { closeSync, existsSync, fdatasyncSync, mkdirSync, openSync, readFileSync, renameSync, writeSync, } from "node:fs";
3
- import { dirname, isAbsolute, join, resolve } from "node:path";
4
- import { DEFAULT_STALE_MINUTES, REVIEW_MONITOR_FILENAME, SCHEMA_VERSION } from "./constants.js";
5
- function resolveMainWorktreeRoot(startDir) {
6
- let root = resolve(startDir);
7
- try {
8
- const out = execFileSync("git", ["rev-parse", "--git-common-dir"], {
9
- cwd: startDir,
10
- encoding: "utf8",
11
- stdio: ["ignore", "pipe", "ignore"],
12
- }).trim();
13
- if (out.length > 0) {
14
- const commonDir = isAbsolute(out) ? out : resolve(startDir, out);
15
- root = dirname(commonDir);
16
- }
17
- }
18
- catch {
19
- // Not a git work tree -- fall back to startDir.
20
- }
21
- return root;
1
+ import { resolve } from "node:path";
2
+ import { resolveRepo } from "../triage/queue/repo.js";
3
+ import { DEFAULT_STALE_MINUTES, EXIT_CONFIG_ERROR, EXIT_CONFLICT, EXIT_READY, SCHEMA_VERSION, } from "./constants.js";
4
+ import { createReviewOwnerComment, deleteReviewOwnerComment, listReviewOwnerComments, resolveGitHubLogin, updateReviewOwnerComment, } from "./github-lease.js";
5
+ import { computeExpiresAt, findActiveLeaseComment, isLeaseExpired, parseIso8601Utc, parseReviewOwnerLease, renderReleasedReviewOwnerComment, renderReviewOwnerComment, selectWinningReviewOwnerComment, } from "./lease-comment.js";
6
+ export { parseIso8601Utc };
7
+ function leaseToRecord(pr, repo, lease, commentId, worktreePath, parentSessionId) {
8
+ return {
9
+ pr,
10
+ repo,
11
+ head_sha: lease.head_sha,
12
+ platform_primitive: lease.platform_primitive,
13
+ monitor_agent_id: lease.monitor_agent_id,
14
+ owner: lease.owner,
15
+ started_at: lease.started_at,
16
+ expires_at: lease.expires_at,
17
+ worktree_path: worktreePath,
18
+ parent_session_id: parentSessionId,
19
+ ended_at: lease.ended_at,
20
+ comment_id: commentId,
21
+ };
22
22
  }
23
- export function reviewMonitorPath(projectRoot) {
24
- const root = resolveMainWorktreeRoot(projectRoot);
25
- return join(root, ".deft", REVIEW_MONITOR_FILENAME);
23
+ function buildLease(input) {
24
+ const startedAtIso = input.startedAt.toISOString();
25
+ return {
26
+ owner: input.owner,
27
+ monitor_agent_id: input.monitorAgentId.trim(),
28
+ head_sha: input.headSha,
29
+ started_at: startedAtIso,
30
+ expires_at: computeExpiresAt(input.startedAt, input.staleMinutes),
31
+ platform_primitive: input.platformPrimitive,
32
+ ended_at: null,
33
+ };
34
+ }
35
+ function resolveRegisterRepo(input) {
36
+ return resolveRepo(input.repo ?? null, resolve(input.projectRoot));
26
37
  }
27
- function atomicWriteJson(path, payload) {
28
- mkdirSync(dirname(path), { recursive: true });
29
- const tmp = `${path}.tmp-${process.pid}`;
30
- const text = `${JSON.stringify(payload, null, 2)}\n`;
31
- const fd = openSync(tmp, "w");
32
- try {
33
- writeSync(fd, text, undefined, "utf8");
34
- fdatasyncSync(fd);
38
+ function sameClaimHolder(lease, owner, monitorAgentId) {
39
+ return lease.monitor_agent_id === monitorAgentId.trim() && lease.owner === owner;
40
+ }
41
+ function conflictMessage(holder, pr) {
42
+ return (`review_monitor_register: PR #${pr} review-owner lease held by ${holder.owner} ` +
43
+ `(monitor_agent_id=${holder.monitor_agent_id}, expires_at=${holder.expires_at}).`);
44
+ }
45
+ function resolveCreateRace(repo, pr, createdId, seams) {
46
+ const listed = listReviewOwnerComments(repo, pr, seams);
47
+ if (!Array.isArray(listed)) {
48
+ return {
49
+ exitCode: EXIT_CONFIG_ERROR,
50
+ message: `review_monitor_register: ${listed.error}`,
51
+ record: null,
52
+ commentId: null,
53
+ priorOwner: null,
54
+ };
35
55
  }
36
- finally {
37
- closeSync(fd);
56
+ const winner = selectWinningReviewOwnerComment(listed);
57
+ if (winner === null || winner.id === createdId) {
58
+ return null;
38
59
  }
39
- renameSync(tmp, path);
40
- }
41
- export function emptyReviewMonitorFile() {
42
- return { schema_version: SCHEMA_VERSION, records: [] };
60
+ if (winner.id !== createdId) {
61
+ const deleteResult = deleteReviewOwnerComment(repo, createdId, seams);
62
+ if ("error" in deleteResult) {
63
+ return {
64
+ exitCode: EXIT_CONFIG_ERROR,
65
+ message: `review_monitor_register: create-race loser could not delete duplicate: ${deleteResult.error}`,
66
+ record: null,
67
+ commentId: null,
68
+ priorOwner: winner?.lease?.owner ?? null,
69
+ };
70
+ }
71
+ const holder = winner?.lease ?? null;
72
+ return {
73
+ exitCode: EXIT_CONFLICT,
74
+ message: holder !== null
75
+ ? conflictMessage(holder, pr)
76
+ : `review_monitor_register: PR #${pr} create-race lost to an older claim comment.`,
77
+ record: null,
78
+ commentId: winner?.id ?? null,
79
+ priorOwner: holder?.owner ?? null,
80
+ };
81
+ }
82
+ return null;
43
83
  }
44
- export function readReviewMonitorFile(path) {
45
- if (!existsSync(path)) {
46
- return { data: emptyReviewMonitorFile(), error: null };
84
+ function verifyExclusiveActiveClaim(repo, pr, commentId, owner, monitorAgentId, seams, options) {
85
+ const relisted = listReviewOwnerComments(repo, pr, seams);
86
+ if (!Array.isArray(relisted)) {
87
+ return {
88
+ exitCode: EXIT_CONFIG_ERROR,
89
+ message: `review_monitor_register: ${relisted.error}`,
90
+ record: null,
91
+ commentId: null,
92
+ priorOwner: null,
93
+ };
47
94
  }
48
- let parsed;
49
- try {
50
- parsed = JSON.parse(readFileSync(path, "utf8"));
95
+ const active = findActiveLeaseComment(relisted, options);
96
+ if (active === null || active.id !== commentId) {
97
+ const holder = active?.lease ?? null;
98
+ return {
99
+ exitCode: EXIT_CONFLICT,
100
+ message: holder !== null
101
+ ? conflictMessage(holder, pr)
102
+ : `review_monitor_register: PR #${pr} post-write verify failed; active lease not on comment ${commentId}.`,
103
+ record: null,
104
+ commentId: active?.id ?? commentId,
105
+ priorOwner: holder?.owner ?? null,
106
+ };
51
107
  }
52
- catch (exc) {
53
- return { data: null, error: `${path}: invalid JSON (${String(exc)}).` };
108
+ if (active.lease === null || !sameClaimHolder(active.lease, owner, monitorAgentId)) {
109
+ const holder = active.lease ?? {
110
+ owner: "unknown",
111
+ monitor_agent_id: "unknown",
112
+ expires_at: "",
113
+ head_sha: null,
114
+ started_at: "",
115
+ platform_primitive: "cursor-task",
116
+ ended_at: null,
117
+ };
118
+ return {
119
+ exitCode: EXIT_CONFLICT,
120
+ message: conflictMessage(holder, pr),
121
+ record: null,
122
+ commentId: active.id,
123
+ priorOwner: holder.owner !== owner ? holder.owner : null,
124
+ };
54
125
  }
55
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
56
- return { data: null, error: `${path}: review-monitor file must be a JSON object.` };
126
+ return null;
127
+ }
128
+ export function registerReviewMonitor(input) {
129
+ const repo = resolveRegisterRepo(input);
130
+ if (repo === null) {
131
+ return {
132
+ exitCode: EXIT_CONFIG_ERROR,
133
+ message: "review_monitor_register: could not resolve owner/repo — pass --repo OWNER/REPO or run inside a git repo with origin",
134
+ record: null,
135
+ commentId: null,
136
+ priorOwner: null,
137
+ };
57
138
  }
58
- const obj = parsed;
59
- const recordsRaw = obj.records;
60
- const records = [];
61
- if (Array.isArray(recordsRaw)) {
62
- for (const entry of recordsRaw) {
63
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
64
- continue;
65
- }
66
- const e = entry;
67
- const pr = e.pr;
68
- const monitorAgentId = e.monitor_agent_id;
69
- const platformPrimitive = e.platform_primitive;
70
- const startedAt = e.started_at;
71
- const worktreePath = e.worktree_path;
72
- if (typeof pr !== "number" ||
73
- !Number.isInteger(pr) ||
74
- pr <= 0 ||
75
- typeof monitorAgentId !== "string" ||
76
- monitorAgentId.trim().length === 0 ||
77
- typeof platformPrimitive !== "string" ||
78
- typeof startedAt !== "string" ||
79
- typeof worktreePath !== "string") {
80
- continue;
81
- }
82
- records.push({
83
- pr,
84
- repo: typeof e.repo === "string" ? e.repo : null,
85
- head_sha: typeof e.head_sha === "string" ? e.head_sha : null,
86
- platform_primitive: platformPrimitive,
87
- monitor_agent_id: monitorAgentId.trim(),
88
- started_at: startedAt,
89
- worktree_path: worktreePath,
90
- parent_session_id: typeof e.parent_session_id === "string" ? e.parent_session_id : null,
91
- ended_at: typeof e.ended_at === "string" ? e.ended_at : null,
139
+ const seams = input.seams ?? {};
140
+ const owner = input.owner ?? resolveGitHubLogin(seams);
141
+ if (owner === null || owner.length === 0) {
142
+ return {
143
+ exitCode: EXIT_CONFIG_ERROR,
144
+ message: "review_monitor_register: could not resolve GitHub login pass --owner or authenticate gh",
145
+ record: null,
146
+ commentId: null,
147
+ priorOwner: null,
148
+ };
149
+ }
150
+ const startedAt = input.startedAt ?? new Date();
151
+ const staleMinutes = input.staleMinutes ?? DEFAULT_STALE_MINUTES;
152
+ const monitorAgentId = input.monitorAgentId.trim();
153
+ const worktreePath = resolve(input.projectRoot);
154
+ const listed = listReviewOwnerComments(repo, input.pr, seams);
155
+ if (!Array.isArray(listed)) {
156
+ return {
157
+ exitCode: EXIT_CONFIG_ERROR,
158
+ message: `review_monitor_register: ${listed.error}`,
159
+ record: null,
160
+ commentId: null,
161
+ priorOwner: null,
162
+ };
163
+ }
164
+ const anchor = selectWinningReviewOwnerComment(listed);
165
+ const active = findActiveLeaseComment(listed, { now: startedAt, headSha: input.headSha ?? null });
166
+ if (active?.lease !== null && active?.lease !== undefined) {
167
+ const holder = active.lease;
168
+ if (sameClaimHolder(holder, owner, monitorAgentId)) {
169
+ const renewed = buildLease({
170
+ owner,
171
+ monitorAgentId,
172
+ headSha: input.headSha ?? holder.head_sha,
173
+ platformPrimitive: input.platformPrimitive,
174
+ startedAt,
175
+ staleMinutes,
92
176
  });
177
+ const body = renderReviewOwnerComment(renewed);
178
+ const updated = updateReviewOwnerComment(repo, active.id, body, seams);
179
+ if ("error" in updated) {
180
+ return {
181
+ exitCode: EXIT_CONFIG_ERROR,
182
+ message: `review_monitor_register: ${updated.error}`,
183
+ record: null,
184
+ commentId: active.id,
185
+ priorOwner: null,
186
+ };
187
+ }
188
+ return {
189
+ exitCode: EXIT_READY,
190
+ message: `review_monitor_register: renewed PR #${input.pr} review-owner lease for ${monitorAgentId} (comment ${active.id}).`,
191
+ record: leaseToRecord(input.pr, repo, renewed, active.id, worktreePath, input.parentSessionId ?? null),
192
+ commentId: active.id,
193
+ priorOwner: null,
194
+ };
195
+ }
196
+ if (!isLeaseExpired(holder, startedAt) && input.force !== true) {
197
+ return {
198
+ exitCode: EXIT_CONFLICT,
199
+ message: conflictMessage(holder, input.pr),
200
+ record: leaseToRecord(input.pr, repo, holder, active.id, null, null),
201
+ commentId: active.id,
202
+ priorOwner: holder.owner,
203
+ };
93
204
  }
205
+ const priorOwner = holder.owner;
206
+ const takeover = buildLease({
207
+ owner,
208
+ monitorAgentId,
209
+ headSha: input.headSha ?? null,
210
+ platformPrimitive: input.platformPrimitive,
211
+ startedAt,
212
+ staleMinutes,
213
+ });
214
+ const body = renderReviewOwnerComment(takeover);
215
+ const updated = updateReviewOwnerComment(repo, active.id, body, seams);
216
+ if ("error" in updated) {
217
+ return {
218
+ exitCode: EXIT_CONFIG_ERROR,
219
+ message: `review_monitor_register: ${updated.error}`,
220
+ record: null,
221
+ commentId: active.id,
222
+ priorOwner,
223
+ };
224
+ }
225
+ const forceNote = input.force === true && !isLeaseExpired(holder, startedAt)
226
+ ? ` (forced takeover from ${priorOwner})`
227
+ : isLeaseExpired(holder, startedAt)
228
+ ? " (expired lease takeover)"
229
+ : "";
230
+ return {
231
+ exitCode: EXIT_READY,
232
+ message: `review_monitor_register: claimed PR #${input.pr} review-owner lease for ${monitorAgentId}` +
233
+ `${forceNote} (comment ${active.id}).`,
234
+ record: leaseToRecord(input.pr, repo, takeover, active.id, worktreePath, input.parentSessionId ?? null),
235
+ commentId: active.id,
236
+ priorOwner: priorOwner !== owner ? priorOwner : null,
237
+ };
238
+ }
239
+ const lease = buildLease({
240
+ owner,
241
+ monitorAgentId,
242
+ headSha: input.headSha ?? null,
243
+ platformPrimitive: input.platformPrimitive,
244
+ startedAt,
245
+ staleMinutes,
246
+ });
247
+ const body = renderReviewOwnerComment(lease);
248
+ if (anchor !== null) {
249
+ const updated = updateReviewOwnerComment(repo, anchor.id, body, seams);
250
+ if ("error" in updated) {
251
+ return {
252
+ exitCode: EXIT_CONFIG_ERROR,
253
+ message: `review_monitor_register: ${updated.error}`,
254
+ record: null,
255
+ commentId: anchor.id,
256
+ priorOwner: null,
257
+ };
258
+ }
259
+ const verified = verifyExclusiveActiveClaim(repo, input.pr, anchor.id, owner, monitorAgentId, seams, { now: startedAt, headSha: input.headSha ?? null });
260
+ if (verified !== null) {
261
+ return verified;
262
+ }
263
+ return {
264
+ exitCode: EXIT_READY,
265
+ message: `review_monitor_register: claimed PR #${input.pr} review-owner lease for ${monitorAgentId} (comment ${anchor.id}).`,
266
+ record: leaseToRecord(input.pr, repo, lease, anchor.id, worktreePath, input.parentSessionId ?? null),
267
+ commentId: anchor.id,
268
+ priorOwner: null,
269
+ };
270
+ }
271
+ const created = createReviewOwnerComment(repo, input.pr, body, seams);
272
+ if ("error" in created) {
273
+ return {
274
+ exitCode: EXIT_CONFIG_ERROR,
275
+ message: `review_monitor_register: ${created.error}`,
276
+ record: null,
277
+ commentId: null,
278
+ priorOwner: null,
279
+ };
280
+ }
281
+ const race = resolveCreateRace(repo, input.pr, created.id, seams);
282
+ if (race !== null) {
283
+ return race;
94
284
  }
95
285
  return {
96
- data: {
97
- schema_version: typeof obj.schema_version === "number" ? obj.schema_version : SCHEMA_VERSION,
98
- records,
99
- },
100
- error: null,
101
- };
102
- }
103
- export function writeReviewMonitorFile(path, data) {
104
- atomicWriteJson(path, data);
105
- }
106
- export function registerReviewMonitor(input) {
107
- const path = reviewMonitorPath(input.projectRoot);
108
- const { data, error } = readReviewMonitorFile(path);
109
- if (data === null) {
110
- throw new Error(error ?? "could not read review-monitor file");
111
- }
112
- const startedAt = (input.startedAt ?? new Date()).toISOString();
113
- const record = {
114
- pr: input.pr,
115
- repo: input.repo ?? null,
116
- head_sha: input.headSha ?? null,
117
- platform_primitive: input.platformPrimitive,
118
- monitor_agent_id: input.monitorAgentId.trim(),
119
- started_at: startedAt,
120
- worktree_path: resolve(input.projectRoot),
121
- parent_session_id: input.parentSessionId ?? null,
122
- ended_at: null,
286
+ exitCode: EXIT_READY,
287
+ message: `review_monitor_register: claimed PR #${input.pr} review-owner lease for ${monitorAgentId} (comment ${created.id}).`,
288
+ record: leaseToRecord(input.pr, repo, lease, created.id, worktreePath, input.parentSessionId ?? null),
289
+ commentId: created.id,
290
+ priorOwner: null,
123
291
  };
124
- const active = data.records.filter((r) => r.ended_at === null && r.pr !== input.pr);
125
- writeReviewMonitorFile(path, {
126
- schema_version: SCHEMA_VERSION,
127
- records: [...active, record],
128
- });
129
- return { path, record };
130
292
  }
131
- export function parseIso8601Utc(value) {
132
- if (typeof value !== "string" || value.length === 0) {
133
- return null;
293
+ export function releaseReviewMonitor(input) {
294
+ const repo = resolveRepo(input.repo ?? null, resolve(input.projectRoot));
295
+ if (repo === null) {
296
+ return {
297
+ exitCode: EXIT_CONFIG_ERROR,
298
+ message: "review_monitor_release: could not resolve owner/repo — pass --repo OWNER/REPO or run inside a git repo with origin",
299
+ };
134
300
  }
135
- const trimmed = value.trim();
136
- let candidate = trimmed;
137
- if (trimmed.endsWith("Z")) {
138
- candidate = `${trimmed.slice(0, -1)}+00:00`;
301
+ const seams = input.seams ?? {};
302
+ const listed = listReviewOwnerComments(repo, input.pr, seams);
303
+ if (!Array.isArray(listed)) {
304
+ return {
305
+ exitCode: EXIT_CONFIG_ERROR,
306
+ message: `review_monitor_release: ${listed.error}`,
307
+ };
139
308
  }
140
- if (!candidate.endsWith("+00:00")) {
141
- return null;
309
+ const anchor = selectWinningReviewOwnerComment(listed);
310
+ if (anchor === null) {
311
+ return {
312
+ exitCode: EXIT_READY,
313
+ message: `review_monitor_release: no review-owner comment on PR #${input.pr}; nothing to release.`,
314
+ };
315
+ }
316
+ const active = findActiveLeaseComment(listed, { now: input.endedAt ?? new Date() });
317
+ const owner = input.owner ?? resolveGitHubLogin(seams);
318
+ if (active?.lease !== null && active?.lease !== undefined) {
319
+ const holder = active.lease;
320
+ if (owner === null || owner.length === 0) {
321
+ return {
322
+ exitCode: EXIT_CONFIG_ERROR,
323
+ message: "review_monitor_release: could not resolve GitHub login — pass --owner or authenticate gh",
324
+ };
325
+ }
326
+ const monitorAgentId = input.monitorAgentId?.trim() ?? null;
327
+ const ownerMatches = holder.owner === owner;
328
+ const authorized = monitorAgentId !== null && monitorAgentId.length > 0
329
+ ? ownerMatches && holder.monitor_agent_id === monitorAgentId
330
+ : ownerMatches;
331
+ if (!authorized) {
332
+ return {
333
+ exitCode: EXIT_CONFLICT,
334
+ message: conflictMessage(holder, input.pr),
335
+ };
336
+ }
142
337
  }
143
- const parsed = new Date(candidate);
144
- if (Number.isNaN(parsed.getTime())) {
338
+ const releaseTarget = active ?? anchor;
339
+ const endedAt = (input.endedAt ?? new Date()).toISOString();
340
+ const body = renderReleasedReviewOwnerComment(endedAt);
341
+ const updated = updateReviewOwnerComment(repo, releaseTarget.id, body, seams);
342
+ if ("error" in updated) {
343
+ return {
344
+ exitCode: EXIT_CONFIG_ERROR,
345
+ message: `review_monitor_release: ${updated.error}`,
346
+ };
347
+ }
348
+ return {
349
+ exitCode: EXIT_READY,
350
+ message: `review_monitor_release: released PR #${input.pr} review-owner lease (comment ${releaseTarget.id}).`,
351
+ };
352
+ }
353
+ export function findActiveMonitorForPrFromComments(comments, pr, options) {
354
+ const mapped = comments
355
+ .map((entry) => ({
356
+ id: entry.id,
357
+ body: entry.body,
358
+ createdAt: "",
359
+ lease: parseReviewOwnerLease(entry.body),
360
+ }))
361
+ .filter((entry) => entry.lease !== null);
362
+ const active = findActiveLeaseComment(mapped, {
363
+ now: options.now,
364
+ headSha: options.headSha ?? null,
365
+ });
366
+ if (active?.lease === null || active?.lease === undefined) {
145
367
  return null;
146
368
  }
147
- return parsed;
369
+ return leaseToRecord(pr, options.repo ?? null, active.lease, active.id, null, null);
148
370
  }
149
371
  export function isRecordActive(record, options) {
150
372
  if (record.ended_at !== null) {
151
373
  return false;
152
374
  }
153
375
  const started = parseIso8601Utc(record.started_at);
154
- if (started === null) {
376
+ const expires = parseIso8601Utc(record.expires_at);
377
+ if (started === null || expires === null) {
155
378
  return false;
156
379
  }
157
380
  const now = options.now ?? new Date();
158
- const staleMinutes = options.staleMinutes ?? DEFAULT_STALE_MINUTES;
159
- const ageMs = now.getTime() - started.getTime();
160
- if (ageMs > staleMinutes * 60 * 1000) {
381
+ if (now.getTime() > expires.getTime()) {
161
382
  return false;
162
383
  }
163
384
  if (options.headSha !== undefined && options.headSha !== null && record.head_sha !== null) {
@@ -172,7 +393,33 @@ export function findActiveMonitorForPr(file, pr, options) {
172
393
  }
173
394
  return matches.sort((a, b) => b.started_at.localeCompare(a.started_at))[0] ?? null;
174
395
  }
396
+ export function emptyReviewMonitorFile() {
397
+ return { schema_version: SCHEMA_VERSION, records: [] };
398
+ }
399
+ /** @deprecated Local `.deft/review-monitor.json` is obsolete (#2814); always empty. */
400
+ export function readReviewMonitorFile(_path) {
401
+ return { data: emptyReviewMonitorFile(), error: null };
402
+ }
403
+ /** @deprecated Local `.deft/review-monitor.json` is obsolete (#2814); no-op. */
404
+ export function writeReviewMonitorFile(_path, _data) {
405
+ // intentionally no-op
406
+ }
407
+ /** @deprecated Local `.deft/review-monitor.json` is obsolete (#2814). */
408
+ export function reviewMonitorPath(projectRoot) {
409
+ return resolve(projectRoot, ".deft", "review-monitor.json");
410
+ }
175
411
  export function defaultSubagentStatusDir(projectRoot) {
176
- return join(resolve(projectRoot), ".deft-scratch", "subagent-status");
412
+ return resolve(projectRoot, ".deft-scratch", "subagent-status");
413
+ }
414
+ export function fetchActiveMonitorFromGithub(repo, pr, options = {}) {
415
+ const listed = listReviewOwnerComments(repo, pr, options.seams ?? {});
416
+ if (!Array.isArray(listed)) {
417
+ return { error: listed.error };
418
+ }
419
+ return findActiveMonitorForPrFromComments(listed, pr, {
420
+ now: options.now,
421
+ headSha: options.headSha ?? null,
422
+ repo,
423
+ });
177
424
  }
178
425
  //# sourceMappingURL=record.js.map
@@ -1,4 +1,5 @@
1
1
  import { EXIT_CONFIG_ERROR, EXIT_NOT_READY, EXIT_READY, MONITORING_TIER_1, MONITORING_TIER_3 } from "./constants.js";
2
+ import type { ReviewOwnerGithubSeams } from "./github-lease.js";
2
3
  import { type ReviewMonitorRecord } from "./record.js";
3
4
  import { type MonitoringTierProbe } from "./tier-detection.js";
4
5
  export type ReviewMonitorCallSite = "solo" | "swarm-phase5-6" | "swarm-phase6-cascade" | "unspecified";
@@ -13,6 +14,7 @@ export interface VerifyReviewMonitorArgs {
13
14
  readonly staleMinutes?: number;
14
15
  readonly now?: Date;
15
16
  readonly environ?: NodeJS.ProcessEnv;
17
+ readonly seams?: ReviewOwnerGithubSeams;
16
18
  }
17
19
  export interface VerifyReviewMonitorResult {
18
20
  readonly exitCode: typeof EXIT_READY | typeof EXIT_NOT_READY | typeof EXIT_CONFIG_ERROR;