@multiclawcli/core 0.7.0 → 0.8.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.
package/dist/index.js CHANGED
@@ -2054,6 +2054,19 @@ var AgentSessionManager = class {
2054
2054
  }
2055
2055
  }
2056
2056
  }
2057
+ /**
2058
+ * Whether this manager still holds a session for `agentId`.
2059
+ *
2060
+ * A caller about to continue a past conversation uses this to avoid naming a
2061
+ * session it already has open: passing the id would change the entry's
2062
+ * fingerprint, and the manager would close a perfectly good session and
2063
+ * `session/load` it straight back.
2064
+ */
2065
+ has(agentId) {
2066
+ if (this.closed) return false;
2067
+ const entry3 = this.entries.get(agentId);
2068
+ return entry3 !== void 0 && !entry3.closing && entry3.session?.state !== "closed";
2069
+ }
2057
2070
  /**
2058
2071
  * Deliver a message into every session that is running a turn right now.
2059
2072
  *
@@ -2735,7 +2748,7 @@ var Orchestrator = class {
2735
2748
  }
2736
2749
  if (options.signal?.aborted)
2737
2750
  return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
2738
- this.clearStaleTaskPlan();
2751
+ this.prepareTaskPlan();
2739
2752
  const resolution = await this.resolveTurn(options.signal);
2740
2753
  if (resolution.kind === "cancelled" || options.signal?.aborted) {
2741
2754
  return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
@@ -2786,7 +2799,7 @@ var Orchestrator = class {
2786
2799
  return this.finishEmpty(startTime, void 0, false);
2787
2800
  }
2788
2801
  if (options.signal?.aborted) return this.finishCancelled(startTime);
2789
- this.clearStaleTaskPlan();
2802
+ this.prepareTaskPlan();
2790
2803
  const resolution = await this.resolveTurn(options.signal);
2791
2804
  if (resolution.kind === "cancelled" || options.signal?.aborted) {
2792
2805
  return this.finishCancelled(startTime);
@@ -2795,6 +2808,7 @@ var Orchestrator = class {
2795
2808
  return this.finishWithMessage(resolution.mode, resolution.message, startTime);
2796
2809
  }
2797
2810
  const agents2 = resolution.agents;
2811
+ if (this.config.resume) this.applyResumeTaskPlan(agents2);
2798
2812
  if (agents2.length === 0)
2799
2813
  return this.finishEmpty(
2800
2814
  startTime,
@@ -3093,14 +3107,20 @@ Address these findings within your assigned scope, preserve the complete task re
3093
3107
  }
3094
3108
  }
3095
3109
  /**
3096
- * Drop last run's task plan before the architect gets a chance to write one.
3110
+ * Decide what to do with last run's task plan before this turn starts.
3097
3111
  *
3098
3112
  * The console runs every turn against the same workspace, so the plan file
3099
3113
  * survives between turns. If a turn's architect fails to write a new one,
3100
3114
  * `applyTaskPlan` would silently re-apply the previous turn's titles, scopes
3101
3115
  * and dependencies to a completely different requirement.
3116
+ *
3117
+ * A resumed turn is the exception: the plan in the workspace describes the
3118
+ * task being continued, and it is the only record of what each resumed agent
3119
+ * was asked to do. Deleting it would leave every retried agent with the
3120
+ * generic "no plan was provided" fallback.
3102
3121
  */
