toolroll 0.9.34 → 0.9.37

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 (195) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +7 -0
  3. package/dist/assignment-adapters.d.ts +12 -48
  4. package/dist/assignment-adapters.js +33 -43
  5. package/dist/assignment.d.ts +1 -0
  6. package/dist/assignment.js +2 -2
  7. package/dist/browser-workspace.d.ts +3 -3
  8. package/dist/build-review.d.ts +1 -1
  9. package/dist/build-review.js +2 -1
  10. package/dist/builder.js +4 -7
  11. package/dist/chat-acceptance.d.ts +2 -2
  12. package/dist/chat-actions.d.ts +18 -12
  13. package/dist/chat-actions.js +15 -65
  14. package/dist/chat-ask.js +5 -5
  15. package/dist/chat-decide.d.ts +14 -1
  16. package/dist/chat-decide.js +12 -6
  17. package/dist/chat-delivery-state.d.ts +12 -70
  18. package/dist/chat-delivery-state.js +17 -135
  19. package/dist/chat-delivery.js +58 -30
  20. package/dist/chat-flow.js +8 -6
  21. package/dist/chat-inbox.js +1 -1
  22. package/dist/coding-context.d.ts +2 -39
  23. package/dist/coding-context.js +2 -1
  24. package/dist/coding-handoff.d.ts +6 -25
  25. package/dist/coding-handoff.js +11 -5
  26. package/dist/coding-types.d.ts +2 -26
  27. package/dist/coding-workspace.d.ts +2 -0
  28. package/dist/coding-workspace.js +50 -14
  29. package/dist/contracts/acceptance-terms.d.ts +14 -0
  30. package/dist/contracts/acceptance-terms.js +16 -0
  31. package/dist/contracts/chat-actions.d.ts +328 -0
  32. package/dist/contracts/chat-actions.js +133 -0
  33. package/dist/contracts/chat-callback-rows.d.ts +59 -0
  34. package/dist/contracts/chat-callback-rows.js +46 -0
  35. package/dist/contracts/chat-content.d.ts +276 -0
  36. package/dist/contracts/chat-content.js +127 -0
  37. package/dist/contracts/chat-tables.d.ts +6 -0
  38. package/dist/contracts/chat-tables.js +136 -0
  39. package/dist/contracts/coding-context.d.ts +97 -0
  40. package/dist/contracts/coding-context.js +60 -0
  41. package/dist/contracts/coding-handoff.d.ts +44 -0
  42. package/dist/contracts/coding-handoff.js +47 -0
  43. package/dist/contracts/coding-workspace.d.ts +165 -0
  44. package/dist/contracts/coding-workspace.js +71 -0
  45. package/dist/contracts/contract.d.ts +40 -7
  46. package/dist/contracts/contract.js +155 -19
  47. package/dist/contracts/decision.d.ts +45 -0
  48. package/dist/contracts/decision.js +45 -0
  49. package/dist/contracts/discord-callback.d.ts +45 -0
  50. package/dist/contracts/discord-callback.js +34 -0
  51. package/dist/contracts/flow-card.d.ts +50 -0
  52. package/dist/contracts/flow-card.js +46 -0
  53. package/dist/contracts/flow-propose.d.ts +325 -0
  54. package/dist/contracts/flow-propose.js +41 -0
  55. package/dist/contracts/flow-send.d.ts +119 -0
  56. package/dist/contracts/flow-send.js +112 -0
  57. package/dist/contracts/flow.d.ts +19405 -0
  58. package/dist/contracts/flow.js +364 -0
  59. package/dist/contracts/gateway-tools.d.ts +374 -0
  60. package/dist/contracts/gateway-tools.js +94 -0
  61. package/dist/contracts/handoff.d.ts +91 -0
  62. package/dist/contracts/handoff.js +82 -0
  63. package/dist/contracts/lead-context.d.ts +165 -0
  64. package/dist/contracts/lead-context.js +73 -0
  65. package/dist/contracts/lead-tools.d.ts +1905 -0
  66. package/dist/contracts/lead-tools.js +265 -0
  67. package/dist/contracts/memory-pass.d.ts +158 -0
  68. package/dist/contracts/memory-pass.js +102 -0
  69. package/dist/contracts/project-knowledge.d.ts +126 -0
  70. package/dist/contracts/project-knowledge.js +93 -0
  71. package/dist/contracts/project-memory.d.ts +117 -0
  72. package/dist/contracts/project-memory.js +51 -0
  73. package/dist/contracts/project-skills.d.ts +121 -0
  74. package/dist/contracts/project-skills.js +76 -0
  75. package/dist/contracts/proof.d.ts +136 -0
  76. package/dist/contracts/proof.js +110 -0
  77. package/dist/contracts/review-findings.d.ts +46 -0
  78. package/dist/contracts/review-findings.js +48 -0
  79. package/dist/contracts/route.d.ts +484 -0
  80. package/dist/contracts/route.js +128 -0
  81. package/dist/contracts/scope.d.ts +105 -0
  82. package/dist/contracts/scope.js +114 -0
  83. package/dist/contracts/scout-report.d.ts +234 -0
  84. package/dist/contracts/scout-report.js +141 -0
  85. package/dist/contracts/slack-callback.d.ts +43 -0
  86. package/dist/contracts/slack-callback.js +35 -0
  87. package/dist/contracts/sort-answer.d.ts +83 -0
  88. package/dist/contracts/sort-answer.js +77 -0
  89. package/dist/contracts/stage-output.d.ts +127 -0
  90. package/dist/contracts/stage-output.js +166 -0
  91. package/dist/contracts/task-sizing.d.ts +24 -0
  92. package/dist/contracts/task-sizing.js +28 -0
  93. package/dist/contracts/teammate-turn.d.ts +43 -0
  94. package/dist/contracts/teammate-turn.js +51 -0
  95. package/dist/contracts/teams-callback.d.ts +14 -0
  96. package/dist/contracts/teams-callback.js +14 -0
  97. package/dist/contracts/telegram-callback.d.ts +120 -0
  98. package/dist/contracts/telegram-callback.js +97 -0
  99. package/dist/contracts/verification-receipt.d.ts +148 -0
  100. package/dist/contracts/verification-receipt.js +87 -0
  101. package/dist/decision.d.ts +11 -20
  102. package/dist/decision.js +86 -167
  103. package/dist/demo.js +28 -11
  104. package/dist/discord-chat.js +31 -20
  105. package/dist/evidence.d.ts +3 -40
  106. package/dist/evidence.js +5 -1
  107. package/dist/flow-engine.js +14 -17
  108. package/dist/flow-gallery.d.ts +3 -2
  109. package/dist/flow-items.d.ts +4 -13
  110. package/dist/flow-items.js +1 -15
  111. package/dist/flow-send.d.ts +6 -37
  112. package/dist/flow-send.js +18 -14
  113. package/dist/flow-share.d.ts +10 -32
  114. package/dist/flow-share.js +72 -84
  115. package/dist/flow-sort.d.ts +7 -26
  116. package/dist/flow-sort.js +25 -20
  117. package/dist/flow-starters.d.ts +2 -1
  118. package/dist/flow-steps.js +3 -2
  119. package/dist/flow-triggers.d.ts +15 -72
  120. package/dist/flow-triggers.js +94 -83
  121. package/dist/flows.d.ts +37 -220
  122. package/dist/flows.js +356 -387
  123. package/dist/invoke.js +4 -4
  124. package/dist/lead-context.d.ts +4 -2
  125. package/dist/lead-context.js +24 -17
  126. package/dist/mate-tools.d.ts +4 -0
  127. package/dist/mate-tools.js +111 -308
  128. package/dist/mcp-connect.d.ts +27 -4
  129. package/dist/mcp-connect.js +92 -17
  130. package/dist/mcp.js +84 -216
  131. package/dist/memory-pass.d.ts +17 -42
  132. package/dist/memory-pass.js +54 -26
  133. package/dist/operate.js +31 -2
  134. package/dist/phase-routing.d.ts +19 -72
  135. package/dist/phase-routing.js +34 -164
  136. package/dist/plan.d.ts +5 -5
  137. package/dist/plan.js +5 -25
  138. package/dist/process-recovery-eligibility.d.ts +2 -2
  139. package/dist/process-recovery-eligibility.js +13 -8
  140. package/dist/project-knowledge.d.ts +16 -35
  141. package/dist/project-knowledge.js +42 -21
  142. package/dist/project-memory.d.ts +2 -28
  143. package/dist/project-memory.js +28 -13
  144. package/dist/project-skills.d.ts +16 -38
  145. package/dist/project-skills.js +41 -32
  146. package/dist/project-tools.d.ts +1 -0
  147. package/dist/project-tools.js +4 -4
  148. package/dist/proof.d.ts +15 -91
  149. package/dist/proof.js +90 -249
  150. package/dist/provider.d.ts +2 -0
  151. package/dist/provider.js +4 -16
  152. package/dist/result-evidence-readers.d.ts +42 -0
  153. package/dist/result-evidence-readers.js +91 -0
  154. package/dist/result-shots.d.ts +1 -1
  155. package/dist/result-shots.js +2 -1
  156. package/dist/review-switch.d.ts +3 -8
  157. package/dist/reviewer.d.ts +8 -12
  158. package/dist/reviewer.js +11 -35
  159. package/dist/scope.d.ts +21 -43
  160. package/dist/scope.js +42 -128
  161. package/dist/scout-report.d.ts +9 -194
  162. package/dist/scout-report.js +113 -264
  163. package/dist/scout.js +15 -11
  164. package/dist/serve.js +17 -101
  165. package/dist/slack-chat.js +26 -20
  166. package/dist/slack-state.js +2 -1
  167. package/dist/store.d.ts +9 -32
  168. package/dist/store.js +18 -15
  169. package/dist/structured-output.js +2 -1
  170. package/dist/subscription-chat.d.ts +4 -0
  171. package/dist/subscription-chat.js +4 -1
  172. package/dist/surface.js +8 -1
  173. package/dist/task-review-brief.d.ts +69 -0
  174. package/dist/task-review-brief.js +179 -0
  175. package/dist/task-sizing.d.ts +3 -27
  176. package/dist/task-sizing.js +11 -26
  177. package/dist/task-text.d.ts +0 -5
  178. package/dist/task-text.js +1 -3
  179. package/dist/teammate-question.js +7 -5
  180. package/dist/teammate-work.js +9 -5
  181. package/dist/teammates.d.ts +13 -64
  182. package/dist/teammates.js +32 -33
  183. package/dist/teams-chat.js +20 -11
  184. package/dist/telegram-flow.js +4 -3
  185. package/dist/telegram-mate.d.ts +4 -14
  186. package/dist/telegram-mate.js +8 -7
  187. package/dist/telegram-team.d.ts +2 -19
  188. package/dist/telegram.d.ts +1 -1
  189. package/dist/telegram.js +52 -42
  190. package/dist/text-limits.d.ts +212 -1
  191. package/dist/text-limits.js +214 -1
  192. package/dist/tools-ui.d.ts +2 -1
  193. package/dist/tools-ui.js +7 -4
  194. package/dist/verification-evidence.js +24 -12
  195. package/package.json +1 -1
