symbiot 0.63.7 → 0.63.8

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,11 @@
2
2
 
3
3
  What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
4
4
 
5
+ ## 0.63.8
6
+
7
+ - **No more file names on permission questions, anywhere.** When an agent asked for a permission, Home said it in plain words with an Allow button, but the same question on the Workdesk, on a project's card and on your phone still read "allow the list in .symbiot/allowlist.proposed.json". Every screen now says what the agent wants to do ("connect to the computer at 13.51.104.186") with Allow.
8
+ - **Allow sticks.** Saying Allow along with a few words of your own that sounded like git ("check the deployed commit, git status and served files") was taken for a git chore and handed back to the agent, which can't give itself permission, so it asked you again. A permission is now always turned on as one.
9
+
5
10
  ## 0.63.7 — 2026-10-12
6
11
 
7
12
  - **`symbiot capture Symbiot` works in one go.** It used to refuse, because your running Symbiot shows your real work, and needed a copy started by hand. It now starts a throwaway Symbiot with a made-up company in it (Acme: its repos, its agents' questions, what arrived, its week, a Marketing draft), captures Home, the Workdesk, the Dashboard, the Week, the Map and Marketing, then stops it and deletes it. Symbiot's own tray can be brought up to date after every release.
package/agents.mjs CHANGED
@@ -12,6 +12,7 @@ import { randomBytes, createHash } from "node:crypto";
12
12
  import { CONFIG_DIR, loadConfig, saveConfig, loadTasks, TASK_MAX, clipWords, sameTask, taskWords, sh, hasCmd } from "./core.mjs";
13
13
  import { parseRun, lastRunText, readRunLog, briefPlan, stepOf, runLine } from "./work.mjs";
14
14
  import { noteDuration, kindOf } from "./estimate.mjs";
15
+ import { plainAsk } from "./plain.mjs";
15
16
  import { parseFacts, leftToYou, gitWork, agentsGitWork, agentsPosting, linkedAsks, linkedChore, PUSH_OK } from "./handover.mjs";
16
17
  import { scanRoots, repoPathMap } from "./scan.mjs";
17
18
  import { knowledgeFolders } from "./knowledge.mjs";
@@ -1079,6 +1080,13 @@ function skipIdea(path, text) {
1079
1080
  }
1080
1081
  return { ok: true };
1081
1082
  }
