@ordewell/core 0.6.4 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/dist/{IFileSystem-Qj2MWHn_.d.mts → IFileSystem-BnLh-_SK.d.mts} +1 -1
  3. package/dist/{IFileSystem-DQEKyC6H.d.ts → IFileSystem-DofiRcCo.d.ts} +1 -1
  4. package/dist/{ModeResolver-rqaY_4Ex.d.mts → ModeResolver-DZzJSdIU.d.mts} +3 -3
  5. package/dist/{ModeResolver-ByNpC0EP.d.ts → ModeResolver-DkMYf4uS.d.ts} +3 -3
  6. package/dist/{Task-xqdxugt2.d.mts → Task-BnLxkVQF.d.mts} +414 -22
  7. package/dist/{Task-xqdxugt2.d.ts → Task-BnLxkVQF.d.ts} +414 -22
  8. package/dist/{chunk-JRZPSNRO.mjs → chunk-LGQNXPBX.mjs} +10 -2
  9. package/dist/chunk-LGQNXPBX.mjs.map +1 -0
  10. package/dist/{chunk-N66ZHFGN.mjs → chunk-OYUSADFM.mjs} +11 -1
  11. package/dist/chunk-OYUSADFM.mjs.map +1 -0
  12. package/dist/{chunk-HPNUFBIT.mjs → chunk-V3JHS7AX.mjs} +9 -1
  13. package/dist/chunk-V3JHS7AX.mjs.map +1 -0
  14. package/dist/{chunk-I6U3PVHL.mjs → chunk-Y36NIASW.mjs} +255 -32
  15. package/dist/chunk-Y36NIASW.mjs.map +1 -0
  16. package/dist/{chunk-M4KXBEFJ.mjs → chunk-ZV5LDNCO.mjs} +44 -43
  17. package/dist/chunk-ZV5LDNCO.mjs.map +1 -0
  18. package/dist/index.d.mts +518 -238
  19. package/dist/index.d.ts +518 -238
  20. package/dist/index.js +3925 -1568
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +3525 -1393
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/order-labels.d.mts +16 -2
  25. package/dist/order-labels.d.ts +16 -2
  26. package/dist/order-labels.js +12 -2
  27. package/dist/order-labels.js.map +1 -1
  28. package/dist/order-labels.mjs +7 -3
  29. package/dist/{parsing-DDZ1rUYa.d.mts → parsing-B6ctfF80.d.mts} +24 -3
  30. package/dist/{parsing-COPG8-Ep.d.ts → parsing-ddZXZFP4.d.ts} +24 -3
  31. package/dist/parsing.d.mts +4 -3
  32. package/dist/parsing.d.ts +4 -3
  33. package/dist/parsing.js +42 -41
  34. package/dist/parsing.js.map +1 -1
  35. package/dist/parsing.mjs +3 -3
  36. package/dist/{plan-utils-Br8UAoiu.d.ts → plan-utils--kQRzSE-.d.ts} +251 -7
  37. package/dist/{plan-utils-DMgajMvd.d.mts → plan-utils-CKgNtDwQ.d.mts} +251 -7
  38. package/dist/plan-utils.d.mts +5 -3
  39. package/dist/plan-utils.d.ts +5 -3
  40. package/dist/plan-utils.js +313 -61
  41. package/dist/plan-utils.js.map +1 -1
  42. package/dist/plan-utils.mjs +46 -3
  43. package/dist/testing.d.mts +26 -5
  44. package/dist/testing.d.ts +26 -5
  45. package/dist/testing.js +62 -2
  46. package/dist/testing.js.map +1 -1
  47. package/dist/testing.mjs +62 -4
  48. package/dist/testing.mjs.map +1 -1
  49. package/package.json +4 -2
  50. package/dist/chunk-HPNUFBIT.mjs.map +0 -1
  51. package/dist/chunk-I6U3PVHL.mjs.map +0 -1
  52. package/dist/chunk-JRZPSNRO.mjs.map +0 -1
  53. package/dist/chunk-M4KXBEFJ.mjs.map +0 -1
  54. package/dist/chunk-N66ZHFGN.mjs.map +0 -1
