@poncho-ai/harness 0.25.0 → 0.26.0

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.
@@ -0,0 +1,363 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
4
+ import type { StateConfig } from "./state.js";
5
+ import {
6
+ ensureAgentIdentity,
7
+ getAgentStoreDirectory,
8
+ slugifyStorageComponent,
9
+ STORAGE_SCHEMA_VERSION,
10
+ } from "./agent-identity.js";
11
+ import { createRawKVStore, type RawKVStore } from "./kv-store.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Data model
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export type TodoStatus = "pending" | "in_progress" | "completed";
18
+ export type TodoPriority = "high" | "medium" | "low";
19
+
20
+ export interface TodoItem {
21
+ id: string;
22
+ content: string;
23
+ status: TodoStatus;
24
+ priority: TodoPriority;
25
+ createdAt: number;
26
+ updatedAt: number;
27
+ }
28
+
29
+ export interface TodoStore {
30
+ get(conversationId: string): Promise<TodoItem[]>;
31
+ set(conversationId: string, todos: TodoItem[]): Promise<void>;
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Helpers
36
+ // ---------------------------------------------------------------------------
37
+
38
+ const VALID_STATUSES: TodoStatus[] = ["pending", "in_progress", "completed"];
39
+ const VALID_PRIORITIES: TodoPriority[] = ["high", "medium", "low"];
40
+ const TODOS_DIRECTORY = "todos";
41
+
42
+ const writeJsonAtomic = async (filePath: string, payload: unknown): Promise<void> => {
43
+ await mkdir(dirname(filePath), { recursive: true });
44
+ const tmpPath = `${filePath}.tmp`;
45
+ await writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf8");
46
+ await rename(tmpPath, filePath);
47
+ };
48
+
49
+ const parseTodoList = (raw: unknown): TodoItem[] => {
50
+ if (!Array.isArray(raw)) return [];
51
+ return raw.filter(
52
+ (item): item is TodoItem =>
53
+ typeof item === "object" &&
54
+ item !== null &&
55
+ typeof (item as Record<string, unknown>).id === "string" &&
56
+ typeof (item as Record<string, unknown>).content === "string",
57
+ );
58
+ };
59
+
60
+ const generateId = (): string =>
61
+ (globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`).slice(0, 8);
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // InMemoryTodoStore
65
+ // ---------------------------------------------------------------------------
66
+
67
+ class InMemoryTodoStore implements TodoStore {
68
+ private readonly store = new Map<string, TodoItem[]>();
69
+
70
+ async get(conversationId: string): Promise<TodoItem[]> {
71
+ return this.store.get(conversationId) ?? [];
72
+ }
73
+
74
+ async set(conversationId: string, todos: TodoItem[]): Promise<void> {
75
+ this.store.set(conversationId, todos);
76
+ }
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // FileTodoStore — one JSON file per conversation
81
+ // ---------------------------------------------------------------------------
82
+
83
+ class FileTodoStore implements TodoStore {
84
+ private readonly workingDir: string;
85
+ private todosDir = "";
86
+
87
+ constructor(workingDir: string) {
88
+ this.workingDir = workingDir;
89
+ }
90
+
91
+ private async ensureTodosDir(): Promise<string> {
92
+ if (this.todosDir) return this.todosDir;
93
+ const identity = await ensureAgentIdentity(this.workingDir);
94
+ this.todosDir = resolve(getAgentStoreDirectory(identity), TODOS_DIRECTORY);
95
+ await mkdir(this.todosDir, { recursive: true });
96
+ return this.todosDir;
97
+ }
98
+
99
+ private async filePath(conversationId: string): Promise<string> {
100
+ const dir = await this.ensureTodosDir();
101
+ return resolve(dir, `${slugifyStorageComponent(conversationId)}.json`);
102
+ }
103
+
104
+ async get(conversationId: string): Promise<TodoItem[]> {
105
+ try {
106
+ const fp = await this.filePath(conversationId);
107
+ const raw = await readFile(fp, "utf8");
108
+ return parseTodoList(JSON.parse(raw));
109
+ } catch {
110
+ return [];
111
+ }
112
+ }
113
+
114
+ async set(conversationId: string, todos: TodoItem[]): Promise<void> {
115
+ const fp = await this.filePath(conversationId);
116
+ await writeJsonAtomic(fp, todos);
117
+ }
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // KVBackedTodoStore — wraps any RawKVStore (Upstash, Redis, DynamoDB)
122
+ // ---------------------------------------------------------------------------
123
+
124
+ class KVBackedTodoStore implements TodoStore {
125
+ private readonly kv: RawKVStore;
126
+ private readonly baseKey: string;
127
+ private readonly ttl?: number;
128
+ private readonly memoryFallback = new InMemoryTodoStore();
129
+
130
+ constructor(kv: RawKVStore, baseKey: string, ttl?: number) {
131
+ this.kv = kv;
132
+ this.baseKey = baseKey;
133
+ this.ttl = ttl;
134
+ }
135
+
136
+ private keyFor(conversationId: string): string {
137
+ return `${this.baseKey}:${slugifyStorageComponent(conversationId)}`;
138
+ }
139
+
140
+ async get(conversationId: string): Promise<TodoItem[]> {
141
+ try {
142
+ const raw = await this.kv.get(this.keyFor(conversationId));
143
+ if (!raw) return [];
144
+ return parseTodoList(JSON.parse(raw));
145
+ } catch {
146
+ return this.memoryFallback.get(conversationId);
147
+ }
148
+ }
149
+
150
+ async set(conversationId: string, todos: TodoItem[]): Promise<void> {
151
+ try {
152
+ const serialized = JSON.stringify(todos);
153
+ const key = this.keyFor(conversationId);
154
+ if (typeof this.ttl === "number") {
155
+ await this.kv.setWithTtl(key, serialized, Math.max(1, this.ttl));
156
+ } else {
157
+ await this.kv.set(key, serialized);
158
+ }
159
+ } catch {
160
+ await this.memoryFallback.set(conversationId, todos);
161
+ }
162
+ }
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Factory
167
+ // ---------------------------------------------------------------------------
168
+
169
+ export const createTodoStore = (
170
+ agentId: string,
171
+ config?: StateConfig,
172
+ options?: { workingDir?: string },
173
+ ): TodoStore => {
174
+ const provider = config?.provider ?? "local";
175
+ const ttl = config?.ttl;
176
+ const workingDir = options?.workingDir ?? process.cwd();
177
+
178
+ if (provider === "local") {
179
+ return new FileTodoStore(workingDir);
180
+ }
181
+ if (provider === "memory") {
182
+ return new InMemoryTodoStore();
183
+ }
184
+
185
+ const kv = createRawKVStore(config);
186
+ if (kv) {
187
+ const baseKey = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}:todos`;
188
+ return new KVBackedTodoStore(kv, baseKey, ttl);
189
+ }
190
+ return new InMemoryTodoStore();
191
+ };
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // Tool definitions
195
+ // ---------------------------------------------------------------------------
196
+
197
+ export const createTodoTools = (store: TodoStore): ToolDefinition[] => {
198
+ const resolveKey = (context: { conversationId?: string; runId: string }): string =>
199
+ context.conversationId || context.runId;
200
+
201
+ return [
202
+ defineTool({
203
+ name: "todo_list",
204
+ description:
205
+ "List all todo items for the current conversation. " +
206
+ "Use this to check progress and plan next steps.",
207
+ inputSchema: {
208
+ type: "object",
209
+ properties: {
210
+ status: {
211
+ type: "string",
212
+ enum: VALID_STATUSES,
213
+ description: "Filter by status (omit to list all)",
214
+ },
215
+ },
216
+ additionalProperties: false,
217
+ },
218
+ handler: async (input, context) => {
219
+ const key = resolveKey(context);
220
+ let todos = await store.get(key);
221
+ const status = typeof input.status === "string" ? input.status : undefined;
222
+ if (status && VALID_STATUSES.includes(status as TodoStatus)) {
223
+ todos = todos.filter((t) => t.status === status);
224
+ }
225
+ return { todos, count: todos.length };
226
+ },
227
+ }),
228
+
229
+ defineTool({
230
+ name: "todo_add",
231
+ description:
232
+ "Add a new todo item for the current conversation. " +
233
+ "Use proactively for complex multi-step tasks (3+ steps).",
234
+ inputSchema: {
235
+ type: "object",
236
+ properties: {
237
+ content: {
238
+ type: "string",
239
+ description: "Description of the task",
240
+ },
241
+ status: {
242
+ type: "string",
243
+ enum: VALID_STATUSES,
244
+ description: "Initial status (default: pending)",
245
+ },
246
+ priority: {
247
+ type: "string",
248
+ enum: VALID_PRIORITIES,
249
+ description: "Priority level (default: medium)",
250
+ },
251
+ },
252
+ required: ["content"],
253
+ additionalProperties: false,
254
+ },
255
+ handler: async (input, context) => {
256
+ const content = typeof input.content === "string" ? input.content.trim() : "";
257
+ if (!content) throw new Error("content is required");
258
+ const status: TodoStatus =
259
+ typeof input.status === "string" && VALID_STATUSES.includes(input.status as TodoStatus)
260
+ ? (input.status as TodoStatus)
261
+ : "pending";
262
+ const priority: TodoPriority =
263
+ typeof input.priority === "string" && VALID_PRIORITIES.includes(input.priority as TodoPriority)
264
+ ? (input.priority as TodoPriority)
265
+ : "medium";
266
+ const now = Date.now();
267
+ const todo: TodoItem = {
268
+ id: generateId(),
269
+ content,
270
+ status,
271
+ priority,
272
+ createdAt: now,
273
+ updatedAt: now,
274
+ };
275
+ const key = resolveKey(context);
276
+ const todos = await store.get(key);
277
+ todos.push(todo);
278
+ await store.set(key, todos);
279
+ return { todo, todos };
280
+ },
281
+ }),
282
+
283
+ defineTool({
284
+ name: "todo_update",
285
+ description:
286
+ "Update an existing todo item's status, content, or priority. " +
287
+ "Mark tasks in_progress when starting and completed when done.",
288
+ inputSchema: {
289
+ type: "object",
290
+ properties: {
291
+ id: {
292
+ type: "string",
293
+ description: "ID of the todo to update",
294
+ },
295
+ status: {
296
+ type: "string",
297
+ enum: VALID_STATUSES,
298
+ description: "New status",
299
+ },
300
+ content: {
301
+ type: "string",
302
+ description: "New content/description",
303
+ },
304
+ priority: {
305
+ type: "string",
306
+ enum: VALID_PRIORITIES,
307
+ description: "New priority level",
308
+ },
309
+ },
310
+ required: ["id"],
311
+ additionalProperties: false,
312
+ },
313
+ handler: async (input, context) => {
314
+ const id = typeof input.id === "string" ? input.id : "";
315
+ if (!id) throw new Error("id is required");
316
+ const key = resolveKey(context);
317
+ const todos = await store.get(key);
318
+ const todo = todos.find((t) => t.id === id);
319
+ if (!todo) throw new Error(`Todo with id "${id}" not found`);
320
+
321
+ if (typeof input.status === "string" && VALID_STATUSES.includes(input.status as TodoStatus)) {
322
+ todo.status = input.status as TodoStatus;
323
+ }
324
+ if (typeof input.content === "string" && input.content.trim()) {
325
+ todo.content = input.content.trim();
326
+ }
327
+ if (typeof input.priority === "string" && VALID_PRIORITIES.includes(input.priority as TodoPriority)) {
328
+ todo.priority = input.priority as TodoPriority;
329
+ }
330
+ todo.updatedAt = Date.now();
331
+ await store.set(key, todos);
332
+ return { todo, todos };
333
+ },
334
+ }),
335
+
336
+ defineTool({
337
+ name: "todo_remove",
338
+ description: "Remove a todo item by ID.",
339
+ inputSchema: {
340
+ type: "object",
341
+ properties: {
342
+ id: {
343
+ type: "string",
344
+ description: "ID of the todo to remove",
345
+ },
346
+ },
347
+ required: ["id"],
348
+ additionalProperties: false,
349
+ },
350
+ handler: async (input, context) => {
351
+ const id = typeof input.id === "string" ? input.id : "";
352
+ if (!id) throw new Error("id is required");
353
+ const key = resolveKey(context);
354
+ const todos = await store.get(key);
355
+ const index = todos.findIndex((t) => t.id === id);
356
+ if (index === -1) throw new Error(`Todo with id "${id}" not found`);
357
+ const [removed] = todos.splice(index, 1);
358
+ await store.set(key, todos);
359
+ return { removed, todos };
360
+ },
361
+ }),
362
+ ];
363
+ };
@@ -1,6 +0,0 @@
1
-
2
- > @poncho-ai/harness@0.11.2 lint /Users/cesar/Dev/latitude/poncho-ai/packages/harness
3
- > eslint src/
4
-
5
- sh: eslint: command not found
6
-  ELIFECYCLE  Command failed.
@@ -1,135 +0,0 @@
1
-
2
- > @poncho-ai/harness@0.16.1 test /Users/cesar/Dev/latitude/poncho-ai/packages/harness
3
- > vitest
4
-
5
-
6
-  RUN  v1.6.1 /Users/cesar/Dev/latitude/poncho-ai/packages/harness
7
-
8
- ✓ test/telemetry.test.ts  (3 tests) 2ms
9
- [event] step:completed {"type":"step:completed","step":1,"duration":1}
10
- [event] step:started {"type":"step:started","step":2}
11
- ✓ test/schema-converter.test.ts  (27 tests) 19ms
12
- stdout | test/mcp.test.ts > mcp bridge protocol transports > discovers and calls tools over streamable HTTP
13
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
14
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
15
-
16
- stdout | test/mcp.test.ts > mcp bridge protocol transports > selects discovered tools by requested patterns
17
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":2}
18
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
19
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":2,"filteredByPolicyCount":0,"filteredByIntentCount":0}
20
-
21
- ✓ test/agent-parser.test.ts  (10 tests) 24ms
22
- stdout | test/mcp.test.ts > mcp bridge protocol transports > skips discovery when bearer token env value is missing
23
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":0,"filteredByPolicyCount":0,"filteredByIntentCount":0}
24
-
25
- stderr | test/mcp.test.ts > mcp bridge protocol transports > skips discovery when bearer token env value is missing
26
- [poncho][mcp] {"event":"auth.token_missing","server":"remote","tokenEnv":"MISSING_TOKEN_ENV"}
27
-
28
- stdout | test/mcp.test.ts > mcp bridge protocol transports > returns actionable errors for 403 permission failures
29
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
30
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
31
-
32
- ✓ test/mcp.test.ts  (6 tests) 81ms
33
- ✓ test/memory.test.ts  (4 tests) 56ms
34
- ✓ test/state.test.ts  (5 tests) 237ms
35
- ✓ test/model-factory.test.ts  (4 tests) 2ms
36
- ✓ test/agent-identity.test.ts  (2 tests) 43ms
37
- stdout | test/harness.test.ts > agent harness > registers default filesystem tools
38
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
39
-
40
- stdout | test/harness.test.ts > agent harness > disables write_file by default in production environment
41
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
42
-
43
- stdout | test/harness.test.ts > agent harness > allows disabling built-in tools via poncho.config.js
44
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
45
-
46
- stdout | test/harness.test.ts > agent harness > supports per-environment tool overrides
47
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
48
-
49
- stdout | test/harness.test.ts > agent harness > supports per-environment tool overrides
50
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
51
-
52
- stdout | test/harness.test.ts > agent harness > does not auto-register exported tool objects from skill scripts
53
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
54
-
55
- stdout | test/harness.test.ts > agent harness > refreshes skill metadata and tools in development mode
56
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
57
-
58
- stdout | test/harness.test.ts > agent harness > refreshes skill metadata and tools in development mode
59
- [poncho][mcp] {"event":"tools.cleared","reason":"skills:changed","requestedPatterns":[]}
60
- [poncho][mcp] {"event":"tools.cleared","reason":"activate:beta","requestedPatterns":[]}
61
-
62
- stdout | test/harness.test.ts > agent harness > prunes removed active skills after refresh in development mode
63
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
64
- [poncho][mcp] {"event":"tools.cleared","reason":"activate:obsolete","requestedPatterns":[]}
65
-
66
- stdout | test/harness.test.ts > agent harness > prunes removed active skills after refresh in development mode
67
- [poncho][mcp] {"event":"tools.cleared","reason":"skills:changed","requestedPatterns":[]}
68
-
69
- stdout | test/harness.test.ts > agent harness > does not refresh skills outside development mode
70
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
71
-
72
- stdout | test/harness.test.ts > agent harness > clears active skills when skill metadata changes in development mode
73
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
74
- [poncho][mcp] {"event":"tools.cleared","reason":"activate:alpha","requestedPatterns":[]}
75
-
76
- stdout | test/harness.test.ts > agent harness > clears active skills when skill metadata changes in development mode
77
- [poncho][mcp] {"event":"tools.cleared","reason":"skills:changed","requestedPatterns":[]}
78
-
79
- stdout | test/harness.test.ts > agent harness > lists skill scripts through list_skill_scripts
80
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
81
-
82
- stdout | test/harness.test.ts > agent harness > runs JavaScript/TypeScript skill scripts through run_skill_script
83
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
84
-
85
- stdout | test/harness.test.ts > agent harness > runs AGENT-scope scripts from root scripts directory
86
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
87
-
88
- stdout | test/harness.test.ts > agent harness > blocks path traversal in run_skill_script
89
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
90
-
91
- stdout | test/harness.test.ts > agent harness > requires allowed-tools entries for non-standard script directories
92
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
93
-
94
- stdout | test/harness.test.ts > agent harness > registers MCP tools dynamically for stacked active skills and supports deactivation
95
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":2}
96
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
97
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
98
- [poncho][mcp] {"event":"tools.refreshed","reason":"activate:skill-a","requestedPatterns":["remote/a"],"registeredCount":1,"activeSkills":["skill-a"]}
99
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":2,"registeredCount":2,"filteredByPolicyCount":0,"filteredByIntentCount":0}
100
- [poncho][mcp] {"event":"tools.refreshed","reason":"activate:skill-b","requestedPatterns":["remote/a","remote/b"],"registeredCount":2,"activeSkills":["skill-a","skill-b"]}
101
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
102
- [poncho][mcp] {"event":"tools.refreshed","reason":"deactivate:skill-a","requestedPatterns":["remote/b"],"registeredCount":1,"activeSkills":["skill-b"]}
103
-
104
- stdout | test/harness.test.ts > agent harness > supports flat tool access config format
105
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
106
-
107
- stdout | test/harness.test.ts > agent harness > flat tool access takes priority over legacy defaults
108
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
109
-
110
- stdout | test/harness.test.ts > agent harness > byEnvironment overrides flat tool access
111
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
112
-
113
- stdout | test/harness.test.ts > agent harness > registerTools skips tools disabled via config
114
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
115
-
116
- stdout | test/harness.test.ts > agent harness > approval access level registers the tool but marks it for approval
117
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
118
-
119
- stdout | test/harness.test.ts > agent harness > tools without approval config do not require approval
120
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
121
-
122
- stdout | test/harness.test.ts > agent harness > allows in-flight MCP calls to finish after skill deactivation
123
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
124
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
125
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
126
- [poncho][mcp] {"event":"tools.refreshed","reason":"activate:skill-slow","requestedPatterns":["remote/slow"],"registeredCount":1,"activeSkills":["skill-slow"]}
127
- [poncho][mcp] {"event":"tools.cleared","reason":"deactivate:skill-slow","requestedPatterns":[]}
128
-
129
- ✓ test/harness.test.ts  (25 tests) 291ms
130
-
131
-  Test Files  9 passed (9)
132
-  Tests  86 passed (86)
133
-  Start at  17:47:43
134
-  Duration  1.88s (transform 684ms, setup 1ms, collect 2.34s, tests 755ms, environment 2ms, prepare 1.27s)
135
-