3103
- clearStaleTaskPlan() {
3122
+ prepareTaskPlan() {
3123
+ if (this.config.resume) return;
3104
3124
  fs4.rmSync(path5.join(this.workDir, "task-plan.json"), { force: true });
3105
3125
  }
3106
3126
  /**
@@ -3113,7 +3133,7 @@ Address these findings within your assigned scope, preserve the complete task re
3113
3133
  */
3114
3134
  async resolveTurn(signal) {
3115
3135
  if (signal?.aborted) return { kind: "cancelled" };
3116
- if (this.config.resume) {
3136
+ if (this.config.resume?.agents) {
3117
3137
  return {
3118
3138
  kind: "run",
3119
3139
  agents: this.selectAgents(this.config.resume.agents, "The resume list")
@@ -3153,7 +3173,7 @@ Address these findings within your assigned scope, preserve the complete task re
3153
3173
  * `applyTaskPlan` writes `taskTitle` / `dependsOn` / `agentScope` onto whatever
3154
3174
  * objects it is handed. The console reuses a single config across turns, so
3155
3175
  * returning the config's own objects would leak turn 1's dependency graph into
3156
- * turn 2 — the exact cross-turn contamination `clearStaleTaskPlan` exists to
3176
+ * turn 2 — the exact cross-turn contamination `prepareTaskPlan` exists to
3157
3177
  * prevent, but for the in-memory fields rather than the file.
3158
3178
  */
3159
3179
  configuredAgents() {
@@ -3230,9 +3250,21 @@ Address these findings within your assigned scope, preserve the complete task re
3230
3250
  isPlanner(agent) {
3231
3251
  return agent.role === "planner" || agent.id === "architect";
3232
3252
  }
3233
- async applyTaskPlan(agents2, executedIds) {
3253
+ /**
3254
+ * Read and validate `task-plan.json`, one entry per agent id.
3255
+ *
3256
+ * `undefined` means "there is no usable plan" — no file, unparseable JSON, or
3257
+ * no `tasks` array. Each of those is reported rather than treated as an empty
3258
+ * plan: a broken plan must not be indistinguishable from a missing one.
3259
+ *
3260
+ * Validation stops at "is this entry well-formed and unique". Whether an id
3261
+ * belongs to the run is the caller's question, because a normal turn and a
3262
+ * resume answer it differently — a resume must not warn about every teammate
3263
+ * it deliberately left out.
3264
+ */
3265
+ readTaskPlan() {
3234
3266
  const taskPlanPath = path5.join(this.workDir, "task-plan.json");
3235
- if (!fs4.existsSync(taskPlanPath)) return;
3267
+ if (!fs4.existsSync(taskPlanPath)) return void 0;
3236
3268
  let taskPlan;
3237
3269
  try {
3238
3270
  taskPlan = JSON.parse(fs4.readFileSync(taskPlanPath, "utf-8"));
@@ -3240,14 +3272,13 @@ Address these findings within your assigned scope, preserve the complete task re
3240
3272
  this.warn(
3241
3273
  `Could not parse task-plan.json \u2014 continuing with the original agent set: ${errorMessage2(err)}`
3242
3274
  );
3243
- return;
3275
+ return void 0;
3244
3276
  }
3245
3277
  if (typeof taskPlan !== "object" || taskPlan === null || !("tasks" in taskPlan) || !Array.isArray(taskPlan.tasks)) {
3246
3278
  this.warn("task-plan.json has no `tasks` array \u2014 ignoring it.");
3247
- return;
3279
+ return void 0;
3248
3280
  }
3249
3281
  const taskMap = /* @__PURE__ */ new Map();
3250
- const selected = new Set(agents2.map((a) => a.id));
3251
3282
  for (const [index, entry3] of taskPlan.tasks.entries()) {
3252
3283
  if (typeof entry3 !== "object" || entry3 === null || Array.isArray(entry3)) {
3253
3284
  this.warn(`task-plan.json contains a malformed task at index ${index} \u2014 ignoring it.`);
@@ -3258,18 +3289,23 @@ Address these findings within your assigned scope, preserve the complete task re
3258
3289
  this.warn(`task-plan.json contains a malformed task at index ${index} \u2014 ignoring it.`);
3259
3290
  continue;
3260
3291
  }
3261
- if (!selected.has(task.id)) {
3262
- this.warn(
3263
- `The task plan names "${task.id}", which is not in this run \u2014 ignoring that task.`
3264
- );
3265
- continue;
3266
- }
3267
3292
  if (taskMap.has(task.id)) {
3268
3293
  this.warn(`The task plan contains a duplicate task for "${task.id}" \u2014 ignoring that task.`);
3269
3294
  continue;
3270
3295
  }
3271
3296
  taskMap.set(task.id, task);
3272
3297
  }
3298
+ return taskMap;
3299
+ }
3300
+ async applyTaskPlan(agents2, executedIds) {
3301
+ const taskMap = this.readTaskPlan();
3302
+ if (!taskMap) return;
3303
+ const selected = new Set(agents2.map((a) => a.id));
3304
+ for (const id of taskMap.keys()) {
3305
+ if (!selected.has(id)) {
3306
+ this.warn(`The task plan names "${id}", which is not in this run \u2014 ignoring that task.`);
3307
+ }
3308
+ }
3273
3309
  for (const agent of agents2) {
3274
3310
  const task = taskMap.get(agent.id);
3275
3311
  if (task) {
@@ -3291,6 +3327,27 @@ Address these findings within your assigned scope, preserve the complete task re
3291
3327
  }
3292
3328
  }
3293
3329
  }
3330
+ /**
3331
+ * Hand a resumed agent the plan entry it was originally given.
3332
+ *
3333
+ * `dependsOn` is deliberately not applied: `selectAgents` has already pruned
3334
+ * the stage graph for this run, and re-applying the plan's dependencies would
3335
+ * undo that pruning — including the one that lets a single-agent retry run
3336
+ * without the teammate it depends on.
3337
+ */
3338
+ applyResumeTaskPlan(agents2) {
3339
+ const taskMap = this.readTaskPlan();
3340
+ if (!taskMap) return;
3341
+ for (const agent of agents2) {
3342
+ const task = taskMap.get(agent.id);
3343
+ if (!task) continue;
3344
+ agent.taskTitle = task.title;
3345
+ agent.agentScope = task.scope;
3346
+ if (task.plan?.length) {
3347
+ agent.agentPlan = task.plan.map((s, i) => `${i + 1}. ${s}`).join("\n");
3348
+ }
3349
+ }
3350
+ }
3294
3351
  /**
3295
3352
  * The `requirement` field for a result, present only when the caller set one.
3296
3353
  *
@@ -16,6 +16,15 @@ export declare class AgentSessionManager {
16
16
  private closing?;
17
17
  private closed;
18
18
  execute(runtime: AgentRuntime, task: AgentTask): Promise<AgentOutput>;
19
+ /**
20
+ * Whether this manager still holds a session for `agentId`.
21
+ *
22
+ * A caller about to continue a past conversation uses this to avoid naming a
23
+ * session it already has open: passing the id would change the entry's
24
+ * fingerprint, and the manager would close a perfectly good session and
25
+ * `session/load` it straight back.
26
+ */
27
+ has(agentId: string): boolean;
19
28
  /**
20
29
  * Deliver a message into every session that is running a turn right now.
21
30
  *
@@ -28,14 +28,19 @@ export declare class Orchestrator {
28
28
  */
29
29
  private buildStagesOrFallback;
30
30
  /**
31
- * Drop last run's task plan before the architect gets a chance to write one.
31
+ * Decide what to do with last run's task plan before this turn starts.
32
32
  *
33
33
  * The console runs every turn against the same workspace, so the plan file
34
34
  * survives between turns. If a turn's architect fails to write a new one,
35
35
  * `applyTaskPlan` would silently re-apply the previous turn's titles, scopes
36
36
  * and dependencies to a completely different requirement.
37
+ *
38
+ * A resumed turn is the exception: the plan in the workspace describes the
39
+ * task being continued, and it is the only record of what each resumed agent
40
+ * was asked to do. Deleting it would leave every retried agent with the
41
+ * generic "no plan was provided" fallback.
37
42
  */
38
- private clearStaleTaskPlan;
43
+ private prepareTaskPlan;
39
44
  /**
40
45
  * Decide what this turn is: a pipeline, an answer, or a question.
41
46
  *
@@ -51,7 +56,7 @@ export declare class Orchestrator {
51
56
  * `applyTaskPlan` writes `taskTitle` / `dependsOn` / `agentScope` onto whatever
52
57
  * objects it is handed. The console reuses a single config across turns, so
53
58
  * returning the config's own objects would leak turn 1's dependency graph into
54
- * turn 2 — the exact cross-turn contamination `clearStaleTaskPlan` exists to
59
+ * turn 2 — the exact cross-turn contamination `prepareTaskPlan` exists to
55
60
  * prevent, but for the in-memory fields rather than the file.
56
61
  */
57
62
  private configuredAgents;
@@ -76,7 +81,29 @@ export declare class Orchestrator {
76
81
  * before `role` existed keeps working unchanged.
77
82
  */
78
83
  private isPlanner;
84
+ /**
85
+ * Read and validate `task-plan.json`, one entry per agent id.
86
+ *
87
+ * `undefined` means "there is no usable plan" — no file, unparseable JSON, or
88
+ * no `tasks` array. Each of those is reported rather than treated as an empty
89
+ * plan: a broken plan must not be indistinguishable from a missing one.
90
+ *
91
+ * Validation stops at "is this entry well-formed and unique". Whether an id
92
+ * belongs to the run is the caller's question, because a normal turn and a
93
+ * resume answer it differently — a resume must not warn about every teammate
94
+ * it deliberately left out.
95
+ */
96
+ private readTaskPlan;
79
97
  private applyTaskPlan;
98
+ /**
99
+ * Hand a resumed agent the plan entry it was originally given.
100
+ *
101
+ * `dependsOn` is deliberately not applied: `selectAgents` has already pruned
102
+ * the stage graph for this run, and re-applying the plan's dependencies would
103
+ * undo that pruning — including the one that lets a single-agent retry run
104
+ * without the teammate it depends on.
105
+ */
106
+ private applyResumeTaskPlan;
80
107
  /**
81
108
  * The `requirement` field for a result, present only when the caller set one.
82
109
  *
@@ -66,17 +66,26 @@ export interface MultiClawConfig {
66
66
  */
67
67
  history?: TurnRecord[];
68
68
  /**
69
- * Re-run a known set of agents instead of letting the leader route, continuing
70
- * their sessions where one is known.
69
+ * Continue known agent sessions, and optionally re-run a fixed set of agents
70
+ * instead of letting the leader route.
71
71
  *
72
- * Set by a resume/retry driver — the CLI's `--resume`, the console's `/retry` —
73
- * which already knows which agents to redo. Like `history`, it is per-run state
74
- * the caller carries: core has no session of its own, and a config that never
75
- * resumes leaves it unset and behaves exactly as before.
72
+ * Two shapes, and the difference is who decides what runs:
73
+ *
74
+ * - `agents` set — the CLI's `--resume`, the console's `/retry`: exactly those
75
+ * agents run, in that order. A `dependsOn` outside the set is pruned, which
76
+ * is what lets a single-agent retry run without its absent dependency.
77
+ * - `agents` absent — the console continuing an earlier session: the turn
78
+ * routes as usual (leader, or the whole team) and the sessions are there for
79
+ * whichever agents the route happens to pick. This is what makes a new
80
+ * instruction land in a conversation the agents already remember.
81
+ *
82
+ * Like `history`, it is per-run state the caller carries: core has no session
83
+ * of its own, and a config that never resumes leaves it unset and behaves
84
+ * exactly as before.
76
85
  */
77
86
  resume?: {
78
87
  /** Agent ids to run, in the order they were originally selected. */
79
- agents: string[];
88
+ agents?: string[];
80
89
  /** agentId → session id, for agents whose session is worth continuing. */
81
90
  sessions?: Record<string, string>;
82
91
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiclawcli/core",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",