toolroll 0.9.84 → 0.9.85

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +94 -1193
  3. package/dist/agent-fence.d.ts +3 -3
  4. package/dist/agent-fence.js +3 -3
  5. package/dist/agent-onboard.d.ts +2 -0
  6. package/dist/agent-onboard.js +3 -0
  7. package/dist/browser/workspace.css +1 -1
  8. package/dist/browser/workspace.js +31 -31
  9. package/dist/browser-workspace.d.ts +5 -0
  10. package/dist/chat-channel.d.ts +10 -1
  11. package/dist/chat-channel.js +35 -5
  12. package/dist/chat-decide.d.ts +16 -4
  13. package/dist/chat-decide.js +239 -31
  14. package/dist/chat-messages.d.ts +5 -0
  15. package/dist/chat-messages.js +1 -1
  16. package/dist/chat-quiet.js +3 -2
  17. package/dist/chat-review.d.ts +4 -0
  18. package/dist/chat-review.js +13 -4
  19. package/dist/chat-terms.d.ts +9 -0
  20. package/dist/chat-terms.js +46 -0
  21. package/dist/contracts/console-api.d.ts +2 -1
  22. package/dist/contracts/console-api.js +2 -1
  23. package/dist/contracts/lead-tools.d.ts +30 -0
  24. package/dist/contracts/lead-tools.js +3 -0
  25. package/dist/contracts/youiyoux-request.d.ts +248 -0
  26. package/dist/contracts/youiyoux-request.js +152 -0
  27. package/dist/flow-cadence.d.ts +8 -2
  28. package/dist/flow-cadence.js +16 -3
  29. package/dist/flow-card-task.d.ts +9 -0
  30. package/dist/flow-card-task.js +15 -6
  31. package/dist/integrations-ui.js +9 -4
  32. package/dist/integrations.d.ts +9 -1
  33. package/dist/integrations.js +43 -2
  34. package/dist/landed-cleanup.d.ts +4 -0
  35. package/dist/landed-cleanup.js +3 -2
  36. package/dist/lead-capabilities.js +2 -1
  37. package/dist/lead-cli.js +3 -1
  38. package/dist/lead-tools.d.ts +2 -0
  39. package/dist/lead-tools.js +30 -4
  40. package/dist/lead.d.ts +2 -0
  41. package/dist/lead.js +3 -3
  42. package/dist/operate.d.ts +1 -2
  43. package/dist/operate.js +11 -15
  44. package/dist/project-activity.d.ts +20 -0
  45. package/dist/project-activity.js +23 -0
  46. package/dist/project-cli.d.ts +1 -0
  47. package/dist/project-cli.js +36 -19
  48. package/dist/project-knowledge.d.ts +3 -3
  49. package/dist/project-outcomes.d.ts +119 -0
  50. package/dist/project-outcomes.js +79 -0
  51. package/dist/result-actions.d.ts +4 -0
  52. package/dist/result-actions.js +41 -9
  53. package/dist/result-review.d.ts +9 -0
  54. package/dist/result-review.js +45 -5
  55. package/dist/server/console.css +12 -0
  56. package/dist/server/pages.js +11 -1
  57. package/dist/server/render-chat.js +3 -2
  58. package/dist/server/render-pages.d.ts +2 -2
  59. package/dist/server/render-pages.js +7 -2
  60. package/dist/server/render-results.d.ts +2 -0
  61. package/dist/server/render-results.js +20 -7
  62. package/dist/server/render-tasks.d.ts +1 -0
  63. package/dist/server/render-tasks.js +15 -5
  64. package/dist/server/route-table.js +1 -0
  65. package/dist/server/settings.js +18 -1
  66. package/dist/server/task-views.js +6 -0
  67. package/dist/server/tasks.js +42 -6
  68. package/dist/store.d.ts +46 -3
  69. package/dist/store.js +165 -6
  70. package/dist/task-source.d.ts +13 -1
  71. package/dist/task-source.js +17 -1
  72. package/dist/telegram-lead.js +7 -5
  73. package/dist/telegram.js +1 -1
  74. package/dist/toolroll-update.js +2 -2
  75. package/dist/webhooks.d.ts +6 -0
  76. package/dist/webhooks.js +25 -0
  77. package/dist/work-index.d.ts +4 -0
  78. package/dist/work-index.js +32 -0
  79. package/dist/youiyoux-intake.d.ts +234 -0
  80. package/dist/youiyoux-intake.js +908 -0
  81. package/package.json +11 -8
@@ -13,26 +13,32 @@ import { type TriggerIo, type TriggerPass } from "./flow-triggers.js";
13
13
  import { type ReplyWatch, type ReplyWatchIo } from "./flow-replies.js";
14
14
  import { type StepIo, type StepPass } from "./flow-steps.js";
15
15
  import type { Store } from "./store.js";
16
+ import { type Git } from "./landed-cleanup.js";
17
+ import { type YouiyouxPass } from "./youiyoux-intake.js";
16
18
  /** How often the worker moves cards, checks triggers, reads replies and runs steps, builds or not. */
17
19
  export declare const FLOW_EVERY_MS = 15000;
18
20
  /** One pass of a kind for a project at a time in this process: a second one asked for while it runs is skipped
19
21
  * (`idle`), and the next cadence takes up whatever it would have done. Cards are also claimed in the database,
20
22
  * so passes in other processes can't act twice either. */
21
23
  export declare function alone<T>(kind: string, project: string, idle: T, run: () => Promise<T>): Promise<T>;
24
+ /** `youiyouxGit`: the hardened git an options request's result is read with (the plane's own runner; gitOf by default). */
22
25
  export type FlowIo = {
23
26
  triggers: TriggerIo;
24
27
  replies: ReplyWatchIo;
25
28
  steps: StepIo;
26
29
  evidenceRoot?: string;
30
+ youiyouxGit?: Git;
27
31
  };
28
32
  export type FlowHousekeeping = {
29
33
  triggers: TriggerPass;
34
+ youiyoux: YouiyouxPass;
30
35
  replies: ReplyWatch;
31
36
  steps: StepPass;
32
37
  flows: FlowAdvance;
33
38
  };
34
- /** One project's flow pass: triggers (which may start cards), replies (which move waiting cards), steps, then moves.
35
- * `halted` (a stop or paused admission) skips everything that reaches outside; moves still run. */
39
+ /** One project's flow pass: triggers (which may start cards), youiyoux requests (which file tasks that wait for a
40
+ * person, and move an options request's options to youiyoux's hub and a decision there back), replies (which move waiting cards), steps, then moves. `halted` (a stop or paused admission) skips
41
+ * everything that reaches outside; moves still run. */
36
42
  export declare function flowHousekeeping(store: Store, repo: string, clock: () => Date, io: FlowIo, halted: () => boolean): Promise<FlowHousekeeping>;
