tau-coding-agent 0.1.4 → 0.1.5

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 (45) hide show
  1. package/README.md +1 -1
  2. package/extensions/answer.ts +48 -42
  3. package/extensions/branch-term.ts +8 -13
  4. package/extensions/btw.ts +4 -4
  5. package/extensions/ghostty.ts +64 -11
  6. package/extensions/insights.ts +4 -3
  7. package/extensions/loop.ts +37 -9
  8. package/extensions/memory.ts +5 -10
  9. package/extensions/notify.ts +24 -5
  10. package/extensions/openai-fast.ts +6 -4
  11. package/extensions/openai-verbosity.ts +6 -4
  12. package/extensions/review/fix.ts +239 -0
  13. package/extensions/review/git.ts +517 -0
  14. package/extensions/review/index.ts +232 -0
  15. package/extensions/review/message-queue.ts +249 -0
  16. package/extensions/review/models.ts +589 -0
  17. package/extensions/review/prompts.ts +337 -0
  18. package/extensions/review/renderers/inline.ts +126 -0
  19. package/extensions/review/request.ts +504 -0
  20. package/extensions/review/review.ts +1305 -0
  21. package/extensions/review/runner.ts +522 -0
  22. package/extensions/review/runtime.ts +275 -0
  23. package/extensions/review/schema.ts +160 -0
  24. package/extensions/review/submit-review-tool.ts +66 -0
  25. package/extensions/review/submit-triage-tool.ts +59 -0
  26. package/extensions/review/triage.ts +746 -0
  27. package/extensions/sandbox/index.ts +399 -102
  28. package/extensions/{interlude.ts → stash.ts} +13 -11
  29. package/extensions/tool-display-mode.ts +2 -3
  30. package/extensions/usage/index.ts +8 -4
  31. package/extensions/websearch/README.md +1 -1
  32. package/extensions/websearch/browser/chromium.ts +42 -31
  33. package/extensions/websearch/browser/discovery.ts +11 -9
  34. package/extensions/websearch/browser/firefox.ts +10 -7
  35. package/extensions/websearch/browser/fs.ts +10 -0
  36. package/extensions/websearch/browser/sqlite.ts +22 -15
  37. package/extensions/websearch/config.ts +2 -2
  38. package/extensions/websearch/index.ts +6 -6
  39. package/extensions/worktree.ts +33 -33
  40. package/package.json +3 -3
  41. package/skills/git-clean-history/SKILL.md +1 -0
  42. package/skills/git-commit/SKILL.md +2 -2
  43. package/skills/oracle/scripts/oracle +28 -11
  44. package/themes/tau-dark.json +2 -1
  45. package/extensions/review.ts +0 -4420