@@ -0,0 +1,179 @@
1
+ import { assignmentChecksForRun, assignmentOf } from './assignment.js';
2
+ import { assignmentPresentationOf } from './assignment-presentation.js';
3
+ import { readVerifiedArtifact, readVerifiedProofForRun, reportShotsOf, scanForSecrets } from './evidence.js';
4
+ import { SCREENSHOT_CAPTURE, structuredHandoffView, terminalDiffView } from './result-evidence-readers.js';
5
+ import { FINDING_SEVERITIES } from './contracts/review-findings.js';
6
+ import { buildReviewOf } from './review-switch.js';
7
+ import { parseReport } from './scout-report.js';
8
+ import { sanitizeTranscriptLine } from './live.js';
9
+ import { redactSecretAssignments } from './builder.js';
10
+ import { parseExecutionPlanDocument } from './plan.js';
11
+ const MISSING = 'not recorded';
12
+ /** Redact before shortening so a clipped credential cannot escape recognition. Apply to every data string in both formats. */
13
+ function safeText(value, secrets, cap = 300) {
14
+ if (typeof value !== 'string' || !value.trim())
15
+ return MISSING;
16
+ const flat = value.replace(/\s+/g, ' ').trim();
17
+ const cleaned = sanitizeTranscriptLine(flat);
18
+ if (secrets.some(secret => secret !== '' && value.includes(secret)) ||
19
+ /\blt_[a-f0-9]{12}_[A-Za-z0-9_-]{43}\b|\bBearer\s+\S+/i.test(flat) ||
20
+ scanForSecrets(flat).length || redactSecretAssignments(flat) !== flat || cleaned.startsWith('[a credential-shaped'))
21
+ return '[redacted]';
22
+ return cleaned.length <= cap ? cleaned : `${cleaned.slice(0, cap - 1)}…`;
23
+ }
24
+ function verifiedText(root, artifact) {
25
+ if (!artifact || artifact.truncated || artifact.captureStatus === 'failed')
26
+ return null;
27
+ const read = readVerifiedArtifact(root, artifact);
28
+ return read.ok ? read.content.toString('utf8') : null;
29
+ }
30
+ /** Select only finished build/report attempts in the admitted family. An explicit run never crosses that boundary. */
31
+ export function taskReviewBrief(store, task, root, now, access, options = {}) {
32
+ const family = store.taskFamilyOf(task, access.repos, access.principal === 'operator' && access.includeUnplaced === true);
33
+ if (family === null)
34
+ return { ok: false, reason: 'not-found', message: 'That task is unavailable in your projects.' };
35
+ const runs = family.versions.flatMap(version => store.runsFor(version.refId))
36
+ .filter(run => (run.role === 'builder' || run.role === 'scout') && run.finishedAt !== null && run.outcome !== null)
37
+ .sort((a, b) => b.id - a.id);
38
+ const run = options.run === undefined ? runs[0] ?? null : runs.find(one => one.id === options.run) ?? null;
39
+ if (options.run !== undefined && run === null)
40
+ return { ok: false, reason: 'not-found', message: 'That finished build or report is unavailable for this task.' };
41
+ const assignment = assignmentOf(store, task, now, access, root);
42
+ const safe = (value, cap) => safeText(value, options.secrets ?? [], cap);
43
+ const sha = (value) => /^[a-f0-9]{40,64}$/.test(value ?? '') ? value.slice(0, 12) : MISSING;
44
+ const artifacts = run === null ? [] : store.artifactsFor(run.id);
45
+ // Pass only the inventory: this reader does not need to open the patch for a brief.
46
+ const statView = terminalDiffView(artifacts.filter(one => one.kind === 'diff-stat' && one.captureStatus !== 'failed'), root)?.stat;
47
+ const stat = statView && !('problem' in statView) ? statView : null;
48
+ const handoff = structuredHandoffView(artifacts, root, value => safe(value, 600));
49
+ const reportText = run?.role === 'scout' ? verifiedText(root, artifacts.find(one => one.kind === 'report')) : null;
50
+ const report = reportText === null ? null : parseReport(reportText, { stored: true });
51
+ const conclusion = handoff?.conclusion ?? (report?.ok ? report.report.summary : run?.handoff);
52
+ const duration = run?.finishedAt ? (Date.parse(run.finishedAt) - Date.parse(run.startedAt)) / 60_000 : NaN;
53
+ const recordedCheck = run === null ? null : store.runCheckFor(run.id);
54
+ const receipt = assignment?.receipt?.runId === run?.id ? assignment?.receipt : null;
55
+ // The shared assignment reader includes later checks on the same candidate. Its result wins;
56
+ // old named failures must not ride a newer passing (or differently failed) check.
57
+ const checks = receipt?.checks ?? (run === null ? null : assignmentChecksForRun(store, root, run.id, now));
58
+ const status = checks && checks.status !== 'unavailable' && checks.status !== 'not-run' ? checks.status : recordedCheck?.status ?? checks?.status ?? MISSING;
59
+ const exit = checks && checks.status === status ? checks.exitCode : recordedCheck?.exitCode;
60
+ const sameCheck = recordedCheck?.status === status && recordedCheck.exitCode === exit &&
61
+ (!checks?.logArtifactId || artifacts.some(one => one.id === checks.logArtifactId && one.kind === 'check-log'));
62
+ const progress = run === null ? null : store.checkProgress(run.id);
63
+ const failed = sameCheck ? [...new Set([
64
+ ...recordedCheck.suites.filter(one => one.status === 'failed').map(one => safe(one.name)),
65
+ ...Object.entries(progress?.suites ?? {}).filter(([, suite]) => suite.state === 'failed').map(([name, suite]) => `${name}: ${suite.failed} failed`),
66
+ ])] : [];
67
+ const review = run === null ? null : buildReviewOf(store, run.id);
68
+ const findings = review === null ? [] : [...review.high, ...review.followUps].filter(one => one && FINDING_SEVERITIES.includes(one.severity));
69
+ const counts = Object.fromEntries(FINDING_SEVERITIES.map(severity => [severity, findings.filter(one => one.severity === severity).length]));
70
+ const shown = FINDING_SEVERITIES.flatMap(severity => severity === 'LOW' && !options.all ? [] : findings.filter(one => one.severity === severity))
71
+ .slice(0, 12).map(one => ({ severity: one.severity, file: safe(one.file), line: Number.isSafeInteger(one.line) ? one.line : 0, scenario: safe(one.scenario) }));
72
+ const omitted = Object.fromEntries(FINDING_SEVERITIES.map(severity => [severity, counts[severity] - shown.filter(one => one.severity === severity).length]));
73
+ const proof = run === null ? null : readVerifiedProofForRun(store, root, run.id);
74
+ const shots = artifacts.filter(one => one.kind === 'screenshot');
75
+ const reportShots = report?.ok && run !== null ? reportShotsOf(artifacts, root, run.id, report.report.images) : [];
76
+ const pathOf = (artifact) => SCREENSHOT_CAPTURE.exec(artifact.capture)?.[1] ??
77
+ (reportShots.some(one => one.artifactId === artifact.id) ? `evidence/${artifact.key}` : null);
78
+ const declared = proof?.ok ? proof.proof.screenshots.map(one => one.path) : [];
79
+ const paths = [...new Set([...declared, ...shots.flatMap(one => pathOf(one) ?? [])])];
80
+ const screenshots = paths.map(path => {
81
+ const artifact = shots.find(one => pathOf(one) === path);
82
+ return { path: safe(path), bytes: artifact && artifact.captureStatus !== 'failed' && readVerifiedArtifact(root, artifact).ok ? artifact.bytesStored : MISSING };
83
+ });
84
+ const plan = run === null ? null : planTitles(store, root, run);
85
+ const current = run !== null && receipt !== null && family.current.refId === run.taskRef;
86
+ const nextActions = [];
87
+ if (!run)
88
+ nextActions.push('Open the task to inspect its progress.');
89
+ else if (!current)
90
+ nextActions.push('Open the current result before completing or requesting a revision.');
91
+ else {
92
+ if (assignment?.state === 'ready-to-check')
93
+ nextActions.push(`Complete: toolroll task complete ${safe(family.root.id)} --digest ${receipt.digest}`);
94
+ const diff = artifacts.find(one => one.kind === 'terminal-diff');
95
+ if (run.role === 'builder' && diff && readVerifiedArtifact(root, diff).ok) {
96
+ nextActions.push(`Revise: toolroll task revise ${safe(family.root.id)} --run ${run.id} --feedback "requested change"`);
97
+ }
98
+ else if (run.role === 'builder')
99
+ nextActions.push('Revision unavailable: saved changes are not recorded or cannot be read.');
100
+ if (assignment?.state === 'ready-to-check' || assignment?.state === 'complete') {
101
+ if (run.role === 'builder' && run.outcome === 'built')
102
+ nextActions.push(assignment.state === 'complete'
103
+ ? 'Merge into a release through the normal release approval and check process.'
104
+ : 'After completion, merge into a release through the normal release approval and check process.');
105
+ }
106
+ else
107
+ nextActions.push(`Open the task: ${safe(assignment?.detail)}`);
108
+ }
109
+ return { ok: true, brief: {
110
+ header: { task: safe(run === null ? task : store.externalIdFor(run.taskRef)), run: run?.id ?? MISSING,
111
+ provider: safe(run?.provider), model: safe(run?.model), minutes: Number.isFinite(duration) && duration >= 0 ? Math.round(duration * 10) / 10 : MISSING,
112
+ outcome: safe(run?.outcome), base: sha(run?.baseRevision), head: sha(run?.headRevision), state: run !== null && !current ? 'Earlier result' : assignment === null ? MISSING : assignmentPresentationOf(assignment).status.label },
113
+ conclusion: safe(conclusion, 600),
114
+ changedFiles: stat === null ? MISSING : { files: [...stat.files].sort((a, b) => ((b.additions ?? 0) + (b.deletions ?? 0)) - ((a.additions ?? 0) + (a.deletions ?? 0)))
115
+ .slice(0, 15).map(one => ({ path: safe(one.path), additions: one.additions ?? MISSING, deletions: one.deletions ?? MISSING })), total: stat.fileCount, omitted: Math.max(0, stat.fileCount - Math.min(15, stat.files.length)) },
116
+ check: { status: status === 'unavailable' ? MISSING : status, exit: exit ?? MISSING, failed: status !== 'failed' ? [] : failed.length ? failed : MISSING },
117
+ review: review === null ? MISSING : { state: review.state, reason: safe(review.reason), findings: shown, counts, omitted },
118
+ screenshots: screenshots.length ? { files: screenshots.slice(0, 8), omitted: Math.max(0, screenshots.length - 8) } : MISSING,
119
+ plan: plan === null || plan.length === 0 ? MISSING : { titles: plan.slice(0, 5).map(one => safe(one)), omitted: Math.max(0, plan.length - 5) },
120
+ nextActions,
121
+ } };
122
+ }
123
+ function planTitles(store, root, run) {
124
+ const revision = run.planRevision == null ? null : store.getPlanRevision(run.planRevision);
125
+ // Historical runs cannot borrow a plan written after they started.
126
+ const artifact = revision === null ? store.runsFor(run.taskRef).filter(one => one.role === 'planner' && one.id < run.id)
127
+ .sort((a, b) => b.id - a.id).flatMap(one => store.artifactsFor(one.id)).find(one => one.kind === 'plan') : store.getArtifact(revision.artifact);
128
+ const text = verifiedText(root, artifact);
129
+ if (text === null)
130
+ return null;
131
+ const parsed = parseExecutionPlanDocument(text);
132
+ return parsed.ok ? parsed.document.milestones : text.split(/\r?\n/).filter(line => /^#{1,6}\s+\S/.test(line)).map(line => line.replace(/^#{1,6}\s+/, ''));
133
+ }
134
+ /** Text and JSON share this bounded projection; --all includes LOW details within the same line budget. */
135
+ export function renderReviewBrief(brief) {
136
+ const h = brief.header;
137
+ const lines = [`${h.task} · run ${h.run} · ${h.provider}/${h.model} · ${h.minutes} min · ${h.outcome} · ${h.base} → ${h.head} · ${h.state}`,
138
+ `Conclusion: ${brief.conclusion}`, 'Changed files:'];
139
+ if (brief.changedFiles === MISSING)
140
+ lines.push(` ${MISSING}`);
141
+ else {
142
+ lines.push(...brief.changedFiles.files.map(one => ` ${one.path} +${one.additions} / -${one.deletions}`));
143
+ if (brief.changedFiles.total === 0)
144
+ lines.push(' none');
145
+ if (brief.changedFiles.omitted)
146
+ lines.push(` and ${brief.changedFiles.omitted} more`);
147
+ }
148
+ const check = brief.check;
149
+ lines.push(`Check: ${check.status} · exit ${check.exit}${check.failed === MISSING ? ` · failed parts: ${MISSING}` : check.failed.length ? ` · failed: ${check.failed.join('; ')}` : ''}`);
150
+ if (brief.review === MISSING)
151
+ lines.push(`Automatic review: ${MISSING}`);
152
+ else {
153
+ lines.push(`Automatic review: ${brief.review.state}${brief.review.reason === MISSING ? '' : ` · ${brief.review.reason}`}`);
154
+ lines.push(...brief.review.findings.map(one => ` ${one.severity} ${one.file}:${one.line} — ${one.scenario}`));
155
+ for (const severity of FINDING_SEVERITIES)
156
+ if (brief.review.omitted[severity])
157
+ lines.push(` ${severity}: ${brief.review.omitted[severity]} more (${brief.review.counts[severity]} total)`);
158
+ if (Object.values(brief.review.counts).every(count => count === 0) && brief.review.state === 'reviewed')
159
+ lines.push(' no findings');
160
+ }
161
+ lines.push('Screenshots:');
162
+ if (brief.screenshots === MISSING)
163
+ lines.push(` ${MISSING}`);
164
+ else {
165
+ lines.push(...brief.screenshots.files.map(one => ` ${one.path} (${one.bytes}${typeof one.bytes === 'number' ? ' bytes' : ''})`));
166
+ if (brief.screenshots.omitted)
167
+ lines.push(` and ${brief.screenshots.omitted} more`);
168
+ }
169
+ lines.push('Plan:');
170
+ if (brief.plan === MISSING)
171
+ lines.push(` ${MISSING}`);
172
+ else {
173
+ lines.push(...brief.plan.titles.map(one => ` ${one}`));
174
+ if (brief.plan.omitted)
175
+ lines.push(` and ${brief.plan.omitted} more`);
176
+ }
177
+ lines.push('Next:', ...brief.nextActions.map(one => ` ${one}`));
178
+ return lines;
179
+ }
@@ -1,5 +1,6 @@
1
+ import { type SizeAnswer } from "./contracts/task-sizing.js";
1
2
  import { run, type ExecResult } from "./exec.js";
2
- import { type TaskSize, type TaskSizing } from "./phase-routing.js";
3
+ import { type TaskSizing } from "./phase-routing.js";
3
4
  import type { Store } from "./store.js";
4
5
  /** The classifier's whole budget, from asking to answer. */
5
6
  export declare const SIZING_BUDGET_MS = 5000;
@@ -11,11 +12,7 @@ export type SizingInput = {
11
12
  outOfScope?: string | null | undefined;
12
13
  touches?: readonly string[] | undefined;
13
14
  };
14
- export type SizeAnswer = {
15
- size: TaskSize;
16
- risky: boolean;
17
- reason: string;
18
- };
15
+ export type { SizeAnswer } from "./contracts/task-sizing.js";
19
16
  /** One classifier: null (or a throw) is "no answer"; the signal ends at the budget. */
20
17
  export type Sizer = (input: SizingInput, signal: AbortSignal) => Promise<SizeAnswer | null>;
21
18
  /** Structural or broad work — the description signal planning has always read. */
@@ -27,26 +24,6 @@ export declare function planningSignals(input: SizingInput): boolean;
27
24
  export declare function heuristicSizing(input: SizingInput, why?: string | null): TaskSizing;
28
25
  /** What the classifier reads: the task as data, bounded, keys blanked. */
29
26
  export declare function sizingText(input: SizingInput): string;
30
- export declare const SIZING_SCHEMA: {
31
- readonly type: "object";
32
- readonly properties: {
33
- readonly size: {
34
- readonly type: "string";
35
- readonly enum: readonly ["small", "medium", "large"];
36
- };
37
- readonly risky: {
38
- readonly type: "boolean";
39
- };
40
- readonly reason: {
41
- readonly type: "string";
42
- readonly maxLength: 160;
43
- };
44
- };
45
- readonly required: readonly ["size", "risky", "reason"];
46
- readonly additionalProperties: false;
47
- };
48
- /** Read an answer back: only the three sizes, a boolean, and a short reason. */
49
- export declare function readSizeAnswer(value: unknown): SizeAnswer | null;
50
27
  type CommandRunner = (file: string, args: readonly string[], options: Parameters<typeof run>[2]) => Promise<ExecResult>;
51
28
  /** A small Claude model on this computer's sign-in: no tools, no MCP servers, no repository, credential keys stripped.
52
29
  * The abort ends it: a late answer never keeps a command (or its process) waiting. */
@@ -75,4 +52,3 @@ export declare function filingSizerInstalled(): boolean;
75
52
  export declare function refineFiledSizing(store: Store, taskId: string, input: SizingInput, followPlanning: boolean, clock?: () => Date): Promise<void> | null;
76
53
  /** Wait for every classification still in flight (each is bounded by the budget). */
77
54
  export declare function settleSizings(): Promise<void>;
78
- export {};
@@ -20,12 +20,13 @@ import { randomUUID } from "node:crypto";
20
20
  import { mkdtempSync, rmSync } from "node:fs";
21
21
  import { tmpdir } from "node:os";
22
22
  import { join } from "node:path";
23
+ import { readSizeAnswer, SIZING_MODEL_SCHEMA } from "./contracts/task-sizing.js";
23
24
  import { strictJsonParse } from "./converse.js";
24
25
  import { redactSecretLines, scanForSecrets } from "./evidence.js";
25
26
  import { run, terminateOwnedProcesses } from "./exec.js";
26
27
  import { JEV_MODEL, JEV_URL } from "./flow-sort.js";
27
28
  import { readProviderKey } from "./keys.js";
28
- import { isTaskSize } from "./phase-routing.js";
29
+ import {} from "./phase-routing.js";
29
30
  import { ALL_CREDENTIAL_ENV } from "./provider.js";
30
31
  /** The classifier's whole budget, from asking to answer. */
31
32
  export const SIZING_BUDGET_MS = 5_000;
@@ -49,8 +50,11 @@ export function heuristicSizing(input, why = null) {
49
50
  const clip = (text, cap) => (text.length <= cap ? text : `${text.slice(0, cap - 1)}…`);
50
51
  /** Key-shaped lines never leave the machine. */
51
52
  const blank = (text) => redactSecretLines(text, scanForSecrets(text));
52
- /** One plain line, bounded. */
53
- const oneLine = (text) => clip(text.replace(/\s+/g, " ").trim(), 160);
53
+ /** An answer read through the contract (src/contracts/task-sizing.ts), its reason on one line; null when it isn't one. */
54
+ function answerOf(value) {
55
+ const read = readSizeAnswer(value);
56
+ return read.ok ? read.value : null;
57
+ }
54
58
  const SIZE_MEANS = {
55
59
  small: "a contained change a fast model can make directly: copy, a style, a config value, one small function, one or two files",
56
60
  medium: "ordinary feature or fix work across a few files that benefits from the everyday model",
@@ -66,25 +70,6 @@ export function sizingText(input) {
66
70
  ...(input.touches === undefined || input.touches.length === 0 ? [] : [`Paths: ${clip(input.touches.slice(0, 40).join(", "), 1500)}`]),
67
71
  ].join("\n");
68
72
  }
69
- export const SIZING_SCHEMA = {
70
- type: "object",
71
- properties: {
72
- size: { type: "string", enum: ["small", "medium", "large"] },
73
- risky: { type: "boolean" },
74
- reason: { type: "string", maxLength: 160 },
75
- },
76
- required: ["size", "risky", "reason"],
77
- additionalProperties: false,
78
- };
79
- /** Read an answer back: only the three sizes, a boolean, and a short reason. */
80
- export function readSizeAnswer(value) {
81
- if (value === null || typeof value !== "object" || Array.isArray(value))
82
- return null;
83
- const v = value;
84
- if (!isTaskSize(v["size"]) || typeof v["risky"] !== "boolean")
85
- return null;
86
- return { size: v["size"], risky: v["risky"], reason: typeof v["reason"] === "string" ? oneLine(v["reason"]) : "" };
87
- }
88
73
  /** A small Claude model on this computer's sign-in: no tools, no MCP servers, no repository, credential keys stripped.
89
74
  * The abort ends it: a late answer never keeps a command (or its process) waiting. */
90
75
  export function claudeSizer(runner = run, model = SIZING_CLAUDE_MODEL, budgetMs = SIZING_BUDGET_MS) {
@@ -108,7 +93,7 @@ export function claudeSizer(runner = run, model = SIZING_CLAUDE_MODEL, budgetMs
108
93
  sizingText(input),
109
94
  ].join("\n");
110
95
  const result = await runner("claude", [
111
- "-p", "--output-format", "json", "--json-schema", JSON.stringify(SIZING_SCHEMA), "--safe-mode", "--no-session-persistence", "--tools", "", "--permission-mode", "dontAsk",
96
+ "-p", "--output-format", "json", "--json-schema", JSON.stringify(SIZING_MODEL_SCHEMA), "--safe-mode", "--no-session-persistence", "--tools", "", "--permission-mode", "dontAsk",
112
97
  "--permission-prompts", "none", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--model", model,
113
98
  ], { cwd: dir, stdin: prompt, timeoutMs: budgetMs, maxBuffer: 256 * 1024, omitEnv: ALL_CREDENTIAL_ENV, processGroup: true, owner, beforeSpawn: () => !signal.aborted });
114
99
  if (signal.aborted || result.notFound || result.timedOut || result.code !== 0)
@@ -117,7 +102,7 @@ export function claudeSizer(runner = run, model = SIZING_CLAUDE_MODEL, budgetMs
117
102
  const body = parsed.ok && typeof parsed.value === "object" && parsed.value !== null && !Array.isArray(parsed.value) ? parsed.value : null;
118
103
  if (body === null || body["is_error"] === true || body["subtype"] !== "success")
119
104
  return null;
120
- return readSizeAnswer(body["structured_output"]);
105
+ return answerOf(body["structured_output"]);
121
106
  }
122
107
  finally {
123
108
  signal.removeEventListener("abort", stop);
@@ -149,10 +134,10 @@ export function jevSizer(fetcher, key) {
149
134
  const answers = (body["answers"] ?? {});
150
135
  const size = answers["size"]?.["choice"];
151
136
  const risky = answers["risky"]?.["noul"];
152
- if (!isTaskSize(size) || typeof risky !== "number" || !Number.isFinite(risky))
137
+ if (typeof risky !== "number" || !Number.isFinite(risky))
153
138
  return null;
154
139
  const sure = answers["size"]?.["confidence"];
155
- return { size, risky: risky >= 0.5, reason: typeof sure === "number" && Number.isFinite(sure) ? `Jev is ${Math.round(Math.min(1, Math.max(0, sure)) * 100)}% sure` : "" };
140
+ return answerOf({ size, risky: risky >= 0.5, reason: typeof sure === "number" && Number.isFinite(sure) ? `Jev is ${Math.round(Math.min(1, Math.max(0, sure)) * 100)}% sure` : "" });
156
141
  };
157
142
  }
158
143
  /** The owner's own classifier: Jev when an OpenRouter key is set up, otherwise Claude on this computer's sign-in. */
@@ -3,11 +3,6 @@ export declare const TASK_TEXT_LIMITS: {
3
3
  readonly text: 8000;
4
4
  readonly textBytes: 32000;
5
5
  };
6
- export declare const TASK_SCOPE_TEXT_SCHEMA: {
7
- readonly type: "string";
8
- readonly maxLength: 8000;
9
- readonly description: `At most ${string} characters (UTF-16 code units) and ${string} UTF-8 bytes; no control or disguised text. Longer text is refused with its length, not cut: shorten it and call again.`;
10
- };
11
6
  export type TaskTextRefusal = {
12
7
  ok: false;
13
8
  reason: "bad-title" | "bad-goal";
package/dist/task-text.js CHANGED
@@ -6,10 +6,8 @@
6
6
  * runtime with lower limits. */
7
7
  import { hasForbiddenControls, hasDisguisedText } from "./decision.js";
8
8
  import { TEXT_LIMITS } from "./text-limits.js";
9
- export const TASK_TEXT_LIMITS = { title: 200, text: TEXT_LIMITS.goal, textBytes: TEXT_LIMITS.goalBytes };
9
+ export const TASK_TEXT_LIMITS = { title: TEXT_LIMITS.taskTitle, text: TEXT_LIMITS.goal, textBytes: TEXT_LIMITS.goalBytes };
10
10
  const n = (value) => value.toLocaleString("en-US");
11
- export const TASK_SCOPE_TEXT_SCHEMA = { type: "string", maxLength: TASK_TEXT_LIMITS.text,
12
- description: `At most ${n(TASK_TEXT_LIMITS.text)} characters (UTF-16 code units) and ${n(TASK_TEXT_LIMITS.textBytes)} UTF-8 bytes; no control or disguised text. Longer text is refused with its length, not cut: shorten it and call again.` };
13
11
  const dishonest = (text) => hasForbiddenControls(text) || hasDisguisedText(text);
14
12
  const overBytes = (text, cap) => Buffer.byteLength(text, "utf8") > cap;
15
13
  const BYTE_CAPS = { title: 800, text: TASK_TEXT_LIMITS.textBytes, path: 800 };
@@ -11,8 +11,10 @@
11
11
  * the person it was asked of. Answered anywhere, every button retires.
12
12
  */
13
13
  import { randomBytes } from "node:crypto";
14
+ import { partContent, savedChatPart } from "./chat-delivery-state.js";
14
15
  import { answerTeammateQuestion } from "./teammate-work.js";
15
16
  import { labelOf } from "./teammate-admin.js";
17
+ import { telegramButton } from "./contracts/telegram-callback.js";
16
18
  /** The key notifyPeople gives a teammate's question: flow-card:<card>:teammate-q:<question>:<person>. */
17
19
  export const TEAMMATE_Q_KEY = /^flow-card:[1-9][0-9]{0,14}:teammate-q:([1-9][0-9]{0,14}):/;
18
20
  export const IN_WORDS = "Answer in words";
@@ -41,8 +43,8 @@ export function telegramQuestionButtons(store, binding, question, now) {
41
43
  const rows = [];
42
44
  const options = choices.filter(one => one.choice !== null);
43
45
  for (let at = 0; at < options.length; at += 2)
44
- rows.push(options.slice(at, at + 2).map(one => ({ text: one.label.slice(0, 60), callback_data: one.token })));
45
- rows.push([{ text: choices.at(-1).label, callback_data: choices.at(-1).token }]);
46
+ rows.push(options.slice(at, at + 2).map(one => telegramButton(one.label.slice(0, 60), one.token)));
47
+ rows.push([telegramButton(choices.at(-1).label, choices.at(-1).token)]);
46
48
  return { keyboard: rows, tokens: choices.map(one => one.token) };
47
49
  }
48
50
  /** A tapped question button, inside the update's transaction. */
@@ -90,7 +92,7 @@ export function questionParts(store, notification, binding) {
90
92
  /** The live buttons on one part, in the order they were minted. */
91
93
  export function chatQuestionButtons(state, part, now) {
92
94
  const row = state.prepare("SELECT payload FROM chat_part WHERE id=?").get(part);
93
- const content = row === undefined ? null : JSON.parse(String(row["payload"]));
95
+ const content = row === undefined ? null : partContent(String(row["payload"]));
94
96
  const labels = new Map((content?.question?.choices ?? []).map(one => [one.choice ?? "", one.label]));
95
97
  return state.prepare("SELECT token,choice FROM chat_question_action WHERE part=? AND consumed IS NULL AND expires>? ORDER BY rowid").all(part, now.toISOString())
96
98
  .map(one => ({ token: String(one.token), label: (labels.get(one.choice ?? "") ?? IN_WORDS).slice(0, 75), words: one.choice === null }));
@@ -98,9 +100,9 @@ export function chatQuestionButtons(state, part, now) {
98
100
  /** Show the tapped notice again with the answer, and without its buttons. */
99
101
  function repaint(state, part, event, line) {
100
102
  const row = state.prepare("SELECT payload FROM chat_part WHERE id=?").get(part);
101
- const before = row === undefined ? { text: "" } : JSON.parse(String(row["payload"]));
103
+ const before = row === undefined ? { text: "" } : partContent(String(row["payload"]));
102
104
  const content = { text: `${before.text}\n\n${line}`.slice(0, 3400), edit: event.ts, ...(before.link === undefined ? {} : { link: before.link }), ...(before.channel === undefined ? {} : { channel: before.channel }) };
103
- state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(JSON.stringify(content), part);
105
+ state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(savedChatPart(content), part);
104
106
  }
105
107
  /** A tapped question button, inside the action's transaction; false when the token isn't one. */
106
108
  export function applyChatQuestionTap(options, event, binding, token, now) {
@@ -111,9 +111,10 @@ export async function teammateTurn(store, flow, definition, stage, card, mate, n
111
111
  store.addTeammateTurn({ teammate: mate.id, card: card.id, model, ok: reply.ok, ms: reply.ok ? reply.ms : Date.now() - asked, costUsd: reply.costUsd ?? null, tokensIn: reply.tokensIn ?? null, tokensOut: reply.tokensOut ?? null }, now);
112
112
  if (!reply.ok)
113
113
  return { state: "retry", said: reply.said, ...(log.length === 0 ? {} : { log: log.join("\n\n") }) };
114
- const answer = readTurn(reply.value, context);
115
- if (answer === null)
116
- return { state: "retry", said: `${context.name}'s answer wasn't one this zone allows.`, log: [...log, JSON.stringify(reply.value).slice(0, 4000)].join("\n\n") };
114
+ const read = readTurn(reply.value, context);
115
+ if (!read.ok)
116
+ return { state: "retry", said: `${context.name}'s answer wasn't one this zone allows.`, log: [...log, read.issues.map(issue => issue.line).join("\n"), JSON.stringify(reply.value).slice(0, 4000)].join("\n\n") };
117
+ const answer = read.value;
117
118
  const header = `${context.name} (${model}) · ${(reply.ms / 1000).toFixed(1)} s`;
118
119
  const over = turnOverruns(answer);
119
120
  if (over.length > 0 && !shortened) {
@@ -243,8 +244,11 @@ function carryOut(store, flow, definition, stage, card, mate, context, answer, n
243
244
  // route: the answer names where the card goes; its text is what the next zones use.
244
245
  const picked = handleAnswers(stage).find(one => one.answer.toLowerCase() === answer.answer.toLowerCase());
245
246
  // What the next zones read: whole up to what a step passes on; longer is attached whole to the card, and they read a link to it.
246
- if (answer.text !== "")
247
- store.updateFlowCard(card.id, { outputs: { ...card.outputs, [stage.id]: keptWhole(store, flow, card, actor, wholeDraft(answer.text), TEXT_LIMITS.stageOutput, "a step passes on", `What ${label} wrote for the next zones`, now) } }, now);
247
+ if (answer.text !== "") {
248
+ const whole = wholeDraft(answer.text);
249
+ const text = keptWhole(store, flow, card, actor, whole, TEXT_LIMITS.stageOutput, "a step passes on", `What ${label} wrote for the next zones`, now);
250
+ store.updateFlowCard(card.id, { outputs: { ...card.outputs, [stage.id]: text }, ...(text === whole ? {} : { attached: { [stage.id]: whole } }) }, now);
251
+ }
248
252
  const said = `Sent “${card.title}” to ${titleOf(picked.to)}${picked.answer === CARRY_ON ? "" : ` (${picked.answer})`}${answer.reason === "" ? "" : `: ${answer.reason}`}`;
249
253
  event("handled", said);
250
254
  // v96: a zone that answers whoever asked sends what it wrote back to them, under its name.
@@ -1,4 +1,6 @@
1
1
  import { run, type ExecResult } from "./exec.js";
2
+ import type { ContractResult } from "./contracts/contract.js";
3
+ import { type TurnAnswer } from "./contracts/teammate-turn.js";
2
4
  import { type Overrun } from "./text-limits.js";
3
5
  export declare const SOUL_CHARS = 12000;
4
6
  export type Soul = {
@@ -29,25 +31,11 @@ export declare const TEAMMATE_TEMPLATES: readonly {
29
31
  about: string;
30
32
  soul: string;
31
33
  }[];
32
- /** What a teammate may do on one turn. A decision zone: approve, send back, or hand it to a person. A work zone: pick where it goes, ask its person, or say it can't. */
33
- export type TurnAction = "approve" | "send_back" | "hand_off" | "route" | "ask" | "cant" | "use_tool";
34
- /** v94: "use_tool" asks for one tool call (`tool`: its name, `input`: a JSON object as text); the turn goes on with its answer. */
35
- export type TurnAnswer = {
36
- action: TurnAction;
37
- answer: string;
38
- text: string;
39
- note: string;
40
- question: string;
41
- options: string[];
42
- reason: string;
43
- tool: string;
44
- input: string;
45
- /** v95: one short fact worth keeping for later cards ("": none). */
46
- remember: string;
47
- };
34
+ export type { TurnAction, TurnAnswer } from "./contracts/teammate-turn.js";
48
35
  /** Each field's limit, in characters: stated in the prompt before it answers (turnPrompt), and an answer over one is
49
36
  * asked once to shorten (teammateTurn); one still over is kept whole, never cut. "text" is what the next zones read;
50
- * "note" is a decision or send-back note. */
37
+ * "note" is a decision or send-back note. Not in the turn's schema (src/contracts/teammate-turn.ts): the CLI would
38
+ * refuse an answer a few characters over one whole. */
51
39
  export declare const TURN_LIMITS: {
52
40
  readonly answer: 60;
53
41
  readonly text: 12000;
@@ -58,49 +46,6 @@ export declare const TURN_LIMITS: {
58
46
  };
59
47
  /** The fields of an answer over their limits. */
60
48
  export declare function turnOverruns(answer: TurnAnswer): Overrun[];
61
- /** The shape Claude answers in: one flat object (a root union is refused). No length limits here: an answer
62
- * a few characters over one is refused whole by the CLI, so the limits are TURN_LIMITS, checked after. */
63
- export declare const TURN_SCHEMA: {
64
- readonly type: "object";
65
- readonly additionalProperties: false;
66
- readonly required: readonly ["action", "answer", "text", "note", "question", "options", "reason", "tool", "input", "remember"];
67
- readonly properties: {
68
- readonly action: {
69
- readonly type: "string";
70
- readonly enum: readonly ["approve", "send_back", "hand_off", "route", "ask", "cant", "use_tool"];
71
- };
72
- readonly answer: {
73
- readonly type: "string";
74
- };
75
- readonly text: {
76
- readonly type: "string";
77
- };
78
- readonly note: {
79
- readonly type: "string";
80
- };
81
- readonly question: {
82
- readonly type: "string";
83
- };
84
- readonly options: {
85
- readonly type: "array";
86
- readonly items: {
87
- readonly type: "string";
88
- };
89
- };
90
- readonly reason: {
91
- readonly type: "string";
92
- };
93
- readonly tool: {
94
- readonly type: "string";
95
- };
96
- readonly input: {
97
- readonly type: "string";
98
- };
99
- readonly remember: {
100
- readonly type: "string";
101
- };
102
- };
103
- };
104
49
  /** Everything one turn is told: the zone, the card as data, and what its people said. */
105
50
  export type TurnContext = {
106
51
  soul: string;
@@ -165,8 +110,13 @@ export type TurnContext = {
165
110
  };
166
111
  };
167
112
  export declare function turnPrompt(context: TurnContext): string;
168
- /** A turn's answer, checked against what this zone allows; null when it isn't one. */
169
- export declare function readTurn(value: unknown, context: Pick<TurnContext, "kind" | "canSendBack" | "answers" | "tools">): TurnAnswer | null;
113
+ /**
114
+ * A turn's answer, read through its contract (src/contracts/teammate-turn.ts) and then checked against what this zone
115
+ * allows, each refusal naming its path (`action: "route" isn't one this zone allows`). Text is trimmed and kept whole
116
+ * (a field over its limit is asked to shorten, turnOverruns); blank options are dropped, and an ask offers at most 4,
117
+ * each cut to an answer's length, and a tool name is cut to 140, as they always were.
118
+ */
119
+ export declare function readTurn(value: unknown, context: Pick<TurnContext, "kind" | "canSendBack" | "answers" | "tools">): ContractResult<TurnAnswer>;
170
120
  export type TurnRequest = {
171
121
  model: string;
172
122
  prompt: string;
@@ -190,10 +140,9 @@ export type TurnReply = ({
190
140
  export type TurnRunner = (request: TurnRequest) => Promise<TurnReply>;
191
141
  export declare const TURN_TIMEOUT_MS = 180000;
192
142
  type CommandRunner = (file: string, args: readonly string[], options: Parameters<typeof run>[2]) => Promise<ExecResult>;
193
- /** The production runner: Claude through this computer's sign-in, answering in TURN_SCHEMA, with no tools, no MCP servers and no repository. */
143
+ /** The production runner: Claude through this computer's sign-in, answering in the turn schema (TURN_MODEL_SCHEMA), with no tools, no MCP servers and no repository. */
194
144
  export declare function claudeTurnRunner(runner?: CommandRunner): TurnRunner;
195
145
  /** How a teammate is named to people: "Maya · Support". */
196
146
  export declare const teammateLabel: (soul: Pick<Soul, "name" | "role">) => string;
197
147
  /** How its acts read in a card's history: "Maya (AI)". */
198
148
  export declare const teammateActor: (soul: Pick<Soul, "name">) => string;
199
- export {};