@@ -1,6 +1,7 @@
1
- import { aV as Task } from './Task-xqdxugt2.mjs';
1
+ import { bh as Task } from './Task-BnLxkVQF.mjs';
2
2
  import 'child_process';
3
3
  import 'events';
4
+ import 'zod/v4';
4
5
 
5
6
  /**
6
7
  * Parent-scoped dotted order label for a task, e.g. "2.1" for the first subtask
@@ -13,7 +14,20 @@ declare function taskOrderLabel(task: {
13
14
  }, parent?: {
14
15
  order: number;
15
16
  }): string;
17
+ /** How a surface refers to a task in prose: "#2", or "#2.1" for a subtask. */
18
+ declare function taskRef(task: {
19
+ order: number;
20
+ }, parent?: {
21
+ order: number;
22
+ }): string;
23
+ /** A task named in full, as a confirmation or a picker title does: "#2 Deploy". */
24
+ declare function titledTaskRef(task: {
25
+ order: number;
26
+ title: string;
27
+ }, parent?: {
28
+ order: number;
29
+ }): string;
16
30
  /** Resolve a dotted label to a task in a plan's top-level task list. */
17
31
  declare function resolveOrderLabel(tasks: Task[], label: string): Task | undefined;
18
32
 
19
- export { resolveOrderLabel, taskOrderLabel };
33
+ export { resolveOrderLabel, taskOrderLabel, taskRef, titledTaskRef };
@@ -1,6 +1,7 @@
1
- import { aV as Task } from './Task-xqdxugt2.js';
1
+ import { bh as Task } from './Task-BnLxkVQF.js';
2
2
  import 'child_process';
3
3
  import 'events';
4
+ import 'zod/v4';
4
5
 
5
6
  /**
6
7
  * Parent-scoped dotted order label for a task, e.g. "2.1" for the first subtask
@@ -13,7 +14,20 @@ declare function taskOrderLabel(task: {
13
14
  }, parent?: {
14
15
  order: number;
15
16
  }): string;
17
+ /** How a surface refers to a task in prose: "#2", or "#2.1" for a subtask. */
18
+ declare function taskRef(task: {
19
+ order: number;
20
+ }, parent?: {
21
+ order: number;
22
+ }): string;
23
+ /** A task named in full, as a confirmation or a picker title does: "#2 Deploy". */
24
+ declare function titledTaskRef(task: {
25
+ order: number;
26
+ title: string;
27
+ }, parent?: {
28
+ order: number;
29
+ }): string;
16
30
  /** Resolve a dotted label to a task in a plan's top-level task list. */
17
31
  declare function resolveOrderLabel(tasks: Task[], label: string): Task | undefined;
18
32
 
19
- export { resolveOrderLabel, taskOrderLabel };
33
+ export { resolveOrderLabel, taskOrderLabel, taskRef, titledTaskRef };
@@ -21,12 +21,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var order_labels_exports = {};
22
22
  __export(order_labels_exports, {
23
23
  resolveOrderLabel: () => resolveOrderLabel,
24
- taskOrderLabel: () => taskOrderLabel
24
+ taskOrderLabel: () => taskOrderLabel,
25
+ taskRef: () => taskRef,
26
+ titledTaskRef: () => titledTaskRef
25
27
  });
26
28
  module.exports = __toCommonJS(order_labels_exports);
27
29
  function taskOrderLabel(task, parent) {
28
30
  return parent ? `${parent.order}.${task.order}` : String(task.order);
29
31
  }
