@ordewell/core 0.10.1 → 0.10.3

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 (45) hide show
  1. package/dist/{IFileSystem-Bj6fy1cG.d.mts → IFileSystem-B8Nkgg4e.d.mts} +1 -1
  2. package/dist/{IFileSystem-BCJBHFwB.d.ts → IFileSystem-DCd-rkeH.d.ts} +1 -1
  3. package/dist/{IRunner-hWAJC4hg.d.mts → IRunner-CXdYyIZo.d.mts} +213 -202
  4. package/dist/{IRunner-C7_SSpzG.d.ts → IRunner-QBdYcatb.d.ts} +213 -202
  5. package/dist/{ModeResolver-m1d8Cb-j.d.mts → ModeResolver-BOU90Nn-.d.ts} +1 -1
  6. package/dist/{ModeResolver-u5ZhRC8x.d.ts → ModeResolver-geqjKkLY.d.mts} +1 -1
  7. package/dist/{Task-CxwnCJLi.d.mts → Task-CRVIqSfy.d.mts} +58 -11
  8. package/dist/{Task-CxwnCJLi.d.ts → Task-CRVIqSfy.d.ts} +58 -11
  9. package/dist/{chunk-DNFT4SQG.mjs → chunk-645FALZX.mjs} +5 -5
  10. package/dist/chunk-645FALZX.mjs.map +1 -0
  11. package/dist/{chunk-DJAPDO6E.mjs → chunk-A57ITQE7.mjs} +41 -21
  12. package/dist/chunk-A57ITQE7.mjs.map +1 -0
  13. package/dist/{chunk-CH5IGWFS.mjs → chunk-FH6M5QZG.mjs} +168 -122
  14. package/dist/chunk-FH6M5QZG.mjs.map +1 -0
  15. package/dist/{chunk-2IIRSBEQ.mjs → chunk-SM6BV6XS.mjs} +20 -4
  16. package/dist/chunk-SM6BV6XS.mjs.map +1 -0
  17. package/dist/index.d.mts +454 -63
  18. package/dist/index.d.ts +454 -63
  19. package/dist/index.js +19575 -18631
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +17991 -17137
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/parsing.d.mts +2 -2
  24. package/dist/parsing.d.ts +2 -2
  25. package/dist/parsing.js +15 -14
  26. package/dist/parsing.js.map +1 -1
  27. package/dist/parsing.mjs +3 -3
  28. package/dist/{plan-utils-CKXdD1AD.d.mts → plan-utils-KU1Rse82.d.mts} +1322 -1146
  29. package/dist/{plan-utils-B6yJdpgi.d.ts → plan-utils-oC8bO4pN.d.ts} +1322 -1146
  30. package/dist/plan-utils.d.mts +4 -4
  31. package/dist/plan-utils.d.ts +4 -4
  32. package/dist/plan-utils.js +60 -0
  33. package/dist/plan-utils.js.map +1 -1
  34. package/dist/plan-utils.mjs +11 -3
  35. package/dist/testing.d.mts +3 -3
  36. package/dist/testing.d.ts +3 -3
  37. package/dist/testing.js +3 -1
  38. package/dist/testing.js.map +1 -1
  39. package/dist/testing.mjs +4 -2
  40. package/dist/testing.mjs.map +1 -1
  41. package/package.json +5 -1
  42. package/dist/chunk-2IIRSBEQ.mjs.map +0 -1
  43. package/dist/chunk-CH5IGWFS.mjs.map +0 -1
  44. package/dist/chunk-DJAPDO6E.mjs.map +0 -1
  45. package/dist/chunk-DNFT4SQG.mjs.map +0 -1
@@ -330,8 +330,26 @@ interface PreparedTask {
330
330
  setupFailed?: SetupFailure[];
331
331
  /** What the attempt this one replaces left unlanded, kept on a branch before its worktree went; absent when nothing was. */
332
332
  preserved?: PreservedWork[];
333
- continuation?: 'restored' | 'fallback';
333
+ /** Set for a `restore` prepare: whether the previous attempt's work was put back, or every repo fell back to its tip. */
334
+ restoration?: 'restored' | 'fallback';
334
335
  }
336
+ /**
337
+ * How a task workspace starts: `fresh` from the integration tips, `restore`
338
+ * with the previous attempt's preserved work merged on top of them — for a
339
+ * continue (ADR-0018, K1) and for a carry-over to another runner.
340
+ */
341
+ type PrepareIntent = 'fresh' | 'restore';
342
+ /**
343
+ * What a restoring attempt is told became of the previous attempt's work: in
344
+ * its working tree, or kept on `preserved` branches because restoring it
345
+ * conflicted with the integration tips.
346
+ */
347
+ type PreviousWork = {
348
+ outcome: 'restored';
349
+ } | {
350
+ outcome: 'fallback';
351
+ preserved: readonly PreservedWork[];
352
+ };
335
353
  /** A task as a removal names it. */
336
354
  type IsolationTaskRef = Pick<IsolationTaskRecord, 'taskId' | 'order' | 'title'>;