1083
+ // A question as every screen shows it (plain.mjs): `shown`, its words with no file in them,
1084
+ // and `labels`, its choices the same way ("Allow" for an allow list it proposed). q and
1085
+ // options stay as the agent wrote them: an answer is saved under the agent's own words.
1086
+ function plainly(path, x) {
1087
+ let p = {}; try { p = plainAsk(path, x, { max: x.options.length }); } catch {}
1088
+ return { ...x, ...(p.raw ? { shown: p.q } : {}), ...(p.labels ? { labels: p.labels } : {}) };
1089
+ }
1082
1090
  function agentQuestions(path, repo) {
1083
1091
  const p = parseQuestions(readSymbiot(path, "QUESTIONS.md"));
1084
1092
  const done = new Set([...readSymbiot(path, "ANSWERS.md").matchAll(/^###\s+(.+)$/gm)].map((m) => qKey(m[1])));
@@ -1087,7 +1095,7 @@ function agentQuestions(path, repo) {
1087
1095
  const ideas = p.suggestions.map(suggestionTarget).filter(({ text }) => !skipped.some((s) => sameTask(s, text)))
1088
1096
  .map(({ repo: to, text }) => ({ text, repo: to || repo, other: !!to && to !== repo, added: tasks.some((t) => t.repo === (to || repo) && sameTask(t.text, text)) })).filter((s) => !s.added);
1089
1097
  return {
1090
- questions: open.slice(0, QUESTIONS_MAX).map((x) => ({ ...x, options: x.options.slice(0, OPTIONS_SHOWN) })), answered: p.questions.length - open.length,
1098
+ questions: open.slice(0, QUESTIONS_MAX).map((x) => plainly(path, { ...x, options: x.options.slice(0, OPTIONS_SHOWN) })), answered: p.questions.length - open.length,
1091
1099
  suggestions: ideas, ideasShown: IDEAS_SHOWN,
1092
1100
  };
1093
1101
  }
package/handover.mjs CHANGED
@@ -66,7 +66,11 @@ function parseAwaiting(text) {
66
66
  // is still theirs.
67
67
  const GIT_WORK = /\bgit\s+(?:add|commit|push|pull|fetch|merge|rebase|stash|restore|checkout|switch)\b|\b(?:stage|commit|push|pull)(?:es|ed|ing)?\b(?:[^.;:]{0,60})\b(?:changes?|diff|fix|files?|branch|main|master|origin|upstream|remote|commits?|them|it)\b/i;
68
68
  const IDENTITY = /\bpassword\b|\b2fa\b|\btwo-factor\b|\b(?:api )?token\b|\bsign(?:ing)? in\b|\blog ?in\b|\bssh key\b|\bpassphrase\b|\bsudo\b/i;
69
- const gitWork = (text) => { const t = String(text || ""); return GIT_WORK.test(t) && !IDENTITY.test(t); };
69
+ // A permission is never git work, whatever its words: "allow the list … to check the
70
+ // deployed commit, git status and served files" was taken for a commit, and handed back to
71
+ // an agent that can't give itself permission, which asked again (WinLove, 2026-10-12).
72
+ const PERMISSION = /allowlist\.proposed|only you: a permission/i;
73
+ const gitWork = (text) => { const t = String(text || ""); return GIT_WORK.test(t) && !IDENTITY.test(t) && !PERMISSION.test(t); };
70
74
  const PUSH_OK = "asking for your OK on the Workdesk, with the files, before it pushes";
71
75
  // A "👤 You: …" option that's local git work, as the agent's: "🤖 Agent: … (asking for
72
76
  // your OK … before it pushes)". Dropped instead when another option already has the
package/home.mjs CHANGED
@@ -10,6 +10,7 @@
10
10
  // how you use them; this is the live data around them. Home's talk goes to the
11
11
  // same Symbiot as every chat (mind.mjs converse), told what home shows.
12
12
  import { claudeSetup } from "./claudesetup.mjs";
13
+ import { plainAsk } from "./plain.mjs";
13
14
  import { join } from "node:path";
14
15
  import { homedir } from "node:os";
15
16
  import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
@@ -409,48 +410,8 @@ function noteAnswer(path, heading, text) {
409
410
  const f = join(path, ".symbiot", "ANSWERS.md"); let had = ""; try { had = readFileSync(f, "utf8"); } catch {}
410
411
  try { writeFileSync(f, `${(had || "# Answers\n").replace(/\s*$/, "")}\n\n### ${String(heading).replace(/\s+/g, " ").trim()}\n${text}\n_answered ${new Date().toISOString().slice(0, 10)}_\n`); } catch {}
411
412
  }
412
- // An ask in words for someone who doesn't code: a permission request lists what the agent
413
- // wants to run, with Allow / Don't allow; any file path left in the question or choices
414
- // becomes "a file". The answer sent back is still the choice's own text (labels only change what shows).
415
- const PATHISH = /`?(?:~|\.{0,2})?\/?(?:[\w.-]+\/)+[\w.-]+\.(?:json|md|mjs|js|env|txt|sh)`?|`?\b[\w-]+\.(?:json|mjs)\b`?/g;
416
- // What a rule lets an agent do, said the way you'd say it to someone who doesn't code.
417
- const base = (p) => String(p || "").replace(/[/*]+$/, "").split("/").filter(Boolean).pop() || "your files";
418
- function PLAIN_RULE(r) {
419
- const m = String(r).match(/^(\w+)\((.*)\)$/); if (!m) return "work in " + base(r);
420
- const [, tool, arg] = m, a = arg.replace(/:?\*+$/, "").trim(), first = a.split(/\s+/)[0].replace(/:\*?$/, "").split("/").pop();
421
- if (tool === "Read") return `read files in ${base(a)}`;
422
- if (/^(Edit|Write|MultiEdit)$/.test(tool)) return `change files in ${base(a)}`;
423
- if (tool !== "Bash") return `use ${tool}`;
424
- const host = (a.match(/@([\w.-]+)/) || [])[1];
425
- if (/^(ssh|scp|rsync)$/.test(first)) return host ? `connect to the computer at ${host}` : "connect to another computer";
426
- if (first === "adb") return "use your phone over the USB cable";
427
- if (/^(flutter|gradle|gradlew|godot|xcodebuild)$/.test(first) || /\bbuild\b/.test(a)) return "build the app";
428
- if (first === "ffmpeg" || first === "ffprobe") return "make and trim video clips";
429
- if (first === "git" && /\bpush\b/.test(a)) return "send code to GitHub";
430
- if (/^(npm|yarn|pnpm)$/.test(first) && /\bpublish\b/.test(a)) return "publish a new version";
431
- if (/^(bash|sh|zsh)$/.test(first)) return "run a script it wrote";
432
- if (/^(python\d*|node)$/.test(first)) return "run code in this project";
433
- if (/^(rm|shred)$/.test(first)) return "delete files";
434
- if (first === "sudo") return "make changes as administrator";
435
- return `run ${first}`;
436
- }
437
413
  // a label without its "(recommended)": a second button isn't the recommended one
438
414
  const blobLabel = (l) => String(l || "").replace(/\s*\(recommended\)\s*$/i, "");
439
- function plainAsk(path, q) {
440
- const text = String(q.q || ""), opts = (q.options || []).slice(0, 2);
441
- let ask = text, labels = opts.map((o) => o.replace(PATHISH, "a file"));
442
- if (opts.some((o) => /allowlist\.proposed/i.test(o))) {
443
- let rules = []; try { const p = JSON.parse(readFileSync(join(path, ".symbiot", "allowlist.proposed.json"), "utf8")).permissions || {}; rules = [...new Set([...(p.allow || []).map(PLAIN_RULE), ...(p.additionalDirectories || []).map((d) => `work in ${base(d)}`)])]; } catch {}
444
- ask = `${text.replace(PATHISH, "a file")}${rules.length ? ` It wants to ${rules.slice(0, 6).join(", ")}${rules.length > 6 ? `, and ${rules.length - 6} more things` : ""}.` : ""} If you say no, it skips that part and tells you what it left out.`;
445
- labels = opts.map((o) => (/allowlist\.proposed/i.test(o) ? "Allow" + (/recommended/i.test(o) ? " (recommended)" : "") : o.replace(PATHISH, "a file")));
446
- }
447
- // raw: the question as the agent wrote it, the key its answer is saved under (agents.mjs
448
- // answerQuestions). Sending the plain words back matched nothing, so a permission card's
449
- // Allow, and Send under it, did nothing at all (whatsapp_module, 2026-10-12).
450
- const shown = ask.replace(PATHISH, "a file");
451
- return { q: shown, ...(shown !== text ? { raw: text } : {}), ...(labels.some((l, i) => l !== opts[i]) ? { labels } : {}) };
452
- }
453
-
454
415
  function homeAnswer(id, { pick, text = "" } = {}, deps = {}) {
455
416
  id = String(id || ""); text = String(text || "").trim().slice(0, 2000);
456
417
  const go = text ? !SAYS_NO.test(text) : Number(pick) === 0, ref = id.replace(/^\w+:/, "");
@@ -688,7 +649,7 @@ function workScene({ deps = {}, full = false } = {}) {
688
649
  const line = plain(String(t).split(/\n\s*\n/)[0], 220);
689
650
  if (line) { p.summary = line; p.state = a.status === "running" ? "at work" : qs.length ? "asks you" : "done"; p.path = a.path; }
690
651
  }
691
- for (const q of qs) { p.qs = p.qs || []; if (p.qs.length < 3) p.qs.push({ q: q.q, options: (q.options || []).slice(0, 3), path: a.path, ...signInCard([q.q, ...(q.options || []).slice(0, 3)]) }); }
652
+ for (const q of qs) { p.qs = p.qs || []; if (p.qs.length < 3) p.qs.push({ q: q.shown || q.q, ...(q.shown ? { raw: q.q } : {}), options: (q.options || []).slice(0, 3), ...(q.labels ? { labels: q.labels } : {}), path: a.path, ...signInCard([q.q, ...(q.options || []).slice(0, 3)]) }); }
692
653
  for (const s of ideas) { p.ideas = p.ideas || []; if (p.ideas.length < 4) p.ideas.push({ text: plain(s.text, 200), full: String(s.text), repo: s.repo || l, path: a.path }); }
693
654
  }
694
655
  // what each lane handed over and hasn't heard back on, with where it stands and how far the run on it has got
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.63.7",
3
+ "version": "0.63.8",
4
4
  "description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice \u2014 Claude, OpenAI, Gemini, or a local model.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,6 +46,7 @@
46
46
  "away.mjs",
47
47
  "marketing.mjs",
48
48
  "tray.mjs",
49
+ "plain.mjs",
49
50
  "capture.mjs",
50
51
  "demo.mjs",
51
52
  "postinstall.mjs",
package/phone.mjs CHANGED
@@ -283,7 +283,7 @@ async function snapshot({ now = Date.now(), fresh = false } = {}) {
283
283
  const asks = [];
284
284
  for (const y of (await tryOr(() => agents.agentsList(), [])).filter((a, i, all) => all.findIndex((b) => b.path === a.path) === i)) {
285
285
  const qs = await tryOr(() => agents.agentQuestions(y.path, "").questions, []);
286
- if (qs.length) asks.push({ id: askId(y.path), path: y.path, name: plainText(String(y.name || "an agent").replace(/^Agent:\s*/, ""), 60), questions: qs.slice(0, 5).map((q) => ({ q: plainText(q.q, 300), options: (q.options || []).slice(0, 2).map((o) => plainText(o, 200)) })) });
286
+ if (qs.length) asks.push({ id: askId(y.path), path: y.path, name: plainText(String(y.name || "an agent").replace(/^Agent:\s*/, ""), 60), questions: qs.slice(0, 5).map((q) => ({ q: plainText(q.q, 300), ...(q.shown ? { shown: plainText(q.shown, 400) } : {}), options: (q.options || []).slice(0, 2).map((o) => plainText(o, 200)), ...(q.labels ? { labels: q.labels.slice(0, 2).map((o) => plainText(o, 200)) } : {}) })) });
287
287
  }
288
288
  const approves = (await tryOr(() => tasksMod.pendingReview(), [])).filter((r) => r.path && !r.running && ((r.tasks || []).length || (r.files || []).length))
289
289
  .map((r) => ({ repo: r.repo, tasks: (r.tasks || []).map((t) => ({ id: t.id, text: plainText(t.text, 200) })), files: (r.files || []).length, stat: plainText(r.stat, 120), ...(r.untasked ? { untasked: true } : {}), sum: approveSum(r) }));
package/plain.mjs ADDED
@@ -0,0 +1,48 @@
1
+ // A question an agent left, in words for someone who doesn't code (plainAsk): Home,
2
+ // the Workdesk's agent blocks, a project's card and the phone all show it this way, so
3
+ // "allow the list in .symbiot/allowlist.proposed.json" never reaches a screen. It was
4
+ // only Home's list: the same ask on the Workdesk still read as a file (2026-10-12).
5
+ import { readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+
8
+ // An ask in words for someone who doesn't code: a permission request lists what the agent
9
+ // wants to run, with Allow / Don't allow; any file path left in the question or choices
10
+ // becomes "a file". The answer sent back is still the choice's own text (labels only change what shows).
11
+ const PATHISH = /`?(?:~|\.{0,2})?\/?(?:[\w.-]+\/)+[\w.-]+\.(?:json|md|mjs|js|env|txt|sh)`?|`?\b[\w-]+\.(?:json|mjs)\b`?/g;
12
+ // What a rule lets an agent do, said the way you'd say it to someone who doesn't code.
13
+ const base = (p) => String(p || "").replace(/[/*]+$/, "").split("/").filter(Boolean).pop() || "your files";
14
+ function PLAIN_RULE(r) {
15
+ const m = String(r).match(/^(\w+)\((.*)\)$/); if (!m) return "work in " + base(r);
16
+ const [, tool, arg] = m, a = arg.replace(/:?\*+$/, "").trim(), first = a.split(/\s+/)[0].replace(/:\*?$/, "").split("/").pop();
17
+ if (tool === "Read") return `read files in ${base(a)}`;
18
+ if (/^(Edit|Write|MultiEdit)$/.test(tool)) return `change files in ${base(a)}`;
19
+ if (tool !== "Bash") return `use ${tool}`;
20
+ const host = (a.match(/@([\w.-]+)/) || [])[1];
21
+ if (/^(ssh|scp|rsync)$/.test(first)) return host ? `connect to the computer at ${host}` : "connect to another computer";
22
+ if (first === "adb") return "use your phone over the USB cable";
23
+ if (/^(flutter|gradle|gradlew|godot|xcodebuild)$/.test(first) || /\bbuild\b/.test(a)) return "build the app";
24
+ if (first === "ffmpeg" || first === "ffprobe") return "make and trim video clips";
25
+ if (first === "git" && /\bpush\b/.test(a)) return "send code to GitHub";
26
+ if (/^(npm|yarn|pnpm)$/.test(first) && /\bpublish\b/.test(a)) return "publish a new version";
27
+ if (/^(bash|sh|zsh)$/.test(first)) return "run a script it wrote";
28
+ if (/^(python\d*|node)$/.test(first)) return "run code in this project";
29
+ if (/^(rm|shred)$/.test(first)) return "delete files";
30
+ if (first === "sudo") return "make changes as administrator";
31
+ return `run ${first}`;
32
+ }
33
+ function plainAsk(path, q, { max = 2 } = {}) {
34
+ const text = String(q.q || ""), opts = (q.options || []).slice(0, max);
35
+ let ask = text, labels = opts.map((o) => o.replace(PATHISH, "a file"));
36
+ if (opts.some((o) => /allowlist\.proposed/i.test(o))) {
37
+ let rules = []; try { const p = JSON.parse(readFileSync(join(path, ".symbiot", "allowlist.proposed.json"), "utf8")).permissions || {}; rules = [...new Set([...(p.allow || []).map(PLAIN_RULE), ...(p.additionalDirectories || []).map((d) => `work in ${base(d)}`)])]; } catch {}
38
+ ask = `${text.replace(PATHISH, "a file")}${rules.length ? ` It wants to ${rules.slice(0, 6).join(", ")}${rules.length > 6 ? `, and ${rules.length - 6} more things` : ""}.` : ""} If you say no, it skips that part and tells you what it left out.`;
39
+ labels = opts.map((o) => (/allowlist\.proposed/i.test(o) ? "Allow" + (/recommended/i.test(o) ? " (recommended)" : "") : o.replace(PATHISH, "a file")));
40
+ }
41
+ // raw: the question as the agent wrote it, the key its answer is saved under (agents.mjs
42
+ // answerQuestions). Sending the plain words back matched nothing, so a permission card's
43
+ // Allow, and Send under it, did nothing at all (whatsapp_module, 2026-10-12).
44
+ const shown = ask.replace(PATHISH, "a file");
45
+ return { q: shown, ...(shown !== text ? { raw: text } : {}), ...(labels.some((l, i) => l !== opts[i]) ? { labels } : {}) };
46
+ }
47
+
48
+ export { PATHISH, PLAIN_RULE, plainAsk };
package/ui.mjs CHANGED
@@ -1890,8 +1890,8 @@ function relHtml(rl){return "<div class='qrel"+(rl.waiting?" wait":"")+"'>Needs
1890
1890
  function askHtml(a){var k=a.ask;if(!k)return '';var qs=k.questions||[],ss=k.suggestions||[];if(!qs.length&&!ss.length)return '';
1891
1891
  var h="<div class='aq' data-id='"+esc(a.id)+"'>";
1892
1892
  if(qs.length){h+="<h4><i class=ic-ask></i> "+qs.length+" question"+(qs.length>1?"s":"")+" for you</h4>";
1893
- qs.forEach(function(q,i){var rl=q.release;h+="<div class='q' data-i='"+i+"'><div class='qt'>"+esc(q.q)+"</div>"+(q.context?"<div class='qc'>"+esc(q.context)+"</div>":"")+(rl?relHtml(rl):"");
1894
- (q.options||[]).forEach(function(o,j){var off=rl&&rl.waiting&&DONEOPT.test(o);h+="<label class='opt"+(off?" off":"")+"'><input type='radio' name='q_"+esc(a.id)+"_"+i+"' value='"+j+"'"+(off?" disabled":"")+"><span>"+whoHtml(o)+(off?" <i>(once "+esc(rl.name+" "+rl.needs)+" is installed)</i>":"")+"</span></label>";});
1893
+ qs.forEach(function(q,i){var rl=q.release;h+="<div class='q' data-i='"+i+"'><div class='qt'>"+esc(q.shown||q.q)+"</div>"+(q.context?"<div class='qc'>"+esc(q.context)+"</div>":"")+(rl?relHtml(rl):"");
1894
+ (q.options||[]).forEach(function(o,j){var off=rl&&rl.waiting&&DONEOPT.test(o);h+="<label class='opt"+(off?" off":"")+"'><input type='radio' name='q_"+esc(a.id)+"_"+i+"' value='"+j+"'"+(off?" disabled":"")+"><span>"+whoHtml((q.labels&&q.labels[j])||o)+(off?" <i>(once "+esc(rl.name+" "+rl.needs)+" is installed)</i>":"")+"</span></label>";});
1895
1895
  h+="<input class='qother' placeholder='"+((q.options&&q.options.length)?"or answer in your own words":"your answer")+"'></div>";});
1896
1896
  h+="<div class='row'><button class='act qsend' title='save the answers and hand the repo back to your agent'>Send answers &amp; continue</button><button class='ghost qsave' title='save the answers for the next run'>Save only</button></div>";}
1897
1897
  if(ss.length){var shown=IDEASOPEN[a.path]?ss.length:(k.ideasShown||ss.length);h+="<h4><i class=ic-idea></i> Ideas from the agent</h4>";ss.forEach(function(s,i){if(i>=shown)return;h+="<div class='idea'><span style='flex:1'>"+esc(s.text)+(s.other?" <span class='tag' title='this idea is for another project, so + task adds it to that one'>for "+esc(s.repo)+"</span>":"")+"</span>"+(s.added?"<span class='tag'>in Tasks</span>":"<button class='ghost qidea' data-i='"+i+"' style='padding:3px 9px;font-size:12px'>+ task</button><button class='ghost qskip' data-i='"+i+"' title='turn this idea down: the next one moves up, and the agent is told not to suggest it again' style='padding:3px 9px;font-size:12px'>Skip</button>")+"</div>";});
@@ -2175,8 +2175,8 @@ var h='',W="<span class='pcwait'> waiting to send</span>";
2175
2175
  (c.notes||[]).forEach(function(n){h+="<div class='task' data-note='"+escQ(n.id)+"'><span class='t err'>"+esc(n.error)+(n.what?" <span class='muted'>("+esc(n.what)+")</span>":"")+"</span><button class='ghost pcnote'>OK</button></div>";});
2176
2176
  var asks=c.asks||[],aps=c.approves||[],needs=c.needs||[];
2177
2177
  if(asks.length||aps.length||needs.length){h+="<div class='mkneeds lit'><div class='pshead'><span>Needs you &middot; "+(asks.length+aps.length+needs.length)+"</span><small>on "+esc(d.name)+"</small></div>";
2178
- asks.forEach(function(a,i){var q=(a.questions||[])[0]||{};h+="<div class='pcq' data-ask='"+i+"'><div class='qn'>"+esc(a.name)+" asks"+((a.questions||[]).length>1?" &middot; 1 of "+a.questions.length:"")+"</div><div class='qq'>"+esc(q.q||'')+"</div>"+
2179
- (a.waiting?"<div class='note' style='margin:0'>You answered: "+esc((a.answered||[])[0]||'')+W+"</div>":"<div class='opts'>"+(q.options||[]).map(function(o,j){return "<button class='"+(j?'ghost':'act')+" pcopt' data-j='"+j+"'>"+esc(o)+"</button>";}).join('')+"</div><form class='pcown'><input placeholder='Or say it in your own words' maxlength='2000'><button class='ghost' type='submit'>Send</button></form>")+"</div>";});
2178
+ asks.forEach(function(a,i){var q=(a.questions||[])[0]||{};h+="<div class='pcq' data-ask='"+i+"'><div class='qn'>"+esc(a.name)+" asks"+((a.questions||[]).length>1?" &middot; 1 of "+a.questions.length:"")+"</div><div class='qq'>"+esc(q.shown||q.q||'')+"</div>"+
2179
+ (a.waiting?"<div class='note' style='margin:0'>You answered: "+esc((a.answered||[])[0]||'')+W+"</div>":"<div class='opts'>"+(q.options||[]).map(function(o,j){return "<button class='"+(j?'ghost':'act')+" pcopt' data-j='"+j+"'>"+esc((q.labels&&q.labels[j])||o)+"</button>";}).join('')+"</div><form class='pcown'><input placeholder='Or say it in your own words' maxlength='2000'><button class='ghost' type='submit'>Send</button></form>")+"</div>";});
2180
2180
  aps.forEach(function(a,i){h+="<div class='pcq' data-ap='"+i+"'><div class='qn'>"+esc(a.repo)+" &middot; waiting for your Approve</div><div class='qq'>"+(a.tasks.length?a.tasks.length+" task"+(a.tasks.length>1?"s":"")+" done":"Changes without a task")+" &middot; <span class='muted'>"+esc(a.stat||(a.files+" file"+(a.files===1?"":"s")+" changed"))+"</span></div>"+(a.tasks.length?"<ul>"+a.tasks.map(function(t){return "<li>"+esc(t.text)+"</li>";}).join('')+"</ul>":"")+
2181
2181
  (a.waiting?"<div class='note' style='margin:0'>Approved here"+W+"</div>":"<div class='row' style='margin:0'><button class='act pcapprove'>Approve</button><span class='muted' style='font-size:12px'>exactly this: if the work changes before your computer gets it, it isn&rsquo;t approved</span></div>")+"</div>";});
2182
2182
  needs.forEach(function(n){h+="<div class='task'><span class='t'>"+esc(n.title)+" <span class='muted' style='font-size:12px'>"+esc(n.sub)+" &middot; on your computer</span></span></div>";});
@@ -3118,13 +3118,13 @@ if(!ps.length){el.innerHTML='';return;}
3118
3118
  el.innerHTML=ps.map(function(p,i){var nm=p.name||p.repo,q=(p.qs||[])[0],st=p.parked?'parked':(p.qs&&p.qs.length)?'asks you':p.ready?'ready for your OK':p.state||'';
3119
3119
  var h="<section class='rly"+((p.qs&&p.qs.length)||p.ready?' lit':'')+"' data-i='"+i+"'><div class='rlh'><b>"+esc(nm)+"</b>"+(st?"<span class='rls'>"+esc(st)+"</span>":"")+"</div>";
3120
3120
  if(p.summary)h+="<p class='rlsum' title='"+escQ(p.summary)+"'>"+esc(p.summary)+"</p>";
3121
- if(q)h+="<div class='lqblob rlask'>"+blobBody({kind:'ask',name:nm,repo:p.repo,q:q.q,options:q.options,path:q.path,fix:q.fix,id:q.id,sub:q.sub,draft:q.draft,signInTo:q.signInTo,more:Math.max(0,(p.asks||p.qs.length)-1)},false)+"</div>";
3121
+ if(q)h+="<div class='lqblob rlask'>"+blobBody({kind:'ask',name:nm,repo:p.repo,q:q.q,raw:q.raw,labels:q.labels,options:q.options,path:q.path,fix:q.fix,id:q.id,sub:q.sub,draft:q.draft,signInTo:q.signInTo,more:Math.max(0,(p.asks||p.qs.length)-1)},false)+"</div>";
3122
3122
  if(p.ready)h+="<button type='button' class='act rlok'>Review and approve</button>";
3123
3123
  if(p.handing&&p.handing.length)h+="<div class='rlk'>Handed over</div>"+p.handing.map(function(x){var pg=handProg(x);return "<p class='rlsum rlhand' title='"+escQ(x.text)+"'><b>"+esc(x.lane)+"</b>: "+esc(x.why)+(pg?" \u00b7 "+esc(pg):"")+"</p>";}).join('');
3124
3124
  if(p.ideas&&p.ideas.length)h+="<div class='rlk'>Extra tasks it suggests</div>"+p.ideas.map(function(d,j){return "<div class='rli' data-j='"+j+"'><span title='"+escQ(d.full)+"'>"+esc(d.text)+"</span><span class='rlia'><button type='button' class='rladd'>+ task</button><button type='button' class='rlskip'>Skip</button></span></div>";}).join('');
3125
3125
  return h+"<div class='rlf'><button type='button' class='rlopen'>Its tasks &rsaquo;</button><button type='button' class='rlag'>Its agents &rsaquo;</button></div></section>";}).join('');
3126
3126
  el.querySelectorAll('.rly').forEach(function(box){var p=ps[+box.getAttribute('data-i')],q=(p.qs||[])[0],nm=p.name||p.repo;
3127
- var ab=box.querySelector('.rlask');if(ab&&q)wireBlob(ab,{kind:'ask',name:nm,repo:p.repo,q:q.q,options:q.options,path:q.path,fix:q.fix,id:q.id},{done:function(){LQ.relaySig='';lqLoadWork();}});
3127
+ var ab=box.querySelector('.rlask');if(ab&&q)wireBlob(ab,{kind:'ask',name:nm,repo:p.repo,q:q.q,raw:q.raw,labels:q.labels,options:q.options,path:q.path,fix:q.fix,id:q.id},{done:function(){LQ.relaySig='';lqLoadWork();}});
3128
3128
  var ok=box.querySelector('.rlok');if(ok)ok.addEventListener('click',function(){lqOpen({kind:'proj',repo:p.repo,lit:true});});
3129
3129
  box.querySelector('.rlopen').addEventListener('click',function(){lqOpen({kind:'proj',repo:p.repo});});
3130
3130
  box.querySelector('.rlag').addEventListener('click',function(){lqPool('agents');});