32
+ function taskRef(task, parent) {
33
+ return `#${taskOrderLabel(task, parent)}`;
34
+ }
35
+ function titledTaskRef(task, parent) {
36
+ return `${taskRef(task, parent)} ${task.title}`;
37
+ }
30
38
  function resolveOrderLabel(tasks, label) {
31
39
  const parts = label.split(".");
32
40
  if (parts.length > 2 || parts.some((p) => !/^\d+$/.test(p))) return void 0;
@@ -39,6 +47,8 @@ function resolveOrderLabel(tasks, label) {
39
47
  // Annotate the CommonJS export names for ESM import in node:
40
48
  0 && (module.exports = {
41
49
  resolveOrderLabel,
42
- taskOrderLabel
50
+ taskOrderLabel,
51
+ taskRef,
52
+ titledTaskRef
43
53
  });
44
54
  //# sourceMappingURL=order-labels.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/order-labels.ts"],"sourcesContent":["import type { Task } from './models/Task';\n\n/**\n * Parent-scoped dotted order label for a task, e.g. \"2.1\" for the first subtask\n * of task 2. Kept in its own module (no Node imports) so a browser-bundled\n * surface — the VS Code webview — can import it via `@ordewell/core/order-labels`\n * without dragging the rest of core in.\n */\nexport function taskOrderLabel(task: { order: number }, parent?: { order: number }): string {\n return parent ? `${parent.order}.${task.order}` : String(task.order);\n}\n\n/** Resolve a dotted label to a task in a plan's top-level task list. */\nexport function resolveOrderLabel(tasks: Task[], label: string): Task | undefined {\n const parts = label.split('.');\n if (parts.length > 2 || parts.some((p) => !/^\\d+$/.test(p))) return undefined;\n const [topOrder, childOrder] = parts.map(Number);\n const top = tasks.find((t) => t.order === topOrder);\n if (!top) return undefined;\n if (childOrder === undefined) return top;\n return top.subtasks.find((s) => s.order === childOrder);\n}"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQO,SAAS,eAAe,MAAyB,QAAoC;AAC1F,SAAO,SAAS,GAAG,OAAO,KAAK,IAAI,KAAK,KAAK,KAAK,OAAO,KAAK,KAAK;AACrE;AAGO,SAAS,kBAAkB,OAAe,OAAiC;AAChF,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,SAAS,KAAK,MAAM,KAAK,CAAC,MAAM,CAAC,QAAQ,KAAK,CAAC,CAAC,EAAG,QAAO;AACpE,QAAM,CAAC,UAAU,UAAU,IAAI,MAAM,IAAI,MAAM;AAC/C,QAAM,MAAM,MAAM,KAAK,CAAC,MAAM,EAAE,UAAU,QAAQ;AAClD,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI,eAAe,OAAW,QAAO;AACrC,SAAO,IAAI,SAAS,KAAK,CAAC,MAAM,EAAE,UAAU,UAAU;AACxD;","names":[]}
1
+ {"version":3,"sources":["../src/order-labels.ts"],"sourcesContent":["import type { Task } from './models/Task';\n\n/**\n * Parent-scoped dotted order label for a task, e.g. \"2.1\" for the first subtask\n * of task 2. Kept in its own module (no Node imports) so a browser-bundled\n * surface — the VS Code webview — can import it via `@ordewell/core/order-labels`\n * without dragging the rest of core in.\n */\nexport function taskOrderLabel(task: { order: number }, parent?: { order: number }): string {\n return parent ? `${parent.order}.${task.order}` : String(task.order);\n}\n\n/** How a surface refers to a task in prose: \"#2\", or \"#2.1\" for a subtask. */\nexport function taskRef(task: { order: number }, parent?: { order: number }): string {\n return `#${taskOrderLabel(task, parent)}`;\n}\n\n/** A task named in full, as a confirmation or a picker title does: \"#2 Deploy\". */\nexport function titledTaskRef(task: { order: number; title: string }, parent?: { order: number }): string {\n return `${taskRef(task, parent)} ${task.title}`;\n}\n\n/** Resolve a dotted label to a task in a plan's top-level task list. */\nexport function resolveOrderLabel(tasks: Task[], label: string): Task | undefined {\n const parts = label.split('.');\n if (parts.length > 2 || parts.some((p) => !/^\\d+$/.test(p))) return undefined;\n const [topOrder, childOrder] = parts.map(Number);\n const top = tasks.find((t) => t.order === topOrder);\n if (!top) return undefined;\n if (childOrder === undefined) return top;\n return top.subtasks.find((s) => s.order === childOrder);\n}"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQO,SAAS,eAAe,MAAyB,QAAoC;AAC1F,SAAO,SAAS,GAAG,OAAO,KAAK,IAAI,KAAK,KAAK,KAAK,OAAO,KAAK,KAAK;AACrE;AAGO,SAAS,QAAQ,MAAyB,QAAoC;AACnF,SAAO,IAAI,eAAe,MAAM,MAAM,CAAC;AACzC;AAGO,SAAS,cAAc,MAAwC,QAAoC;AACxG,SAAO,GAAG,QAAQ,MAAM,MAAM,CAAC,IAAI,KAAK,KAAK;AAC/C;AAGO,SAAS,kBAAkB,OAAe,OAAiC;AAChF,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,SAAS,KAAK,MAAM,KAAK,CAAC,MAAM,CAAC,QAAQ,KAAK,CAAC,CAAC,EAAG,QAAO;AACpE,QAAM,CAAC,UAAU,UAAU,IAAI,MAAM,IAAI,MAAM;AAC/C,QAAM,MAAM,MAAM,KAAK,CAAC,MAAM,EAAE,UAAU,QAAQ;AAClD,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI,eAAe,OAAW,QAAO;AACrC,SAAO,IAAI,SAAS,KAAK,CAAC,MAAM,EAAE,UAAU,UAAU;AACxD;","names":[]}
@@ -1,9 +1,13 @@
1
1
  import {
2
2
  resolveOrderLabel,
3
- taskOrderLabel
4
- } from "./chunk-HPNUFBIT.mjs";
3
+ taskOrderLabel,
4
+ taskRef,
5
+ titledTaskRef
6
+ } from "./chunk-V3JHS7AX.mjs";
5
7
  export {
6
8
  resolveOrderLabel,
7
- taskOrderLabel
9
+ taskOrderLabel,
10
+ taskRef,
11
+ titledTaskRef
8
12
  };
9
13
  //# sourceMappingURL=order-labels.mjs.map
@@ -1,5 +1,5 @@
1
- import { bg as ValidationContext, bh as ValidationResult, aG as RunnerId, aV as Task } from './Task-xqdxugt2.mjs';
2
- import { R as RunnerModeInfo } from './ModeResolver-rqaY_4Ex.mjs';
1
+ import { bh as Task, bJ as ValidationContext, bK as ValidationResult, a$ as RunnerId } from './Task-BnLxkVQF.mjs';
2
+ import { R as RunnerModeInfo } from './ModeResolver-DZzJSdIU.mjs';
3
3
 
4
4
  /** The envelope key a full plan reply is keyed by: `{"tasks":[...]}`. */
5
5
  declare const PLAN_ENVELOPE_KEY = "tasks";
@@ -114,6 +114,27 @@ interface PartialPlanTask {
114
114
  declare function parsePartialPlan(partial: string): PartialPlanTask[];
115
115
 
116
116
  declare function parsePlanJson(raw: string, runners: RunnerId[], runnerModes?: Record<RunnerId, RunnerModeInfo[]>, autonomousDefault?: boolean): Task[];
117
+ /** One reason a plan was refused, pinned to the task and field it is about. */
118
+ interface PlanTaskError {
119
+ /** Absent when the envelope itself is wrong rather than one of its tasks. */
120
+ taskId?: string;
121
+ field: string;
122
+ message: string;
123
+ }
124
+ type PlanTasksResult = {
125
+ ok: true;
126
+ tasks: Task[];
127
+ } | {
128
+ ok: false;
129
+ errors: PlanTaskError[];
130
+ };
131
+ /**
132
+ * Every check a plan's tasks must pass, on an already-parsed `{ tasks }`
133
+ * object — what a plan is held to whichever channel it arrived on. All
134
+ * problems are reported, in the order the text path has always met them, so
135
+ * the first is the one its corrective names.
136
+ */
137
+ declare function validatePlanTasks(obj: unknown, runners: RunnerId[], runnerModes?: Record<RunnerId, RunnerModeInfo[]>, autonomousDefault?: boolean): PlanTasksResult;
117
138
  /**
118
139
  * Whether a reply that failed {@link parsePlanJson} actually LOOKED like a
119
140
  * plan attempt — a balanced tasks-keyed object, or JSON that got cut off.
@@ -128,4 +149,4 @@ declare function checkImmutableLog(ctx: ValidationContext): ValidationResult;
128
149
  declare function checkInProgress(ctx: ValidationContext): ValidationResult;
129
150
  declare function validatePlanModification(ctx: ValidationContext): ValidationResult;
130
151
 
131
- export { PLAN_ENVELOPE_KEY as P, TASK_OPS_ENVELOPE_KEY as T, type PartialPlanTask as a, PlanParseError as b, TASK_QUERY_ENVELOPE_KEY as c, checkDepsResolve as d, checkImmutableLog as e, checkInProgress as f, checkNoCycles as g, checkUniqueIds as h, escapeControlCharsInStrings as i, extractJsonObject as j, extractObjectWithBalance as k, extractObjectsWithKey as l, looksLikePlanAttempt as m, parsePlanJson as n, opensWithJsonObject as o, parsePartialPlan as p, stripTrailingCommas as q, stripModelNoise as s, validatePlanModification as v };
152
+ export { PLAN_ENVELOPE_KEY as P, TASK_OPS_ENVELOPE_KEY as T, type PartialPlanTask as a, PlanParseError as b, type PlanTaskError as c, type PlanTasksResult as d, TASK_QUERY_ENVELOPE_KEY as e, checkDepsResolve as f, checkImmutableLog as g, checkInProgress as h, checkNoCycles as i, checkUniqueIds as j, escapeControlCharsInStrings as k, extractJsonObject as l, extractObjectWithBalance as m, extractObjectsWithKey as n, looksLikePlanAttempt as o, opensWithJsonObject as p, parsePartialPlan as q, parsePlanJson as r, stripModelNoise as s, stripTrailingCommas as t, validatePlanTasks as u, validatePlanModification as v };
@@ -1,5 +1,5 @@
1
- import { bg as ValidationContext, bh as ValidationResult, aG as RunnerId, aV as Task } from './Task-xqdxugt2.js';
2
- import { R as RunnerModeInfo } from './ModeResolver-ByNpC0EP.js';
1
+ import { bh as Task, bJ as ValidationContext, bK as ValidationResult, a$ as RunnerId } from './Task-BnLxkVQF.js';
2
+ import { R as RunnerModeInfo } from './ModeResolver-DkMYf4uS.js';
3
3
 
4
4
  /** The envelope key a full plan reply is keyed by: `{"tasks":[...]}`. */
5
5
  declare const PLAN_ENVELOPE_KEY = "tasks";
@@ -114,6 +114,27 @@ interface PartialPlanTask {
114
114
  declare function parsePartialPlan(partial: string): PartialPlanTask[];
115
115
 
116
116
  declare function parsePlanJson(raw: string, runners: RunnerId[], runnerModes?: Record<RunnerId, RunnerModeInfo[]>, autonomousDefault?: boolean): Task[];
117
+ /** One reason a plan was refused, pinned to the task and field it is about. */
118
+ interface PlanTaskError {
119
+ /** Absent when the envelope itself is wrong rather than one of its tasks. */
120
+ taskId?: string;
121
+ field: string;
122
+ message: string;
123
+ }
124
+ type PlanTasksResult = {
125
+ ok: true;
126
+ tasks: Task[];
127
+ } | {
128
+ ok: false;
129
+ errors: PlanTaskError[];
130
+ };
131
+ /**
132
+ * Every check a plan's tasks must pass, on an already-parsed `{ tasks }`
133
+ * object — what a plan is held to whichever channel it arrived on. All
134
+ * problems are reported, in the order the text path has always met them, so
135
+ * the first is the one its corrective names.
136
+ */
137
+ declare function validatePlanTasks(obj: unknown, runners: RunnerId[], runnerModes?: Record<RunnerId, RunnerModeInfo[]>, autonomousDefault?: boolean): PlanTasksResult;
117
138
  /**
118
139
  * Whether a reply that failed {@link parsePlanJson} actually LOOKED like a
119
140
  * plan attempt — a balanced tasks-keyed object, or JSON that got cut off.
@@ -128,4 +149,4 @@ declare function checkImmutableLog(ctx: ValidationContext): ValidationResult;
128
149
  declare function checkInProgress(ctx: ValidationContext): ValidationResult;
129
150
  declare function validatePlanModification(ctx: ValidationContext): ValidationResult;
130
151
 
131
- export { PLAN_ENVELOPE_KEY as P, TASK_OPS_ENVELOPE_KEY as T, type PartialPlanTask as a, PlanParseError as b, TASK_QUERY_ENVELOPE_KEY as c, checkDepsResolve as d, checkImmutableLog as e, checkInProgress as f, checkNoCycles as g, checkUniqueIds as h, escapeControlCharsInStrings as i, extractJsonObject as j, extractObjectWithBalance as k, extractObjectsWithKey as l, looksLikePlanAttempt as m, parsePlanJson as n, opensWithJsonObject as o, parsePartialPlan as p, stripTrailingCommas as q, stripModelNoise as s, validatePlanModification as v };
152
+ export { PLAN_ENVELOPE_KEY as P, TASK_OPS_ENVELOPE_KEY as T, type PartialPlanTask as a, PlanParseError as b, type PlanTaskError as c, type PlanTasksResult as d, TASK_QUERY_ENVELOPE_KEY as e, checkDepsResolve as f, checkImmutableLog as g, checkInProgress as h, checkNoCycles as i, checkUniqueIds as j, escapeControlCharsInStrings as k, extractJsonObject as l, extractObjectWithBalance as m, extractObjectsWithKey as n, looksLikePlanAttempt as o, opensWithJsonObject as p, parsePartialPlan as q, parsePlanJson as r, stripModelNoise as s, stripTrailingCommas as t, validatePlanTasks as u, validatePlanModification as v };
@@ -1,5 +1,6 @@
1
- export { a as PartialPlanTask, b as PlanParseError, j as extractJsonObject, p as parsePartialPlan, n as parsePlanJson } from './parsing-DDZ1rUYa.mjs';
2
- import './Task-xqdxugt2.mjs';
1
+ export { a as PartialPlanTask, b as PlanParseError, l as extractJsonObject, q as parsePartialPlan, r as parsePlanJson } from './parsing-B6ctfF80.mjs';
2
+ import './Task-BnLxkVQF.mjs';
3
3
  import 'child_process';
4
4
  import 'events';
5
- import './ModeResolver-rqaY_4Ex.mjs';
5
+ import 'zod/v4';
6
+ import './ModeResolver-DZzJSdIU.mjs';
package/dist/parsing.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- export { a as PartialPlanTask, b as PlanParseError, j as extractJsonObject, p as parsePartialPlan, n as parsePlanJson } from './parsing-COPG8-Ep.js';
2
- import './Task-xqdxugt2.js';
1
+ export { a as PartialPlanTask, b as PlanParseError, l as extractJsonObject, q as parsePartialPlan, r as parsePlanJson } from './parsing-ddZXZFP4.js';
2
+ import './Task-BnLxkVQF.js';
3
3
  import 'child_process';
4
4
  import 'events';
5
- import './ModeResolver-ByNpC0EP.js';
5
+ import 'zod/v4';
6
+ import './ModeResolver-DkMYf4uS.js';
package/dist/parsing.js CHANGED
@@ -51,9 +51,12 @@ function createTask(overrides = {}) {
51
51
  autonomy: overrides.autonomy,
52
52
  sliceType: overrides.sliceType,
53
53
  userStoriesCovered: overrides.userStoriesCovered,
54
- ...overrides.ops === true && (overrides.type ?? "ai") === "ai" ? { ops: true } : {}
54
+ ...opsFlag(overrides.ops, overrides.type) ? { ops: true } : {}
55
55
  };
56
56
  }
57
+ function opsFlag(value, type = "ai") {
58
+ return value === true && type === "ai" ? true : void 0;
59
+ }
57
60
 
58
61
  // src/services/JsonExtractor.ts
59
62
  var PLAN_ENVELOPE_KEY = "tasks";
@@ -263,25 +266,37 @@ function parsePlanObject(json, raw, runners, runnerModes, autonomousDefault = tr
263
266
  const detail = err instanceof Error ? err.message : String(err);
264
267
  throw new PlanParseError(`Plan response was not valid JSON: ${detail}`, raw);
265
268
  }
266
- if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.tasks)) {
267
- throw new PlanParseError("Invalid plan: missing tasks array", raw);
269
+ const result = validatePlanTasks(parsed, runners, runnerModes, autonomousDefault);
270
+ if (result.ok) return result.tasks;
271
+ const [first] = result.errors;
272
+ throw new PlanParseError(first.message, raw, { semantic: first.taskId !== void 0 });
273
+ }
274
+ function validatePlanTasks(obj, runners, runnerModes, autonomousDefault = true) {
275
+ const rawTasks = isRecord(obj) ? obj.tasks : void 0;
276
+ if (!Array.isArray(rawTasks)) {
277
+ return { ok: false, errors: [{ field: "tasks", message: "Invalid plan: missing tasks array" }] };
268
278
  }
269
- const rawTasks = parsed.tasks;
270
279
  if (rawTasks.length === 0) {
271
- throw new PlanParseError("Plan contained no tasks", raw);
280
+ return { ok: false, errors: [{ field: "tasks", message: "Plan contained no tasks" }] };
272
281
  }
273
- const tasks = rawTasks.map((t) => parseTask(t, runners, runnerModes, autonomousDefault));
274
- validateVerticalSliceShape(tasks, raw);
282
+ const tasks = rawTasks.map((t) => parseTask(asRecord(t), runners, runnerModes, autonomousDefault));
283
+ const errors = verticalSliceErrors(tasks);
275
284
  for (const t of tasks) {
276
285
  if (!runners.includes(t.assignedRunner)) {
277
- throw new PlanParseError(
278
- `Task "${t.title}" has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`,
279
- raw,
280
- { semantic: true }
281
- );
286
+ errors.push({
287
+ taskId: t.id,
288
+ field: "assignedRunner",
289
+ message: `Task "${t.title}" has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`
290
+ });
282
291
  }
283
292
  }
284
- return tasks;
293
+ return errors.length > 0 ? { ok: false, errors } : { ok: true, tasks };
294
+ }
295
+ function isRecord(value) {
296
+ return typeof value === "object" && value !== null && !Array.isArray(value);
297
+ }
298
+ function asRecord(value) {
299
+ return isRecord(value) ? value : {};
285
300
  }
286
301
  function parseTask(raw, runners, runnerModes, autonomousDefault = true, inherited) {
287
302
  const assignedRunner = typeof raw.assignedRunner === "string" ? raw.assignedRunner : runners[0] ?? "claude-code";
@@ -301,12 +316,12 @@ function parseTask(raw, runners, runnerModes, autonomousDefault = true, inherite
301
316
  // description/title — same fallback TaskOps.applyTaskOps uses for `add`, so
302
317
  // a task never silently ends up unschedulable (getReadyTasks requires prompt).
303
318
  prompt: raw.prompt ? String(raw.prompt) : description || title || void 0,
304
- userSteps: Array.isArray(raw.userSteps) ? raw.userSteps.map((s) => ({
319
+ userSteps: Array.isArray(raw.userSteps) ? raw.userSteps.map(asRecord).map((s) => ({
305
320
  order: Number(s.order ?? 0),
306
321
  instruction: String(s.instruction ?? ""),
307
322
  completed: false
308
323
  })) : void 0,
309
- subtasks: Array.isArray(raw.subtasks) ? raw.subtasks.map((s) => parseTask(s, runners, runnerModes, autonomousDefault, { sliceType, autonomy })) : [],
324
+ subtasks: Array.isArray(raw.subtasks) ? raw.subtasks.map((s) => parseTask(asRecord(s), runners, runnerModes, autonomousDefault, { sliceType, autonomy })) : [],
310
325
  assignedModel: raw.assignedModel ? {
311
326
  modelId: String(raw.assignedModel.modelId ?? ""),
312
327
  modelLabel: String(raw.assignedModel.modelLabel ?? ""),
@@ -323,46 +338,32 @@ function parseTask(raw, runners, runnerModes, autonomousDefault = true, inherite
323
338
  autonomy,
324
339
  sliceType,
325
340
  userStoriesCovered: Array.isArray(raw.userStoriesCovered) ? raw.userStoriesCovered.map(String) : void 0,
326
- // A subtask runs with its parent, so only a top-level AI task carries it (ADR-0020).
327
- ops: !inherited && taskType === "ai" && raw.ops === true
341
+ // A subtask runs with its parent, so only a top-level task carries it (ADR-0020).
342
+ ops: inherited ? void 0 : opsFlag(raw.ops)
328
343
  });
329
344
  }
330
- function validateVerticalSliceShape(tasks, raw, parentTitle) {
345
+ function verticalSliceErrors(tasks, parentTitle) {
331
346
  const where = (t) => parentTitle ? `subtask "${t.title}" of "${parentTitle}"` : `Task "${t.title}"`;
332
- const semantic = { semantic: true };
347
+ const errors = [];
333
348
  for (const t of tasks) {
349
+ const fail = (field, message) => errors.push({ taskId: t.id, field, message });
334
350
  if (!t.sliceType) {
335
- throw new PlanParseError(
336
- `${where(t)} is missing sliceType. Every task must specify sliceType ("AFK" or "HITL").`,
337
- raw,
338
- semantic
339
- );
351
+ fail("sliceType", `${where(t)} is missing sliceType. Every task must specify sliceType ("AFK" or "HITL").`);
340
352
  }
341
353
  if (t.type === "ai" && !t.autonomy) {
342
- throw new PlanParseError(
343
- `AI ${where(t)} is missing autonomy. Every AI task must specify autonomy ("AFK" or "HITL").`,
344
- raw,
345
- semantic
346
- );
354
+ fail("autonomy", `AI ${where(t)} is missing autonomy. Every AI task must specify autonomy ("AFK" or "HITL").`);
347
355
  }
348
356
  if (t.autonomy === "AFK" && t.userSteps && t.userSteps.length > 0) {
349
- throw new PlanParseError(
350
- `${where(t)} has autonomy "AFK" but contains userSteps. AFK tasks must not have user touchpoints.`,
351
- raw,
352
- semantic
353
- );
357
+ fail("userSteps", `${where(t)} has autonomy "AFK" but contains userSteps. AFK tasks must not have user touchpoints.`);
354
358
  }
355
- if (t.type === "user" && t.sliceType !== "HITL") {
356
- throw new PlanParseError(
357
- `User ${where(t)} has sliceType "${t.sliceType}". User tasks must have sliceType "HITL".`,
358
- raw,
359
- semantic
360
- );
359
+ if (t.type === "user" && t.sliceType && t.sliceType !== "HITL") {
360
+ fail("sliceType", `User ${where(t)} has sliceType "${t.sliceType}". User tasks must have sliceType "HITL".`);
361
361
  }
362
362
  if (t.subtasks.length > 0) {
363
- validateVerticalSliceShape(t.subtasks, raw, t.title);
363
+ errors.push(...verticalSliceErrors(t.subtasks, t.title));
364
364
  }
365
365
  }
366
+ return errors;
366
367
  }
367
368
 
368
369
  // src/services/PartialPlanParser.ts