337
355
  /**
@@ -400,11 +418,11 @@ interface IWorktreeIsolation {
400
418
  /**
401
419
  * Create the task workspace — one worktree per isolated repo from its
402
420
  * integration tip, the shared paths linked in — and return the cwd to spawn
403
- * the Runner into. Fresh attempts start clean from the tips. A continuation
404
- * restores the previous attempt's preserved work on top of them, falling
405
- * back to the tips in every repo when any restoration conflicts (ADR-0018).
421
+ * the Runner into. Fresh attempts start clean from the tips. A `restore`
422
+ * puts the previous attempt's preserved work on top of them, falling back
423
+ * to the tips in every repo when any restoration conflicts (ADR-0018).
406
424
  */
407
- prepare(task: Task, run: IsolationRun, intent?: 'fresh' | 'continuation'): Promise<PreparedTask>;
425
+ prepare(task: Task, run: IsolationRun, intent?: PrepareIntent): Promise<PreparedTask>;
408
426
  /**
409
427
  * Hand a conflicted task's kept workspace to a conflict repair (ADR-0015)
410
428
  * as it is — nothing is re-cut — and return the same cwd. Records each
@@ -914,7 +932,8 @@ interface TaskOutputSummary {
914
932
  type TaskType = 'ai' | 'user';
915
933
  /**
916
934
  * `skipped` is terminal and verdict-less: the task will not be waited for, and
917
- * nothing it did lands. Its dependents go ahead without it.
935
+ * nothing it did lands. Its dependents go ahead without it once the work it
936
+ * waited on is done.
918
937
  */
919
938
  type TaskStatus = 'pending' | 'approved' | 'in_progress' | 'completed' | 'failed' | 'blocked' | 'awaiting_user' | 'skipped';
920
939
  type TaskMode = string;
@@ -983,6 +1002,12 @@ interface Task {
983
1002
  * waits for a manual retry. Set only with `awaitingReason` `usage-limit`.
984
1003
  */
985
1004
  resumeAt?: string;
1005
+ /**
1006
+ * The runner a task paused on its usage limit was moved off (ADR-0018, U1):
1007
+ * its next attempt is a carry-over, a fresh session that starts from that
1008
+ * attempt's files. Cleared once that attempt runs, and by a retry.
1009
+ */
1010
+ carriedFrom?: RunnerId;
986
1011
  /** Why the task was skipped, as whoever skipped it put it; set only while `status` is `skipped`, and not always then. */
987
1012
  skipReason?: string;
988
1013
  /**
@@ -1274,9 +1299,17 @@ interface LegacyPlanState {
1274
1299
  * The tasks the user approved to run: every task when they executed the
1275
1300
  * plan, and each one they started or retried themselves since. A command
1276
1301
  * from anyone else starts only these (ADR-0029); a task the planner added
1277
- * after the approval waits for the user's.
1302
+ * after the approval waits for the user's. Only tasks still in the plan: a
1303
+ * task that leaves takes its approval with it.
1278
1304
  */
1279
1305
  approvedTasks?: string[];
1306
+ /**
1307
+ * The user approved the plan to run: Execute plan, signing off a review, or
1308
+ * autorun's start (ADR-0029, ADR-0030). Kept apart from {@link status},
1309
+ * which surfaces rewrite for display; a user's start of one task approves
1310
+ * that task, not the plan.
1311
+ */
1312
+ approved?: boolean;
1280
1313
  }
