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
@@ -1,5 +1,6 @@
1
1
  import type { Store } from "./store.js";
2
2
  import type { VerifiedApprover } from "./principal.js";
3
+ import { type FeedbackImage } from "./result-review.js";
3
4
  export type ReviewSnapshot = {
4
5
  task: string;
5
6
  root: string;
@@ -9,11 +10,13 @@ export type ReviewSnapshot = {
9
10
  sha: string;
10
11
  source: string;
11
12
  title: string;
13
+ images?: FeedbackImage[];
12
14
  notes: {
13
15
  id: number;
14
16
  note: string;
15
17
  path: string | null;
16
18
  line: number | null;
19
+ image?: FeedbackImage | null;
17
20
  }[];
18
21
  };
19
22
  type Problem = {
@@ -32,6 +35,7 @@ export type ReviewRequest = {
32
35
  path: string | null;
33
36
  line: number | null;
34
37
  notes: number[];
38
+ image?: FeedbackImage | null;
35
39
  };
36
40
  export declare function reviewInputProblem(note: string | null, path: string | null, line: number | null): string | null;
37
41
  /** Chat and the result form write the same comments and call the same revision service.
@@ -1,7 +1,8 @@
1
+ import { selectResultImages } from "./chat-evidence.js";
1
2
  import { readVerifiedArtifact } from "./evidence.js";
2
3
  import { hasForbiddenControls, validateNote } from "./decision.js";
3
- import { isRevisionFeedback, revisionSourceOf } from "./result-review.js";
4
- import { createResultRevision } from "./result-actions.js";
4
+ import { parseFeedbackImage, isRevisionFeedback, revisionSourceOf } from "./result-review.js";
5
+ import { createResultRevision, feedbackImageProblem } from "./result-actions.js";
5
6
  /** Read precisely the result named, never silently substitute a newer version. */
6
7
  export function readChatResult(store, who, evidenceRoot, task, run) {
7
8
  const ref = store.lookupRef(task);
@@ -21,11 +22,13 @@ export function readChatResult(store, who, evidenceRoot, task, run) {
21
22
  const proven = readVerifiedArtifact(evidenceRoot, artifact);
22
23
  if (!proven.ok)
23
24
  return { ok: false, message: "The saved changes could not be verified. Restore the result before adding feedback." };
25
+ const images = selectResultImages(store, who, evidenceRoot, task, found.id);
24
26
  return { ok: true, snapshot: {
25
27
  task, root: family.root.id, execution: family.current.id, run: found.id,
26
28
  artifact: artifact.id, sha: artifact.sha256, source: revisionSourceOf(store.getScope(task)?.digest ?? null),
27
29
  title: store.getTask(task)?.title ?? task,
28
- notes: store.liveDiffComments(found.id).filter(isRevisionFeedback).map(one => ({ id: one.id, note: one.note, path: one.path, line: one.line })),
30
+ images: images.ok ? images.selection.images.map(one => ({ run: found.id, artifact: one.artifact, sha256: one.sha256 })) : [],
31
+ notes: store.liveDiffComments(found.id).filter(isRevisionFeedback).map(one => ({ id: one.id, note: one.note, path: one.path, line: one.line, image: one.image ?? null })),
29
32
  } };
30
33
  }
31
34
  export function reviewInputProblem(note, path, line) {
@@ -65,9 +68,15 @@ export function applyChatReview(store, who, evidenceRoot, request, now, allowMod
65
68
  return fail("Some feedback changed or was already used. Review it again.");
66
69
  if (operation === "note" && (note === null || notes.length > 0))
67
70
  return fail("A saved note needs feedback text and cannot consume other notes.");
71
+ const image = parseFeedbackImage(request.image);
72
+ if (image === false || (image !== null && note === null))
73
+ return fail("Choose a screenshot and write its feedback again.");
74
+ const imageProblem = feedbackImageProblem(store, evidenceRoot, live.run, image);
75
+ if (imageProblem !== null)
76
+ return fail(imageProblem);
68
77
  const batch = [...notes].sort((a, b) => a - b);
69
78
  if (note !== null) {
70
- const id = store.addDiffComment({ artifactId: live.artifact, runId: live.run, path, line, note, author: who.name }, now);
79
+ const id = store.addDiffComment({ artifactId: live.artifact, runId: live.run, path, line, note, author: who.name, image }, now);
71
80
  if (id === null)
72
81
  return fail("The note could not be saved.");
73
82
  batch.push(id);
@@ -0,0 +1,9 @@
1
+ import type { Scope } from "./scope.js";
2
+ /** What a plan's yes allows an agent to do, in a few plain words (the approval sheet's "You're allowing"). */
3
+ export declare function permissionPlainWords(profile: Scope["profile"] | null | undefined): string | null;
4
+ export type ShownScope = Pick<Scope, "goal" | "outOfScope" | "touches" | "acceptance" | "profile" | "budgetMicrousd">;
5
+ /** Every word of the terms a yes agrees to, as the card shows them: the goal, what's excluded, the files (or plainly any
6
+ * file), each requirement with the evidence it needs, and what the agent may do and spend. Nothing is shortened. */
7
+ export declare function scopeTermsText(scope: ShownScope): string;
8
+ /** The digest of the terms shown, bound to the scope's own digest: a changed word, limit or route is another digest. */
9
+ export declare function shownTermsDigest(scope: ShownScope & Pick<Scope, "digest">): string;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A scope's terms as a chat card shows them whole, and the digest of exactly those words. A task filed from outside
3
+ * text (a youiyoux change request) approves from chat only on a card that showed every word of its terms, nothing cut:
4
+ * the card binds that digest to its button, and the seal (store.ts, the outside-request quarantine) recomputes it from
5
+ * the live scope and refuses a tap that names anything else. A leaf: the store and the chat road both read it.
6
+ */
7
+ import { createHash } from "node:crypto";
8
+ import { isAcpProfile } from "./acp-agents.js";
9
+ /** What a plan's yes allows an agent to do, in a few plain words (the approval sheet's "You're allowing"). */
10
+ export function permissionPlainWords(profile) {
11
+ if (profile === null || profile === undefined)
12
+ return null;
13
+ const full = "full access, nothing asks first";
14
+ if (profile.provider === "claude") {
15
+ return profile.permissionArgv === "bypassPermissions" ? full
16
+ : profile.permissionArgv === "auto" ? "file edits and routine commands; anything risky stops"
17
+ : "file edits only; commands are refused";
18
+ }
19
+ if (profile.provider === "gemini")
20
+ return profile.approvalArgv === "yolo" ? full : "file edits only; other tools are refused";
21
+ if (isAcpProfile(profile))
22
+ return "full access inside the OS fence, nothing asks first";
23
+ return profile.sandboxMode === "danger-full-access" ? full : "file edits and commands inside the project only";
24
+ }
25
+ const money = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
26
+ /** Every word of the terms a yes agrees to, as the card shows them: the goal, what's excluded, the files (or plainly any
27
+ * file), each requirement with the evidence it needs, and what the agent may do and spend. Nothing is shortened. */
28
+ export function scopeTermsText(scope) {
29
+ const allows = [permissionPlainWords(scope.profile) ?? "agent permissions not known", scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`];
30
+ return [
31
+ "Goal:", scope.goal,
32
+ "",
33
+ "Not this:", scope.outOfScope ?? "Nothing excluded.",
34
+ "",
35
+ `Files: ${scope.touches.length === 0 ? "any file (no file list)" : scope.touches.join(", ")}`,
36
+ "",
37
+ "Done when:",
38
+ ...(scope.acceptance.length === 0 ? ["You decide when you review the result."] : scope.acceptance.map(one => `• ${one.statement} (${one.evidence.join(", ")})`)),
39
+ "",
40
+ `Starting allows: ${allows.join(" · ")}`,
41
+ ].join("\n");
42
+ }
43
+ /** The digest of the terms shown, bound to the scope's own digest: a changed word, limit or route is another digest. */
44
+ export function shownTermsDigest(scope) {
45
+ return createHash("sha256").update(JSON.stringify({ v: 1, scope: scope.digest, shown: scopeTermsText(scope) })).digest("hex");
46
+ }
@@ -102,6 +102,7 @@ export declare const CONSOLE_FORMS: {
102
102
  readonly data: FormContract<"password">;
103
103
  readonly flowsOn: FormContract<"repo" | "starter">;
104
104
  readonly integrationsTest: FormContract<"key">;
105
+ readonly integrationsYouiyoux: FormContract<"on" | "repo">;
105
106
  readonly monitoring: FormContract<"folder" | "header-name" | "header-value" | "password" | "rotate" | "traces" | "webhook">;
106
107
  readonly toolsConnect: FormContract<"access" | "also" | "kit" | "password" | "repo" | "service" | "shown" | "template">;
107
108
  readonly toolsChange: FormContract<"action" | "catalog" | "import" | "name" | "password" | "repo" | "secret" | "secrets" | "shown" | "target" | "transport" | "value">;
@@ -168,7 +169,7 @@ export declare const CONSOLE_FORMS: {
168
169
  readonly chatAck: FormContract<"nonce" | "token">;
169
170
  readonly recipes: FormContract<"document" | "next" | "preview" | "projectRevision" | "purpose" | "recipe" | "recipeRevision" | "repo" | "source" | "sourceRevision">;
170
171
  readonly runNote: FormContract<"note">;
171
- readonly diffComment: FormContract<"batch" | "intent" | "line" | "note" | "path" | "request" | "return" | "source" | "tab">;
172
+ readonly diffComment: FormContract<"batch" | "comment" | "image" | "intent" | "line" | "note" | "path" | "request" | "return" | "source" | "tab">;
172
173
  readonly editorLinks: FormContract<"on" | "return">;
173
174
  readonly revise: FormContract<"batch" | "return" | "source">;
174
175
  readonly followUp: FormContract<"level" | "return">;
@@ -111,6 +111,7 @@ export const CONSOLE_FORMS = {
111
111
  data: formContract("POST /settings/data", ["password"]),
112
112
  flowsOn: formContract("POST /settings/flows/on", ["repo", "starter"]),
113
113
  integrationsTest: formContract("POST /settings/integrations/test", ["key"]),
114
+ integrationsYouiyoux: formContract("POST /settings/integrations/youiyoux", ["repo", "on"]),
114
115
  monitoring: formContract("POST /settings/monitoring", ["password", "webhook", "folder", "traces", "header-name", "header-value", "rotate"]),
115
116
  toolsConnect: formContract("POST /settings/tools/connect", ["repo", "also", "service", "template", "kit", "shown", "password", "access"]),
116
117
  toolsChange: formContract("POST /settings/tools/change", ["repo", "shown", "action", "name", "password", "catalog", "transport", "target", "secrets", "import", "secret", "value"]),
@@ -182,7 +183,7 @@ export const CONSOLE_FORMS = {
182
183
  // A recipe's own answers and editor fields are read by recipes.ts from the whole form (`sent`; item 17).
183
184
  recipes: formContract("POST /recipes/prepare, /recipes/preview, /recipes/import, /recipes/launch, /recipes/save", ["repo", "projectRevision", "recipe", "recipeRevision", "document", "purpose", "source", "sourceRevision", "preview", "next"]),
184
185
  runNote: formContract("POST /r/<run>/note", ["note"]),
185
- diffComment: formContract("POST /r/<run>/comment", ["return", "tab", "intent", "batch", "source", "note", "path", "line", "request"]),
186
+ diffComment: formContract("POST /r/<run>/comment", ["return", "tab", "intent", "batch", "source", "note", "path", "line", "request", "image", "comment"]),
186
187
  editorLinks: formContract("POST /session/editor-links", ["on", "return"]),
187
188
  revise: formContract("POST /r/<run>/revise", ["return", "batch", "source"]),
188
189
  followUp: formContract("POST /r/<run>/checks|add-tests", ["return", "level"]),
@@ -36,6 +36,10 @@ export declare const LEAD_TOOL_INPUTS: {
36
36
  readonly get_brief: z.ZodObject<{
37
37
  repo: z.ZodOptional<z.ZodString>;
38
38
  }, z.core.$strict>;
39
+ readonly get_project_activity: z.ZodObject<{
40
+ repo: z.ZodString;
41
+ offset: z.ZodOptional<z.ZodInt>;
42
+ }, z.core.$strict>;
39
43
  readonly get_project_context: z.ZodObject<{
40
44
  repo: z.ZodString;
41
45
  query: z.ZodString;
@@ -130,6 +134,7 @@ export declare const LEAD_TOOL_INPUTS: {
130
134
  path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
131
135
  line: z.ZodOptional<z.ZodNullable<z.ZodInt>>;
132
136
  saved_notes: z.ZodOptional<z.ZodArray<z.ZodInt>>;
137
+ image: z.ZodOptional<z.ZodNullable<z.ZodInt>>;
133
138
  }, z.core.$strict>;
134
139
  readonly recap: z.ZodObject<{
135
140
  since: z.ZodOptional<z.ZodString>;
@@ -762,6 +767,10 @@ export declare const LEAD_TOOL_READ_INPUTS: {
762
767
  readonly get_brief: z.ZodObject<{
763
768
  repo: z.ZodOptional<z.ZodString>;
764
769
  }, z.core.$strict>;
770
+ readonly get_project_activity: z.ZodObject<{
771
+ repo: z.ZodString;
772
+ offset: z.ZodOptional<z.ZodInt>;
773
+ }, z.core.$strict>;
765
774
  readonly get_project_context: z.ZodObject<{
766
775
  repo: z.ZodString;
767
776
  query: z.ZodString;
@@ -856,6 +865,7 @@ export declare const LEAD_TOOL_READ_INPUTS: {
856
865
  path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
857
866
  line: z.ZodOptional<z.ZodNullable<z.ZodInt>>;
858
867
  saved_notes: z.ZodOptional<z.ZodArray<z.ZodInt>>;
868
+ image: z.ZodOptional<z.ZodNullable<z.ZodInt>>;
859
869
  }, z.core.$strict>;
860
870
  readonly recap: z.ZodObject<{
861
871
  since: z.ZodOptional<z.ZodString>;
@@ -1556,6 +1566,26 @@ export declare const LEAD_TOOL_OUTPUTS: {
1556
1566
  projects: z.ZodArray<z.ZodUnknown>;
1557
1567
  omissions: z.ZodObject<{}, z.core.$loose>;
1558
1568
  }, z.core.$loose>;
1569
+ readonly get_project_activity: z.ZodObject<{
1570
+ asOf: z.ZodString;
1571
+ since: z.ZodString;
1572
+ days: z.ZodNumber;
1573
+ limit: z.ZodNumber;
1574
+ truncated: z.ZodBoolean;
1575
+ ready: z.ZodObject<{
1576
+ count: z.ZodNumber;
1577
+ oldestFinishedAgeMs: z.ZodNullable<z.ZodNumber>;
1578
+ dated: z.ZodNumber;
1579
+ }, z.core.$strip>;
1580
+ completed: z.ZodObject<{
1581
+ count: z.ZodNumber;
1582
+ }, z.core.$loose>;
1583
+ families: z.ZodArray<z.ZodUnknown>;
1584
+ familiesTotal: z.ZodNumber;
1585
+ familiesOffset: z.ZodNumber;
1586
+ nextOffset: z.ZodNullable<z.ZodNumber>;
1587
+ incompleteHistory: z.ZodNumber;
1588
+ }, z.core.$loose>;
1559
1589
  readonly get_project_context: z.ZodObject<{
1560
1590
  version: z.ZodLiteral<1>;
1561
1591
  readOnly: z.ZodBoolean;
@@ -52,6 +52,7 @@ export const LISTED_TASK_STATES = ["queued", "running", "done", "failed", "cance
52
52
  const ACTION_OPERATIONS = Object.keys(CHAT_ACTIONS).filter(one => !one.startsWith("flow_") && !one.startsWith("subagent_"));
53
53
  export const LEAD_TOOL_INPUTS = {
54
54
  get_brief: z.strictObject({ repo: repoId.optional() }),
55
+ get_project_activity: z.strictObject({ repo: repoId, offset: offset.optional() }),
55
56
  get_project_context: z.strictObject({ repo: repoId, query: text("contextQuery").min(1), mode: z.enum(["search", "impact"]).optional() }),
56
57
  get_action_status: z.strictObject({ proposal: id }),
57
58
  get_actions: z.strictObject({}),
@@ -77,6 +78,7 @@ export const LEAD_TOOL_INPUTS = {
77
78
  note: z.string().max(LIMITS.note).nullable().optional(), path: text("diffPath").nullable().optional(),
78
79
  line: z.int().min(1).max(1_000_000).nullable().optional(),
79
80
  saved_notes: z.array(id).max(100).optional(),
81
+ image: id.nullable().optional().describe("Screenshot artifact id from get_result_images; attaches the exact verified image to this feedback. null leaves off the screenshot a reply targets."),
80
82
  }),
81
83
  recap: z.strictObject({ since: text("recapSince").optional() }),
82
84
  list_repos: z.strictObject({}),
@@ -196,6 +198,7 @@ export const decisionOutput = z.looseObject({ repo: str, decision: int, task: st
196
198
  export const queueColumnsOutput = { columns: rows({ column: str, tasks: list }) };
197
199
  export const LEAD_TOOL_OUTPUTS = {
198
200
  get_brief: catchUpOutput,
201
+ get_project_activity: z.looseObject({ asOf: str, since: str, days: int, limit: int, truncated: z.boolean(), ready: z.object({ count: int, oldestFinishedAgeMs: nullable(int), dated: int }), completed: z.looseObject({ count: int }), families: list, familiesTotal: int, familiesOffset: int, nextOffset: nullable(int), incompleteHistory: int }),
199
202
  get_project_context: repositoryContextOutput,
200
203
  get_action_status: z.looseObject({ proposal: int, operation: str, state: str, outcome: z.unknown(), finishedAt: nullable(str) }),
201
204
  get_actions: z.looseObject({ actions: rows({ operation: str, label: str, secureReview: z.boolean(), inputs: z.array(str) }), notice: str }),
@@ -0,0 +1,248 @@
1
+ /**
2
+ * A youiyoux request as Toolroll reads it (youiyoux-intake.ts): the published `youi.request/v1` schema (youiyoux's
3
+ * schema/youi.request.schema.json), held to its own limits, and the `youi.proposal/v1` options file an options request's
4
+ * agent writes (schema/youi.proposal.schema.json). Unknown keys are allowed, as the published schemas allow them;
5
+ * everything Toolroll reads is checked here, and every string is outside text all the same.
6
+ */
7
+ import { z } from "zod";
8
+ import { type ContractResult } from "./contract.js";
9
+ export declare const REQUEST_ID: RegExp;
10
+ /** An options file's id: `p-` and the same 10 hex as the options request it answers. */
11
+ export declare const PROPOSAL_ID: RegExp;
12
+ /** The provenance every task filed from a request carries (task_ref.filed_via): immutable, and the seal reads it. */
13
+ export declare const YOUIYOUX_FILED_VIA = "youiyoux";
14
+ /** The one chat card a pass's filings make for a project (youiyoux-intake.ts), drawn by chat-decide.ts: the new tasks,
15
+ * Approve all and each one's own Approve and Open. */
16
+ export declare const YOUIYOUX_CARD_KIND = "youiyoux-requests";
17
+ /** Where a person sees and approves everything waiting on them in the console (flow-triggers.ts NEEDS_YOU_HREF). */
18
+ export declare const YOUIYOUX_HOME_PATH = "/work?view=needs-you";
19
+ export declare const YOUIYOUX_EVIDENCE: readonly ["screenshot", "check", "changed-path", "manual-review"];
20
+ /** What an options request offers its agent: the one file to write (`.youi/proposals/<proposal>.json`), the props an
21
+ * option may set (enum values from `values`), and whether it may give the component new text. */
22
+ export declare const youiyouxOfferSchema: z.ZodObject<{
23
+ proposal: z.ZodString;
24
+ file: z.ZodString;
25
+ choices: z.ZodRecord<z.ZodString, z.ZodObject<{
26
+ type: z.ZodEnum<{
27
+ boolean: "boolean";
28
+ enum: "enum";
29
+ number: "number";
30
+ string: "string";
31
+ }>;
32
+ values: z.ZodOptional<z.ZodArray<z.ZodString>>;
33
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
34
+ }, z.core.$strip>>;
35
+ text: z.ZodBoolean;
36
+ }, z.core.$strip>;
37
+ export type YouiyouxOffer = z.infer<typeof youiyouxOfferSchema>;
38
+ export declare const youiyouxTaskSchema: z.ZodObject<{
39
+ title: z.ZodString;
40
+ goal: z.ZodString;
41
+ touches: z.ZodArray<z.ZodString>;
42
+ touchesKnown: z.ZodBoolean;
43
+ why: z.ZodOptional<z.ZodString>;
44
+ not: z.ZodArray<z.ZodString>;
45
+ acceptance: z.ZodArray<z.ZodObject<{
46
+ statement: z.ZodString;
47
+ evidence: z.ZodArray<z.ZodEnum<{
48
+ "changed-path": "changed-path";
49
+ check: "check";
50
+ "manual-review": "manual-review";
51
+ screenshot: "screenshot";
52
+ }>>;
53
+ }, z.core.$strip>>;
54
+ offer: z.ZodOptional<z.ZodObject<{
55
+ proposal: z.ZodString;
56
+ file: z.ZodString;
57
+ choices: z.ZodRecord<z.ZodString, z.ZodObject<{
58
+ type: z.ZodEnum<{
59
+ boolean: "boolean";
60
+ enum: "enum";
61
+ number: "number";
62
+ string: "string";
63
+ }>;
64
+ values: z.ZodOptional<z.ZodArray<z.ZodString>>;
65
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
66
+ }, z.core.$strip>>;
67
+ text: z.ZodBoolean;
68
+ }, z.core.$strip>>;
69
+ }, z.core.$strip>;
70
+ export declare const youiyouxRequestSchema: z.ZodObject<{
71
+ schema: z.ZodLiteral<"youi.request/v1">;
72
+ id: z.ZodString;
73
+ project: z.ZodString;
74
+ kind: z.ZodEnum<{
75
+ change: "change";
76
+ comment: "comment";
77
+ options: "options";
78
+ }>;
79
+ status: z.ZodEnum<{
80
+ addressed: "addressed";
81
+ closed: "closed";
82
+ open: "open";
83
+ }>;
84
+ body: z.ZodString;
85
+ author: z.ZodObject<{
86
+ kind: z.ZodEnum<{
87
+ agent: "agent";
88
+ person: "person";
89
+ }>;
90
+ name: z.ZodString;
91
+ }, z.core.$strip>;
92
+ createdAt: z.ZodString;
93
+ updatedAt: z.ZodString;
94
+ target: z.ZodObject<{
95
+ route: z.ZodString;
96
+ scope: z.ZodEnum<{
97
+ element: "element";
98
+ here: "here";
99
+ }>;
100
+ name: z.ZodString;
101
+ file: z.ZodOptional<z.ZodString>;
102
+ line: z.ZodOptional<z.ZodInt>;
103
+ }, z.core.$strip>;
104
+ task: z.ZodOptional<z.ZodObject<{
105
+ title: z.ZodString;
106
+ goal: z.ZodString;
107
+ touches: z.ZodArray<z.ZodString>;
108
+ touchesKnown: z.ZodBoolean;
109
+ why: z.ZodOptional<z.ZodString>;
110
+ not: z.ZodArray<z.ZodString>;
111
+ acceptance: z.ZodArray<z.ZodObject<{
112
+ statement: z.ZodString;
113
+ evidence: z.ZodArray<z.ZodEnum<{
114
+ "changed-path": "changed-path";
115
+ check: "check";
116
+ "manual-review": "manual-review";
117
+ screenshot: "screenshot";
118
+ }>>;
119
+ }, z.core.$strip>>;
120
+ offer: z.ZodOptional<z.ZodObject<{
121
+ proposal: z.ZodString;
122
+ file: z.ZodString;
123
+ choices: z.ZodRecord<z.ZodString, z.ZodObject<{
124
+ type: z.ZodEnum<{
125
+ boolean: "boolean";
126
+ enum: "enum";
127
+ number: "number";
128
+ string: "string";
129
+ }>;
130
+ values: z.ZodOptional<z.ZodArray<z.ZodString>>;
131
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
132
+ }, z.core.$strip>>;
133
+ text: z.ZodBoolean;
134
+ }, z.core.$strip>>;
135
+ }, z.core.$strip>>;
136
+ thread: z.ZodArray<z.ZodUnknown>;
137
+ }, z.core.$strip>;
138
+ export type YouiyouxRequest = z.infer<typeof youiyouxRequestSchema>;
139
+ export type YouiyouxTask = z.infer<typeof youiyouxTaskSchema>;
140
+ export declare function readYouiyouxRequest(raw: unknown): ContractResult<YouiyouxRequest>;
141
+ /** The one file an options request's agent may write: `.youi/proposals/p-<the request's hex>.json`. */
142
+ export declare const proposalIdOf: (request: string) => string;
143
+ export declare const proposalFileOf: (proposal: string) => string;
144
+ /** Why an options request can't be filed as youiyoux's contract has it, or null: it carries a task with an offer whose
145
+ * proposal is `p-<its own hex>`, whose file is that proposal's, and the task may touch that one file and nothing else. */
146
+ export declare function optionsProblem(request: YouiyouxRequest): string | null;
147
+ export declare const PROPOSAL_STATUSES: readonly ["open", "chosen", "dismissed"];
148
+ export declare const youiyouxProposalSchema: z.ZodObject<{
149
+ $schema: z.ZodOptional<z.ZodString>;
150
+ schema: z.ZodLiteral<"youi.proposal/v1">;
151
+ id: z.ZodString;
152
+ project: z.ZodString;
153
+ route: z.ZodString;
154
+ target: z.ZodObject<{
155
+ name: z.ZodString;
156
+ element: z.ZodOptional<z.ZodString>;
157
+ file: z.ZodOptional<z.ZodString>;
158
+ line: z.ZodOptional<z.ZodInt>;
159
+ column: z.ZodOptional<z.ZodInt>;
160
+ box: z.ZodObject<{
161
+ x: z.ZodNumber;
162
+ y: z.ZodNumber;
163
+ width: z.ZodNumber;
164
+ height: z.ZodNumber;
165
+ }, z.core.$strip>;
166
+ page: z.ZodOptional<z.ZodObject<{
167
+ width: z.ZodNumber;
168
+ height: z.ZodNumber;
169
+ }, z.core.$strip>>;
170
+ }, z.core.$strip>;
171
+ ask: z.ZodString;
172
+ author: z.ZodObject<{
173
+ kind: z.ZodEnum<{
174
+ agent: "agent";
175
+ person: "person";
176
+ }>;
177
+ name: z.ZodString;
178
+ }, z.core.$strip>;
179
+ createdAt: z.ZodString;
180
+ updatedAt: z.ZodString;
181
+ options: z.ZodArray<z.ZodObject<{
182
+ id: z.ZodEnum<{
183
+ a: "a";
184
+ b: "b";
185
+ c: "c";
186
+ d: "d";
187
+ }>;
188
+ label: z.ZodString;
189
+ why: z.ZodString;
190
+ set: z.ZodArray<z.ZodObject<{
191
+ prop: z.ZodString;
192
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
193
+ }, z.core.$strip>>;
194
+ text: z.ZodNullable<z.ZodString>;
195
+ }, z.core.$strip>>;
196
+ status: z.ZodEnum<{
197
+ chosen: "chosen";
198
+ dismissed: "dismissed";
199
+ open: "open";
200
+ }>;
201
+ choice: z.ZodOptional<z.ZodEnum<{
202
+ a: "a";
203
+ b: "b";
204
+ c: "c";
205
+ d: "d";
206
+ }>>;
207
+ decidedBy: z.ZodOptional<z.ZodObject<{
208
+ kind: z.ZodEnum<{
209
+ agent: "agent";
210
+ person: "person";
211
+ }>;
212
+ name: z.ZodString;
213
+ }, z.core.$strip>>;
214
+ decidedAt: z.ZodOptional<z.ZodString>;
215
+ request: z.ZodOptional<z.ZodString>;
216
+ for: z.ZodString;
217
+ }, z.core.$strip>;
218
+ export type YouiyouxProposal = z.infer<typeof youiyouxProposalSchema>;
219
+ export declare function readYouiyouxProposal(raw: unknown): ContractResult<YouiyouxProposal>;
220
+ /** What Toolroll writes beside a request (`youi.request-task/v1`): the task it became, how that task stands, and, when
221
+ * the console has an address, the task's link and `home`, where a person sees and approves all waiting work. */
222
+ export declare const YOUIYOUX_TASK_STATUSES: readonly ["queued", "needs-you", "building", "ready", "done", "failed", "stopped"];
223
+ export type YouiyouxTaskStatus = (typeof YOUIYOUX_TASK_STATUSES)[number];
224
+ /** A link youiyoux opens: http or https, at most 2,000 characters, with no user name or password in it. */
225
+ export declare const youiyouxLinkSchema: z.ZodString;
226
+ export declare const youiyouxRequestTaskSchema: z.ZodObject<{
227
+ schema: z.ZodLiteral<"youi.request-task/v1">;
228
+ request: z.ZodString;
229
+ runner: z.ZodLiteral<"Toolroll">;
230
+ task: z.ZodObject<{
231
+ id: z.ZodString;
232
+ url: z.ZodNullable<z.ZodString>;
233
+ }, z.core.$strict>;
234
+ home: z.ZodOptional<z.ZodString>;
235
+ status: z.ZodEnum<{
236
+ building: "building";
237
+ done: "done";
238
+ failed: "failed";
239
+ "needs-you": "needs-you";
240
+ queued: "queued";
241
+ ready: "ready";
242
+ stopped: "stopped";
243
+ }>;
244
+ detail: z.ZodString;
245
+ updatedAt: z.ZodString;
246
+ }, z.core.$strict>;
247
+ export type YouiyouxRequestTask = z.infer<typeof youiyouxRequestTaskSchema>;
248
+ export declare function readYouiyouxRequestTask(raw: unknown): ContractResult<YouiyouxRequestTask>;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * A youiyoux request as Toolroll reads it (youiyoux-intake.ts): the published `youi.request/v1` schema (youiyoux's
3
+ * schema/youi.request.schema.json), held to its own limits, and the `youi.proposal/v1` options file an options request's
4
+ * agent writes (schema/youi.proposal.schema.json). Unknown keys are allowed, as the published schemas allow them;
5
+ * everything Toolroll reads is checked here, and every string is outside text all the same.
6
+ */
7
+ import { z } from "zod";
8
+ import { parseContract } from "./contract.js";
9
+ export const REQUEST_ID = /^r-[0-9a-f]{10}$/;
10
+ /** An options file's id: `p-` and the same 10 hex as the options request it answers. */
11
+ export const PROPOSAL_ID = /^p-[0-9a-f]{10}$/;
12
+ /** The provenance every task filed from a request carries (task_ref.filed_via): immutable, and the seal reads it. */
13
+ export const YOUIYOUX_FILED_VIA = "youiyoux";
14
+ /** The one chat card a pass's filings make for a project (youiyoux-intake.ts), drawn by chat-decide.ts: the new tasks,
15
+ * Approve all and each one's own Approve and Open. */
16
+ export const YOUIYOUX_CARD_KIND = "youiyoux-requests";
17
+ /** Where a person sees and approves everything waiting on them in the console (flow-triggers.ts NEEDS_YOU_HREF). */
18
+ export const YOUIYOUX_HOME_PATH = "/work?view=needs-you";
19
+ const text = (max) => z.string().min(1).max(max);
20
+ const who = z.object({ kind: z.enum(["person", "agent"]), name: text(100) });
21
+ // The published schema's date-time: an ISO timestamp with a zone.
22
+ const when = z.string().max(40).regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/);
23
+ export const YOUIYOUX_EVIDENCE = ["screenshot", "check", "changed-path", "manual-review"];
24
+ /** A component prop as youiyoux names one: what an option may set. */
25
+ const PROP = /^[A-Za-z_$][\w$]{0,63}$/;
26
+ const value = z.union([z.string().max(2000), z.number().finite(), z.boolean()]);
27
+ /** What an options request offers its agent: the one file to write (`.youi/proposals/<proposal>.json`), the props an
28
+ * option may set (enum values from `values`), and whether it may give the component new text. */
29
+ export const youiyouxOfferSchema = z.object({
30
+ proposal: z.string().regex(PROPOSAL_ID),
31
+ file: text(500),
32
+ choices: z.record(z.string().regex(PROP), z.object({
33
+ type: z.enum(["enum", "string", "boolean", "number"]),
34
+ values: z.array(text(200)).max(100).optional(),
35
+ default: value.optional(),
36
+ })).refine(choices => Object.keys(choices).length <= 50, "at most 50 props"),
37
+ text: z.boolean(),
38
+ });
39
+ export const youiyouxTaskSchema = z.object({
40
+ title: text(120),
41
+ goal: text(4000),
42
+ touches: z.array(text(500)).max(50),
43
+ touchesKnown: z.boolean(),
44
+ why: text(500).optional(),
45
+ not: z.array(text(300)).max(10),
46
+ acceptance: z.array(z.object({ statement: text(300), evidence: z.array(z.enum(YOUIYOUX_EVIDENCE)).min(1) })).min(1).max(10),
47
+ /** An options request's: what its agent writes (optionsProblem holds it to the request). */
48
+ offer: youiyouxOfferSchema.optional(),
49
+ });
50
+ export const youiyouxRequestSchema = z.object({
51
+ schema: z.literal("youi.request/v1"),
52
+ id: z.string().regex(REQUEST_ID),
53
+ project: text(200),
54
+ kind: z.enum(["comment", "change", "options"]),
55
+ status: z.enum(["open", "addressed", "closed"]),
56
+ body: text(4000),
57
+ author: who,
58
+ createdAt: when,
59
+ updatedAt: when,
60
+ // Where the person pointed: the page, the component, and (when youiyoux knew it) the file and line that draw it.
61
+ target: z.object({ route: text(300), scope: z.enum(["here", "element"]), name: text(200), file: text(500).optional(), line: z.int().positive().max(Number.MAX_SAFE_INTEGER).optional() }),
62
+ task: youiyouxTaskSchema.optional(),
63
+ thread: z.array(z.unknown()).max(500),
64
+ });
65
+ export function readYouiyouxRequest(raw) {
66
+ return parseContract(youiyouxRequestSchema, raw);
67
+ }
68
+ /** The one file an options request's agent may write: `.youi/proposals/p-<the request's hex>.json`. */
69
+ export const proposalIdOf = (request) => `p-${request.slice(2)}`;
70
+ export const proposalFileOf = (proposal) => `.youi/proposals/${proposal}.json`;
71
+ /** Why an options request can't be filed as youiyoux's contract has it, or null: it carries a task with an offer whose
72
+ * proposal is `p-<its own hex>`, whose file is that proposal's, and the task may touch that one file and nothing else. */
73
+ export function optionsProblem(request) {
74
+ if (request.kind !== "options")
75
+ return null;
76
+ const offer = request.task?.offer;
77
+ if (request.task === undefined || offer === undefined)
78
+ return "it asks for options with no offer";
79
+ const proposal = proposalIdOf(request.id);
80
+ if (offer.proposal !== proposal)
81
+ return `its offer is for ${offer.proposal}, not ${proposal}`;
82
+ if (offer.file !== proposalFileOf(proposal))
83
+ return `its offer's file isn't ${proposalFileOf(proposal)}`;
84
+ if (!request.task.touchesKnown || request.task.touches.length !== 1 || request.task.touches[0] !== offer.file)
85
+ return "its task may touch more than its options file";
86
+ return null;
87
+ }
88
+ /** `youi.proposal/v1`: the options an agent puts on youiyoux's Screens canvas (youiyoux's packages/core/src/proposals.ts,
89
+ * mirrored). Toolroll checks only the shape and what it reads; youiyoux checks each option against the component's
90
+ * contract when it reads the file. */
91
+ const OPTION_IDS = ["a", "b", "c", "d"];
92
+ export const PROPOSAL_STATUSES = ["open", "chosen", "dismissed"];
93
+ export const youiyouxProposalSchema = z.object({
94
+ $schema: z.string().max(500).optional(),
95
+ schema: z.literal("youi.proposal/v1"),
96
+ id: z.string().regex(PROPOSAL_ID),
97
+ project: text(200),
98
+ route: text(300),
99
+ target: z.object({
100
+ name: text(200), element: text(200).optional(), file: text(500).optional(),
101
+ line: z.int().positive().max(Number.MAX_SAFE_INTEGER).optional(), column: z.int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
102
+ box: z.object({ x: z.number().finite(), y: z.number().finite(), width: z.number().finite().nonnegative(), height: z.number().finite().nonnegative() }),
103
+ page: z.object({ width: z.number().finite().positive(), height: z.number().finite().positive() }).optional(),
104
+ }),
105
+ ask: text(1000),
106
+ author: who,
107
+ createdAt: when,
108
+ updatedAt: when,
109
+ options: z.array(z.object({
110
+ id: z.enum(OPTION_IDS),
111
+ label: text(60),
112
+ why: text(500),
113
+ set: z.array(z.object({ prop: z.string().regex(PROP), value })).max(20),
114
+ text: text(2000).nullable(),
115
+ }).refine(option => option.set.length > 0 || option.text !== null, "an option changes a prop or the text")).min(2).max(4),
116
+ status: z.enum(PROPOSAL_STATUSES),
117
+ choice: z.enum(OPTION_IDS).optional(),
118
+ decidedBy: who.optional(),
119
+ decidedAt: when.optional(),
120
+ request: z.string().regex(REQUEST_ID).optional(),
121
+ for: z.string().regex(REQUEST_ID),
122
+ }).refine(doc => new Set(doc.options.map(option => option.id)).size === doc.options.length, "each option has its own letter")
123
+ .refine(doc => doc.choice === undefined || doc.options.some(option => option.id === doc.choice), "the choice is one of its options");
124
+ export function readYouiyouxProposal(raw) {
125
+ return parseContract(youiyouxProposalSchema, raw);
126
+ }
127
+ /** What Toolroll writes beside a request (`youi.request-task/v1`): the task it became, how that task stands, and, when
128
+ * the console has an address, the task's link and `home`, where a person sees and approves all waiting work. */
129
+ export const YOUIYOUX_TASK_STATUSES = ["queued", "needs-you", "building", "ready", "done", "failed", "stopped"];
130
+ /** A link youiyoux opens: http or https, at most 2,000 characters, with no user name or password in it. */
131
+ export const youiyouxLinkSchema = z.string().max(2000).refine(value => {
132
+ try {
133
+ const url = new URL(value);
134
+ return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "";
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ }, "an http or https address with no credentials");
140
+ export const youiyouxRequestTaskSchema = z.strictObject({
141
+ schema: z.literal("youi.request-task/v1"),
142
+ request: z.string().regex(REQUEST_ID),
143
+ runner: z.literal("Toolroll"),
144
+ task: z.strictObject({ id: z.string().min(1).max(120), url: youiyouxLinkSchema.nullable() }),
145
+ home: youiyouxLinkSchema.optional(),
146
+ status: z.enum(YOUIYOUX_TASK_STATUSES),
147
+ detail: z.string().max(200),
148
+ updatedAt: when,
149
+ });
150
+ export function readYouiyouxRequestTask(raw) {
151
+ return parseContract(youiyouxRequestTaskSchema, raw);
152
+ }