@dungle-scrubs/tallow 0.8.7 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +244 -54
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +1 -1
  5. package/dist/config.js +1 -1
  6. package/dist/sdk.d.ts +39 -0
  7. package/dist/sdk.d.ts.map +1 -1
  8. package/dist/sdk.js +322 -13
  9. package/dist/sdk.js.map +1 -1
  10. package/dist/session-utils.d.ts +8 -0
  11. package/dist/session-utils.d.ts.map +1 -1
  12. package/dist/session-utils.js +38 -1
  13. package/dist/session-utils.js.map +1 -1
  14. package/extensions/__integration__/plan-rejection-feedback.test.ts +272 -0
  15. package/extensions/__integration__/worktree.test.ts +88 -0
  16. package/extensions/_icons/index.ts +2 -3
  17. package/extensions/_shared/inline-preview.ts +2 -4
  18. package/extensions/_shared/interop-events.ts +48 -0
  19. package/extensions/_shared/shell-policy.ts +2 -1
  20. package/extensions/_shared/tallow-paths.ts +48 -0
  21. package/extensions/agent-commands-tool/__tests__/parsing.test.ts +40 -1
  22. package/extensions/agent-commands-tool/index.ts +38 -6
  23. package/extensions/background-task-tool/index.ts +2 -2
  24. package/extensions/bash-tool-enhanced/index.ts +3 -4
  25. package/extensions/cd-tool/index.ts +3 -3
  26. package/extensions/claude-bridge/index.ts +2 -1
  27. package/extensions/command-prompt/__tests__/plugin-sources.test.ts +49 -0
  28. package/extensions/command-prompt/index.ts +36 -0
  29. package/extensions/context-files/index.ts +2 -1
  30. package/extensions/context-fork/index.ts +2 -1
  31. package/extensions/context-usage/__tests__/tool-result-memory.test.ts +62 -0
  32. package/extensions/context-usage/index.ts +169 -8
  33. package/extensions/debug/__tests__/analysis.test.ts +35 -2
  34. package/extensions/debug/__tests__/diagnostics-commands.test.ts +11 -2
  35. package/extensions/debug/analysis.ts +53 -16
  36. package/extensions/debug/index.ts +84 -10
  37. package/extensions/debug/logger.ts +2 -2
  38. package/extensions/health/index.ts +2 -2
  39. package/extensions/hooks/__tests__/claude-compat.test.ts +31 -0
  40. package/extensions/hooks/extension.json +3 -1
  41. package/extensions/hooks/hooks.schema.json +17 -1
  42. package/extensions/hooks/index.ts +52 -7
  43. package/extensions/lsp/index.ts +2 -7
  44. package/extensions/output-styles-tool/index.ts +2 -4
  45. package/extensions/plan-mode-tool/extension.json +1 -0
  46. package/extensions/plan-mode-tool/index.ts +119 -4
  47. package/extensions/prompt-suggestions/index.ts +2 -3
  48. package/extensions/random-spinner/index.ts +2 -3
  49. package/extensions/read-tool-enhanced/__tests__/notebook-read.test.ts +100 -0
  50. package/extensions/read-tool-enhanced/__tests__/notebook.test.ts +136 -0
  51. package/extensions/read-tool-enhanced/extension.json +4 -4
  52. package/extensions/read-tool-enhanced/index.ts +112 -10
  53. package/extensions/read-tool-enhanced/notebook.ts +526 -0
  54. package/extensions/rewind/__tests__/snapshots.test.ts +43 -1
  55. package/extensions/rewind/snapshots.ts +18 -6
  56. package/extensions/session-memory/index.ts +3 -2
  57. package/extensions/stats/stats-log.ts +2 -3
  58. package/extensions/subagent-tool/__tests__/discovery-defaults.test.ts +23 -0
  59. package/extensions/subagent-tool/__tests__/isolation-frontmatter.test.ts +100 -0
  60. package/extensions/subagent-tool/__tests__/model-router.test.ts +8 -0
  61. package/extensions/subagent-tool/agents.ts +77 -16
  62. package/extensions/subagent-tool/index.ts +213 -48
  63. package/extensions/subagent-tool/model-router.ts +3 -3
  64. package/extensions/subagent-tool/process.ts +233 -22
  65. package/extensions/subagent-tool/schema.ts +11 -0
  66. package/extensions/subagent-tool/widget.ts +15 -2
  67. package/extensions/tasks/__tests__/widget-subagents.test.ts +28 -7
  68. package/extensions/tasks/commands/register-tasks-extension.ts +15 -33
  69. package/extensions/tasks/state/index.ts +2 -2
  70. package/extensions/theme-selector/index.ts +3 -7
  71. package/extensions/worktree/__tests__/lifecycle.test.ts +115 -0
  72. package/extensions/worktree/extension.json +31 -0
  73. package/extensions/worktree/index.ts +149 -0
  74. package/extensions/worktree/lifecycle.ts +372 -0
  75. package/package.json +1 -1
  76. package/skills/tallow-expert/SKILL.md +1 -1
