@detergent-software/conductor 1.4.0-dev.14 → 1.4.0-dev.15

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
@@ -2,6 +2,13 @@
2
2
 
3
3
  Releases are tagged `conductor-v<version>` and staged on npm by `atk-conductor-release` (`pipelines/release-conductor.yml`), which writes each section below. `scripts/check-changelog.mjs` fails when a tag has no section here.
4
4
 
5
+ ## [1.4.0-dev.15](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.4.0-dev.14&targetVersion=GTconductor-v1.4.0-dev.15) (2026-10-15)
6
+
7
+ ### Bug Fixes
8
+
9
+ * **conductor:** approve and revise from the console through conduct (ATK-508) ([f3f1785](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/f3f1785136e152ade05cad8cab8e938c5eefef08))
10
+ * **conductor:** say when an approve may have posted, on every path (ATK-508) ([8278f23](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/8278f23a6adfe6a73904674802c6026a54bba983))
11
+
5
12
  ## [1.4.0-dev.14](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.4.0-dev.13&targetVersion=GTconductor-v1.4.0-dev.14) (2026-10-15)
6
13
 
7
14
  ### Bug Fixes
@@ -383,9 +383,15 @@ export function errorOf(answer: ApiAnswer): string {
383
383
  return `the conductor answered ${answer.status}`;
384
384
  }
385
385
 
386
+ // What `conduct approve|revise` adds when the daemon can't say whether it posted the comment.
387
+ export const MAYBE_POSTED = 'the comment may be on the item already: look before answering again';
388
+
386
389
  // POST /inbox/:id/resolve's answer as the line to show, as `conduct approve|revise|done` says it.
