zob-harness 0.3.0 → 0.4.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 (119) hide show
  1. package/.pi/agents/harness-factory-designer.md +47 -0
  2. package/.pi/agents/harness-intake-oracle.md +54 -0
  3. package/.pi/agents/harness-intake-orchestrator.md +40 -0
  4. package/.pi/agents/harness-interpreter.md +45 -0
  5. package/.pi/agents/harness-session-miner.md +45 -0
  6. package/.pi/agents/harness-skill-command-analyst.md +43 -0
  7. package/.pi/agents/harness-source-cartographer.md +40 -0
  8. package/.pi/agents/harness-workflow-pattern-miner.md +40 -0
  9. package/.pi/agents/zob-team-architect.md +45 -0
  10. package/.pi/capabilities/zob-public-runtime-capabilities.json +40 -14
  11. package/.pi/extensions/zob-harness/AGENTS.md +21 -21
  12. package/.pi/extensions/zob-harness/src/AGENTS.md +20 -20
  13. package/.pi/extensions/zob-harness/src/domains/autonomy/interactive-autonomy.ts +14 -14
  14. package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts +44 -1
  15. package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer.ts +32 -4
  16. package/.pi/extensions/zob-harness/src/domains/coms/mission-control.ts +4 -1
  17. package/.pi/extensions/zob-harness/src/domains/delegation/output-contracts.ts +37 -37
  18. package/.pi/extensions/zob-harness/src/domains/factory/agentic-plan.ts +1 -1
  19. package/.pi/extensions/zob-harness/src/domains/goal/goal-todo-types.ts +20 -0
  20. package/.pi/extensions/zob-harness/src/domains/goal/goal-todos.ts +60 -5
  21. package/.pi/extensions/zob-harness/src/domains/project-dna/project-dna.ts +1 -1
  22. package/.pi/extensions/zob-harness/src/domains/telemetry/chronicle.ts +1 -1
  23. package/.pi/extensions/zob-harness/src/factory/AGENTS.md +14 -14
  24. package/.pi/extensions/zob-harness/src/orchestration/AGENTS.md +13 -13
  25. package/.pi/extensions/zob-harness/src/runtime/AGENTS.md +14 -14
  26. package/.pi/extensions/zob-harness/src/runtime/commands.ts +2 -2
  27. package/.pi/extensions/zob-harness/src/runtime/events.ts +6 -5
  28. package/.pi/extensions/zob-harness/src/runtime/goal-runtime.ts +376 -0
  29. package/.pi/extensions/zob-harness/src/runtime/mode-intent.ts +10 -10
  30. package/.pi/extensions/zob-harness/src/runtime/plan-capture.ts +4 -4
  31. package/.pi/extensions/zob-harness/src/runtime/state.ts +3 -3
  32. package/.pi/extensions/zob-harness/src/runtime/tools-delegation.ts +57 -7
  33. package/.pi/extensions/zob-harness/src/runtime/widget.ts +2 -2
  34. package/.pi/extensions/zob-harness/src/topology/AGENTS.md +12 -12
  35. package/.pi/extensions/zob-harness/src/utils/AGENTS.md +12 -12
  36. package/.pi/factories/harness-intake-agent-team/README.md +79 -0
  37. package/.pi/factories/harness-intake-agent-team/batch-manifest.json +19 -0
  38. package/.pi/factories/harness-intake-agent-team/factory.json +127 -0
  39. package/.pi/factories/harness-intake-agent-team/pilot-manifest.json +20 -0
  40. package/.pi/factories/harness-intake-agent-team/schemas/artifact-contracts.schema.json +28 -0
  41. package/.pi/factories/harness-intake-agent-team/schemas/factory-candidates.schema.json +27 -0
  42. package/.pi/factories/harness-intake-agent-team/schemas/inferred-run-spec.schema.json +44 -0
  43. package/.pi/factories/harness-intake-agent-team/schemas/sources-index.schema.json +30 -0
  44. package/.pi/factories/harness-intake-agent-team/schemas/team-candidates.schema.json +28 -0
  45. package/.pi/factories/harness-intake-agent-team/schemas/validation.schema.json +17 -0
  46. package/.pi/factories/harness-intake-agent-team/smoke-manifest.json +25 -0
  47. package/.pi/factories/harness-intake-agent-team/templates/generated-agent.md +22 -0
  48. package/.pi/factories/harness-intake-agent-team/templates/kickoff.md +13 -0
  49. package/.pi/factories/harness-intake-agent-team/validators/validate-no-secrets.mjs +20 -0
  50. package/.pi/factories/harness-intake-agent-team/validators/validate-quarantine.mjs +20 -0
  51. package/.pi/factories/harness-intake-agent-team/validators/validate-run.mjs +9 -0
  52. package/.pi/skills/harness-intake/SKILL.md +100 -0
  53. package/.pi/skills/zob-agentic-spec-team/SKILL.md +4 -1
  54. package/.pi/skills/zob-coms-safety/SKILL.md +17 -2
  55. package/.pi/skills/zob-coms-v2-live/SKILL.md +15 -3
  56. package/.pi/skills/zob-factory/SKILL.md +21 -0
  57. package/.pi/skills/zob-goal-todo-tree/SKILL.md +49 -4
  58. package/.pi/skills/zob-harness/SKILL.md +14 -0
  59. package/.pi/skills/zob-owner-pool-drill-writer/SKILL.md +4 -4
  60. package/.pi/skills/zob-owner-pool-launcher/SKILL.md +7 -7
  61. package/.pi/skills/zob-zagent-creator/SKILL.md +13 -3
  62. package/.pi/teams/harness-intake-team.json +114 -0
  63. package/.pi/zagents/agent-factory-pacman-chief.json +22 -0
  64. package/.pi/zagents/agent-factory-pacman-engine-builder.json +21 -0
  65. package/.pi/zagents/agent-factory-pacman-frontend-builder.json +21 -0
  66. package/.pi/zagents/agent-factory-pacman-game-architect.json +21 -0
  67. package/.pi/zagents/agent-factory-pacman-game-designer.json +21 -0
  68. package/.pi/zagents/agent-factory-pacman-qa-oracle.json +21 -0
  69. package/.pi/zagents/prompts/agent-factory-pacman-chief.md +53 -0
  70. package/.pi/zagents/prompts/agent-factory-pacman-engine-builder.md +41 -0
  71. package/.pi/zagents/prompts/agent-factory-pacman-frontend-builder.md +40 -0
  72. package/.pi/zagents/prompts/agent-factory-pacman-game-architect.md +41 -0
  73. package/.pi/zagents/prompts/agent-factory-pacman-game-designer.md +43 -0
  74. package/.pi/zagents/prompts/agent-factory-pacman-qa-oracle.md +51 -0
  75. package/.pi/zteams/agent-factory-pacman-multiplayer-runtime.mjs +384 -0
  76. package/.pi/zteams/agent-factory-pacman-multiplayer.json +42 -0
  77. package/.pi/zteams/agent-factory-pacman-multiplayer.tmux.sh +256 -0
  78. package/.pi/zteams/templates/agent-factory-pacman-chief-kickoff.template.md +71 -0
  79. package/.pi/zteams/templates/agent-factory-pacman-worker-kickoff.template.md +59 -0
  80. package/README.md +264 -109
  81. package/SOURCE_INDEX.md +4 -0
  82. package/examples/agent-factory-mission-control/AGENTS.md +10 -0
  83. package/examples/agent-factory-mission-control/README.md +17 -0
  84. package/examples/agent-factory-mission-control/apps/api/AGENTS.md +3 -0
  85. package/examples/agent-factory-mission-control/apps/dashboard/AGENTS.md +3 -0
  86. package/examples/agent-factory-mission-control/mission.md +3 -0
  87. package/examples/agent-factory-mission-control/output-contract.md +3 -0
  88. package/examples/agent-factory-mission-control/packages/domain/AGENTS.md +3 -0
  89. package/examples/agent-factory-mission-control/packages/snapshot-reader/AGENTS.md +3 -0
  90. package/examples/agent-factory-pacman-multiplayer/AGENTS.md +27 -0
  91. package/examples/agent-factory-pacman-multiplayer/README.md +84 -0
  92. package/examples/agent-factory-pacman-multiplayer/mission.md +43 -0
  93. package/examples/agent-factory-pacman-multiplayer/output-contract.md +58 -0
  94. package/examples/agent-factory-tmux-comms/README.md +146 -0
  95. package/examples/agent-factory-tmux-comms/chief-kickoff.template.md +54 -0
  96. package/examples/agent-factory-tmux-comms/simple-agent-factory.team.json +92 -0
  97. package/examples/agent-factory-tmux-comms/simple-agent-factory.tmux.sh +248 -0
  98. package/examples/agent-factory-tmux-comms/worker-kickoff.template.md +43 -0
  99. package/examples/harness-intake-fixtures/claude-code-mini/.claude/agents/reviewer.md +7 -0
  100. package/examples/harness-intake-fixtures/claude-code-mini/.claude/agents/specifier.md +7 -0
  101. package/examples/harness-intake-fixtures/claude-code-mini/.claude/commands/review.md +7 -0
  102. package/examples/harness-intake-fixtures/claude-code-mini/.claude/commands/spec.md +9 -0
  103. package/examples/harness-intake-fixtures/claude-code-mini/.claude/sessions/session-001.md +10 -0
  104. package/examples/harness-intake-fixtures/claude-code-mini/CLAUDE.md +22 -0
  105. package/package.json +35 -3
  106. package/scripts/README.md +7 -1
  107. package/scripts/goal-todo/child-goal-ref-smoke.mjs +2 -0
  108. package/scripts/goal-todo/handoff-static-smoke.mjs +32 -0
  109. package/scripts/harness-intake/infer-run-spec.mjs +34 -0
  110. package/scripts/harness-intake/launch.mjs +32 -0
  111. package/scripts/harness-intake/lib.mjs +1521 -0
  112. package/scripts/harness-intake/scan-sources.mjs +30 -0
  113. package/scripts/harness-intake/tmux-launch.mjs +48 -0
  114. package/scripts/harness-intake/validate-run.mjs +28 -0
  115. package/scripts/package-surface/validate-capability-refs.mjs +112 -0
  116. package/scripts/project-dna/query/query-context.mjs +1 -1
  117. package/scripts/project-dna/query/query-steward.mjs +1 -1
  118. package/scripts/zagent-static-smoke.mjs +1 -1
  119. package/scripts/zpeer-local-e2e-smoke.mjs +6 -0
