toolroll 0.9.84 → 0.9.86

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 (132) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +94 -1193
  3. package/dist/agent-fence.d.ts +9 -5
  4. package/dist/agent-fence.js +6 -5
  5. package/dist/agent-onboard.d.ts +2 -0
  6. package/dist/agent-onboard.js +3 -0
  7. package/dist/assignment.js +4 -3
  8. package/dist/browser/workspace.css +1 -1
  9. package/dist/browser/workspace.js +33 -31
  10. package/dist/browser-workspace.d.ts +45 -0
  11. package/dist/chat-actions.d.ts +11 -1
  12. package/dist/chat-actions.js +53 -1
  13. package/dist/chat-channel.d.ts +10 -1
  14. package/dist/chat-channel.js +36 -5
  15. package/dist/chat-decide.d.ts +16 -4
  16. package/dist/chat-decide.js +239 -31
  17. package/dist/chat-messages.d.ts +5 -0
  18. package/dist/chat-messages.js +1 -1
  19. package/dist/chat-quiet.js +6 -4
  20. package/dist/chat-review.d.ts +4 -0
  21. package/dist/chat-review.js +13 -4
  22. package/dist/chat-terms.d.ts +9 -0
  23. package/dist/chat-terms.js +46 -0
  24. package/dist/claim.d.ts +3 -0
  25. package/dist/claim.js +4 -1
  26. package/dist/contracts/chat-actions.d.ts +9 -0
  27. package/dist/contracts/chat-actions.js +3 -0
  28. package/dist/contracts/console-api.d.ts +6 -2
  29. package/dist/contracts/console-api.js +6 -2
  30. package/dist/contracts/flow-propose.d.ts +4 -1
  31. package/dist/contracts/flow-propose.js +1 -1
  32. package/dist/contracts/flow.d.ts +41 -0
  33. package/dist/contracts/flow.js +4 -2
  34. package/dist/contracts/lead-tools.d.ts +51 -0
  35. package/dist/contracts/lead-tools.js +5 -0
  36. package/dist/contracts/store-json.d.ts +2 -0
  37. package/dist/contracts/trigger-payloads.d.ts +20 -0
  38. package/dist/contracts/trigger-payloads.js +4 -0
  39. package/dist/contracts/youiyoux-request.d.ts +248 -0
  40. package/dist/contracts/youiyoux-request.js +152 -0
  41. package/dist/coordinator.js +1 -0
  42. package/dist/deploy-gate.js +4 -0
  43. package/dist/dispatch.d.ts +3 -0
  44. package/dist/dispatch.js +4 -1
  45. package/dist/flow-browser.d.ts +103 -0
  46. package/dist/flow-browser.js +204 -0
  47. package/dist/flow-cadence.d.ts +8 -2
  48. package/dist/flow-cadence.js +16 -3
  49. package/dist/flow-card-task.d.ts +9 -0
  50. package/dist/flow-card-task.js +15 -6
  51. package/dist/flow-code.d.ts +5 -1
  52. package/dist/flow-code.js +4 -1
  53. package/dist/flow-engine.js +16 -0
  54. package/dist/flow-live.js +9 -1
  55. package/dist/flow-pull-request.js +15 -7
  56. package/dist/flow-share.js +3 -1
  57. package/dist/flow-steps.d.ts +9 -1
  58. package/dist/flow-steps.js +55 -5
  59. package/dist/flow-triggers.js +55 -0
  60. package/dist/flows-cli.d.ts +19 -0
  61. package/dist/flows-cli.js +47 -2
  62. package/dist/flows-ui.js +11 -2
  63. package/dist/flows.js +5 -2
  64. package/dist/integrations-ui.js +9 -4
  65. package/dist/integrations.d.ts +9 -1
  66. package/dist/integrations.js +43 -2
  67. package/dist/landed-cleanup.d.ts +4 -0
  68. package/dist/landed-cleanup.js +3 -2
  69. package/dist/lead-capabilities.js +2 -1
  70. package/dist/lead-cli.js +3 -1
  71. package/dist/lead-tools.d.ts +2 -0
  72. package/dist/lead-tools.js +65 -10
  73. package/dist/lead-voice.js +16 -5
  74. package/dist/lead.d.ts +2 -0
  75. package/dist/lead.js +3 -3
  76. package/dist/operate.d.ts +2 -3
  77. package/dist/operate.js +226 -21
  78. package/dist/project-activity.d.ts +20 -0
  79. package/dist/project-activity.js +23 -0
  80. package/dist/project-cli.d.ts +1 -0
  81. package/dist/project-cli.js +36 -19
  82. package/dist/project-delete.js +7 -3
  83. package/dist/project-knowledge.d.ts +3 -3
  84. package/dist/project-outcomes.d.ts +119 -0
  85. package/dist/project-outcomes.js +79 -0
  86. package/dist/pull-request-flow.js +35 -4
  87. package/dist/result-actions.d.ts +4 -0
  88. package/dist/result-actions.js +41 -9
  89. package/dist/result-follow-ups.js +4 -0
  90. package/dist/result-review.d.ts +9 -0
  91. package/dist/result-review.js +45 -5
  92. package/dist/scope.d.ts +5 -0
  93. package/dist/scope.js +7 -0
  94. package/dist/server/chat.js +4 -2
  95. package/dist/server/console.css +41 -0
  96. package/dist/server/flows.js +15 -0
  97. package/dist/server/pages.js +11 -1
  98. package/dist/server/render-chat.js +3 -2
  99. package/dist/server/render-pages.d.ts +15 -2
  100. package/dist/server/render-pages.js +52 -4
  101. package/dist/server/render-results.d.ts +2 -0
  102. package/dist/server/render-results.js +20 -7
  103. package/dist/server/render-tasks.d.ts +9 -0
  104. package/dist/server/render-tasks.js +32 -11
  105. package/dist/server/route-table.js +12 -0
  106. package/dist/server/settings.js +18 -1
  107. package/dist/server/task-views.js +11 -0
  108. package/dist/server/tasks.d.ts +10 -0
  109. package/dist/server/tasks.js +95 -7
  110. package/dist/settings-proofs.js +1 -0
  111. package/dist/store.d.ts +116 -5
  112. package/dist/store.js +385 -24
  113. package/dist/surface.js +18 -2
  114. package/dist/task-archive.d.ts +10 -0
  115. package/dist/task-archive.js +16 -0
  116. package/dist/task-control.d.ts +1 -1
  117. package/dist/task-source.d.ts +13 -1
  118. package/dist/task-source.js +17 -1
  119. package/dist/telegram-lead.js +7 -5
  120. package/dist/telegram.js +1 -1
  121. package/dist/tidy.d.ts +130 -0
  122. package/dist/tidy.js +503 -0
  123. package/dist/toolroll-update.js +2 -2
  124. package/dist/webhooks.d.ts +6 -0
  125. package/dist/webhooks.js +25 -0
  126. package/dist/work-index.d.ts +28 -0
  127. package/dist/work-index.js +82 -28
  128. package/dist/workspace-ui.d.ts +1 -1
  129. package/dist/workspace-ui.js +3 -2
  130. package/dist/youiyoux-intake.d.ts +234 -0
  131. package/dist/youiyoux-intake.js +908 -0
  132. package/package.json +11 -8
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Browser use in a flow's script zone: a per-flow approval a person gives with their own proof, so that zone's launch
3
+ * gets the one sandbox rule a project check gets for Chromium (sandbox-runtime.ts SEATBELT_CHROMIUM) and nothing more.
4
+ *
5
+ * Two halves, both needed. The flow's drawing says which "Run a script" zones start a browser (`browser: true`, part of
6
+ * the flow's digest and said when the flow is confirmed); anyone who may change the flow can say it. Then a person, with
7
+ * their password or sign-in (never a lead's or remote run's token: person-proof.ts), approves browser use for the flow
8
+ * as it stands. The approval seals the flow's digest and, for each browser zone, its script's name and content digest.
9
+ * At launch (flow-steps.ts) the zone gets the rule only while all of that still holds: the same drawing, the same
10
+ * scripts, a person's proof, a signer who still reaches the project. Anything else gets nothing, and the card waits with
11
+ * a plain line naming the missing approval instead of Chromium crashing in the sandbox. Builds, checks, tool servers and
12
+ * every other flow launch (setup, triggers, zones that don't say browser) are untouched.
13
+ */
14
+ import { createHash } from "node:crypto";
15
+ import { canonicalJson } from "./canonical-json.js";
16
+ import { personProof } from "./person-proof.js";
17
+ import { flowDigest, validateFlowDefinition } from "./flows.js";
18
+ /** v144: each approval of browser use, live or ended. `digest` is browserApprovalDigest of what was approved. Additive only. */
19
+ export const FLOW_BROWSER_SCHEMA = `
20
+ CREATE TABLE IF NOT EXISTS flow_browser_grant (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ flow INTEGER NOT NULL,
23
+ repo TEXT NOT NULL,
24
+ flow_revision INTEGER NOT NULL,
25
+ flow_digest TEXT NOT NULL,
26
+ digest TEXT NOT NULL,
27
+ zones_json TEXT NOT NULL,
28
+ signed_by TEXT NOT NULL,
29
+ signed_at TEXT NOT NULL,
30
+ proof TEXT NOT NULL,
31
+ revoked_at TEXT,
32
+ revoked_by TEXT
33
+ );
34
+ CREATE UNIQUE INDEX IF NOT EXISTS flow_browser_live ON flow_browser_grant (flow) WHERE revoked_at IS NULL;
35
+ `;
36
+ function definitionOf(flow) {
37
+ try {
38
+ return validateFlowDefinition(JSON.parse(flow.definitionJson), { stored: true });
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ /** The approval's digest: domain-separated over the whole of its terms, so a changed flow, drawing, revision or any
45
+ * covered script (a restored one is a new version) is a different approval. */
46
+ export function browserApprovalDigest(terms) {
47
+ return createHash("sha256").update(`toolroll:flow-browser:${canonicalJson(terms)}`, "utf8").digest("hex").slice(0, 32);
48
+ }
49
+ /** The flow's script zones that say they start a browser, each with the script it runs now; null when one's script is
50
+ * missing, waiting for approval, or runs a project file (an approval seals the script's own text, never a file the
51
+ * project can change). */
52
+ export function browserZonesOf(store, flow, definition) {
53
+ const out = [];
54
+ for (const stage of definition.stages) {
55
+ if (stage.kind !== "check" || stage.browser !== true)
56
+ continue;
57
+ const script = stage.script === null ? null : store.flowScript(flow.repo, stage.script);
58
+ if (script === null || script.held !== null || script.file !== null)
59
+ return null;
60
+ out.push({ zone: stage.id, script: script.name, version: script.version, digest: script.digest });
61
+ }
62
+ return out;
63
+ }
64
+ /** What an approval of this flow would be about now, read from the store; null when it can't be approved. */
65
+ export function browserTermsOf(store, flow) {
66
+ const definition = definitionOf(flow);
67
+ const zones = definition === null ? null : browserZonesOf(store, flow, definition);
68
+ return definition === null || zones === null ? null : { flow: flow.id, repo: flow.repo, revision: flow.revision, flowDigest: flowDigest(definition), zones };
69
+ }
70
+ function readGrant(row) {
71
+ if (row === undefined)
72
+ return null;
73
+ let zones;
74
+ try {
75
+ zones = JSON.parse(String(row["zones_json"]));
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ if (!Array.isArray(zones) || !zones.every(one => one !== null && typeof one === "object" && typeof one.zone === "string" && typeof one.script === "string" && Number.isInteger(one.version) && typeof one.digest === "string"))
81
+ return null;
82
+ return { id: Number(row["id"]), flow: Number(row["flow"]), repo: String(row["repo"]), revision: Number(row["flow_revision"]), flowDigest: String(row["flow_digest"]), digest: String(row["digest"]),
83
+ zones: zones, signedBy: String(row["signed_by"]), signedAt: String(row["signed_at"]), proof: String(row["proof"]) };
84
+ }
85
+ /** The flow's live approval, as recorded (whether it still stands is browserStanding's question). */
86
+ export function liveBrowserGrant(store, flowId) {
87
+ return readGrant(store.handle.prepare("SELECT * FROM flow_browser_grant WHERE flow = ? AND revoked_at IS NULL").get(flowId));
88
+ }
89
+ /** Whether `name` may approve on `repo` now: a live approver account that reaches the project. */
90
+ function approverNow(store, name, repo) {
91
+ const account = store.accountOf(name);
92
+ return account !== null && account.revokedAt === null && account.role === "approver" && store.accountCanAccess(name, repo);
93
+ }
94
+ /**
95
+ * Whether `zone` of this flow may start a browser now. `run`, when given, is what the caller is about to run — the
96
+ * drawing it read and the very script object it hands the launch — and the answer is about exactly that. The live
97
+ * approval stands only when its digest is the approval this flow would be now (same flow, project, drawing revision and
98
+ * digest, and every browser zone's script name, version and digest), it was a person's, and its signer is still an
99
+ * approver who reaches the project; `run`'s drawing must be that drawing and its script that zone's script. Anything
100
+ * unreadable, changed or mismatched is a no, with the plain words a card waits with.
101
+ */
102
+ export function browserStanding(store, flowId, zone, run) {
103
+ const flow = store.getFlow(flowId);
104
+ const definition = flow === null ? null : definitionOf(flow);
105
+ const stage = definition?.stages.find(one => one.id === zone);
106
+ if (flow === null || definition === null || stage === undefined || stage.kind !== "check" || stage.browser !== true)
107
+ return { allowed: false, said: "This zone doesn't say it starts a browser." };
108
+ const approve = `Approve it with: toolroll flows browser ${flow.id} on`;
109
+ const grant = liveBrowserGrant(store, flow.id);
110
+ if (grant === null)
111
+ return { allowed: false, said: `${stage.title} starts a browser, and no one has approved browser use for the ${flow.name} flow, so it didn't run. ${approve}` };
112
+ const terms = browserTermsOf(store, flow);
113
+ const now = terms?.zones.find(one => one.zone === zone);
114
+ const runStage = run?.definition.stages.find(one => one.id === zone);
115
+ const runs = run === undefined || (terms !== null && now !== undefined && flowDigest(run.definition) === terms.flowDigest && runStage?.kind === "check" && runStage.browser === true
116
+ && runStage.script === run.script.name && run.script.repo === flow.repo && run.script.held === null && run.script.file === null
117
+ && run.script.name === now.script && run.script.version === now.version && run.script.digest === now.digest);
118
+ const stands = runs && terms !== null && now !== undefined && flow.state === "active" && grant.digest === browserApprovalDigest(terms)
119
+ && browserApprovalDigest({ flow: grant.flow, repo: grant.repo, revision: grant.revision, flowDigest: grant.flowDigest, zones: grant.zones }) === grant.digest
120
+ && personProof(grant.proof) && approverNow(store, grant.signedBy, flow.repo);
121
+ return stands ? { allowed: true } : { allowed: false, said: `${stage.title} starts a browser, but browser use was approved for an earlier version of the ${flow.name} flow or its scripts, so it didn't run. ${approve}` };
122
+ }
123
+ /** What approving means, in words the person reads before they give their password. */
124
+ export function browserTerms(zones) {
125
+ return [
126
+ `Lets ${zones.map(one => `the ${one.script} script (zone ${one.zone})`).join(", ")} start Playwright's Chromium headless shell inside the flow's sandbox: the same one macOS service a project check gets for it, and nothing else.`,
127
+ "Changing the flow or one of these scripts (even back to how it was) ends it; nothing else gets it.",
128
+ ];
129
+ }
130
+ /**
131
+ * Approve browser use for the flow as it stands. `proof` is the signer's credential method, proved by the caller just
132
+ * now; only a person's counts (a lead's or remote run's token is refused), from an approver who reaches the project.
133
+ * Re-reads the flow, its zones and scripts in the one transaction that writes; `sawDigest` (required) is the approval
134
+ * (browserApprovalDigest: drawing and scripts) the person read, and anything else is refused. Replaces any live one.
135
+ */
136
+ export function signFlowBrowser(store, input, now) {
137
+ if (!personProof(input.proof))
138
+ return { ok: false, message: "Browser use needs a person's own approval: sign in with your password (a lead's or remote run's token can't give it)." };
139
+ return store.transact(() => {
140
+ const flow = store.getFlow(input.flow);
141
+ if (flow === null || flow.state !== "active")
142
+ return { ok: false, message: "No such flow." };
143
+ if (!approverNow(store, input.signer, flow.repo))
144
+ return { ok: false, message: "Only an approver on this flow's project can approve browser use." };
145
+ if (input.console !== undefined) {
146
+ const account = store.accountOf(input.signer);
147
+ if (account === null || account.generation !== input.console.generation)
148
+ return { ok: false, message: "Your access changed. Sign in again." };
149
+ if (account.projects !== null)
150
+ return { ok: false, message: "Allowing browser use needs an account with access to every project." };
151
+ }
152
+ if (definitionOf(flow) === null)
153
+ return { ok: false, message: "This flow's drawing can't be read." };
154
+ const terms = browserTermsOf(store, flow);
155
+ if (terms === null)
156
+ return { ok: false, message: "A zone that starts a browser has no script it can approve: missing, waiting for approval, or one that runs a project file (write the script on the zone instead)." };
157
+ if (terms.zones.length === 0)
158
+ return { ok: false, message: `No zone in ${flow.name} says it starts a browser. Mark the script zone first (browser: true), then approve.` };
159
+ const digest = browserApprovalDigest(terms);
160
+ if (input.sawDigest !== digest)
161
+ return { ok: false, message: "This flow or its scripts changed while you were reading. Read the terms again." };
162
+ store.handle.prepare("UPDATE flow_browser_grant SET revoked_at = ?, revoked_by = ? WHERE flow = ? AND revoked_at IS NULL").run(now.toISOString(), input.signer, flow.id);
163
+ const id = Number(store.handle.prepare("INSERT INTO flow_browser_grant (flow, repo, flow_revision, flow_digest, digest, zones_json, signed_by, signed_at, proof) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
164
+ .run(flow.id, flow.repo, terms.revision, terms.flowDigest, digest, JSON.stringify(terms.zones), input.signer, now.toISOString(), input.proof).lastInsertRowid);
165
+ store.recordAction({ at: now.toISOString(), actor: input.signer, repo: flow.repo, taskId: null, runId: null, source: "policy", action: "flow browser use: approved", outcome: "signed",
166
+ detail: `${flow.name} (flow #${flow.id}) · ${terms.zones.map(one => `${one.zone}: ${one.script} v${one.version}`).join(", ")}` });
167
+ return { ok: true, grant: { ...terms, id, digest, signedBy: input.signer, signedAt: now.toISOString(), proof: input.proof } };
168
+ });
169
+ }
170
+ /** Any approver on the project ends it, with no password: lowering authority. False when there was none. */
171
+ export function revokeFlowBrowser(store, flowId, by, now) {
172
+ return store.transact(() => {
173
+ const flow = store.getFlow(flowId);
174
+ const ended = store.handle.prepare("UPDATE flow_browser_grant SET revoked_at = ?, revoked_by = ? WHERE flow = ? AND revoked_at IS NULL").run(now.toISOString(), by, flowId).changes > 0;
175
+ if (ended)
176
+ store.recordAction({ at: now.toISOString(), actor: by, repo: flow?.repo ?? null, taskId: null, runId: null, source: "policy", action: "flow browser use: revoked", outcome: "revoked", detail: `flow #${flowId}` });
177
+ return ended;
178
+ });
179
+ }
180
+ const dayWords = (at) => new Date(at).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
181
+ /**
182
+ * Browser use for one flow, read from the store (no writes: page visits call it). Whether it is allowed is exactly
183
+ * browserStanding's answer for the flow's browser zones; the rest only says why in plain words.
184
+ */
185
+ export function browserViewOf(store, flow) {
186
+ const definition = definitionOf(flow);
187
+ const stages = definition?.stages.filter(stage => stage.kind === "check" && stage.browser === true) ?? [];
188
+ if (definition === null || stages.length === 0)
189
+ return null;
190
+ const terms = browserTermsOf(store, flow);
191
+ const grant = liveBrowserGrant(store, flow.id);
192
+ const allowed = grant !== null && stages.every(stage => browserStanding(store, flow.id, stage.id).allowed);
193
+ const zones = (terms?.zones ?? []).map(zone => ({ ...zone, title: definition.stages.find(one => one.id === zone.zone)?.title ?? zone.zone }));
194
+ const problem = terms === null ? "A zone that starts a browser has no script that can be approved: it's missing, waiting for approval, or runs a project file (write the script on the zone instead)." : null;
195
+ const why = grant === null ? "" : terms === null || canonicalJson(grant.zones) !== canonicalJson(terms.zones) ? "the script changed"
196
+ : grant.flowDigest !== terms.flowDigest || grant.revision !== terms.revision ? "the flow changed"
197
+ : !approverNow(store, grant.signedBy, flow.repo) ? `${grant.signedBy} can no longer approve on this project` : "it no longer matches this flow";
198
+ return {
199
+ status: allowed ? "allowed" : grant === null ? "none" : "ended",
200
+ words: allowed ? `Browser: allowed by ${grant.signedBy}, ${dayWords(grant.signedAt)}` : grant === null ? "Browser: not allowed — runs will wait" : `Browser approval ended: ${why}`,
201
+ signedBy: grant?.signedBy ?? null, signedAt: grant?.signedAt ?? null,
202
+ zones, terms: terms === null ? [] : browserTerms(terms.zones), digest: terms === null ? null : browserApprovalDigest(terms), problem,
203
+ };
204
+ }
@@ -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)
@@ -9,6 +9,8 @@ export { LANGUAGE_WORDS, SCRIPT_LANGUAGES, type ScriptLanguage } from "./flows.j
9
9
  export declare const CODE_OUTPUT_CHARS: 12000;
10
10
  export type CodeRun = {
11
11
  script: FlowScriptRow;
12
+ /** Asked right before every spawn attempt (retries too); false refuses it. A browser zone's approval rides here. */
13
+ beforeSpawn?: () => boolean;
12
14
  /** Where it runs, and the project folder a file script is found in. */
13
15
  cwd: string;
14
16
  root: string;
@@ -58,7 +60,8 @@ export declare function cardsFromOutput(stdout: string): {
58
60
  * of its own for its input, an inline script and its temp files; writes as the project's writes setting allows (its
59
61
  * folder, the copy's Git data, its project's caches); the scratch pool denied around its own folder; and its network
60
62
  * open, as flows' scripts have always had it (they reach the services their secrets are for; no approval seals a
61
- * network for them). The fence is required: where this machine can't confine it, it runs only when an instance operator
63
+ * network for them). `browser` (flow-browser.ts browserStanding, proved by the caller at launch) adds Chromium's one
64
+ * service, as a project check has it; nothing else asks for it. The fence is required: where this machine can't confine it, it runs only when an instance operator
62
65
  * allowed unconfined agents, and otherwise nothing runs and the card says why.
63
66
  */
64
67
  export type FlowLaunch = {
@@ -75,6 +78,7 @@ export declare function flowLaunch(store: Store, options: {
75
78
  available?: () => boolean;
76
79
  policy?: FencePolicy;
77
80
  home?: string;
81
+ browser?: true;
78
82
  }): FlowLaunch | null | {
79
83
  problem: string;
80
84
  };
package/dist/flow-code.js CHANGED
@@ -79,6 +79,7 @@ export async function runCode(run) {
79
79
  const ran = await run.shell("/bin/sh", ["-c", 'exec "$@" < "$FLOW_INPUT"', "flow-script", runtime.file, ...runtime.args], {
80
80
  cwd: run.cwd, timeoutMs: script.timeoutMinutes * 60_000, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST, processGroup: true,
81
81
  env: { ...run.env, ...run.secrets, ...(run.launch?.env ?? {}), FLOW_INPUT: inputFile }, ...(run.launch?.fence ?? {}),
82
+ ...(run.beforeSpawn === undefined ? {} : { beforeSpawn: run.beforeSpawn }),
82
83
  });
83
84
  const printed = codeOutput(ran.stdout, run.secrets);
84
85
  const all = scrubSecrets(ran.stdout.slice(0, 1_000_000), run.secrets);
@@ -192,7 +193,9 @@ export function flowLaunch(store, options) {
192
193
  // The project's read denies apply to flows as to builds (read live: a flow seals no list, and these only narrow).
193
194
  const readDeny = store.projectSandbox(options.repo).readDeny.map(one => readDenyPath(one, home));
194
195
  const fence = fenceFor("flow", { databaseFile, worktree: options.folder, home, boundary, readDeny, policy: options.policy ?? 1, scratchRoots: [options.scratch], binding, runFolders: runFolders(own), poolRoots, extra: allowed?.deny ?? [],
195
- writes: allowed === null ? null : { writes: allowed.writes, readOnly: allowed.readOnly }, network: { mode: "open" } });
196
+ writes: allowed === null ? null : { writes: allowed.writes, readOnly: allowed.readOnly }, network: { mode: "open" },
197
+ // Chromium's one service, only for a script zone whose browser use a person approved, proved by the caller now.
198
+ ...(options.browser === true ? { browser: true } : {}) });
196
199
  return { folder: own, env: { ...env, ...(allowed === null ? {} : buildCacheEnv(cache)) }, fence, dispose };
197
200
  }
198
201
  catch {
@@ -23,6 +23,7 @@ import { cardFollowers, notifyPeople } from "./flow-people.js";
23
23
  import { DRAFT_CHARS, keptDraft } from "./flow-draft.js";
24
24
  import { chooseStep, flowPersonOf, sendStep } from "./flow-send.js";
25
25
  import { parseSoul, subagentLabel } from "./subagents.js";
26
+ import { cardTaskArchived, taskArchived } from "./task-archive.js";
26
27
  import { withStageHandoff } from "./contracts/stage-output.js";
27
28
  /** The flow's drawing, or null when it can't be read (the card then says so and waits). */
28
29
  export function flowDefinitionOf(flow) {
@@ -90,6 +91,14 @@ function advanceCard(store, flow, definition, card, now, options, outcome) {
90
91
  return;
91
92
  }
92
93
  const stage = definition.stages.find(one => one.id === card.stage);
94
+ // A put-away task (v145, task-tidy.md) holds its card where it is, whatever the zone: nothing moves on, files, sends
95
+ // or notifies until the task is restored.
96
+ if (cardTaskArchived(store, card.id)) {
97
+ const waiting = "Its task is put away. Restore the task to continue.";
98
+ if (card.waiting !== waiting)
99
+ store.updateFlowCard(card.id, { waiting }, now);
100
+ return;
101
+ }
93
102
  if (stage === undefined) {
94
103
  // The zone was removed while the card sat in it: back to the start.
95
104
  if (store.moveFlowCard(card.id, { to: definition.start, outcome: "moved", actor: "flow", note: null, expectEntry: card.entry }, now))
@@ -311,6 +320,13 @@ function workStage(store, flow, definition, stage, card, now, options, outcome,
311
320
  store.updateFlowCard(card.id, { waiting: "Its task is gone. Move the card to try again." }, now);
312
321
  return;
313
322
  }
323
+ // A put-away task (v145, task-tidy.md) holds its card where it is: nothing moves on, files or sends until it's restored.
324
+ if (taskArchived(store, current) || taskArchived(store, card.task)) {
325
+ const waiting = "Its task is put away. Restore the task to continue.";
326
+ if (card.waiting !== waiting)
327
+ store.updateFlowCard(card.id, { waiting }, now);
328
+ return;
329
+ }
314
330
  if (task.state === "done") {
315
331
  // A build whose project checks failed is done but not good: it takes the failure path, like a failed build.
316
332
  if (stage.kind === "task") {
package/dist/flow-live.js CHANGED
@@ -16,6 +16,7 @@
16
16
  * can't keep one open falls back to reading every few seconds. With nothing
17
17
  * written, an open flow costs no reads; a 30-second check heals a miss.
18
18
  */
19
+ import { browserViewOf } from "./flow-browser.js";
19
20
  import { createHash } from "node:crypto";
20
21
  import { createLiveRooms } from "./live-bus.js";
21
22
  // Statements belong to their database, without keeping closed Stores alive.
@@ -90,7 +91,14 @@ export function flowFingerprint(store, flow, now = new Date()) {
90
91
  // including a build in another project. The page read admits the viewer;
91
92
  // this stream only carries a hash. No evidence files or logs are read.
92
93
  const row = fingerprintStatement(store).get({ $flow: flow, $now: now.toISOString() });
93
- return row === undefined ? null : createHash("sha256").update(Object.values(row).map(String).join("|")).digest("hex").slice(0, 16);
94
+ if (row === undefined)
95
+ return null;
96
+ // Browser use (flow-browser.ts): an approval, a revocation, a changed covered script or a signer who lost standing
97
+ // changes what the page says, so it is part of what open pages are nudged on.
98
+ const saved = store.getFlow(flow);
99
+ const browser = saved === null ? null : browserViewOf(store, saved);
100
+ const browserKey = browser === null ? "" : `${browser.status}:${browser.digest ?? ""}:${browser.signedAt ?? ""}:${browser.problem === null ? "" : "x"}`;
101
+ return createHash("sha256").update([...Object.values(row).map(String), browserKey].join("|")).digest("hex").slice(0, 16);
94
102
  }
95
103
  /** Rooms keyed by flow. With a bus, a write reaches every open page at once; without one, only the safety net. */
96
104
  export function createFlowRooms(fingerprint, options = {}) {
@@ -19,6 +19,7 @@ import { adoptPublishedPullRequest } from "./publish.js";
19
19
  import { failingCheckOf, mergePullRequest, owePullRequest, pullRequestBlocker, pullRequestFollowOf } from "./pull-request-flow.js";
20
20
  import { requestResultChanges } from "./result-actions.js";
21
21
  import { revisionSourceOf } from "./result-review.js";
22
+ import { cardTaskArchived } from "./task-archive.js";
22
23
  /** Whether a person approved this card at a "Person decides" zone since it was last built; their name when so.
23
24
  * The MOST RECENT decision governs (approval policy §1.7): it must be a person's recorded decision, after the card's
24
25
  * last build by the current definition AND after the exact build being merged finished (`builtAt`), so editing the
@@ -41,19 +42,23 @@ export function approvedSinceBuild(store, definition, card, builtAt = null) {
41
42
  /** One pass of a Pull request zone for one card. Returns whether it changed anything. */
42
43
  export async function pullRequestStep(store, flow, definition, stage, card, now, io) {
43
44
  const titleOf = (id) => definition.stages.find(one => one.id === id)?.title ?? "another zone";
44
- const waiting = (said) => {
45
+ // Every write after GitHub answers re-proves, in its own transaction, that the card's work wasn't put away meanwhile
46
+ // (v145): a put away that committed first leaves the card where it is, saying nothing to anyone.
47
+ const unlessHeld = (write) => store.transact(() => cardTaskArchived(store, card.id) ? false : write());
48
+ const waiting = (said) => unlessHeld(() => {
45
49
  if (card.waiting === said)
46
50
  return false;
47
51
  store.updateFlowCard(card.id, { waiting: said }, now);
48
52
  return true;
49
- };
50
- const passed = (said, output) => {
53
+ });
54
+ const passed = (said, output) => unlessHeld(() => {
51
55
  store.updateFlowCard(card.id, { outputs: { ...card.outputs, [stage.id]: output }, waiting: null }, now);
52
56
  if (stage.next === null)
53
57
  return waiting(`${said} Move the card on when you're ready.`);
54
58
  return store.moveFlowCard(card.id, { to: stage.next, outcome: "ok", actor: "flow", historyNote: said, expectEntry: card.entry }, now);
55
- };
56
- const failed = (said, revision = null) => {
59
+ });
60
+ const failed = (said, revision = null) => unlessHeld(() => failedNow(said, revision));
61
+ const failedNow = (said, revision = null) => {
57
62
  store.updateFlowCard(card.id, { outputs: { ...card.outputs, [stage.id]: said } }, now);
58
63
  if (stage.onFail === null) {
59
64
  if (!waiting(`${said} Fix it, then move the card to try again.`))
@@ -68,13 +73,13 @@ export async function pullRequestStep(store, flow, definition, stage, card, now,
68
73
  };
69
74
  // Publishing that can't proceed is not the build's fault: rebuilding wouldn't change it, so the card waits here,
70
75
  // saying what to do, and the people on it hear once. It carries on by itself once the cause is fixed.
71
- const stuck = (said) => {
76
+ const stuck = (said) => unlessHeld(() => {
72
77
  if (!waiting(said))
73
78
  return false;
74
79
  const people = card.owner === null ? cardFollowers(store, card) : [card.owner];
75
80
  notifyPeople(store, card, people, null, { key: `pr-stuck:${card.entry}`, subject: `${stage.title} is waiting on you for “${card.title}”`, attention: true, body: said }, now);
76
81
  return true;
77
- };
82
+ });
78
83
  // With no failure path, a card that didn't pass waits for a person to move it (a move is a fresh visit).
79
84
  if (stage.onFail === null && card.waiting !== null && card.waiting.endsWith(" Fix it, then move the card to try again."))
80
85
  return false;
@@ -128,6 +133,9 @@ export async function pullRequestStep(store, flow, definition, stage, card, now,
128
133
  // Back to a Build zone: a revision of the same work, carrying the failure (fenced as untrusted CI output).
129
134
  let revision = null;
130
135
  const target = definition.stages.find(one => one.id === stage.onFail);
136
+ // No revision of work put away while GitHub answered (v145).
137
+ if (cardTaskArchived(store, card.id))
138
+ return false;
131
139
  if (target?.kind === "task" && io.evidenceRoot !== undefined) {
132
140
  const runTask = store.externalIdFor(store.getRun(runId)?.taskRef ?? -1);
133
141
  const revised = runTask === null ? null : requestResultChanges(store, io.evidenceRoot, {
@@ -54,6 +54,8 @@ function stepOf(stage, ask) {
54
54
  step["runIn"] = stage.runIn;
55
55
  if (stage.secrets !== undefined)
56
56
  step["secrets"] = stage.secrets;
57
+ if (stage.browser === true)
58
+ step["browser"] = true;
57
59
  }
58
60
  if (stage.routes !== undefined)
59
61
  step["routes"] = stage.routes.map(one => ({ answer: one.answer, goesTo: one.to }));
@@ -299,7 +301,7 @@ function stageInputOf(step, index, find) {
299
301
  ...(toOwner ? { toOwner: true } : { approver: anyone ? null : decider }),
300
302
  message: step["message"], close: step["close"], script: step["script"], runIn: step["runIn"],
301
303
  routes: list("routes")?.map((one, n) => ({ answer: one["answer"], to: find(one["goesTo"], `${at}.routes[${n}].goesTo`) })),
302
- secrets: step["secrets"],
304
+ secrets: step["secrets"], browser: step["browser"],
303
305
  ...(kind === "sort" ? { sort: { question: step["question"], answers: (list("answers") ?? []).map((one, n) => ({ answer: one["answer"], means: one["means"], to: find(one["goesTo"], `${at}.answers[${n}].goesTo`) })),
304
306
  sureAt: typeof sureAt === "number" && sureAt > 1 ? sureAt / 100 : sureAt, notes: list("alsoNote") ?? [] } } : {}),
305
307
  ...(kind === "request" ? { request: { method: step["method"], url: step["url"], headers: object("headers") ?? {}, body: step["body"] } } : {}),
@@ -1,7 +1,7 @@
1
1
  import type { Runner } from "./backend.js";
2
2
  import { type DraftRunner } from "./flow-draft.js";
3
3
  import { type MailSender, type ToolCaller } from "./flow-actions.js";
4
- import type { Store } from "./store.js";
4
+ import type { FlowCardRow, FlowStepKind, Store } from "./store.js";
5
5
  import type { TurnRunner } from "./subagents.js";
6
6
  import type { ToolLister } from "./subagent-tools.js";
7
7
  export type StepIo = {
@@ -42,3 +42,11 @@ export type StepPass = {
42
42
  };
43
43
  /** Run every due check and update in one project, then move each card on. */
44
44
  export declare function runFlowSteps(store: Store, repo: string, now: Date, io: StepIo): Promise<StepPass>;
45
+ /** Claim this card's visit to its step zone, only while the card is still there on that visit: a pass that read the card
46
+ * before another pass (or a person) moved it never runs the step for a visit that's over. */
47
+ export declare function claimStep(store: Store, card: FlowCardRow, step: {
48
+ stage: string;
49
+ kind: FlowStepKind;
50
+ script: string | null;
51
+ scriptVersion: number | null;
52
+ }, now: Date): boolean;