local-kanban 2.0.0-beta.5 → 2.0.0-beta.6

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/CHANGELOG.md CHANGED
@@ -8,6 +8,12 @@ version it goes out as and opens an empty one again. Released sections are histo
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [2.0.0-beta.6]
12
+
13
+ ### Fixed
14
+
15
+ - Words typed to the terminal of a task in Review that only lead to commits of other tasks or a release (such as «roll it out») no longer keep the handed-in task in Doing: when the turn changed nothing but commits and none of them names the task, it goes back to Review. A task without an issue keeps the old rule — a commit naming nothing may be its own.
16
+
11
17
  ## [2.0.0-beta.5]
12
18
 
13
19
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-kanban",
3
- "version": "2.0.0-beta.5",
3
+ "version": "2.0.0-beta.6",
4
4
  "license": "MIT",
5
5
  "author": "Dannylamao",
6
6
  "repository": {
@@ -10,7 +10,7 @@ import { resolveProject } from './projects.js';
10
10
  import { kick } from '../sync/worker.js';
11
11
  import { LABELS } from '../sync/github.js';
12
12
  import { repoHttpsBase } from '../repo-base.js';
13
- import { STAT_FORMAT, mirrorName, statCommit, wordsOf } from '../shipped.js';
13
+ import { STAT_FORMAT, mirrorName, namesTask, statCommit, wordsOf } from '../shipped.js';
14
14
  import { isManual, listShape, noclaudeFilter, snapshot, taskEvent } from '../task-shape.js';
15
15
  import { requestSession, requestHostSession, bindSession, taskSessions, mayReprep, unclaimedLaunch, claimLaunch, markLaunch } from '../claude-sessions.js';
16
16
  import { taskFeed } from '../work-feed.js';
@@ -19,7 +19,7 @@ import { currentWorkProjects, liveTasks, terminalQuestions } from '../claude-liv
19
19
  import { activeBoardSessions, doneRuns, nowRuns } from '../claude-now.js';
20
20
  import { closeFinishedTerminals, taskTerminal } from '../phantom.js';
21
21
  import { CHECK_LABEL, OPEN_CHECK, isCheck, leadCheck, openCheck, secondCheck } from '../review-check.js';
22
- import { treePrint } from '../tree-print.js';
22
+ import { treePrint, turnCommits } from '../tree-print.js';
23
23
  import { bin } from '../bin-path.js';
24
24
 
25
25
  const ATTACH_DIR = join(DATA_DIR, 'attachments');
@@ -65,6 +65,15 @@ async function commitUrlFor(task) {
65
65
 
66
66
  // and not the newest commit of the project, whoever made it
67
67
  const STAT_SCAN = 300;
68
+ /** The words that name `task` in a commit of the repository at `path` (wordsOf) and the prefixes of the board. */
69
+ async function taskWords(task, path) {
70
+ const issueNo = task.gh_issue_number;
71
+ const mirror = mirrorName(ghOwner(), ghRepo());
72
+ const base = issueNo && mirror ? await repoHttpsBase(path) : null;
73
+ const home = Boolean(base) && base.toLowerCase() === `https://github.com/${mirror}`.toLowerCase();
74
+ const known = new Set(db.prepare('SELECT upper(prefix) AS prefix FROM projects').all().map((p) => p.prefix));
75
+ return { words: wordsOf(task.key, issueNo, mirror, home), known };
76
+ }
68
77
  function attachGitStat(task, startedAt) {
69
78
  const project = db.prepare('SELECT path FROM projects WHERE id = ?').get(task.project_id);
70
79
  if (!project?.path) return;
@@ -73,14 +82,11 @@ function attachGitStat(task, startedAt) {
73
82
  const git = (args) => new Promise((done) => execFile(bin('git'), ['-C', project.path, ...args], { timeout: 5000, maxBuffer: 16 * 1024 * 1024 }, (err, out) => done(err ? null : out)));
74
83
  (async () => {
75
84
  try {
76
- const mirror = mirrorName(ghOwner(), ghRepo());
77
- const base = issueNo && mirror ? await repoHttpsBase(project.path) : null;
78
- const home = Boolean(base) && base.toLowerCase() === `https://github.com/${mirror}`.toLowerCase();
85
+ const { words, known } = await taskWords(task, project.path);
79
86
  const log = await git(['log', '-n', String(STAT_SCAN), `--format=${STAT_FORMAT}`]);
80
87
  if (log === null) return; // not a git repository, or no git — nothing to show
81
- const known = new Set(db.prepare('SELECT upper(prefix) AS prefix FROM projects').all().map((p) => p.prefix));
82
88
  const since = startedAt ? Math.floor(new Date(startedAt + 'Z').getTime() / 1000) : null;
83
- const hash = statCommit(log, wordsOf(task.key, issueNo, mirror, home), { issue: issueNo, since, known });
89
+ const hash = statCommit(log, words, { issue: issueNo, since, known });
84
90
  if (!hash) return;
85
91
  const stdout = await git(['log', '-1', '--format=%h|%ct|%s', '--shortstat', hash]);
86
92
  const lines = String(stdout || '').trim().split('\n').filter(Boolean);
@@ -361,8 +367,16 @@ export async function settleTurns(states) {
361
367
  const elsewhere = db.prepare('SELECT 1 FROM task_events WHERE session_id = ? AND task_id != ? AND id > ? LIMIT 1').get(m.sid, st.id, m.event);
362
368
  if (!elsewhere) {
363
369
  const before = await m.print;
364
- const now = await treePrint(db.prepare('SELECT p.path FROM tasks t JOIN projects p ON p.id = t.project_id WHERE t.id = ?').get(st.id)?.path);
365
- if (before === null || now === null || before !== now) continue; // committed or changed files — or cannot tell
370
+ const path = db.prepare('SELECT p.path FROM tasks t JOIN projects p ON p.id = t.project_id WHERE t.id = ?').get(st.id)?.path;
371
+ const now = await treePrint(path);
372
+ if (before === null || now === null) continue; // cannot tell
373
+ if (before !== now) {
374
+ const log = await turnCommits(path, before, now, STAT_FORMAT);
375
+ const t = log === null ? null : resolveTask(String(st.id));
376
+ if (!t) continue; // changed files — or cannot tell
377
+ const { words, known } = await taskWords(t, path);
378
+ if (namesTask(log, words, { issue: t.gh_issue_number, known })) continue; // committed for it
379
+ }
366
380
  }
367
381
  back.push({ id: st.id, ...m });
368
382
  }
package/src/shipped.js CHANGED
@@ -68,6 +68,12 @@ export function statCommit(out, words, { issue = null, since = null, known = nul
68
68
  return head.hash;
69
69
  }
70
70
 
71
+ export function namesTask(out, words, { issue = null, known = null } = {}) {
72
+ const commits = parseLog(out, known);
73
+ if (!commits.length) return true;
74
+ return commits.some((c) => words.some((w) => c.words.has(w)) || (!issue && !ownKeys(c.subject, known).length));
75
+ }
76
+
71
77
  export function branchesOf(key, names) {
72
78
  const k = new RegExp(`(^|[-/_])${key}($|[-/_])`, 'i');
73
79
  return names.filter((n) => k.test(n.replace(/^(origin\/)?claude\//, '')));
package/src/tree-print.js CHANGED
@@ -29,6 +29,16 @@ export async function treePrint(path) {
29
29
  if (untracked.length > UNTRACKED_MAX || !top) return null; // too many to look at, or no root to find them: cannot tell
30
30
  const root = top.toString('utf8').trim();
31
31
  const stamps = await Promise.all(untracked.map((f) => stat(join(root, f)).then((s) => `${f}\0${s.size}\0${s.mtimeMs}`, () => `${f}\0-`)));
32
- return createHash('sha1').update(head || '').update('\0').update(status).update('\0').update(diff || '').update('\0')
33
- .update(stamps.join('\0')).digest('hex');
32
+ const rest = createHash('sha1').update(status).update('\0').update(diff || '').update('\0').update(stamps.join('\0')).digest('hex');
33
+ return `${head ? head.toString('utf8').trim() : ''}:${rest}`;
34
+ }
35
+
36
+ export async function turnCommits(path, before, now, format) {
37
+ const cut = (p) => { const i = String(p || '').indexOf(':'); return i > 0 ? [p.slice(0, i), p.slice(i + 1)] : [null, null]; };
38
+ const [a, restA] = cut(before);
39
+ const [b, restB] = cut(now);
40
+ if (!a || !b || a === b || restA !== restB) return null;
41
+ if (await git(path, ['merge-base', '--is-ancestor', a, b]) === null) return null; // a branch switched, a reset: cannot tell
42
+ const out = await git(path, ['log', `--format=${format}`, `${a}..${b}`]);
43
+ return out === null ? null : out.toString('utf8');
34
44
  }