@bermudi/pi-delegate 0.1.0 → 0.1.1

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/lifecycle.ts CHANGED
@@ -11,13 +11,14 @@ import type {
11
11
  TaskProgress,
12
12
  TaskResult,
13
13
  TaskRunEnv,
14
+ ToolActivity,
14
15
  } from "./types.ts";
15
16
  import * as pool from "./pool.ts";
16
17
  import { isSessionBusy } from "./tickets.ts";
17
18
  import {
18
19
  createSubagentSessionManager,
19
- setParentSession,
20
20
  persistSessionHeader,
21
+ setParentSession,
21
22
  } from "./sessions.ts";
22
23
  import { runAgentSession } from "./runner.ts";
23
24
  import { getGitChangedFiles } from "./file-tracking.ts";
@@ -26,6 +27,16 @@ import { resolveCwd, validateResumeFromPath } from "./utils.ts";
26
27
  import { getWholeTaskMaxRetries, getWholeTaskBaseDelayMs } from "./config.ts";
27
28
  import { addUsage, emptyUsage } from "./usage.ts";
28
29
 
30
+ /** Internal seam for lifecycle-level tests without replacing session ownership. */
31
+ type RunAgentSession = typeof runAgentSession;
32
+ let runAgentSessionForTesting: RunAgentSession = runAgentSession;
33
+
34
+ export function _setRunAgentSessionForTesting(
35
+ override: RunAgentSession | undefined,
36
+ ): void {
37
+ runAgentSessionForTesting = override ?? runAgentSession;
38
+ }
39
+
29
40
  /**
30
41
  * Test-only overrides for whole-task retry settings. When set, these bypass
31
42
  * the config-driven values so retry integration tests don't sleep real seconds.
@@ -104,19 +115,57 @@ function disposeOwnedSession(acquired: AcquiredSession): void {
104
115
  }
105
116
  }
106
117
 
107
- /** Mirror a progress update from runAgent into a TaskProgress row. */
118
+ /** Merge per-attempt activities into a live history list while preserving
119
+ * prior-attempt evidence. Activity IDs are used as a stable handle so in-flight
120
+ * updates can replace earlier skeletons for the same call. */
121
+ function mergeToolActivities(
122
+ existing: TaskProgress["activities"],
123
+ incoming: ToolActivity[],
124
+ ): ToolActivity[] {
125
+ const byId = new Map<string, number>();
126
+ for (let i = 0; i < existing.length; i++) {
127
+ byId.set(existing[i]!.id, i);
128
+ }
129
+
130
+ const merged = [...existing];
131
+ for (const incomingActivity of incoming) {
132
+ const index = byId.get(incomingActivity.id);
133
+ if (index === undefined) {
134
+ byId.set(incomingActivity.id, merged.length);
135
+ merged.push(incomingActivity);
136
+ } else {
137
+ merged[index] = { ...merged[index], ...incomingActivity };
138
+ }
139
+ }
140
+ return merged;
141
+ }
142
+
143
+ /** Optional counters are used by retry-aware accounting to keep live progress
144
+ * monotonic across attempts. */
145
+ interface RunUpdateOffset {
146
+ tokensOffset?: number;
147
+ toolUsesOffset?: number;
148
+ }
149
+ /** Mirror a progress update from runAgent into a TaskProgress row.
150
+ *
151
+ * Runner callbacks report attempt-local counters. Offset/merge them so a single
152
+ * TaskProgress row is monotonic across whole-task retries.
153
+ */
108
154
  export function updateProgressFromRun(
109
155
  p: TaskProgress,
110
156
  u: AgentProgressUpdate,
157
+ offsets: RunUpdateOffset = {},
111
158
  ): void {
112
- p.tokens = u.tokens;
113
- p.toolUses = u.toolUses;
114
- p.durationMs = u.durationMs;
159
+ const cumulativeTokens = (offsets.tokensOffset ?? 0) + u.tokens;
160
+ const cumulativeTools = (offsets.toolUsesOffset ?? 0) + u.toolUses;
161
+ p.tokens = Math.max(p.tokens, cumulativeTokens);
162
+ p.toolUses = Math.max(p.toolUses, cumulativeTools);
163
+
164
+ p.durationMs = Math.max(p.durationMs, u.durationMs);
115
165
  p.lastActivityAt = u.lastActivityAt;
116
- p.activities = u.activities;
166
+ p.activities = mergeToolActivities(p.activities, u.activities);
117
167
  p.failureKind = u.failureKind;
118
168
  }
