taskplane 0.10.2 → 0.12.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/README.md CHANGED
@@ -129,7 +129,7 @@ For a single task with full worktree isolation, dashboard, and reviews:
129
129
 
130
130
  This uses the same orchestrator infrastructure as a full batch — isolated worktree, orch branch, supervisor, dashboard, inline reviews — but for just one task.
131
131
 
132
- > **Note:** The `/task` command still exists for direct single-task execution in the current branch, but `/orch` is recommended for all workflows. `/task` does not provide worktree isolation, dashboard, or inline reviews.
132
+ > **Deprecated:** The `/task` command is deprecated and will be removed in a future major version. It does not provide worktree isolation, dashboard, or inline reviews. Use `/orch` for all workflows — including single-task execution.
133
133
 
134
134
  ## Commands
135
135
 
@@ -137,10 +137,10 @@ This uses the same orchestrator infrastructure as a full batch — isolated work
137
137
 
138
138
  | Command | Description |
139
139
  |---------|-------------|
140
- | `/task <path/to/PROMPT.md>` | Execute one task in the current branch/worktree |
141
- | `/task-status` | Show current task progress |
142
- | `/task-pause` | Pause after current worker iteration finishes |
143
- | `/task-resume` | Resume a paused task |
140
+ | `/task <path/to/PROMPT.md>` | ⚠️ **Deprecated.** Execute one task in the current branch/worktree. Use `/orch` instead. |
141
+ | `/task-status` | ⚠️ **Deprecated.** Show current task progress. Use `/orch-status` or dashboard. |
142
+ | `/task-pause` | ⚠️ **Deprecated.** Pause after current worker iteration finishes. Use `/orch-pause`. |
143
+ | `/task-resume` | ⚠️ **Deprecated.** Resume a paused task. Use `/orch-resume`. |
144
144
  | `/orch [<areas\|paths\|all>]` | No args: detect state & guide (onboarding, batch planning, etc.); with args: execute tasks via isolated worktrees |
145
145
  | `/orch-plan <areas\|paths\|all>` | Preview execution plan without running |
146
146
  | `/orch-status` | Show batch progress |
