bothouse-dsh 0.1.47 → 0.1.49
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/client/client.js +3411 -398
- package/collab/bot-port.mjs +30 -0
- package/collab/cli-member.mjs +3 -0
- package/collab/clis.mjs +11 -4
- package/collab/cloudweb.mjs +106 -17
- package/collab/desk/index.html +271 -31
- package/collab/dsh-member.mjs +47 -7
- package/collab/gate-context.mjs +80 -6
- package/collab/hub.mjs +42 -1
- package/collab/later.mjs +21 -0
- package/collab/memory-sources.mjs +159 -0
- package/collab/page.html +461 -6
- package/collab/pairing.mjs +674 -0
- package/collab/panel.mjs +10 -1
- package/collab/plugin.mjs +160 -24
- package/collab/roster.mjs +27 -6
- package/collab/store.mjs +48 -1
- package/collab/tasks.mjs +192 -16
- package/collab/wiki-galaxy.mjs +2 -1
- package/package.json +1 -1
package/collab/tasks.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { mentions, nameCalled, split, withoutLeadingMentions } from './mentions.
|
|
|
12
12
|
import { NOTICE_PREFIXES } from './cloudweb.mjs';
|
|
13
13
|
import { exitMark } from './team-exit.mjs';
|
|
14
14
|
import { checkDispatch, HandoffRulesError } from './handoff-rules.mjs';
|
|
15
|
+
import { execFile } from 'node:child_process';
|
|
15
16
|
|
|
16
17
|
/** Status codes and their labels (the eight names are a #139 B setting, not user-specified). */
|
|
17
18
|
export const STATUS = {
|
|
@@ -58,6 +59,34 @@ const LIVE = `status IN (${PLACEHOLDERS}) AND paused_at IS NULL`;
|
|
|
58
59
|
// from when several members edit the same files. The checks and their messages are in the shared
|
|
59
60
|
// handoff rules (handoff-rules.json, also read by ws-handoff); a dispatch runs the ones listed under
|
|
60
61
|
// its `dispatch` key. The rules file not being readable refuses the dispatch (HTTP 500), never skips it.
|
|
62
|
+
// Review handover (user 11-05「派活的角色忙超过 10 分钟,就转给空着的队友去验收。小活交回时,只要检查都过了
|
|
63
|
+
// 就自动收口,大活才需要人看」): a workspace task handed back waits CHECK_AFTER_S for its dispatcher's
|
|
64
|
+
// verdict, then goes to a free teammate other than its author (reviewOf). A small task closes itself:
|
|
65
|
+
// every GitHub PR its report links is merged with no failing check, or its handoff scope says 改:无.
|
|
66
|
+
// Not user-specified (橘 11-05, listed in the PR): the words below that keep a task for a person, and
|
|
67
|
+
// one extra wake of an idle dispatcher before handing over.
|
|
68
|
+
const PR_URL = /https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/\d+/g;
|
|
69
|
+
const NEEDS_PERSON = /发版|发布|release|线上|生产|部署|deploy|花钱|付费|付款|下单|充值|用户看|用户确认|用户拍板/i;
|
|
70
|
+
const CHANGE_LINE = /^[\s>*-]*改\s*[::]\s*(.*)$/gm;
|
|
71
|
+
const ASKS_USER = /要你做的\s*[::]\s*(.*)/;
|
|
72
|
+
const NOTHING = /^(没有|无)\s*[。.]?\s*$/;
|
|
73
|
+
const GREEN = new Set(['SUCCESS', 'NEUTRAL', 'SKIPPED']);
|
|
74
|
+
|
|
75
|
+
/** gh's view of one PR: { merged, green } (no checks is not green, same as board.py #1003); rejects when gh is missing or fails. */
|
|
76
|
+
export function ghPrStatus(url) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
execFile('gh', ['pr', 'view', url, '--json', 'state,statusCheckRollup'], { timeout: 30_000 }, (error, stdout) => {
|
|
79
|
+
if (error) return reject(error);
|
|
80
|
+
try {
|
|
81
|
+
const pr = JSON.parse(stdout);
|
|
82
|
+
const checks = Array.isArray(pr.statusCheckRollup) ? pr.statusCheckRollup : [];
|
|
83
|
+
const green = checks.length > 0 && checks.every(c => GREEN.has(String(c.conclusion ?? c.state ?? '').toUpperCase()));
|
|
84
|
+
resolve({ merged: pr.state === 'MERGED', green });
|
|
85
|
+
} catch (e) { reject(e); }
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
const MISSING_REWORK_NOTE = '返工要写原因:note 填为什么打回,示例:note: "没先读现有实现"';
|
|
62
91
|
|
|
63
92
|
/** Runs a handoff check; a missing or broken rules file becomes a CollabError instead of letting it through. */
|
|
@@ -90,8 +119,9 @@ export class Tasks {
|
|
|
90
119
|
* start: false defers the timer and pending deliveries until start() (the plugin
|
|
91
120
|
* starts after DSH recovery, so a reminder job is never mistaken for a leftover).
|
|
92
121
|
*/
|
|
93
|
-
constructor(hub, { now = () => Date.now() / 1000, setTimer = setTimeout, clearTimer = clearTimeout, start = true, maxRenewals = MAX_RENEWALS } = {}) {
|
|
122
|
+
constructor(hub, { now = () => Date.now() / 1000, setTimer = setTimeout, clearTimer = clearTimeout, start = true, maxRenewals = MAX_RENEWALS, prStatus = ghPrStatus } = {}) {
|
|
94
123
|
this.hub = hub;
|
|
124
|
+
this.prStatus = prStatus;
|
|
95
125
|
this.maxRenewals = maxRenewals;
|
|
96
126
|
this.store = hub.store;
|
|
97
127
|
this.db = hub.store.db;
|
|
@@ -123,6 +153,10 @@ export class Tasks {
|
|
|
123
153
|
renewals = CASE WHEN paused_at IS NOT NULL THEN 0 ELSE renewals END WHERE id = ? AND status = ? RETURNING *`),
|
|
124
154
|
handBack: this.db.prepare(`UPDATE tasks SET status = 'returned', paused_at = NULL, next_check_at = NULL, remind_msg_id = NULL, result_msg_id = ?, returned_at = ?, updated_at = ?
|
|
125
155
|
WHERE id = ? AND status = ? RETURNING *`),
|
|
156
|
+
// A returned workspace task waiting for its verdict: next_check_at is its review watch (reviewWatch).
|
|
157
|
+
reviewWait: this.db.prepare(`SELECT * FROM tasks WHERE status = 'returned' AND next_check_at IS NOT NULL ORDER BY next_check_at, id`),
|
|
158
|
+
setReviewCheck: this.db.prepare("UPDATE tasks SET next_check_at = ?, remind_msg_id = COALESCE(?, remind_msg_id) WHERE id = ? AND status = 'returned' RETURNING *"),
|
|
159
|
+
openReviews: this.db.prepare(`SELECT * FROM tasks WHERE parent_task = ? AND source = 'review' AND status IN (${PLACEHOLDERS})`),
|
|
126
160
|
bindAssign: this.db.prepare('UPDATE tasks SET assign_msg_id = ? WHERE id = ? AND assign_msg_id IS NULL'),
|
|
127
161
|
due: this.db.prepare(`SELECT * FROM tasks WHERE next_check_at IS NOT NULL AND next_check_at <= ?
|
|
128
162
|
AND ${LIVE} ORDER BY next_check_at, id`),
|
|
@@ -365,7 +399,13 @@ export class Tasks {
|
|
|
365
399
|
const row = this.q.handBack.get(messageId, now, now, id, task.status);
|
|
366
400
|
if (row === undefined) throw new CollabError('conflict', `task ${id} changed meanwhile`);
|
|
367
401
|
this.record(row, 'return', `#${id}「${row.title}」${this.nameOf(row.owner)} 交回,用时 ${duration(row.returned_at - row.created_at)}`);
|
|
368
|
-
this.
|
|
402
|
+
const watched = this.reviewWatched(row);
|
|
403
|
+
const small = watched ? this.smallTask(row, messageId) : undefined;
|
|
404
|
+
// A small task's dispatcher is only woken if it does not close itself (checkPrs).
|
|
405
|
+
const copy = this.returnToChat(row, messageId, { wake: small === undefined });
|
|
406
|
+
if (watched) this.q.setReviewCheck.get(now + CHECK_AFTER_S, null, id);
|
|
407
|
+
if (small?.readOnly) this.autoAccept(id, small.readOnly);
|
|
408
|
+
else if (small?.prs) this.checkPrs(id, small.prs, copy);
|
|
369
409
|
this.releaseIfDone(row.room_id);
|
|
370
410
|
return this.changed(id);
|
|
371
411
|
}
|
|
@@ -376,23 +416,162 @@ export class Tasks {
|
|
|
376
416
|
* as a chat-channel hand-back @-s the lead (lead_return.go). A bridge-claimed workspace is the
|
|
377
417
|
* bridge's (its lead session reads the workspace itself).
|
|
378
418
|
*/
|
|
379
|
-
returnToChat(task, messageId) {
|
|
380
|
-
if (!this.tree || this.store.room(task.room_id)?.kind !== 'workspace' || this.store.bridgeClaimed(task.room_id)) return;
|
|
419
|
+
returnToChat(task, messageId, { wake = true } = {}) {
|
|
420
|
+
if (!this.tree || this.store.room(task.room_id)?.kind !== 'workspace' || this.store.bridgeClaimed(task.room_id)) return undefined;
|
|
381
421
|
const msg = this.store.message(messageId);
|
|
382
422
|
let copy = msg === undefined ? undefined : this.hub.teamExit?.forwardResult(msg);
|
|
383
|
-
if (copy === undefined || task.created_by === HUMAN) return;
|
|
423
|
+
if (copy === undefined || task.created_by === HUMAN) return copy;
|
|
384
424
|
// The dispatcher is woken on this copy alone (a turn reads no other system row), so it says
|
|
385
425
|
// how to review, as the pause record says how to answer (watch).
|
|
386
426
|
const how = `\n\n验收 #${task.id}:回复里单独一行写【验收】#${task.id};要返工就单独一行写【返工】#${task.id} 原因。`;
|
|
387
427
|
if (!copy.text.includes(how)) copy = this.hub.edit(copy.id, 'system', 'system', copy.text + how, copy.card) ?? copy;
|
|
388
|
-
this.wake(task.created_by, copy);
|
|
428
|
+
if (wake) this.wake(task.created_by, copy);
|
|
429
|
+
return copy;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** A workspace task (not a bridge claim's, not a review) whose verdict this gateway waits for. */
|
|
433
|
+
reviewWatched(task) {
|
|
434
|
+
return this.tree && task.created_by !== HUMAN && task.source !== 'review'
|
|
435
|
+
&& this.store.room(task.room_id)?.kind === 'workspace' && !this.store.bridgeClaimed(task.room_id);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* A small task, closed without a person: { prs } (GitHub PR links in its report, checked by checkPrs)
|
|
440
|
+
* or { readOnly: why } (every 改 line of its handoff says 无). undefined when it needs a person: its
|
|
441
|
+
* report asks the user for something, or its handoff touches release, production, deploy or money.
|
|
442
|
+
*/
|
|
443
|
+
smallTask(task, messageId) {
|
|
444
|
+
const report = String(this.store.message(messageId)?.text ?? '');
|
|
445
|
+
const asks = ASKS_USER.exec(report)?.[1]?.trim();
|
|
446
|
+
if (asks !== undefined && !NOTHING.test(asks)) return undefined;
|
|
447
|
+
const handoff = `${task.title}\n${task.reason ?? ''}\n${task.assign_msg_id == null ? '' : this.store.message(task.assign_msg_id)?.text ?? ''}`;
|
|
448
|
+
const changes = [...handoff.matchAll(CHANGE_LINE)].map(m => m[1].trim());
|
|
449
|
+
if (NEEDS_PERSON.test(changes.length ? changes.join('\n') : handoff)) return undefined;
|
|
450
|
+
const prs = [...new Set(report.match(PR_URL) ?? [])];
|
|
451
|
+
if (prs.length) return { prs };
|
|
452
|
+
if (changes.length && changes.every(c => /^无/.test(c))) return { readOnly: '只查不改(交接范围「改:无」)' };
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Every PR merged with no failing check closes the task; anything else (or gh failing) wakes its dispatcher on copy. */
|
|
457
|
+
checkPrs(id, prs, copy) {
|
|
458
|
+
return Promise.all(prs.map(url => this.prStatus(url))).then(states => {
|
|
459
|
+
if (states.every(s => s?.merged && s?.green)) {
|
|
460
|
+
const nums = prs.map(u => `#${u.split('/').pop()}`).join('、');
|
|
461
|
+
if (this.autoAccept(id, `PR ${nums} 已合、检查全绿`)) return;
|
|
462
|
+
}
|
|
463
|
+
this.wakeReviewer(id, copy);
|
|
464
|
+
}, error => {
|
|
465
|
+
this.hub.logger?.warn?.('collab: PR check for task #%s failed: %s', id, error?.message ?? error);
|
|
466
|
+
this.wakeReviewer(id, copy);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
wakeReviewer(id, copy) {
|
|
471
|
+
const task = this.task(id);
|
|
472
|
+
if (!this.closed && copy !== undefined && task?.status === 'returned') this.wake(task.created_by, copy);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Close a returned task with no reviewer: one record says why. True when it closed it now. */
|
|
476
|
+
autoAccept(id, why) {
|
|
477
|
+
const task = this.task(id);
|
|
478
|
+
if (this.closed || task?.status !== 'returned') return false;
|
|
479
|
+
const row = this.q.set.get('done', null, clip(why), this.now(), 0, id, 'returned');
|
|
480
|
+
if (row === undefined) return false;
|
|
481
|
+
this.closeReviews(id, 'done');
|
|
482
|
+
this.record(row, 'done', `#${id}「${row.title}」已自动收口:${why}`);
|
|
483
|
+
this.releaseIfDone(row.room_id);
|
|
484
|
+
this.changed(id);
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Delegated reviews of task id end with its verdict. */
|
|
489
|
+
closeReviews(id, verdict, messageId) {
|
|
490
|
+
for (const review of this.q.openReviews.all(id, ...WORKING)) {
|
|
491
|
+
this.db.prepare(`UPDATE tasks SET status = 'done', next_check_at = NULL, remind_msg_id = NULL,
|
|
492
|
+
paused_at = NULL, result_msg_id = ?, returned_at = ?, updated_at = ? WHERE id = ?`)
|
|
493
|
+
.run(messageId ?? null, this.now(), this.now(), review.id);
|
|
494
|
+
this.record(this.task(review.id), 'done', `#${review.id} 验收 #${id} 已结束:${verdict === 'done' ? '通过' : '返工'}`);
|
|
495
|
+
this.changed(review.id);
|
|
496
|
+
this.releaseIfDone(review.room_id);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Busy or unreachable now: offline, cooling, in a turn, busy on the team status, or owning work. */
|
|
501
|
+
memberBusy(id) {
|
|
502
|
+
const m = this.hub.members.get(id);
|
|
503
|
+
if (m === undefined || m.online === false || m.error) return true;
|
|
504
|
+
if (this.hub.quota?.cooling(id)) return true;
|
|
505
|
+
if ([...(this.hub.thinking?.values() ?? [])].some(s => s.has(id))) return true;
|
|
506
|
+
if (this.hub.presence?.get(id)?.busy) return true;
|
|
507
|
+
return this.busyInfo(id).busy;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* A returned workspace task at its review check (CHECK_AFTER_S after hand-back, then every
|
|
512
|
+
* CHECK_AFTER_S): a delegated review in work → wait on it; an idle dispatcher → woken once more;
|
|
513
|
+
* a busy one (or one woken already) → a free teammate other than the author and the dispatcher
|
|
514
|
+
* reviews it (assign reviewOf), woken on a chat-channel note carrying the report; none free → wait.
|
|
515
|
+
*/
|
|
516
|
+
reviewWatch(task, now) {
|
|
517
|
+
if (!this.reviewWatched(task)) {
|
|
518
|
+
this.db.prepare('UPDATE tasks SET next_check_at = NULL WHERE id = ?').run(task.id);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const next = now + CHECK_AFTER_S;
|
|
522
|
+
const chat = this.chatOf(task.room_id);
|
|
523
|
+
const lead = task.created_by;
|
|
524
|
+
const how = `验收 #${task.id}:回复里单独一行写【验收】#${task.id};要返工就单独一行写【返工】#${task.id} 原因。`;
|
|
525
|
+
// A delegated review waits on its reviewer, unless that reviewer went offline or failed (a turn
|
|
526
|
+
// error, e.g. no model key): then the review moves on to another free teammate.
|
|
527
|
+
const reviews = this.q.openReviews.all(task.id, ...WORKING);
|
|
528
|
+
const unreachable = id => { const m = this.hub.members.get(id); return m === undefined || m.online === false || Boolean(m.error); };
|
|
529
|
+
if (reviews.some(r => !unreachable(r.owner))) {
|
|
530
|
+
this.q.setReviewCheck.get(next, null, task.id);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const tried = new Set(reviews.map(r => r.owner));
|
|
534
|
+
if (!reviews.length && !this.memberBusy(lead) && task.remind_msg_id == null) {
|
|
535
|
+
const { message } = this.hub.post(chat, { authorKind: 'system', author: 'system', name: '任务',
|
|
536
|
+
card: JSON.stringify({ kind: 'task', task_id: task.id, event: 'remind' }),
|
|
537
|
+
text: `@${this.nameOf(lead)} #${task.id}「${task.title}」交回 ${CHECK_AFTER_S / 60} 分钟还没验收。${how}` });
|
|
538
|
+
this.q.setReviewCheck.get(next, message.id, task.id);
|
|
539
|
+
this.wake(lead, message);
|
|
540
|
+
this.changed(task.id);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const reviewer = this.hub.routableIn(chat).find(m => m.id !== task.owner && m.id !== lead && !tried.has(m.id) && m.kind !== 'human' && !this.memberBusy(m.id));
|
|
544
|
+
if (reviewer === undefined) {
|
|
545
|
+
this.q.setReviewCheck.get(next, null, task.id);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const result = task.result_msg_id == null ? undefined : this.store.message(task.result_msg_id);
|
|
549
|
+
const body = result === undefined ? '' : (exitMark(result.text, { lastLine: true, team: this.hub.routable() })?.body ?? String(result.text));
|
|
550
|
+
const cut = String(body).trim().length > BRIEF_HANDOFF ? `${String(body).trim().slice(0, BRIEF_HANDOFF)}…` : String(body).trim();
|
|
551
|
+
const why = reviews.length ? `${reviews.map(r => this.nameOf(r.owner)).join('、')} 掉线或出错`
|
|
552
|
+
: this.memberBusy(lead) ? `${this.nameOf(lead)} 在忙` : `${this.nameOf(lead)} 提醒后仍没验收`;
|
|
553
|
+
const { message } = this.hub.post(chat, { authorKind: 'system', author: 'system', name: '任务',
|
|
554
|
+
text: `@${reviewer.name} #${task.id}「${task.title}」交回超过 ${CHECK_AFTER_S / 60} 分钟,${why},转给你验收(写的人 ${this.nameOf(task.owner)} 不验收自己的活)。`
|
|
555
|
+
+ `${cut ? `\n\n交回内容:\n${cut}` : ''}\n\n${how}` });
|
|
556
|
+
let review;
|
|
557
|
+
try {
|
|
558
|
+
review = this.assign({ roomId: chat, title: `验收 #${task.id}「${task.title}」`, owner: reviewer.id, actor: lead, reviewOf: task.id,
|
|
559
|
+
messageId: message.id, reason: `派活人忙超 ${CHECK_AFTER_S / 60} 分钟,自动转验收` });
|
|
560
|
+
} catch (error) {
|
|
561
|
+
this.hub.logger?.warn?.('collab: review handover for #%s failed: %s', task.id, error?.message ?? error);
|
|
562
|
+
this.q.setReviewCheck.get(next, null, task.id);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
this.record(task, 'handover', `#${task.id}「${task.title}」已转给 ${reviewer.name} 验收(#${review.id}):${why}`, chat);
|
|
566
|
+
this.q.setReviewCheck.get(next, null, task.id);
|
|
567
|
+
this.wake(reviewer.id, message);
|
|
568
|
+
this.changed(task.id);
|
|
389
569
|
}
|
|
390
570
|
|
|
391
571
|
/** The lead (or the human owner) closes the task or sends it back for rework. */
|
|
392
572
|
review(id, { actor, verdict, note, messageId }) {
|
|
393
573
|
const task = this.mustTask(id);
|
|
394
|
-
const reviews = this.
|
|
395
|
-
AND status IN (${PLACEHOLDERS})`).all(id, ...WORKING);
|
|
574
|
+
const reviews = this.q.openReviews.all(id, ...WORKING);
|
|
396
575
|
const delegated = actor !== task.owner && reviews.some(r => r.owner === actor && r.paused_at == null);
|
|
397
576
|
if (actor !== task.created_by && actor !== HUMAN && !delegated) throw new CollabError('forbidden', `only ${this.nameOf(task.created_by)} or their delegated reviewer can review task ${id}`);
|
|
398
577
|
if (!REVIEWABLE.includes(task.status)) throw new CollabError('conflict', `task ${id} is ${STATUS[task.status]}, nothing to review`);
|
|
@@ -404,14 +583,7 @@ export class Tasks {
|
|
|
404
583
|
const reclaim = verdict === 'rework' && this.reworkClaim(task);
|
|
405
584
|
const row = this.q.set.get(verdict, check, clip(note) ?? task.note, this.now(), 0, id, task.status);
|
|
406
585
|
if (row === undefined) throw new CollabError('conflict', `task ${id} changed meanwhile`);
|
|
407
|
-
|
|
408
|
-
this.db.prepare(`UPDATE tasks SET status = 'done', next_check_at = NULL, remind_msg_id = NULL,
|
|
409
|
-
paused_at = NULL, result_msg_id = ?, returned_at = ?, updated_at = ? WHERE id = ?`)
|
|
410
|
-
.run(messageId ?? null, this.now(), this.now(), review.id);
|
|
411
|
-
this.record(this.task(review.id), 'done', `#${review.id} 验收 #${id} 已结束:${verdict === 'done' ? '通过' : '返工'}`);
|
|
412
|
-
this.changed(review.id);
|
|
413
|
-
this.releaseIfDone(review.room_id);
|
|
414
|
-
}
|
|
586
|
+
this.closeReviews(id, verdict, messageId);
|
|
415
587
|
if (verdict === 'rework') this.q.resetRenewals.run(id);
|
|
416
588
|
const tail = clip(note) ? `:${clip(note)}` : '';
|
|
417
589
|
// A workspace task sent back: its owner is woken there on the record (nobody routes it there
|
|
@@ -751,6 +923,9 @@ export class Tasks {
|
|
|
751
923
|
for (const task of this.q.allBusy.all(...WORKING)) {
|
|
752
924
|
if (this.watched(task) && watchDue(task) <= now) this.watch(task, now);
|
|
753
925
|
}
|
|
926
|
+
for (const task of this.q.reviewWait.all()) {
|
|
927
|
+
if (task.next_check_at <= now) this.reviewWatch(task, now);
|
|
928
|
+
}
|
|
754
929
|
for (const task of this.q.due.all(now, ...WORKING)) {
|
|
755
930
|
if (this.store.room(task.room_id)?.kind === 'workspace') continue;
|
|
756
931
|
if (this.hub.quota?.cooling(task.owner)) {
|
|
@@ -892,6 +1067,7 @@ export class Tasks {
|
|
|
892
1067
|
if (t.paused_at != null) return null;
|
|
893
1068
|
return this.store.room(t.room_id)?.kind !== 'workspace' ? t.next_check_at : this.watched(t) ? watchDue(t) : null;
|
|
894
1069
|
})
|
|
1070
|
+
.concat(this.q.reviewWait.all().map(t => t.next_check_at))
|
|
895
1071
|
.filter(t => t != null).sort((a, b) => a - b)[0];
|
|
896
1072
|
if (at == null) return;
|
|
897
1073
|
const ms = Math.min(MAX_TIMER_MS, Math.max(0, (at - this.now()) * 1000));
|
package/collab/wiki-galaxy.mjs
CHANGED
|
@@ -47,7 +47,8 @@ export function scanWiki(root) {
|
|
|
47
47
|
const bodies = [];
|
|
48
48
|
for (const path of files) {
|
|
49
49
|
let text;
|
|
50
|
-
|
|
50
|
+
// Pages saved on Windows are CRLF: without this the frontmatter (`---\n`) is never seen.
|
|
51
|
+
try { text = readFileSync(join(root, path), 'utf8').replace(/\r\n?/g, '\n'); } catch { continue; }
|
|
51
52
|
const { meta, body } = splitFrontmatter(text);
|
|
52
53
|
const heading = /^#\s+(.+)$/m.exec(body)?.[1]?.trim();
|
|
53
54
|
const parts = path.split('/');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bothouse-dsh",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.49",
|
|
4
4
|
"description": "BotHouse channels for DeepSeek Harness: Discord-style rooms where DSH models and local CLIs (Claude Code, Codex, Gemini) are members",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./collab/plugin.mjs",
|