@agent-native/core 0.207.1 → 0.207.2-nightly-20261013011333

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 (51) hide show
  1. package/dist/agent/production-agent.js +2 -0
  2. package/dist/cli/design-connect.d.ts +6 -0
  3. package/dist/cli/design-connect.js +880 -9
  4. package/dist/client/action-query-cache.d.ts +3 -1
  5. package/dist/client/action-query-cache.js +628 -161
  6. package/dist/client/action-query-cache.session-test-fixture.d.ts +49 -0
  7. package/dist/client/action-query-cache.session-test-fixture.js +151 -0
  8. package/dist/client/action-query-cache.test-fixtures.d.ts +2 -0
  9. package/dist/client/action-query-cache.test-fixtures.js +56 -0
  10. package/dist/client/motion-text.css +3 -0
  11. package/dist/client/motion-text.d.ts +38 -0
  12. package/dist/client/motion-text.js +551 -0
  13. package/dist/client/org/hooks.d.ts +3 -1
  14. package/dist/client/org/hooks.js +8 -2
  15. package/dist/client/route-chunk-recovery.d.ts +2 -0
  16. package/dist/client/route-chunk-recovery.js +11 -0
  17. package/dist/client/use-action.d.ts +10 -2
  18. package/dist/client/use-action.js +81 -12
  19. package/dist/client/use-settings-shortcut.js +8 -3
  20. package/dist/collab/awareness.d.ts +2 -2
  21. package/dist/collab/routes.d.ts +1 -1
  22. package/dist/collab/struct-routes.d.ts +1 -1
  23. package/dist/extensions/fetch-tool.js +8 -2
  24. package/dist/extensions/routes.js +7 -3
  25. package/dist/extensions/web-content.js +17 -2
  26. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  27. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  28. package/dist/mcp/build-server.js +5 -0
  29. package/dist/observability/routes.d.ts +4 -4
  30. package/dist/progress/routes.d.ts +1 -1
  31. package/dist/resources/handlers.d.ts +1 -1
  32. package/dist/secrets/routes.d.ts +5 -5
  33. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  34. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  35. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  36. package/dist/server/realtime-token.d.ts +1 -1
  37. package/dist/server/transcribe-voice.d.ts +1 -1
  38. package/dist/server/visual-edit-bridge-capability.d.ts +26 -0
  39. package/dist/server/visual-edit-bridge-capability.js +233 -0
  40. package/dist/shared/motion-easing.d.ts +6 -0
  41. package/dist/shared/motion-easing.js +109 -0
  42. package/dist/shared/motion-source-node-id.d.ts +3 -0
  43. package/dist/shared/motion-source-node-id.js +12 -0
  44. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  45. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +35 -36
  46. package/dist/triggers/actions/manage-automation.d.ts +5 -5
  47. package/dist/vite/client.js +2 -0
  48. package/dist/vite/motion-source-provenance-plugin.d.ts +3 -0
  49. package/dist/vite/motion-source-provenance-plugin.js +339 -0
  50. package/package.json +14 -5
  51. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +35 -36
