zob-harness 0.9.2 → 0.11.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.
Files changed (31) hide show
  1. package/.pi/capabilities/zob-public-runtime-capabilities.json +49 -1
  2. package/.pi/extensions/zob-harness/index.ts +5 -1
  3. package/.pi/extensions/zob-harness/src/core/constants.ts +7 -4
  4. package/.pi/extensions/zob-harness/src/domains/context/context-discovery.ts +83 -22
  5. package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/formatting.ts +5 -2
  6. package/.pi/extensions/zob-harness/src/domains/plan/plan-todos.ts +661 -0
  7. package/.pi/extensions/zob-harness/src/runtime/commands/plans.ts +93 -0
  8. package/.pi/extensions/zob-harness/src/runtime/commands.ts +3 -0
  9. package/.pi/extensions/zob-harness/src/runtime/delegation-activity.ts +289 -0
  10. package/.pi/extensions/zob-harness/src/runtime/delegation-feed.ts +27 -0
  11. package/.pi/extensions/zob-harness/src/runtime/delegation-monitor.ts +8 -0
  12. package/.pi/extensions/zob-harness/src/runtime/events.ts +56 -3
  13. package/.pi/extensions/zob-harness/src/runtime/plan-capture.ts +203 -11
  14. package/.pi/extensions/zob-harness/src/runtime/plan-launch.ts +166 -0
  15. package/.pi/extensions/zob-harness/src/runtime/state.ts +2 -0
  16. package/.pi/extensions/zob-harness/src/runtime/tools-context.ts +3 -3
  17. package/.pi/extensions/zob-harness/src/runtime/tools-delegation/helpers.ts +31 -1
  18. package/.pi/extensions/zob-harness/src/runtime/tools-delegation/register.ts +3 -0
  19. package/.pi/extensions/zob-harness/src/runtime/tools-plan.ts +70 -0
  20. package/.pi/extensions/zob-harness/src/runtime/widget.ts +14 -4
  21. package/.pi/extensions/zob-harness/src/runtime/zobHarness.ts +3 -0
  22. package/.pi/skills/zob-coms-v2-live/SKILL.md +5 -2
  23. package/.pi/skills/zob-context-discovery/SKILL.md +1 -1
  24. package/.pi/skills/zob-goal-todo-tree/SKILL.md +11 -10
  25. package/.pi/skills/zob-harness/SKILL.md +5 -2
  26. package/.pi/skills/zob-tool-router/SKILL.md +5 -1
  27. package/.pi/skills/zob-zagent-creator/SKILL.md +22 -6
  28. package/AGENTS.md +3 -1
  29. package/package.json +1 -1
  30. package/scripts/context-discovery/shared.mjs +48 -14
  31. package/scripts/context-discovery/smoke.mjs +38 -5
