ai-agentmanager 1.7.0 → 1.9.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.
package/src/manager.js CHANGED
@@ -29,6 +29,12 @@ import {
29
29
  parseJsonReply,
30
30
  loadPlanFile,
31
31
  planApprovedPrompt,
32
+ intentMarkdown,
33
+ intentPath,
34
+ readSpecDoc,
35
+ specDocsInfo,
36
+ specPath,
37
+ specPrompt,
32
38
  planPath,
33
39
  planReviewPrompt,
34
40
  } from './brief.js';
@@ -93,6 +99,7 @@ async function repoLayout(dir) {
93
99
  import { spawn } from 'node:child_process';
94
100
  import * as git from './git.js';
95
101
  import { parseDiff } from '../public/review.js';
102
+ import { issueSummary, leadingIssue, wordsAfterLink } from './jira.js';
96
103
  import { conflictMessage, findConflicts, related, repoKey, summarize as summarizeConflicts } from './conflicts.js';
97
104
  import { answerMessage, loadQuestions, questionsPath, questionsPrompt } from './inbox.js';
98
105
  import { STUCK_DEFAULTS, detectStuck } from './stuck.js';
@@ -335,6 +342,8 @@ export class AgentManager extends EventEmitter {
335
342
  const prs = Object.values(agent.prs ?? {});
336
343
  return {
337
344
  ...agent,
345
+ // Intent, spec and plan files that exist, with when each changed (task agents).
346
+ ...(NO_REPO.has(agent.kind) ? {} : { specDocs: specDocsInfo(agent.id) }),
338
347
  results: this.results(agent, rt, prs),
339
348
  priority: agent.priority ?? 2,
340
349
  activeAt: this.activeAt(agent, rt),
@@ -667,9 +676,15 @@ export class AgentManager extends EventEmitter {
667
676
  };
668
677
  for (const pr of refs.prs) agent.prs[pr.url] = { ...pr, source: 'intent' };
669
678
  for (const bug of refs.bugs) agent.bugs[bug.key] = { ...bug, source: 'intent' };
679
+ // A request that starts with an issue link: describe it by the issue's summary (fetched below),
680
+ // and until then by the words after the link.
681
+ const issue = leadingIssue(intent);
682
+ if (issue) agent.synopsis = { text: wordsAfterLink(intent), source: 'request', key: issue.key };
670
683
  this.store.put(agent);
671
684
  this.registerProject(agent);
672
685
  this.changed(agent);
686
+ this.writeIntent(agent.id);
687
+ if (issue) this.fetchSynopsis(agent.id).catch(() => {});
673
688
 
674
689
  this.plugins.emit('agent.created', { agent: this.auditView(agent) });
675
690
  this.prepare(agent, { start, workspace }).catch(() => {});
@@ -682,6 +697,50 @@ export class AgentManager extends EventEmitter {
682
697
  return this.view(agent);
683
698
  }
684
699
 
700
+ /** Fetch the summary of the issue an agent's request starts with; it becomes its description. */
701
+ async fetchSynopsis(ref) {
702
+ const agent = this.store.get(this.store.resolve(ref)?.id ?? ref);
703
+ const issue = agent && leadingIssue(agent.intent);
704
+ if (!issue) return null;
705
+ const summary = await issueSummary(issue, this.jiraOptions ?? {});
706
+ const cur = this.store.get(agent.id);
707
+ if (!summary || !cur) return null;
708
+ const bug = cur.bugs?.[issue.key];
709
+ this.patch(cur.id, {
710
+ synopsis: { text: summary, source: 'jira', key: issue.key },
711
+ ...(bug && !bug.title ? { bugs: { ...cur.bugs, [issue.key]: { ...bug, title: summary } } } : {}),
712
+ });
713
+ this.writeIntent(cur.id);
714
+ return summary;
715
+ }
716
+
717
+ /** INTENT.md for a task agent: the request as given and where it came from (see brief.js). */
718
+ writeIntent(id) {
719
+ const agent = this.store.get(id);
720
+ if (!agent || NO_REPO.has(agent.kind)) return;
721
+ try {
722
+ fs.mkdirSync(briefDir(id), { recursive: true });
723
+ fs.writeFileSync(intentPath(id), intentMarkdown(agent));
724
+ } catch {}
725
+ }
726
+
727
+ /** One of an agent's intent, spec and plan files. */
728
+ specDoc(ref, name) {
729
+ const agent = this.mustGet(ref);
730
+ if (!['intent', 'spec', 'plan'].includes(name)) throw Object.assign(new Error('Unknown file (intent, spec or plan)'), { status: 400 });
731
+ return readSpecDoc(agent.id, name) ?? { name, markdown: null };
732
+ }
733
+
734
+ /** Fill in summaries still missing (after Jira settings change); returns how many it found. */
735
+ async refreshSynopses() {
736
+ let found = 0;
737
+ for (const a of this.store.list()) {
738
+ if (a.synopsis?.source === 'jira' || !leadingIssue(a.intent)) continue;
739
+ if (await this.fetchSynopsis(a.id).catch(() => null)) found++;
740
+ }
741
+ return found;
742
+ }
743
+
685
744
  prepare(agent, { start, workspace }) {
686
745
  const controller = new AbortController();
687
746
  const { signal } = controller;
@@ -951,6 +1010,13 @@ export class AgentManager extends EventEmitter {
951
1010
  prompts.push(briefPrompt(path.join(dir, path.basename(briefPath(agent.id)))));
952
1011
  if (this.config.inbox !== false) prompts.push(questionsPrompt(path.join(dir, path.basename(questionsPath(agent.id)))));
953
1012
  }
1013
+ // SPEC.md before changing code, and PLAN.md kept as it goes (unless the plan review asks for the plan).
1014
+ if (task && this.config.specs !== false) {
1015
+ fs.mkdirSync(briefDir(agent.id), { recursive: true });
1016
+ const dir = fs.realpathSync(briefDir(agent.id));
1017
+ writableDir ??= dir;
1018
+ prompts.push(specPrompt(path.join(dir, path.basename(specPath(agent.id))), path.join(dir, path.basename(planPath(agent.id))), { withPlan: !agent.planReview }));
1019
+ }
954
1020
  if (agent.planReview && task) {
955
1021
  fs.mkdirSync(briefDir(agent.id), { recursive: true });
956
1022
  const file = path.join(fs.realpathSync(briefDir(agent.id)), path.basename(planPath(agent.id)));
@@ -1976,7 +2042,9 @@ export class AgentManager extends EventEmitter {
1976
2042
  .join('\n\n');
1977
2043
  }
1978
2044
  }
1979
- return { intent: agent.intent, status, digest, repo, briefMarkdown };
2045
+ // The spec and plan go along with the brief: what done means, and how it gets there.
2046
+ const extra = ['spec', 'plan'].map((n) => readSpecDoc(agent.id, n)?.markdown.trim()).filter(Boolean);
2047
+ return { intent: agent.intent, status, digest, repo, briefMarkdown: [briefMarkdown, ...extra].filter(Boolean).join('\n\n') };
1980
2048
  }
1981
2049
 
1982
2050
  /** The engine (and model) that writes an agent's summaries: its own when it can make a one-shot call, else Claude Code. */
package/src/review.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // back to the agent as one message, and commit / push / open a pull request from the UI.
3
3
  import * as git from './git.js';
4
4
  import { hash } from './judge.js';
5
+ import { prSections } from './brief.js';
5
6
  import { NO_REPO } from './manager.js';
6
7
  import { ghBin } from './tracker.js';
7
8
 
@@ -193,8 +194,15 @@ export class Review {
193
194
  }
194
195
  const args = ['pr', 'create', '--head', branch];
195
196
  if (target) args.push('--base', target);
196
- if (String(title).trim()) args.push('--title', String(title).trim(), '--body', String(body ?? ''));
197
- else args.push('--fill');
197
+ // The description: your text (else the commits), then the intent, spec and plan.
198
+ const sections = prSections(agent.id);
199
+ if (String(title).trim() || sections) {
200
+ const log = base ? await git.run('git', ['log', '--reverse', '--format=%s', `${base.sha}..HEAD`], opts).catch(() => '') : '';
201
+ const subjects = log.split('\n').filter(Boolean);
202
+ const t = String(title).trim() || subjects[0] || agent.name;
203
+ const lead = String(body ?? '').trim() || (subjects.length > 1 ? subjects.map((x) => `- ${x}`).join('\n') : '');
204
+ args.push('--title', t, '--body', [lead, sections].filter(Boolean).join('\n\n'));
205
+ } else args.push('--fill');
198
206
  if (draft) args.push('--draft');
199
207
  let out;
200
208
  try {
package/src/server.js CHANGED
@@ -33,6 +33,7 @@ import { Previews, parseUrl } from './preview.js';
33
33
  import { LiveBrowser } from './livebrowser.js';
34
34
  import * as remote from './remote.js';
35
35
  import * as service from './service.js';
36
+ import { jiraView, saveJira } from './jira.js';
36
37
  import { Store } from './store.js';
37
38
  import { about, latestRelease } from './about.js';
38
39
 
@@ -60,7 +61,9 @@ function isAllowedOrigin(origin, port) {
60
61
  }
61
62
 
62
63
  const PUBLIC_REMOTE_API = new Set(['POST /api/login', 'GET /api/session']);
63
- const LOCAL_ONLY_API = /^\/api\/(remote(\/|$)|notify(\/|$)|hub(\/|$)|triggers(\/|$)|update(\/|$)|service$|editors$|agents\/[\w-]+\/open-in$|shutdown$)/;
64
+ // Updates are not here: a signed-in device (a phone) can install the newest release too. It only
65
+ // ever installs an official release from where this copy came from (git tag or npm) and restarts.
66
+ const LOCAL_ONLY_API = /^\/api\/(remote(\/|$)|notify(\/|$)|hub(\/|$)|triggers(\/|$)|service$|jira$|editors$|agents\/[\w-]+\/open-in$|shutdown$)/;
64
67
  const HOOK_PATH = '/api/hooks/agents';
65
68
  const NO_REPO_KINDS = new Set(['general', 'assistant']);
66
69
 
@@ -237,6 +240,7 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
237
240
  return manager.document(req.params.id, req.params.kind);
238
241
  }),
239
242
  );
243
+ api.get('/agents/:id/specs/:name', wrap((req) => manager.specDoc(req.params.id, req.params.name)));
240
244
  api.post('/agents/:id/plan', wrap((req) => manager.startPlanning(req.params.id)));
241
245
  api.post('/agents/:id/plan/approve', wrap((req) => manager.approvePlan(req.params.id, req.body?.note)));
242
246
  api.post('/agents/:id/plan/changes', wrap((req) => manager.requestPlanChanges(req.params.id, req.body?.note)));
@@ -508,6 +512,17 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
508
512
  api.get('/agents/:id/memory', wrap((req) => memory.read(req.params.id)));
509
513
  api.put('/agents/:id/memory', wrap((req) => memory.write(req.params.id, req.body?.text)));
510
514
  api.post('/agents/:id/learn', wrap((req) => memory.learn(req.params.id, req.body?.text, { by: req.headers['x-agentmgr-agent'] ? undefined : 'you' })));
515
+ // Jira: the site and API token used to show an issue's summary for agents created from its link.
516
+ api.get('/jira', wrap(() => jiraView()));
517
+ api.put(
518
+ '/jira',
519
+ wrap(async (req) => {
520
+ const view = saveJira(req.body ?? {});
521
+ // Agents already created from links get their summaries now (in the background).
522
+ manager.refreshSynopses().catch(() => {});
523
+ return view;
524
+ }),
525
+ );
511
526
  // Start at login: the login service (launchd or systemd) that brings the daemon back after a reboot.
512
527
  const serviceSupported = ['darwin', 'linux'].includes(process.platform);
513
528
  const serviceView = async () => ({ supported: serviceSupported, ...(serviceSupported ? await service.status().catch(() => ({ installed: false, loaded: false })) : { installed: false, loaded: false }) });