@@ -54,7 +54,7 @@ import {
54
54
  setPiRef,
55
55
  spawnBackgroundSubagent,
56
56
  } from "./process.js";
57
- import type { SubagentCompleteDetails } from "./schema.js";
57
+ import type { IsolationMode, SubagentCompleteDetails } from "./schema.js";
58
58
  import { SubagentParams } from "./schema.js";
59
59
  import {
60
60
  backgroundSubagents,
@@ -173,6 +173,11 @@ export default function (pi: ExtensionAPI) {
173
173
  label: "subagent",
174
174
  description: `Delegate tasks to specialized subagents with isolated context. Modes: single (agent + task), parallel (tasks array), centipede (sequential with {previous} placeholder). Default agent scope is "user" (from ~/.tallow/agents). To include project-local agents, set agentScope: "both". Missing agent names recover gracefully via best-match or ephemeral fallback.
175
175
 
176
+ ISOLATION:
177
+ - isolation: "worktree" runs a subagent in a detached temporary git worktree
178
+ - precedence: per-call param > agent frontmatter > _defaults.md frontmatter
179
+ - in parallel/centipede mode, isolation can be set per task item
180
+
176
181
  MODEL SELECTION:
177
182
  - model: explicit model name (fuzzy matched, e.g. "opus", "haiku", "gemini flash")
178
183
  - No model specified: auto-routes based on task type and complexity
@@ -246,7 +251,22 @@ WHEN NOT TO USE SUBAGENTS:
246
251
  }
247
252
 
248
253
  const agentScope: AgentScope = params.agentScope ?? "user";
249
- const discovery = discoverAgents(ctx.cwd, agentScope);
254
+ let discovery: ReturnType<typeof discoverAgents>;
255
+ try {
256
+ discovery = discoverAgents(ctx.cwd, agentScope);
257
+ } catch (error) {
258
+ const reason = error instanceof Error ? error.message : String(error);
259
+ return {
260
+ content: [{ type: "text", text: `Invalid agent frontmatter: ${reason}` }],
261
+ details: {
262
+ mode: "single" as const,
263
+ agentScope,
264
+ projectAgentsDir: null,
265
+ results: [],
266
+ },
267
+ isError: true,
268
+ };
269
+ }
250
270
  const agents = discovery.agents;
251
271
  const defaults = discovery.defaults;
252
272
 
@@ -376,12 +396,15 @@ WHEN NOT TO USE SUBAGENTS:
376
396
  bg.historyRetainedMessageCount !== undefined
377
397
  ? `\n**History:** compacted (${bg.historyRetainedMessageCount}/${bg.historyOriginalMessageCount} messages retained)`
378
398
  : "";
399
+ const isolationLine = bg.isolationMode
400
+ ? `\n**Isolation:** ${bg.isolationMode}${bg.worktreePath ? ` (${bg.worktreePath})` : ""}`
401
+ : "";
379
402
  return {
380
403
  details: {},
381
404
  content: [
382
405
  {
383
406
  type: "text",
384
- text: `**Task:** ${bg.id}\n**Agent:** ${bg.agent}\n**Status:** ${bg.status}\n**Duration:** ${duration}\n**Task:** ${bg.task}${historyLine}\n\n**Output:**\n${output}`,
407
+ text: `**Task:** ${bg.id}\n**Agent:** ${bg.agent}\n**Status:** ${bg.status}\n**Duration:** ${duration}\n**Task:** ${bg.task}${isolationLine}${historyLine}\n\n**Output:**\n${output}`,
385
408
  },
386
409
  ],
387
410
  };
@@ -406,7 +429,8 @@ WHEN NOT TO USE SUBAGENTS:
406
429
  : getIcon("error");
407
430
  const preview = bg.task.length > 40 ? `${bg.task.slice(0, 37)}...` : bg.task;
408
431
  const compactedBadge = bg.historyCompacted ? " · compacted" : "";
409
- return `${statusIcon} **${bg.id}** (${bg.agent}) - ${bg.status}${compactedBadge} (${duration})\n ${preview}`;
432
+ const isolationBadge = bg.isolationMode ? ` · ${bg.isolationMode}` : "";
433
+ return `${statusIcon} **${bg.id}** (${bg.agent}) - ${bg.status}${compactedBadge}${isolationBadge} (${duration})\n ${preview}`;
410
434
  });
411
435
 
412
436
  return {
@@ -428,7 +452,13 @@ WHEN NOT TO USE SUBAGENTS:
428
452
  * Execute centipede (sequential) mode.
429
453
  */
430
454
  async function executeCentipede(
431
- centipede: { agent: string; task: string; cwd?: string; model?: string }[],
455
+ centipede: {
456
+ agent: string;
457
+ task: string;
458
+ cwd?: string;
459
+ model?: string;
460
+ isolation?: IsolationMode;
461
+ }[],
432
462
  ctx: ExtensionContext,
433
463
  agents: { name: string; source: string }[],
434
464
  defaults: Parameters<typeof runSingleAgent>[13],
@@ -511,7 +541,8 @@ async function executeCentipede(
511
541
  step.model,
512
542
  parentModelId,
513
543
  defaults,
514
- routingHints
544
+ routingHints,
545
+ step.isolation
515
546
  );
516
547
  results.push(result);
517
548
  latestPartialResult = undefined;
@@ -552,11 +583,20 @@ async function executeCentipede(
552
583
  }
553
584
  }
554
585
 
586
+ /** Parallel-task payload for subagent parallel mode. */
587
+ type ParallelTask = {
588
+ agent: string;
589
+ task: string;
590
+ cwd?: string;
591
+ model?: string;
592
+ isolation?: IsolationMode;
593
+ };
594
+
555
595
  /**
556
596
  * Execute parallel mode.
557
597
  */
558
598
  async function executeParallel(
559
- tasks: { agent: string; task: string; cwd?: string; model?: string }[],
599
+ tasks: ParallelTask[],
560
600
  params: { background?: boolean },
561
601
  ctx: ExtensionContext,
562
602
  agents: Parameters<typeof runSingleAgent>[1],
@@ -586,7 +626,8 @@ async function executeParallel(
586
626
  (t as { model?: string }).model,
587
627
  parentModelId,
588
628
  defaults,
589
- routingHints
629
+ routingHints,
630
+ t.isolation
590
631
  );
591
632
  if (result?.startsWith("bg_")) taskIds.push(result);
592
633
  else if (result) errors.push(result);
@@ -710,7 +751,8 @@ async function executeParallel(
710
751
  (t as { model?: string }).model,
711
752
  parentModelId,
712
753
  defaults,
713
- routingHints
754
+ routingHints,
755
+ t.isolation
714
756
  );
715
757
  allResults[globalIndex] = result;
716
758
  return result;
@@ -725,7 +767,95 @@ async function executeParallel(
725
767
  ctx.ui.setWorkingMessage();
726
768
  }
727
769
 
728
- const counts = countParallelResultStates(results);
770
+ let counts = countParallelResultStates(results);
771
+ let stalledRetrySummary: string | undefined;
772
+
773
+ const initialStalledIndexes = collectParallelStateIndices(results, "stalled");
774
+ if (initialStalledIndexes.length > 0) {
775
+ const retrySummaryLines: string[] = [];
776
+ const totalRetries = initialStalledIndexes.length;
777
+
778
+ try {
779
+ ctx.ui.setWorkingMessage(
780
+ `Rerunning ${totalRetries} stalled worker${totalRetries === 1 ? "" : "s"} individually`
781
+ );
782
+
783
+ for (let retryIndex = 0; retryIndex < initialStalledIndexes.length; retryIndex++) {
784
+ const stalledIndex = initialStalledIndexes[retryIndex];
785
+ const stalledTask = tasks[stalledIndex];
786
+ const priorResult = allResults[stalledIndex];
787
+ const retryTask = buildStalledRetryTask(stalledTask.task);
788
+ const explicitRetryModel = stalledTask.model ?? priorResult.model ?? parentModelId;
789
+ const retryRoutingHints = explicitRetryModel ? undefined : routingHints;
790
+ const retryLabel = explicitRetryModel
791
+ ? `model:${explicitRetryModel}`
792
+ : retryRoutingHints?.modelScope
793
+ ? `modelScope:${retryRoutingHints.modelScope}`
794
+ : "auto-routing";
795
+
796
+ ctx.ui.setWorkingMessage(
797
+ `Retrying stalled worker ${retryIndex + 1}/${totalRetries}: ${stalledTask.agent}`
798
+ );
799
+
800
+ const retryResult = await runSingleAgent(
801
+ ctx.cwd,
802
+ agents,
803
+ stalledTask.agent,
804
+ retryTask,
805
+ stalledTask.cwd,
806
+ undefined,
807
+ signal,
808
+ (partial) => {
809
+ if (partial.details?.results[0]) {
810
+ const partialResult = {
811
+ ...partial.details.results[0],
812
+ task: stalledTask.task,
813
+ };
814
+ allResults[stalledIndex] = partialResult;
815
+ emitParallelUpdate();
816
+ }
817
+ },
818
+ makeDetails("parallel"),
819
+ pi.events,
820
+ undefined,
821
+ explicitRetryModel,
822
+ parentModelId,
823
+ defaults,
824
+ retryRoutingHints,
825
+ stalledTask.isolation
826
+ );
827
+
828
+ retryResult.task = stalledTask.task;
829
+ retryResult.stderr = appendStderrNote(
830
+ retryResult.stderr,
831
+ `[Auto-rerun] retried stalled worker individually (${retryLabel})`
832
+ );
833
+ allResults[stalledIndex] = retryResult;
834
+ retrySummaryLines.push(`- [${stalledTask.agent}] ${retryLabel}`);
835
+ emitParallelUpdate();
836
+ }
837
+ } finally {
838
+ ctx.ui.setWorkingMessage();
839
+ }
840
+
841
+ results = [...allResults];
842
+ counts = countParallelResultStates(results);
843
+ const remainingStalledIndexes = collectParallelStateIndices(results, "stalled");
844
+ const recoveredCount = totalRetries - remainingStalledIndexes.length;
845
+ const remainingAgents = remainingStalledIndexes.map((index) => results[index].agent).join(", ");
846
+ const lines = [
847
+ `Auto-rerun: retried ${totalRetries} stalled worker${totalRetries === 1 ? "" : "s"} individually with narrowed scope.`,
848
+ `Recovered ${recoveredCount}/${totalRetries}.`,
849
+ ];
850
+ if (remainingStalledIndexes.length > 0) {
851
+ lines.push(`Still stalled: ${remainingAgents}.`);
852
+ }
853
+ stalledRetrySummary = lines.join(" ");
854
+ if (retrySummaryLines.length > 0) {
855
+ stalledRetrySummary += `\n${retrySummaryLines.join("\n")}`;
856
+ }
857
+ }
858
+
729
859
  const summaries = results.map((result) => {
730
860
  const output = getFinalOutput(result.messages);
731
861
  const fallback = result.errorMessage || result.stderr || "(no output)";
@@ -737,7 +867,6 @@ async function executeParallel(
737
867
  return `[${result.agent}] ${stateLabel}: ${preview}`;
738
868
  });
739
869
 
740
- const finishedNonStalled = counts.completed + counts.failed;
741
870
  let isError = false;
742
871
  let stalledRemediation: string | undefined;
743
872
 
@@ -748,41 +877,16 @@ async function executeParallel(
748
877
  const stalledAgentList = stalledAgents.join(", ");
749
878
  const remediation =
750
879
  `Stalled workers: ${stalledAgentList}. ` +
751
- "Rerun stalled tasks individually, reduce task scope, " +
752
- "or set an explicit model/modelScope for those workers.";
753
-
754
- if (ctx.hasUI && finishedNonStalled > 0) {
755
- const continueWithPartials = await ctx.ui.confirm(
756
- "Parallel workers stalled",
757
- `${counts.stalled} worker${counts.stalled === 1 ? "" : "s"} stalled while ` +
758
- `${finishedNonStalled} other task${finishedNonStalled === 1 ? "" : "s"} finished.\n\n` +
759
- "Continue and return partial results from finished workers?\n\n" +
760
- 'Select "No" to abort now and rerun the stalled workers.'
761
- );
762
- if (!continueWithPartials) {
763
- return {
764
- content: [
765
- {
766
- type: "text" as const,
767
- text:
768
- "Parallel run aborted because stalled workers were detected and " +
769
- "you chose not to continue with partial results.\n\n" +
770
- `${remediation}`,
771
- },
772
- ],
773
- details: makeDetails("parallel")(results),
774
- isError: true,
775
- };
776
- }
777
- } else {
778
- isError = true;
779
- stalledRemediation = ctx.hasUI
780
- ? "Parallel run finished with stalled workers and no completed non-stalled results.\n" +
781
- `${remediation}`
782
- : "Non-interactive mode: returning partial results and marking this " +
783
- "parallel call as error because some workers stalled.\n" +
784
- `${remediation}`;
785
- }
880
+ "Automatic rerun already attempted once per stalled worker. " +
881
+ "Split task scope further, avoid confirmation-gated steps, " +
882
+ "or pin a different explicit model/modelScope.";
883
+ isError = true;
884
+ stalledRemediation = ctx.hasUI
885
+ ? "Parallel run finished with unrecovered stalled workers after automatic reruns.\n" +
886
+ `${remediation}`
887
+ : "Non-interactive mode: returning partial results with error because " +
888
+ "workers remained stalled after automatic reruns.\n" +
889
+ `${remediation}`;
786
890
  }
787
891
 
788
892
  const batchNote =
@@ -796,6 +900,7 @@ async function executeParallel(
796
900
  `(${counts.completed} completed, ${counts.failed} failed, ${counts.stalled} stalled)`;
797
901
  const summaryText =
798
902
  `${header}${batchNote}\n\n${summaries.join("\n\n")}` +
903
+ (stalledRetrySummary ? `\n\n${stalledRetrySummary}` : "") +
799
904
  (stalledRemediation ? `\n\n${stalledRemediation}` : "");
800
905
 
801
906
  return {
@@ -816,6 +921,7 @@ async function executeSingle(
816
921
  session?: string;
817
922
  model?: string;
818
923
  background?: boolean;
924
+ isolation?: IsolationMode;
819
925
  },
820
926
  ctx: ExtensionContext,
821
927
  agents: Parameters<typeof runSingleAgent>[1],
@@ -845,7 +951,8 @@ async function executeSingle(
845
951
  params.model,
846
952
  parentModelId,
847
953
  defaults,
848
- routingHints
954
+ routingHints,
955
+ params.isolation
849
956
  );
850
957
  if (result?.startsWith("bg_")) {
851
958
  return {
@@ -907,7 +1014,8 @@ async function executeSingle(
907
1014
  params.model,
908
1015
  parentModelId,
909
1016
  defaults,
910
- routingHints
1017
+ routingHints,
1018
+ params.isolation
911
1019
  );
912
1020
 
913
1021
  if (singleSpinnerInterval) {
@@ -1138,6 +1246,63 @@ function countParallelResultStates(results: SingleResult[]): ParallelResultCount
1138
1246
  return counts;
1139
1247
  }
1140
1248
 
1249
+ /**
1250
+ * Retry directive appended to stalled-worker reruns.
1251
+ *
1252
+ * Instructs the worker to aggressively narrow scope so retried tasks
1253
+ * are less likely to deadlock on long/interactive paths.
1254
+ */
1255
+ const STALLED_RETRY_SCOPE_GUIDANCE =
1256
+ "[Automatic retry after stall]\n" +
1257
+ "Your previous parallel attempt stalled. Execute a narrower slice now: " +
1258
+ "complete only the smallest high-confidence chunk you can finish quickly.\n" +
1259
+ "If the original task is already small, complete it fully. " +
1260
+ "If it is broad, finish one concrete slice and list remaining follow-ups.";
1261
+
1262
+ /**
1263
+ * Build the retry task text for a stalled parallel worker.
1264
+ *
1265
+ * @param task - Original worker task
1266
+ * @returns Task text with explicit scope-narrowing retry guidance
1267
+ */
1268
+ function buildStalledRetryTask(task: string): string {
1269
+ const baseTask = task.trim();
1270
+ if (baseTask.length === 0) return STALLED_RETRY_SCOPE_GUIDANCE;
1271
+ return `${baseTask}\n\n${STALLED_RETRY_SCOPE_GUIDANCE}`;
1272
+ }
1273
+
1274
+ /**
1275
+ * Collect indexes of results that match a target parallel state.
1276
+ *
1277
+ * @param results - Parallel result list
1278
+ * @param state - Target state to match
1279
+ * @returns Zero-based indexes for matching rows
1280
+ */
1281
+ function collectParallelStateIndices(
1282
+ results: SingleResult[],
1283
+ state: ParallelResultState
1284
+ ): number[] {
1285
+ const indexes: number[] = [];
1286
+ for (let index = 0; index < results.length; index++) {
1287
+ if (getParallelResultState(results[index]) === state) {
1288
+ indexes.push(index);
1289
+ }
1290
+ }
1291
+ return indexes;
1292
+ }
1293
+
1294
+ /**
1295
+ * Append a note to stderr while preserving existing diagnostics.
1296
+ *
1297
+ * @param stderr - Existing stderr text
1298
+ * @param note - Diagnostic note to append
1299
+ * @returns Combined stderr string
1300
+ */
1301
+ function appendStderrNote(stderr: string, note: string): string {
1302
+ if (!stderr.trim()) return note;
1303
+ return `${stderr}\n${note}`;
1304
+ }
1305
+
1141
1306
  /**
1142
1307
  * Render the tool call header for the subagent tool.
1143
1308
  *
@@ -31,6 +31,7 @@ import {
31
31
  resolveModelFuzzy,
32
32
  selectModels,
33
33
  } from "@dungle-scrubs/synapse";
34
+ import { getTallowPath, getTallowSettingsPath } from "../_shared/tallow-paths.js";
34
35
  import { classifyTask } from "./task-classifier.js";
35
36
 
36
37
  // ─── Types ───────────────────────────────────────────────────────────────────
@@ -412,8 +413,7 @@ function resolveModePolicyOverridesField(
412
413
  * @returns Merged routing config
413
414
  */
414
415
  export function loadRoutingConfig(cwd: string = process.cwd()): RoutingConfig {
415
- const home = process.env.HOME || os.homedir();
416
- const userSettingsPath = path.join(home, ".tallow", "settings.json");
416
+ const userSettingsPath = getTallowSettingsPath();
417
417
  const projectSettingsPath = path.join(cwd, ".tallow", "settings.json");
418
418
 
419
419
  const userRouting = readRawRoutingConfig(userSettingsPath);
@@ -698,7 +698,7 @@ export function loadRoutingSignalsSnapshot(
698
698
  */
699
699
  function getSubscriptionProviders(): string[] {
700
700
  try {
701
- const authPath = path.join(os.homedir(), ".tallow", "auth.json");
701
+ const authPath = getTallowPath("auth.json");
702
702
  const raw = fs.readFileSync(authPath, "utf-8");
703
703
  const data = JSON.parse(raw) as Record<string, { type?: string }>;
704
704
  return Object.entries(data)