@rubytech/taskmaster 1.0.10 → 1.0.11
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.
- package/dist/agents/pi-embedded-runner/compact.js +6 -0
- package/dist/agents/pi-embedded-runner/run/attempt.js +6 -0
- package/dist/agents/pi-embedded-runner/system-prompt.js +1 -0
- package/dist/agents/pi-tools.js +1 -0
- package/dist/agents/skills/workspace.js +40 -3
- package/dist/agents/system-prompt.js +19 -1
- package/dist/agents/taskmaster-tools.js +6 -0
- package/dist/agents/tool-display.json +5 -0
- package/dist/agents/tools/skill-read-tool.js +75 -0
- package/dist/agents/workspace.js +2 -2
- package/dist/auto-reply/reply/get-reply-run.js +1 -2
- package/dist/build-info.json +3 -3
- package/dist/cli/gateway-cli/dev.js +1 -1
- package/dist/cli/provision-seed.js +1 -0
- package/dist/control-ui/assets/{index-Dcwiyz6o.js → index-DKEqjln4.js} +36 -50
- package/dist/control-ui/assets/index-DKEqjln4.js.map +1 -0
- package/dist/control-ui/assets/{index-DE-53MKi.css → index-QrR3OWrg.css} +1 -1
- package/dist/control-ui/index.html +2 -2
- package/package.json +1 -1
- package/taskmaster-docs/USER-GUIDE.md +40 -11
- package/templates/customer/agents/admin/TOOLS.md +36 -0
- package/dist/control-ui/assets/index-Dcwiyz6o.js.map +0 -1
|
@@ -138,6 +138,10 @@ export async function compactEmbeddedPiSessionDirect(params) {
|
|
|
138
138
|
sessionId: params.sessionId,
|
|
139
139
|
warn: makeBootstrapWarn({ sessionLabel, warn: (message) => log.warn(message) }),
|
|
140
140
|
});
|
|
141
|
+
// Extract skill base dirs for scoped skill_read tool
|
|
142
|
+
const skillBaseDirs = (params.skillsSnapshot?.resolvedSkills ?? [])
|
|
143
|
+
.map((s) => s.baseDir)
|
|
144
|
+
.filter(Boolean);
|
|
141
145
|
const runAbortController = new AbortController();
|
|
142
146
|
const toolsRaw = createTaskmasterCodingTools({
|
|
143
147
|
exec: {
|
|
@@ -159,6 +163,7 @@ export async function compactEmbeddedPiSessionDirect(params) {
|
|
|
159
163
|
modelProvider: model.provider,
|
|
160
164
|
modelId,
|
|
161
165
|
modelAuthMode: resolveModelAuthMode(model.provider, params.config),
|
|
166
|
+
skillBaseDirs,
|
|
162
167
|
});
|
|
163
168
|
const tools = sanitizeToolsForGoogle({ tools: toolsRaw, provider });
|
|
164
169
|
logToolSchemasForGoogle({ tools, provider });
|
|
@@ -254,6 +259,7 @@ export async function compactEmbeddedPiSessionDirect(params) {
|
|
|
254
259
|
extraSystemPrompt: params.extraSystemPrompt,
|
|
255
260
|
ownerNumbers: params.ownerNumbers,
|
|
256
261
|
reasoningTagHint,
|
|
262
|
+
finalTagHint: !reasoningTagHint,
|
|
257
263
|
heartbeatPrompt: isDefaultAgent
|
|
258
264
|
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
|
259
265
|
: undefined,
|
|
@@ -143,6 +143,10 @@ export async function runEmbeddedAttempt(params) {
|
|
|
143
143
|
? ["Reminder: commit your changes in this workspace after edits."]
|
|
144
144
|
: undefined;
|
|
145
145
|
const agentDir = params.agentDir ?? resolveTaskmasterAgentDir();
|
|
146
|
+
// Extract skill base dirs for scoped skill_read tool
|
|
147
|
+
const skillBaseDirs = (params.skillsSnapshot?.resolvedSkills ?? [])
|
|
148
|
+
.map((s) => s.baseDir)
|
|
149
|
+
.filter(Boolean);
|
|
146
150
|
// Check if the model supports native image input
|
|
147
151
|
const modelHasVision = params.model.input?.includes("image") ?? false;
|
|
148
152
|
const toolsRaw = params.disableTools
|
|
@@ -175,6 +179,7 @@ export async function runEmbeddedAttempt(params) {
|
|
|
175
179
|
hasRepliedRef: params.hasRepliedRef,
|
|
176
180
|
modelHasVision,
|
|
177
181
|
skipMessageDelivery: params.skipMessageDelivery,
|
|
182
|
+
skillBaseDirs,
|
|
178
183
|
});
|
|
179
184
|
const tools = sanitizeToolsForGoogle({ tools: toolsRaw, provider: params.provider });
|
|
180
185
|
logToolSchemasForGoogle({ tools, provider: params.provider });
|
|
@@ -279,6 +284,7 @@ export async function runEmbeddedAttempt(params) {
|
|
|
279
284
|
extraSystemPrompt: params.extraSystemPrompt,
|
|
280
285
|
ownerNumbers: params.ownerNumbers,
|
|
281
286
|
reasoningTagHint,
|
|
287
|
+
finalTagHint: !reasoningTagHint,
|
|
282
288
|
heartbeatPrompt: isDefaultAgent
|
|
283
289
|
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
|
284
290
|
: undefined,
|
|
@@ -8,6 +8,7 @@ export function buildEmbeddedSystemPrompt(params) {
|
|
|
8
8
|
extraSystemPrompt: params.extraSystemPrompt,
|
|
9
9
|
ownerNumbers: params.ownerNumbers,
|
|
10
10
|
reasoningTagHint: params.reasoningTagHint,
|
|
11
|
+
finalTagHint: params.finalTagHint,
|
|
11
12
|
heartbeatPrompt: params.heartbeatPrompt,
|
|
12
13
|
skillsPrompt: params.skillsPrompt,
|
|
13
14
|
docsPath: params.docsPath,
|
package/dist/agents/pi-tools.js
CHANGED
|
@@ -218,6 +218,7 @@ export function createTaskmasterCodingTools(options) {
|
|
|
218
218
|
modelHasVision: options?.modelHasVision,
|
|
219
219
|
requesterAgentIdOverride: agentId,
|
|
220
220
|
messageSkipDelivery: options?.skipMessageDelivery,
|
|
221
|
+
skillBaseDirs: options?.skillBaseDirs,
|
|
221
222
|
}),
|
|
222
223
|
];
|
|
223
224
|
const coreToolNames = new Set(tools
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { loadSkillsFromDir } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
|
5
5
|
import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
|
|
6
6
|
import { resolveBundledSkillsDir } from "./bundled-dir.js";
|
|
@@ -10,6 +10,41 @@ import { resolvePluginSkillDirs } from "./plugin-skills.js";
|
|
|
10
10
|
import { serializeByKey } from "./serialize.js";
|
|
11
11
|
const fsp = fs.promises;
|
|
12
12
|
const skillsLogger = createSubsystemLogger("skills");
|
|
13
|
+
function escapeXml(str) {
|
|
14
|
+
return str
|
|
15
|
+
.replace(/&/g, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, """)
|
|
19
|
+
.replace(/'/g, "'");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Format skills for inclusion in a system prompt.
|
|
23
|
+
* Identical to the library's formatSkillsForPrompt except it references
|
|
24
|
+
* `skill_read` instead of `read` so agents denied group:fs can still
|
|
25
|
+
* load skill content.
|
|
26
|
+
*/
|
|
27
|
+
function formatSkillsForPromptTaskmaster(skills) {
|
|
28
|
+
const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
|
|
29
|
+
if (visibleSkills.length === 0)
|
|
30
|
+
return "";
|
|
31
|
+
const lines = [
|
|
32
|
+
"\n\nThe following skills provide specialized instructions for specific tasks.",
|
|
33
|
+
"Use the skill_read tool to load a skill's file when the task matches its description.",
|
|
34
|
+
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
|
|
35
|
+
"",
|
|
36
|
+
"<available_skills>",
|
|
37
|
+
];
|
|
38
|
+
for (const skill of visibleSkills) {
|
|
39
|
+
lines.push(" <skill>");
|
|
40
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
41
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
42
|
+
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
|
|
43
|
+
lines.push(" </skill>");
|
|
44
|
+
}
|
|
45
|
+
lines.push("</available_skills>");
|
|
46
|
+
return lines.join("\n");
|
|
47
|
+
}
|
|
13
48
|
const skillCommandDebugOnce = new Set();
|
|
14
49
|
function debugSkillCommandOnce(messageKey, message, meta) {
|
|
15
50
|
if (skillCommandDebugOnce.has(messageKey))
|
|
@@ -141,7 +176,9 @@ export function buildWorkspaceSkillSnapshot(workspaceDir, opts) {
|
|
|
141
176
|
const promptEntries = eligible.filter((entry) => entry.invocation?.disableModelInvocation !== true);
|
|
142
177
|
const resolvedSkills = promptEntries.map((entry) => entry.skill);
|
|
143
178
|
const remoteNote = opts?.eligibility?.remote?.note?.trim();
|
|
144
|
-
const prompt = [remoteNote,
|
|
179
|
+
const prompt = [remoteNote, formatSkillsForPromptTaskmaster(resolvedSkills)]
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.join("\n");
|
|
145
182
|
return {
|
|
146
183
|
prompt,
|
|
147
184
|
skills: eligible.map((entry) => ({
|
|
@@ -157,7 +194,7 @@ export function buildWorkspaceSkillsPrompt(workspaceDir, opts) {
|
|
|
157
194
|
const eligible = filterSkillEntries(skillEntries, opts?.config, opts?.skillFilter, opts?.eligibility);
|
|
158
195
|
const promptEntries = eligible.filter((entry) => entry.invocation?.disableModelInvocation !== true);
|
|
159
196
|
const remoteNote = opts?.eligibility?.remote?.note?.trim();
|
|
160
|
-
return [remoteNote,
|
|
197
|
+
return [remoteNote, formatSkillsForPromptTaskmaster(promptEntries.map((entry) => entry.skill))]
|
|
161
198
|
.filter(Boolean)
|
|
162
199
|
.join("\n");
|
|
163
200
|
}
|
|
@@ -132,9 +132,11 @@ export function buildAgentSystemPrompt(params) {
|
|
|
132
132
|
sessions_spawn: "Spawn a sub-agent session",
|
|
133
133
|
session_status: "Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
|
|
134
134
|
image: "Analyze an image with the configured image model",
|
|
135
|
+
skill_read: "Read files within skill directories (SKILL.md, references)",
|
|
135
136
|
};
|
|
136
137
|
const toolOrder = [
|
|
137
138
|
"read",
|
|
139
|
+
"skill_read",
|
|
138
140
|
"write",
|
|
139
141
|
"edit",
|
|
140
142
|
"apply_patch",
|
|
@@ -192,6 +194,10 @@ export function buildAgentSystemPrompt(params) {
|
|
|
192
194
|
}
|
|
193
195
|
const hasGateway = availableTools.has("gateway");
|
|
194
196
|
const readToolName = resolveToolName("read");
|
|
197
|
+
// For skills section: prefer skill_read when agent has it but not read
|
|
198
|
+
const skillReadToolName = availableTools.has("skill_read") && !availableTools.has("read")
|
|
199
|
+
? resolveToolName("skill_read")
|
|
200
|
+
: readToolName;
|
|
195
201
|
const execToolName = resolveToolName("exec");
|
|
196
202
|
const processToolName = resolveToolName("process");
|
|
197
203
|
const extraSystemPrompt = params.extraSystemPrompt?.trim();
|
|
@@ -211,6 +217,15 @@ export function buildAgentSystemPrompt(params) {
|
|
|
211
217
|
"<final>Hey there! What would you like to do next?</final>",
|
|
212
218
|
].join(" ")
|
|
213
219
|
: undefined;
|
|
220
|
+
// For providers with native thinking (e.g. Anthropic), we only need the <final> instruction
|
|
221
|
+
// to prevent internal reasoning/planning text from leaking to the user.
|
|
222
|
+
const finalOnlyHint = !reasoningHint && params.finalTagHint
|
|
223
|
+
? [
|
|
224
|
+
"Wrap every user-facing reply in <final>...</final> tags.",
|
|
225
|
+
"Only text inside <final> is shown to the user; everything outside is discarded.",
|
|
226
|
+
"Do not narrate your plan or next steps outside <final> — the user cannot see it.",
|
|
227
|
+
].join(" ")
|
|
228
|
+
: undefined;
|
|
214
229
|
const reasoningLevel = params.reasoningLevel ?? "off";
|
|
215
230
|
const userTimezone = params.userTimezone?.trim();
|
|
216
231
|
const skillsPrompt = params.skillsPrompt?.trim();
|
|
@@ -231,7 +246,7 @@ export function buildAgentSystemPrompt(params) {
|
|
|
231
246
|
const skillsSection = buildSkillsSection({
|
|
232
247
|
skillsPrompt,
|
|
233
248
|
isMinimal,
|
|
234
|
-
readToolName,
|
|
249
|
+
readToolName: skillReadToolName,
|
|
235
250
|
});
|
|
236
251
|
const memorySection = buildMemorySection({ isMinimal, availableTools });
|
|
237
252
|
const docsSection = buildDocsSection({
|
|
@@ -417,6 +432,9 @@ export function buildAgentSystemPrompt(params) {
|
|
|
417
432
|
if (reasoningHint) {
|
|
418
433
|
lines.push("## Reasoning Format", reasoningHint, "");
|
|
419
434
|
}
|
|
435
|
+
else if (finalOnlyHint) {
|
|
436
|
+
lines.push("## Output Format", finalOnlyHint, "");
|
|
437
|
+
}
|
|
420
438
|
const contextFiles = params.contextFiles ?? [];
|
|
421
439
|
if (contextFiles.length > 0) {
|
|
422
440
|
const hasSoulFile = contextFiles.some((file) => {
|
|
@@ -23,6 +23,7 @@ import { createLicenseGenerateTool } from "./tools/license-tool.js";
|
|
|
23
23
|
import { createCustomerLookupTool } from "./tools/customer-lookup-tool.js";
|
|
24
24
|
import { createCustomerUpdateTool } from "./tools/customer-update-tool.js";
|
|
25
25
|
import { createRelayMessageTool } from "./tools/relay-message-tool.js";
|
|
26
|
+
import { createSkillReadTool } from "./tools/skill-read-tool.js";
|
|
26
27
|
export function createTaskmasterTools(options) {
|
|
27
28
|
const imageTool = options?.agentDir?.trim()
|
|
28
29
|
? createImageTool({
|
|
@@ -127,6 +128,11 @@ export function createTaskmasterTools(options) {
|
|
|
127
128
|
createCustomerUpdateTool(),
|
|
128
129
|
createRelayMessageTool(),
|
|
129
130
|
];
|
|
131
|
+
// Add scoped skill_read tool when skill directories are provided
|
|
132
|
+
const skillDirs = (options?.skillBaseDirs ?? []).filter(Boolean);
|
|
133
|
+
if (skillDirs.length > 0) {
|
|
134
|
+
tools.push(createSkillReadTool({ allowedSkillDirs: skillDirs }));
|
|
135
|
+
}
|
|
130
136
|
// Add memory tools (conditionally based on config)
|
|
131
137
|
const memoryToolOptions = {
|
|
132
138
|
config: options?.config,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { readStringParam } from "./common.js";
|
|
6
|
+
const SkillReadSchema = Type.Object({
|
|
7
|
+
path: Type.String({ description: "Absolute or relative path to the file to read" }),
|
|
8
|
+
skill_dir: Type.Optional(Type.String({
|
|
9
|
+
description: "Skill directory for resolving relative paths. If omitted, path must be absolute.",
|
|
10
|
+
})),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Create a scoped file-read tool that only allows reading files within
|
|
14
|
+
* the provided skill directories. This gives agents access to skill
|
|
15
|
+
* content (SKILL.md, references/) without exposing the full filesystem.
|
|
16
|
+
*/
|
|
17
|
+
export function createSkillReadTool(opts) {
|
|
18
|
+
// Pre-resolve allowed dirs at creation time (async realpath done at call time).
|
|
19
|
+
const allowedDirs = opts.allowedSkillDirs.filter(Boolean);
|
|
20
|
+
return {
|
|
21
|
+
label: "Skill Read",
|
|
22
|
+
name: "skill_read",
|
|
23
|
+
description: "Read a file within a skill directory (SKILL.md, references, etc.). " +
|
|
24
|
+
"Use this to load skill content on demand. " +
|
|
25
|
+
"Only files inside skill directories are accessible.",
|
|
26
|
+
parameters: SkillReadSchema,
|
|
27
|
+
execute: async (_toolCallId, args) => {
|
|
28
|
+
const params = args;
|
|
29
|
+
const rawPath = readStringParam(params, "path", { required: true });
|
|
30
|
+
const skillDir = readStringParam(params, "skill_dir");
|
|
31
|
+
// Resolve the target path
|
|
32
|
+
let targetPath;
|
|
33
|
+
if (path.isAbsolute(rawPath)) {
|
|
34
|
+
targetPath = path.resolve(rawPath);
|
|
35
|
+
}
|
|
36
|
+
else if (skillDir) {
|
|
37
|
+
targetPath = path.resolve(skillDir, rawPath);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
throw new Error("Relative path requires skill_dir. Provide an absolute path or include skill_dir.");
|
|
41
|
+
}
|
|
42
|
+
// Security: resolve symlinks on the target and check containment
|
|
43
|
+
let realTarget;
|
|
44
|
+
try {
|
|
45
|
+
realTarget = await realpath(targetPath);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error(`File not found: ${targetPath}`);
|
|
49
|
+
}
|
|
50
|
+
let allowed = false;
|
|
51
|
+
for (const dir of allowedDirs) {
|
|
52
|
+
let realDir;
|
|
53
|
+
try {
|
|
54
|
+
realDir = await realpath(dir);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const prefix = realDir.endsWith("/") ? realDir : `${realDir}/`;
|
|
60
|
+
if (realTarget === realDir || realTarget.startsWith(prefix)) {
|
|
61
|
+
allowed = true;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!allowed) {
|
|
66
|
+
throw new Error(`Access denied: ${rawPath} is outside all skill directories.`);
|
|
67
|
+
}
|
|
68
|
+
const content = await fs.readFile(realTarget, "utf-8");
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: content }],
|
|
71
|
+
details: { path: realTarget },
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
package/dist/agents/workspace.js
CHANGED
|
@@ -20,7 +20,7 @@ export const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
|
|
|
20
20
|
export const DEFAULT_USER_FILENAME = "USER.md";
|
|
21
21
|
export const DEFAULT_HEARTBEAT_FILENAME = "HEARTBEAT.md";
|
|
22
22
|
export const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
|
|
23
|
-
const TEMPLATE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../
|
|
23
|
+
const TEMPLATE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../templates/customer/agents/admin");
|
|
24
24
|
function stripFrontMatter(content) {
|
|
25
25
|
if (!content.startsWith("---"))
|
|
26
26
|
return content;
|
|
@@ -39,7 +39,7 @@ async function loadTemplate(name) {
|
|
|
39
39
|
return stripFrontMatter(content);
|
|
40
40
|
}
|
|
41
41
|
catch {
|
|
42
|
-
throw new Error(`Missing workspace template: ${name} (${templatePath}). Ensure
|
|
42
|
+
throw new Error(`Missing workspace template: ${name} (${templatePath}). Ensure templates/customer is packaged.`);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
async function writeFileIfMissing(filePath, content) {
|
|
@@ -9,7 +9,6 @@ import { resolveGroupSessionKey, resolveSessionFilePath, } from "../../config/se
|
|
|
9
9
|
import { logVerbose } from "../../globals.js";
|
|
10
10
|
import { clearCommandLane, getQueueSize } from "../../process/command-queue.js";
|
|
11
11
|
import { normalizeMainKey } from "../../routing/session-key.js";
|
|
12
|
-
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
|
|
13
12
|
import { hasControlCommand } from "../command-detection.js";
|
|
14
13
|
import { buildInboundMediaNote } from "../media-note.js";
|
|
15
14
|
import { formatXHighModelHint, normalizeThinkLevel, supportsXHighThinking, } from "../thinking.js";
|
|
@@ -257,7 +256,7 @@ export async function runPreparedReply(params) {
|
|
|
257
256
|
blockReplyBreak: resolvedBlockStreamingBreak,
|
|
258
257
|
ownerNumbers: command.ownerList.length > 0 ? command.ownerList : undefined,
|
|
259
258
|
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
260
|
-
|
|
259
|
+
enforceFinalTag: true,
|
|
261
260
|
skipMessageDelivery: opts?.skipMessageDelivery,
|
|
262
261
|
},
|
|
263
262
|
};
|
package/dist/build-info.json
CHANGED
|
@@ -10,7 +10,7 @@ const DEV_IDENTITY_NAME = "C3-PO";
|
|
|
10
10
|
const DEV_IDENTITY_THEME = "protocol droid";
|
|
11
11
|
const DEV_IDENTITY_EMOJI = "🤖";
|
|
12
12
|
const DEV_AGENT_WORKSPACE_SUFFIX = "dev";
|
|
13
|
-
const DEV_TEMPLATE_DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../../
|
|
13
|
+
const DEV_TEMPLATE_DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../../templates/customer/agents/admin");
|
|
14
14
|
async function loadDevTemplate(name, fallback) {
|
|
15
15
|
try {
|
|
16
16
|
const raw = await fs.promises.readFile(path.join(DEV_TEMPLATE_DIR, name), "utf-8");
|