387
390
  export function answerText(action: 'approve' | 'done' | 'revise', id: number, answer: ApiAnswer): string {
388
- if (answer.status !== 200) return `not ${action === 'done' ? 'done' : `${action}d`}: ${errorOf(answer)}`;
391
+ if (answer.status !== 200) {
392
+ const unknown = answer.status === 502 && isRecord(answer.body) && answer.body.posted === 'unknown';
393
+ return `not ${action === 'done' ? 'done' : `${action}d`}: ${errorOf(answer)}${unknown ? `; ${MAYBE_POSTED}` : ''}`;
394
+ }
389
395
  const body = isRecord(answer.body) ? answer.body : {};
390
396
  const key = isRecord(body.item) && typeof body.item.item === 'string' ? body.item.item : `#${id}`;
391
397
  if (action === 'done') return `done: ${key}`;
@@ -395,23 +401,26 @@ export function answerText(action: 'approve' | 'done' | 'revise', id: number, an
395
401
  return `${action === 'approve' ? 'approved' : 'asked for a revision of'} ${key}: ${where}`;
396
402
  }
397
403
 
398
- // A `conduct attach` run as the line to show: what it printed when it attached (`woke …`, and how
399
- // to open the tab when none opened), else its error and details without the `conduct: ` prefix, and
400
- // without anything node wrote before it (a warning).
401
- export function attachRunText(name: string, run: { exitCode: number; stderr: string; stdout: string }): string {
404
+ // A `conduct` run (attach, approve, revise) as the line to show: what it printed when it succeeded
405
+ // (`woke …`, `approved …`), else `failed`, then its error and details without the `conduct: `
406
+ // prefix, and without anything node wrote before it (a warning).
407
+ export function runText(
408
+ run: { exitCode: number; stderr: string; stdout: string },
409
+ says: { failed: string; ok: string },
410
+ ): string {
402
411
  const lines = (text: string): string[] =>
403
412
  text
404
413
  .split('\n')
405
414
  .map((l) => l.trim())
406
415
  .filter((l) => l !== '');
407
- if (run.exitCode === 0) return lines(run.stdout).join('; ') || `attached to ${name}`;
416
+ if (run.exitCode === 0) return lines(run.stdout).join('; ') || says.ok;
408
417
  const stderr = lines(run.stderr);
409
418
  const from = stderr.findIndex((l) => l.startsWith('conduct: '));
410
419
  const error = stderr
411
420
  .slice(Math.max(from, 0))
412
421
  .map((l) => l.replace(/^conduct: /, ''))
413
422
  .join('; ');
414
- return `could not attach to ${name}: ${error || `conduct attach exited ${run.exitCode}`}`;
423
+ return `${says.failed}: ${error || `conduct exited ${run.exitCode}`}`;
415
424
  }
416
425
 
417
426
  // The daemon's home as the CLI finds it (apps/conductor/src/paths.ts): CONDUCTOR_HOME, else
@@ -7,7 +7,6 @@ import {
7
7
  age,
8
8
  type ApiAnswer,
9
9
  answerText,
10
- attachRunText,
11
10
  bandEntries,
12
11
  clearedText,
13
12
  CONSOLE_SKILL,
@@ -20,6 +19,7 @@ import {
20
19
  idleAction,
21
20
  inboxText,
22
21
  isWebLink,
22
+ MAYBE_POSTED,
23
23
  NO_SKILL_CONTEXT,
24
24
  NO_SKILL_WARNING,
25
25
  notifyArgs,
@@ -37,6 +37,7 @@ import {
37
37
  pushWarning,
38
38
  reaction,
39
39
  reportedState,
40
+ runText,
40
41
  snapshotOf,
41
42
  spendToday,
42
43
  statusText,
@@ -60,6 +61,9 @@ const CONFIRM_MS = 30_000;
60
61
  // gives up on an answer not complete within 30 s (ATK-507). `conduct attach` waits that long, and
61
62
  // $.process.run allows a command ten minutes at most.
62
63
  const ATTACH_RUN_MS = 10 * 60_000;
64
+ // An approve or a revise asks Linear twice, and `conduct approve|revise` allows that 60 s (ATK-508);
65
+ // this adds node's start.
66
+ const ANSWER_RUN_MS = 2 * 60_000;
63
67
  const NOT_RUNNING = 'the conductor is not running (conduct serve)';
64
68
 
65
69
  const snapshot = atom({ plugin: 'conductor', key: 'snapshot' } as const, null);
@@ -363,13 +367,38 @@ async function follow($: EngineInterface): Promise<void> {
363
367
  }
364
368
 
365
369
  // Jason's answer to an inbox item, as `conduct approve|revise|done` posts it; the line to show.
370
+ // Approve and revise run `conduct` (ANSWER_RUN_MS), by inbox id. Without the CLI, the API, which
371
+ // $.http.fetch cuts off after 30 s, possibly after the comment was posted. Done doesn't ask Linear.
366
372
  async function answer(
367
373
  $: EngineInterface,
368
374
  id: number,
369
375
  body: { action: 'approve' | 'done' } | { action: 'revise'; text: string },
370
376
  ): Promise<string> {
371
377
  try {
372
- const result = await callApi($, 'POST', `/inbox/${id}/resolve`, body);
378
+ const cli = cliOf($);
379
+ if (body.action !== 'done' && (await $.fs.exists(cli))) {
380
+ const failed = `not ${body.action}d`;
381
+ const argv = body.action === 'approve' ? ['approve', `#${id}`] : ['revise', `#${id}`, body.text];
382
+ let run;
383
+ try {
384
+ run = await $.process.run(['node', cli, ...argv], { timeoutMs: ANSWER_RUN_MS });
385
+ } catch (error) {
386
+ return `${failed}: ${error instanceof Error ? error.message : String(error)}`;
387
+ }
388
+ refreshSoon($);
389
+ return runText(run, {
390
+ failed,
391
+ ok: `${body.action === 'approve' ? 'approved' : 'asked for a revision of'} #${id}`,
392
+ });
393
+ }
394
+ const at = await endpoint($);
395
+ let result: ApiAnswer;
396
+ try {
397
+ result = await callApi($, 'POST', `/inbox/${id}/resolve`, body, at);
398
+ } catch (error) {
399
+ if (body.action === 'done') throw error;
400
+ return `not ${body.action}d: ${error instanceof Error ? error.message : String(error)}; ${MAYBE_POSTED}`;
401
+ }
373
402
  refreshSoon($);
374
403
  return answerText(body.action, id, result);
375
404
  } catch (error) {
@@ -415,7 +444,7 @@ async function attach($: EngineInterface, id: number, name: string): Promise<str
415
444
  $.ui.toast(`attaching to ${name}…`);
416
445
  const run = await $.process.run(['node', cli, 'attach', `#${id}`], { timeoutMs: ATTACH_RUN_MS });
417
446
  refreshSoon($);
418
- return attachRunText(name, run);
447
+ return runText(run, { failed: `could not attach to ${name}`, ok: `attached to ${name}` });
419
448
  }
420
449
  const result = await callApi($, 'POST', `/workers/${id}/attach`);
421
450
  refreshSoon($);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@detergent-software/conductor",
3
- "version": "1.4.0-dev.14",
3
+ "version": "1.4.0-dev.15",
4
4
  "description": "Conductor: the local daemon that runs conducted projects' work loops (conduct serve) and its conduct CLI.",
5
5
  "type": "module",
6
6
  "bin": {