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/CHANGELOG.md +12 -0
- package/README.md +9 -5
- package/package.json +1 -1
- package/public/app.js +93 -14
- package/public/export.js +110 -0
- package/public/index.html +23 -3
- package/public/menu.js +40 -4
- package/public/review.js +13 -3
- package/public/share.js +1 -1
- package/public/side.js +47 -9
- package/public/speak.js +171 -0
- package/public/style.css +34 -1
- package/public/update.js +3 -5
- package/public/walkthrough.js +16 -0
- package/src/brief.js +79 -0
- package/src/jira.js +100 -0
- package/src/manager.js +69 -1
- package/src/review.js +10 -2
- package/src/server.js +16 -1
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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 }) });
|