@@ -0,0 +1,339 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { parse } from "@babel/parser";
5
+ import MagicString from "magic-string";
6
+ import { parse as parseHtml } from "parse5";
7
+ import { generatedMotionNodeId, isMotionSourceHostElementTag, } from "../shared/motion-source-node-id.js";
8
+ const NODE_ID_ATTRIBUTE = "data-agent-native-node-id";
9
+ const SOURCE_FILE_ATTRIBUTE = "data-agent-native-motion-source-file";
10
+ const SOURCE_HASH_ATTRIBUTE = "data-agent-native-motion-source-hash";
11
+ const HTML_NODE_ID_ATTRIBUTE = "data-agent-native-node-id";
12
+ function escapeHtmlAttribute(value) {
13
+ return value
14
+ .replaceAll("&", "&")
15
+ .replaceAll('"', """)
16
+ .replaceAll("<", "&lt;")
17
+ .replaceAll(">", "&gt;");
18
+ }
19
+ function instrumentMotionHtmlProvenance(args) {
20
+ const parseErrors = [];
21
+ const document = parseHtml(args.html, {
22
+ sourceCodeLocationInfo: true,
23
+ onParseError(error) {
24
+ parseErrors.push(error);
25
+ },
26
+ });
27
+ if (parseErrors.length > 0)
28
+ return null;
29
+ const hostNodes = [];
30
+ const visit = (node) => {
31
+ if (node.tagName && isMotionSourceHostElementTag(node.tagName)) {
32
+ hostNodes.push({ node, ordinal: hostNodes.length });
33
+ }
34
+ for (const child of node.childNodes ?? [])
35
+ visit(child);
36
+ };
37
+ visit(document);
38
+ const nodeIds = hostNodes.map(({ node, ordinal }) => {
39
+ const matches = (node.attrs ?? []).filter((attribute) => attribute.name === HTML_NODE_ID_ATTRIBUTE);
40
+ if (matches.length > 1)
41
+ return null;
42
+ const authoredId = matches[0]?.value;
43
+ const nodeId = authoredId ?? generatedMotionNodeId(args.modulePath, ordinal);
44
+ return /^[A-Za-z0-9_-]+$/.test(nodeId) ? nodeId : null;
45
+ });
46
+ const counts = new Map();
47
+ for (const nodeId of nodeIds) {
48
+ if (nodeId)
49
+ counts.set(nodeId, (counts.get(nodeId) ?? 0) + 1);
50
+ }
51
+ const edits = [];
52
+ for (const [index, { node }] of hostNodes.entries()) {
53
+ const nodeId = nodeIds[index];
54
+ const tag = node.sourceCodeLocation?.startTag;
55
+ if (!nodeId || counts.get(nodeId) !== 1 || !tag)
56
+ continue;
57
+ const attrs = new Map((node.attrs ?? []).map((attribute) => [attribute.name, attribute.value]));
58
+ const values = [
59
+ [HTML_NODE_ID_ATTRIBUTE, nodeId],
60
+ [SOURCE_FILE_ATTRIBUTE, args.modulePath],
61
+ [SOURCE_HASH_ATTRIBUTE, args.moduleVersionHash],
62
+ ];
63
+ const additions = [];
64
+ for (const [name, value] of values) {
65
+ const existingValue = attrs.get(name);
66
+ if (existingValue === value)
67
+ continue;
68
+ const location = tag.attrs?.[name];
69
+ const attribute = `${name}="${escapeHtmlAttribute(value)}"`;
70
+ if (location) {
71
+ edits.push({
72
+ start: location.startOffset,
73
+ end: location.endOffset,
74
+ text: attribute,
75
+ });
76
+ }
77
+ else {
78
+ if (name === HTML_NODE_ID_ATTRIBUTE &&
79
+ node.attrs?.some((entry) => entry.name === HTML_NODE_ID_ATTRIBUTE)) {
80
+ continue;
81
+ }
82
+ additions.push(attribute);
83
+ }
84
+ }
85
+ if (additions.length === 0)
86
+ continue;
87
+ const tagEnd = args.html[tag.endOffset - 2] === "/"
88
+ ? tag.endOffset - 2
89
+ : tag.endOffset - 1;
90
+ edits.push({ start: tagEnd, end: tagEnd, text: ` ${additions.join(" ")}` });
91
+ }
92
+ if (edits.length === 0)
93
+ return null;
94
+ const ordered = [...edits].sort((left, right) => right.start - left.start || right.end - left.end);
95
+ let result = args.html;
96
+ let previousStart = args.html.length + 1;
97
+ for (const edit of ordered) {
98
+ if (edit.end > previousStart || edit.end > args.html.length)
99
+ return null;
100
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
101
+ previousStart = edit.start;
102
+ }
103
+ return result;
104
+ }
105
+ /** Adds dev-only JSX identity evidence so source edits follow rendered nodes. */
106
+ export function motionSourceProvenancePlugin() {
107
+ let root = process.cwd();
108
+ let command = "serve";
109
+ return {
110
+ name: "agent-native-motion-source-provenance",
111
+ enforce: "pre",
112
+ configResolved(config) {
113
+ root = path.resolve(config.root);
114
+ command = config.command;
115
+ },
116
+ transformIndexHtml: {
117
+ order: "pre",
118
+ handler(html, context) {
119
+ if (command !== "serve" || !context.filename)
120
+ return;
121
+ const absolutePath = path.resolve(context.filename);
122
+ const modulePath = path
123
+ .relative(root, absolutePath)
124
+ .split(path.sep)
125
+ .join("/");
126
+ const extension = path.extname(modulePath).toLowerCase();
127
+ if (!modulePath ||
128
+ path.isAbsolute(modulePath) ||
129
+ modulePath === ".." ||
130
+ modulePath.startsWith("../") ||
131
+ ![".html", ".htm"].includes(extension) ||
132
+ modulePath
133
+ .split("/")
134
+ .some((segment) => ["node_modules", ".vite", "dist", "build"].includes(segment))) {
135
+ return;
136
+ }
137
+ let diskSource;
138
+ try {
139
+ diskSource = readFileSync(absolutePath, "utf8");
140
+ }
141
+ catch (error) {
142
+ throw new Error(`Unable to read the Motion HTML source at ${absolutePath}.`, { cause: error });
143
+ }
144
+ if (diskSource !== html)
145
+ return;
146
+ const moduleVersionHash = createHash("sha256")
147
+ .update(diskSource)
148
+ .digest("hex");
149
+ return (instrumentMotionHtmlProvenance({
150
+ html,
151
+ modulePath,
152
+ moduleVersionHash,
153
+ }) ?? undefined);
154
+ },
155
+ },
156
+ transform(code, id) {
157
+ if (command !== "serve" || !id || id.startsWith("\0"))
158
+ return null;
159
+ const fileName = id.split("?", 1)[0];
160
+ const extension = path.extname(fileName).toLowerCase();
161
+ if (extension !== ".tsx" && extension !== ".jsx")
162
+ return null;
163
+ const absolutePath = path.resolve(fileName);
164
+ let diskSource;
165
+ try {
166
+ diskSource = readFileSync(absolutePath, "utf8");
167
+ }
168
+ catch (error) {
169
+ throw new Error(`Unable to read the Motion source module at ${absolutePath}.`, { cause: error });
170
+ }
171
+ if (diskSource !== code)
172
+ return null;
173
+ const modulePath = path
174
+ .relative(root, absolutePath)
175
+ .split(path.sep)
176
+ .join("/");
177
+ if (!modulePath ||
178
+ modulePath === ".." ||
179
+ modulePath.startsWith("../") ||
180
+ modulePath
181
+ .split("/")
182
+ .some((segment) => ["node_modules", ".vite", "dist", "build"].includes(segment))) {
183
+ return null;
184
+ }
185
+ if (!code.includes("<"))
186
+ return null;
187
+ let syntaxTree;
188
+ try {
189
+ syntaxTree = parse(code, {
190
+ plugins: extension === ".tsx" ? ["jsx", "typescript"] : ["jsx"],
191
+ sourceType: "unambiguous",
192
+ });
193
+ }
194
+ catch (error) {
195
+ if (error instanceof SyntaxError)
196
+ return null;
197
+ throw error;
198
+ }
199
+ const sourceHash = createHash("sha256").update(diskSource).digest("hex");
200
+ const magic = new MagicString(code, { filename: absolutePath });
201
+ let hostElementOrdinal = 0;
202
+ let changed = false;
203
+ const visit = (node) => {
204
+ if (node.type === "JSXElement") {
205
+ const opening = asBabelNode(node.openingElement);
206
+ const tag = asBabelNode(opening?.name);
207
+ if (opening?.type === "JSXOpeningElement" &&
208
+ tag?.type === "JSXIdentifier" &&
209
+ typeof tag.name === "string" &&
210
+ isMotionSourceHostElementTag(tag.name)) {
211
+ const ordinal = hostElementOrdinal++;
212
+ const properties = Array.isArray(opening.attributes)
213
+ ? opening.attributes
214
+ .map(asBabelNode)
215
+ .filter((property) => property !== null)
216
+ : [];
217
+ const hasSpread = properties.some((property) => property.type === "JSXSpreadAttribute");
218
+ const attributes = new Map();
219
+ for (const property of properties) {
220
+ if (property.type !== "JSXAttribute")
221
+ continue;
222
+ const name = asBabelNode(property.name);
223
+ if (name?.type !== "JSXIdentifier" ||
224
+ typeof name.name !== "string") {
225
+ continue;
226
+ }
227
+ const matching = attributes.get(name.name) ?? [];
228
+ matching.push(property);
229
+ attributes.set(name.name, matching);
230
+ }
231
+ const isStaticStringAttribute = (property) => {
232
+ const value = asBabelNode(property.value);
233
+ return (value?.type === "StringLiteral" &&
234
+ typeof value.value === "string");
235
+ };
236
+ const staticStringValue = (property) => {
237
+ const value = property ? asBabelNode(property.value) : null;
238
+ return value?.type === "StringLiteral" &&
239
+ typeof value.value === "string"
240
+ ? value.value
241
+ : undefined;
242
+ };
243
+ const idAttributes = attributes.get(NODE_ID_ATTRIBUTE) ?? [];
244
+ const hasDynamicNodeId = idAttributes.length > 0 &&
245
+ (idAttributes.length !== 1 ||
246
+ !isStaticStringAttribute(idAttributes[0]));
247
+ const authoredNodeId = idAttributes.length === 1 && !hasDynamicNodeId
248
+ ? staticStringValue(idAttributes[0])
249
+ : undefined;
250
+ const nodeId = authoredNodeId ?? generatedMotionNodeId(modulePath, ordinal);
251
+ const malformedReservedAttribute = [
252
+ NODE_ID_ATTRIBUTE,
253
+ SOURCE_FILE_ATTRIBUTE,
254
+ SOURCE_HASH_ATTRIBUTE,
255
+ ].some((name) => {
256
+ const matches = attributes.get(name) ?? [];
257
+ return (matches.length > 1 ||
258
+ (matches.length === 1 && !isStaticStringAttribute(matches[0])));
259
+ });
260
+ if (!hasSpread &&
261
+ !hasDynamicNodeId &&
262
+ !malformedReservedAttribute &&
263
+ /^[A-Za-z0-9_-]+$/.test(nodeId)) {
264
+ const expectedAttributes = [
265
+ ...(!authoredNodeId
266
+ ? [[NODE_ID_ATTRIBUTE, nodeId]]
267
+ : []),
268
+ [SOURCE_FILE_ATTRIBUTE, modulePath],
269
+ [SOURCE_HASH_ATTRIBUTE, sourceHash],
270
+ ];
271
+ for (const [name, value] of expectedAttributes) {
272
+ const existing = attributes.get(name) ?? [];
273
+ if (existing.length === 0) {
274
+ const insertionPoint = properties.at(-1)?.end ?? tag.end;
275
+ if (typeof insertionPoint !== "number") {
276
+ throw new Error(`Unable to locate JSX attributes in ${absolutePath}.`);
277
+ }
278
+ magic.appendLeft(insertionPoint, ` ${name}=${JSON.stringify(value)}`);
279
+ changed = true;
280
+ continue;
281
+ }
282
+ const attribute = existing[0];
283
+ const start = attribute.start;
284
+ const end = attribute.end;
285
+ if (existing.length !== 1 ||
286
+ !isStaticStringAttribute(attribute) ||
287
+ typeof start !== "number" ||
288
+ typeof end !== "number") {
289
+ continue;
290
+ }
291
+ if (staticStringValue(attribute) !== value) {
292
+ magic.update(start, end, `${name}=${JSON.stringify(value)}`);
293
+ changed = true;
294
+ }
295
+ }
296
+ }
297
+ }
298
+ }
299
+ for (const value of Object.values(node)) {
300
+ if (Array.isArray(value)) {
301
+ for (const child of value) {
302
+ const astChild = asBabelNode(child);
303
+ if (astChild)
304
+ visit(astChild);
305
+ }
306
+ }
307
+ else {
308
+ const astChild = asBabelNode(value);
309
+ if (astChild)
310
+ visit(astChild);
311
+ }
312
+ }
313
+ };
314
+ const program = asBabelNode(syntaxTree.program);
315
+ if (!program)
316
+ return null;
317
+ visit(program);
318
+ if (!changed)
319
+ return null;
320
+ return {
321
+ code: magic.toString(),
322
+ map: magic.generateMap({
323
+ hires: "boundary",
324
+ includeContent: true,
325
+ source: modulePath,
326
+ }),
327
+ };
328
+ },
329
+ };
330
+ }
331
+ function asBabelNode(value) {
332
+ if (!value ||
333
+ typeof value !== "object" ||
334
+ !("type" in value) ||
335
+ typeof value.type !== "string") {
336
+ return null;
337
+ }
338
+ return value;
339
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.207.1",
3
+ "version": "0.207.2-nightly-20261013011333",
4
4
  "description": "The agentic application framework for building autonomous agents with intuitive UIs",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -109,6 +109,7 @@
109
109
  "./server/interval-job": "./dist/server/interval-job.js",
110
110
  "./server/sse": "./dist/server/sse.js",
111
111
  "./server/short-lived-token": "./dist/server/short-lived-token.js",
112
+ "./visual-edit-bridge-capability": "./dist/server/visual-edit-bridge-capability.js",
112
113
  "./integrations": "./dist/integrations/index.js",
113
114
  "./integrations/computer-supervision": "./dist/integrations/computer-supervision.js",
114
115
  "./integrations/runtime": "./dist/integrations/runtime.js",
@@ -523,6 +524,8 @@
523
524
  "./client/integrations/webhook-url": "./dist/client/integrations/webhook-url.js",
524
525
  "./client/labs/use-lab": "./dist/client/labs/use-lab.js",
525
526
  "./client/mcp-app-host": "./dist/client/mcp-app-host.js",
527
+ "./client/motion-text": "./dist/client/motion-text.js",
528
+ "./client/motion-text.css": "./dist/client/motion-text.css",
526
529
  "./client/onboarding/first-run-enabled": "./dist/client/onboarding/first-run-enabled.js",
527
530
  "./client/onboarding/first-run-registry": "./dist/client/onboarding/first-run-registry.js",
528
531
  "./client/onboarding/first-run-status": "./dist/client/onboarding/first-run-status.js",
@@ -609,6 +612,8 @@
609
612
  "./shared/loading-labels": "./dist/shared/loading-labels.js",
610
613
  "./shared/mcp-app-widget-embed": "./dist/shared/mcp-app-widget-embed.js",
611
614
  "./shared/mcp-connect-content": "./dist/shared/mcp-connect-content.js",
615
+ "./shared/motion-easing": "./dist/shared/motion-easing.js",
616
+ "./shared/motion-source-node-id": "./dist/shared/motion-source-node-id.js",
612
617
  "./shared/password-policy": "./dist/shared/password-policy.js",
613
618
  "./shared/qa-test-email": "./dist/shared/qa-test-email.js",
614
619
  "./shared/runtime-config": "./dist/shared/runtime-config.js",
@@ -653,6 +658,7 @@
653
658
  },
654
659
  "dependencies": {
655
660
  "@anthropic-ai/sdk": "^0.90.0",
661
+ "@babel/parser": "8.0.7",
656
662
  "@clack/prompts": "^1.7.0",
657
663
  "@mcp-b/webmcp-polyfill": "^5.1.0",
658
664
  "@modelcontextprotocol/client": "2.0.0",
@@ -681,8 +687,10 @@
681
687
  "jiti": "^2.7.0",
682
688
  "jose": "^6.2.2",
683
689
  "linkedom": "^0.18.13",
690
+ "magic-string": "^1.4.3",
684
691
  "minimatch": "^10.0.0",
685
692
  "nitro": "3.0.260903-beta",
693
+ "parse5": "8.0.1",
686
694
  "postgres": "^3.4.9",
687
695
  "react-i18next": "17.0.8",
688
696
  "run": "2.0.1",
@@ -694,8 +702,8 @@
694
702
  "yaml": "^2.9.0",
695
703
  "yjs": "^13.6.33",
696
704
  "zod": "^4.3.6",
697
- "@agent-native/agentkit": "^0.207.1",
698
- "@agent-native/recap-cli": "^0.5.70"
705
+ "@agent-native/recap-cli": "^0.5.70",
706
+ "@agent-native/agentkit": "^0.207.2-nightly-20261013011333"
699
707
  },
700
708
  "devDependencies": {
701
709
  "@ai-sdk/anthropic": "^3.0.71",
@@ -765,7 +773,7 @@
765
773
  "xlsx": "^0.18.5"
766
774
  },
767
775
  "peerDependencies": {
768
- "@agent-native/core-corpus": ">=0.196.0",
776
+ "@agent-native/core-corpus": "0.207.2-nightly-20261013011333",
769
777
  "@ai-sdk/anthropic": ">=3",
770
778
  "@ai-sdk/cohere": ">=3",
771
779
  "@ai-sdk/google": ">=3",
@@ -951,6 +959,7 @@
951
959
  "build": "tsc && tsc -p tsconfig.cli.json && node scripts/finalize-build.mjs && node scripts/check-dist-imports.mjs",
952
960
  "dev": "tsc --watch",
953
961
  "typecheck": "tsc --noEmit",
954
- "test": "vitest --run --dir src"
962
+ "test": "vitest --run --dir src",
963
+ "test:motion-text-package": "vitest --run scripts/motion-text-package.spec.ts"
955
964
  }
956
965
  }
@@ -19,7 +19,7 @@ Four phases, in order. Phase 0 comes before any investigation, not after.
19
19
  before investigating any of it.
20
20
  1. **Answer the people who answered you.** Older open questions first.
21
21
  2. **Fix** what the evidence actually proves, at the owning boundary.
22
- 3. **Reply**, under a hard question budget, then recap.
22
+ 3. **Reply** with useful questions, then recap.
23
23
 
24
24
  Output is fixes. Reply in-thread for every workflow reaction added this run;
25
25
  continuing work gets a concrete progress reply, and resolved work gets a final
@@ -82,9 +82,13 @@ error message, URL, or visible flow is enough to start tracing its owner; don't
82
82
  wait for details that would not change the reproduction attempt. Phase 2's
83
83
  local reproduction procedure applies to all defect reports, not only bashes.
84
84
 
85
- If no safe repo-owned fix is evident after that attempt, record the specific
86
- evidence limit. Ask only a question whose answer could change the reproduction
87
- or unblock a fix; after four days without an answer, record **Abandoned - no
85
+ If no safe repo-owned fix is evident after reproduction, record the evidence
86
+ limit. For each defect where reporter input changes reproduction or unblocks a
87
+ fix, ask one direct question naming the missing detail or action and why it
88
+ gates the next step. Inspect supplied run IDs, logs, links, and artifacts
89
+ yourself. Keep internal access, config, observability, and deployment gaps
90
+ internal; never ask reporters for developer traces. Do not label the thread
91
+ **Open** instead of asking. After four days unanswered, record **Abandoned - no
88
92
  answer in 4 days**.
89
93
 
90
94
  Use **Skipped** only for non-defects, never breakage. **Open - no question**
@@ -448,9 +452,11 @@ instruction or prompt exception.
448
452
 
449
453
  ## Phase 3: reply
450
454
 
451
- Thank issue reporters for opening it and Slack reporters for sharing. Give the
452
- status or ask a useful question. Follow `address-feedback-with-replies` for
453
- Slack voice. End each Slack reply with `this was sent from a bot.` after its
455
+ Thank issue reporters for opening it and Slack reporters for sharing. Give an
456
+ active status while work continues. If reporter input gates a fix, ask the
457
+ direct question specified in Phase 0; do not substitute **Open**, **pending**,
458
+ or an evidence description. Follow `address-feedback-with-replies` for Slack
459
+ voice. End each Slack reply with `this was sent from a bot.` after its
454
460
  plain-language status.
455
461
 
456
462
  Short replies: say the change, limits, and next step. Link merged PRs and say
@@ -474,32 +480,26 @@ needed. Do not reopen Slack in later runs to reply.
474
480
  observations may be silent; merged Slack fixes still get the reply above.
475
481
  **Shipped** is for upvoted improvements.
476
482
  - **In progress** — name continuing work and its owner; ask nothing.
477
- - **A question** — subject to the budget below.
483
+ - **A question** — follow the rules below.
478
484
 
479
- Unclaimed scope/noise gets an internal recap row. Ask about an unverified defect
480
- only when one answer would unblock it; otherwise record **Open - no question**
481
- once. Cluster duplicates. Before re-reading any thread to reply, apply the Slack
482
- ownership gate; stay out of marked messages and active human work.
485
+ Unclaimed scope/noise gets an internal recap row. Cluster duplicates. Before
486
+ replying, apply the Slack ownership gate; stay out of marked messages and active
487
+ human work.
483
488
 
484
- ### The question budget
489
+ ### Ask only answerable questions
485
490
 
486
- **At most three questions per run, across all sources.** Most runs ask zero or
487
- one.
491
+ Ask one direct question for each distinct blocked defect when the reporter's
492
+ answer or action changes reproduction or unblocks the fix. There is no per-run
493
+ cap; do not replace a useful question with **Open** or an in-progress status.
494
+ Keep one pending question per thread. After an answer, try the fix before asking
495
+ anything else. Never ask for supplied logs/IDs, internal access/config/tooling,
496
+ subjective choices, or details that do not change the next step. If a
497
+ user-visible link or ID alone blocks reproduction, ask for it and say where to
498
+ find it.
488
499
 
489
- So rank before you ask. For each candidate, state: *if I get this answer, I
490
- can ship the fix.* Ask the three with the strongest answer. If fewer than
491
- three clear that bar, ask fewer. Everything below the cut is an internal open
492
- item, not a message.
493
-
494
- Never ask for supplied/inspectable evidence, irrelevant IDs/build numbers,
495
- subjective choices, or internal blockers. If a user-visible link or ID is the
496
- sole blocker, ask plainly and say where to find it.
497
-
498
- At most one clarification question may be pending per thread at a time. Once it
499
- is answered or resolved, attempt the fix; if that exposes a different required
500
- detail, ask at most one new, non-repeating question. Never stack questions or
501
- repeat a pending one. If a needed artifact is inaccessible to you, ask for a
502
- fresh link - not for its contents again.
500
+ If an answer exposes a different required detail, ask at most one new,
501
+ non-repeating question. If an artifact is inaccessible, request a fresh link,
502
+ not its contents.
503
503
 
504
504
  ## Verification and identity
505
505
 
@@ -563,7 +563,7 @@ Messages: product N · QA N · dev N (total N)
563
563
  Deployment cursor: <timestamp> · carried active run IDs/rows: <ids/count>
564
564
  Release lanes: N inspected · failed/stale N · target verified N
565
565
  Reaction-gated skips: N · claimed N · answered N
566
- Questions asked: N/3 · Dropped at 4 days: N
566
+ Questions asked: N · Dropped at 4 days: N
567
567
  Repeats of a prior Fixed claim: N (each with its earlier thread and failed fix)
568
568
  Upvoted items in scope: N (built: N)
569
569
 
@@ -576,12 +576,11 @@ Tracker: <sheet/export and bounded range> - N rows enumerated, N ledgers complet
576
576
  Unavailable or unverified: ...
577
577
  ```
578
578
 
579
- `Open - no question` is a last resort, not a success state. It requires that
580
- you worked the defect, could not fix it, and could not form a question that would
581
- unblock it; a run whose ledger is mostly `Open - no question` has under-asked, not
582
- finished. It keeps our eye and has no checkmark. "Nothing
583
- matched" is valid only after each source was queried successfully, with the
584
- cursor stated.
579
+ `Open - no question` follows reproduction when there is no safe fix and no
580
+ reporter answer or action that changes the next step. Ask when reporter input
581
+ unblocks the fix; keep internal access/tooling blockers internal. Mostly open
582
+ ledgers mean under-asking. Keep `👀`, no `✅`. "Nothing matched" is valid only
583
+ after every source was queried successfully and the cursor is stated.
585
584
 
586
585
  ## Related skills
587
586