@@ -0,0 +1,517 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { spawn } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { promises as fs } from "node:fs";
5
+ import path from "node:path";
6
+
7
+ import { parsePrReference } from "./request.js";
8
+ import type { ReviewFingerprint, ReviewTarget } from "./schema.js";
9
+
10
+ export type ResolvedScope =
11
+ | {
12
+ kind: "working-tree";
13
+ trackedFiles: string[];
14
+ untrackedFiles: string[];
15
+ hasHead: boolean;
16
+ description: string;
17
+ }
18
+ | {
19
+ kind: "branch-diff";
20
+ baseBranch: string;
21
+ mergeBase: string;
22
+ diffFiles: string[];
23
+ description: string;
24
+ }
25
+ | {
26
+ kind: "commit";
27
+ sha: string;
28
+ description: string;
29
+ }
30
+ | {
31
+ kind: "folder";
32
+ paths: string[];
33
+ description: string;
34
+ }
35
+ | {
36
+ kind: "custom";
37
+ instructions: string;
38
+ description: string;
39
+ };
40
+
41
+ const REVIEW_UNTRACKED_HASH_DISABLED = "__disabled__";
42
+
43
+ export type GitStatusReporter = (message: string, type?: "info" | "warning" | "error") => void;
44
+
45
+ function hashString(value: string): string {
46
+ return createHash("sha256").update(value).digest("hex");
47
+ }
48
+
49
+ async function runGit(
50
+ pi: ExtensionAPI,
51
+ args: string[],
52
+ ): Promise<{ stdout: string; stderr: string; code: number }> {
53
+ const { stdout, stderr, code } = await pi.exec("git", args);
54
+ return { stdout, stderr, code };
55
+ }
56
+
57
+ export async function isGitRepo(pi: ExtensionAPI): Promise<boolean> {
58
+ const result = await runGit(pi, ["rev-parse", "--git-dir"]);
59
+ return result.code === 0;
60
+ }
61
+
62
+ async function getCurrentBranch(pi: ExtensionAPI): Promise<string | null> {
63
+ const { stdout, code } = await runGit(pi, ["branch", "--show-current"]);
64
+ if (code !== 0) return null;
65
+ const branch = stdout.trim();
66
+ return branch.length > 0 ? branch : null;
67
+ }
68
+
69
+ async function resolveHeadSha(pi: ExtensionAPI): Promise<string | null> {
70
+ const { code, stdout } = await runGit(pi, ["rev-parse", "--verify", "HEAD"]);
71
+ if (code !== 0) return null;
72
+ const sha = stdout.trim();
73
+ return sha.length > 0 ? sha : null;
74
+ }
75
+
76
+ async function hasHeadCommit(pi: ExtensionAPI): Promise<boolean> {
77
+ return (await resolveHeadSha(pi)) !== null;
78
+ }
79
+
80
+ async function getDefaultBranch(pi: ExtensionAPI): Promise<string> {
81
+ const remoteHead = await runGit(pi, ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"]);
82
+ if (remoteHead.code === 0 && remoteHead.stdout.trim()) {
83
+ return remoteHead.stdout.trim().replace(/^origin\//, "");
84
+ }
85
+
86
+ const branches = await runGit(pi, ["branch", "--format=%(refname:short)"]);
87
+ if (branches.code === 0) {
88
+ const names = parseGitFileList(branches.stdout);
89
+ if (names.includes("main")) return "main";
90
+ if (names.includes("master")) return "master";
91
+ if (names.length > 0) return names[0];
92
+ }
93
+
94
+ return "main";
95
+ }
96
+
97
+ async function getMergeBase(pi: ExtensionAPI, branch: string): Promise<string | null> {
98
+ const { stdout, code } = await runGit(pi, ["merge-base", "HEAD", branch]);
99
+ if (code !== 0) return null;
100
+ const sha = stdout.trim();
101
+ return sha.length > 0 ? sha : null;
102
+ }
103
+
104
+ function parseGitFileList(stdout: string): string[] {
105
+ return stdout
106
+ .split("\n")
107
+ .map((line) => line.trim())
108
+ .filter(Boolean);
109
+ }
110
+
111
+ async function getTrackedChangedFiles(pi: ExtensionAPI, hasHead?: boolean): Promise<string[]> {
112
+ const headAvailable = hasHead ?? (await hasHeadCommit(pi));
113
+ if (!headAvailable) {
114
+ const [staged, unstaged] = await Promise.all([
115
+ runGit(pi, ["diff", "--cached", "--name-only"]),
116
+ runGit(pi, ["diff", "--name-only"]),
117
+ ]);
118
+
119
+ const files = new Set<string>([
120
+ ...(staged.code === 0 ? parseGitFileList(staged.stdout) : []),
121
+ ...(unstaged.code === 0 ? parseGitFileList(unstaged.stdout) : []),
122
+ ]);
123
+ return Array.from(files).sort((a, b) => a.localeCompare(b));
124
+ }
125
+
126
+ const { stdout, code } = await runGit(pi, ["diff", "HEAD", "--name-only"]);
127
+ if (code !== 0) return [];
128
+ return parseGitFileList(stdout);
129
+ }
130
+
131
+ async function getUntrackedFiles(pi: ExtensionAPI): Promise<string[]> {
132
+ const { stdout, code } = await runGit(pi, ["ls-files", "--others", "--exclude-standard"]);
133
+ if (code !== 0) return [];
134
+ return parseGitFileList(stdout);
135
+ }
136
+
137
+ async function getDiffFilesInRange(pi: ExtensionAPI, range: string): Promise<string[]> {
138
+ const { stdout, code } = await runGit(pi, ["diff", "--name-only", range]);
139
+ if (code !== 0) return [];
140
+ return parseGitFileList(stdout);
141
+ }
142
+
143
+ async function hasPendingTrackedChanges(pi: ExtensionAPI): Promise<boolean> {
144
+ const { stdout, code } = await runGit(pi, ["status", "--porcelain"]);
145
+ if (code !== 0) return false;
146
+ const lines = stdout.split("\n").filter((line) => line.length > 0);
147
+ return lines.some((line) => !line.startsWith("??"));
148
+ }
149
+
150
+ async function getPrInfo(
151
+ pi: ExtensionAPI,
152
+ prNumber: number,
153
+ ): Promise<{ baseBranch: string; headBranch: string } | null> {
154
+ const { stdout, code } = await pi.exec("gh", [
155
+ "pr",
156
+ "view",
157
+ String(prNumber),
158
+ "--json",
159
+ "baseRefName,headRefName",
160
+ ]);
161
+ if (code !== 0) return null;
162
+ try {
163
+ const data = JSON.parse(stdout);
164
+ if (typeof data?.baseRefName === "string" && typeof data?.headRefName === "string") {
165
+ return {
166
+ baseBranch: data.baseRefName,
167
+ headBranch: data.headRefName,
168
+ };
169
+ }
170
+ return null;
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+
176
+ async function checkoutPr(
177
+ pi: ExtensionAPI,
178
+ prNumber: number,
179
+ ): Promise<{ ok: boolean; error?: string }> {
180
+ const fetch = await runGit(pi, ["fetch", "origin", `refs/pull/${prNumber}/head`]);
181
+ if (fetch.code !== 0) {
182
+ const error = (fetch.stderr || fetch.stdout || "Failed to fetch PR").trim();
183
+ return { ok: false, error };
184
+ }
185
+
186
+ const checkout = await runGit(pi, ["switch", "--detach", "FETCH_HEAD"]);
187
+ if (checkout.code !== 0) {
188
+ const error = (checkout.stderr || checkout.stdout || "Failed to checkout PR").trim();
189
+ return { ok: false, error };
190
+ }
191
+
192
+ return { ok: true };
193
+ }
194
+
195
+ export async function getPrCheckoutBlockedError(pi: ExtensionAPI): Promise<string | null> {
196
+ return (await hasPendingTrackedChanges(pi))
197
+ ? "Cannot checkout PR with pending tracked changes. Commit or stash first."
198
+ : null;
199
+ }
200
+
201
+ export async function preparePrCheckoutScope(
202
+ pi: ExtensionAPI,
203
+ reportStatus: GitStatusReporter | undefined,
204
+ details: { prNumber: number; baseBranch: string; headBranch: string },
205
+ ): Promise<
206
+ | { ok: false; error: string }
207
+ | { ok: true; scope: Extract<ResolvedScope, { kind: "branch-diff" }> }
208
+ > {
209
+ const blockedError = await getPrCheckoutBlockedError(pi);
210
+ if (blockedError) {
211
+ return { ok: false, error: blockedError };
212
+ }
213
+
214
+ reportStatus?.(`Checking out PR #${details.prNumber}...`, "info");
215
+ const checkout = await checkoutPr(pi, details.prNumber);
216
+ if (!checkout.ok) {
217
+ return {
218
+ ok: false,
219
+ error: `Failed to checkout PR #${details.prNumber}: ${checkout.error ?? "unknown error"}`,
220
+ };
221
+ }
222
+ reportStatus?.(`Checked out PR #${details.prNumber} (${details.headBranch}).`, "info");
223
+
224
+ const resolvedScope = await resolveBranchDiffScope(pi, {
225
+ baseBranch: details.baseBranch,
226
+ description: (diffFileCount) =>
227
+ `PR #${details.prNumber} diff vs ${details.baseBranch} (${diffFileCount} files)`,
228
+ mergeBaseError: `Could not determine merge-base against PR base branch ${details.baseBranch}.`,
229
+ emptyDiffError: `No differences found for PR #${details.prNumber} against ${details.baseBranch}.`,
230
+ });
231
+ if (!resolvedScope.scope || resolvedScope.scope.kind !== "branch-diff") {
232
+ return {
233
+ ok: false,
234
+ error: resolvedScope.error ?? `Failed to resolve PR #${details.prNumber} scope.`,
235
+ };
236
+ }
237
+
238
+ return { ok: true, scope: resolvedScope.scope };
239
+ }
240
+
241
+ export async function loadProjectReviewGuidelines(cwd: string): Promise<string | null> {
242
+ let currentDir = path.resolve(cwd);
243
+
244
+ while (true) {
245
+ const piDir = path.join(currentDir, ".pi");
246
+ const guidelinesPath = path.join(currentDir, "REVIEW_GUIDELINES.md");
247
+
248
+ const piStats = await fs.stat(piDir).catch(() => null);
249
+ if (piStats?.isDirectory()) {
250
+ try {
251
+ const content = await fs.readFile(guidelinesPath, "utf8");
252
+ const trimmed = content.trim();
253
+ return trimmed.length > 0 ? trimmed : null;
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
258
+
259
+ const parent = path.dirname(currentDir);
260
+ if (parent === currentDir) return null;
261
+ currentDir = parent;
262
+ }
263
+ }
264
+
265
+ async function hashGitDiff(pi: ExtensionAPI): Promise<string> {
266
+ const head = await runGit(pi, ["diff", "--no-ext-diff", "HEAD"]);
267
+ if (head.code === 0) return hashString(head.stdout);
268
+
269
+ const [staged, unstaged] = await Promise.all([
270
+ runGit(pi, ["diff", "--no-ext-diff", "--cached"]),
271
+ runGit(pi, ["diff", "--no-ext-diff"]),
272
+ ]);
273
+ const stagedHash = staged.code === 0 ? hashString(staged.stdout) : null;
274
+ const unstagedHash = unstaged.code === 0 ? hashString(unstaged.stdout) : null;
275
+ if (!stagedHash && !unstagedHash) return hashString("");
276
+ return hashString(`${stagedHash ?? ""}\n${unstagedHash ?? ""}`);
277
+ }
278
+
279
+ async function computeUntrackedContentHash(
280
+ pi: ExtensionAPI,
281
+ cwd: string,
282
+ precomputedUntrackedFiles?: string[],
283
+ ): Promise<string> {
284
+ const untrackedFiles = [...(precomputedUntrackedFiles ?? (await getUntrackedFiles(pi)))].sort(
285
+ (a, b) => a.localeCompare(b),
286
+ );
287
+ if (untrackedFiles.length === 0) return hashString("");
288
+
289
+ const hashes = await hashObjectBatch(cwd, untrackedFiles);
290
+ const entries = untrackedFiles.map((file, i) => `${file}\0${hashes[i] ?? "missing"}`);
291
+ return hashString(entries.join("\n"));
292
+ }
293
+
294
+ function hashObjectBatch(cwd: string, files: string[]): Promise<string[]> {
295
+ return new Promise((resolve) => {
296
+ const proc = spawn("git", ["hash-object", "--stdin-paths"], {
297
+ cwd,
298
+ stdio: ["pipe", "pipe", "ignore"],
299
+ });
300
+ let stdout = "";
301
+ proc.stdout.on("data", (chunk: Buffer) => {
302
+ stdout += chunk.toString();
303
+ });
304
+ proc.on("error", () => resolve([]));
305
+ proc.on("close", (code) => {
306
+ resolve(code === 0 ? stdout.trim().split("\n") : []);
307
+ });
308
+ proc.stdin.on("error", () => {
309
+ /* ignore broken pipe */
310
+ });
311
+ proc.stdin.end(files.join("\n"));
312
+ });
313
+ }
314
+
315
+ export async function computeCurrentFingerprint(
316
+ pi: ExtensionAPI,
317
+ cwd: string,
318
+ includeUntracked: boolean,
319
+ precomputedUntrackedFiles?: string[],
320
+ ): Promise<ReviewFingerprint> {
321
+ const [headSha, branch, trackedDiffHash, untrackedHash] = await Promise.all([
322
+ resolveHeadSha(pi).then((sha) => sha ?? ""),
323
+ getCurrentBranch(pi).then((branch) => branch ?? ""),
324
+ hashGitDiff(pi),
325
+ includeUntracked
326
+ ? computeUntrackedContentHash(pi, cwd, precomputedUntrackedFiles)
327
+ : Promise.resolve(REVIEW_UNTRACKED_HASH_DISABLED),
328
+ ]);
329
+
330
+ return { headSha, branch, trackedDiffHash, untrackedHash };
331
+ }
332
+
333
+ export function fingerprintsEqual(a: ReviewFingerprint, b: ReviewFingerprint): boolean {
334
+ return (
335
+ a.headSha === b.headSha &&
336
+ a.branch === b.branch &&
337
+ a.trackedDiffHash === b.trackedDiffHash &&
338
+ a.untrackedHash === b.untrackedHash
339
+ );
340
+ }
341
+
342
+ // --- Scope resolution ---
343
+
344
+ type BranchDiffScopeOptions = {
345
+ baseBranch: string;
346
+ description: (diffFileCount: number) => string;
347
+ mergeBaseError: string;
348
+ emptyDiffError: string;
349
+ };
350
+
351
+ async function resolveBranchDiffScope(
352
+ pi: ExtensionAPI,
353
+ options: BranchDiffScopeOptions,
354
+ ): Promise<{ scope?: ResolvedScope; error?: string }> {
355
+ const mergeBase = await getMergeBase(pi, options.baseBranch);
356
+ if (!mergeBase) {
357
+ return { error: options.mergeBaseError };
358
+ }
359
+
360
+ const range = `${mergeBase}..HEAD`;
361
+ const diffFiles = await getDiffFilesInRange(pi, range);
362
+ if (diffFiles.length === 0) {
363
+ return { error: options.emptyDiffError };
364
+ }
365
+
366
+ return {
367
+ scope: {
368
+ kind: "branch-diff",
369
+ baseBranch: options.baseBranch,
370
+ mergeBase,
371
+ diffFiles,
372
+ description: options.description(diffFiles.length),
373
+ },
374
+ };
375
+ }
376
+
377
+ export async function resolveScope(
378
+ pi: ExtensionAPI,
379
+ target: ReviewTarget,
380
+ reportStatus?: GitStatusReporter,
381
+ ): Promise<{ scope?: ResolvedScope; error?: string }> {
382
+ switch (target.type) {
383
+ case "auto":
384
+ case "uncommitted": {
385
+ const untrackedPromise = getUntrackedFiles(pi);
386
+ const headSha = await resolveHeadSha(pi);
387
+ const hasHead = headSha !== null;
388
+ const [trackedFiles, untrackedFiles] = await Promise.all([
389
+ getTrackedChangedFiles(pi, hasHead),
390
+ untrackedPromise,
391
+ ]);
392
+ if (trackedFiles.length > 0 || untrackedFiles.length > 0) {
393
+ return {
394
+ scope: {
395
+ kind: "working-tree",
396
+ trackedFiles,
397
+ untrackedFiles,
398
+ hasHead,
399
+ description: `working tree (tracked: ${trackedFiles.length}, untracked: ${untrackedFiles.length})`,
400
+ },
401
+ };
402
+ }
403
+
404
+ if (target.type === "uncommitted") {
405
+ return { error: "No uncommitted changes to review." };
406
+ }
407
+
408
+ const baseBranch = await getDefaultBranch(pi);
409
+ return resolveBranchDiffScope(pi, {
410
+ baseBranch,
411
+ description: (diffFileCount) => `branch diff vs ${baseBranch} (${diffFileCount} files)`,
412
+ mergeBaseError: `Could not determine merge-base against ${baseBranch}`,
413
+ emptyDiffError: `No reviewable changes found (clean working tree and no branch diff vs ${baseBranch}). Use an explicit mode (branch/commit/pr/folder/custom).`,
414
+ });
415
+ }
416
+ case "branch": {
417
+ return resolveBranchDiffScope(pi, {
418
+ baseBranch: target.branch,
419
+ description: (diffFileCount) => `branch diff vs ${target.branch} (${diffFileCount} files)`,
420
+ mergeBaseError: `Could not determine merge-base against ${target.branch}`,
421
+ emptyDiffError: `No differences found against branch ${target.branch}.`,
422
+ });
423
+ }
424
+ case "commit": {
425
+ return {
426
+ scope: {
427
+ kind: "commit",
428
+ sha: target.sha,
429
+ description: `commit ${target.sha}`,
430
+ },
431
+ };
432
+ }
433
+ case "pr": {
434
+ const blockedError = await getPrCheckoutBlockedError(pi);
435
+ if (blockedError) {
436
+ return { error: blockedError };
437
+ }
438
+
439
+ const prNumber = parsePrReference(target.ref);
440
+ if (!prNumber) {
441
+ return { error: `Invalid PR reference: ${target.ref}` };
442
+ }
443
+
444
+ reportStatus?.(`Fetching PR #${prNumber} information...`, "info");
445
+ const prInfo = await getPrInfo(pi, prNumber);
446
+ if (!prInfo) {
447
+ return {
448
+ error: `Could not load PR #${prNumber}. Ensure gh is authenticated and PR exists.`,
449
+ };
450
+ }
451
+
452
+ const preparedPrScope = await preparePrCheckoutScope(pi, reportStatus, {
453
+ prNumber,
454
+ baseBranch: prInfo.baseBranch,
455
+ headBranch: prInfo.headBranch,
456
+ });
457
+ if (!preparedPrScope.ok) {
458
+ return { error: preparedPrScope.error };
459
+ }
460
+
461
+ return { scope: preparedPrScope.scope };
462
+ }
463
+ case "folder": {
464
+ if (target.paths.length === 0) return { error: "No folder/file paths provided." };
465
+ return {
466
+ scope: {
467
+ kind: "folder",
468
+ paths: target.paths,
469
+ description: `snapshot review for ${target.paths.join(", ")}`,
470
+ },
471
+ };
472
+ }
473
+ case "custom": {
474
+ if (!target.instructions.trim()) return { error: "Custom instructions are empty." };
475
+ return {
476
+ scope: {
477
+ kind: "custom",
478
+ instructions: target.instructions.trim(),
479
+ description: "custom review instructions",
480
+ },
481
+ };
482
+ }
483
+ }
484
+ }
485
+
486
+ export function buildScopeInstructions(scope: ResolvedScope): string {
487
+ switch (scope.kind) {
488
+ case "working-tree": {
489
+ const trackedCommand = scope.hasHead
490
+ ? "`git diff HEAD`"
491
+ : "`git diff --cached` and `git diff`";
492
+ const tracked =
493
+ scope.trackedFiles.length > 0
494
+ ? `- First capture the full tracked diff with: ${trackedCommand} (treat this diff as mandatory review context).\n- Tracked files (${scope.trackedFiles.length}):\n${scope.trackedFiles.map((f) => ` - ${f}`).join("\n")}`
495
+ : "- There are no tracked-file diffs.";
496
+ const untracked =
497
+ scope.untrackedFiles.length > 0
498
+ ? `- Also review untracked files as snapshots by reading them directly.\n- Untracked files (${scope.untrackedFiles.length}):\n${scope.untrackedFiles.map((f) => ` - ${f}`).join("\n")}`
499
+ : "- There are no untracked files.";
500
+ return `Scope: working tree review.\n${tracked}\n${untracked}`;
501
+ }
502
+ case "branch-diff": {
503
+ return `Scope: branch diff review against base branch ${scope.baseBranch}.\n- Merge base: ${scope.mergeBase}\n- First capture the full diff with: \`git diff ${scope.mergeBase}..HEAD\` (treat this diff as mandatory review context).\n- Files in diff (${scope.diffFiles.length}):\n${scope.diffFiles.map((f) => ` - ${f}`).join("\n")}`;
504
+ }
505
+ case "commit": {
506
+ return `Scope: commit review for ${scope.sha}.\n- First capture the full commit patch with: \`git show --stat --patch ${scope.sha}\` (treat this patch as mandatory review context).\n- Focus only on changes introduced by this commit.`;
507
+ }
508
+ case "folder": {
509
+ return `Scope: snapshot review of selected paths (not a diff).\n- Paths:\n${scope.paths
510
+ .map((p) => ` - ${p}`)
511
+ .join("\n")}\n- Read files directly from these paths and review what exists currently.`;
512
+ }
513
+ case "custom": {
514
+ return `Scope: custom review instructions.\n- Additional user instruction: ${scope.instructions}`;
515
+ }
516
+ }
517
+ }