@@ -437,6 +437,10 @@ function loadTelemetryData(batchState) {
437
437
  for (const event of events) {
438
438
  switch (event.type) {
439
439
  case "message_end": {
440
+ // A successful message_end means any prior retry resolved.
441
+ // Clear retryActive to prevent stale retry badges from persisting
442
+ // across batches or after transient API errors recover.
443
+ acc.retryActive = false;
440
444
  const usage = event.message?.usage;
441
445
  if (usage) {
442
446
  acc.inputTokens += usage.input || 0;
@@ -2098,9 +2098,13 @@ export default function (pi: ExtensionAPI) {
2098
2098
 
2099
2099
  // Load reviewer agent definition
2100
2100
  const reviewerDef = loadAgentDef(ctx.cwd, "task-reviewer");
2101
- const reviewerModel = config.reviewer.model
2102
- || reviewerDef?.model
2103
- || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
2101
+ // TP-055: model fallback — use session model when TASKPLANE_MODEL_FALLBACK=1
2102
+ const reviewerModelFallback = process.env.TASKPLANE_MODEL_FALLBACK === "1";
2103
+ const reviewerModel = reviewerModelFallback
2104
+ ? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514")
2105
+ : (config.reviewer.model
2106
+ || reviewerDef?.model
2107
+ || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514"));
2104
2108
  const reviewerPrompt = reviewerDef?.systemPrompt
2105
2109
  || "You are a code reviewer. Read the request and write your review to the specified output file.";
2106
2110
  const systemPrompt = reviewerPrompt + "\n\n" + buildProjectContext(config, task.taskFolder);
@@ -2585,9 +2589,15 @@ export default function (pi: ExtensionAPI) {
2585
2589
  const basePrompt = workerDef?.systemPrompt || "You are a task execution agent. Read STATUS.md first, find unchecked items, work on them, checkpoint after each.";
2586
2590
  const systemPrompt = basePrompt + "\n\n" + buildProjectContext(config, task.taskFolder);
2587
2591
 
2588
- const model = config.worker.model
2589
- || workerDef?.model
2590
- || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
2592
+ // TP-055: When TASKPLANE_MODEL_FALLBACK=1 is set, skip configured model
2593
+ // and fall back to the session model. This is set by the orchestrator's
2594
+ // model fallback retry when the configured model becomes unavailable.
2595
+ const modelFallbackActive = process.env.TASKPLANE_MODEL_FALLBACK === "1";
2596
+ const model = modelFallbackActive
2597
+ ? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514")
2598
+ : (config.worker.model
2599
+ || workerDef?.model
2600
+ || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514"));
2591
2601
 
2592
2602
  const contextDocsList = task.contextDocs.length > 0
2593
2603
  ? "\n\nContext docs to read if needed:\n" + task.contextDocs.map(d => `- ${d}`).join("\n")
@@ -2904,7 +2914,11 @@ export default function (pi: ExtensionAPI) {
2904
2914
  writeFileSync(requestPath, request);
2905
2915
 
2906
2916
  const reviewerDef = loadAgentDef(ctx.cwd, "task-reviewer");
2907
- const reviewerModel = config.reviewer.model || reviewerDef?.model || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
2917
+ // TP-055: model fallback use session model when TASKPLANE_MODEL_FALLBACK=1
2918
+ const reviewerModelFallback2 = process.env.TASKPLANE_MODEL_FALLBACK === "1";
2919
+ const reviewerModel = reviewerModelFallback2
2920
+ ? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514")
2921
+ : (config.reviewer.model || reviewerDef?.model || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514"));
2908
2922
  const reviewerPrompt = reviewerDef?.systemPrompt || "You are a code reviewer. Read the request and write your review to the specified output file.";
2909
2923
  const systemPrompt = reviewerPrompt + "\n\n" + buildProjectContext(config, task.taskFolder);
2910
2924
 
@@ -3040,10 +3054,14 @@ export default function (pi: ExtensionAPI) {
3040
3054
  // Determine review model with fallback chain:
3041
3055
  // quality_gate.review_model → reviewer.model → agent def → default
3042
3056
  const reviewerDef = loadAgentDef(ctx.cwd, "task-reviewer");
3043
- const reviewModel = config.quality_gate.review_model
3044
- || config.reviewer.model
3045
- || reviewerDef?.model
3046
- || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
3057
+ // TP-055: model fallback — use session model when TASKPLANE_MODEL_FALLBACK=1
3058
+ const qgModelFallback = process.env.TASKPLANE_MODEL_FALLBACK === "1";
3059
+ const reviewModel = qgModelFallback
3060
+ ? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514")
3061
+ : (config.quality_gate.review_model
3062
+ || config.reviewer.model
3063
+ || reviewerDef?.model
3064
+ || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514"));
3047
3065
 
3048
3066
  const reviewerPrompt = reviewerDef?.systemPrompt
3049
3067
  || "You are a quality gate reviewer. Read the review request and write your JSON verdict to the specified file.";
@@ -3210,9 +3228,13 @@ export default function (pi: ExtensionAPI) {
3210
3228
 
3211
3229
  // Use worker model and tools for fix agent (it needs to edit code)
3212
3230
  const workerDef = loadAgentDef(ctx.cwd, "task-worker");
3213
- const fixModel = config.worker.model
3214
- || workerDef?.model
3215
- || "anthropic/claude-sonnet-4-20250514";
3231
+ // TP-055: model fallback — use session model when TASKPLANE_MODEL_FALLBACK=1
3232
+ const fixModelFallback = process.env.TASKPLANE_MODEL_FALLBACK === "1";
3233
+ const fixModel = fixModelFallback
3234
+ ? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514")
3235
+ : (config.worker.model
3236
+ || workerDef?.model
3237
+ || "anthropic/claude-sonnet-4-20250514");
3216
3238
 
3217
3239
  const basePrompt = workerDef?.systemPrompt
3218
3240
  || "You are a fix agent addressing quality gate findings. Read the feedback and make targeted code fixes.";
@@ -3405,9 +3427,15 @@ export default function (pi: ExtensionAPI) {
3405
3427
  }
3406
3428
 
3407
3429
  pi.registerCommand("task", {
3408
- description: "Start executing a task: /task <path/to/PROMPT.md>",
3430
+ description: "⚠️ [Deprecated] Start executing a task: /task <path/to/PROMPT.md>",
3409
3431
  handler: async (args, ctx) => {
3410
3432
  widgetCtx = ctx;
3433
+ ctx.ui.notify(
3434
+ "⚠️ /task is deprecated. Use /orch instead — it provides worktree isolation, " +
3435
+ "dashboard, inline reviews, and supervisor monitoring. " +
3436
+ "/task will be removed in a future major version.",
3437
+ "warning",
3438
+ );
3411
3439
  const promptPath = args?.trim();
3412
3440
  if (!promptPath) {
3413
3441
  ctx.ui.notify("Usage: /task <path/to/PROMPT.md>", "error");
@@ -3425,9 +3453,13 @@ export default function (pi: ExtensionAPI) {
3425
3453
  });
3426
3454
 
3427
3455
  pi.registerCommand("task-status", {
3428
- description: "Show current task progress",
3456
+ description: "⚠️ [Deprecated] Show current task progress",
3429
3457
  handler: async (_args, ctx) => {
3430
3458
  widgetCtx = ctx;
3459
+ ctx.ui.notify(
3460
+ "⚠️ /task-status is deprecated. Use the dashboard (`taskplane dashboard`) or `/orch-status` instead.",
3461
+ "warning",
3462
+ );
3431
3463
  if (!state.task) {
3432
3464
  ctx.ui.notify("No task loaded. Use /task <path/to/PROMPT.md>", "info");
3433
3465
  return;
@@ -3459,9 +3491,13 @@ export default function (pi: ExtensionAPI) {
3459
3491
  });
3460
3492
 
3461
3493
  pi.registerCommand("task-pause", {
3462
- description: "Pause task after current worker finishes",
3494
+ description: "⚠️ [Deprecated] Pause task after current worker finishes",
3463
3495
  handler: async (_args, ctx) => {
3464
3496
  widgetCtx = ctx;
3497
+ ctx.ui.notify(
3498
+ "⚠️ /task-pause is deprecated. Use `/orch-pause` instead.",
3499
+ "warning",
3500
+ );
3465
3501
  if (state.phase !== "running") {
3466
3502
  ctx.ui.notify("No task is running", "warning");
3467
3503
  return;
@@ -3473,9 +3509,13 @@ export default function (pi: ExtensionAPI) {
3473
3509
  });
3474
3510
 
3475
3511
  pi.registerCommand("task-resume", {
3476
- description: "Resume a paused task",
3512
+ description: "⚠️ [Deprecated] Resume a paused task",
3477
3513
  handler: async (_args, ctx) => {
3478
3514
  widgetCtx = ctx;
3515
+ ctx.ui.notify(
3516
+ "⚠️ /task-resume is deprecated. Use `/orch-resume` instead.",
3517
+ "warning",
3518
+ );
3479
3519
  if (state.phase !== "paused") {
3480
3520
  ctx.ui.notify("Task is not paused", "warning");
3481
3521
  return;
@@ -222,6 +222,9 @@ function mapTaskRunnerYaml(raw: any): Partial<TaskRunnerSection> {
222
222
  // Quality gate (structural — all keys are schema-defined)
223
223
  if (raw.quality_gate) result.qualityGate = convertStructuralKeys(raw.quality_gate);
224
224
 
225
+ // Model fallback (scalar — "inherit" or "fail")
226
+ if (raw.model_fallback) result.modelFallback = raw.model_fallback;
227
+
225
228
  return result;
226
229
  }
227
230
 
@@ -814,6 +817,7 @@ export function toTaskRunnerConfig(config: TaskplaneConfig): import("./types.ts"
814
817
  task_areas: taskAreas,
815
818
  reference_docs: { ...config.taskRunner.referenceDocs },
816
819
  ...(hasTestingCommands ? { testing_commands: { ...testingCommands } } : {}),
820
+ model_fallback: config.taskRunner.modelFallback ?? "inherit",
817
821
  };
818
822
  }
819
823
 
@@ -170,6 +170,19 @@ export interface SelfDocTarget {
170
170
  */
171
171
  export type PassThreshold = "no_critical" | "no_important" | "all_clear";
172
172
 
173
+ /**
174
+ * Model fallback behavior when a configured agent model becomes unavailable mid-batch.
175
+ *
176
+ * - `"inherit"`: Fall back to the session model and retry (default). The task is
177
+ * retried without an explicit --model flag, so pi uses whatever model the
178
+ * session is configured with.
179
+ * - `"fail"`: Fail immediately — the normal failure/retry path handles the error
180
+ * without any model substitution.
181
+ *
182
+ * @since TP-055
183
+ */
184
+ export type ModelFallbackMode = "inherit" | "fail";
185
+
173
186
  /** Quality gate configuration — opt-in post-completion review */
174
187
  export interface QualityGateConfig {
175
188
  /** Enable quality gate review before .DONE creation (default: false) */
@@ -222,6 +235,16 @@ export interface TaskRunnerSection {
222
235
  protectedDocs: string[];
223
236
  /** Quality gate configuration — opt-in post-completion review */
224
237
  qualityGate: QualityGateConfig;
238
+ /**
239
+ * Model fallback behavior when a configured model becomes unavailable mid-batch.
240
+ *
241
+ * - `"inherit"` (default): Retry the task without an explicit model flag,
242
+ * falling back to the session model.
243
+ * - `"fail"`: Fail immediately without model substitution.
244
+ *
245
+ * @since TP-055
246
+ */
247
+ modelFallback: ModelFallbackMode;
225
248
  }
226
249
 
227
250
 
@@ -515,6 +538,7 @@ export const DEFAULT_TASK_RUNNER_SECTION: TaskRunnerSection = {
515
538
  maxFixCycles: 1,
516
539
  passThreshold: "no_critical",
517
540
  },
541
+ modelFallback: "inherit",
518
542
  };
519
543
 
520
544
  /** Default orchestrator section values */
@@ -45,6 +45,7 @@ export interface SessionTokenCounts {
45
45
  * |----------------------|------------------------------------------------------|
46
46
  * | `completed` | `.DONE` file found — task finished successfully |
47
47
  * | `api_error` | API returned error (auth, rate limit, overload) |
48
+ * | `model_access_error` | Model unavailable (401/403/429, model not found) |
48
49
  * | `context_overflow` | Hit context window limit (compactions + high ctx %) |
49
50
  * | `wall_clock_timeout` | Killed by task-runner's max_worker_minutes timer |
50
51
  * | `process_crash` | Non-zero exit code with no API error indicators |
@@ -56,6 +57,7 @@ export interface SessionTokenCounts {
56
57
  export type ExitClassification =
57
58
  | "completed"
58
59
  | "api_error"
60
+ | "model_access_error"
59
61
  | "context_overflow"
60
62
  | "wall_clock_timeout"
61
63
  | "process_crash"
@@ -70,6 +72,7 @@ export type ExitClassification =
70
72
  export const EXIT_CLASSIFICATIONS: readonly ExitClassification[] = [
71
73
  "completed",
72
74
  "api_error",
75
+ "model_access_error",
73
76
  "context_overflow",
74
77
  "wall_clock_timeout",
75
78
  "process_crash",
@@ -223,6 +226,51 @@ export interface TaskExitDiagnostic {
223
226
  */
224
227
  export const CONTEXT_OVERFLOW_THRESHOLD_PCT = 90;
225
228
 
229
+ /**
230
+ * Patterns that indicate a model access error (as opposed to a generic API error).
231
+ *
232
+ * These patterns match error messages from API providers when:
233
+ * - The model is not found or deprecated
234
+ * - Authentication/authorization fails (HTTP 401/403)
235
+ * - Rate limits are hit specifically for the model (HTTP 429)
236
+ * - API key is expired or invalid
237
+ *
238
+ * The patterns are case-insensitive and tested against the error string.
239
+ *
240
+ * @since TP-055
241
+ */
242
+ export const MODEL_ACCESS_ERROR_PATTERNS: readonly RegExp[] = [
243
+ /\b(?:401|403)\b/, // HTTP auth/forbidden status codes
244
+ /\b429\b/, // HTTP rate limit
245
+ /model[_ ]not[_ ]found/i, // Model not found
246
+ /model[_ ](?:is[_ ])?unavailable/i, // Model unavailable
247
+ /model[_ ](?:has[_ ]been[_ ])?deprecated/i, // Model deprecated
248
+ /api[_ ]key[_ ](?:expired|invalid|revoked)/i, // API key issues
249
+ /invalid[_ ]api[_ ]key/i, // Invalid API key (alternate phrasing)
250
+ /authentication[_ ](?:failed|error|required)/i, // Auth failures
251
+ /authorization[_ ](?:failed|error|denied)/i, // Authz failures
252
+ /access[_ ]denied/i, // Generic access denied
253
+ /permission[_ ]denied/i, // Permission denied
254
+ /quota[_ ]exceeded/i, // Quota exceeded
255
+ /rate[_ ]limit/i, // Rate limit (phrase)
256
+ /insufficient[_ ]quota/i, // Insufficient quota
257
+ ];
258
+
259
+ /**
260
+ * Test whether an error message indicates a model access error.
261
+ *
262
+ * Used by `classifyExit()` to distinguish model-specific failures from
263
+ * generic API errors, enabling targeted fallback to the session model.
264
+ *
265
+ * @param errorMessage - Error message to test
266
+ * @returns true if the error matches a model access pattern
267
+ * @since TP-055
268
+ */
269
+ export function isModelAccessError(errorMessage: string): boolean {
270
+ if (!errorMessage) return false;
271
+ return MODEL_ACCESS_ERROR_PATTERNS.some(pattern => pattern.test(errorMessage));
272
+ }
273
+
226
274
  /**
227
275
  * Classify a task session's exit into a deterministic category.
228
276
  *
@@ -232,22 +280,26 @@ export const CONTEXT_OVERFLOW_THRESHOLD_PCT = 90;
232
280
  *
233
281
  * **Classification precedence (highest → lowest):**
234
282
  *
235
- * | Priority | Condition | Result |
236
- * |----------|------------------------------------------------------|---------------------|
237
- * | 1 | `.DONE` file found | `completed` |
238
- * | 2 | Retries present with final retry failed | `api_error` |
239
- * | 3 | Compactions > 0 AND contextPct ≥ 90% | `context_overflow` |
240
- * | 3b | Task-runner explicitly context-killed | `context_overflow` |
241
- * | 4 | Timer killed the session | `wall_clock_timeout`|
242
- * | 5 | Non-zero exit code, no API error | `process_crash` |
243
- * | 6 | No exit summary file (session vanished) | `session_vanished` |
244
- * | 7 | Stall detected (no STATUS.md progress) | `stall_timeout` |
245
- * | 8 | User manually killed the session | `user_killed` |
246
- * | 9 | None of the above | `unknown` |
283
+ * | Priority | Condition | Result |
284
+ * |----------|------------------------------------------------------|----------------------|
285
+ * | 1 | `.DONE` file found | `completed` |
286
+ * | 2a | Retries with model-access error pattern | `model_access_error` |
287
+ * | 2b | Retries present with final retry failed | `api_error` |
288
+ * | 2c | Error message has model-access pattern (no retries) | `model_access_error` |
289
+ * | 3 | Compactions > 0 AND contextPct ≥ 90% | `context_overflow` |
290
+ * | 3b | Task-runner explicitly context-killed | `context_overflow` |
291
+ * | 4 | Timer killed the session | `wall_clock_timeout` |
292
+ * | 5 | Non-zero exit code, no API error | `process_crash` |
293
+ * | 6 | No exit summary file (session vanished) | `session_vanished` |
294
+ * | 7 | Stall detected (no STATUS.md progress) | `stall_timeout` |
295
+ * | 8 | User manually killed the session | `user_killed` |
296
+ * | 9 | None of the above | `unknown` |
247
297
  *
248
298
  * **Tie-break rationale:**
249
299
  * - `.DONE` always wins because the task succeeded regardless of how messy
250
300
  * the session was (retries, compactions, etc.).
301
+ * - `model_access_error` beats generic `api_error` because it's more specific
302
+ * and enables targeted fallback (retry with session model).
251
303
  * - `api_error` beats `context_overflow` because API failures are more
252
304
  * actionable (auth fix, rate limit backoff).
253
305
  * - `wall_clock_timeout` beats `process_crash` because the timer kill
@@ -269,14 +321,24 @@ export function classifyExit(input: ExitClassificationInput): ExitClassification
269
321
  return "completed";
270
322
  }
271
323
 
272
- // 2. Retries present with final retry failedapi_error
324
+ // 2a. Retries present with model-access error patternmodel_access_error
325
+ // 2b. Retries present with final retry failed → api_error
273
326
  if (exitSummary?.retries && exitSummary.retries.length > 0) {
274
327
  const lastRetry = exitSummary.retries[exitSummary.retries.length - 1];
275
328
  if (!lastRetry.succeeded) {
329
+ // Check if the retry error indicates a model access issue
330
+ if (isModelAccessError(lastRetry.error)) {
331
+ return "model_access_error";
332
+ }
276
333
  return "api_error";
277
334
  }
278
335
  }
279
336
 
337
+ // 2c. Error message (no retries) indicates model access issue → model_access_error
338
+ if (exitSummary?.error && isModelAccessError(exitSummary.error)) {
339
+ return "model_access_error";
340
+ }
341
+
280
342
  // 3. Compactions > 0 AND high context utilization → context_overflow
281
343
  if (exitSummary && exitSummary.compactions > 0) {
282
344
  const effectivePct = contextPct ?? 0;