@@ -0,0 +1,661 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, relative, resolve } from "node:path";
3
+
4
+ import { sha256 } from "../../core/utils/hashing.js";
5
+ import { isRecord } from "../../core/utils/records.js";
6
+ import type { GoalTodoOwner, GoalTodoPolicy, GoalTodoPriority, GoalTodoStatus } from "../goal/goal-todo-types.js";
7
+ import { defaultGoalTodoPolicy } from "../goal/goal-todos.js";
8
+
9
+ export const PLAN_TODOS_BLOCK_START = "<!-- ZOB_PLAN_TODOS_START -->";
10
+ export const PLAN_TODOS_BLOCK_END = "<!-- ZOB_PLAN_TODOS_END -->";
11
+ export const PLAN_TODOS_INPUT_SCHEMA = "zob.plan-todos.v1";
12
+ export const PLAN_TODOS_CANONICAL_SCHEMA = "zob.plan-todos.canonical.v1";
13
+ export const PLAN_TODOS_SIDECAR_SCHEMA = "zob.plan-todos.sidecar.v1";
14
+ export const PLAN_TODOS_DISPLAY_CARD_START = "<!-- ZOB_PLAN_TODOS_DISPLAY_CARD_START -->";
15
+ export const PLAN_TODOS_DISPLAY_CARD_END = "<!-- ZOB_PLAN_TODOS_DISPLAY_CARD_END -->";
16
+ const PLAN_TODOS_BLOCK_PATTERN = /<!--\s*ZOB_PLAN_TODOS_START\s*-->\s*(?:```[^\r\n]*\r?\n)?([\s\S]*?)(?:\r?\n```)?\s*<!--\s*ZOB_PLAN_TODOS_END\s*-->/i;
17
+
18
+ export type PlanLaunchStatus = "needs_manifest" | "invalid_manifest" | "launchable" | "launched";
19
+ export type PlanTodoManifestSource = "explicit_block" | "markdown_fallback";
20
+ export type PlanTodoManifestQuality = "explicit" | "fallback_structured";
21
+
22
+ export interface PlanTodoCanonicalItem {
23
+ ref: string;
24
+ parent_ref?: string;
25
+ title: string;
26
+ owner: GoalTodoOwner;
27
+ required: boolean;
28
+ priority: GoalTodoPriority;
29
+ status: Exclude<GoalTodoStatus, "done" | "skipped" | "delegated" | "claim_returned">;
30
+ acceptance_criteria: string[];
31
+ validation_commands: string[];
32
+ }
33
+
34
+ export interface PlanTodoCanonicalManifest {
35
+ schema: typeof PLAN_TODOS_CANONICAL_SCHEMA;
36
+ objective: string;
37
+ max_turns?: number;
38
+ oracle_required: true;
39
+ todos: PlanTodoCanonicalItem[];
40
+ todo_count: number;
41
+ max_depth: number;
42
+ }
43
+
44
+ export interface PlanTodoSidecar extends Omit<PlanTodoCanonicalManifest, "schema"> {
45
+ schema: typeof PLAN_TODOS_SIDECAR_SCHEMA;
46
+ manifest_schema: typeof PLAN_TODOS_CANONICAL_SCHEMA;
47
+ plan_id: string;
48
+ plan_path: string;
49
+ plan_body_hash: string;
50
+ user_request_hash: string;
51
+ assistant_output_hash: string;
52
+ manifest_hash: string;
53
+ manifest_source: PlanTodoManifestSource;
54
+ manifest_quality: PlanTodoManifestQuality;
55
+ manifest_warnings?: string[];
56
+ manifest_errors?: string[];
57
+ launch_status: PlanLaunchStatus;
58
+ created_at: string;
59
+ launched_goal_id?: string;
60
+ launched_at?: string;
61
+ bodyStored: false;
62
+ promptBodiesStored: false;
63
+ }
64
+
65
+ export interface PlanTodoManifestResult {
66
+ found: boolean;
67
+ manifest?: PlanTodoCanonicalManifest;
68
+ errors: string[];
69
+ warnings: string[];
70
+ rawHash?: string;
71
+ source?: PlanTodoManifestSource;
72
+ quality?: PlanTodoManifestQuality;
73
+ }
74
+
75
+ export interface PlanTodoSidecarBuildInput {
76
+ planId: string;
77
+ planPath: string;
78
+ planBodyHash: string;
79
+ userRequestHash: string;
80
+ assistantOutputHash: string;
81
+ createdAt: string;
82
+ manifest: PlanTodoCanonicalManifest;
83
+ manifestSource?: PlanTodoManifestSource;
84
+ manifestQuality?: PlanTodoManifestQuality;
85
+ manifestWarnings?: string[];
86
+ manifestErrors?: string[];
87
+ }
88
+
89
+ const VALID_OWNER: readonly GoalTodoOwner[] = ["agent", "user", "oracle", "subagent", "factory", "orchestration"];
90
+ const VALID_PRIORITY: readonly GoalTodoPriority[] = ["low", "normal", "high", "critical"];
91
+ const VALID_INITIAL_STATUS: readonly PlanTodoCanonicalItem["status"][] = ["planned", "ready", "in_progress", "needs_review", "needs_oracle", "needs_user", "blocked"];
92
+ const REF_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
93
+
94
+ function stringField(record: Record<string, unknown>, ...keys: string[]): string | undefined {
95
+ for (const key of keys) {
96
+ const value = record[key];
97
+ if (typeof value === "string" && value.trim().length > 0) return value.trim();
98
+ }
99
+ return undefined;
100
+ }
101
+
102
+ function boolField(record: Record<string, unknown>, key: string, fallback: boolean): boolean {
103
+ const value = record[key];
104
+ return typeof value === "boolean" ? value : fallback;
105
+ }
106
+
107
+ function positiveIntField(record: Record<string, unknown>, key: string): number | undefined {
108
+ const value = record[key];
109
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
110
+ const integer = Math.trunc(value);
111
+ return integer > 0 ? integer : undefined;
112
+ }
113
+
114
+ function stringArrayField(record: Record<string, unknown>, ...keys: string[]): string[] {
115
+ for (const key of keys) {
116
+ const value = record[key];
117
+ if (Array.isArray(value)) {
118
+ return value
119
+ .flatMap((item) => typeof item === "string" ? [item.trim()] : [])
120
+ .filter((item) => item.length > 0 && !/^(none|n\/a|null)$/i.test(item));
121
+ }
122
+ }
123
+ return [];
124
+ }
125
+
126
+ function enumValue<T extends string>(values: readonly T[], value: unknown, fallback: T): T {
127
+ return typeof value === "string" && (values as readonly string[]).includes(value) ? value as T : fallback;
128
+ }
129
+
130
+ function normalizeRef(raw: string, errors: string[], label: string): string {
131
+ const ref = raw.trim();
132
+ if (!REF_PATTERN.test(ref)) errors.push(`${label} ref '${ref}' must match ${REF_PATTERN.source}`);
133
+ return ref;
134
+ }
135
+
136
+ function maxDepthForTodos(todos: PlanTodoCanonicalItem[]): number {
137
+ const byRef = new Map(todos.map((todo) => [todo.ref, todo]));
138
+ const depthOf = (todo: PlanTodoCanonicalItem, seen = new Set<string>()): number => {
139
+ if (!todo.parent_ref) return 1;
140
+ if (seen.has(todo.ref)) return 999;
141
+ seen.add(todo.ref);
142
+ const parent = byRef.get(todo.parent_ref);
143
+ return parent ? depthOf(parent, seen) + 1 : 2;
144
+ };
145
+ return todos.reduce((max, todo) => Math.max(max, depthOf(todo)), 0);
146
+ }
147
+
148
+ function childrenFrom(record: Record<string, unknown>): unknown[] {
149
+ const candidates = [record.children, record.subtodos, record.sub_todos, record.todos];
150
+ const value = candidates.find((candidate) => Array.isArray(candidate));
151
+ return Array.isArray(value) ? value : [];
152
+ }
153
+
154
+ function normalizeTodoItem(value: unknown, parentRef: string | undefined, path: number[], todos: PlanTodoCanonicalItem[], errors: string[], warnings: string[]): void {
155
+ if (!isRecord(value)) {
156
+ errors.push(`todo ${path.join(".")} must be an object`);
157
+ return;
158
+ }
159
+ const generatedRef = `t${path.join("_")}`;
160
+ const rawRef = stringField(value, "key", "ref", "id") ?? generatedRef;
161
+ if (!stringField(value, "key", "ref", "id")) warnings.push(`todo ${path.join(".")} is missing key/ref; generated ${generatedRef}`);
162
+ const ref = normalizeRef(rawRef, errors, `todo ${path.join(".")}`);
163
+ const explicitParent = stringField(value, "parent_ref", "parentRef", "parent");
164
+ const normalizedParent = explicitParent ? normalizeRef(explicitParent, errors, `todo ${path.join(".")} parent_ref`) : parentRef;
165
+ const title = stringField(value, "title", "name");
166
+ if (!title) errors.push(`todo ${ref} requires non-empty title`);
167
+ const rawStatus = value.status;
168
+ if (typeof rawStatus === "string" && (rawStatus === "done" || rawStatus === "skipped" || rawStatus === "delegated" || rawStatus === "claim_returned")) {
169
+ errors.push(`todo ${ref} status '${rawStatus}' is not a valid initial plan-launch status`);
170
+ }
171
+ const item: PlanTodoCanonicalItem = {
172
+ ref,
173
+ parent_ref: normalizedParent,
174
+ title: title ?? `Untitled TODO ${path.join(".")}`,
175
+ owner: enumValue(VALID_OWNER, value.owner, "agent"),
176
+ required: boolField(value, "required", true),
177
+ priority: enumValue(VALID_PRIORITY, value.priority, "normal"),
178
+ status: enumValue(VALID_INITIAL_STATUS, rawStatus, "planned"),
179
+ acceptance_criteria: stringArrayField(value, "acceptance_criteria", "acceptanceCriteria", "done_when", "doneWhen"),
180
+ validation_commands: stringArrayField(value, "validation_commands", "validationCommands", "checks"),
181
+ };
182
+ todos.push(item);
183
+ childrenFrom(value).forEach((child, index) => normalizeTodoItem(child, ref, [...path, index + 1], todos, errors, warnings));
184
+ }
185
+
186
+ export function extractPlanTodosJson(text: string): { found: boolean; jsonText?: string; errors: string[]; rawHash?: string } {
187
+ const match = text.match(PLAN_TODOS_BLOCK_PATTERN);
188
+ if (!match) return { found: false, errors: [] };
189
+ const jsonText = (match[1] ?? "").trim();
190
+ if (!jsonText) return { found: true, errors: ["ZOB_PLAN_TODOS block is empty"] };
191
+ return { found: true, jsonText, errors: [], rawHash: sha256(jsonText) };
192
+ }
193
+
194
+ export function normalizePlanTodoManifest(value: unknown, options: { defaultObjective?: string; policy?: GoalTodoPolicy } = {}): PlanTodoManifestResult {
195
+ const errors: string[] = [];
196
+ const warnings: string[] = [];
197
+ const policy = options.policy ?? defaultGoalTodoPolicy();
198
+ if (!isRecord(value)) return { found: true, errors: ["plan TODO manifest must be a JSON object"], warnings };
199
+ const schema = typeof value.schema === "string" ? value.schema : PLAN_TODOS_INPUT_SCHEMA;
200
+ if (schema !== PLAN_TODOS_INPUT_SCHEMA && schema !== PLAN_TODOS_CANONICAL_SCHEMA && schema !== PLAN_TODOS_SIDECAR_SCHEMA) {
201
+ errors.push(`unsupported plan TODO schema: ${schema}`);
202
+ }
203
+ const objective = stringField(value, "objective", "goal", "active_goal") ?? options.defaultObjective ?? "ZOB saved plan";
204
+ const rawTodos = Array.isArray(value.todos) ? value.todos : [];
205
+ if (rawTodos.length === 0) errors.push("plan TODO manifest requires at least one todo");
206
+ const todos: PlanTodoCanonicalItem[] = [];
207
+ rawTodos.forEach((item, index) => normalizeTodoItem(item, undefined, [index + 1], todos, errors, warnings));
208
+
209
+ const refs = new Set<string>();
210
+ for (const todo of todos) {
211
+ if (refs.has(todo.ref)) errors.push(`duplicate todo ref: ${todo.ref}`);
212
+ refs.add(todo.ref);
213
+ }
214
+ for (const todo of todos) {
215
+ if (todo.parent_ref && !refs.has(todo.parent_ref)) errors.push(`todo ${todo.ref} references missing parent_ref ${todo.parent_ref}`);
216
+ }
217
+ const byParent = new Map<string | undefined, number>();
218
+ for (const todo of todos) byParent.set(todo.parent_ref, (byParent.get(todo.parent_ref) ?? 0) + 1);
219
+ for (const [parentRef, count] of byParent) {
220
+ if (parentRef && count > policy.maxChildrenPerTodo) errors.push(`parent ${parentRef} exceeds maxChildrenPerTodo=${policy.maxChildrenPerTodo}`);
221
+ }
222
+ if (todos.length > policy.maxOpenTodos) errors.push(`plan TODO count exceeds maxOpenTodos=${policy.maxOpenTodos}`);
223
+ const maxDepth = maxDepthForTodos(todos);
224
+ if (maxDepth > policy.maxTodoDepth) errors.push(`plan TODO depth ${maxDepth} exceeds maxTodoDepth=${policy.maxTodoDepth}`);
225
+
226
+ const manifest: PlanTodoCanonicalManifest = {
227
+ schema: PLAN_TODOS_CANONICAL_SCHEMA,
228
+ objective,
229
+ max_turns: positiveIntField(value, "max_turns") ?? positiveIntField(value, "maxTurns"),
230
+ oracle_required: true,
231
+ todos,
232
+ todo_count: todos.length,
233
+ max_depth: maxDepth,
234
+ };
235
+ return { found: true, manifest: errors.length === 0 ? manifest : undefined, errors: [...new Set(errors)], warnings: [...new Set(warnings)] };
236
+ }
237
+
238
+
239
+ function stripPlanCaptureEnvelope(text: string): string {
240
+ let body = text.replace(/^---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n?/, "");
241
+ const lines = body.split(/\r?\n/);
242
+ const capturedPlanIndex = lines.findIndex((line) => /^\s*##\s+Captured Plan\s*$/i.test(line));
243
+ const scoped = capturedPlanIndex >= 0 ? lines.slice(capturedPlanIndex + 1) : lines;
244
+ const metadataIndex = scoped.findIndex((line) => /^\s*##\s+Metadata\s*$/i.test(line));
245
+ body = (metadataIndex >= 0 ? scoped.slice(0, metadataIndex) : scoped).join("\n");
246
+ return body;
247
+ }
248
+
249
+ function cleanMarkdownListText(raw: string): string {
250
+ return raw
251
+ .replace(/^\[[ xX-]\]\s+/, "")
252
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
253
+ .replace(/__([^_]+)__/g, "$1")
254
+ .replace(/`([^`]+)`/g, "$1")
255
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
256
+ .replace(/\s+/g, " ")
257
+ .trim()
258
+ .replace(/\s*[::]\s*$/, "")
259
+ .trim();
260
+ }
261
+
262
+ function isIgnoredFallbackSection(title: string): boolean {
263
+ const normalized = cleanMarkdownListText(title).toLowerCase();
264
+ return /^(metadata|méta|meta|fichiers?\b|files?\b|likely files?\b|probable files?\b|project files?\b|paths?\b|artefacts?\b|artifacts?\b)/i.test(normalized);
265
+ }
266
+
267
+ function isNonActionableFallbackTitle(title: string): boolean {
268
+ const normalized = title.trim();
269
+ if (normalized.length < 3) return true;
270
+ if (/^(oui|voici|pr[eê]t|ready|note|notes?)\b/i.test(normalized)) return true;
271
+ if (/^(lancer|run|ex[eé]cuter)\s*$/i.test(normalized)) return true;
272
+ if (/^[-–—]+$/.test(normalized)) return true;
273
+ return false;
274
+ }
275
+
276
+ function looksLikeValidationCommand(value: string): boolean {
277
+ const candidate = value.trim().replace(/^`|`$/g, "");
278
+ return /^(?:npm\s+(?:run\s+|test\b|exec\b)|pnpm\s+|yarn\s+|bun\s+|node\s+|npx\s+|tsx\s+|tsc\b|jest\b|vitest\b|pytest\b|python3?\s+|deno\s+|go\s+test\b|cargo\s+(?:test|check|build)\b|make\b)/i.test(candidate);
279
+ }
280
+
281
+ function validationCommandFromListText(raw: string): string | undefined {
282
+ const trimmed = raw.trim();
283
+ const inline = trimmed.match(/`([^`]+)`/);
284
+ if (inline && looksLikeValidationCommand(inline[1] ?? "")) return (inline[1] ?? "").trim();
285
+ const cleaned = trimmed.replace(/^`([^`]+)`$/, "$1").trim();
286
+ return looksLikeValidationCommand(cleaned) ? cleaned : undefined;
287
+ }
288
+
289
+ function addUnique(target: string[], value: string): void {
290
+ if (!target.includes(value)) target.push(value);
291
+ }
292
+
293
+ function fallbackTodo(ref: string, title: string, parentRef?: string): PlanTodoCanonicalItem {
294
+ return {
295
+ ref,
296
+ parent_ref: parentRef,
297
+ title,
298
+ owner: "agent",
299
+ required: true,
300
+ priority: /\b(test|validation|oracle|safety|security|risk)\b/i.test(title) ? "high" : "normal",
301
+ status: "planned",
302
+ acceptance_criteria: parentRef ? [`Completed: ${title}`] : [],
303
+ validation_commands: [],
304
+ };
305
+ }
306
+
307
+ function parseMarkdownFallbackTodos(text: string, allowRootBullets: boolean): PlanTodoCanonicalItem[] {
308
+ const todos: PlanTodoCanonicalItem[] = [];
309
+ const childCounts = new Map<string, number>();
310
+ let currentRoot: PlanTodoCanonicalItem | undefined;
311
+ let rootCount = 0;
312
+ let ignoredSection = false;
313
+ let inFence = false;
314
+
315
+ for (const rawLine of text.split(/\r?\n/)) {
316
+ const line = rawLine.replace(/\t/g, " ");
317
+ if (/^\s*```/.test(line)) {
318
+ inFence = !inFence;
319
+ continue;
320
+ }
321
+ if (inFence) continue;
322
+
323
+ const heading = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/);
324
+ if (heading) {
325
+ ignoredSection = isIgnoredFallbackSection(heading[1] ?? "");
326
+ currentRoot = undefined;
327
+ continue;
328
+ }
329
+
330
+ const numbered = line.match(/^\s{0,3}\d+[.)]\s+(.+)$/);
331
+ if (numbered && !ignoredSection) {
332
+ const title = cleanMarkdownListText(numbered[1] ?? "");
333
+ if (!isNonActionableFallbackTitle(title)) {
334
+ rootCount += 1;
335
+ currentRoot = fallbackTodo(`t${rootCount}`, title);
336
+ todos.push(currentRoot);
337
+ }
338
+ continue;
339
+ }
340
+
341
+ const bullet = line.match(/^(\s*)[-*+]\s+(.+)$/);
342
+ if (!bullet || ignoredSection) continue;
343
+
344
+ const rawText = bullet[2] ?? "";
345
+ const command = validationCommandFromListText(rawText);
346
+ if (command) {
347
+ if (currentRoot) addUnique(currentRoot.validation_commands, command);
348
+ continue;
349
+ }
350
+
351
+ const title = cleanMarkdownListText(rawText);
352
+ if (isNonActionableFallbackTitle(title)) continue;
353
+
354
+ if (currentRoot) {
355
+ const nextChild = (childCounts.get(currentRoot.ref) ?? 0) + 1;
356
+ childCounts.set(currentRoot.ref, nextChild);
357
+ todos.push(fallbackTodo(`${currentRoot.ref}_${nextChild}`, title, currentRoot.ref));
358
+ continue;
359
+ }
360
+
361
+ if (allowRootBullets) {
362
+ rootCount += 1;
363
+ currentRoot = fallbackTodo(`t${rootCount}`, title);
364
+ todos.push(currentRoot);
365
+ }
366
+ }
367
+
368
+ return todos;
369
+ }
370
+
371
+ function fallbackObjectiveFromMarkdown(text: string, defaultObjective?: string): string {
372
+ const heading = text.split(/\r?\n/)
373
+ .map((line) => line.match(/^\s{0,3}#{1,3}\s+(.+?)\s*#*\s*$/)?.[1])
374
+ .find((value): value is string => typeof value === "string" && value.trim().length > 0);
375
+ return cleanMarkdownListText(heading ?? defaultObjective ?? "ZOB saved plan");
376
+ }
377
+
378
+ export function compileMarkdownPlanTodoManifest(text: string, options: { defaultObjective?: string; policy?: GoalTodoPolicy } = {}): PlanTodoManifestResult {
379
+ const body = stripPlanCaptureEnvelope(text);
380
+ const todos = parseMarkdownFallbackTodos(body, false);
381
+ const fallbackTodos = todos.length > 0 ? todos : parseMarkdownFallbackTodos(body, true);
382
+ const warnings = [
383
+ "ZOB_PLAN_TODOS block missing; used deterministic Markdown fallback compiler.",
384
+ "Fallback manifests are best-effort; explicit ZOB_PLAN_TODOS JSON remains the preferred plan contract.",
385
+ ];
386
+ if (fallbackTodos.length === 0) {
387
+ return {
388
+ found: false,
389
+ errors: ["No ZOB_PLAN_TODOS block and deterministic Markdown fallback found no numbered/bulleted TODO list."],
390
+ warnings,
391
+ rawHash: sha256(body),
392
+ source: "markdown_fallback",
393
+ quality: "fallback_structured",
394
+ };
395
+ }
396
+
397
+ const normalized = normalizePlanTodoManifest({
398
+ schema: PLAN_TODOS_CANONICAL_SCHEMA,
399
+ objective: fallbackObjectiveFromMarkdown(body, options.defaultObjective),
400
+ todos: fallbackTodos,
401
+ }, options);
402
+ return {
403
+ ...normalized,
404
+ warnings: [...new Set([...warnings, ...normalized.warnings])],
405
+ rawHash: sha256(body),
406
+ source: "markdown_fallback",
407
+ quality: "fallback_structured",
408
+ };
409
+ }
410
+
411
+ export function extractAndNormalizePlanTodoManifest(text: string, options: { defaultObjective?: string; policy?: GoalTodoPolicy } = {}): PlanTodoManifestResult {
412
+ const block = extractPlanTodosJson(text);
413
+ if (!block.found) return compileMarkdownPlanTodoManifest(text, options);
414
+ if (block.errors.length > 0 || !block.jsonText) return { found: true, errors: block.errors, warnings: [], rawHash: block.rawHash, source: "explicit_block", quality: "explicit" };
415
+ try {
416
+ const parsed = JSON.parse(block.jsonText) as unknown;
417
+ const normalized = normalizePlanTodoManifest(parsed, options);
418
+ return { ...normalized, rawHash: block.rawHash, source: "explicit_block", quality: "explicit" };
419
+ } catch (error) {
420
+ return { found: true, errors: [`invalid JSON in ZOB_PLAN_TODOS block: ${error instanceof Error ? error.message : String(error)}`], warnings: [], rawHash: block.rawHash, source: "explicit_block", quality: "explicit" };
421
+ }
422
+ }
423
+
424
+
425
+ export interface PlanTodoDisplayCardOptions {
426
+ planPath?: string;
427
+ sidecarPath?: string;
428
+ launchStatus?: PlanLaunchStatus;
429
+ source?: PlanTodoManifestSource;
430
+ quality?: PlanTodoManifestQuality;
431
+ maxRoots?: number;
432
+ maxChildrenPerRoot?: number;
433
+ includeRawHint?: boolean;
434
+ includeLaunchHint?: boolean;
435
+ }
436
+
437
+ export interface PlanTodoDisplayRedactionResult {
438
+ text: string;
439
+ changed: boolean;
440
+ manifest?: PlanTodoCanonicalManifest;
441
+ errors: string[];
442
+ warnings: string[];
443
+ source?: PlanTodoManifestSource;
444
+ quality?: PlanTodoManifestQuality;
445
+ }
446
+
447
+ function truncateForDisplay(value: string, max = 96): string {
448
+ const normalized = value.replace(/\s+/g, " ").trim();
449
+ return normalized.length > max ? `${normalized.slice(0, Math.max(0, max - 1)).trimEnd()}…` : normalized;
450
+ }
451
+
452
+ function displaySourceLabel(source: PlanTodoManifestSource | undefined, quality: PlanTodoManifestQuality | undefined): string {
453
+ if (source === "markdown_fallback") return quality === "fallback_structured" ? "Markdown fallback" : "fallback";
454
+ return "explicit manifest";
455
+ }
456
+
457
+ export function formatPlanTodoManifestDisplayCard(manifest: PlanTodoCanonicalManifest | PlanTodoSidecar, options: PlanTodoDisplayCardOptions = {}): string {
458
+ const byParent = new Map<string | undefined, PlanTodoCanonicalItem[]>();
459
+ for (const todo of manifest.todos) {
460
+ const list = byParent.get(todo.parent_ref) ?? [];
461
+ list.push(todo);
462
+ byParent.set(todo.parent_ref, list);
463
+ }
464
+ const roots = byParent.get(undefined) ?? [];
465
+ const childCount = manifest.todos.filter((todo) => Boolean(todo.parent_ref)).length;
466
+ const launchStatus = options.launchStatus ?? ("launch_status" in manifest ? manifest.launch_status : "launchable");
467
+ const source = options.source ?? ("manifest_source" in manifest ? manifest.manifest_source : "explicit_block");
468
+ const quality = options.quality ?? ("manifest_quality" in manifest ? manifest.manifest_quality : source === "markdown_fallback" ? "fallback_structured" : "explicit");
469
+ const maxRoots = options.maxRoots ?? 8;
470
+ const maxChildrenPerRoot = options.maxChildrenPerRoot ?? 3;
471
+ const icon = launchStatus === "invalid_manifest" || launchStatus === "needs_manifest" ? "⚠️" : launchStatus === "launched" ? "🚀" : "✅";
472
+ const title = launchStatus === "launched" ? "ZOB plan launched" : launchStatus === "launchable" ? "ZOB plan launchable" : "ZOB plan manifest";
473
+ const lines: string[] = [
474
+ PLAN_TODOS_DISPLAY_CARD_START,
475
+ `> ${icon} **${title}** · ${manifest.todo_count} TODO${manifest.todo_count === 1 ? "" : "s"}${childCount ? ` · ${childCount} subtask${childCount === 1 ? "" : "s"}` : ""} · depth ${manifest.max_depth}`,
476
+ `> **Objective:** ${truncateForDisplay(manifest.objective, 140)}`,
477
+ `> **Source:** ${displaySourceLabel(source, quality)} · raw JSON hidden from feed · sidecar is canonical`,
478
+ ];
479
+ if (options.planPath) lines.push(`> **Plan:** \`${options.planPath}\``);
480
+ const sidecarPath = options.sidecarPath ?? ("plan_path" in manifest ? planTodoSidecarRelativePath(manifest.plan_path) : options.planPath ? planTodoSidecarRelativePath(options.planPath) : undefined);
481
+ if (sidecarPath) lines.push(`> **TODO sidecar:** \`${sidecarPath}\``);
482
+ lines.push(">", "> **TODO preview**");
483
+ roots.slice(0, maxRoots).forEach((todo) => {
484
+ lines.push(`> ${todo.ref}. ${truncateForDisplay(todo.title)}`);
485
+ const children = byParent.get(todo.ref) ?? [];
486
+ children.slice(0, maxChildrenPerRoot).forEach((child, index) => {
487
+ const prefix = index === children.length - 1 || index === maxChildrenPerRoot - 1 ? "└─" : "├─";
488
+ lines.push(`> ${prefix} ${child.ref} ${truncateForDisplay(child.title)}`);
489
+ });
490
+ if (children.length > maxChildrenPerRoot) lines.push(`> … ${children.length - maxChildrenPerRoot} more subtask${children.length - maxChildrenPerRoot === 1 ? "" : "s"}`);
491
+ });
492
+ if (roots.length > maxRoots) lines.push(`> … ${roots.length - maxRoots} more top-level TODO${roots.length - maxRoots === 1 ? "" : "s"}`);
493
+ if (options.includeLaunchHint !== false) lines.push(">", "> Use `/plan inspect latest_launchable` or `zob_plan_launch` to preview/launch from the sidecar.");
494
+ if (options.includeRawHint !== false) lines.push("> Raw manifest available in the `.todos.json` sidecar; not repeated in chat.");
495
+ lines.push(PLAN_TODOS_DISPLAY_CARD_END);
496
+ return lines.join("\n");
497
+ }
498
+
499
+ export function redactPlanTodosBlockForDisplay(text: string, options: PlanTodoDisplayCardOptions & { defaultObjective?: string; policy?: GoalTodoPolicy } = {}): PlanTodoDisplayRedactionResult {
500
+ const block = extractPlanTodosJson(text);
501
+ if (!block.found) return { text, changed: false, errors: [], warnings: [] };
502
+ if (block.errors.length > 0 || !block.jsonText) return { text, changed: false, errors: block.errors, warnings: [] };
503
+ const normalized = extractAndNormalizePlanTodoManifest(text, { defaultObjective: options.defaultObjective, policy: options.policy });
504
+ if (!normalized.manifest) return { text, changed: false, errors: normalized.errors, warnings: normalized.warnings, source: normalized.source, quality: normalized.quality };
505
+ const card = formatPlanTodoManifestDisplayCard(normalized.manifest, {
506
+ ...options,
507
+ source: normalized.source ?? options.source,
508
+ quality: normalized.quality ?? options.quality,
509
+ launchStatus: options.launchStatus ?? "launchable",
510
+ });
511
+ return {
512
+ text: text.replace(PLAN_TODOS_BLOCK_PATTERN, card),
513
+ changed: true,
514
+ manifest: normalized.manifest,
515
+ errors: normalized.errors,
516
+ warnings: normalized.warnings,
517
+ source: normalized.source,
518
+ quality: normalized.quality,
519
+ };
520
+ }
521
+
522
+ export function canonicalManifestHash(manifest: PlanTodoCanonicalManifest): string {
523
+ return sha256(JSON.stringify(manifest));
524
+ }
525
+
526
+ export function buildPlanTodoSidecar(input: PlanTodoSidecarBuildInput): PlanTodoSidecar {
527
+ const manifestHash = canonicalManifestHash(input.manifest);
528
+ return {
529
+ schema: PLAN_TODOS_SIDECAR_SCHEMA,
530
+ manifest_schema: PLAN_TODOS_CANONICAL_SCHEMA,
531
+ plan_id: input.planId,
532
+ plan_path: input.planPath,
533
+ plan_body_hash: input.planBodyHash,
534
+ user_request_hash: input.userRequestHash,
535
+ assistant_output_hash: input.assistantOutputHash,
536
+ manifest_hash: manifestHash,
537
+ manifest_source: input.manifestSource ?? "explicit_block",
538
+ manifest_quality: input.manifestQuality ?? "explicit",
539
+ manifest_warnings: input.manifestWarnings && input.manifestWarnings.length > 0 ? [...new Set(input.manifestWarnings)] : undefined,
540
+ manifest_errors: input.manifestErrors && input.manifestErrors.length > 0 ? [...new Set(input.manifestErrors)] : undefined,
541
+ launch_status: "launchable",
542
+ created_at: input.createdAt,
543
+ objective: input.manifest.objective,
544
+ max_turns: input.manifest.max_turns,
545
+ oracle_required: true,
546
+ todos: input.manifest.todos,
547
+ todo_count: input.manifest.todo_count,
548
+ max_depth: input.manifest.max_depth,
549
+ bodyStored: false,
550
+ promptBodiesStored: false,
551
+ };
552
+ }
553
+
554
+ export function planTodoSidecarRelativePath(planRelativePath: string): string {
555
+ return planRelativePath.replace(/\.md$/i, ".todos.json");
556
+ }
557
+
558
+ export function safePlanArtifactPath(repoRoot: string, relativePath: string, extension: ".md" | ".json"): { absolutePath: string; relativePath: string; errors: string[] } {
559
+ const normalizedInput = relativePath.replace(/^\.\//, "");
560
+ const absolutePath = resolve(repoRoot, normalizedInput);
561
+ const root = resolve(repoRoot);
562
+ const rel = relative(root, absolutePath).split("\\").join("/");
563
+ const errors: string[] = [];
564
+ if (rel.startsWith("../") || rel === ".." || rel.startsWith("/")) errors.push(`plan artifact path must stay inside repo: ${relativePath}`);
565
+ if (!rel.startsWith("plans/")) errors.push(`plan artifact path must be under plans/: ${relativePath}`);
566
+ if (!rel.endsWith(extension)) errors.push(`plan artifact path must end with ${extension}: ${relativePath}`);
567
+ return { absolutePath, relativePath: rel, errors };
568
+ }
569
+
570
+ export function writePlanTodoSidecar(repoRoot: string, sidecar: PlanTodoSidecar): string {
571
+ const sidecarRelativePath = planTodoSidecarRelativePath(sidecar.plan_path);
572
+ const safe = safePlanArtifactPath(repoRoot, sidecarRelativePath, ".json");
573
+ if (safe.errors.length > 0) throw new Error(safe.errors.join("; "));
574
+ mkdirSync(dirname(safe.absolutePath), { recursive: true });
575
+ writeFileSync(safe.absolutePath, `${JSON.stringify(sidecar, null, 2)}\n`, "utf8");
576
+ return safe.relativePath;
577
+ }
578
+
579
+ export function readPlanTodoSidecar(repoRoot: string, sidecarRelativePath: string): { sidecar?: PlanTodoSidecar; errors: string[] } {
580
+ const safe = safePlanArtifactPath(repoRoot, sidecarRelativePath, ".json");
581
+ if (safe.errors.length > 0) return { errors: safe.errors };
582
+ if (!existsSync(safe.absolutePath)) return { errors: [`plan TODO sidecar not found: ${safe.relativePath}`] };
583
+ try {
584
+ const parsed = JSON.parse(readFileSync(safe.absolutePath, "utf8")) as unknown;
585
+ const validation = validatePlanTodoSidecar(parsed);
586
+ return validation.errors.length > 0 ? { errors: validation.errors } : { sidecar: validation.sidecar, errors: [] };
587
+ } catch (error) {
588
+ return { errors: [`failed to read plan TODO sidecar ${safe.relativePath}: ${error instanceof Error ? error.message : String(error)}`] };
589
+ }
590
+ }
591
+
592
+ export function writeUpdatedPlanTodoSidecar(repoRoot: string, sidecar: PlanTodoSidecar): void {
593
+ const safe = safePlanArtifactPath(repoRoot, planTodoSidecarRelativePath(sidecar.plan_path), ".json");
594
+ if (safe.errors.length > 0) throw new Error(safe.errors.join("; "));
595
+ writeFileSync(safe.absolutePath, `${JSON.stringify(sidecar, null, 2)}\n`, "utf8");
596
+ }
597
+
598
+ export function validatePlanTodoSidecar(value: unknown, policy: GoalTodoPolicy = defaultGoalTodoPolicy()): { sidecar?: PlanTodoSidecar; errors: string[] } {
599
+ if (!isRecord(value)) return { errors: ["plan TODO sidecar must be an object"] };
600
+ if (value.schema !== PLAN_TODOS_SIDECAR_SCHEMA) return { errors: [`unsupported plan TODO sidecar schema: ${String(value.schema)}`] };
601
+ const normalized = normalizePlanTodoManifest({ ...value, schema: PLAN_TODOS_CANONICAL_SCHEMA }, { defaultObjective: typeof value.objective === "string" ? value.objective : undefined, policy });
602
+ const errors = [...normalized.errors];
603
+ for (const key of ["plan_id", "plan_path", "plan_body_hash", "user_request_hash", "assistant_output_hash", "manifest_hash", "created_at"] as const) {
604
+ if (typeof value[key] !== "string" || !value[key].trim()) errors.push(`plan TODO sidecar missing ${key}`);
605
+ }
606
+ if (value.launch_status !== "launchable" && value.launch_status !== "launched") errors.push(`plan TODO sidecar launch_status must be launchable or launched`);
607
+ const manifestSource = enumValue(["explicit_block", "markdown_fallback"] as const, value.manifest_source, "explicit_block");
608
+ const manifestQuality = enumValue(["explicit", "fallback_structured"] as const, value.manifest_quality, manifestSource === "markdown_fallback" ? "fallback_structured" : "explicit");
609
+ if (value.bodyStored !== false || value.promptBodiesStored !== false) errors.push("plan TODO sidecar must be body-free metadata only outside TODO titles/criteria");
610
+ if (normalized.manifest && typeof value.manifest_hash === "string" && canonicalManifestHash(normalized.manifest) !== value.manifest_hash) errors.push("plan TODO sidecar manifest_hash mismatch");
611
+ if (errors.length > 0 || !normalized.manifest) return { errors: [...new Set(errors)] };
612
+ return {
613
+ sidecar: {
614
+ schema: PLAN_TODOS_SIDECAR_SCHEMA,
615
+ manifest_schema: PLAN_TODOS_CANONICAL_SCHEMA,
616
+ plan_id: String(value.plan_id),
617
+ plan_path: String(value.plan_path),
618
+ plan_body_hash: String(value.plan_body_hash),
619
+ user_request_hash: String(value.user_request_hash),
620
+ assistant_output_hash: String(value.assistant_output_hash),
621
+ manifest_hash: String(value.manifest_hash),
622
+ manifest_source: manifestSource,
623
+ manifest_quality: manifestQuality,
624
+ manifest_warnings: stringArrayField(value, "manifest_warnings"),
625
+ manifest_errors: stringArrayField(value, "manifest_errors"),
626
+ launch_status: value.launch_status as PlanLaunchStatus,
627
+ created_at: String(value.created_at),
628
+ launched_goal_id: typeof value.launched_goal_id === "string" ? value.launched_goal_id : undefined,
629
+ launched_at: typeof value.launched_at === "string" ? value.launched_at : undefined,
630
+ objective: normalized.manifest.objective,
631
+ max_turns: normalized.manifest.max_turns,
632
+ oracle_required: true,
633
+ todos: normalized.manifest.todos,
634
+ todo_count: normalized.manifest.todo_count,
635
+ max_depth: normalized.manifest.max_depth,
636
+ bodyStored: false,
637
+ promptBodiesStored: false,
638
+ },
639
+ errors: [],
640
+ };
641
+ }
642
+
643
+ export function formatPlanTodoManifestTree(manifest: PlanTodoCanonicalManifest | PlanTodoSidecar): string {
644
+ const byParent = new Map<string | undefined, PlanTodoCanonicalItem[]>();
645
+ for (const todo of manifest.todos) {
646
+ const list = byParent.get(todo.parent_ref) ?? [];
647
+ list.push(todo);
648
+ byParent.set(todo.parent_ref, list);
649
+ }
650
+ const lines = [`${manifest.objective} · todos ${manifest.todo_count} · depth ${manifest.max_depth}`];
651
+ const walk = (parentRef: string | undefined, indent = ""): void => {
652
+ const children = byParent.get(parentRef) ?? [];
653
+ children.forEach((todo, index) => {
654
+ const last = index === children.length - 1;
655
+ lines.push(`${indent}${indent ? (last ? "└─" : "├─") : ""}${todo.ref} ${todo.title} [${todo.status}/${todo.owner}/${todo.required ? "req" : "opt"}/${todo.priority}]`);
656
+ walk(todo.ref, `${indent}${indent ? (last ? " " : "│ ") : " "}`);
657
+ });
658
+ };
659
+ walk(undefined);
660
+ return lines.join("\n");
661
+ }