@@ -249,10 +249,10 @@ function patternMatchesAny(text: string, patterns: string[]): boolean {
249
249
  });
250
250
  }
251
251
 
252
- const SECRET_ACCESS_VERB_PATTERN = /\b(read|cat|open|inspect|print|show|copy|extract|use|lire|ouvrir|affiche|imprime|copie|extrais|utilise)\b/i;
253
- const SECRET_ACCESS_CONTEXT_PATTERN = /\b(secret|token|api[_ -]?key|private\s+key|ssh\s+key|credential|identifiant)\b.{0,80}\b(read|show|print|copy|use|lire|affiche|copie|utilise)\b/i;
254
- const NEGATIVE_SAFETY_DIRECTIVE_PATTERN = /\b(do not|don't|dont|never|must not|mustn't|avoid|forbidden|denylist|deny list|blocked|without|no\s+secrets?|ne\s+pas|ne\s+jamais|n'ouvre\s+pas|ne\s+lis\s+pas|interdit|sans)\b/i;
255
- const CONTRAST_OR_EXCEPTION_PATTERN = /\b(but|however|except|unless|sauf|mais|pourtant)\b/i;
252
+ const SECRET_ACCESS_VERB_PATTERN = /\b(read|cat|open|inspect|print|show|copy|extract|use)\b/i;
253
+ const SECRET_ACCESS_CONTEXT_PATTERN = /\b(secret|token|api[_ -]?key|private\s+key|ssh\s+key|credential|identifier)\b.{0,80}\b(read|show|print|copy|use)\b/i;
254
+ const NEGATIVE_SAFETY_DIRECTIVE_PATTERN = /\b(do not|don't|dont|never|must not|mustn't|avoid|forbidden|denylist|deny list|blocked|without|no\s+secrets?)\b/i;
255
+ const CONTRAST_OR_EXCEPTION_PATTERN = /\b(but|however|except|unless)\b/i;
256
256
 
257
257
  function isNegativeSecretSafetyLine(line: string, policy: InteractiveAutonomyPolicy): boolean {
258
258
  const trimmed = line.trim();
@@ -278,8 +278,8 @@ function secretAccessRequested(text: string, policy: InteractiveAutonomyPolicy):
278
278
  }
279
279
 
280
280
  function productionApplyRequested(text: string): boolean {
281
- return /\b(deploy|release|ship|apply|write|push|publish|déploie|deploie|publie|livre|applique)\b.{0,80}\b(prod|production|live)\b/i.test(text)
282
- || /\b(prod|production|live)\b.{0,80}\b(deploy|release|ship|apply|write|push|publish|déploie|deploie|publie|livre|applique)\b/i.test(text);
281
+ return /\b(deploy|release|ship|apply|write|push|publish)\b.{0,80}\b(prod|production|live)\b/i.test(text)
282
+ || /\b(prod|production|live)\b.{0,80}\b(deploy|release|ship|apply|write|push|publish)\b/i.test(text);
283
283
  }
284
284
 
285
285
  function extractTargetPaths(text: string): string[] {
@@ -360,14 +360,14 @@ export function scoreMissionReadiness(text: string, options: { mode: Interactive
360
360
  const clarificationCodes: string[] = [];
361
361
  const safetyGateCodes = ["no_secrets", "no_destructive_commands", "no_production_apply", "no_global_production_claim", "validation_required"];
362
362
 
363
- const hasActionVerb = /\b(implement|impl[eé]mente|build|cr[eé]e|create|add|ajoute|fix|corrige|modify|modifie|change|update|mets?|refactor|refactorise|wire|branche|brancher|validate|test|smoke|audit|review|documente|write|edit|fais|make)\b/i.test(raw);
364
- const hasAcceptanceLanguage = /\b(acceptance|criteria|crit[eè]res?|must|must not|doit|ne doit pas|validation|validate|test|smoke|preuve|evidence|oracle|done when|definition of done)\b/i.test(raw);
365
- const hasTestabilityLanguage = /\b(test|tests|smoke|npm run|check|typecheck|validation|validate|oracle|proof|preuve|assert|v[eé]rifie|verify)\b/i.test(raw);
366
- const asksClarificationOnly = /\b(explain|explique|pourquoi|question|aide|help|status|statut|montre|show)\b/i.test(raw) && !hasActionVerb;
363
+ const hasActionVerb = /\b(implement|build|create|add|fix|modify|change|update|refactor|wire|validate|test|smoke|audit|review|document|write|edit|make)\b/i.test(raw);
364
+ const hasAcceptanceLanguage = /\b(acceptance|criteria|must|must not|validation|validate|test|smoke|proof|evidence|oracle|done when|definition of done)\b/i.test(raw);
365
+ const hasTestabilityLanguage = /\b(test|tests|smoke|npm run|check|typecheck|validation|validate|oracle|proof|assert|verify)\b/i.test(raw);
366
+ const asksClarificationOnly = /\b(explain|why|question|help|status|show)\b/i.test(raw) && !hasActionVerb;
367
367
  const destructiveRequested = policy.safety.blockDestructiveCommands && patternMatchesAny(raw, policy.safety.destructivePatterns);
368
368
  const secretRequested = policy.safety.blockSecretAccess && secretAccessRequested(raw, policy);
369
369
  const productionRequested = policy.safety.blockProductionApply && productionApplyRequested(raw);
370
- const globalClaimRequested = /\b(100%|global|production[- ]?wide|full production|prod)\b.{0,80}\b(claim|declare|prouve|prove|certifie|certify|ready|autonomous|autonomie)\b/i.test(raw);
370
+ const globalClaimRequested = /\b(100%|global|production[- ]?wide|full production|prod)\b.{0,80}\b(claim|declare|prove|certify|ready|autonomous|autonomy)\b/i.test(raw);
371
371
 
372
372
  if (!raw) blockerCodes.push("user_input_missing");
373
373
  if (destructiveRequested) blockerCodes.push("destructive_action_requested");
@@ -376,9 +376,9 @@ export function scoreMissionReadiness(text: string, options: { mode: Interactive
376
376
  if (globalClaimRequested) clarificationCodes.push("global_production_claim_requires_fresh_oracle_proof");
377
377
 
378
378
  const clarity = raw.length >= 80 && hasActionVerb ? 0.9 : raw.length >= 35 && hasActionVerb ? 0.72 : hasActionVerb ? 0.55 : asksClarificationOnly ? 0.35 : raw.length > 0 ? 0.25 : 0;
379
- const acceptanceCriteria = hasAcceptanceLanguage ? 0.8 : /\b(done|fini|termin[eé]|livrable|deliverable)\b/i.test(raw) ? 0.5 : 0.15;
380
- const targetPaths = targetPathRefs.length > 0 ? 0.85 : /\b(repo|project|harness|pi|extension|codebase|projet)\b/i.test(raw) ? 0.45 : 0.15;
381
- const testability = hasTestabilityLanguage ? 0.85 : /\b(works|fonctionne|ready|ok)\b/i.test(raw) ? 0.35 : 0.15;
379
+ const acceptanceCriteria = hasAcceptanceLanguage ? 0.8 : /\b(done|complete|finished|deliverable)\b/i.test(raw) ? 0.5 : 0.15;
380
+ const targetPaths = targetPathRefs.length > 0 ? 0.85 : /\b(repo|project|harness|pi|extension|codebase)\b/i.test(raw) ? 0.45 : 0.15;
381
+ const testability = hasTestabilityLanguage ? 0.85 : /\b(works|ready|ok)\b/i.test(raw) ? 0.35 : 0.15;
382
382
  const risk: MissionRiskLevel = blockerCodes.length > 0 || productionRequested ? "high" : /\b(network|browser|cloud|api|deploy|publish|commit|database|db|payment|auth)\b/i.test(normalized) ? "medium" : "low";
383
383
  const safety = risk === "high" ? 0 : risk === "medium" ? 0.55 : 1;
384
384
  const signals: MissionReadinessSignals = {
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
 
@@ -10,6 +10,8 @@ import { readZobComsV2Policy, zobComsRegistryEnabled } from "./policy.js";
10
10
  import type { ZobLivePeerCard, ZobLivePeerStatus, ZobLiveRegistrySnapshot } from "./types.js";
11
11
 
12
12
  const FORBIDDEN_PERSISTED_KEYS = new Set(["body", "task", "prompt", "output", "content", "message", "rationale", "text", "diff", "patch"]);
13
+ const DEFAULT_OFFLINE_PEER_RETENTION_MS = 24 * 60 * 60 * 1000;
14
+ const MIN_OFFLINE_PEER_RETENTION_MS = 5 * 60 * 1000;
13
15
 
14
16
  function registryRoot(): { path: string; kind: "user_runtime" | "env_override" } {
15
17
  const override = process.env.ZOB_COMS_REGISTRY_ROOT;
@@ -54,6 +56,19 @@ function readPeerCardsFromAgentsDir(dir: string, nowMs: number, teamName?: strin
54
56
  .map((peer) => ({ ...peer, status: derivePeerStatus(peer, nowMs) }));
55
57
  }
56
58
 
59
+ function boundedOfflinePeerRetentionMs(value: number | undefined): number {
60
+ const env = Number.parseInt(process.env.ZOB_COMS_OFFLINE_PEER_RETENTION_MS ?? "", 10);
61
+ const raw = typeof value === "number" && Number.isFinite(value) ? value : Number.isFinite(env) ? env : DEFAULT_OFFLINE_PEER_RETENTION_MS;
62
+ return Math.max(MIN_OFFLINE_PEER_RETENTION_MS, Math.floor(raw));
63
+ }
64
+
65
+ function offlinePeerExpired(peer: ZobLivePeerCard, nowMs: number, retentionMs: number): boolean {
66
+ if (derivePeerStatus(peer, nowMs) !== "offline") return false;
67
+ const heartbeatMs = Date.parse(peer.heartbeatAt);
68
+ if (!Number.isFinite(heartbeatMs)) return true;
69
+ return nowMs - heartbeatMs >= Math.max(peer.offlineAfterMs, retentionMs);
70
+ }
71
+
57
72
  function allProjectAgentsDirs(): string[] {
58
73
  const root = registryRoot();
59
74
  const projectsDir = join(root.path, "projects");
@@ -94,6 +109,34 @@ function derivePeerStatus(peer: ZobLivePeerCard, nowMs: number): ZobLivePeerStat
94
109
  return "online";
95
110
  }
96
111
 
112
+ export function pruneExpiredZobLivePeers(repoRoot: string, input: { teamName?: string; nowMs?: number; retentionMs?: number } = {}): { schema: "zob.live-registry-prune.v1"; pruned: number; retained: number; retentionMs: number; bodyStored: false } {
113
+ const { dir } = projectAgentsDir(repoRoot);
114
+ const nowMs = input.nowMs ?? Date.now();
115
+ const retentionMs = boundedOfflinePeerRetentionMs(input.retentionMs);
116
+ let pruned = 0;
117
+ let retained = 0;
118
+ if (!existsSync(dir)) return { schema: "zob.live-registry-prune.v1", pruned, retained, retentionMs, bodyStored: false };
119
+ for (const entry of readdirSync(dir).filter((name) => name.endsWith(".json"))) {
120
+ const filePath = join(dir, entry);
121
+ try {
122
+ const peer = parsePeerCard(JSON.parse(readFileSync(filePath, "utf8")) as unknown);
123
+ if (!peer || (input.teamName && peer.team !== input.teamName)) {
124
+ retained += 1;
125
+ continue;
126
+ }
127
+ if (offlinePeerExpired(peer, nowMs, retentionMs)) {
128
+ unlinkSync(filePath);
129
+ pruned += 1;
130
+ } else {
131
+ retained += 1;
132
+ }
133
+ } catch {
134
+ retained += 1;
135
+ }
136
+ }
137
+ return { schema: "zob.live-registry-prune.v1", pruned, retained, retentionMs, bodyStored: false };
138
+ }
139
+
97
140
  export function writeZobLivePeerCard(repoRoot: string, peer: ZobLivePeerCard): ZobLivePeerCard {
98
141
  if (hasForbiddenPersistedKey(peer)) throw new Error("Refusing to persist ZOB live peer card with forbidden body-like keys");
99
142
  if (peer.bodyStored !== false) throw new Error("ZOB live peer card bodyStored must be false");
@@ -26,6 +26,9 @@ export interface ZpeerRoomSummary {
26
26
  stale: number;
27
27
  offline: number;
28
28
  aliases: string[];
29
+ onlineAliases: string[];
30
+ staleAliases: string[];
31
+ offlineAliases: string[];
29
32
  duplicateAliases: string[];
30
33
  membershipCount?: number;
31
34
  localOnly: true;
@@ -248,9 +251,15 @@ function peersInRoom(repoRoot: string, roomId: string): ZpeerRoomPeer[] {
248
251
 
249
252
  function buildZpeerRoomSummaryFromPeers(projectId: string, self: ZobLivePeerCard | undefined, roomId: string, peers: ZpeerRoomPeer[]): ZpeerRoomSummary {
250
253
  const counts: Record<ZobLivePeerStatus, number> = { online: 0, stale: 0, offline: 0 };
254
+ const statusAliases: Record<ZobLivePeerStatus, string[]> = { online: [], stale: [], offline: [] };
251
255
  const aliases = peers.map((entry) => entry.membership.alias).sort();
252
- for (const entry of peers) counts[zpeerReachableStatus(entry.peer)] += 1;
253
- const duplicateAliases = aliases.filter((alias, index) => aliases.indexOf(alias) !== index).filter((alias, index, all) => all.indexOf(alias) === index);
256
+ for (const entry of peers) {
257
+ const status = zpeerReachableStatus(entry.peer);
258
+ counts[status] += 1;
259
+ statusAliases[status].push(entry.membership.alias);
260
+ }
261
+ const onlineAliases = statusAliases.online.sort();
262
+ const duplicateAliases = onlineAliases.filter((alias, index) => onlineAliases.indexOf(alias) !== index).filter((alias, index, all) => all.indexOf(alias) === index);
254
263
  return {
255
264
  schema: "zob.zpeer-room-summary.v1",
256
265
  projectId,
@@ -261,6 +270,9 @@ function buildZpeerRoomSummaryFromPeers(projectId: string, self: ZobLivePeerCard
261
270
  stale: counts.stale,
262
271
  offline: counts.offline,
263
272
  aliases,
273
+ onlineAliases,
274
+ staleAliases: statusAliases.stale.sort(),
275
+ offlineAliases: statusAliases.offline.sort(),
264
276
  duplicateAliases,
265
277
  membershipCount: self ? zpeerMembershipsForPeer(self).length : undefined,
266
278
  localOnly: true,
@@ -471,8 +483,24 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
471
483
  const candidates = peersInRoom(repoRoot, roomId).filter((entry) => entry.membership.alias === targetAlias && entry.peer.sessionHash !== self.sessionHash);
472
484
  if (targetAlias === senderAlias) return finish("attempt", { status: "blocked", reason: "cannot send to self", targetAlias, taskHash, bodyStored: false });
473
485
  if (candidates.length === 0) return finish("attempt", { status: "blocked", reason: `peer @${targetAlias} not found in room '${roomId}'`, targetAlias, taskHash, bodyStored: false }, 0);
474
- if (candidates.length > 1) return finish("attempt", { status: "blocked", reason: `duplicate alias @${targetAlias} in room '${roomId}'`, targetAlias, taskHash, bodyStored: false }, candidates.length);
475
- const target = candidates[0];
486
+ let liveCandidates = candidates.filter((entry) => zpeerReachableStatus(entry.peer) === "online");
487
+ if (liveCandidates.length > 1) {
488
+ const responsiveCandidates: ZpeerRoomPeer[] = [];
489
+ for (const entry of liveCandidates) {
490
+ if (await peerRespondsToAliasPing(entry.peer)) {
491
+ responsiveCandidates.push(entry);
492
+ } else {
493
+ try { writeZobLivePeerCard(repoRoot, { ...entry.peer, heartbeatAt: new Date().toISOString(), status: "offline" }); } catch { /* best-effort ghost alias release */ }
494
+ }
495
+ }
496
+ liveCandidates = responsiveCandidates;
497
+ }
498
+ if (liveCandidates.length === 0) {
499
+ const statuses = [...new Set(candidates.map((entry) => zpeerReachableStatus(entry.peer)))].sort().join("/") || "offline";
500
+ return finish("attempt", { status: "blocked", reason: `peer @${targetAlias} is ${statuses}`, targetAlias, taskHash, bodyStored: false }, candidates.length);
501
+ }
502
+ if (liveCandidates.length > 1) return finish("attempt", { status: "blocked", reason: `duplicate live alias @${targetAlias} in room '${roomId}'`, targetAlias, taskHash, bodyStored: false }, liveCandidates.length);
503
+ const target = liveCandidates[0];
476
504
  const targetReachableStatus = zpeerReachableStatus(target.peer);
477
505
  if (targetReachableStatus !== "online") return finish("attempt", { status: "blocked", reason: `peer @${targetAlias} is ${targetReachableStatus}`, targetAlias, taskHash, bodyStored: false }, 1);
478
506
  const topologyBlocker = validateZpeerTopology(repoRoot, self, target.peer, roomId, senderAlias, target.membership.alias);
@@ -63,6 +63,7 @@ function summarizeZpeerRooms(peers: Array<Record<string, unknown>>): Array<Recor
63
63
  }
64
64
  return [...rooms.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([roomId, roomPeers]) => {
65
65
  const aliases = roomPeers.map((entry) => entry.alias).filter((alias): alias is string => Boolean(alias)).sort();
66
+ const onlineAliases = roomPeers.filter((entry) => entry.peer.status === "online").map((entry) => entry.alias).filter((alias): alias is string => Boolean(alias)).sort();
66
67
  const sessionHashes = roomPeers.map((entry) => typeof entry.peer.sessionHash === "string" ? entry.peer.sessionHash : undefined).filter((sessionHash): sessionHash is string => Boolean(sessionHash));
67
68
  return {
68
69
  schema: "zob.zpeer-room-summary.v1",
@@ -73,7 +74,9 @@ function summarizeZpeerRooms(peers: Array<Record<string, unknown>>): Array<Recor
73
74
  stale: roomPeers.filter((entry) => entry.peer.status === "stale").length,
74
75
  offline: roomPeers.filter((entry) => entry.peer.status === "offline").length,
75
76
  aliasHashes: aliases.map((alias) => sha256(alias)),
76
- duplicateAliasHashes: aliases.filter((alias, index) => aliases.indexOf(alias) !== index).filter((alias, index, all) => all.indexOf(alias) === index).map((alias) => sha256(alias)),
77
+ onlineAliasHashes: onlineAliases.map((alias) => sha256(alias)),
78
+ duplicateAliasHashes: onlineAliases.filter((alias, index) => onlineAliases.indexOf(alias) !== index).filter((alias, index, all) => all.indexOf(alias) === index).map((alias) => sha256(alias)),
79
+ duplicateAliasScope: "online_only",
77
80
  localOnly: true,
78
81
  networkEnabled: false,
79
82
  bodyStored: false,
@@ -11,17 +11,17 @@ const COMMON_OUTPUT_REQUIREMENTS: OutputRequirement[] = [
11
11
  },
12
12
  {
13
13
  name: "evidence",
14
- pattern: "<evidence>|\\bevidence\\b|preuve|preuves",
14
+ pattern: "<evidence>|\\bevidence\\b|proof",
15
15
  message: "Missing evidence/proof section",
16
16
  },
17
17
  {
18
18
  name: "risks_blockers",
19
- pattern: "<risks_blockers>|risks?/blockers?|risks?|blockers?|risques?|blocage",
19
+ pattern: "<risks_blockers>|risks?/blockers?|risks?|blockers?",
20
20
  message: "Missing risks/blockers section",
21
21
  },
22
22
  {
23
23
  name: "compliance",
24
- pattern: "<compliance>|\\bcompliance\\b|conformité|must not",
24
+ pattern: "<compliance>|\\bcompliance\\b|must not",
25
25
  message: "Missing compliance line",
26
26
  },
27
27
  ];
@@ -42,10 +42,10 @@ const OUTPUT_CONTRACTS: OutputContract[] = [
42
42
  { name: "literal_request", pattern: "<literal_request>|literal request|literal_request", message: "Explore output missing literal_request" },
43
43
  { name: "actual_need", pattern: "<actual_need>|actual need|actual_need", message: "Explore output missing actual_need" },
44
44
  { name: "success_looks_like", pattern: "<success_looks_like>|success looks like|success_looks_like", message: "Explore output missing success_looks_like" },
45
- { name: "files", pattern: "<files>|\\bfiles\\b|fichiers", message: "Explore output missing files section" },
46
- { name: "answer", pattern: "<answer>|\\banswer\\b|réponse|reponse", message: "Explore output missing answer section" },
47
- { name: "gaps", pattern: "<gaps>|\\bgaps\\b|trous|manques", message: "Explore output missing gaps section" },
48
- { name: "next_steps", pattern: "<next_steps>|next steps|next_steps|prochaines étapes", message: "Explore output missing next_steps section" },
45
+ { name: "files", pattern: "<files>|\\bfiles\\b", message: "Explore output missing files section" },
46
+ { name: "answer", pattern: "<answer>|\\banswer\\b", message: "Explore output missing answer section" },
47
+ { name: "gaps", pattern: "<gaps>|\\bgaps\\b", message: "Explore output missing gaps section" },
48
+ { name: "next_steps", pattern: "<next_steps>|next steps|next_steps", message: "Explore output missing next_steps section" },
49
49
  ],
50
50
  },
51
51
  {
@@ -55,10 +55,10 @@ const OUTPUT_CONTRACTS: OutputContract[] = [
55
55
  required: [
56
56
  ...COMMON_OUTPUT_REQUIREMENTS,
57
57
  { name: "scope", pattern: "scope|in-scope|out-of-scope|forbidden", message: "Plan output missing scope table" },
58
- { name: "assumptions", pattern: "assumptions?|hypothèses", message: "Plan output missing assumptions" },
59
- { name: "implementation_slices", pattern: "implementation (steps|slices)|slices?|étapes", message: "Plan output missing implementation slices/steps" },
58
+ { name: "assumptions", pattern: "assumptions?", message: "Plan output missing assumptions" },
59
+ { name: "implementation_slices", pattern: "implementation (steps|slices)|slices?", message: "Plan output missing implementation slices/steps" },
60
60
  { name: "validation_ladder", pattern: "validation ladder|validation", message: "Plan output missing validation ladder" },
61
- { name: "stop_conditions", pattern: "stop conditions?|conditions d'arrêt|conditions d’arret", message: "Plan output missing stop conditions" },
61
+ { name: "stop_conditions", pattern: "stop conditions?", message: "Plan output missing stop conditions" },
62
62
  { name: "handoff_contract", pattern: "handoff|TASK:|EXPECTED OUTCOME:|MUST NOT", message: "Plan output missing implementer handoff contract" },
63
63
  ],
64
64
  },
@@ -69,9 +69,9 @@ const OUTPUT_CONTRACTS: OutputContract[] = [
69
69
  required: [
70
70
  ...COMMON_OUTPUT_REQUIREMENTS,
71
71
  { name: "gap_verdict", pattern: "gap verdict|SUFFICIENT|GAP|no change|no-change", message: "Implement output missing gap/no-change verdict" },
72
- { name: "changed_files", pattern: "changed files|fichiers modifiés|no change|no-change", message: "Implement output missing changed files or no-change evidence" },
73
- { name: "verification_commands", pattern: "verification|vérification|commands?|commandes?", message: "Implement output missing verification commands" },
74
- { name: "results", pattern: "results?|résultats?|exit code|passed|failed", message: "Implement output missing command/results evidence" },
72
+ { name: "changed_files", pattern: "changed files|no change|no-change", message: "Implement output missing changed files or no-change evidence" },
73
+ { name: "verification_commands", pattern: "verification|commands?", message: "Implement output missing verification commands" },
74
+ { name: "results", pattern: "results?|exit code|passed|failed", message: "Implement output missing command/results evidence" },
75
75
  ],
76
76
  },
77
77
  {
@@ -81,8 +81,8 @@ const OUTPUT_CONTRACTS: OutputContract[] = [
81
81
  required: [
82
82
  ...COMMON_OUTPUT_REQUIREMENTS,
83
83
  { name: "verdict", pattern: "<verdict>\\s*(PASS|FAIL|WARN)\\s*</verdict>|\\b(PASS|FAIL|WARN)\\b", message: "Oracle output missing PASS/FAIL/WARN verdict" },
84
- { name: "confidence", pattern: "<confidence>|\\bconfidence\\b|confiance", message: "Oracle output missing confidence" },
85
- { name: "blocking_issues", pattern: "<blocking_issues>|blocking issues?|blockers?|bloqueurs", message: "Oracle output missing blocking issues" },
84
+ { name: "confidence", pattern: "<confidence>|\\bconfidence\\b", message: "Oracle output missing confidence" },
85
+ { name: "blocking_issues", pattern: "<blocking_issues>|blocking issues?|blockers?", message: "Oracle output missing blocking issues" },
86
86
  { name: "non_blocking_notes", pattern: "<non_blocking_notes>|non[-_ ]blocking", message: "Oracle output missing non-blocking notes" },
87
87
  { name: "no_ship", pattern: "<no_ship>|no_ship|no ship", message: "Oracle output missing no_ship decision" },
88
88
  ],
@@ -94,7 +94,7 @@ const OUTPUT_CONTRACTS: OutputContract[] = [
94
94
  required: [
95
95
  ...COMMON_OUTPUT_REQUIREMENTS,
96
96
  { name: "verdict", pattern: "\\b(PASS|FAIL|WARN|INCONCLUSIVE)\\b|verdict", message: "QA output missing verification verdict" },
97
- { name: "command", pattern: "commands?|commandes?|cwd", message: "QA output missing command/cwd evidence" },
97
+ { name: "command", pattern: "commands?|cwd", message: "QA output missing command/cwd evidence" },
98
98
  { name: "exit_or_output", pattern: "exit code|output|stdout|stderr|important output", message: "QA output missing exit/output evidence" },
99
99
  { name: "reproduction", pattern: "reproduction|reproduce|steps", message: "QA output missing reproduction steps" },
100
100
  ],
@@ -106,8 +106,8 @@ const OUTPUT_CONTRACTS: OutputContract[] = [
106
106
  required: [
107
107
  ...COMMON_OUTPUT_REQUIREMENTS,
108
108
  { name: "consensus", pattern: "<consensus>|\\bconsensus\\b", message: "Synthesis output missing consensus section" },
109
- { name: "conflicts", pattern: "<conflicts>|conflicts?|désaccords?", message: "Synthesis output missing conflicts section" },
110
- { name: "missing_evidence", pattern: "<missing_evidence>|missing_evidence|missing evidence|preuves manquantes", message: "Synthesis output missing missing_evidence section" },
109
+ { name: "conflicts", pattern: "<conflicts>|conflicts?", message: "Synthesis output missing conflicts section" },
110
+ { name: "missing_evidence", pattern: "<missing_evidence>|missing_evidence|missing evidence", message: "Synthesis output missing missing_evidence section" },
111
111
  { name: "recommended_next_action", pattern: "<recommended_next_action>|recommended_next_action|recommended next action", message: "Synthesis output missing recommended_next_action section" },
112
112
  { name: "tasks_to_rerun", pattern: "<tasks_to_rerun>|tasks_to_rerun|tasks to rerun|rerun", message: "Synthesis output missing tasks_to_rerun section" },
113
113
  ],
@@ -119,9 +119,9 @@ const OUTPUT_CONTRACTS: OutputContract[] = [
119
119
  required: [
120
120
  ...COMMON_OUTPUT_REQUIREMENTS,
121
121
  { name: "verdict", pattern: "<verdict>\\s*(PASS|FAIL|WARN)\\s*</verdict>|\\b(PASS|FAIL|WARN)\\b", message: "Oracle merge output missing PASS/FAIL/WARN verdict" },
122
- { name: "confidence", pattern: "<confidence>|\\bconfidence\\b|confiance", message: "Oracle merge output missing confidence" },
122
+ { name: "confidence", pattern: "<confidence>|\\bconfidence\\b", message: "Oracle merge output missing confidence" },
123
123
  { name: "no_ship", pattern: "<no_ship>|no_ship|no ship", message: "Oracle merge output missing no_ship decision" },
124
- { name: "blocking_issues", pattern: "<blocking_issues>|blocking issues?|blockers?|bloqueurs", message: "Oracle merge output missing blocking issues" },
124
+ { name: "blocking_issues", pattern: "<blocking_issues>|blocking issues?|blockers?", message: "Oracle merge output missing blocking issues" },
125
125
  { name: "merged_lanes", pattern: "<merged_lanes>|merged_lanes|merged lanes", message: "Oracle merge output missing merged_lanes" },
126
126
  ],
127
127
  },
@@ -146,8 +146,8 @@ const OUTPUT_CONTRACTS: OutputContract[] = [
146
146
  required: [
147
147
  ...COMMON_OUTPUT_REQUIREMENTS,
148
148
  { name: "sources", pattern: "sources?|sources_consulted|URLs?|paths?", message: "Research output missing sources" },
149
- { name: "unknowns", pattern: "unknowns?|uncertainties|incertitudes", message: "Research output missing unknowns/uncertainties" },
150
- { name: "recommendation", pattern: "recommendation|recommandation", message: "Research output missing recommendation" },
149
+ { name: "unknowns", pattern: "unknowns?|uncertainties", message: "Research output missing unknowns/uncertainties" },
150
+ { name: "recommendation", pattern: "recommendation", message: "Research output missing recommendation" },
151
151
  ],
152
152
  },
153
153
  {
@@ -163,7 +163,7 @@ const OUTPUT_CONTRACTS: OutputContract[] = [
163
163
  { name: "facts_or_patterns", pattern: "facts_or_patterns|facts|patterns", message: "Brain lookup output missing facts_or_patterns" },
164
164
  { name: "gaps", pattern: "gaps|source gaps", message: "Brain lookup output missing gaps" },
165
165
  { name: "freshness", pattern: "freshness|stale", message: "Brain lookup output missing freshness" },
166
- { name: "confidence", pattern: "confidence|confiance", message: "Brain lookup output missing confidence" },
166
+ { name: "confidence", pattern: "confidence", message: "Brain lookup output missing confidence" },
167
167
  { name: "no_body_storage", pattern: "no_body_storage|bodyStored\s*[:=]\s*false|no body storage", message: "Brain lookup output missing no_body_storage" },
168
168
  ],
169
169
  },
@@ -218,15 +218,15 @@ const OUTPUT_CONTRACTS: OutputContract[] = [
218
218
  agentNames: ["specifier", "spec"],
219
219
  required: [
220
220
  ...COMMON_OUTPUT_REQUIREMENTS,
221
- { name: "problem", pattern: "<problem>|\\bproblem\\b|problème|probleme", message: "Spec output missing problem" },
222
- { name: "context", pattern: "<context>|\\bcontext\\b|contexte", message: "Spec output missing context" },
223
- { name: "objectives", pattern: "<objectives>|objectives?|objectifs?", message: "Spec output missing objectives" },
224
- { name: "non_goals", pattern: "<non_goals>|non[-_ ]goals?|non[-_ ]objectifs?", message: "Spec output missing non_goals" },
225
- { name: "in_scope", pattern: "<in_scope>|in[-_ ]scope|périmètre inclus|perimetre inclus", message: "Spec output missing in_scope" },
226
- { name: "out_of_scope", pattern: "<out_of_scope>|out[-_ ]of[-_ ]scope|hors[-_ ]scope|hors périmètre|hors perimetre", message: "Spec output missing out_of_scope" },
227
- { name: "constraints", pattern: "<constraints>|constraints?|contraintes?", message: "Spec output missing constraints" },
228
- { name: "acceptance_criteria", pattern: "<acceptance_criteria>|acceptance criteria|acceptance_criteria|critères d'acceptation|criteres d'acceptation", message: "Spec output missing acceptance_criteria" },
229
- { name: "open_questions", pattern: "<open_questions>|open questions|open_questions|questions ouvertes", message: "Spec output missing open_questions" },
221
+ { name: "problem", pattern: "<problem>|\\bproblem\\b", message: "Spec output missing problem" },
222
+ { name: "context", pattern: "<context>|\\bcontext\\b", message: "Spec output missing context" },
223
+ { name: "objectives", pattern: "<objectives>|objectives?", message: "Spec output missing objectives" },
224
+ { name: "non_goals", pattern: "<non_goals>|non[-_ ]goals?", message: "Spec output missing non_goals" },
225
+ { name: "in_scope", pattern: "<in_scope>|in[-_ ]scope", message: "Spec output missing in_scope" },
226
+ { name: "out_of_scope", pattern: "<out_of_scope>|out[-_ ]of[-_ ]scope", message: "Spec output missing out_of_scope" },
227
+ { name: "constraints", pattern: "<constraints>|constraints?", message: "Spec output missing constraints" },
228
+ { name: "acceptance_criteria", pattern: "<acceptance_criteria>|acceptance criteria|acceptance_criteria", message: "Spec output missing acceptance_criteria" },
229
+ { name: "open_questions", pattern: "<open_questions>|open questions|open_questions", message: "Spec output missing open_questions" },
230
230
  { name: "handoff_to_planner", pattern: "<handoff_to_planner>|handoff to planner|handoff_to_planner|planner", message: "Spec output missing handoff_to_planner" },
231
231
  ],
232
232
  },
@@ -236,15 +236,15 @@ const OUTPUT_CONTRACTS: OutputContract[] = [
236
236
  agentNames: ["clarifier"],
237
237
  required: [
238
238
  ...COMMON_OUTPUT_REQUIREMENTS,
239
- { name: "clarity_score", pattern: "<clarity_score>|clarity_score|clarity score|score de clarté", message: "Clarification output missing clarity_score" },
239
+ { name: "clarity_score", pattern: "<clarity_score>|clarity_score|clarity score", message: "Clarification output missing clarity_score" },
240
240
  { name: "verdict", pattern: "<verdict>\\s*(CLEAR|NEEDS_CLARIFICATION|BLOCKED)\\s*</verdict>|\\b(CLEAR|NEEDS_CLARIFICATION|BLOCKED)\\b", message: "Clarification output missing CLEAR/NEEDS_CLARIFICATION/BLOCKED verdict" },
241
241
  { name: "allow_plan", pattern: "<allow_plan>\\s*(yes|no)\\s*</allow_plan>|allow_plan\\s*[:=]\\s*(yes|no)", message: "Clarification output missing allow_plan yes/no" },
242
- { name: "ambiguities", pattern: "<ambiguities>|ambiguities|ambiguïtés|ambiguites", message: "Clarification output missing ambiguities" },
242
+ { name: "ambiguities", pattern: "<ambiguities>|ambiguities", message: "Clarification output missing ambiguities" },
243
243
  { name: "questions", pattern: "<questions>|questions?", message: "Clarification output missing questions" },
244
- { name: "assumptions", pattern: "<assumptions>|assumptions?|hypothèses|hypotheses", message: "Clarification output missing assumptions" },
245
- { name: "refined_spec", pattern: "<refined_spec>|refined spec|refined_spec|spec affinée|spec affinee", message: "Clarification output missing refined_spec" },
244
+ { name: "assumptions", pattern: "<assumptions>|assumptions?", message: "Clarification output missing assumptions" },
245
+ { name: "refined_spec", pattern: "<refined_spec>|refined spec|refined_spec", message: "Clarification output missing refined_spec" },
246
246
  { name: "minimum_to_plan", pattern: "<minimum_to_plan>|minimum to plan|minimum_to_plan", message: "Clarification output missing minimum_to_plan" },
247
- { name: "acceptance_criteria", pattern: "<acceptance_criteria>|acceptance criteria|acceptance_criteria|critères d'acceptation|criteres d'acceptation", message: "Clarification output missing acceptance_criteria" },
247
+ { name: "acceptance_criteria", pattern: "<acceptance_criteria>|acceptance criteria|acceptance_criteria", message: "Clarification output missing acceptance_criteria" }
248
248
  ],
249
249
  },
250
250
  {
@@ -5,7 +5,7 @@ function detectCanonicalPatterns(text: string): string[] {
5
5
  if (/TASK\s*:|EXPECTED\s+OUTCOME\s*:|MUST\s+NOT/i.test(text)) patterns.add("delegation.contract.structured");
6
6
  if (/delegate_(agent|task)|sub-?agent|oracle|explore/i.test(text)) patterns.add("routing.subagent.specialized");
7
7
  if (/PASS|FAIL|WARN|verdict/i.test(text)) patterns.add("verification.verdict_first");
8
- if (/evidence|preuve|preuves|sentinel|DONE/i.test(text)) patterns.add("verification.evidence_required");
8
+ if (/evidence|proof|sentinel|DONE/i.test(text)) patterns.add("verification.evidence_required");
9
9
  if (/factory_run|software factory|factory|manifest|checkpoint/i.test(text)) patterns.add("factory.workflow.manifest_checkpoint");
10
10
  if (/damage-control|destructive|secret|zero-access|sandbox/i.test(text)) patterns.add("safety.damage_control");
11
11
  if (/truncat|output cut|silent response/i.test(text)) patterns.add("failure.output.truncation");
@@ -69,6 +69,26 @@ export interface TodoSplitRequest {
69
69
  hasFinalMarker: boolean;
70
70
  }
71
71
 
72
+ export type TodoPeerStatusClaim = "done" | "incomplete" | "blocked";
73
+
74
+ export interface TodoPeerResultItem {
75
+ todoId?: string;
76
+ statusClaim?: TodoPeerStatusClaim;
77
+ evidenceRefs: string[];
78
+ validationCommands: string[];
79
+ risks: string[];
80
+ acceptanceBlockers: string[];
81
+ noShip?: boolean;
82
+ hasFinalMarker: boolean;
83
+ }
84
+
85
+ export interface TodoPeerResultParseResult {
86
+ contract?: "TODO_PEER_RESULT.v1" | "TODO_PEER_BUNDLE_RESULT.v1";
87
+ items: TodoPeerResultItem[];
88
+ hasFinalMarker: boolean;
89
+ errors: string[];
90
+ }
91
+
72
92
  export interface GoalTodoDelegationRef {
73
93
  runId?: string;
74
94
  agent?: string;
@@ -30,6 +30,8 @@ import type {
30
30
  GoalTodoSummary,
31
31
  ResolveGoalTodoAction,
32
32
  TodoClaimValidationResult,
33
+ TodoPeerResultItem,
34
+ TodoPeerResultParseResult,
33
35
  TodoSplitRequest,
34
36
  } from "./goal-todo-types.js";
35
37
  import { recordZcommitOwnedPaths, type ZcommitChildChangedPathRef } from "../git/git-ops.js";
@@ -67,6 +69,8 @@ export type {
67
69
  GoalTodoSummary,
68
70
  ResolveGoalTodoAction,
69
71
  TodoClaimValidationResult,
72
+ TodoPeerResultItem,
73
+ TodoPeerResultParseResult,
70
74
  TodoSplitRequest,
71
75
  TodoSplitRequestAction,
72
76
  TodoSplitRiskLevel,
@@ -395,15 +399,21 @@ function applyEvent(state: GoalTodoState, event: GoalTodoEvent): void {
395
399
  childChangedPaths: event.childChangedPaths ?? [],
396
400
  returnedAt: event.at,
397
401
  };
402
+ const claimStatus: GoalTodoStatus = event.statusClaim === "blocked"
403
+ ? "blocked"
404
+ : event.statusClaim === "incomplete" || event.noShip === true || (event.acceptanceBlockers ?? []).length > 0 || event.targetReadiness === "needs_parent_review" || event.targetReadiness === "blocked"
405
+ ? "needs_review"
406
+ : "claim_returned";
398
407
  replaceNode(state, applyPatchToNode(existing, {
399
- status: "claim_returned",
408
+ status: claimStatus,
409
+ owner: claimStatus === "claim_returned" ? existing.owner : "agent",
400
410
  evidenceRefs,
401
411
  validationCommands,
402
412
  delegation: existing.delegation ? { ...existing.delegation, status: "claim_returned" } : undefined,
403
413
  claim,
404
414
  artifacts: { ...(existing.artifacts ?? {}), outputHash: event.outputHash ?? event.claimHash },
405
- blocker: event.noShip === true ? "delegated claim returned advisory no_ship=true; parent review required" : undefined,
406
- reviewNoShip: event.noShip === true,
415
+ blocker: claimStatus === "claim_returned" ? undefined : event.acceptanceBlockers?.[0] ?? (event.noShip === true ? "delegated claim returned advisory no_ship=true; parent review required" : `delegated claim status ${event.statusClaim ?? "needs_review"}; parent review required`),
416
+ reviewNoShip: claimStatus === "claim_returned" ? undefined : true,
407
417
  }));
408
418
  }
409
419
  return;
@@ -880,7 +890,7 @@ export function splitGoalTodo(pi: ExtensionAPI, state: HarnessRuntimeState, goal
880
890
  return children;
881
891
  }
882
892
 
883
- export function linkGoalTodoDelegation(pi: ExtensionAPI, state: HarnessRuntimeState, goalId: string, todoId: string, input: { runId: string; agent?: string; childGoalId?: string; requestId?: string; delegationDepth?: number }, source: GoalTodoEventSource = "delegation"): GoalTodoNode | undefined {
893
+ export function linkGoalTodoDelegation(pi: ExtensionAPI, state: HarnessRuntimeState, goalId: string, todoId: string, input: { runId: string; agent?: string; childGoalId?: string; requestId?: string; delegationDepth?: number; status?: GoalTodoDelegationStatus }, source: GoalTodoEventSource = "delegation"): GoalTodoNode | undefined {
884
894
  const existing = state.goalTodos.nodes.find((node) => node.goalId === goalId && node.id === todoId);
885
895
  if (!existing) return undefined;
886
896
  const delegation: GoalTodoDelegationRef = {
@@ -889,7 +899,7 @@ export function linkGoalTodoDelegation(pi: ExtensionAPI, state: HarnessRuntimeSt
889
899
  childGoalId: input.childGoalId,
890
900
  requestId: input.requestId,
891
901
  delegationDepth: Math.max(0, Math.trunc(input.delegationDepth ?? existing.delegation?.delegationDepth ?? 1)),
892
- status: "running",
902
+ status: input.status ?? "running",
893
903
  };
894
904
  appendGoalTodoEvent(pi, state, { version: 1, kind: "delegate_link", source, goalId, todoId, runId: input.runId, delegation, at: unixSeconds() });
895
905
  return state.goalTodos.nodes.find((node) => node.goalId === goalId && node.id === todoId);
@@ -1250,6 +1260,51 @@ function extractLabeledScalar(text: string, label: string): string | undefined {
1250
1260
  return match?.[1]?.trim();
1251
1261
  }
1252
1262
 
1263
+ function normalizePeerStatusClaim(value: string | undefined): TodoPeerResultItem["statusClaim"] {
1264
+ const normalized = value?.trim().toLowerCase().replace(/[ -]/g, "_");
1265
+ return normalized === "done" || normalized === "incomplete" || normalized === "blocked" ? normalized : undefined;
1266
+ }
1267
+
1268
+ function extractTodoPeerResultItemFromText(text: string, hasFinalMarker: boolean): TodoPeerResultItem {
1269
+ const todoIdMatch = text.match(/todo_id\s*[:=]\s*([^\s]+)/i);
1270
+ const statusRaw = extractLabeledScalar(text, "status_claim") ?? extractLabeledScalar(text, "status");
1271
+ const noShipMatch = text.match(/no_ship\s*[:=]\s*(true|yes|false|no)/i);
1272
+ return {
1273
+ todoId: todoIdMatch?.[1]?.trim(),
1274
+ statusClaim: normalizePeerStatusClaim(statusRaw),
1275
+ evidenceRefs: collectLabeledLines(text, /^\s*(?:[-*]\s*)?evidence_refs\s*[:=]/i),
1276
+ validationCommands: collectLabeledLines(text, /^\s*(?:[-*]\s*)?validation_commands\s*[:=]/i),
1277
+ risks: collectLabeledLines(text, /^\s*(?:[-*]\s*)?risks\s*[:=]/i),
1278
+ acceptanceBlockers: collectLabeledLines(text, /^\s*(?:[-*]\s*)?acceptance_blockers\s*[:=]/i),
1279
+ noShip: noShipMatch ? /^(true|yes)$/i.test(noShipMatch[1]) : undefined,
1280
+ hasFinalMarker,
1281
+ };
1282
+ }
1283
+
1284
+ export function extractTodoPeerResultFromText(text: string): TodoPeerResultParseResult {
1285
+ const bundle = /TODO_PEER_BUNDLE_RESULT\.v1/i.test(text);
1286
+ const single = /TODO_PEER_RESULT\.v1/i.test(text);
1287
+ const hasBundleMarker = /FINAL_MARKER\s*:\s*TODO_PEER_BUNDLE_RESULT_END|TODO_PEER_BUNDLE_RESULT_END/.test(text);
1288
+ const hasSingleMarker = /FINAL_MARKER\s*:\s*TODO_PEER_RESULT_END|TODO_PEER_RESULT_END/.test(text);
1289
+ const contract = bundle ? "TODO_PEER_BUNDLE_RESULT.v1" : single ? "TODO_PEER_RESULT.v1" : undefined;
1290
+ const hasFinalMarker = bundle ? hasBundleMarker : single ? hasSingleMarker : false;
1291
+ const errors: string[] = [];
1292
+ if (!contract) return { items: [], hasFinalMarker: false, errors };
1293
+ if (!hasFinalMarker) errors.push("missing_final_marker");
1294
+ const itemTexts = bundle
1295
+ ? text.split(/(?=^\s*(?:[-*]\s*)?todo_id\s*[:=])/gim).filter((part) => /todo_id\s*[:=]/i.test(part))
1296
+ : [text];
1297
+ const items = itemTexts.map((part) => extractTodoPeerResultItemFromText(part, hasFinalMarker));
1298
+ if (items.length === 0) errors.push("missing_result_items");
1299
+ for (const item of items) {
1300
+ if (!item.todoId) errors.push("missing_todo_id");
1301
+ if (!item.statusClaim) errors.push(`missing_status_claim:${item.todoId ?? "unknown"}`);
1302
+ if (item.statusClaim === "done" && item.evidenceRefs.length === 0 && item.validationCommands.length === 0) errors.push(`missing_evidence_for_done:${item.todoId ?? "unknown"}`);
1303
+ if (item.noShip === true) errors.push(`no_ship_true:${item.todoId ?? "unknown"}`);
1304
+ }
1305
+ return { contract, items, hasFinalMarker, errors: [...new Set(errors)] };
1306
+ }
1307
+
1253
1308
  export function extractTodoClaimValidationFromText(text: string): TodoClaimValidationResult {
1254
1309
  const todoIdMatch = text.match(/todo_id\s*[:=]\s*([^\s]+)/i);
1255
1310
  const claimHashMatch = text.match(/claim_hash\s*[:=]\s*([a-f0-9]{64})/i);
@@ -5,7 +5,7 @@ import { sha256 } from "../../core/utils/hashing.js";
5
5
  import { isRecord } from "../../core/utils/records.js";
6
6
  import { safeRunId } from "../../core/utils/paths.js";
7
7
 
8
- const STOPWORDS = new Set(["the", "and", "for", "with", "how", "does", "this", "that", "dans", "avec", "pour", "comment", "faire", "using", "use", "project", "style"]);
8
+ const STOPWORDS = new Set(["the", "and", "for", "with", "how", "does", "this", "that", "using", "use", "project", "style"]);
9
9
  const DEFAULT_SCAN_DIR = "reports/project-dna-scans/project-dna-factory-smoke";
10
10
  const SAFE_SCAN_PREFIXES = ["reports/project-dna-scans/"];
11
11
  const MAX_CONTEXT_TOKENS_CAP = 8000;
@@ -89,7 +89,7 @@ export function evaluateBudgetPreflightDryRun(input: BudgetPreflightDryRunInput)
89
89
 
90
90
  function outputHasEvidenceMarker(output: string | undefined): boolean {
91
91
  if (!output) return false;
92
- return /(?:<evidence>[\s\S]*?<\/evidence>|<evidence>|\bevidence\b|preuve|preuves)/i.test(output);
92
+ return /(?:<evidence>[\s\S]*?<\/evidence>|<evidence>|\bevidence\b|proof)/i.test(output);
93
93
  }
94
94
 
95
95
  export function detectOracleFail(output: string | undefined): { oracleFailed: boolean; noShip: boolean; stopCondition: ChildStopCondition } {
@@ -1,24 +1,24 @@
1
- # Scope du dossier
1
+ # Directory scope
2
2
 
3
- - Validation factory, plan agentic, exécution factory et quarantine review/activate/verify.
4
- - Ce dossier ne doit pas enregistrer de Pi tools directement; le runtime délègue ici.
3
+ - Factory validation, agentic plans, factory execution, and quarantine review/activate/verify.
4
+ - This directory must not register Pi tools directly; runtime delegates here.
5
5
 
6
6
  # Invariants
7
7
 
8
- - Préserver `SMOKE_PASSED.sentinel`, `PILOT_PASSED.sentinel`, `BATCH_PASSED.sentinel`, `DONE.sentinel`.
9
- - `plan_only` ne crée pas de sentinels de complétion.
10
- - Pilot exige une review oracle persistée et un manifest multi-item.
11
- - Factory-forge quarantine ne s'auto-active jamais.
12
- - Activation refuse overwrite et exige phrase exacte.
13
- - Ne pas changer noms d'artefacts, statuts ou validations.
8
+ - Preserve `SMOKE_PASSED.sentinel`, `PILOT_PASSED.sentinel`, `BATCH_PASSED.sentinel`, and `DONE.sentinel`.
9
+ - `plan_only` does not create completion sentinels.
10
+ - Pilot requires a persisted oracle review and a multi-item manifest.
11
+ - Factory-forge quarantine never self-activates.
12
+ - Activation refuses overwrite and requires the exact phrase.
13
+ - Do not change artifact names, statuses, or validations.
14
14
 
15
15
  # Imports
16
16
 
17
- - Peut importer utils/safety/output-contracts/telemetry/child-runner selon besoin.
18
- - Interdit: importer depuis `index.ts`.
19
- - Imports relatifs runtime avec suffixe `.js`.
17
+ - May import utils/safety/output-contracts/telemetry/child-runner as needed.
18
+ - Forbidden: importing from `index.ts`.
19
+ - Use runtime-relative imports with a `.js` suffix.
20
20
 
21
- # Validation locale
21
+ # Local validation
22
22
 
23
23
  - `npm run check -- --pretty false`.
24
- - `npm run smoke:harness` après toute tranche factory/quarantine.
24
+ - `npm run smoke:harness` after any factory/quarantine slice.