119
-
120
169
  /** Mirror a completed TaskResult into a TaskProgress row (status/duration/error). */
121
170
  function updateProgressFromResult(p: TaskProgress, r: TaskResult): void {
122
171
  p.status = r.error ? "failed" : "done";
@@ -195,19 +244,32 @@ function isClearlyTransientFinalError(error: string | undefined): boolean {
195
244
  );
196
245
  }
197
246
 
198
- function canRetryWholeTask(task: ResolvedTask, result: TaskResult): boolean {
247
+ function canRetryWholeTask(
248
+ task: ResolvedTask,
249
+ result: TaskResult,
250
+ hasBashExecution = false,
251
+ ): boolean {
199
252
  // Whole-task retry can repeat tool side effects. Keep it to stateless fresh
200
253
  // tasks, and only when our touched-file accounting says the failed attempt
201
254
  // did not write/edit anything. A `model_error` (usage limit, auth, quota) is
202
255
  // not transient for the resolved model — retrying with the same model just
203
256
  // hits the same wall, so skip it and let the parent resume with a different
204
- // model (see the hint in formatFailedTask).
257
+ // model (see the hint in formatFailedTask). Similarly, once bash executes,
258
+ // any retry would replay non-idempotent side effects, so suppress it.
259
+ // We gate on *observed* bash activity plus touchedFiles, not on the tool set
260
+ // itself: the default tool set includes `bash` for most tasks, and suppressing
261
+ // retry for every bash-capable task would disable the useful transient-error
262
+ // retry path even when no side effects occurred. The stricter “any bash tool
263
+ // → no retry” rule matches the “no filesystem isolation” stance, but is too
264
+ // restrictive for retry safety — touched-file accounting and observed activity
265
+ // are the direct side-effect signals.
205
266
  return (
206
267
  result.failureKind !== "stalled" &&
207
268
  result.failureKind !== "model_error" &&
208
269
  !task.sessionId &&
209
270
  !task.resumeFrom &&
210
271
  result.touchedFiles.length === 0 &&
272
+ !hasBashExecution &&
211
273
  isClearlyTransientFinalError(result.error)
212
274
  );
213
275
  }
@@ -244,7 +306,7 @@ async function buildDelegateSession(
244
306
  // resource loaders are cached after the first call; provider-configured or
245
307
  // allowlisted-extension loaders are deliberately fresh per session because
246
308
  // their extension runtime is mutable. The resourceLoader is cwd-scoped (it
247
- // scans for AGENTS.md/skills) and the system prompt is per named-agent.
309
+ // scans for project AGENTS.md/skills) and the system prompt is per named-agent.
248
310
  // The custom prompt overrides the default AgentSession system prompt.
249
311
  // Pass only the provider needed by this task. This keeps a non-Kilo task
250
312
  // from receiving Kilo's provider/auth adapter merely because Kilo is also
@@ -504,7 +566,10 @@ async function runResolvedTaskUnlocked(
504
566
  failTask(task, "action='close' requires sessionId."),
505
567
  );
506
568
  }
507
- const closed = await pool.closePooledAgent(task.sessionId);
569
+ // The per-session lock for action-based operations is already held by the
570
+ // outer runResolvedTask() wrapper. Use the internal close helper to avoid a
571
+ // reentrant deadlock on the same key.
572
+ const closed = await pool._closePooledAgentWithoutLock(task.sessionId);
508
573
  return finishTask(
509
574
  env,
510
575
  p,
@@ -530,15 +595,49 @@ async function runResolvedTaskUnlocked(
530
595
  );
531
596
  }
532
597
 
598
+ let hasBashExecution = false;
599
+ let cumulativeTokens = 0;
600
+ let cumulativeToolUses = 0;
601
+ const taskStartedAt = Date.now();
602
+ let accumulatedUsage = emptyUsage();
603
+
604
+ const onAttemptProgress = (u: AgentProgressUpdate): void => {
605
+ if (
606
+ !hasBashExecution &&
607
+ u.activities.some((activity) => activity.name === "bash")
608
+ ) {
609
+ hasBashExecution = true;
610
+ }
611
+
612
+ const mapped: AgentProgressUpdate = {
613
+ ...u,
614
+ tokens: cumulativeTokens + u.tokens,
615
+ toolUses: cumulativeToolUses + u.toolUses,
616
+ durationMs: Date.now() - taskStartedAt,
617
+ };
618
+
619
+ // Keep live totals monotonic across attempts.
620
+ p.tokens = Math.max(p.tokens, mapped.tokens);
621
+ p.toolUses = Math.max(p.toolUses, mapped.toolUses);
622
+ if (mapped.durationMs > p.durationMs) {
623
+ p.durationMs = mapped.durationMs;
624
+ }
625
+ env.onProgress(p, mapped);
626
+ };
627
+
533
628
  const runAttempt = async (): Promise<TaskResult> => {
629
+ let attemptToolUsesObserved = 0;
630
+ const onProgress = (u: AgentProgressUpdate): void => {
631
+ attemptToolUsesObserved = Math.max(attemptToolUsesObserved, u.toolUses);
632
+ onAttemptProgress(u);
633
+ };
634
+
534
635
  // ── Pool / resume / fresh-agent resolution ────────────────────────
535
636
  const acquired = await acquireAgentSession(env, task, p);
536
637
  if ("error" in acquired) return acquired.error;
537
638
 
538
639
  // A pool hit is already owned by the pool. Fresh/resumed sessions belong
539
- // to this attempt until commit() explicitly transfers ownership. Keeping
540
- // this state local makes cleanup a finally invariant rather than a list
541
- // of special cases for aborts, stalls, and provider failures.
640
+ // to this attempt until commit/recordUse/close logic runs.
542
641
  let sessionReleased = !acquired.lifecycleOwnsSession;
543
642
  try {
544
643
  // Re-check abort after acquisition. The pre-acquire check at the top can
@@ -546,22 +645,25 @@ async function runResolvedTaskUnlocked(
546
645
  // baseline. runAgentSession re-checks after attaching its listener, but a
547
646
  // cancelled ticket should not even start the subagent (no file writes, no
548
647
  // pool insert).
549
- if (env.signal?.aborted) return failTask(task, "Aborted");
648
+ if (env.signal?.aborted) {
649
+ return failTask(task, "Aborted");
650
+ }
550
651
 
551
652
  // Snapshot git status before the run so touchedFiles can diff after.
552
653
  // AgentSession owns retry/compaction internally — runAgentSession just
553
654
  // drives the prompt and maps events to the progress model.
554
655
  const gitBaseline = await getGitChangedFiles(task.cwd);
555
- let r = await runAgentSession(
656
+ let r = await runAgentSessionForTesting(
556
657
  acquired.session,
557
658
  task.prompt,
558
659
  { cwd: task.cwd },
559
660
  env.signal,
560
- (u) => env.onProgress(p, u),
661
+ onProgress,
561
662
  gitBaseline,
562
663
  Date.now(),
563
664
  );
564
665
 
666
+ cumulativeTokens += r.tokens;
565
667
  // The signal can fire after the pre-run check or while the runner is
566
668
  // collecting post-prompt evidence. Keep cancellation from looking like
567
669
  // success; finally below releases any uncommitted session.
@@ -569,62 +671,72 @@ async function runResolvedTaskUnlocked(
569
671
  r = { ...r, error: "Aborted" };
570
672
  }
571
673
 
572
- // A stalled prompt was explicitly aborted and is no longer a safe
573
- // continuation. Close a pooled hit; a fresh/resumed session is still
574
- // released by the finally below. closePooledAgent removes the pooled
575
- // entry before surfacing cleanup errors, so a pool-owned session is
576
- // never directly disposed here.
577
- if (r.failureKind === "stalled" && task.sessionId) {
578
- try {
579
- if (await pool.closePooledAgent(task.sessionId)) {
580
- sessionReleased = true;
581
- }
582
- } catch (error) {
583
- // Preserve the primary stalled result while logging the cleanup
584
- // failure explicitly. A pooled session is already removed by the
585
- // pool; a pool miss remains lifecycle-owned and is handled below.
586
- console.error(
587
- `[delegate] failed to dispose stalled pooled session '${task.sessionId}'`,
588
- error,
589
- );
590
- }
591
- }
592
-
593
674
  const sessionFile = resolveResumableSessionFile(
594
675
  acquired.sessionFile,
595
676
  acquired.sessionManager,
596
677
  r.error,
597
678
  );
598
679
 
599
- // Pool bookkeeping. commit() is the sole mutator: it decides
600
- // insert-vs-recordUse by map presence (sound because the session lock
601
- // serializes same-sessionId tasks). A successful insert transfers
602
- // ownership; a failed insert leaves finally responsible for disposal.
603
- if (task.sessionId && !r.error) {
604
- sessionReleased = pool.commit(task.sessionId, {
605
- session: acquired.session,
606
- sessionManager: acquired.sessionManager,
607
- sessionFile: acquired.sessionFile,
608
- frozen: {
609
- systemPrompt: task.systemPrompt,
610
- model: task.model,
611
- thinking: task.thinking,
612
- tools: task.tools,
613
- cwd: task.cwd,
614
- },
615
- tokens: r.tokens,
616
- });
680
+ if (task.sessionId) {
681
+ if (acquired.lifecycleOwnsSession) {
682
+ // Pool misses (including resumeFrom) transfer ownership only on
683
+ // successful completion; failures are owned by lifecycle and must
684
+ // be disposed in this finally path.
685
+ if (!r.error && r.failureKind !== "stalled") {
686
+ const committed = pool.commit(task.sessionId, {
687
+ session: acquired.session,
688
+ sessionManager: acquired.sessionManager,
689
+ sessionFile: acquired.sessionFile,
690
+ frozen: {
691
+ systemPrompt: task.systemPrompt,
692
+ model: task.model,
693
+ thinking: task.thinking,
694
+ tools: task.tools,
695
+ cwd: task.cwd,
696
+ },
697
+ tokens: r.tokens,
698
+ });
699
+ sessionReleased = sessionReleased || committed;
700
+ }
701
+ } else {
702
+ // A stalled pooled attempt is not safe to keep; remove from the
703
+ // pool and let the lifecycle-owned finalizer handle disposal.
704
+ if (r.failureKind === "stalled") {
705
+ try {
706
+ sessionReleased =
707
+ (await pool._closePooledAgentWithoutLock(task.sessionId)) ||
708
+ sessionReleased;
709
+ } catch (error) {
710
+ // Preserve the primary stalled result while logging the cleanup
711
+ // failure explicitly. A pooled session may still be removed by
712
+ // the pool; a pool-miss remains lifecycle-owned and is handled
713
+ // by the finally path above.
714
+ console.error(
715
+ `[delegate] failed to dispose stalled pooled session '${task.sessionId}'`,
716
+ error,
717
+ );
718
+ }
719
+ } else {
720
+ // Pool hits stay owned by the pool, and non-stalled completions
721
+ // (including failed attempts) must still count usage.
722
+ pool.recordUse(task.sessionId, r.tokens);
723
+ }
724
+ }
617
725
  }
618
726
 
727
+ accumulatedUsage = addUsage(accumulatedUsage, r.usage);
728
+ cumulativeToolUses += attemptToolUsesObserved;
729
+
619
730
  return {
620
731
  agent: task.agentName,
621
732
  output: r.output,
622
733
  error: r.error,
623
- // Classify the failure: the runner sets `stalled` for the inactivity
624
- // watchdog; here we add `model_error` for failures attributable to
625
- // the resolved model (usage limit, auth, quota) so the parent gets a
626
- // "switch model" hint instead of a same-model retry hint, and so
627
- // canRetryWholeTask skips the pointless same-model retry.
734
+ // Classify the failure: the runner sets `stalled` for the
735
+ // inactivity watchdog; here we add `model_error` for failures
736
+ // attributable to the resolved model (usage limit, auth, quota) so the
737
+ // parent gets a "switch model" hint instead of a same-model retry
738
+ // hint, and so canRetryWholeTask skips the pointless same-model
739
+ // retry.
628
740
  failureKind:
629
741
  r.failureKind ??
630
742
  (r.error && isModelAttributableError(r.error)
@@ -644,28 +756,31 @@ async function runResolvedTaskUnlocked(
644
756
  }
645
757
  };
646
758
 
647
- // The session lock is now taken at the top of runResolvedTask (covers the
648
- // full acquire/run/close lifecycle), so attempts execute serially per
649
- // sessionId without needing an inner lock here.
650
- let result = await runAttempt();
651
- // Accumulate usage across whole-task retries: the parent pays for every
652
- // attempt, including the transient failures that retry. Keep tokens tied
653
- // to the accumulated usage as well; otherwise the final result and the
654
- // final progress row describe different amounts of work.
655
- let accumulatedUsage = result.usage;
656
- result = {
657
- ...result,
658
- tokens: accumulatedUsage.totalTokens,
659
- usage: accumulatedUsage,
660
- };
759
+ let result: TaskResult;
760
+ try {
761
+ result = await runAttempt();
762
+ } catch (err) {
763
+ const failure = failTask(
764
+ task,
765
+ err instanceof Error ? err.message : String(err),
766
+ );
767
+ return finishTask(env, p, {
768
+ ...failure,
769
+ durationMs: Math.max(failure.durationMs, Date.now() - taskStartedAt),
770
+ tokens: accumulatedUsage.totalTokens,
771
+ usage: accumulatedUsage,
772
+ });
773
+ }
774
+
661
775
  const maxRetries = resolvedWholeTaskMaxRetries();
662
776
  const baseDelayMs = resolvedWholeTaskBaseDelayMs();
663
777
  for (
664
778
  let retry = 0;
665
- retry < maxRetries && canRetryWholeTask(task, result);
779
+ retry < maxRetries && canRetryWholeTask(task, result, hasBashExecution);
666
780
  retry++
667
781
  ) {
668
782
  const delayMs = baseDelayMs * 2 ** retry;
783
+ p.durationMs = Math.max(p.durationMs, Date.now() - taskStartedAt);
669
784
  await sleepForWholeTaskRetry(env.signal, delayMs);
670
785
  if (env.signal?.aborted) {
671
786
  // Preserve any partial output/session path from the last failed attempt
@@ -674,27 +789,47 @@ async function runResolvedTaskUnlocked(
674
789
  result = {
675
790
  ...result,
676
791
  error: "Aborted",
792
+ durationMs: Date.now() - taskStartedAt,
677
793
  tokens: accumulatedUsage.totalTokens,
678
794
  usage: accumulatedUsage,
795
+ touchedFiles: result.touchedFiles,
796
+ sessionFile: result.sessionFile,
679
797
  };
680
798
  break;
681
799
  }
800
+
682
801
  p.status = "running";
683
802
  p.error = undefined;
684
803
  p.failureKind = undefined;
685
804
  env.onStatusChange?.();
686
- result = await runAttempt();
687
- accumulatedUsage = addUsage(accumulatedUsage, result.usage);
688
- result = {
689
- ...result,
690
- tokens: accumulatedUsage.totalTokens,
691
- usage: accumulatedUsage,
692
- };
805
+ try {
806
+ result = await runAttempt();
807
+ } catch (err) {
808
+ const failure = failTask(
809
+ task,
810
+ err instanceof Error ? err.message : String(err),
811
+ );
812
+ result = {
813
+ ...failure,
814
+ durationMs: Math.max(failure.durationMs, Date.now() - taskStartedAt),
815
+ tokens: accumulatedUsage.totalTokens,
816
+ usage: accumulatedUsage,
817
+ };
818
+ break;
819
+ }
693
820
  }
694
- return finishTask(env, p, result);
821
+
822
+ return finishTask(env, p, {
823
+ ...result,
824
+ durationMs: Math.max(result.durationMs, Date.now() - taskStartedAt),
825
+ tokens: Math.max(cumulativeTokens, accumulatedUsage.totalTokens),
826
+ usage: accumulatedUsage,
827
+ });
695
828
  } catch (err) {
696
829
  // Any acquired session is released by runAttempt's finally before an
697
- // exception reaches this boundary.
830
+ // exception reaches this boundary. This outer catch handles unexpected
831
+ // throws before retry accounting is initialized, so report a minimal
832
+ // failure without accumulated usage.
698
833
  return finishTask(
699
834
  env,
700
835
  p,
package/manual.ts CHANGED
@@ -70,10 +70,16 @@ export function getSubagentManualMarkdown(
70
70
  "**Why you're seeing this:** no tasks were provided, so the tool returned help instead of dispatching. Nothing is broken. To dispatch subagents, put task fields inside `tasks: [{ ... }]`.",
71
71
  "",
72
72
  "```ts",
73
- 'delegate({ tasks: [{ prompt: "Investigate the auth module" }] })',
73
+ 'delegate({ tasks: [{ agent: "default", prompt: "Investigate the auth module" }] })',
74
74
  "```",
75
75
  "",
76
- "Delegate subagents to execute tasks in parallel. Each subagent gets an independent context, system prompt, model, tools, and thinking level. Custom agents can be defined inline in a task or persisted as Markdown files.",
76
+ "Delegate subagents to execute tasks in parallel. Each subagent gets an independent context. Use the built-in `default` profile when it should mirror the live parent's model, thinking level, delegatable native tools, and base system prompt. Custom agents can be defined inline in a task or persisted as Markdown files.",
77
+ "",
78
+ "## Built-in Agent",
79
+ "",
80
+ "- **default**: mirrors the live parent model, thinking level, delegatable native tools, and base system prompt.",
81
+ "",
82
+ "Parent extension/MCP tools are not copied, and project context is rebuilt safely for the task's `cwd`. Per-task fields remain explicit overrides.",
77
83
  "",
78
84
  "## Available Custom Agents",
79
85
  "",
@@ -170,7 +176,8 @@ export function getSubagentManualMarkdown(
170
176
  "",
171
177
  "- `*` means read/write/edit/bash, not every tool. `grep`, `find`, and `ls` are valid explicit tools and are the `ro` preset.",
172
178
  '- `tasks` is an array. The tool recovers common stringified calls for compatibility, but canonical calls use `{ tasks: [{ prompt: "..." }] }`.',
173
- "- An ad-hoc task with no `tools` uses `*`; a named task uses its profile; a profile with no tools uses `*`.",
179
+ '- Use `agent: "default"` for the parent\'s live model/thinking/native tools/base prompt. Omitting `agent` creates an ad-hoc task with delegate defaults.',
180
+ "- An ad-hoc task with no `tools` uses `*`; a named custom task uses its profile; a profile with no tools uses `*`.",
174
181
  "- Subagents inherit all skills discovered in their `cwd` (via AgentSession's resource loader). Per-task skill filtering is not supported — curate the cwd's skill set instead.",
175
182
  `- Sync \`delegate\` runs at most ${getMaxConcurrent()} tasks at once (the rest queue, not fail). Use \`async: true\` to move work to the background.`,
176
183
  "",
package/model.ts CHANGED
@@ -6,8 +6,7 @@ import { VALID_THINKING } from "./constants.ts";
6
6
  export interface ResolvedModelRequest {
7
7
  model: Model<Api> | undefined;
8
8
  /** Pi-style `:<thinking-level>` suffix stripped to make the reference
9
- * resolve. Reported so the caller can warn it is NOT honored as a
10
- * thinking level; the task's `thinking` field is the only thinking input. */
9
+ * resolve. The caller may use it as a last-resort thinking default. */
11
10
  strippedSuffix?: ThinkingLevel;
12
11
  }
13
12
 
@@ -32,8 +31,8 @@ function resolveModelReference(
32
31
  * stripped and the base reference is resolved — models learned this syntax
33
32
  * from Pi's CLI (e.g. `openai-codex/gpt-5.6-luna:max`) and keep emitting it,
34
33
  * so hard-failing the whole call over it is worse than tolerating it. The
35
- * suffix is deliberately NOT fed into thinking resolution: a single knob
36
- * (the `thinking` field) beats two knobs with a silent precedence rule. */
34
+ * suffix is returned separately so task resolution can use it as a
35
+ * last-resort default while keeping explicit `thinking` authoritative. */
37
36
  export function resolveModelRequest(
38
37
  spec: string | undefined,
39
38
  registry: ModelRegistry,
package/package.json CHANGED
@@ -1,25 +1,6 @@
1
1
  {
2
2
  "name": "@bermudi/pi-delegate",
3
- "version": "0.1.0",
4
- "devDependencies": {
5
- "@earendil-works/pi-agent-core": "^0.80.9",
6
- "@earendil-works/pi-ai": "^0.80.9",
7
- "@earendil-works/pi-coding-agent": "^0.80.9",
8
- "@earendil-works/pi-tui": "^0.80.9",
9
- "@marcfargas/pi-test-harness": "^0.6.1",
10
- "@sinclair/typebox": "^0.34.0",
11
- "esbuild": "^0.27.0",
12
- "prettier": "^3.8.4",
13
- "typescript": "^5.9.0"
14
- },
15
- "private": false,
16
- "scripts": {
17
- "test": "bun test",
18
- "typecheck": "tsc --noEmit",
19
- "build": "esbuild delegate.ts --bundle --platform=neutral --packages=external --format=esm --banner:js=\"// @ts-nocheck\" --outfile=delegate.bundle.ts",
20
- "format": "prettier --write \"**/*.ts\""
21
- },
22
- "type": "module",
3
+ "version": "0.1.1",
23
4
  "description": "Delegate tool for the Pi coding agent.",
24
5
  "keywords": [
25
6
  "pi-package"
@@ -28,6 +9,7 @@
28
9
  "type": "git",
29
10
  "url": "git+https://github.com/bermudi/pi-delegate.git"
30
11
  },
12
+ "type": "module",
31
13
  "files": [
32
14
  "*.ts",
33
15
  "!*.test.ts",
@@ -39,5 +21,25 @@
39
21
  "extensions": [
40
22
  "./delegate.ts"
41
23
  ]
24
+ },
25
+ "devDependencies": {
26
+ "@earendil-works/pi-agent-core": "^0.80.9",
27
+ "@earendil-works/pi-ai": "^0.80.9",
28
+ "@earendil-works/pi-coding-agent": "^0.80.9",
29
+ "@earendil-works/pi-tui": "^0.80.9",
30
+ "@marcfargas/pi-test-harness": "^0.6.1",
31
+ "esbuild": "^0.27.0",
32
+ "prettier": "^3.8.4",
33
+ "typescript": "^5.9.0"
34
+ },
35
+ "private": false,
36
+ "scripts": {
37
+ "test": "bun test",
38
+ "typecheck": "tsc --noEmit",
39
+ "build": "esbuild delegate.ts --bundle --platform=neutral --packages=external --format=esm --banner:js=\"// @ts-nocheck\" --outfile=.build/delegate.bundle.ts",
40
+ "format": "prettier --write \"**/*.ts\""
41
+ },
42
+ "dependencies": {
43
+ "@sinclair/typebox": "0.34.52"
42
44
  }
43
45
  }