1281
1314
  interface Message {
1282
1315
  id: string;
@@ -1290,7 +1323,8 @@ interface TaskSnapshot extends Task {
1290
1323
  retryCount: number;
1291
1324
  finalized: boolean;
1292
1325
  }
1293
- type PlanState = {
1326
+ /** The plan a surface reads whole carries the approval record, so it can tell which tasks wait for the user (ADR-0035). */
1327
+ type PlanState = ({
1294
1328
  phase: 'planning';
1295
1329
  history: Message[];
1296
1330
  message: string;
@@ -1304,11 +1338,15 @@ type PlanState = {
1304
1338
  goal: string;
1305
1339
  runners: string[];
1306
1340
  status: PlanStatus;
1307
- };
1341
+ }) & ApprovalRecord;
1308
1342
  declare function migratePlanState(raw: unknown): PlanState;
1309
1343
 
1310
1344
  declare function createTask(overrides?: Partial<Task>): Task;
1311
- /** The runner set a plan has until one is chosen: the one runner every host ships. */
1345
+ /**
1346
+ * The runner set a plan has until one is chosen. The one runner named outside
1347
+ * its declaration on purpose: a default is a product choice that no manifest
1348
+ * or connector implies.
1349
+ */
1312
1350
  declare const DEFAULT_RUNNERS: readonly RunnerId[];
1313
1351
  declare function createEmptyPlan(): LegacyPlanState;
1314
1352
  declare function flattenTasks(tasks: readonly Task[]): Task[];
@@ -1337,5 +1375,14 @@ interface TaskWithParent {
1337
1375
  declare function flattenTasksWithParents(tasks: readonly Task[]): TaskWithParent[];
1338
1376
  /** A task paused on its runner's usage limit, whether it resumes on its own or waits for the user (#84). */
1339
1377
  declare function pausedOnLimit(task: Readonly<Task>): boolean;
1378
+ /** The user's approval record, as a plan carries it (ADR-0029). */
1379
+ type ApprovalRecord = Pick<LegacyPlanState, 'approved' | 'approvedTasks'>;
1380
+ /**
1381
+ * A task the scheduler leaves for the user (ADR-0035): an AI task not started
1382
+ * yet, in a plan they approved, outside their approval record — the planner
1383
+ * added it since. It starts once they start it or execute the plan. In a plan
1384
+ * they never approved nothing is theirs yet, so nothing is singled out.
1385
+ */
1386
+ declare function waitsForApproval(task: Pick<Task, 'id' | 'type' | 'status'>, record: ApprovalRecord): boolean;
1340
1387
 
1341
- export { type RunnerId as $, type AttemptUsage as A, BUILTIN_SKILL_NAMES as B, type ConversationMessage as C, DEFAULT_RUNNERS as D, type ExecutionActor as E, type IsolationView as F, type GuardTarget as G, type PlanIsolation as H, type IWorktreeIsolation as I, type PlanState as J, type PlanStatus as K, type LegacyPlanState as L, type ModelUsage as M, type PlanUsage as N, type PlannerUsage as O, type PendingPlanTurn as P, type PreparedTask as Q, type PreservedWork as R, type RefusedRemoval as S, type RepairEvidence as T, type RepoGroupLayout as U, type ResearchLogEntry as V, type ResearchProgress as W, type ResearchStep as X, type ResearchStepOutcome as Y, type ResearchToolType as Z, type RunSettings as _, type AwaitingReason as a, type SetupFailure as a0, type ShadowedSkill as a1, type SkillAppliesTo as a2, type SkillCatalog as a3, type SkillInfo as a4, type SkillLoad as a5, type SkillLoadNotice as a6, type SkillMetadata as a7, type SkillSource as a8, SkillsService as a9, endAttemptUsage as aA, flattenTasks as aB, flattenTasksWithParents as aC, isAwaitingReason as aD, isMeasured as aE, isSupervisorLevel as aF, isUserMessage as aG, migratePlanState as aH, partedPromptUsage as aI, pausedOnLimit as aJ, planUsage as aK, plannerContextFill as aL, rejoinPlanTree as aM, startAttemptUsage as aN, usageLine as aO, type SubagentOutcome as aa, type SupervisorLevel as ab, type Task as ac, type TaskDiffRead as ad, type TaskDiffResult as ae, type TaskIsolation as af, type TaskIsolationState as ag, type TaskModelAssignment as ah, type TaskSkillNotice as ai, type TaskSkillSnapshot as aj, type TaskSnapshot as ak, type TaskStatus as al, type TreeSnapshot as am, type UsageLine as an, type UsageRecord as ao, type UsageTotals as ap, type Verdict as aq, type VerificationCheck as ar, addAttemptUsage as as, addModelUsage as at, addPlannerUsage as au, addUsage as av, addUsageTotals as aw, createEmptyPlan as ax, createSkillsService as ay, createTask as az, DEFAULT_SUPERVISOR_RETRY_LIMIT as b, type DiscoveredModel as c, type ExecutionCommandName as d, type ExecutionRecord as e, type GuardTrip as f, type IntegrationDisposal as g, type InvalidSkill as h, type IsolationAvailability as i, type IsolationHandoff as j, type IsolationHandoffRepo as k, type IsolationInactiveReason as l, type IsolationLandedTask as m, type IsolationLanding as n, type IsolationMergeBlock as o, type IsolationMergeBlockReason as p, type IsolationMergeResult as q, type IsolationOutcome as r, type IsolationPruneResult as s, type IsolationRemoval as t, type IsolationRepo as u, type IsolationRun as v, type IsolationTaskRecord as w, type IsolationTaskRef as x, type IsolationTaskRepo as y, type IsolationTaskStatus as z };
1388
+ export { type ResearchStepOutcome as $, type ApprovalRecord as A, BUILTIN_SKILL_NAMES as B, type ConversationMessage as C, DEFAULT_RUNNERS as D, type ExecutionActor as E, type IsolationTaskStatus as F, type GuardTarget as G, type IsolationView as H, type IWorktreeIsolation as I, type PlanIsolation as J, type PlanState as K, type LegacyPlanState as L, type ModelUsage as M, type PlanStatus as N, type PlanUsage as O, type PendingPlanTurn as P, type PlannerUsage as Q, type PrepareIntent as R, type PreparedTask as S, type PreservedWork as T, type PreviousWork as U, type RefusedRemoval as V, type RepairEvidence as W, type RepoGroupLayout as X, type ResearchLogEntry as Y, type ResearchProgress as Z, type ResearchStep as _, type AttemptUsage as a, type ResearchToolType as a0, type RunSettings as a1, type RunnerId as a2, type SetupFailure as a3, type ShadowedSkill as a4, type SkillAppliesTo as a5, type SkillCatalog as a6, type SkillInfo as a7, type SkillLoad as a8, type SkillLoadNotice as a9, createEmptyPlan as aA, createSkillsService as aB, createTask as aC, endAttemptUsage as aD, flattenTasks as aE, flattenTasksWithParents as aF, isAwaitingReason as aG, isMeasured as aH, isSupervisorLevel as aI, isUserMessage as aJ, migratePlanState as aK, partedPromptUsage as aL, pausedOnLimit as aM, planUsage as aN, plannerContextFill as aO, rejoinPlanTree as aP, startAttemptUsage as aQ, usageLine as aR, waitsForApproval as aS, type SkillMetadata as aa, type SkillSource as ab, SkillsService as ac, type SubagentOutcome as ad, type SupervisorLevel as ae, type Task as af, type TaskDiffRead as ag, type TaskDiffResult as ah, type TaskIsolation as ai, type TaskIsolationState as aj, type TaskModelAssignment as ak, type TaskSkillNotice as al, type TaskSkillSnapshot as am, type TaskSnapshot as an, type TaskStatus as ao, type TreeSnapshot as ap, type UsageLine as aq, type UsageRecord as ar, type UsageTotals as as, type Verdict as at, type VerificationCheck as au, addAttemptUsage as av, addModelUsage as aw, addPlannerUsage as ax, addUsage as ay, addUsageTotals as az, type AwaitingReason as b, DEFAULT_SUPERVISOR_RETRY_LIMIT as c, type DiscoveredModel as d, type ExecutionCommandName as e, type ExecutionRecord as f, type GuardTrip as g, type IntegrationDisposal as h, type InvalidSkill as i, type IsolationAvailability as j, type IsolationHandoff as k, type IsolationHandoffRepo as l, type IsolationInactiveReason as m, type IsolationLandedTask as n, type IsolationLanding as o, type IsolationMergeBlock as p, type IsolationMergeBlockReason as q, type IsolationMergeResult as r, type IsolationOutcome as s, type IsolationPruneResult as t, type IsolationRemoval as u, type IsolationRepo as v, type IsolationRun as w, type IsolationTaskRecord as x, type IsolationTaskRef as y, type IsolationTaskRepo as z };
@@ -7,10 +7,10 @@ import {
7
7
  plannedTask,
8
8
  stripTrailingCommas,
9
9
  taskWhere
10
- } from "./chunk-DJAPDO6E.mjs";
10
+ } from "./chunk-A57ITQE7.mjs";
11
11
  import {
12
12
  flattenTasksWithParents
13
- } from "./chunk-2IIRSBEQ.mjs";
13
+ } from "./chunk-SM6BV6XS.mjs";
14
14
 
15
15
  // src/services/PlanValidator.ts
16
16
  function parsePlanJson(raw, runners, runnerModes, autonomousDefault = true) {
@@ -27,7 +27,7 @@ function parsePlanJson(raw, runners, runnerModes, autonomousDefault = true) {
27
27
  }
28
28
  if (matches.length === 0 && sawUnbalanced) {
29
29
  throw new PlanParseError(
30
- "Plan response appears truncated \u2014 the model likely hit its output length limit before finishing the JSON.",
30
+ "Plan response appears truncated: the model likely hit its output length limit before finishing the JSON.",
31
31
  raw,
32
32
  { truncated: true }
33
33
  );
@@ -67,7 +67,7 @@ function validatePlanTasks(obj, runners, runnerModes, autonomousDefault = true)
67
67
  const ignored = planned.flatMap((p) => p.ignored);
68
68
  const warnings = [
69
69
  ...ignored.some((i) => i.field === "taskMode") ? [`"taskMode" is not yours to set and was ignored: each task runs in its runner's mode for the user's autonomy level, or its read-only mode when you set "readOnly": true.`] : [],
70
- ...ignored.filter((i) => i.field === "readOnly").map((i) => `${i.message} \u2014 it was ignored.`)
70
+ ...ignored.filter((i) => i.field === "readOnly").map((i) => `${i.message}: it was ignored.`)
71
71
  ];
72
72
  return warnings.length > 0 ? { ok: true, tasks, warnings } : { ok: true, tasks };
73
73
  }
@@ -94,4 +94,4 @@ export {
94
94
  validatePlanTasks,
95
95
  looksLikePlanAttempt
96
96
  };
97
- //# sourceMappingURL=chunk-DNFT4SQG.mjs.map
97
+ //# sourceMappingURL=chunk-645FALZX.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/services/PlanValidator.ts"],"sourcesContent":["import { Task, RunnerId, flattenTasksWithParents } from '../models/Task';\nimport { extractObjectsWithKey, stripTrailingCommas, escapeControlCharsInStrings, PlanParseError, PLAN_ENVELOPE_KEY } from './JsonExtractor';\nimport type { RunnerModeInfo } from './ModeResolver';\nimport { plannedTask, checkPlanInvariants, taskWhere, type PlanTaskError } from './PlannedTask';\n\nexport function parsePlanJson(\n raw: string,\n runners: RunnerId[],\n runnerModes?: Record<RunnerId, RunnerModeInfo[]>,\n autonomousDefault = true,\n): Task[] {\n const { matches, fallback, sawUnbalanced } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);\n\n // A reply can carry several tasks-keyed objects — a schema echo from the\n // prompt, an inline draft, then the real plan. The model's LAST emission is\n // its final answer, so candidates are tried back to front and the first one\n // that fully parses and validates wins.\n const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];\n\n let lastError: PlanParseError | null = null;\n for (const json of candidates) {\n try {\n return parsePlanObject(json, raw, runners, runnerModes, autonomousDefault);\n } catch (err) {\n if (!(err instanceof PlanParseError)) throw err;\n // Prefer surfacing the error from the most plan-like candidate: the\n // last one carrying a tasks key (tried first).\n if (!lastError) lastError = err;\n }\n }\n\n // No usable tasks object anywhere, but an object ran off the end of the\n // text: the plan was almost certainly cut off mid-stream.\n if (matches.length === 0 && sawUnbalanced) {\n throw new PlanParseError(\n 'Plan response appears truncated: the model likely hit its output length limit before finishing the JSON.',\n raw,\n { truncated: true },\n );\n }\n throw lastError ?? new PlanParseError('Invalid plan: missing tasks array', raw);\n}\n\nfunction parsePlanObject(\n json: string,\n raw: string,\n runners: RunnerId[],\n runnerModes?: Record<RunnerId, RunnerModeInfo[]>,\n autonomousDefault = true,\n): Task[] {\n const cleaned = stripTrailingCommas(escapeControlCharsInStrings(json));\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(cleaned);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new PlanParseError(`Plan response was not valid JSON: ${detail}`, raw);\n }\n\n const result = validatePlanTasks(parsed, runners, runnerModes, autonomousDefault);\n if (result.ok) return result.tasks;\n const [first] = result.errors;\n // An envelope problem (no tasks array, an empty one) keeps the generic\n // re-emit; only a rule broken by a task is worth naming in the corrective.\n throw new PlanParseError(first.message, raw, { semantic: first.taskId !== undefined });\n}\n\nexport type { PlanTaskError } from './PlannedTask';\nexport { requestedSkills } from './PlannedTask';\n\n/** `warnings` name what was accepted but not used, for a channel that can tell the planner. */\nexport type PlanTasksResult = { ok: true; tasks: Task[]; warnings?: string[] } | { ok: false; errors: PlanTaskError[] };\n\n/**\n * Every check a plan's tasks must pass, on an already-parsed `{ tasks }`\n * object — what a plan is held to whichever channel it arrived on. All\n * problems are reported, the slice classification first, so the first is the\n * one the text path's corrective names.\n */\nexport function validatePlanTasks(\n obj: unknown,\n runners: RunnerId[],\n runnerModes?: Record<RunnerId, RunnerModeInfo[]>,\n autonomousDefault = true,\n): PlanTasksResult {\n const rawTasks = isRecord(obj) ? obj.tasks : undefined;\n if (!Array.isArray(rawTasks)) {\n return { ok: false, errors: [{ field: 'tasks', message: 'Invalid plan: missing tasks array' }] };\n }\n if (rawTasks.length === 0) {\n return { ok: false, errors: [{ field: 'tasks', message: 'Plan contained no tasks' }] };\n }\n\n const planned = rawTasks.map((t) => plannedTask(t, { runners, runnerModes, autonomousDefault }));\n const tasks = planned.map((p) => p.task);\n const errors = [\n ...sliceRequired(tasks),\n ...planned.flatMap((p) => p.errors),\n ...checkPlanInvariants(tasks, runners),\n ];\n\n if (errors.length > 0) return { ok: false, errors };\n const ignored = planned.flatMap((p) => p.ignored);\n const warnings = [\n ...(ignored.some((i) => i.field === 'taskMode')\n ? ['\"taskMode\" is not yours to set and was ignored: each task runs in its runner\\'s mode for the user\\'s autonomy level, or its read-only mode when you set \"readOnly\": true.']\n : []),\n ...ignored.filter((i) => i.field === 'readOnly').map((i) => `${i.message}: it was ignored.`),\n ];\n return warnings.length > 0 ? { ok: true, tasks, warnings } : { ok: true, tasks };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Whether a reply that failed {@link parsePlanJson} actually LOOKED like a\n * plan attempt — a balanced tasks-keyed object, or JSON that got cut off.\n * Distinguishes \"the model tried to emit a plan and botched it\" (worth a\n * corrective retry) from prose that merely mentions `\"tasks\"` (leave alone).\n */\nexport function looksLikePlanAttempt(raw: string): boolean {\n const { matches, sawUnbalanced } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);\n if (matches.length > 0) return true;\n return sawUnbalanced && raw.includes(`\"${PLAN_ENVELOPE_KEY}\"`);\n}\n\n/**\n * A whole plan says how each task is sliced: an edit may leave it to the\n * task's parent or the tasks it was made from, a submitted plan may not.\n */\nfunction sliceRequired(tasks: readonly Task[]): PlanTaskError[] {\n return flattenTasksWithParents(tasks).flatMap(({ task: t, parent }) => {\n const where = taskWhere(t.title, parent?.title);\n const errors: PlanTaskError[] = [];\n if (!t.sliceType) errors.push({ taskId: t.id, field: 'sliceType', message: `${where} is missing sliceType. Every task must specify sliceType (\"AFK\" or \"HITL\").` });\n if (t.type === 'ai' && !t.autonomy) errors.push({ taskId: t.id, field: 'autonomy', message: `AI ${where} is missing autonomy. Every AI task must specify autonomy (\"AFK\" or \"HITL\").` });\n return errors;\n });\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,5 @@
1
1
  import {
2
+ DEFAULT_RUNNERS,
2
3
  addTaskToPlan,
3
4
  createTask,
4
5
  flattenTasks,
@@ -11,7 +12,7 @@ import {
11
12
  resolveTaskRef,
12
13
  skillNames,
13
14
  updateTaskInPlan
14
- } from "./chunk-2IIRSBEQ.mjs";
15
+ } from "./chunk-SM6BV6XS.mjs";
15
16
 
16
17
  // src/services/JsonExtractor.ts
17
18
  var PLAN_ENVELOPE_KEY = "tasks";
@@ -241,7 +242,7 @@ function plannedMode(runner, runnerModes, { readOnly, current }, autonomousDefau
241
242
  const reason2 = unavailableModeReason(modes, taggedReadOnlyMode(modes));
242
243
  return {
243
244
  ok: false,
244
- error: `Runner "${runner}" has no read-only mode${reason2 ? " available on this host" : ""} \u2014 assign a read-only task to ${able.length > 0 ? `one of: ${able.join(", ")}` : "no runner: none of the enabled runners has one"}, or drop "readOnly"${reason2 ? `. ${reason2}` : ""}`
245
+ error: `Runner "${runner}" has no read-only mode${reason2 ? " available on this host" : ""}: assign a read-only task to ${able.length > 0 ? `one of: ${able.join(", ")}` : "no runner: none of the enabled runners has one"}, or drop "readOnly"${reason2 ? `. ${reason2}` : ""}`
245
246
  };
246
247
  }
247
248
  const keep = current !== void 0 && modes.some((m) => m.id === current) && !(readOnly === false && current === taggedReadOnlyMode(modes));
@@ -300,6 +301,7 @@ function blockedByLabel(refs) {
300
301
  function skippedLabel(reason) {
301
302
  return reason ? `Skipped: ${reason}` : "Skipped";
302
303
  }
304
+ var WAITS_FOR_APPROVAL_LABEL = "Waiting for your approval to start";
303
305
  function approvalLabel(count) {
304
306
  if (!count) return null;
305
307
  return count > 1 ? `Waiting for approval (${count})` : "Waiting for approval";
@@ -315,6 +317,9 @@ function resumeLabel(task, now = /* @__PURE__ */ new Date()) {
315
317
  return Number.isNaN(at.getTime()) ? null : `Resumes at ${resumeTime(at, now)}`;
316
318
  }
317
319
 
320
+ // src/services/TaskOps.ts
321
+ import { v4 as uuidv42 } from "uuid";
322
+
318
323
  // src/services/ModelAllowlistResolver.ts
319
324
  function effectiveAllowlist(allowlist, runner, modelsByRunner) {
320
325
  if (!allowlist || allowlist.length === 0) return void 0;
@@ -422,9 +427,9 @@ function coerceAssignments(tasks, perRunnerAllowlist, modelsByRunner) {
422
427
 
423
428
  // src/services/PlannedTask.ts
424
429
  import { v4 as uuidv4 } from "uuid";
425
- var MODE_NOT_THE_PLANNERS = `"taskMode" is the user's to choose \u2014 set "readOnly": true on a task that must not change files, and leave it out otherwise`;
426
- var MANUAL_NOT_READ_ONLY = '"readOnly" is for AI tasks \u2014 a manual task runs in no mode';
427
- var OPS_NOT_READ_ONLY = `"readOnly" is not for an ops task \u2014 it runs commands, in the session's mode, and a read-only mode refuses them`;
430
+ var MODE_NOT_THE_PLANNERS = `"taskMode" is the user's to choose: set "readOnly": true on a task that must not change files, and leave it out otherwise`;
431
+ var MANUAL_NOT_READ_ONLY = '"readOnly" is for AI tasks: a manual task runs in no mode';
432
+ var OPS_NOT_READ_ONLY = `"readOnly" is not for an ops task: it runs commands, in the session's mode, and a read-only mode refuses them`;
428
433
  function isRecord(value) {
429
434
  return typeof value === "object" && value !== null && !Array.isArray(value);
430
435
  }
@@ -432,8 +437,8 @@ function text(value) {
432
437
  return typeof value === "string" || typeof value === "number" ? String(value) : void 0;
433
438
  }
434
439
  function opsPlacementRefusal(type, isSubtask) {
435
- if (type !== "ai") return "is a manual task \u2014 only an AI task can be an ops task";
436
- if (isSubtask) return "is a subtask, which runs with its parent \u2014 only a top-level task can be an ops task";
440
+ if (type !== "ai") return "is a manual task: only an AI task can be an ops task";
441
+ if (isSubtask) return "is a subtask, which runs with its parent: only a top-level task can be an ops task";
437
442
  return null;
438
443
  }
439
444
  function taskWhere(title, parentTitle) {
@@ -457,7 +462,7 @@ function modelOf(raw, from) {
457
462
  }
458
463
  function plannedTask(spec, catalog, at = {}) {
459
464
  const raw = isRecord(spec) ? spec : {};
460
- const { parent, from, freshIds } = at;
465
+ const { parent, from, freshIds, newId = uuidv4 } = at;
461
466
  const base = from?.fields ?? {};
462
467
  const errors = [];
463
468
  const ignored = [];
@@ -465,11 +470,11 @@ function plannedTask(spec, catalog, at = {}) {
465
470
  const title = text(raw.title) ?? "";
466
471
  const where = taskWhere(title, parent?.title);
467
472
  const plannerId = text(raw.id);
468
- const id = freshIds || !plannerId ? uuidv4() : plannerId;
473
+ const id = freshIds || !plannerId ? newId() : plannerId;
469
474
  if (freshIds && plannerId) renamed.set(plannerId, id);
470
475
  const type = raw.type === "user" || raw.type === "ai" ? raw.type : base.type ?? "ai";
471
476
  const ai = type === "ai";
472
- const assignedRunner = text(raw.assignedRunner) ?? base.assignedRunner ?? catalog.runners[0] ?? "claude-code";
477
+ const assignedRunner = text(raw.assignedRunner) ?? base.assignedRunner ?? catalog.runners[0] ?? DEFAULT_RUNNERS[0];
473
478
  const description = text(raw.description) ?? base.description ?? title;
474
479
  if (raw.ops === true && !ai) errors.push({ taskId: id, field: "ops", message: `${where} ${opsPlacementRefusal(type, !!parent)}` });
475
480
  if (raw.taskMode !== void 0) ignored.push({ taskId: id, field: "taskMode", message: `${where}: ${MODE_NOT_THE_PLANNERS}` });
@@ -496,7 +501,7 @@ function plannedTask(spec, catalog, at = {}) {
496
501
  const subtasks = [];
497
502
  if (Array.isArray(raw.subtasks)) {
498
503
  for (const s of raw.subtasks) {
499
- const child = plannedTask(s, catalog, { parent: { title, sliceType, autonomy, readOnly }, freshIds });
504
+ const child = plannedTask(s, catalog, { parent: { title, sliceType, autonomy, readOnly }, freshIds, newId });
500
505
  subtasks.push(child.task);
501
506
  errors.push(...child.errors);
502
507
  ignored.push(...child.ignored);
@@ -608,7 +613,7 @@ function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
608
613
  if (taskMode !== void 0) {
609
614
  const modes = catalog.runnerModes[runner];
610
615
  if (modes && modes.length > 0 && !modes.some((m) => m.id === taskMode)) {
611
- return { ok: false, error: `Runner "${runner}" has no task mode "${taskMode}" \u2014 valid modes: ${modes.map((m) => m.id).join(", ")}` };
616
+ return { ok: false, error: `Runner "${runner}" has no task mode "${taskMode}": valid modes: ${modes.map((m) => m.id).join(", ")}` };
612
617
  }
613
618
  const unavailable = unavailableModeReason(modes, taskMode);
614
619
  if (unavailable) return { ok: false, error: `Mode "${taskMode}" of runner "${runner}" is unavailable on this host: ${unavailable}` };
@@ -619,7 +624,7 @@ function typeCoherenceCheck(target, changes) {
619
624
  if (!changes.type || changes.type === target.type) return null;
620
625
  if (changes.type === "user") {
621
626
  if (!Array.isArray(changes.userSteps) || changes.userSteps.length === 0) {
622
- return { ok: false, error: `"${target.title}" cannot become a manual task without user steps \u2014 include "userSteps" in this edit` };
627
+ return { ok: false, error: `"${target.title}" cannot become a manual task without user steps: include "userSteps" in this edit` };
623
628
  }
624
629
  return {
625
630
  ok: true,
@@ -629,7 +634,7 @@ function typeCoherenceCheck(target, changes) {
629
634
  };
630
635
  }
631
636
  if (!changes.prompt || !changes.prompt.trim()) {
632
- return { ok: false, error: `"${target.title}" cannot become an AI task without a prompt \u2014 include "prompt" in this edit` };
637
+ return { ok: false, error: `"${target.title}" cannot become an AI task without a prompt: include "prompt" in this edit` };
633
638
  }
634
639
  return { ok: true, clear: target.userSteps && target.userSteps.length > 0 ? ["userSteps"] : [] };
635
640
  }
@@ -879,6 +884,17 @@ function changedFields(target, changes, catalog) {
879
884
  if ("taskMode" in changes && isReadOnly({ ...target, ...changes }, catalog) !== isReadOnly(target, catalog)) fields.push("readOnly");
880
885
  return fields;
881
886
  }
887
+ var keptIds = /* @__PURE__ */ new WeakMap();
888
+ function idsOf(op) {
889
+ if (typeof op !== "object" || op === null) return uuidv42;
890
+ const ids = keptIds.get(op) ?? [];
891
+ keptIds.set(op, ids);
892
+ let next = 0;
893
+ return () => {
894
+ if (next === ids.length) ids.push(uuidv42());
895
+ return ids[next++];
896
+ };
897
+ }
882
898
  function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
883
899
  const ops = [...held, ...newOps];
884
900
  const opLabel = (i) => i < held.length ? `earlier op ${i + 1} (${ops[i].op})` : `op ${i - held.length + 1} (${ops[i].op})`;
@@ -994,6 +1010,7 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
994
1010
  let lastLabel = opLabel(0);
995
1011
  for (const [i, op] of ops.entries()) {
996
1012
  const label = opLabel(i);
1013
+ const newId = idsOf(op);
997
1014
  switch (op.op) {
998
1015
  case "update": {
999
1016
  const prep = prepareEdit(op, i, label, "planner");
@@ -1023,7 +1040,7 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
1023
1040
  errors.push(`${label}: new task needs a title`);
1024
1041
  break;
1025
1042
  }
1026
- const planned = plannedTask(spec, intake, { freshIds: true });
1043
+ const planned = plannedTask(spec, intake, { freshIds: true, newId });
1027
1044
  const refused = refusalOf(planned);
1028
1045
  if (refused) {
1029
1046
  errors.push(`${label}: ${refused}`);
@@ -1119,6 +1136,7 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
1119
1136
  const stories = [...new Set(toMerge.flatMap((t) => t.userStoriesCovered ?? []))];
1120
1137
  const planned = plannedTask(spec, intake, {
1121
1138
  freshIds: true,
1139
+ newId,
1122
1140
  from: {
1123
1141
  readOnly: !writes,
1124
1142
  fields: {
@@ -1212,6 +1230,7 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
1212
1230
  }
1213
1231
  const planned = plannedTask(spec, intake, {
1214
1232
  freshIds: true,
1233
+ newId,
1215
1234
  from: {
1216
1235
  readOnly: isReadOnly(target, catalog),
1217
1236
  fields: {
@@ -1302,7 +1321,7 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
1302
1321
  tasks = renumberTasks(repaired.tasks);
1303
1322
  }
1304
1323
  }
1305
- return errors.length ? { ok: false, tasks: currentTasks, errors, summary: [] } : { ok: true, tasks, errors: [], summary };
1324
+ return errors.length ? { ok: false, tasks: currentTasks, errors, summary: [] } : { ok: true, tasks, errors: [], summary, handles: handleId };
1306
1325
  }
1307
1326
 
1308
1327
  export {
@@ -1316,10 +1335,6 @@ export {
1316
1335
  extractJsonObject,
1317
1336
  stripTrailingCommas,
1318
1337
  escapeControlCharsInStrings,
1319
- effectiveAllowlist,
1320
- filterModelsForPrompt,
1321
- clampThinkingEffort,
1322
- coerceAssignments,
1323
1338
  runnerModesFrom,
1324
1339
  resolveDefaultMode,
1325
1340
  autonomyLevelLabel,
@@ -1330,12 +1345,17 @@ export {
1330
1345
  unavailableTaskModeMessage,
1331
1346
  carryPlannedModes,
1332
1347
  readOnlyRunners,
1348
+ effectiveAllowlist,
1349
+ filterModelsForPrompt,
1350
+ clampThinkingEffort,
1351
+ coerceAssignments,
1333
1352
  isTaskRunning,
1334
1353
  NOT_STARTED,
1335
1354
  taskStatusKind,
1336
1355
  awaitingLabel,
1337
1356
  blockedByLabel,
1338
1357
  skippedLabel,
1358
+ WAITS_FOR_APPROVAL_LABEL,
1339
1359
  approvalLabel,
1340
1360
  resumeTime,
1341
1361
  resumeLabel,
@@ -1355,4 +1375,4 @@ export {
1355
1375
  canSplitTask,
1356
1376
  applyTaskOps
1357
1377
  };
1358
- //# sourceMappingURL=chunk-DJAPDO6E.mjs.map
1378
+ //# sourceMappingURL=chunk-A57ITQE7.mjs.map