symbiot 0.55.1 → 0.55.3
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 +19 -0
- package/README.md +142 -50
- package/agents.mjs +136 -6
- package/ai.mjs +79 -7
- package/guard.mjs +6 -2
- package/handback.mjs +2 -2
- package/handover.mjs +14 -3
- package/home.mjs +185 -35
- package/index.mjs +17 -9
- package/lanes.mjs +6 -6
- package/marketing.mjs +130 -0
- package/mind.mjs +26 -13
- package/package.json +4 -3
- package/reports.mjs +3 -3
- package/scan.mjs +9 -2
- package/server.mjs +23 -12
- package/tasks.mjs +14 -10
- package/ui.mjs +307 -132
- package/watch.mjs +5 -5
- package/writeups.mjs +3 -3
package/tasks.mjs
CHANGED
|
@@ -12,7 +12,8 @@ import { userStyleLine } from "./adapt.mjs";
|
|
|
12
12
|
import { QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, handoffCmd, runningHandoff, writeTasks, droppedTasks, startHeldTasks, connectorsLine } from "./agents.mjs";
|
|
13
13
|
import { gitDefaultBranch } from "./drift.mjs";
|
|
14
14
|
import { readRunLog } from "./work.mjs";
|
|
15
|
-
import { repoPathMap, openWork, detectRepo } from "./scan.mjs";
|
|
15
|
+
import { repoPathMap, laneMap, openWork, detectRepo } from "./scan.mjs";
|
|
16
|
+
import { MARKETING_DIR, ensureMarketing, marketingBrief } from "./marketing.mjs";
|
|
16
17
|
|
|
17
18
|
// ---- tasks: a persistent checklist (stored by core.mjs) -------------------
|
|
18
19
|
// One line (TASKS.md has a task per line, and a tick only matches a whole one),
|
|
@@ -107,7 +108,7 @@ function applyDrops(t, map) {
|
|
|
107
108
|
// Tasks held for an agent that has since finished land here too, and an agent
|
|
108
109
|
// starts on what's still open in them (one `push --open` started can't do that).
|
|
109
110
|
function syncTasks() {
|
|
110
|
-
const t = loadTasks(); const map =
|
|
111
|
+
const t = loadTasks(); const map = laneMap(); let review = 0, archived = 0, started = 0; const checkedByRepo = {};
|
|
111
112
|
const dropped = applyDrops(t, map);
|
|
112
113
|
for (const x of t) {
|
|
113
114
|
if (x.archived || x.done || x.review || !x.repo) continue;
|
|
@@ -375,7 +376,7 @@ function finishedOffer(path, open) {
|
|
|
375
376
|
function pendingReview() {
|
|
376
377
|
const t = loadTasks(), by = {}; for (const x of t) if (x.review && !x.done && !x.archived) (by[x.repo] = by[x.repo] || []).push(x);
|
|
377
378
|
const sent = [...new Set(t.filter((x) => x.repo && !x.archived && !by[x.repo]).map((x) => x.repo))];
|
|
378
|
-
const map = Object.keys(by).length || sent.length ?
|
|
379
|
+
const map = Object.keys(by).length || sent.length ? laneMap() : {};
|
|
379
380
|
const am = autoMergeRepos();
|
|
380
381
|
const out = Object.keys(by).sort().map((repo) => { const path = map[repo] || ""; return { repo, path, tasks: by[repo], autoMerge: am.includes(repo), running: !!(path && runningHandoff(path)), ...(path ? { ...workingChanges(path), unreleased: unreleased(path), bumpOffer: bumpOffer(path), publishesOnMerge: publishesOnMerge(path) } : { branch: "", files: [], stat: "" }) }; });
|
|
381
382
|
for (const repo of sent.sort()) {
|
|
@@ -497,7 +498,7 @@ function setAutoMerge(repo, on) { const cfg = loadConfig(); let a = (Array.isArr
|
|
|
497
498
|
function approveRepo(repo, opts = {}) {
|
|
498
499
|
const t = loadTasks(); const items = t.filter((x) => x.repo === repo && x.review && !x.done && !x.archived);
|
|
499
500
|
if (!items.length) return { error: "Nothing awaiting review for " + (repo || "(no repo)") + "." };
|
|
500
|
-
const path =
|
|
501
|
+
const path = laneMap()[repo]; if (!path) return { error: "Repo not found: " + repo };
|
|
501
502
|
const busy = stillWorking(repo, path); if (busy) return busy;
|
|
502
503
|
const r = shipWithBump(path, uniqueTasks(items.map((x) => x.text)), { ...opts, autoMerge: opts.autoMerge !== undefined ? opts.autoMerge : autoMergeRepos().includes(repo) });
|
|
503
504
|
if (r.error) return r;
|
|
@@ -514,7 +515,7 @@ function approveRepo(repo, opts = {}) {
|
|
|
514
515
|
// approved with the changes, as Approve does, so they don't go out again.
|
|
515
516
|
function approveChanges(repo, opts = {}) {
|
|
516
517
|
if (loadTasks().some((x) => x.repo === repo && x.review && !x.done && !x.archived)) return approveRepo(repo, opts);
|
|
517
|
-
const path =
|
|
518
|
+
const path = laneMap()[repo]; if (!path) return { error: "Repo not found: " + (repo || "(no repo)") };
|
|
518
519
|
const busy = stillWorking(repo, path); if (busy) return busy;
|
|
519
520
|
if (!workingChanges(path).files.length) return { error: "No uncommitted changes in " + repo + "." };
|
|
520
521
|
const ids = new Set(Array.isArray(opts.tick) ? opts.tick.map(String) : []);
|
|
@@ -532,7 +533,7 @@ function approveChanges(repo, opts = {}) {
|
|
|
532
533
|
function sendBack(id) {
|
|
533
534
|
const t = loadTasks(); const it = t.find((x) => x.id === id); if (!it) return { error: "not found" };
|
|
534
535
|
it.review = false; delete it.reviewAt; saveTasks(t);
|
|
535
|
-
const path = it.repo &&
|
|
536
|
+
const path = it.repo && laneMap()[it.repo];
|
|
536
537
|
if (path) { const f = join(path, ".symbiot", "TASKS.md"); try { writeFileSync(f, readFileSync(f, "utf8").split("\n").map((l) => /^\s*-\s*\[x\]/i.test(l) && sameTask(l.replace(/^\s*-\s*\[x\]\s*/i, ""), it.text) ? l.replace(/\[x\]/i, "[ ]") : l).join("\n")); } catch {} }
|
|
537
538
|
return it;
|
|
538
539
|
}
|
|
@@ -562,13 +563,14 @@ function buildTasksMd(name, ctx, list) {
|
|
|
562
563
|
if (ctx.open && ctx.open.length) { L.push("- **Open markers (TODO/FIXME + uncommitted):**"); for (const o of ctx.open) L.push(` - ${o}`); }
|
|
563
564
|
if (ctx.drift && ctx.drift.length) { L.push("- **Current drift / risk:**"); for (const d of ctx.drift) L.push(` - ${d}`); }
|
|
564
565
|
if (ctx.connectors) L.push(`- **Connectors:** ${ctx.connectors}`);
|
|
566
|
+
if (ctx.about && ctx.about.length) L.push("", ...ctx.about);
|
|
565
567
|
L.push("", "## Tasks");
|
|
566
568
|
const byType = {}; for (const t of list) { const ty = taskType(t.text); (byType[ty] = byType[ty] || []).push(t); }
|
|
567
569
|
const keys = Object.keys(byType).sort((a, b) => TASK_ORDER.indexOf(a) - TASK_ORDER.indexOf(b));
|
|
568
570
|
for (const ty of keys) { L.push(`### ${ty}`); for (const t of byType[ty]) L.push(`- [ ] ${t.text}`); L.push(""); }
|
|
569
571
|
L.push("## When you finish an item", "- Tick it here (`- [x]`) as soon as it's done — that's how it reaches review. Ticking doesn't archive it: the user approves it in Symbiot, which commits it on a branch and opens a PR.", "- Leave your changes **uncommitted**, and don't tick anything you didn't finish or couldn't verify.", "- If the user says to drop an item (in ANSWERS.md, say), delete its line here: Symbiot closes it, so it isn't sent again. Don't delete one for any other reason.", "");
|
|
570
572
|
L.push(...handoverRules(ctx.lanes || [], name), ...HANDBACK);
|
|
571
|
-
L.push("## If you need a decision, or have ideas", "You may be running unattended, so you can't ask in chat. Write `.symbiot/QUESTIONS.md` instead: Symbiot shows it to the user on your block in its
|
|
573
|
+
L.push("## If you need a decision, or have ideas", "You may be running unattended, so you can't ask in chat. Write `.symbiot/QUESTIONS.md` instead: Symbiot shows it to the user on your block in its Workdesk, and their answers come back in `.symbiot/ANSWERS.md` (read that first if it exists).",
|
|
572
574
|
`- At most ${QUESTIONS_MAX} questions, under a \`## Questions\` heading. Each is a \`### \` heading, then a line of context, then exactly ${OPTIONS_SHOWN} options as \`- \` bullets, the one you recommend first, marked \`(recommended)\`. Symbiot shows only the first ${OPTIONS_SHOWN}; the user can always answer in their own words.`,
|
|
573
575
|
"- Judge the options before you ask. Most people pick the recommended option without weighing the other, and Symbiot works for a whole company (developers, sales, everyone), not one person, so the choice is really yours. Both options must be good routes to the best solution, never filler or one you wouldn't take. Each says in plain words, with no jargon, what it does and what it changes from then on for the project, the people working on it and the company. Recommend the one that's best for, in this order, the company, the people doing the work, then the task's goal. Base that on evidence you can check here (git history, tests, logs, how it's used, the answers so far), not on what's quickest, and give that evidence in the context line in a sentence.",
|
|
574
576
|
ONLY_YOU,
|
|
@@ -582,7 +584,7 @@ function buildTasksMd(name, ctx, list) {
|
|
|
582
584
|
return L.join("\n") + "\n";
|
|
583
585
|
}
|
|
584
586
|
function pushTasks(filter) {
|
|
585
|
-
const byName =
|
|
587
|
+
const byName = laneMap(); // from the already-scanned map when there is one, and Marketing (marketing.mjs)
|
|
586
588
|
// what an approved Drop/Merge named, or an agent deleted from its brief, never goes out again
|
|
587
589
|
const all = loadTasks(); let changed = applyRemovals(all) + applyDrops(all, byName);
|
|
588
590
|
// what an agent ticked since the Tasks tab last looked goes to review first, as
|
|
@@ -603,7 +605,7 @@ function pushTasks(filter) {
|
|
|
603
605
|
const written = [], unresolved = [], connectors = connectorsLine(); // the same for every repo's run
|
|
604
606
|
for (const name of Object.keys(groups)) {
|
|
605
607
|
const list = uniqueTasks(groups[name].map((t) => t.text)).map((text) => ({ text })), path = name && byName[name]; // near-duplicates once
|
|
606
|
-
if (!path) { unresolved.push({ name: name || "(no repo)", count: list.length }); continue; }
|
|
608
|
+
if (!path || (path === MARKETING_DIR && !ensureMarketing(path))) { unresolved.push({ name: name || "(no repo)", count: list.length }); continue; }
|
|
607
609
|
try {
|
|
608
610
|
// Cheap, per-repo signals only — no full drift scan (that can be very slow
|
|
609
611
|
// on big repos and would block the request).
|
|
@@ -619,7 +621,9 @@ function pushTasks(filter) {
|
|
|
619
621
|
if (st.behind) risk.push(`${st.behind} behind upstream on ${st.branch}`);
|
|
620
622
|
if (st.dirty && !st.stale) risk.push(`${st.dirty} uncommitted (${st.mod} mod / ${st.del} del / ${st.add} new)`);
|
|
621
623
|
// held: an agent is still running there, so it lands when that one exits
|
|
622
|
-
|
|
624
|
+
// Marketing's brief says what the lane is, the products its tasks name and where they are
|
|
625
|
+
const about = path === MARKETING_DIR ? marketingBrief(list, { map: byName }) : [];
|
|
626
|
+
const held = writeTasks(path, buildTasksMd(name, { branch: st.branch, commits, open, drift: risk, stack, lanes: Object.keys(byName), connectors, about }, list));
|
|
623
627
|
written.push({ name, file: join(path, ".symbiot", "TASKS.md"), path, count: list.length, held });
|
|
624
628
|
} catch (e) { unresolved.push({ name, count: list.length, error: String((e && e.message) || e) }); }
|
|
625
629
|
}
|