pi-crew 0.1.46 โ 0.1.49
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/CHANGELOG.md +97 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/next-upgrade-roadmap.md +117 -42
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
- package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
- package/docs/research/AUDIT_OH_MY_PI.md +261 -0
- package/docs/research/AUDIT_PI_CREW.md +457 -0
- package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
- package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
- package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
- package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
- package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
- package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
- package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
- package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
- package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
- package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
- package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
- package/docs/research-awesome-agent-skills-distillation.md +100 -100
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-oh-my-pi-distillation.md +56 -9
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-phase10-distillation.md +198 -198
- package/docs/research-phase11-distillation.md +201 -201
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +107 -107
- package/index.ts +6 -6
- package/package.json +99 -98
- package/schema.json +8 -0
- package/skills/async-worker-recovery/SKILL.md +42 -42
- package/skills/context-artifact-hygiene/SKILL.md +52 -52
- package/skills/delegation-patterns/SKILL.md +54 -54
- package/skills/mailbox-interactive/SKILL.md +40 -40
- package/skills/model-routing-context/SKILL.md +39 -39
- package/skills/multi-perspective-review/SKILL.md +58 -58
- package/skills/observability-reliability/SKILL.md +41 -41
- package/skills/orchestration/SKILL.md +157 -0
- package/skills/ownership-session-security/SKILL.md +41 -41
- package/skills/pi-extension-lifecycle/SKILL.md +39 -39
- package/skills/requirements-to-task-packet/SKILL.md +63 -63
- package/skills/resource-discovery-config/SKILL.md +41 -41
- package/skills/runtime-state-reader/SKILL.md +44 -44
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
- package/skills/state-mutation-locking/SKILL.md +42 -42
- package/skills/systematic-debugging/SKILL.md +67 -67
- package/skills/ui-render-performance/SKILL.md +39 -39
- package/skills/verification-before-done/SKILL.md +57 -57
- package/skills/worktree-isolation/SKILL.md +39 -39
- package/src/agents/agent-config.ts +6 -0
- package/src/agents/agent-search.ts +98 -0
- package/src/agents/agent-serializer.ts +4 -0
- package/src/agents/discover-agents.ts +17 -4
- package/src/config/config.ts +24 -0
- package/src/config/defaults.ts +11 -0
- package/src/extension/autonomous-policy.ts +26 -33
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/help.ts +1 -0
- package/src/extension/management.ts +5 -0
- package/src/extension/register.ts +58 -13
- package/src/extension/registration/commands.ts +33 -1
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/team-tool.ts +6 -4
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-index.ts +24 -18
- package/src/extension/run-maintenance.ts +68 -62
- package/src/extension/team-tool/api.ts +23 -2
- package/src/extension/team-tool/cancel.ts +86 -11
- package/src/extension/team-tool/context.ts +3 -0
- package/src/extension/team-tool/handle-settings.ts +188 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/intent-policy.ts +42 -0
- package/src/extension/team-tool/lifecycle-actions.ts +47 -18
- package/src/extension/team-tool/parallel-dispatch.ts +156 -0
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +10 -2
- package/src/extension/team-tool/run.ts +3 -2
- package/src/extension/team-tool/status.ts +1 -1
- package/src/extension/team-tool-types.ts +1 -0
- package/src/extension/team-tool.ts +13 -3
- package/src/hooks/registry.ts +61 -0
- package/src/hooks/types.ts +41 -0
- package/src/i18n.ts +184 -184
- package/src/observability/exporters/otlp-exporter.ts +77 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-control.ts +108 -2
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/async-runner.ts +3 -1
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/background-runner.ts +19 -0
- package/src/runtime/cancellation-token.ts +89 -0
- package/src/runtime/cancellation.ts +61 -51
- package/src/runtime/capability-inventory.ts +116 -0
- package/src/runtime/child-pi.ts +2 -1
- package/src/runtime/code-summary.ts +247 -0
- package/src/runtime/completion-guard.ts +190 -190
- package/src/runtime/crash-recovery.ts +181 -0
- package/src/runtime/crew-agent-records.ts +35 -7
- package/src/runtime/crew-agent-runtime.ts +1 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
- package/src/runtime/delivery-coordinator.ts +3 -1
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +81 -76
- package/src/runtime/event-stream-bridge.ts +90 -0
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +124 -124
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-agent-manager.ts +78 -2
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-extension-bridge.ts +150 -0
- package/src/runtime/live-irc.ts +92 -0
- package/src/runtime/live-session-health.ts +100 -0
- package/src/runtime/live-session-runtime.ts +297 -7
- package/src/runtime/mcp-proxy.ts +113 -0
- package/src/runtime/notebook-helpers.ts +90 -0
- package/src/runtime/orphan-sentinel.ts +7 -0
- package/src/runtime/output-validator.ts +187 -0
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +57 -0
- package/src/runtime/parent-guard.ts +80 -0
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/prose-compressor.ts +164 -0
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/result-extractor.ts +121 -0
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +1 -4
- package/src/runtime/semaphore.ts +131 -0
- package/src/runtime/sensitive-paths.ts +92 -0
- package/src/runtime/session-resources.ts +25 -25
- package/src/runtime/session-snapshot.ts +59 -59
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/stream-preview.ts +177 -0
- package/src/runtime/subagent-manager.ts +3 -2
- package/src/runtime/subprocess-tool-registry.ts +67 -0
- package/src/runtime/supervisor-contact.ts +59 -59
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +59 -9
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +2 -0
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +70 -8
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +104 -0
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +75 -4
- package/src/runtime/team-runner.ts +60 -8
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workspace-tree.ts +298 -0
- package/src/runtime/yield-handler.ts +189 -0
- package/src/schema/config-schema.ts +6 -0
- package/src/schema/team-tool-schema.ts +11 -1
- package/src/skills/discover-skills.ts +67 -0
- package/src/state/active-run-registry.ts +4 -2
- package/src/state/artifact-store.ts +4 -1
- package/src/state/atomic-write.ts +50 -1
- package/src/state/blob-store.ts +117 -0
- package/src/state/contracts.ts +1 -0
- package/src/state/event-log-rotation.ts +158 -0
- package/src/state/event-log.ts +52 -2
- package/src/state/mailbox.ts +87 -7
- package/src/state/state-store.ts +24 -4
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +20 -0
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/agent-management-overlay.ts +144 -0
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +11 -2
- package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
- package/src/ui/dashboard-panes/capability-pane.ts +60 -0
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/live-run-sidebar.ts +4 -0
- package/src/ui/loaders.ts +158 -158
- package/src/ui/powerbar-publisher.ts +77 -15
- package/src/ui/render-coalescer.ts +51 -0
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-dashboard.ts +4 -0
- package/src/ui/run-event-bus.ts +209 -0
- package/src/ui/run-snapshot-cache.ts +68 -16
- package/src/ui/snapshot-types.ts +8 -0
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -58
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-entries.ts +258 -0
- package/src/utils/atomic-write.ts +33 -33
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +17 -12
- package/src/utils/incremental-reader.ts +104 -0
- package/src/utils/names.ts +27 -27
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +137 -0
- package/src/utils/sleep.ts +32 -32
- package/src/utils/sse-parser.ts +134 -0
- package/src/utils/task-name-generator.ts +337 -0
- package/src/utils/visual.ts +33 -2
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +2 -1
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
package/src/state/state-store.ts
CHANGED
|
@@ -27,12 +27,15 @@ interface ManifestCacheEntry {
|
|
|
27
27
|
manifestSize: number;
|
|
28
28
|
tasksMtimeMs: number;
|
|
29
29
|
tasksSize: number;
|
|
30
|
+
cachedAt?: number;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
const MANIFEST_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
32
34
|
const manifestCache = new Map<string, ManifestCacheEntry>();
|
|
33
35
|
|
|
34
36
|
function setManifestCache(stateRoot: string, entry: ManifestCacheEntry): void {
|
|
35
37
|
if (manifestCache.has(stateRoot)) manifestCache.delete(stateRoot);
|
|
38
|
+
entry.cachedAt = Date.now();
|
|
36
39
|
manifestCache.set(stateRoot, entry);
|
|
37
40
|
while (manifestCache.size > DEFAULT_CACHE.manifestMaxEntries) {
|
|
38
41
|
const oldest = manifestCache.keys().next().value;
|
|
@@ -196,6 +199,15 @@ export async function saveRunTasksAsync(manifest: TeamRunManifest, tasks: TeamTa
|
|
|
196
199
|
invalidateRunCache(manifest.stateRoot);
|
|
197
200
|
}
|
|
198
201
|
|
|
202
|
+
/** M8: Atomically save manifest + tasks and invalidate cache once to prevent stale reads between saves */
|
|
203
|
+
export async function saveManifestAndTasksAtomic(manifest: TeamRunManifest, tasks: TeamTaskState[]): Promise<void> {
|
|
204
|
+
await Promise.all([
|
|
205
|
+
atomicWriteJsonAsync(path.join(manifest.stateRoot, "manifest.json"), manifest),
|
|
206
|
+
atomicWriteJsonAsync(manifest.tasksPath, tasks),
|
|
207
|
+
]);
|
|
208
|
+
invalidateRunCache(manifest.stateRoot);
|
|
209
|
+
}
|
|
210
|
+
|
|
199
211
|
export interface UpdateRunStatusOptions {
|
|
200
212
|
data?: Record<string, unknown>;
|
|
201
213
|
metadata?: Parameters<typeof appendEvent>[1]["metadata"];
|
|
@@ -266,11 +278,15 @@ export function loadRunManifestById(cwd: string, runId: string): { manifest: Tea
|
|
|
266
278
|
&& cached.tasksMtimeMs === tasksMtimeMs
|
|
267
279
|
&& cached.tasksSize === (tasksStat?.size ?? 0)
|
|
268
280
|
) {
|
|
269
|
-
|
|
281
|
+
// TTL eviction: expire stale entries even if mtime matches
|
|
282
|
+
if (cached.cachedAt && Date.now() - cached.cachedAt > MANIFEST_CACHE_TTL_MS) {
|
|
283
|
+
manifestCache.delete(stateRoot);
|
|
284
|
+
} else if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
|
|
270
285
|
manifestCache.delete(stateRoot);
|
|
271
286
|
return undefined;
|
|
287
|
+
} else {
|
|
288
|
+
return { manifest: cached.manifest, tasks: cached.tasks };
|
|
272
289
|
}
|
|
273
|
-
return { manifest: cached.manifest, tasks: cached.tasks };
|
|
274
290
|
}
|
|
275
291
|
|
|
276
292
|
const manifest = readJsonFile<TeamRunManifest>(manifestPath);
|
|
@@ -307,11 +323,15 @@ export async function loadRunManifestByIdAsync(cwd: string, runId: string): Prom
|
|
|
307
323
|
}
|
|
308
324
|
const tasksMtimeMs = tasksStat?.mtimeMs ?? 0;
|
|
309
325
|
if (cached && cached.manifestMtimeMs === manifestStat.mtimeMs && cached.manifestSize === manifestStat.size && cached.tasksMtimeMs === tasksMtimeMs && cached.tasksSize === (tasksStat?.size ?? 0)) {
|
|
310
|
-
|
|
326
|
+
// TTL eviction: expire stale entries even if mtime matches
|
|
327
|
+
if (cached.cachedAt && Date.now() - cached.cachedAt > MANIFEST_CACHE_TTL_MS) {
|
|
328
|
+
manifestCache.delete(stateRoot);
|
|
329
|
+
} else if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
|
|
311
330
|
manifestCache.delete(stateRoot);
|
|
312
331
|
return undefined;
|
|
332
|
+
} else {
|
|
333
|
+
return { manifest: cached.manifest, tasks: cached.tasks };
|
|
313
334
|
}
|
|
314
|
-
return { manifest: cached.manifest, tasks: cached.tasks };
|
|
315
335
|
}
|
|
316
336
|
const manifest = await readJsonFileAsync<TeamRunManifest>(manifestPath);
|
|
317
337
|
if (!manifest || !validateRunManifestPaths(cwd, runId, manifest, stateRoot, tasksPath)) return undefined;
|
package/src/state/task-claims.ts
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import type { TeamTaskState } from "./types.ts";
|
|
3
|
-
|
|
4
|
-
export interface TaskClaimState {
|
|
5
|
-
owner: string;
|
|
6
|
-
token: string;
|
|
7
|
-
leasedUntil: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function createTaskClaim(owner: string, leaseMs = 5 * 60_000, now = new Date()): TaskClaimState {
|
|
11
|
-
return { owner, token: randomUUID(), leasedUntil: new Date(now.getTime() + leaseMs).toISOString() };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function isTaskClaimExpired(claim: TaskClaimState | undefined, now = new Date()): boolean {
|
|
15
|
-
if (!claim) return false;
|
|
16
|
-
const parsed = Date.parse(claim.leasedUntil);
|
|
17
|
-
// Corrupt or invalid date strings produce NaN โ treat as expired immediately.
|
|
18
|
-
return Number.isFinite(parsed) ? parsed <= now.getTime() : true;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function canUseTaskClaim(task: Pick<TeamTaskState, "claim">, owner: string, token: string, now = new Date()): boolean {
|
|
22
|
-
return task.claim?.owner === owner && task.claim.token === token && !isTaskClaimExpired(task.claim, now);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function claimTask<T extends TeamTaskState>(task: T, owner: string, leaseMs?: number, now = new Date()): T {
|
|
26
|
-
if (task.claim && !isTaskClaimExpired(task.claim, now)) {
|
|
27
|
-
throw new Error(`Task '${task.id}' is already claimed by '${task.claim.owner}'.`);
|
|
28
|
-
}
|
|
29
|
-
return { ...task, claim: createTaskClaim(owner, leaseMs, now) };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function releaseTaskClaim<T extends TeamTaskState>(task: T, owner: string, token: string, now = new Date()): T {
|
|
33
|
-
if (!canUseTaskClaim(task, owner, token, now)) {
|
|
34
|
-
throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
|
|
35
|
-
}
|
|
36
|
-
return { ...task, claim: undefined };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function transitionClaimedTaskStatus<T extends TeamTaskState>(task: T, owner: string, token: string, status: T["status"], now = new Date()): T {
|
|
40
|
-
if (!canUseTaskClaim(task, owner, token, now)) {
|
|
41
|
-
throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
|
|
42
|
-
}
|
|
43
|
-
return { ...task, status };
|
|
44
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { TeamTaskState } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export interface TaskClaimState {
|
|
5
|
+
owner: string;
|
|
6
|
+
token: string;
|
|
7
|
+
leasedUntil: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createTaskClaim(owner: string, leaseMs = 5 * 60_000, now = new Date()): TaskClaimState {
|
|
11
|
+
return { owner, token: randomUUID(), leasedUntil: new Date(now.getTime() + leaseMs).toISOString() };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isTaskClaimExpired(claim: TaskClaimState | undefined, now = new Date()): boolean {
|
|
15
|
+
if (!claim) return false;
|
|
16
|
+
const parsed = Date.parse(claim.leasedUntil);
|
|
17
|
+
// Corrupt or invalid date strings produce NaN โ treat as expired immediately.
|
|
18
|
+
return Number.isFinite(parsed) ? parsed <= now.getTime() : true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function canUseTaskClaim(task: Pick<TeamTaskState, "claim">, owner: string, token: string, now = new Date()): boolean {
|
|
22
|
+
return task.claim?.owner === owner && task.claim.token === token && !isTaskClaimExpired(task.claim, now);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function claimTask<T extends TeamTaskState>(task: T, owner: string, leaseMs?: number, now = new Date()): T {
|
|
26
|
+
if (task.claim && !isTaskClaimExpired(task.claim, now)) {
|
|
27
|
+
throw new Error(`Task '${task.id}' is already claimed by '${task.claim.owner}'.`);
|
|
28
|
+
}
|
|
29
|
+
return { ...task, claim: createTaskClaim(owner, leaseMs, now) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function releaseTaskClaim<T extends TeamTaskState>(task: T, owner: string, token: string, now = new Date()): T {
|
|
33
|
+
if (!canUseTaskClaim(task, owner, token, now)) {
|
|
34
|
+
throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
|
|
35
|
+
}
|
|
36
|
+
return { ...task, claim: undefined };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function transitionClaimedTaskStatus<T extends TeamTaskState>(task: T, owner: string, token: string, status: T["status"], now = new Date()): T {
|
|
40
|
+
if (!canUseTaskClaim(task, owner, token, now)) {
|
|
41
|
+
throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
|
|
42
|
+
}
|
|
43
|
+
return { ...task, status };
|
|
44
|
+
}
|
package/src/state/types.ts
CHANGED
|
@@ -39,6 +39,17 @@ export interface VerificationEvidence {
|
|
|
39
39
|
notes?: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export interface TaskOutputSchema {
|
|
43
|
+
/** Output format expected from the worker */
|
|
44
|
+
format: "json" | "markdown" | "text";
|
|
45
|
+
/** JTD or JSON Schema for validating JSON output (only when format="json") */
|
|
46
|
+
schema?: Record<string, unknown>;
|
|
47
|
+
/** Human-readable description of expected output */
|
|
48
|
+
description?: string;
|
|
49
|
+
/** Example of valid output (for prompt guidance) */
|
|
50
|
+
example?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
export interface TaskPacket {
|
|
43
54
|
objective: string;
|
|
44
55
|
scope: TaskScope;
|
|
@@ -53,6 +64,7 @@ export interface TaskPacket {
|
|
|
53
64
|
constraints: string[];
|
|
54
65
|
expectedArtifacts: string[];
|
|
55
66
|
verification: VerificationContract;
|
|
67
|
+
outputSchema?: TaskOutputSchema;
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
export type PolicyDecisionAction = "retry" | "reassign" | "escalate" | "block" | "notify" | "cleanup" | "closeout" | "fail";
|
|
@@ -218,6 +230,7 @@ export interface TeamTaskState {
|
|
|
218
230
|
role: string;
|
|
219
231
|
agent: string;
|
|
220
232
|
title: string;
|
|
233
|
+
displayName?: string;
|
|
221
234
|
status: TeamTaskStatus;
|
|
222
235
|
dependsOn: string[];
|
|
223
236
|
cwd: string;
|
|
@@ -253,4 +266,11 @@ export interface TeamTaskState {
|
|
|
253
266
|
retryCount?: number;
|
|
254
267
|
lastDecision?: PolicyDecision;
|
|
255
268
|
};
|
|
269
|
+
controlReservation?: ControlReservation;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface ControlReservation {
|
|
273
|
+
reservedAt: string;
|
|
274
|
+
controllerId: string;
|
|
275
|
+
acceptsControlEvents: boolean;
|
|
256
276
|
}
|
package/src/state/usage.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import type { TeamTaskState, UsageState } from "./types.ts";
|
|
2
|
-
|
|
3
|
-
export function aggregateUsage(tasks: TeamTaskState[]): UsageState | undefined {
|
|
4
|
-
const total: UsageState = {};
|
|
5
|
-
let found = false;
|
|
6
|
-
for (const task of tasks) {
|
|
7
|
-
if (!task.usage) continue;
|
|
8
|
-
found = true;
|
|
9
|
-
total.input = (total.input ?? 0) + (task.usage.input ?? 0);
|
|
10
|
-
total.output = (total.output ?? 0) + (task.usage.output ?? 0);
|
|
11
|
-
total.cacheRead = (total.cacheRead ?? 0) + (task.usage.cacheRead ?? 0);
|
|
12
|
-
total.cacheWrite = (total.cacheWrite ?? 0) + (task.usage.cacheWrite ?? 0);
|
|
13
|
-
total.cost = (total.cost ?? 0) + (task.usage.cost ?? 0);
|
|
14
|
-
total.turns = (total.turns ?? 0) + (task.usage.turns ?? 0);
|
|
15
|
-
}
|
|
16
|
-
return found ? total : undefined;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function formatUsage(usage: UsageState | undefined): string {
|
|
20
|
-
if (!usage) return "(none)";
|
|
21
|
-
const parts: string[] = [];
|
|
22
|
-
if (usage.input !== undefined) parts.push(`input=${usage.input}`);
|
|
23
|
-
if (usage.output !== undefined) parts.push(`output=${usage.output}`);
|
|
24
|
-
if (usage.cacheRead !== undefined) parts.push(`cacheRead=${usage.cacheRead}`);
|
|
25
|
-
if (usage.cacheWrite !== undefined) parts.push(`cacheWrite=${usage.cacheWrite}`);
|
|
26
|
-
if (usage.cost !== undefined && Number.isFinite(usage.cost)) parts.push(`cost=${usage.cost.toFixed(6)}`);
|
|
27
|
-
if (usage.turns !== undefined) parts.push(`turns=${usage.turns}`);
|
|
28
|
-
return parts.join(", ") || "(none)";
|
|
29
|
-
}
|
|
1
|
+
import type { TeamTaskState, UsageState } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export function aggregateUsage(tasks: TeamTaskState[]): UsageState | undefined {
|
|
4
|
+
const total: UsageState = {};
|
|
5
|
+
let found = false;
|
|
6
|
+
for (const task of tasks) {
|
|
7
|
+
if (!task.usage) continue;
|
|
8
|
+
found = true;
|
|
9
|
+
total.input = (total.input ?? 0) + (task.usage.input ?? 0);
|
|
10
|
+
total.output = (total.output ?? 0) + (task.usage.output ?? 0);
|
|
11
|
+
total.cacheRead = (total.cacheRead ?? 0) + (task.usage.cacheRead ?? 0);
|
|
12
|
+
total.cacheWrite = (total.cacheWrite ?? 0) + (task.usage.cacheWrite ?? 0);
|
|
13
|
+
total.cost = (total.cost ?? 0) + (task.usage.cost ?? 0);
|
|
14
|
+
total.turns = (total.turns ?? 0) + (task.usage.turns ?? 0);
|
|
15
|
+
}
|
|
16
|
+
return found ? total : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatUsage(usage: UsageState | undefined): string {
|
|
20
|
+
if (!usage) return "(none)";
|
|
21
|
+
const parts: string[] = [];
|
|
22
|
+
if (usage.input !== undefined) parts.push(`input=${usage.input}`);
|
|
23
|
+
if (usage.output !== undefined) parts.push(`output=${usage.output}`);
|
|
24
|
+
if (usage.cacheRead !== undefined) parts.push(`cacheRead=${usage.cacheRead}`);
|
|
25
|
+
if (usage.cacheWrite !== undefined) parts.push(`cacheWrite=${usage.cacheWrite}`);
|
|
26
|
+
if (usage.cost !== undefined && Number.isFinite(usage.cost)) parts.push(`cost=${usage.cost.toFixed(6)}`);
|
|
27
|
+
if (usage.turns !== undefined) parts.push(`turns=${usage.turns}`);
|
|
28
|
+
return parts.join(", ") || "(none)";
|
|
29
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../runtime/async-runner.ts";
|
|
1
|
+
export * from "../runtime/async-runner.ts";
|
package/src/subagents/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./spawn.ts";
|
|
2
|
-
export * from "./manager.ts";
|
|
3
|
-
export * from "./async-entry.ts";
|
|
1
|
+
export * from "./spawn.ts";
|
|
2
|
+
export * from "./manager.ts";
|
|
3
|
+
export * from "./async-entry.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../../runtime/live-agent-control.ts";
|
|
1
|
+
export * from "../../runtime/live-agent-control.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../../runtime/live-agent-manager.ts";
|
|
1
|
+
export * from "../../runtime/live-agent-manager.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../../runtime/live-control-realtime.ts";
|
|
1
|
+
export * from "../../runtime/live-control-realtime.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../../runtime/live-session-runtime.ts";
|
|
1
|
+
export * from "../../runtime/live-session-runtime.ts";
|
package/src/subagents/manager.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../runtime/subagent-manager.ts";
|
|
1
|
+
export * from "../runtime/subagent-manager.ts";
|
package/src/subagents/spawn.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../runtime/child-pi.ts";
|
|
1
|
+
export * from "../runtime/child-pi.ts";
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import type { TeamConfig, TeamRole } from "./team-config.ts";
|
|
2
|
-
|
|
3
|
-
function line(key: string, value: string | string[] | undefined): string | undefined {
|
|
4
|
-
if (value === undefined) return undefined;
|
|
5
|
-
if (Array.isArray(value)) return `${key}: ${value.join(", ")}`;
|
|
6
|
-
return `${key}: ${value}`;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function serializeRole(role: TeamRole): string {
|
|
10
|
-
const parts = [`agent=${role.agent}`];
|
|
11
|
-
if (role.model) parts.push(`model=${role.model}`);
|
|
12
|
-
if (role.skills === false) parts.push("skills=false");
|
|
13
|
-
else if (role.skills?.length) parts.push(`skills=${role.skills.join(",")}`);
|
|
14
|
-
if (role.maxConcurrency !== undefined) parts.push(`maxConcurrency=${role.maxConcurrency}`);
|
|
15
|
-
if (role.description) parts.push(role.description);
|
|
16
|
-
return `- ${role.name}: ${parts.join(" ")}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function serializeTeam(team: TeamConfig): string {
|
|
20
|
-
const lines = [
|
|
21
|
-
"---",
|
|
22
|
-
`name: ${team.name}`,
|
|
23
|
-
`description: ${team.description}`,
|
|
24
|
-
team.defaultWorkflow ? `defaultWorkflow: ${team.defaultWorkflow}` : undefined,
|
|
25
|
-
team.workspaceMode ? `workspaceMode: ${team.workspaceMode}` : undefined,
|
|
26
|
-
team.maxConcurrency !== undefined ? `maxConcurrency: ${team.maxConcurrency}` : undefined,
|
|
27
|
-
line("triggers", team.routing?.triggers),
|
|
28
|
-
line("useWhen", team.routing?.useWhen),
|
|
29
|
-
line("avoidWhen", team.routing?.avoidWhen),
|
|
30
|
-
line("cost", team.routing?.cost),
|
|
31
|
-
line("category", team.routing?.category),
|
|
32
|
-
"---",
|
|
33
|
-
"",
|
|
34
|
-
...team.roles.map(serializeRole),
|
|
35
|
-
"",
|
|
36
|
-
].filter((entry): entry is string => entry !== undefined);
|
|
37
|
-
return lines.join("\n");
|
|
38
|
-
}
|
|
1
|
+
import type { TeamConfig, TeamRole } from "./team-config.ts";
|
|
2
|
+
|
|
3
|
+
function line(key: string, value: string | string[] | undefined): string | undefined {
|
|
4
|
+
if (value === undefined) return undefined;
|
|
5
|
+
if (Array.isArray(value)) return `${key}: ${value.join(", ")}`;
|
|
6
|
+
return `${key}: ${value}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function serializeRole(role: TeamRole): string {
|
|
10
|
+
const parts = [`agent=${role.agent}`];
|
|
11
|
+
if (role.model) parts.push(`model=${role.model}`);
|
|
12
|
+
if (role.skills === false) parts.push("skills=false");
|
|
13
|
+
else if (role.skills?.length) parts.push(`skills=${role.skills.join(",")}`);
|
|
14
|
+
if (role.maxConcurrency !== undefined) parts.push(`maxConcurrency=${role.maxConcurrency}`);
|
|
15
|
+
if (role.description) parts.push(role.description);
|
|
16
|
+
return `- ${role.name}: ${parts.join(" ")}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function serializeTeam(team: TeamConfig): string {
|
|
20
|
+
const lines = [
|
|
21
|
+
"---",
|
|
22
|
+
`name: ${team.name}`,
|
|
23
|
+
`description: ${team.description}`,
|
|
24
|
+
team.defaultWorkflow ? `defaultWorkflow: ${team.defaultWorkflow}` : undefined,
|
|
25
|
+
team.workspaceMode ? `workspaceMode: ${team.workspaceMode}` : undefined,
|
|
26
|
+
team.maxConcurrency !== undefined ? `maxConcurrency: ${team.maxConcurrency}` : undefined,
|
|
27
|
+
line("triggers", team.routing?.triggers),
|
|
28
|
+
line("useWhen", team.routing?.useWhen),
|
|
29
|
+
line("avoidWhen", team.routing?.avoidWhen),
|
|
30
|
+
line("cost", team.routing?.cost),
|
|
31
|
+
line("category", team.routing?.category),
|
|
32
|
+
"---",
|
|
33
|
+
"",
|
|
34
|
+
...team.roles.map(serializeRole),
|
|
35
|
+
"",
|
|
36
|
+
].filter((entry): entry is string => entry !== undefined);
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
package/src/types/diff.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
declare module "diff" {
|
|
2
|
-
export interface Change {
|
|
3
|
-
value: string;
|
|
4
|
-
count?: number;
|
|
5
|
-
added?: boolean;
|
|
6
|
-
removed?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface DiffOptions {
|
|
10
|
-
ignoreCase?: boolean;
|
|
11
|
-
newlineIsToken?: boolean;
|
|
12
|
-
ignoreWhitespace?: boolean;
|
|
13
|
-
stripTrailingCr?: boolean;
|
|
14
|
-
oneChangePerToken?: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function diffWords(oldStr: string, newStr: string, options?: DiffOptions): Change[];
|
|
18
|
-
}
|
|
1
|
+
declare module "diff" {
|
|
2
|
+
export interface Change {
|
|
3
|
+
value: string;
|
|
4
|
+
count?: number;
|
|
5
|
+
added?: boolean;
|
|
6
|
+
removed?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DiffOptions {
|
|
10
|
+
ignoreCase?: boolean;
|
|
11
|
+
newlineIsToken?: boolean;
|
|
12
|
+
ignoreWhitespace?: boolean;
|
|
13
|
+
stripTrailingCr?: boolean;
|
|
14
|
+
oneChangePerToken?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function diffWords(oldStr: string, newStr: string, options?: DiffOptions): Change[];
|
|
18
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Management Overlay โ displays discovered agents with their configuration.
|
|
3
|
+
* Read-only view of agent definitions from builtin/user/project sources.
|
|
4
|
+
* Future: enable/disable toggle, model override editing.
|
|
5
|
+
*/
|
|
6
|
+
import type { AgentConfig, ResourceSource } from "../agents/agent-config.ts";
|
|
7
|
+
import { truncate } from "../utils/visual.ts";
|
|
8
|
+
|
|
9
|
+
export interface AgentEntry {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
source: ResourceSource;
|
|
13
|
+
model?: string;
|
|
14
|
+
thinking?: string;
|
|
15
|
+
loadMode?: string;
|
|
16
|
+
contextMode?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
filePath: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function agentToEntry(agent: AgentConfig): AgentEntry {
|
|
22
|
+
return {
|
|
23
|
+
name: agent.name,
|
|
24
|
+
description: agent.description,
|
|
25
|
+
source: agent.source,
|
|
26
|
+
model: agent.model,
|
|
27
|
+
thinking: agent.thinking,
|
|
28
|
+
loadMode: agent.loadMode,
|
|
29
|
+
contextMode: agent.contextMode,
|
|
30
|
+
disabled: agent.disabled,
|
|
31
|
+
filePath: agent.filePath,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sourceIcon(source: ResourceSource): string {
|
|
36
|
+
switch (source) {
|
|
37
|
+
case "builtin": return "๐ฆ";
|
|
38
|
+
case "user": return "๐ค";
|
|
39
|
+
case "project": return "๐";
|
|
40
|
+
case "git": return "๐";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sourceLabel(source: ResourceSource): string {
|
|
45
|
+
switch (source) {
|
|
46
|
+
case "builtin": return "builtin";
|
|
47
|
+
case "user": return "user";
|
|
48
|
+
case "project": return "project";
|
|
49
|
+
case "git": return "git";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AgentOverlayState {
|
|
54
|
+
entries: AgentEntry[];
|
|
55
|
+
selectedIndex: number;
|
|
56
|
+
scrollOffset: number;
|
|
57
|
+
expanded: Set<number>;
|
|
58
|
+
maxVisible: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createAgentOverlayState(entries: AgentEntry[], maxVisible = 20): AgentOverlayState {
|
|
62
|
+
return {
|
|
63
|
+
entries: entries.sort((a, b) => {
|
|
64
|
+
const order: Record<ResourceSource, number> = { project: 0, user: 1, git: 2, builtin: 3 };
|
|
65
|
+
const diff = (order[a.source] ?? 4) - (order[b.source] ?? 4);
|
|
66
|
+
return diff !== 0 ? diff : a.name.localeCompare(b.name);
|
|
67
|
+
}),
|
|
68
|
+
selectedIndex: 0,
|
|
69
|
+
scrollOffset: 0,
|
|
70
|
+
expanded: new Set(),
|
|
71
|
+
maxVisible,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function moveSelection(state: AgentOverlayState, direction: -1 | 1): AgentOverlayState {
|
|
76
|
+
const next = Math.max(0, Math.min(state.entries.length - 1, state.selectedIndex + direction));
|
|
77
|
+
const visibleStart = state.scrollOffset;
|
|
78
|
+
const visibleEnd = state.scrollOffset + state.maxVisible;
|
|
79
|
+
const newScroll = next < visibleStart
|
|
80
|
+
? next
|
|
81
|
+
: next >= visibleEnd
|
|
82
|
+
? Math.max(0, next - state.maxVisible + 1)
|
|
83
|
+
: state.scrollOffset;
|
|
84
|
+
return { ...state, selectedIndex: next, scrollOffset: newScroll };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function toggleExpand(state: AgentOverlayState): AgentOverlayState {
|
|
88
|
+
const expanded = new Set(state.expanded);
|
|
89
|
+
if (expanded.has(state.selectedIndex)) {
|
|
90
|
+
expanded.delete(state.selectedIndex);
|
|
91
|
+
} else {
|
|
92
|
+
expanded.add(state.selectedIndex);
|
|
93
|
+
}
|
|
94
|
+
return { ...state, expanded };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function renderAgentOverlay(state: AgentOverlayState, width: number): string[] {
|
|
98
|
+
const lines: string[] = [];
|
|
99
|
+
const header = ` Agent Configuration (${state.entries.length} agents)`;
|
|
100
|
+
lines.push(truncate(header, width));
|
|
101
|
+
lines.push(truncate("โ".repeat(Math.min(width, 60)), width));
|
|
102
|
+
|
|
103
|
+
if (state.entries.length === 0) {
|
|
104
|
+
lines.push(truncate(" No agents discovered.", width));
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const visible = state.entries.slice(
|
|
109
|
+
state.scrollOffset,
|
|
110
|
+
state.scrollOffset + state.maxVisible,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
for (const [i, entry] of visible.entries()) {
|
|
114
|
+
const globalIndex = state.scrollOffset + i;
|
|
115
|
+
const isSelected = globalIndex === state.selectedIndex;
|
|
116
|
+
const isExpanded = state.expanded.has(globalIndex);
|
|
117
|
+
const cursor = isSelected ? "โธ" : " ";
|
|
118
|
+
const disabled = entry.disabled ? " [disabled]" : "";
|
|
119
|
+
const model = entry.model ? ` (${entry.model})` : "";
|
|
120
|
+
|
|
121
|
+
const summary = `${cursor} ${sourceIcon(entry.source)} ${entry.name}${model}${disabled}`;
|
|
122
|
+
lines.push(truncate(summary, width));
|
|
123
|
+
|
|
124
|
+
if (isExpanded) {
|
|
125
|
+
const desc = ` ${entry.description}`;
|
|
126
|
+
lines.push(truncate(desc, width));
|
|
127
|
+
const meta: string[] = [` source: ${sourceLabel(entry.source)}`];
|
|
128
|
+
if (entry.model) meta.push(`model: ${entry.model}`);
|
|
129
|
+
if (entry.thinking) meta.push(`thinking: ${entry.thinking}`);
|
|
130
|
+
if (entry.loadMode) meta.push(`loadMode: ${entry.loadMode}`);
|
|
131
|
+
if (entry.contextMode) meta.push(`context: ${entry.contextMode}`);
|
|
132
|
+
meta.push(`file: ${entry.filePath}`);
|
|
133
|
+
lines.push(truncate(meta.join(" ยท "), width));
|
|
134
|
+
lines.push(truncate("โ".repeat(Math.min(width - 4, 50)), width));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (state.scrollOffset + state.maxVisible < state.entries.length) {
|
|
139
|
+
const remaining = state.entries.length - state.scrollOffset - state.maxVisible;
|
|
140
|
+
lines.push(truncate(` โฆ +${remaining} more`, width));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return lines;
|
|
144
|
+
}
|