37
43
  /** Move one project's cards. Synchronous, so two can't overlap in a process; each move is checked against the
38
44
  * card's visit in its own transaction, so a pass in another process can't move a card twice. */
@@ -12,6 +12,8 @@ import { advanceFlows } from "./flow-engine.js";
12
12
  import { runFlowTriggers } from "./flow-triggers.js";
13
13
  import { watchFlowReplies } from "./flow-replies.js";
14
14
  import { runFlowSteps } from "./flow-steps.js";
15
+ import { gitOf } from "./landed-cleanup.js";
16
+ import { NO_YOUIYOUX_PASS, runYouiyoux } from "./youiyoux-intake.js";
15
17
  /** How often the worker moves cards, checks triggers, reads replies and runs steps, builds or not. */
16
18
  export const FLOW_EVERY_MS = 15_000;
17
19
  const running = new Set();
@@ -30,11 +32,22 @@ export async function alone(kind, project, idle, run) {
30
32
  running.delete(key);
31
33
  }
32
34
  }
33
- /** One project's flow pass: triggers (which may start cards), replies (which move waiting cards), steps, then moves.
34
- * `halted` (a stop or paused admission) skips everything that reaches outside; moves still run. */
35
+ const NO_REQUESTS = NO_YOUIYOUX_PASS;
36
+ /** One project's flow pass: triggers (which may start cards), youiyoux requests (which file tasks that wait for a
37
+ * person, and move an options request's options to youiyoux's hub and a decision there back), replies (which move waiting cards), steps, then moves. `halted` (a stop or paused admission) skips
38
+ * everything that reaches outside; moves still run. */
35
39
  export async function flowHousekeeping(store, repo, clock, io, halted) {
36
40
  const triggers = halted() ? { added: 0, checked: 0, problems: [] }
37
41
  : await alone("triggers", repo, { added: 0, checked: 0, problems: [] }, () => runFlowTriggers(store, repo, clock(), io.triggers));
42
+ const youiyoux = halted() ? NO_REQUESTS
43
+ : await alone("youiyoux", repo, NO_REQUESTS, async () => {
44
+ try {
45
+ return await runYouiyoux(store, repo, clock(), { dir: io.triggers.dir, env: process.env, git: io.youiyouxGit ?? gitOf(), evidenceRoot: io.evidenceRoot ?? null });
46
+ }
47
+ catch (error) {
48
+ return { ...NO_REQUESTS, problems: [`youiyoux: ${error instanceof Error ? error.message : "could not read requests"}`] };
49
+ }
50
+ });
38
51
  // One mailbox serves every project, so its reads are one at a time across them all.
39
52
  const replies = halted() ? { read: 0, taken: 0, problem: null }
40
53
  : await alone("replies", "*", { read: 0, taken: 0, problem: null }, () => watchFlowReplies(store, clock(), io.replies));
@@ -42,7 +55,7 @@ export async function flowHousekeeping(store, repo, clock, io, halted) {
42
55
  : await alone("steps", repo, { ran: 0, problems: [] }, () => runFlowSteps(store, repo, clock(), io.steps));
43
56
  // Last, so a step that just finished, or a card a trigger just added, moves now.
44
57
  const flows = moveCards(store, repo, clock(), io.evidenceRoot);
45
- return { triggers, replies, steps, flows };
58
+ return { triggers, youiyoux, replies, steps, flows };
46
59
  }
47
60
  /** Move one project's cards. Synchronous, so two can't overlap in a process; each move is checked against the
48
61
  * card's visit in its own transaction, so a pass in another process can't move a card twice. */
@@ -1,2 +1,11 @@
1
1
  import type { Store } from "./store.js";
2
+ import { type TaskStatus } from "./task-status.js";
3
+ /** A task's status as its page reads it, from the store alone (no evidence files); null when it can't be read. */
4
+ /** `root`: where results' saved evidence is. Without it the receipt is read without its saved checks, so a completion
5
+ * (bound to the receipt read with them) doesn't read as one: a status that must say Complete passes it. */
6
+ export declare function storedTaskStatus(store: Store, taskId: string, repo: string, now: Date, root?: string): {
7
+ status: TaskStatus;
8
+ activeTaskId: string;
9
+ live: ReturnType<Store["getRun"]>;
10
+ } | null;
2
11
  export declare function flowCardTaskLine(store: Store, taskId: string, repo: string, now: Date, secrets?: readonly string[]): string;
@@ -19,14 +19,17 @@ function cardReason(text, fallback, secrets) {
19
19
  return fallback;
20
20
  return clean.length > 100 ? `${clean.slice(0, 99).trimEnd()}…` : clean;
21
21
  }
22
- export function flowCardTaskLine(store, taskId, repo, now, secrets = []) {
22
+ /** A task's status as its page reads it, from the store alone (no evidence files); null when it can't be read. */
23
+ /** `root`: where results' saved evidence is. Without it the receipt is read without its saved checks, so a completion
24
+ * (bound to the receipt read with them) doesn't read as one: a status that must say Complete passes it. */
25
+ export function storedTaskStatus(store, taskId, repo, now, root) {
23
26
  const access = { principal: "operator", repos: [repo], dependencyRepos: [repo] };
24
- const assignment = assignmentOf(store, taskId, now, access);
27
+ const assignment = assignmentOf(store, taskId, now, access, root);
25
28
  if (assignment === null)
26
- return "Task unavailable";
29
+ return null;
27
30
  const work = taskWorkSummaryOf(store, assignment.activeTaskId, now, access);
28
31
  if (work === null)
29
- return "Task unavailable";
32
+ return null;
30
33
  const live = work.liveRunId === null ? null : store.getRun(work.liveRunId);
31
34
  const facts = assignmentStatusFacts(assignment, { work: work.status, planning: live?.role === "planner" });
32
35
  // The canvas reads no evidence files. Keep the machine's recorded check visible
@@ -35,7 +38,13 @@ export function flowCardTaskLine(store, taskId, repo, now, secrets = []) {
35
38
  // With neither, nothing about the check was read here: the line says nothing about it, never a pass and never an ask.
36
39
  if (facts.checks?.status === "unavailable")
37
40
  facts.checks = check === null ? null : { ...facts.checks, status: check.status, exitCode: check.exitCode };
38
- const status = taskStatusOf(facts);
41
+ return { status: taskStatusOf(facts), activeTaskId: assignment.activeTaskId, live };
42
+ }
43
+ export function flowCardTaskLine(store, taskId, repo, now, secrets = []) {
44
+ const read = storedTaskStatus(store, taskId, repo, now);
45
+ if (read === null)
46
+ return "Task unavailable";
47
+ const { status, live } = read;
39
48
  if (status.headline === "Needs you") {
40
49
  const fallback = status.need?.action.label ?? "Open the task";
41
50
  // The question itself is useful; routine approvals are clearer as the short action.
@@ -44,7 +53,7 @@ export function flowCardTaskLine(store, taskId, repo, now, secrets = []) {
44
53
  return `Waiting on you: ${what}`;
45
54
  }
46
55
  if (status.headline === "Building" && live !== null) {
47
- const ref = store.lookupRef(assignment.activeTaskId);
56
+ const ref = store.lookupRef(read.activeTaskId);
48
57
  const checkpoint = store.latestCheckpointForRun(live.id);
49
58
  const progress = buildProgressOf(checkpoint?.taskRef !== ref?.id ? null : checkpoint?.snapshot.milestones.map(one => ({ description: null, state: one.state, note: one.note })));
50
59
  if (progress?.stuck != null)
@@ -14,7 +14,7 @@ export const INTEGRATIONS_CSS = `.integrations{max-width:760px;min-width:0}.inte
14
14
  `.integrations .integration>:not(.integration-action),.integrations .integration-body>*{grid-column:1}.integrations .integration-body{display:contents}.integrations .integration:first-of-type{border-top:0}.integrations .integration-head{margin:0;display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center;min-width:0}` +
15
15
  `.integrations .integration .integration-body>*{margin-left:calc(var(--brand-mark) + 10px)}.integrations .integration-head strong{overflow-wrap:anywhere}.integrations .integration p{margin:2px 0 0;min-width:0;overflow-wrap:anywhere}` +
16
16
  `.integrations .integration-action{grid-column:2;grid-row:1 / span 3;align-self:center;margin:0}.integrations .integration-action button,.integrations .integration-action .button-link{min-height:44px;white-space:nowrap}` +
17
- `.integrations .integration .integration-action button{width:auto}.integrations .integration .integration-action a.integration-quiet{background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
17
+ `.integrations .integration .integration-action button{width:auto}.integrations .integration .integration-action a.integration-quiet,.integrations .integration .integration-action button.integration-quiet{background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
18
18
  `.integrations .integration-fix{color:var(--so-danger)}.integrations code{overflow-wrap:anywhere}.integrations details{margin-top:4px;font-size:.8125rem}.integrations summary{cursor:pointer;padding:4px 0}` +
19
19
  `.integration-state{display:inline-flex;align-items:center;gap:6px;font-size:.75rem;font-weight:500;padding:0 8px;border-radius:5px;line-height:20px;white-space:nowrap}` +
20
20
  `.integration-state i{width:6px;height:6px;border-radius:50%;background:currentColor}` +
@@ -33,6 +33,10 @@ function action(one, csrf) {
33
33
  if (a.kind === "test") {
34
34
  return csrf === "" ? "" : postForm("/settings/integrations/test", html `<button type="submit">Send test</button>`, { attrs: { class: "integration-action" }, hidden: { key: one.key } });
35
35
  }
36
+ // A project's youiyoux requests: one switch for that project.
37
+ if (a.kind === "switch") {
38
+ return csrf === "" ? "" : postForm("/settings/integrations/youiyoux", html `<button type="submit"${a.on ? "" : html ` class="integration-quiet"`}>${a.label}</button>`, { attrs: { class: "integration-action" }, hidden: { repo: a.repo, on: a.on ? "1" : "0" } });
39
+ }
36
40
  // A command is run where Toolroll runs; a page is linked. Fix and Set up both say which.
37
41
  // Fix is the one filled action on the page; Set up stays quiet so a list of unused services doesn't shout.
38
42
  if (a.href !== null)
@@ -50,12 +54,13 @@ function row(one, csrf) {
50
54
  ? html `<span class="integration-state integration-state--checking"><i aria-hidden="true"></i>Checking</span>`
51
55
  : html `<span class="integration-state integration-state--${one.state}"><i aria-hidden="true"></i>${STATE_WORDS[one.state]}</span>`;
52
56
  const facts = [one.account, one.detail, one.credentials].filter(Boolean).join(" · ");
53
- const command = one.action.kind !== "test" ? one.action.command : null;
54
- const fix = one.action.kind === "fix" ? html `<p class="integration-fix" role="status">${one.action.words}</p>` : "";
57
+ const command = one.action.kind === "setup" || one.action.kind === "fix" ? one.action.command : null;
58
+ const words = one.action.kind === "fix" || one.action.kind === "switch" ? one.action.words : null;
59
+ const fix = words !== null ? html `<p class="integration-fix" role="status">${words}</p>` : "";
55
60
  const run = command === null || (one.action.kind === "fix" && one.action.words.includes(command)) ? "" : html `<p class="meta">Run <code>${command}</code> on the computer running Toolroll.</p>`;
56
61
  const seen = [one.lastSuccessAt === null ? null : html `Last success ${when(one.lastSuccessAt)}`, one.usedBy.length === 0 ? null : html `Used by ${one.usedBy.join(", ")}`].filter(Boolean);
57
62
  const history = one.state === "not-set-up" || seen.length === 0 ? "" : html `<p class="meta">${joinHtml(seen, " · ")}</p>`;
58
- const lastError = one.lastError === null || one.state === "not-set-up" ? "" : one.state === "broken" && one.action.kind === "fix" && one.action.words === one.lastError
63
+ const lastError = one.lastError === null || one.state === "not-set-up" ? "" : one.state === "broken" && words === one.lastError
59
64
  ? html `<p class="meta">Failed ${when(one.lastErrorAt ?? "")}</p>`
60
65
  : html `<details><summary>Last error ${one.lastErrorAt === null ? "" : when(one.lastErrorAt)}</summary><p class="meta">${one.lastError}</p></details>`;
61
66
  return html `<div class="integration" data-integration="${one.key}" data-state="${one.state}"><p class="integration-head">${brandMarkHtml(markFor(one), one.name, one.state === "connected")}<strong>${one.name}</strong> ${badge}</p>${action(one, csrf)}<div class="integration-body">${facts === "" ? "" : html `<p class="meta">${facts}</p>`}${fix}${run}${history}${lastError}</div></div>`;
@@ -18,7 +18,15 @@ export type IntegrationAction = ({
18
18
  kind: "fix";
19
19
  label: "Fix";
20
20
  words: string;
21
- } & IntegrationPlace);
21
+ } & IntegrationPlace)
22
+ /** A project's youiyoux requests: one switch, and the problem to fix when it is Broken. */
23
+ | {
24
+ kind: "switch";
25
+ label: "Turn off" | "Turn on";
26
+ repo: string;
27
+ on: boolean;
28
+ words: string | null;
29
+ };
22
30
  export type Integration = {
23
31
  key: string;
24
32
  group: IntegrationGroup;
@@ -31,6 +31,7 @@ import { LINEAR_URL, readHooksBase, readLinearKey } from "./flow-triggers.js";
31
31
  import { credentialModeOf, projectToolsOf, readToolSecrets, missingSecrets, secretsSetFor, testToolOf } from "./project-tools.js";
32
32
  import { origin, readMonitoring } from "./monitoring-settings.js";
33
33
  import { targetOf } from "./monitoring.js";
34
+ import { youiyouxState } from "./youiyoux-intake.js";
34
35
  export const STATE_WORDS = { connected: "Connected", "not-set-up": "Not set up", broken: "Broken" };
35
36
  const NO_ACTIVITY = { okAt: null, error: null, errorAt: null, failing: false };
36
37
  const PROBE_MS = 8_000;
@@ -459,10 +460,46 @@ function combine(target, row) {
459
460
  }
460
461
  return { ...base, ...common, state: "connected", action: { kind: "test", label: "Send test" } };
461
462
  }
463
+ /**
464
+ * youiyoux change requests, one row per project that uses youiyoux (youiyoux-intake.ts): Connected with how many of
465
+ * its requests are queued, Broken when the last pass couldn't read its folder or some of its files, or Not set up when
466
+ * a person turned it off; one row Not set up when no project uses it. Read from the store and the folders' presence.
467
+ */
468
+ function youiyouxIntegrations(io) {
469
+ // In the order every other row's keys come (combine), so `--json` reads alike.
470
+ const row = (key, account, facts, state, action) => ({
471
+ key, group: "code", name: "youiyoux", usedBy: facts.usedBy ?? [], account, detail: facts.detail, checked: true, checkedAt: facts.checkedAt ?? null,
472
+ lastSuccessAt: facts.lastSuccessAt ?? null, lastError: facts.lastError ?? null, lastErrorAt: facts.lastErrorAt ?? null, state, action,
473
+ });
474
+ const rows = io.repos.flatMap((repo) => {
475
+ let state;
476
+ try {
477
+ state = youiyouxState(io.store, repo);
478
+ }
479
+ catch {
480
+ return [];
481
+ }
482
+ if (!state.uses)
483
+ return [];
484
+ const key = `youiyoux:${repo}`, account = basename(repo);
485
+ const turn = (on, words) => ({ kind: "switch", label: on ? "Turn on" : "Turn off", repo, on, words });
486
+ if (state.off)
487
+ return [row(key, account, { detail: "Turned off" }, "not-set-up", turn(true, null))];
488
+ const facts = { usedBy: [`Tasks in ${account}`], detail: `${plural(state.queued, "request")} queued`, checkedAt: state.checkedAt };
489
+ if (state.problem === null)
490
+ return [row(key, account, { ...facts, lastSuccessAt: state.checkedAt }, "connected", turn(false, null))];
491
+ const words = scrubIntegrationText(state.problem);
492
+ return [row(key, account, { ...facts, lastError: words, lastErrorAt: state.checkedAt }, "broken", turn(false, words))];
493
+ });
494
+ return rows.length > 0 ? rows : [row("youiyoux", null, { detail: "Starts by itself in a project with a youi.project.json" }, "not-set-up", { kind: "setup", label: "Set up", href: null, command: null })];
495
+ }
462
496
  /** Every integration as it stands now, from files and saved history only: no network, no process, never slow. */
463
497
  export function integrationsNow(io) {
464
498
  const rows = readIntegrationChecks(io.store);
465
- return targets(io).map(target => combine(target, rows.get(target.key)));
499
+ const list = targets(io).map(target => combine(target, rows.get(target.key)));
500
+ const linear = list.findIndex(one => one.key === "linear");
501
+ list.splice(linear + 1, 0, ...youiyouxIntegrations(io));
502
+ return list;
466
503
  }
467
504
  /** Run the checks (all, or the named ones), record each answer, and return the list. */
468
505
  export async function checkIntegrations(io, keys) {
@@ -512,7 +549,11 @@ export function renderIntegrations(list, secrets = null) {
512
549
  lines.push(`${"".padEnd(width)} last success ${whenText(one.lastSuccessAt)}${one.lastError === null ? "" : ` · last error ${whenText(one.lastErrorAt)}: ${one.lastError}`}`);
513
550
  if (one.action.kind === "fix")
514
551
  lines.push(`${"".padEnd(width)} Fix: ${one.action.words}${one.action.command === null || one.action.words.includes(one.action.command) ? "" : ` (run ${one.action.command})`}`);
515
- if (one.action.kind === "setup")
552
+ if (one.action.kind === "switch" && one.action.words !== null)
553
+ lines.push(`${"".padEnd(width)} Fix: ${one.action.words}`);
554
+ if (one.action.kind === "switch" && one.action.on)
555
+ lines.push(`${"".padEnd(width)} Turn on: Settings → Integrations`);
556
+ if (one.action.kind === "setup" && (one.action.command !== null || one.action.href !== null))
516
557
  lines.push(`${"".padEnd(width)} Set up: ${one.action.command !== null ? `run ${one.action.command}` : `Settings (${one.action.href ?? "/settings"})`}`);
517
558
  return lines;
518
559
  }), ...note];
@@ -1,3 +1,4 @@
1
+ import { type AssignmentSnapshot } from "./assignment.js";
1
2
  import type { RemoteMainRead, RemoteMains } from "./remote-main.js";
2
3
  import type { Store } from "./store.js";
3
4
  import { type Git } from "./teardown-git.js";
@@ -70,6 +71,9 @@ export type LandedCandidate = {
70
71
  targets: LandedTarget[];
71
72
  keeps: LandedKeep[];
72
73
  };
74
+ /** Use the current manual completion guards. Only a recorded merge of this exact result can settle them. Also why a
75
+ * youiyoux options task a person decided on in the hub stays for review (youiyoux-intake.ts). */
76
+ export declare function completionProblem(store: Store, assignment: AssignmentSnapshot, root: string): string | null;
73
77
  /**
74
78
  * Results in a project whose work landed, with what their teardown would remove and what stays. Reads only.
75
79
  * `limit` caps how many results are read against git (reconcile's bound; results read before and unchanged since
@@ -237,8 +237,9 @@ function resultProblem(store, runId, head) {
237
237
  return "its result couldn't be read";
238
238
  return run.outcome === "no-change" || run.baseRevision === head ? "its result has no new work" : null;
239
239
  }
240
- /** Use the current manual completion guards. Only a recorded merge of this exact result can settle them. */
241
- function completionProblem(store, assignment, root) {
240
+ /** Use the current manual completion guards. Only a recorded merge of this exact result can settle them. Also why a
241
+ * youiyoux options task a person decided on in the hub stays for review (youiyoux-intake.ts). */
242
+ export function completionProblem(store, assignment, root) {
242
243
  if (assignment.state !== "ready-to-check" && assignment.state !== "complete")
243
244
  return "it needs a decision first";
244
245
  if (assignment.state === "complete")
@@ -181,7 +181,8 @@ export function integrationCapabilities(list) {
181
181
  return list.map(one => {
182
182
  const link = one.group === "agents" ? "providers" : one.group === "tools" ? "tools" : INTEGRATION_LINK[one.key.replace(/^.*:/, "").toLowerCase()] ?? INTEGRATION_LINK[one.name.toLowerCase()] ?? "integrations";
183
183
  const lets = one.usedBy.length > 0 ? `use ${one.name} for ${one.usedBy.join(", ")}` : `use ${one.name}`;
184
- const next = one.action.kind === "setup" ? `Not set up: set up ${one.name} in Settings` : one.action.kind === "fix" ? `Fix: ${one.action.words}` : null;
184
+ const next = one.action.kind === "setup" ? `Not set up: set up ${one.name} in Settings` : one.action.kind === "fix" ? `Fix: ${one.action.words}`
185
+ : one.action.kind === "switch" ? (one.action.words !== null ? `Fix: ${one.action.words}` : one.action.on ? `Turned off: turn ${one.name} on in Settings → Integrations` : null) : null;
185
186
  return { name: one.name, kind: one.group, state: STATE_WORDS[one.state], ok: one.state === "connected", account: one.account, usedBy: one.usedBy, lastError: one.lastError, lets, next, link: next === null ? null : link };
186
187
  });
187
188
  }
package/dist/lead-cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { parseFeedbackImage } from "./result-review.js";
1
2
  import { configureLeadFollow, leadFollowStatus } from './lead-follow.js';
2
3
  import { closedQuestionWords } from "./decision.js";
3
4
  /**
@@ -104,7 +105,8 @@ export function proposalBlock(store, proposal, repos, number) {
104
105
  const snapshot = proposal.payload["snapshot"];
105
106
  const selected = Array.isArray(proposal.payload["notes"]) ? proposal.payload["notes"] : [];
106
107
  const notes = snapshot?.notes.filter(one => selected.includes(one.id)) ?? [];
107
- return [line, ...notes.map(one => ` ${one.path === null ? "" : `${one.path}${one.line === null ? "" : `:${one.line}`}: `}${one.note}`),
108
+ const image = parseFeedbackImage(proposal.payload["image"]);
109
+ return [line, ...(image ? [` Screenshot ${image.artifact} · result #${image.run}`] : []), ...notes.map(one => ` ${one.image ? `Screenshot ${one.image.artifact} · result #${one.image.run}: ` : ""}${one.path === null ? "" : `${one.path}${one.line === null ? "" : `:${one.line}`}: `}${one.note}`),
108
110
  proposal.payload["operation"] === "revise" ? " Creates a revision of the same task; approval is separate." : " Saves feedback; no work starts."];
109
111
  }
110
112
  return proposal.kind === "answer" ? [line, ...answerContextLines(store, proposal.payload)] : [line];
@@ -1,3 +1,4 @@
1
+ import { type FeedbackImage } from "./result-review.js";
1
2
  import { type CommitmentChannel } from "./lead-commitments.js";
2
3
  import { type Integration } from "./integrations.js";
3
4
  import type { AuthMode } from "./keys.js";
@@ -23,6 +24,7 @@ export type LeadToolContext = {
23
24
  step: number;
24
25
  snapshot: ReviewSnapshot;
25
26
  }>;
27
+ replyImage?: FeedbackImage;
26
28
  /** Where evidence lives, when the surface knows — a scout's report reads from here. */
27
29
  evidenceRoot?: string;
28
30
  /** Records the screenshots this turn selected, under the turn, for a channel that delivers files; returns how many are recorded. Absent: nothing durable is kept. */
@@ -1,9 +1,11 @@
1
+ import { sameFeedbackImage } from "./result-review.js";
1
2
  import { sweepLine, sweepTable } from "./lead-sweep.js";
2
3
  import { connectionsOf, oneClickOf } from "./mcp-connect.js";
3
4
  import { checkLevelFromWords, isCheckLevel } from "./check-levels.js";
4
5
  import { policyParts } from "./policy.js";
5
6
  import { randomUUID } from 'node:crypto';
6
7
  import { dirname, join } from 'node:path';
8
+ import { projectOutcomes, projectOutcomesPage } from './project-outcomes.js';
7
9
  import { repositoryContextRead } from './repository-context.js';
8
10
  import { assignmentCatchUp } from './assignment-brief.js';
9
11
  import { CHAT_ACTIONS, CHAT_ACTION_FIELDS, isChatAction, prepareSharedAction, sharedActionNeedsReview, sharedActionPayload } from './chat-actions.js';
@@ -18,6 +20,7 @@ import { cancelCommitment, CHECK_RESULTS, conditionWords, latestResultRun, recor
18
20
  import { searchMemory } from "./project-memory.js";
19
21
  import { integrationsNow, STATE_WORDS } from "./integrations.js";
20
22
  import { CAPABILITIES_UNREAD, capabilitiesOf, needsCapabilities } from "./lead-capabilities.js";
23
+ import { feedbackImageProblem } from "./result-actions.js";
21
24
  import { readChatResult, reviewInputProblem } from "./chat-review.js";
22
25
  import { RESULT_IMAGES_PER_TURN_CAP, selectResultImages } from "./chat-evidence.js";
23
26
  import { CHAT_CONTROLS, isChatControl } from "./chat-controls.js";
@@ -377,7 +380,7 @@ export function agentsOver(store, taskId, now) {
377
380
  * find_tools and advertised from the next request of the same reply.
378
381
  */
379
382
  export const LEAD_CORE_TOOLS = new Set([
380
- "find_tools", "get_brief", "recap", "list_repos", "get_project_context", "search_project_memory", "get_capabilities",
383
+ "find_tools", "get_brief", "recap", "list_repos", "get_project_context", "get_project_activity", "search_project_memory", "get_capabilities",
381
384
  "list_tasks", "get_task", "get_result", "get_diff", "get_check_log", "list_decisions", "get_decision",
382
385
  "get_actions", "get_action_status", "propose_action", "propose_task", "propose_scope", "propose_steer",
383
386
  "propose_answer", "propose_review", "remember", "ask_owner", "commit_to",
@@ -475,6 +478,15 @@ const LEAD_TOOL_HANDLERS = {
475
478
  return { ok: true, body: assignmentCatchUp(ctx.store, ctx.now, { principal: 'operator', repos: ctx.who.repos }, repo === null ? {} : { repo }, ctx.evidenceRoot) };
476
479
  },
477
480
  },
481
+ get_project_activity: {
482
+ description: 'Read project Activity: current Ready backlog, finished-to-decision elapsed time, explicit revisions and recorded cost for completed families in the last 60 days. Includes coverage and a page of underlying tasks (offset: nextOffset for more); not review labor or a quality score.',
483
+ handle: (ctx, args) => {
484
+ const repo = repoPathOf(ctx.who, args['repo']);
485
+ if (!repo || !ctx.store.accountCanAccess(ctx.who.name, repo))
486
+ return { ok: false, message: 'Choose an available project from list_repos.' };
487
+ return { ok: true, body: projectOutcomesPage(projectOutcomes(ctx.store, repo, ctx.now, { principal: 'operator', repos: ctx.who.repos }), Number(args['offset'] ?? 0)) };
488
+ },
489
+ },
478
490
  get_project_context: {
479
491
  description: 'Find source excerpts or advisory import impact. Read-only; unavailable indexing falls back to source search.',
480
492
  handle: (ctx, args) => {
@@ -656,6 +668,7 @@ const LEAD_TOOL_HANDLERS = {
656
668
  changes: snippet.slice(0, 1000), changesShortened: snippet.length > 1000,
657
669
  verification: ctx.store.proofVerdictFor(snapshot.run)?.verdict ?? "not verified",
658
670
  accepted: ctx.store.proofAcceptance(snapshot.run) !== null,
671
+ images: (snapshot.images ?? []).map(one => ({ id: one.artifact, run: one.run })),
659
672
  evidenceTool: "get_acceptance_evidence",
660
673
  canRevise: snapshot.execution === snapshot.task } };
661
674
  },
@@ -789,7 +802,7 @@ const LEAD_TOOL_HANDLERS = {
789
802
  },
790
803
  },
791
804
  propose_review: {
792
- description: "Read get_result first. revise uses selected saved_notes plus optional note on the SAME task; note saves only. Omitted saved_notes stays untouched.",
805
+ description: "Read get_result first. revise uses selected saved_notes plus optional note on the SAME task; note saves only. Omitted saved_notes stays untouched. To comment on a screenshot, use its image id from get_result_images. If this channel cannot select one, link to the result Comment control and say feedback still needs submitting there.",
793
806
  handle: (ctx, args) => {
794
807
  const read = ctx.readResults?.get(Number(args["run"]));
795
808
  if (read === undefined || read.step >= ctx.step)
@@ -812,8 +825,20 @@ const LEAD_TOOL_HANDLERS = {
812
825
  return { ok: false, message: "Write feedback or select saved notes for a revision." };
813
826
  if (operation === "revise" && read.snapshot.task !== read.snapshot.execution)
814
827
  return { ok: false, message: "A newer revision is current. Read that version before requesting changes." };
828
+ const wanted = args["image"] === null ? undefined : args["image"] ?? (note !== null && ctx.replyImage?.run === read.snapshot.run ? ctx.replyImage.artifact : undefined);
829
+ let image = null;
830
+ if (wanted !== undefined) {
831
+ image = read.snapshot.images?.find(one => one.artifact === wanted) ?? null;
832
+ if (image === null || ctx.evidenceRoot === undefined || note === null)
833
+ return { ok: false, message: "Choose a saved screenshot from the result you read and write feedback." };
834
+ if (ctx.replyImage?.artifact === wanted && !sameFeedbackImage(ctx.replyImage, image))
835
+ return { ok: false, message: "The screenshot you replied to changed. Open the result and select it again." };
836
+ const problem = feedbackImageProblem(ctx.store, ctx.evidenceRoot, read.snapshot.run, image);
837
+ if (problem !== null)
838
+ return { ok: false, message: problem };
839
+ }
815
840
  const id = ctx.draft("review", { task: read.snapshot.task, taskTitle: read.snapshot.title, run: read.snapshot.run,
816
- snapshot: read.snapshot, operation, note, path, line, notes });
841
+ snapshot: read.snapshot, operation, note, path, line, notes, image });
817
842
  return id === null ? tooMany() : { ok: true, body: { proposal: id, kind: "review", awaiting: "confirmation", operation } };
818
843
  },
819
844
  },
@@ -1497,7 +1522,8 @@ const LEAD_TOOL_HANDLERS = {
1497
1522
  const integrations = list.map(one => ({
1498
1523
  name: one.name, kind: one.group, state: STATE_WORDS[one.state], account: one.account, detail: one.detail, usedBy: one.usedBy,
1499
1524
  lastSuccessAt: one.lastSuccessAt, lastError: one.lastError,
1500
- next: one.action.kind === "setup" ? "Not set up: open Settings → Integrations to set it up" : one.action.kind === "fix" ? `Fix: ${one.action.words}` : null,
1525
+ next: one.action.kind === "setup" ? "Not set up: open Settings → Integrations to set it up" : one.action.kind === "fix" ? `Fix: ${one.action.words}`
1526
+ : one.action.kind === "switch" ? (one.action.words !== null ? `Fix: ${one.action.words}` : one.action.on ? "Turned off: turn it on in Settings → Integrations" : null) : null,
1501
1527
  }));
1502
1528
  return { ok: true, body: { integrations, settings: "show_control integrations opens Settings → Integrations; never invent a link" } };
1503
1529
  },
package/dist/lead.d.ts CHANGED
@@ -38,6 +38,8 @@ export type LeadTurnInput = {
38
38
  * visible thread so a task-scoped composer still reads like a normal
39
39
  * conversation. */
40
40
  context?: string;
41
+ /** Exact image from the transport record of the message being answered. */
42
+ replyImage?: import("./result-review.js").FeedbackImage;
41
43
  fetcher?: typeof fetch;
42
44
  /** Injected by tests; production invokes the isolated local harness. */
43
45
  subscriptionRunner?: SubscriptionLeadRunner;
package/dist/lead.js CHANGED
@@ -110,7 +110,7 @@ function settleBy(work, ms) {
110
110
  const late = new Promise(resolve => { timer = setTimeout(() => resolve(LATE), Math.max(0, ms)); });
111
111
  return Promise.race([work, late]).finally(() => clearTimeout(timer));
112
112
  }
113
- const READ_TOOLS = new Set(["get_brief", "get_project_context", "get_actions", "get_action_status", "get_skills", "get_acceptance_evidence", "recap", "list_repos", "list_tasks", "get_task", "get_result", "get_result_images", "get_controls", "get_agents", "get_project_knowledge", "get_task_conversation", "get_diff", "get_check_log", "get_project_tools", "get_flows", "get_flow_insights", "list_decisions", "get_decision", "queue"]);
113
+ const READ_TOOLS = new Set(["get_brief", "get_project_context", "get_project_activity", "get_actions", "get_action_status", "get_skills", "get_acceptance_evidence", "recap", "list_repos", "list_tasks", "get_task", "get_result", "get_result_images", "get_controls", "get_agents", "get_project_knowledge", "get_task_conversation", "get_diff", "get_check_log", "get_project_tools", "get_flows", "get_flow_insights", "list_decisions", "get_decision", "queue"]);
114
114
  /** The last messages of the thread as provider-neutral history, newest kept first until the byte cap. */
115
115
  export function historyFor(store, thread, queuedMessageId, named = queuedMessageId !== undefined) {
116
116
  // Admitted messages only (v128): a queued message — this one, a teammate's, or one removed while waiting — never
@@ -205,7 +205,7 @@ export async function runLeadTurn(input) {
205
205
  return refuse("thread-closed");
206
206
  if (liveSession.ceilingDigest !== who.ceilingDigest || liveThread.ceilingDigest !== who.ceilingDigest)
207
207
  return refuse("ceiling-changed");
208
- const digest = createHash("sha256").update(JSON.stringify([thread.id, message, input.context ?? null])).digest("hex");
208
+ const digest = createHash("sha256").update(JSON.stringify([thread.id, message, input.context ?? null, ...(input.replyImage === undefined ? [] : [input.replyImage])])).digest("hex");
209
209
  const receipt = request === undefined ? null : store.leadRequestReceipt(session.id, request);
210
210
  if (receipt !== null)
211
211
  return receipt.digest === digest ? { ok: true, replayed: true, turn: receipt.turn } : refuse("request-changed");
@@ -568,7 +568,7 @@ export async function runLeadTurn(input) {
568
568
  // caller of executeLeadTool): the model learns the road (find_tools, then the next step) and the reply goes on.
569
569
  const outcome = !advertisedNames.has(call.name)
570
570
  ? { ok: false, message: `${call.name} isn't available in this step. Call find_tools with what you want to do; tools it finds can be called from your next step.` }
571
- : executeLeadTool({ store, who, now: clock(), draft, selectEvidence, step: steps, readDecisions, readResults, searchedMemory, checkedCapabilities, ask, findTools, ...(input.integrations === undefined ? {} : { integrations: input.integrations }), thread: thread.id, turn: turnId, ...(promiseChannelOfTurn(input.channel) === undefined ? {} : { channel: promiseChannelOfTurn(input.channel) }), ...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }), ...(input.mediaDelivery === undefined ? {} : { mediaDelivery: input.mediaDelivery }) }, call.name, call.args, view);
571
+ : executeLeadTool({ store, who, now: clock(), draft, selectEvidence, ...(input.replyImage === undefined ? {} : { replyImage: input.replyImage }), step: steps, readDecisions, readResults, searchedMemory, checkedCapabilities, ask, findTools, ...(input.integrations === undefined ? {} : { integrations: input.integrations }), thread: thread.id, turn: turnId, ...(promiseChannelOfTurn(input.channel) === undefined ? {} : { channel: promiseChannelOfTurn(input.channel) }), ...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }), ...(input.mediaDelivery === undefined ? {} : { mediaDelivery: input.mediaDelivery }) }, call.name, call.args, view);
572
572
  let displayOutcome = { state: "succeeded" };
573
573
  if (!outcome.ok) {
574
574
  const reason = leadToolFailureReason(call.name, outcome.message);
package/dist/operate.d.ts CHANGED
@@ -99,7 +99,7 @@ export type OperateOptions = {
99
99
  /** Injected by tests: how the console looks at project folders off its thread (folder-look.ts). */
100
100
  folderLook?: FolderLookOptions;
101
101
  };
102
- export type OnboardSeams = Partial<Pick<OnboardIo, "home" | "env" | "cwd" | "interactive" | "confirm" | "findRepo" | "checkConnection" | "pullRequests">>;
102
+ export type OnboardSeams = Partial<Pick<OnboardIo, "home" | "env" | "cwd" | "interactive" | "confirm" | "findRepo" | "checkConnection" | "pullRequests" | "youiyoux">>;
103
103
  export declare const OPERATE_HELP: string;
104
104
  /** Parsed flags, with the positionals left over. */
105
105
  type Args = {
@@ -237,7 +237,6 @@ type Context = {
237
237
  export declare function writeLoginFileDurably(path: string, name: string, password: string): void;
238
238
  /** The platform browser opener — detached, silent, never load-bearing. */
239
239
  export declare function openBrowser(url: string): void;
240
- export declare function readUpConsoleUrl(dir: string): string | null;
241
240
  /**
242
241
  * The people allowed to agree to a scope.
243
242
  *
package/dist/operate.js CHANGED
@@ -54,6 +54,7 @@ import { packageVersion, startMonitoring, targetOf } from "./monitoring.js";
54
54
  import { origin, readMonitoring } from "./monitoring-settings.js";
55
55
  import { logEvent } from "./log.js";
56
56
  import { FLOW_EVERY_MS, flowHousekeeping, moveCards, moveCardsAfter } from "./flow-cadence.js";
57
+ import { NO_YOUIYOUX_PASS, youiyouxOnboardLine } from "./youiyoux-intake.js";
57
58
  import { readHooksBase, scheduleFromWords } from "./flow-triggers.js";
58
59
  import { sendSubagentSummaries } from "./subagent-admin.js";
59
60
  import { runRequestedUndos, sendSubagentWeeklies } from "./subagent-week.js";
@@ -227,7 +228,7 @@ import { isTaskSize, legOf, projectRoute, routeDigestOf, routeWords, TASK_SIZES,
227
228
  import { observeProviderReadiness, reportProviderReadinessAuthed } from "./runner.js";
228
229
  import { parseDemoUrl, projectDemoUrl, saveProjectDemo } from "./project-demo.js";
229
230
  import { effectiveConcurrency, maySlotTake, parseProjectConcurrency, PROJECT_CONCURRENCY_DEFAULT, ProjectPasses, projectConcurrency, saveProjectConcurrency, savedProjectConcurrency } from "./project-concurrency.js";
230
- import { activeWebhookTargets, webhookPass, LEGACY_WEBHOOK_WARNING, loadWebhookTargets, effectivePrimary, isMessagingChannel, loadConsoleUrl, phoneOrigin, saveConsoleUrl, savePrimary } from "./webhooks.js";
231
+ import { activeWebhookTargets, webhookPass, LEGACY_WEBHOOK_WARNING, loadWebhookTargets, effectivePrimary, isMessagingChannel, loadConsoleUrl, phoneOrigin, readUpConsoleUrl, saveConsoleUrl, savePrimary, UP_CONSOLE_FILE } from "./webhooks.js";
231
232
  import { auditOf, inspectionOf, isProviderId, MONEY_CAPABILITIES, PROVIDER_IDS, validModelId, validateSpec, ALL_CREDENTIAL_ENV } from "./provider.js";
232
233
  import { attestProvider, attestationOf, versionInRange } from "./attest.js";
233
234
  import { build, proveApprovedProfile, } from "./builder.js";
@@ -278,6 +279,7 @@ export const OPERATE_HELP = `toolroll — operating the queue
278
279
  wait for this attempt to finish or need a person
279
280
  toolroll project use <path> remember a saved project (optional --token-file)
280
281
  toolroll project show show the current project and credential reference
282
+ toolroll project activity --json read the current project supervision report
281
283
  toolroll project rules --repo <p> a project's approval rules; an instance operator changes them with
282
284
  --not-requester on|off, --protect none|project, --protect-paths "a/**,b/**"
283
285
  toolroll project network --repo <p> [--allow h,…] [--remove h,…] [--open|--allowlist] [--local any|none]
@@ -2259,6 +2261,7 @@ function flowIoOf(context, pool, git, base) {
2259
2261
  dir: context.flowStepIo?.dir ?? dir, scratch: context.flowStepIo?.scratch ?? join(pool, "flow-checks"), base,
2260
2262
  ...(context.evidenceRoot === undefined ? {} : { evidenceRoot: context.evidenceRoot }) },
2261
2263
  ...(context.evidenceRoot === undefined ? {} : { evidenceRoot: context.evidenceRoot }),
2264
+ youiyouxGit: gitOf(context.gitRunner),
2262
2265
  };
2263
2266
  }
2264
2267
  /**
@@ -3384,8 +3387,10 @@ async function tickCommand(flags, context) {
3384
3387
  syncAssignmentHandoffs(store, clock(), auth.runner.repos, context.evidenceRoot);
3385
3388
  settleFinished();
3386
3389
  const replied = replyPass.taken > 0 || replyPass.problem !== null;
3387
- const flows = flowPass.moved + flowPass.filed.length + flowPass.problems.length + triggerPass.added + triggerPass.problems.length + stepPass.ran + stepPass.problems.length === 0 && !replied ? {} : { flows: { ...flowPass,
3388
- ...(triggerPass.added + triggerPass.problems.length === 0 ? {} : { triggers: triggerPass }), ...(stepPass.ran + stepPass.problems.length === 0 ? {} : { steps: stepPass }), ...(replied ? { replies: replyPass } : {}) } };
3390
+ const requestPass = housekeeping?.youiyoux ?? NO_YOUIYOUX_PASS;
3391
+ const requested = requestPass.filed + requestPass.copied + requestPass.decided + requestPass.problems.length > 0;
3392
+ const flows = flowPass.moved + flowPass.filed.length + flowPass.problems.length + triggerPass.added + triggerPass.problems.length + stepPass.ran + stepPass.problems.length === 0 && !replied && !requested ? {} : { flows: { ...flowPass,
3393
+ ...(triggerPass.added + triggerPass.problems.length === 0 ? {} : { triggers: triggerPass }), ...(requested ? { youiyoux: requestPass } : {}), ...(stepPass.ran + stepPass.problems.length === 0 ? {} : { steps: stepPass }), ...(replied ? { replies: replyPass } : {}) } };
3389
3394
  const summary = () => {
3390
3395
  const lines = [`Considered ${considered}, built ${built}, parked ${parked}, broke ${broke}.`];
3391
3396
  for (const entry of dispatched) {
@@ -6396,7 +6401,7 @@ async function runWatchLoop(args) {
6396
6401
  base = head.stdout.trim();
6397
6402
  }
6398
6403
  const pass = await flowHousekeeping(store, repo, context.clock, flowIoOf(context, flowPool, flowGit, base), admissionStopped);
6399
- flowsDid(pass.flows.moved + pass.triggers.added + pass.steps.ran + pass.replies.taken, pass.flows.filed.length, `flow pass — ${pass.triggers.added} card(s) added, ${pass.replies.taken} reply(ies) taken, ${pass.steps.ran} step(s) run, ${pass.flows.moved} moved`);
6404
+ flowsDid(pass.flows.moved + pass.triggers.added + pass.steps.ran + pass.replies.taken, pass.flows.filed.length + pass.youiyoux.filed, `flow pass — ${pass.triggers.added} card(s) added, ${pass.youiyoux.filed} youiyoux request(s) filed, ${pass.replies.taken} reply(ies) taken, ${pass.steps.ran} step(s) run, ${pass.flows.moved} moved`);
6400
6405
  },
6401
6406
  });
6402
6407
  const modelWatch = startMaintenance({
@@ -6936,8 +6941,6 @@ export function openBrowser(url) {
6936
6941
  // A browser that will not open is a URL the greeting already printed.
6937
6942
  }
6938
6943
  }
6939
- /** Where `up` last served its console, kept beside the database so `onboard` names the same address. */
6940
- const UP_CONSOLE_FILE = "up-console.json";
6941
6944
  function recordUpConsoleUrl(dir, url, now) {
6942
6945
  try {
6943
6946
  const file = join(dir, UP_CONSOLE_FILE), temporary = `${file}.${process.pid}.tmp`;
@@ -6948,15 +6951,6 @@ function recordUpConsoleUrl(dir, url, now) {
6948
6951
  // A display fact: a failed write only leaves onboard on the default port.
6949
6952
  }
6950
6953
  }
6951
- export function readUpConsoleUrl(dir) {
6952
- try {
6953
- const parsed = JSON.parse(readFileSync(join(dir, UP_CONSOLE_FILE), "utf8"));
6954
- return typeof parsed.url === "string" && /^https?:\/\/[^\s]+$/.test(parsed.url) ? parsed.url.replace(/\/+$/, "") : null;
6955
- }
6956
- catch {
6957
- return null;
6958
- }
6959
- }
6960
6954
  /** How onboard says to sign in: the saved login file only while it still works (as `up` advertises it), else the
6961
6955
  * account and its password; null only when there is no account yet. Never the password itself. */
6962
6956
  function onboardLogin(store, loginFile) {
@@ -7041,6 +7035,8 @@ async function onboardCommand(flags, context) {
7041
7035
  return `off: ${checked.message}`;
7042
7036
  return `off: turn on with toolroll publish setup --repo ${repo} --yes (asks your password), or in Settings → Projects`;
7043
7037
  }),
7038
+ // A project that uses youiyoux has its change requests filed as tasks with no step of its own (youiyoux-intake.ts).
7039
+ youiyoux: seams.youiyoux ?? (async (repo) => youiyouxOnboardLine(store, repo)),
7044
7040
  starterFlag: text(flags, "starter"),
7045
7041
  // Starter flows belong to a person: the flow's decisions ask them. With no account yet, none can be switched on.
7046
7042
  starters: {
@@ -0,0 +1,20 @@
1
+ /** Shared presentation for browser and no-script Activity; facts come only
2
+ * from projectOutcomes. No alternate counting or status interpretation. */
3
+ import type { ActivityFamily, ProjectOutcomes } from './project-outcomes.js';
4
+ export declare const activityAge: (ms: number | null) => string;
5
+ export declare const activityMoney: (usd: number | null) => string;
6
+ export declare const ACTIVITY_ACTORS: {
7
+ human: string;
8
+ lead: string;
9
+ landed: string;
10
+ 'machine-or-unknown': string;
11
+ };
12
+ export type ActivityMetric = {
13
+ label: string;
14
+ value: string;
15
+ detail: string;
16
+ rows: ActivityFamily[];
17
+ };
18
+ export declare const activityScope: (report: ProjectOutcomes) => string;
19
+ export declare const activityLimit: (report: ProjectOutcomes) => string;
20
+ export declare function activityMetrics(report: ProjectOutcomes): ActivityMetric[];