zob-harness 0.9.0 → 0.9.2

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 (84) hide show
  1. package/.pi/capabilities/zob-public-runtime-capabilities.json +16 -17
  2. package/.pi/extensions/zob-harness/index.ts +1 -1
  3. package/.pi/extensions/zob-harness/src/domains/autonomy/autonomous-runtime/dry-run.ts +1107 -0
  4. package/.pi/extensions/zob-harness/src/domains/autonomy/autonomous-runtime/report-writers.ts +325 -0
  5. package/.pi/extensions/zob-harness/src/domains/autonomy/autonomous-runtime/smoke-run.ts +1286 -0
  6. package/.pi/extensions/zob-harness/src/domains/autonomy/autonomous-runtime/types.ts +30 -0
  7. package/.pi/extensions/zob-harness/src/domains/autonomy/autonomous-runtime/validation.ts +184 -0
  8. package/.pi/extensions/zob-harness/src/domains/autonomy/autonomous-runtime.ts +4 -2912
  9. package/.pi/extensions/zob-harness/src/domains/compute/compute-profile.ts +2 -1
  10. package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts +24 -3
  11. package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/types.ts +1 -0
  12. package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer.ts +5 -3
  13. package/.pi/extensions/zob-harness/src/domains/delegation/child-runner.ts +28 -3
  14. package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/constants.ts +19 -0
  15. package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/formatting.ts +148 -0
  16. package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/normalize.ts +476 -0
  17. package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/operations.ts +393 -0
  18. package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/parsing.ts +277 -0
  19. package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/reducer.ts +110 -0
  20. package/.pi/extensions/zob-harness/src/domains/goal/goal-todos.ts +6 -1429
  21. package/.pi/extensions/zob-harness/src/domains/governance/governed-requests.ts +3 -1
  22. package/.pi/extensions/zob-harness/src/domains/governance/merge-queue.ts +3 -1
  23. package/.pi/extensions/zob-harness/src/domains/governance/sandbox/helpers.ts +124 -0
  24. package/.pi/extensions/zob-harness/src/domains/governance/sandbox/runners.ts +444 -0
  25. package/.pi/extensions/zob-harness/src/domains/governance/sandbox/simulation.ts +569 -0
  26. package/.pi/extensions/zob-harness/src/domains/governance/sandbox/types.ts +127 -0
  27. package/.pi/extensions/zob-harness/src/domains/governance/sandbox/validation.ts +273 -0
  28. package/.pi/extensions/zob-harness/src/domains/governance/sandbox.ts +4 -1508
  29. package/.pi/extensions/zob-harness/src/domains/governance/worker-pool.ts +3 -1
  30. package/.pi/extensions/zob-harness/src/domains/governance/workspace-claims.ts +3 -1
  31. package/.pi/extensions/zob-harness/src/domains/orchestration/room.ts +8 -2
  32. package/.pi/extensions/zob-harness/src/domains/promotion/coms.ts +8 -1
  33. package/.pi/extensions/zob-harness/src/runtime/commands/autonomy.ts +188 -0
  34. package/.pi/extensions/zob-harness/src/runtime/commands/compute.ts +165 -0
  35. package/.pi/extensions/zob-harness/src/runtime/commands/daemon.ts +191 -0
  36. package/.pi/extensions/zob-harness/src/runtime/commands/delegates.ts +47 -0
  37. package/.pi/extensions/zob-harness/src/runtime/commands/goal.ts +70 -0
  38. package/.pi/extensions/zob-harness/src/runtime/commands/intent.ts +383 -0
  39. package/.pi/extensions/zob-harness/src/runtime/commands/misc.ts +229 -0
  40. package/.pi/extensions/zob-harness/src/runtime/commands/project-dna.ts +130 -0
  41. package/.pi/extensions/zob-harness/src/runtime/commands/types.ts +3 -0
  42. package/.pi/extensions/zob-harness/src/runtime/commands/zcommit.ts +145 -0
  43. package/.pi/extensions/zob-harness/src/runtime/commands/zlive.ts +1606 -0
  44. package/.pi/extensions/zob-harness/src/runtime/commands/zmode.ts +42 -0
  45. package/.pi/extensions/zob-harness/src/runtime/commands.ts +26 -3109
  46. package/.pi/extensions/zob-harness/src/runtime/events.ts +67 -33
  47. package/.pi/extensions/zob-harness/src/runtime/goal-runtime/commands.ts +194 -0
  48. package/.pi/extensions/zob-harness/src/runtime/goal-runtime/events.ts +81 -0
  49. package/.pi/extensions/zob-harness/src/runtime/goal-runtime/state.ts +662 -0
  50. package/.pi/extensions/zob-harness/src/runtime/goal-runtime/tools.ts +1005 -0
  51. package/.pi/extensions/zob-harness/src/runtime/goal-runtime.ts +5 -1949
  52. package/.pi/extensions/zob-harness/src/runtime/tools-delegation/helpers.ts +786 -0
  53. package/.pi/extensions/zob-harness/src/runtime/tools-delegation/register.ts +1120 -0
  54. package/.pi/extensions/zob-harness/src/runtime/tools-delegation/types.ts +77 -0
  55. package/.pi/extensions/zob-harness/src/runtime/tools-delegation.ts +1 -1904
  56. package/.pi/extensions/zob-harness/src/runtime/zob-intro.ts +46 -15
  57. package/.pi/factories/project-dna/batch-manifest.json +1 -1
  58. package/.pi/factories/project-dna/pilot-manifest.json +1 -1
  59. package/.pi/factories/project-dna/smoke-manifest.json +1 -1
  60. package/README.md +29 -8
  61. package/package.json +14 -5
  62. package/scripts/git-ops/commit-policy-smoke.mjs +33 -6
  63. package/scripts/goal-todo/child-goal-ref-smoke.mjs +30 -3
  64. package/scripts/goal-todo/handoff-static-smoke.mjs +31 -3
  65. package/scripts/harness-intake/lib/cli-io.mjs +89 -0
  66. package/scripts/harness-intake/lib/constants.mjs +59 -0
  67. package/scripts/harness-intake/lib/infer-spec.mjs +127 -0
  68. package/scripts/harness-intake/lib/profiles.mjs +458 -0
  69. package/scripts/harness-intake/lib/run-init.mjs +307 -0
  70. package/scripts/harness-intake/lib/scan.mjs +266 -0
  71. package/scripts/harness-intake/lib/tmux.mjs +92 -0
  72. package/scripts/harness-intake/lib/validate.mjs +152 -0
  73. package/scripts/harness-intake/lib.mjs +8 -1521
  74. package/scripts/harness-switch/static-smoke.mjs +1 -1
  75. package/scripts/model-catalog/validate-economy.mjs +3 -1
  76. package/scripts/model-catalog/validate.mjs +3 -1
  77. package/scripts/project-dna/scan/scan.mjs +5 -2
  78. package/scripts/project-dna/scan/validate-scan-artifacts.mjs +3 -1
  79. package/scripts/project-dna/validation/validate-ontology.mjs +3 -1
  80. package/scripts/project-dna/validation/validate-scaffold.mjs +2 -2
  81. package/scripts/zagent-static-smoke.mjs +30 -2
  82. package/scripts/zpeer-local-e2e-smoke.mjs +18 -0
  83. package/scripts/zpeer-static-smoke.mjs +40 -5
  84. package/scripts/zteam-hot-add/smoke.mjs +30 -2
@@ -1,3 +1,4 @@
1
+ import type { Dirent } from "node:fs";
1
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
3
  import { basename, extname, join, relative, resolve } from "node:path";
3
4
 
@@ -261,7 +262,7 @@ function collectTargetStats(repoRoot: string, targetPath: string | undefined): T
261
262
  stats.maxFilesReached = true;
262
263
  return;
263
264
  }
264
- let entries;
265
+ let entries: Dirent[];
265
266
  try {
266
267
  entries = readdirSync(absolutePath, { withFileTypes: true });
267
268
  } catch (error) {
@@ -97,6 +97,21 @@ function readLeasesFromDir(dir: string, nowMs: number, teamName?: string): ZobLi
97
97
  .map((lease) => leaseToPeerCard(lease, nowMs));
98
98
  }
99
99
 
100
+ function peerRegistryKey(peer: Pick<ZobLivePeerCard, "projectId" | "roleId" | "sessionHash">): string {
101
+ return `${peer.projectId}:${peer.roleId}:${peer.sessionHash}`;
102
+ }
103
+
104
+ function mergeLeaseBackedAndAdhocPeers(leasePeers: ZobLivePeerCard[], agentPeers: ZobLivePeerCard[]): ZobLivePeerCard[] {
105
+ const byKey = new Map<string, ZobLivePeerCard>();
106
+ for (const peer of leasePeers) byKey.set(peerRegistryKey(peer), peer);
107
+ for (const peer of agentPeers) {
108
+ if (peer.zpeerAdhoc !== true) continue;
109
+ const key = peerRegistryKey(peer);
110
+ if (!byKey.has(key)) byKey.set(key, peer);
111
+ }
112
+ return [...byKey.values()];
113
+ }
114
+
100
115
  function boundedOfflinePeerRetentionMs(value: number | undefined): number {
101
116
  const env = Number.parseInt(process.env.ZOB_COMS_OFFLINE_PEER_RETENTION_MS ?? "", 10);
102
117
  const raw = typeof value === "number" && Number.isFinite(value) ? value : Number.isFinite(env) ? env : DEFAULT_OFFLINE_PEER_RETENTION_MS;
@@ -514,8 +529,13 @@ export function unregisterCurrentZobLivePeer(repoRoot: string, teamName = "zob-c
514
529
  export function readZobLiveRegistrySnapshot(repoRoot: string, teamName?: string): ZobLiveRegistrySnapshot {
515
530
  const { dir, projectId, kind } = projectLeasesDir(repoRoot);
516
531
  const nowMs = Date.now();
517
- if (existsSync(dir)) return buildSnapshot(projectId, kind, readLeasesFromDir(dir, nowMs, teamName), teamName);
518
532
  const agents = projectAgentsDir(repoRoot);
533
+ if (existsSync(dir)) {
534
+ return buildSnapshot(projectId, kind, mergeLeaseBackedAndAdhocPeers(
535
+ readLeasesFromDir(dir, nowMs, teamName),
536
+ readPeerCardsFromAgentsDir(agents.dir, nowMs, teamName),
537
+ ), teamName);
538
+ }
519
539
  return buildSnapshot(agents.projectId, agents.kind, readPeerCardsFromAgentsDir(agents.dir, nowMs, teamName), teamName);
520
540
  }
521
541
 
@@ -523,9 +543,10 @@ export function readZobLiveRegistryAllProjectsSnapshot(repoRoot: string, teamNam
523
543
  const { projectId, kind } = projectAgentsDir(repoRoot);
524
544
  const nowMs = Date.now();
525
545
  const leaseDirs = allProjectLeasesDirs();
546
+ const agentPeers = allProjectAgentsDirs().flatMap((dir) => readPeerCardsFromAgentsDir(dir, nowMs, teamName));
526
547
  const hasLeaseDomain = leaseDirs.some((dir) => existsSync(dir));
527
548
  const peers = hasLeaseDomain
528
- ? leaseDirs.flatMap((dir) => readLeasesFromDir(dir, nowMs, teamName))
529
- : allProjectAgentsDirs().flatMap((dir) => readPeerCardsFromAgentsDir(dir, nowMs, teamName));
549
+ ? mergeLeaseBackedAndAdhocPeers(leaseDirs.flatMap((dir) => readLeasesFromDir(dir, nowMs, teamName)), agentPeers)
550
+ : agentPeers;
530
551
  return buildSnapshot(projectId, kind, peers, teamName);
531
552
  }
@@ -109,6 +109,7 @@ export interface ZobLivePeerCard {
109
109
  zpeerActiveRoomId?: string;
110
110
  zpeerMemberships?: ZpeerRoomMembership[];
111
111
  zpeerLocalOnly?: true;
112
+ zpeerAdhoc?: true;
112
113
  staleAfterMs: number;
113
114
  offlineAfterMs: number;
114
115
  bodyStored: false;
@@ -234,7 +234,7 @@ export function refreshZpeerSelf(repoRoot: string, peer: ZobLivePeerCard, roomId
234
234
  const ensured = ensureZpeerFields(repoRoot, peer, roomId, alias, restoredMemberships);
235
235
  if (!hasLocalSocketEndpointEvidence(ensured)) return ensured;
236
236
  const refreshed = writeZobLivePeerCard(repoRoot, { ...ensured, heartbeatAt: new Date().toISOString(), status: "online" });
237
- writeZobLiveTeamAgentLease(repoRoot, refreshed, { reason: "zpeer_refresh" });
237
+ if (refreshed.zpeerAdhoc !== true) writeZobLiveTeamAgentLease(repoRoot, refreshed, { reason: "zpeer_refresh" });
238
238
  return refreshed;
239
239
  }
240
240
 
@@ -479,8 +479,10 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
479
479
  };
480
480
 
481
481
  if (!selfMembership) return finish("attempt", { status: "blocked", reason: `current peer is not a member of room '${roomId}'`, targetAlias: targetAlias ?? undefined, taskHash, bodyStored: false });
482
- const leaseOwnership = ownsZobLiveTeamAgentLease(repoRoot, self);
483
- if (!leaseOwnership.owned) return finish("attempt", { status: "blocked", reason: `current peer does not own stable team-agent lease (${leaseOwnership.reason})`, targetAlias: targetAlias ?? undefined, taskHash, bodyStored: false });
482
+ if (self.zpeerAdhoc !== true) {
483
+ const leaseOwnership = ownsZobLiveTeamAgentLease(repoRoot, self);
484
+ if (!leaseOwnership.owned) return finish("attempt", { status: "blocked", reason: `current peer does not own stable team-agent lease (${leaseOwnership.reason})`, targetAlias: targetAlias ?? undefined, taskHash, bodyStored: false });
485
+ }
484
486
  if (selfMembership.role === "observer") return finish("attempt", { status: "blocked", reason: `current peer is observer-only in room '${roomId}'`, targetAlias: targetAlias ?? undefined, taskHash, bodyStored: false });
485
487
  if (!targetAlias) return finish("attempt", { status: "blocked", reason: "invalid target alias", bodyStored: false });
486
488
  if (!transientPrompt.trim()) return finish("attempt", { status: "blocked", reason: "empty peer prompt", targetAlias, bodyStored: false });
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync } from "node:fs";
3
3
  import { mkdtemp, rm } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
+ import { getAgentDir, type ExtensionContext } from "@earendil-works/pi-coding-agent";
7
7
 
8
8
  import { discoverAgents } from "./agents.js";
9
9
  import { SUPERVISED_READONLY_CHILD_TOOLS } from "../../core/constants.js";
@@ -24,6 +24,28 @@ function getPiInvocation(args: string[]): { command: string; args: string[] } {
24
24
  return { command: "pi", args };
25
25
  }
26
26
 
27
+ function childModelPattern(ctx: ExtensionContext, agent: HarnessAgent, modelOverride: string | undefined): string | undefined {
28
+ if (modelOverride?.trim()) return modelOverride.trim();
29
+ if (agent.model?.trim()) return agent.model.trim();
30
+ const model = ctx.model;
31
+ if (!model?.provider || !model.id) return undefined;
32
+ return `${model.provider}/${model.id}`;
33
+ }
34
+
35
+ function providerFromModelPattern(model: string | undefined): string | undefined {
36
+ if (!model) return undefined;
37
+ const [provider] = model.split("/");
38
+ return provider && provider !== model ? provider : undefined;
39
+ }
40
+
41
+ function resolveCodexFastModeExtension(ctx: ExtensionContext, model: string | undefined): string | undefined {
42
+ const provider = ctx.model?.provider ?? providerFromModelPattern(model);
43
+ const usesCodexProvider = provider === "openai-codex" || provider === "codex-auto" || provider?.startsWith("codex-") === true;
44
+ if (!usesCodexProvider) return undefined;
45
+ const extensionPath = join(getAgentDir(), "extensions", "codex-fast-mode.ts");
46
+ return existsSync(extensionPath) ? extensionPath : undefined;
47
+ }
48
+
27
49
  const CHILD_THINKING_LEVELS = new Set<string>(["low", "medium", "high", "xhigh"]);
28
50
 
29
51
  function validateChildThinkingOverride(thinking: string | undefined, fieldName = "thinking"): string[] {
@@ -90,13 +112,14 @@ async function runChildAgent(
90
112
  pathPolicy?: { allowedPaths?: string[]; forbiddenPaths?: string[]; sandboxRoot?: string },
91
113
  thinkingOverride?: ChildThinkingLevel,
92
114
  ): Promise<ChildResult> {
115
+ const resolvedModel = childModelPattern(ctx, agent, modelOverride);
93
116
  const result: ChildResult = {
94
117
  agent: agent.name,
95
118
  task,
96
119
  exitCode: 0,
97
120
  output: "",
98
121
  stderr: "",
99
- model: modelOverride ?? agent.model,
122
+ model: resolvedModel,
100
123
  usage: usageEmpty(),
101
124
  };
102
125
 
@@ -123,10 +146,12 @@ async function runChildAgent(
123
146
 
124
147
  try {
125
148
  const childSafetyExtension = join(ctx.cwd, ".pi", "extensions", "zob-child-safety", "index.ts");
149
+ const childCodexFastModeExtension = resolveCodexFastModeExtension(ctx, resolvedModel);
126
150
  const args = ["--mode", "json", "-p", "--no-extensions"];
151
+ if (childCodexFastModeExtension) args.push("-e", childCodexFastModeExtension);
127
152
  if (existsSync(childSafetyExtension)) args.push("-e", childSafetyExtension);
128
153
  args.push("--session", sessionPath);
129
- const model = modelOverride ?? agent.model;
154
+ const model = resolvedModel;
130
155
  if (model) args.push("--model", model);
131
156
  const thinking = resolveChildThinking(agent, thinkingOverride);
132
157
  if (thinking) args.push("--thinking", thinking);
@@ -0,0 +1,19 @@
1
+ import type { GoalTodoChildGoalStatus, GoalTodoClaimTargetReadiness, GoalTodoClaimValidationConfidence, GoalTodoClaimValidationRecommendedAction, GoalTodoClaimValidationStatus, GoalTodoClaimValidationVerdict, GoalTodoDelegationStatus, GoalTodoOwner, GoalTodoPriority, GoalTodoStatus, GoalTodoStatusClaim } from "../goal-todo-types.js";
2
+
3
+ export const ZOB_GOAL_TODO_ENTRY_TYPE = "zob-goal-todo";
4
+
5
+ export const OPEN_REQUIRED_STATUSES = new Set<GoalTodoStatus>(["planned", "ready", "in_progress", "delegated", "claim_returned", "needs_review", "needs_oracle", "needs_user", "blocked"]);
6
+ export const ACTIVE_STATUSES = new Set<GoalTodoStatus>(["ready", "in_progress", "delegated", "claim_returned", "needs_review"]);
7
+ export const ACTIONABLE_STATUSES = new Set<GoalTodoStatus>(["planned", "ready", "in_progress", "needs_review", "needs_user", "needs_oracle", "blocked"]);
8
+ export const VALID_STATUS: readonly GoalTodoStatus[] = ["planned", "ready", "in_progress", "delegated", "claim_returned", "needs_review", "needs_oracle", "needs_user", "blocked", "done", "skipped"];
9
+ export const VALID_OWNER: readonly GoalTodoOwner[] = ["agent", "user", "oracle", "subagent", "factory", "orchestration"];
10
+ export const VALID_PRIORITY: readonly GoalTodoPriority[] = ["low", "normal", "high", "critical"];
11
+ export const VALID_DELEGATION_STATUS: readonly GoalTodoDelegationStatus[] = ["queued", "running", "claim_returned", "accepted", "rejected", "failed"];
12
+ export const VALID_CHILD_GOAL_STATUS: readonly GoalTodoChildGoalStatus[] = ["ready_for_oracle", "incomplete", "blocked"];
13
+ export const VALID_STATUS_CLAIM: readonly GoalTodoStatusClaim[] = ["done", "incomplete", "blocked"];
14
+ export const VALID_TARGET_READINESS: readonly GoalTodoClaimTargetReadiness[] = ["ready_for_parent_acceptance", "needs_parent_review", "blocked"];
15
+ export const VALID_VALIDATION_STATUS: readonly GoalTodoClaimValidationStatus[] = ["queued", "running", "passed", "warn", "failed", "blocked"];
16
+ export const VALID_VALIDATION_VERDICT: readonly GoalTodoClaimValidationVerdict[] = ["PASS", "WARN", "FAIL"];
17
+ export const VALID_VALIDATION_ACTION: readonly GoalTodoClaimValidationRecommendedAction[] = ["accept_claim", "needs_review", "reject_claim", "block"];
18
+ export const VALID_VALIDATION_CONFIDENCE: readonly GoalTodoClaimValidationConfidence[] = ["LOW", "MEDIUM", "HIGH"];
19
+ export const SHA256_HEX = /^[a-f0-9]{64}$/i;
@@ -0,0 +1,148 @@
1
+ import type { GoalTodoCompletionDiagnostics, GoalTodoNode, GoalTodoState, GoalTodoSummary } from "../goal-todo-types.js";
2
+ import { ACTIONABLE_STATUSES, ACTIVE_STATUSES, OPEN_REQUIRED_STATUSES } from "./constants.js";
3
+ import { cloneNode } from "./normalize.js";
4
+ import { childrenOf, nextValidGoalTodoActions, validateGoalTodoGraph } from "./operations.js";
5
+ import { goalNodes } from "./reducer.js";
6
+
7
+ export function summarizeGoalTodos(todoState: GoalTodoState, goalId?: string): GoalTodoSummary {
8
+ const nodes = goalId ? goalNodes(todoState, goalId) : todoState.nodes.map(cloneNode);
9
+ const done = nodes.filter((node) => node.status === "done").length;
10
+ const skipped = nodes.filter((node) => node.status === "skipped").length;
11
+ const open = nodes.filter((node) => OPEN_REQUIRED_STATUSES.has(node.status)).length;
12
+ const active = nodes.filter((node) => ACTIVE_STATUSES.has(node.status)).length;
13
+ const blocked = nodes.filter((node) => node.status === "blocked").length;
14
+ const delegated = nodes.filter((node) => node.status === "delegated" || node.delegation?.status === "running" || node.delegation?.status === "queued").length;
15
+ const claimReturned = nodes.filter((node) => node.status === "claim_returned").length;
16
+ const validationQueued = nodes.filter((node) => node.validation?.status === "queued").length;
17
+ const validationRunning = nodes.filter((node) => node.validation?.status === "running").length;
18
+ const validationPassed = nodes.filter((node) => node.validation?.status === "passed").length;
19
+ const validationFailed = nodes.filter((node) => node.validation?.status === "failed" || node.validation?.status === "blocked" || node.validation?.status === "warn").length;
20
+ const needsUser = nodes.filter((node) => node.status === "needs_user").length;
21
+ const needsOracle = nodes.filter((node) => node.status === "needs_oracle").length;
22
+ const nextAgent = nodes.find((node) => node.owner === "agent" && (node.status === "ready" || node.status === "planned" || node.status === "in_progress"));
23
+ const nextUser = nodes.find((node) => ACTIONABLE_STATUSES.has(node.status) && (node.owner === "user" || node.status === "needs_user"));
24
+ return {
25
+ goalId,
26
+ total: nodes.length,
27
+ required: nodes.filter((node) => node.required).length,
28
+ done,
29
+ skipped,
30
+ open,
31
+ active,
32
+ blocked,
33
+ delegated,
34
+ claimReturned,
35
+ validationQueued,
36
+ validationRunning,
37
+ validationPassed,
38
+ validationFailed,
39
+ needsUser,
40
+ needsOracle,
41
+ nextAgent,
42
+ nextUser,
43
+ };
44
+ }
45
+
46
+ export function formatGoalTodoSummary(summary: GoalTodoSummary): string {
47
+ if (summary.total === 0) return "todos unset";
48
+ const closed = summary.done + summary.skipped;
49
+ const validation = summary.validationQueued + summary.validationRunning > 0 ? ` · validation ${summary.validationQueued + summary.validationRunning}` : summary.validationFailed > 0 ? ` · validation_alerts ${summary.validationFailed}` : "";
50
+ return `todos ${closed}/${summary.total} · open ${summary.open} · active ${summary.active} · blocked ${summary.blocked} · delegated ${summary.delegated} · claims ${summary.claimReturned}${validation}`;
51
+ }
52
+
53
+ export function formatGoalTodoHudLine(todoState: GoalTodoState, goalId: string | undefined): string {
54
+ if (!goalId) return "todos unset";
55
+ const summary = summarizeGoalTodos(todoState, goalId);
56
+ const next = summary.nextAgent ? ` · next agent ${summary.nextAgent.path} ${summary.nextAgent.title}` : summary.nextUser ? ` · next user ${summary.nextUser.path} ${summary.nextUser.title}` : "";
57
+ return `${formatGoalTodoSummary(summary)}${next}`;
58
+ }
59
+
60
+ export function formatGoalTodoPromptHint(todoState: GoalTodoState, goalId: string | undefined): string {
61
+ if (!goalId) return "- goal_todos: no active runtime goal";
62
+ const summary = summarizeGoalTodos(todoState, goalId);
63
+ if (summary.total === 0) return "- goal_todos: none set; create TODOs for long, multi-step goal work when appropriate";
64
+ return [
65
+ `- goal_todos: ${formatGoalTodoSummary(summary)}`,
66
+ summary.nextAgent ? `- next_agent_todo: ${summary.nextAgent.path} ${summary.nextAgent.title}` : undefined,
67
+ summary.nextUser ? `- next_user_todo: ${summary.nextUser.path} ${summary.nextUser.title}` : undefined,
68
+ summary.claimReturned > 0 ? "- delegated_claims: claim_returned TODOs require parent evidence checks or agentic validation; use resolve_goal_todo(action=auto|complete|accept_claim), complete_goal_todo, /goal todo done, or accept_goal_todo_claim to accept returned claims" : undefined,
69
+ summary.validationQueued + summary.validationRunning > 0 ? `- claim_validation: ${summary.validationQueued + summary.validationRunning} oracle validation(s) queued/running; auto-accept only after PASS/no_ship=false` : undefined,
70
+ "- completion rule: use resolve_goal_todo for done/skip/claim/block/reopen transitions; required TODOs must be done/skipped with evidence before propose_goal_completion",
71
+ ].filter((line): line is string => typeof line === "string").join("\n");
72
+ }
73
+
74
+ export function formatGoalTodoTree(todoState: GoalTodoState, goalId: string | undefined): string {
75
+ if (!goalId) return "No active runtime goal; goal TODOs require a goalId.";
76
+ const nodes = goalNodes(todoState, goalId);
77
+ if (nodes.length === 0) return "No goal TODOs set. Use /goal todo add <title>.";
78
+ const icon = (node: GoalTodoNode): string => {
79
+ if (node.status === "done") return "✓";
80
+ if (node.status === "skipped") return "↷";
81
+ if (node.status === "blocked") return "▲";
82
+ if (node.status === "delegated") return "⇄";
83
+ if (node.status === "claim_returned") return "◇";
84
+ if (node.status === "needs_user") return "?";
85
+ if (node.status === "needs_oracle") return "◆";
86
+ if (node.status === "in_progress") return "●";
87
+ return "○";
88
+ };
89
+ const diagnostics = goalTodoCompletionDiagnostics(todoState, goalId);
90
+ const lines = [formatGoalTodoSummary(summarizeGoalTodos(todoState, goalId)), formatGoalTodoDiagnostics(diagnostics)];
91
+ const walk = (parentId: string | undefined, indent = ""): void => {
92
+ const children = childrenOf(todoState, goalId, parentId);
93
+ children.forEach((node, index) => {
94
+ const last = index === children.length - 1;
95
+ const branch = indent ? (last ? "└─" : "├─") : "";
96
+ const required = node.required ? "req" : "opt";
97
+ const delegation = node.delegation?.runId ? ` · run ${node.delegation.runId}` : "";
98
+ const claim = node.claim?.claimHash ? ` · claim ${node.claim.claimHash.slice(0, 12)}` : "";
99
+ const validation = node.validation ? ` · validation ${node.validation.status}${node.validation.verdict ? `/${node.validation.verdict}` : ""}${node.validation.runId ? ` ${node.validation.runId}` : ""}` : "";
100
+ const blocker = node.blocker ? ` · blocker ${node.blocker}` : "";
101
+ lines.push(`${indent}${branch}${icon(node)} ${node.path} ${node.title} [${node.status}/${node.owner}/${required}/${node.priority}] · id ${node.id}${delegation}${claim}${validation}${blocker}`);
102
+ walk(node.id, `${indent}${last ? " " : "│ "}`);
103
+ });
104
+ };
105
+ walk(undefined);
106
+ return lines.join("\n");
107
+ }
108
+
109
+ export function goalTodoCompletionBlockers(todoState: GoalTodoState, goalId: string | undefined): string[] {
110
+ if (!goalId) return [];
111
+ const nodes = goalNodes(todoState, goalId);
112
+ if (nodes.length === 0) return [];
113
+ const blockers: string[] = [];
114
+ for (const node of nodes) {
115
+ if (node.required && OPEN_REQUIRED_STATUSES.has(node.status)) blockers.push(`todo ${node.path} '${node.title}' is required and ${node.status}`);
116
+ if (node.required && node.status === "skipped" && !node.skipReason?.trim()) blockers.push(`todo ${node.path} '${node.title}' skipped without explicit reason`);
117
+ const evidenceRequired = todoState.policy.requireEvidenceForCritical && (node.priority === "critical" || Boolean(node.delegation) || node.owner === "factory" || node.owner === "orchestration");
118
+ if (node.status === "done" && evidenceRequired && node.evidenceRefs.length === 0 && node.validationCommands.length === 0 && !node.artifacts?.outputHash) blockers.push(`todo ${node.path} '${node.title}' done without evidence`);
119
+ if (node.status === "skipped" && evidenceRequired && node.evidenceRefs.length === 0 && node.validationCommands.length === 0 && !node.artifacts?.outputHash) blockers.push(`todo ${node.path} '${node.title}' skipped without evidence`);
120
+ if (node.status === "done" && node.delegation && node.delegation.status !== "accepted") blockers.push(`todo ${node.path} '${node.title}' delegated claim is not parent-accepted`);
121
+ if (node.status === "done") {
122
+ const openChildren = nodes.filter((candidate) => candidate.parentId === node.id && candidate.required && OPEN_REQUIRED_STATUSES.has(candidate.status));
123
+ if (openChildren.length > 0) blockers.push(`todo ${node.path} '${node.title}' is done but has open required child TODOs`);
124
+ }
125
+ }
126
+ blockers.push(...validateGoalTodoGraph(todoState, goalId));
127
+ return blockers;
128
+ }
129
+
130
+ export function goalTodoCompletionDiagnostics(todoState: GoalTodoState, goalId: string | undefined): GoalTodoCompletionDiagnostics {
131
+ const nodes = goalId ? goalNodes(todoState, goalId) : [];
132
+ const completionBlockers = goalTodoCompletionBlockers(todoState, goalId);
133
+ const reviewNoShip = nodes.some((node) => node.reviewNoShip === true);
134
+ const hardNoShip = completionBlockers.length > 0;
135
+ return {
136
+ completionReady: completionBlockers.length === 0 && !reviewNoShip,
137
+ hardNoShip,
138
+ reviewNoShip,
139
+ effectiveNoShip: hardNoShip || reviewNoShip,
140
+ completionBlockers,
141
+ nextValidActions: Object.fromEntries(nodes.map((node) => [node.id, nextValidGoalTodoActions(node)])),
142
+ };
143
+ }
144
+
145
+ export function formatGoalTodoDiagnostics(diagnostics: GoalTodoCompletionDiagnostics): string {
146
+ const blockers = diagnostics.completionBlockers.slice(0, 3).join(" | ") || "none";
147
+ return `completion_ready=${diagnostics.completionReady} · hard_no_ship=${diagnostics.hardNoShip} · review_no_ship=${diagnostics.reviewNoShip} · effective_no_ship=${diagnostics.effectiveNoShip} · completion_blockers=${blockers}`;
148
+ }