@tea-agent/loop-agent 0.1.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 (264) hide show
  1. package/AGENTS.md +121 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +144 -0
  4. package/bin/loop-agent.js +21 -0
  5. package/dist/adapters/aimax.js +91 -0
  6. package/dist/adapters/context.js +32 -0
  7. package/dist/adapters/index.js +28 -0
  8. package/dist/adapters/loop-agent.js +98 -0
  9. package/dist/adapters/types.js +1 -0
  10. package/dist/cli/catalog.js +259 -0
  11. package/dist/cli/help.js +55 -0
  12. package/dist/cli/index.js +3 -0
  13. package/dist/cli/program.js +505 -0
  14. package/dist/cli.js +12 -0
  15. package/dist/commands/closeout.js +13 -0
  16. package/dist/commands/coverage-audit.js +14 -0
  17. package/dist/commands/cursor-prompt.js +222 -0
  18. package/dist/commands/cursor-worker.js +43 -0
  19. package/dist/commands/dag-approve.js +102 -0
  20. package/dist/commands/dag-final-verification.js +76 -0
  21. package/dist/commands/dag-init-hybrid.js +56 -0
  22. package/dist/commands/dag-reconcile-tasks.js +51 -0
  23. package/dist/commands/dag-reject.js +91 -0
  24. package/dist/commands/dag-report.js +177 -0
  25. package/dist/commands/dag-resume.js +34 -0
  26. package/dist/commands/dag-run-task.js +470 -0
  27. package/dist/commands/dag-validate.js +186 -0
  28. package/dist/commands/dag-workflow-compile.js +91 -0
  29. package/dist/commands/dag-workflow-plan.js +130 -0
  30. package/dist/commands/dag-workflow-validate.js +66 -0
  31. package/dist/commands/delegate.js +132 -0
  32. package/dist/commands/docs-archive.js +5 -0
  33. package/dist/commands/docs-audit.js +5 -0
  34. package/dist/commands/doctor.js +50 -0
  35. package/dist/commands/goal.js +92 -0
  36. package/dist/commands/handoff-check.js +5 -0
  37. package/dist/commands/harvest.js +44 -0
  38. package/dist/commands/inspect.js +11 -0
  39. package/dist/commands/instructions.js +195 -0
  40. package/dist/commands/knowledge.js +64 -0
  41. package/dist/commands/loop-benchmark.js +72 -0
  42. package/dist/commands/loop.js +241 -0
  43. package/dist/commands/new-task.js +5 -0
  44. package/dist/commands/pi-prompt.js +181 -0
  45. package/dist/commands/pi-reuse-benchmark.js +153 -0
  46. package/dist/commands/plan-list.js +5 -0
  47. package/dist/commands/promote-run.js +29 -0
  48. package/dist/commands/reference-index.js +16 -0
  49. package/dist/commands/run-dag.js +184 -0
  50. package/dist/commands/spine.js +38 -0
  51. package/dist/commands/stats.js +84 -0
  52. package/dist/commands/status.js +56 -0
  53. package/dist/commands/study-init.js +192 -0
  54. package/dist/commands/workflow.js +259 -0
  55. package/dist/commands/worktree-create.js +31 -0
  56. package/dist/commands/worktree-list.js +5 -0
  57. package/dist/commands/worktree-remove.js +26 -0
  58. package/dist/cursor-worker-entry.js +8 -0
  59. package/dist/executors/config-core.js +55 -0
  60. package/dist/executors/config.js +2 -0
  61. package/dist/executors/cursor-artifacts.js +33 -0
  62. package/dist/executors/cursor-execution-log.js +81 -0
  63. package/dist/executors/cursor-executor-artifacts.js +135 -0
  64. package/dist/executors/cursor-executor.js +468 -0
  65. package/dist/executors/cursor-run.js +115 -0
  66. package/dist/executors/cursor-tool.js +94 -0
  67. package/dist/executors/cursor-worker-client.js +213 -0
  68. package/dist/executors/cursor-worker-protocol.js +18 -0
  69. package/dist/executors/cursor-worker-server.js +54 -0
  70. package/dist/executors/cursor-worker.js +3 -0
  71. package/dist/executors/cursor.js +6 -0
  72. package/dist/executors/dag-cursor-executor.js +88 -0
  73. package/dist/executors/dag-pi-executor.js +322 -0
  74. package/dist/executors/dag-static-executor.js +45 -0
  75. package/dist/executors/dag.js +4 -0
  76. package/dist/executors/index.js +8 -0
  77. package/dist/executors/model-routing.js +60 -0
  78. package/dist/executors/pi-event-serializer.js +43 -0
  79. package/dist/executors/pi-executor.js +606 -0
  80. package/dist/executors/pi-reuse-benchmark.js +316 -0
  81. package/dist/executors/pi-runtime-reuse.js +29 -0
  82. package/dist/executors/pi-sdk-executor.js +255 -0
  83. package/dist/executors/pi-sdk.js +1 -0
  84. package/dist/executors/pi.js +3 -0
  85. package/dist/executors/shell-executor.js +300 -0
  86. package/dist/executors/shell-presets.js +47 -0
  87. package/dist/executors/shell-verification.js +251 -0
  88. package/dist/executors/shell-write-guard.js +126 -0
  89. package/dist/executors/shell.js +3 -0
  90. package/dist/executors/static.js +1 -0
  91. package/dist/governance/checks.js +434 -0
  92. package/dist/governance/harness.js +9 -0
  93. package/dist/governance/index.js +3 -0
  94. package/dist/governance/manifest-types.js +128 -0
  95. package/dist/governance/manifest.js +2 -0
  96. package/dist/governance/path-guard.js +69 -0
  97. package/dist/governance/path-guards.js +2 -0
  98. package/dist/governance/profiles.js +3 -0
  99. package/dist/governance/requirement-coverage.js +425 -0
  100. package/dist/governance/skill-safety.js +135 -0
  101. package/dist/governance/spine-audit.js +152 -0
  102. package/dist/records/closeout.js +2 -0
  103. package/dist/records/harvest.js +236 -0
  104. package/dist/records/index.js +3 -0
  105. package/dist/records/one-shot-runs.js +421 -0
  106. package/dist/records/promotion.js +199 -0
  107. package/dist/shared/artifacts-core.js +88 -0
  108. package/dist/shared/artifacts.js +2 -0
  109. package/dist/shared/context-files.js +32 -0
  110. package/dist/shared/context.js +2 -0
  111. package/dist/shared/copy-dir.js +17 -0
  112. package/dist/shared/git-progress.js +165 -0
  113. package/dist/shared/index.js +5 -0
  114. package/dist/shared/logger.js +23 -0
  115. package/dist/shared/one-shot-prompt-args.js +98 -0
  116. package/dist/shared/path-refs.js +31 -0
  117. package/dist/shared/prompts.js +26 -0
  118. package/dist/shared/reference-context.js +238 -0
  119. package/dist/shared/timeout-policy.js +19 -0
  120. package/dist/shared/timeout.js +1 -0
  121. package/dist/shared/types.js +5 -0
  122. package/dist/task/config-types.js +97 -0
  123. package/dist/task/config.js +2 -0
  124. package/dist/task/delegate.js +220 -0
  125. package/dist/task/goal-audit.js +51 -0
  126. package/dist/task/goal-policy.js +8 -0
  127. package/dist/task/goal.js +3 -0
  128. package/dist/task/ids.js +1 -0
  129. package/dist/task/index.js +9 -0
  130. package/dist/task/lifecycle.js +1 -0
  131. package/dist/task/paths.js +1 -0
  132. package/dist/task/read-model.js +149 -0
  133. package/dist/task/runtime.js +699 -0
  134. package/dist/task/source-state.js +1 -0
  135. package/dist/task/state.js +55 -0
  136. package/dist/task/subagent-guidance.js +1 -0
  137. package/dist/task/workflow-state-types.js +92 -0
  138. package/dist/task/worktree-cleanup.js +140 -0
  139. package/dist/task/worktree.js +171 -0
  140. package/dist/workflows/dag/authoring.js +8 -0
  141. package/dist/workflows/dag/authority-surface.js +138 -0
  142. package/dist/workflows/dag/canvas-observer.js +474 -0
  143. package/dist/workflows/dag/decision-envelope.js +502 -0
  144. package/dist/workflows/dag/decision-evidence.js +153 -0
  145. package/dist/workflows/dag/decision-gates.js +1 -0
  146. package/dist/workflows/dag/executor-registry.js +25 -0
  147. package/dist/workflows/dag/facts.js +4 -0
  148. package/dist/workflows/dag/failure-category.js +111 -0
  149. package/dist/workflows/dag/final-verification.js +180 -0
  150. package/dist/workflows/dag/governance-constants.js +5 -0
  151. package/dist/workflows/dag/governance-profile.js +405 -0
  152. package/dist/workflows/dag/index.js +6 -0
  153. package/dist/workflows/dag/init-hybrid.js +855 -0
  154. package/dist/workflows/dag/knowledge-curator.js +162 -0
  155. package/dist/workflows/dag/lifecycle.js +484 -0
  156. package/dist/workflows/dag/prompt-source.js +88 -0
  157. package/dist/workflows/dag/prompt.js +130 -0
  158. package/dist/workflows/dag/reconcile-tasks.js +404 -0
  159. package/dist/workflows/dag/recovery-recommendation.js +226 -0
  160. package/dist/workflows/dag/repair-artifact.js +136 -0
  161. package/dist/workflows/dag/report.js +1019 -0
  162. package/dist/workflows/dag/runner.js +1677 -0
  163. package/dist/workflows/dag/runtime.js +5 -0
  164. package/dist/workflows/dag/skill-instructions.js +471 -0
  165. package/dist/workflows/dag/skills.js +41 -0
  166. package/dist/workflows/dag/spec.js +3 -0
  167. package/dist/workflows/dag/topo.js +30 -0
  168. package/dist/workflows/dag/types.js +275 -0
  169. package/dist/workflows/dag/upstream-artifacts.js +95 -0
  170. package/dist/workflows/dag/validate.js +527 -0
  171. package/dist/workflows/dynamic/artifacts.js +65 -0
  172. package/dist/workflows/dynamic/compile.js +360 -0
  173. package/dist/workflows/dynamic/compileTypes.js +1 -0
  174. package/dist/workflows/dynamic/errors.js +5 -0
  175. package/dist/workflows/dynamic/index.js +7 -0
  176. package/dist/workflows/dynamic/profiles.js +156 -0
  177. package/dist/workflows/dynamic/spec.js +114 -0
  178. package/dist/workflows/dynamic/validate.js +275 -0
  179. package/dist/workflows/loop/actions.js +1334 -0
  180. package/dist/workflows/loop/benchmark.js +510 -0
  181. package/dist/workflows/loop/closeout.js +134 -0
  182. package/dist/workflows/loop/context.js +48 -0
  183. package/dist/workflows/loop/events.js +25 -0
  184. package/dist/workflows/loop/hash.js +32 -0
  185. package/dist/workflows/loop/index.js +8 -0
  186. package/dist/workflows/loop/paths.js +17 -0
  187. package/dist/workflows/loop/rounds.js +81 -0
  188. package/dist/workflows/loop/signals.js +55 -0
  189. package/dist/workflows/loop/state.js +116 -0
  190. package/dist/workflows/loop/templates.js +54 -0
  191. package/dist/workflows/loop/types.js +28 -0
  192. package/docs/README.md +62 -0
  193. package/docs/agent-dag-recovery-playbook.md +158 -0
  194. package/docs/agent-dag-runner.md +40 -0
  195. package/docs/cursor-executor-usage.md +25 -0
  196. package/docs/decisions/README.md +3 -0
  197. package/docs/design/README.md +36 -0
  198. package/docs/development-principles.md +71 -0
  199. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -0
  200. package/docs/exec-plans/README.md +6 -0
  201. package/docs/exec-plans/active/README.md +5 -0
  202. package/docs/exec-plans/completed/README.md +5 -0
  203. package/docs/feature-workflow.md +184 -0
  204. package/docs/harness-methodology-debugging.md +153 -0
  205. package/docs/harness-methodology-tdd.md +130 -0
  206. package/docs/harness-methodology-verification.md +27 -0
  207. package/docs/loop-agent-harness.md +42 -0
  208. package/docs/progress/README.md +3 -0
  209. package/docs/reports/README.md +3 -0
  210. package/docs/templates/adr.md +60 -0
  211. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
  212. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
  213. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
  214. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
  215. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
  216. package/docs/templates/agent-dag-report.schema.json +423 -0
  217. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
  218. package/docs/templates/agent-dag.base.json +195 -0
  219. package/docs/templates/agent-dag.final-verification.json +190 -0
  220. package/docs/templates/agent-dag.schema.json +316 -0
  221. package/docs/templates/agent-dag.supervised-implementation.json +500 -0
  222. package/docs/templates/exec-plan.md +64 -0
  223. package/docs/templates/feature-spec.md +53 -0
  224. package/docs/templates/hybrid-dag.json +193 -0
  225. package/docs/templates/progress-log.md +17 -0
  226. package/docs/templates/project-start-checklist.md +9 -0
  227. package/docs/templates/qa-report.md +42 -0
  228. package/docs/templates/sprint-contract.md +29 -0
  229. package/docs/verification-matrix.md +30 -0
  230. package/examples/decision-gate-agent-dag.json +123 -0
  231. package/examples/example-dag.json +51 -0
  232. package/examples/hybrid-loop-agent-dag.json +194 -0
  233. package/harness.json +92 -0
  234. package/package.json +61 -0
  235. package/skills/ai-engineering-context/SKILL.md +48 -0
  236. package/skills/loop-agent/SKILL.md +260 -0
  237. package/skills/loop-agent/references/README.md +63 -0
  238. package/skills/loop-agent/references/command-reference.md +315 -0
  239. package/skills/loop-agent/references/harness-policy.md +258 -0
  240. package/skills/loop-agent/references/hybrid-dag.md +216 -0
  241. package/skills/loop-agent/references/learned/README.md +21 -0
  242. package/skills/loop-agent/references/model-routing.md +36 -0
  243. package/skills/loop-agent/references/multi-worktree.md +54 -0
  244. package/skills/loop-agent/references/one-shot-runs.md +85 -0
  245. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -0
  246. package/skills/loop-agent/references/pi-prompt.md +23 -0
  247. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +83 -0
  248. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -0
  249. package/skills/loop-agent/references/task-workflow.md +84 -0
  250. package/skills/loop-agent/references/verification-and-failure-handling.md +74 -0
  251. package/skills/requesting-code-review/SKILL.md +101 -0
  252. package/skills/requesting-code-review/code-reviewer.md +168 -0
  253. package/skills/systematic-debugging/CREATION-LOG.md +119 -0
  254. package/skills/systematic-debugging/SKILL.md +296 -0
  255. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  256. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  257. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  258. package/skills/systematic-debugging/find-polluter.sh +63 -0
  259. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  260. package/skills/systematic-debugging/test-academic.md +14 -0
  261. package/skills/systematic-debugging/test-pressure-1.md +58 -0
  262. package/skills/systematic-debugging/test-pressure-2.md +68 -0
  263. package/skills/systematic-debugging/test-pressure-3.md +69 -0
  264. package/skills/verification-before-completion/SKILL.md +154 -0
@@ -0,0 +1,316 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { isPiReuseRuntimeEffective, PI_REUSE_RUNTIME_ENV, resolvePiReuseRuntimeMode, } from "./pi-runtime-reuse.js";
4
+ const PI_STEPS = new Set([
5
+ "analyze",
6
+ "plan",
7
+ "spec",
8
+ "implement",
9
+ "retrospective",
10
+ ]);
11
+ function isPiStep(step) {
12
+ return PI_STEPS.has(step);
13
+ }
14
+ function toNumber(value) {
15
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
16
+ }
17
+ function toBoolean(value) {
18
+ return typeof value === "boolean" ? value : undefined;
19
+ }
20
+ function toString(value) {
21
+ return typeof value === "string" ? value : undefined;
22
+ }
23
+ export function parseExecutorJsonlContent(content) {
24
+ const steps = [];
25
+ for (const line of content.split(/\r?\n/)) {
26
+ const trimmed = line.trim();
27
+ if (!trimmed)
28
+ continue;
29
+ let record;
30
+ try {
31
+ record = JSON.parse(trimmed);
32
+ }
33
+ catch {
34
+ continue;
35
+ }
36
+ const step = toString(record.step);
37
+ if (!step || !isPiStep(step))
38
+ continue;
39
+ steps.push({
40
+ step,
41
+ durationMs: toNumber(record.durationMs),
42
+ parsedEvents: toNumber(record.parsedEvents),
43
+ tokensUsed: toNumber(record.tokensUsed),
44
+ backend: toString(record.backend),
45
+ sdkAttempted: toBoolean(record.sdkAttempted),
46
+ fallbackUsed: toBoolean(record.fallbackUsed),
47
+ reuseRuntimeActive: toBoolean(record.reuseRuntimeActive),
48
+ failureCategory: toString(record.failureCategory),
49
+ timedOut: toBoolean(record.timedOut),
50
+ ok: toBoolean(record.ok),
51
+ });
52
+ }
53
+ return steps;
54
+ }
55
+ export function aggregateExecutorSteps(steps) {
56
+ return {
57
+ stepCount: steps.length,
58
+ totalDurationMs: steps.reduce((sum, step) => sum + step.durationMs, 0),
59
+ totalParsedEvents: steps.reduce((sum, step) => sum + step.parsedEvents, 0),
60
+ totalTokensUsed: steps.reduce((sum, step) => sum + step.tokensUsed, 0),
61
+ reuseActiveSteps: steps.filter((step) => step.reuseRuntimeActive === true)
62
+ .length,
63
+ failureSteps: steps.filter((step) => step.ok === false ||
64
+ (step.failureCategory && step.failureCategory !== "success")).length,
65
+ timedOutSteps: steps.filter((step) => step.timedOut === true).length,
66
+ cliOnlySteps: steps.filter((step) => step.backend === "cli-only").length,
67
+ steps,
68
+ };
69
+ }
70
+ export function compareModeAggregates(off, on) {
71
+ return {
72
+ durationMs: on.totalDurationMs - off.totalDurationMs,
73
+ parsedEvents: on.totalParsedEvents - off.totalParsedEvents,
74
+ tokensUsed: on.totalTokensUsed - off.totalTokensUsed,
75
+ };
76
+ }
77
+ export function parseApprovalJsonContent(content, source) {
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(content);
81
+ }
82
+ catch {
83
+ return { status: "unknown", source };
84
+ }
85
+ const rawStatus = toString(parsed.status)?.toLowerCase();
86
+ if (rawStatus === "approved") {
87
+ return {
88
+ status: "approved",
89
+ approver: toString(parsed.approver),
90
+ approvedAt: toString(parsed.approvedAt) ?? toString(parsed.date),
91
+ channel: toString(parsed.channel),
92
+ source,
93
+ };
94
+ }
95
+ if (rawStatus === "rejected") {
96
+ return { status: "rejected", source };
97
+ }
98
+ if (rawStatus === "pending") {
99
+ return { status: "pending", source };
100
+ }
101
+ if (parsed.approved === true) {
102
+ return {
103
+ status: "approved",
104
+ approver: toString(parsed.approver),
105
+ approvedAt: toString(parsed.approvedAt) ?? toString(parsed.date),
106
+ channel: toString(parsed.channel),
107
+ source,
108
+ };
109
+ }
110
+ return { status: "unknown", source };
111
+ }
112
+ export function parseApprovalFromReportMarkdown(content, source) {
113
+ const lower = content.toLowerCase();
114
+ if (content.includes("PENDING HUMAN APPROVAL") ||
115
+ content.includes("未批准") ||
116
+ /\|\s*Live benchmark 批准\s*\|\s*\*\*未批准\*\*/.test(content)) {
117
+ return { status: "pending", source };
118
+ }
119
+ const approverMatch = content.match(/\|\s*批准人\s*\|\s*([^|_\n]+)/);
120
+ const dateMatch = content.match(/\|\s*批准日期\s*\|\s*([^|_\n]+)/);
121
+ const approver = approverMatch?.[1]?.trim();
122
+ const approvedAt = dateMatch?.[1]?.trim();
123
+ const hasApprover = Boolean(approver) &&
124
+ !approver?.includes("待填写") &&
125
+ approver !== "_(待填写)_";
126
+ const hasDate = Boolean(approvedAt) &&
127
+ !approvedAt?.includes("待填写") &&
128
+ approvedAt !== "_(待填写)_";
129
+ if ((content.includes("COMPLETED") || lower.includes("measured results")) &&
130
+ hasApprover &&
131
+ hasDate) {
132
+ return {
133
+ status: "approved",
134
+ approver,
135
+ approvedAt,
136
+ source,
137
+ };
138
+ }
139
+ if (content.includes("**已批准**") ||
140
+ /\|\s*Live benchmark 批准\s*\|\s*\*\*已批准\*\*/.test(content)) {
141
+ return {
142
+ status: "approved",
143
+ approver,
144
+ approvedAt,
145
+ source,
146
+ };
147
+ }
148
+ return { status: "unknown", source };
149
+ }
150
+ function resolveTaskExecutorPath(repoRoot, taskId) {
151
+ return path.join(repoRoot, ".harness", "tasks", taskId, "logs", "executor.jsonl");
152
+ }
153
+ async function readOptionalFile(filePath) {
154
+ try {
155
+ return await readFile(filePath, "utf-8");
156
+ }
157
+ catch {
158
+ return undefined;
159
+ }
160
+ }
161
+ export function analyzePiReuseEvidence(input) {
162
+ const evidenceCount = (input.off?.stepCount ?? 0) + (input.on?.stepCount ?? 0);
163
+ const missingApproval = input.approval.status !== "approved";
164
+ const insufficientEvidence = !input.off ||
165
+ !input.on ||
166
+ input.off.stepCount === 0 ||
167
+ input.on.stepCount === 0;
168
+ const baselineReuseUnexpected = (input.off?.reuseActiveSteps ?? 0) > 0;
169
+ const treatmentReuseMissing = (input.on?.stepCount ?? 0) > 0 && (input.on?.reuseActiveSteps ?? 0) === 0;
170
+ const cliOnlyBypassObserved = (input.off?.cliOnlySteps ?? 0) > 0 || (input.on?.cliOnlySteps ?? 0) > 0;
171
+ const cliOnlyBypassViolated = [
172
+ ...(input.off?.steps ?? []),
173
+ ...(input.on?.steps ?? []),
174
+ ].some((step) => step.backend === "cli-only" && step.reuseRuntimeActive === true);
175
+ const offFailures = input.off?.failureSteps ?? 0;
176
+ const onFailures = input.on?.failureSteps ?? 0;
177
+ const treatmentOnlyFailures = onFailures > offFailures;
178
+ const hasFailure = offFailures > 0 || onFailures > 0;
179
+ const hasLeakEvidence = baselineReuseUnexpected || cliOnlyBypassViolated || treatmentReuseMissing;
180
+ const flags = {
181
+ hasFailure,
182
+ hasLeakEvidence,
183
+ cliOnlyBypassObserved,
184
+ cliOnlyBypassViolated,
185
+ missingApproval,
186
+ insufficientEvidence,
187
+ baselineReuseUnexpected,
188
+ treatmentReuseMissing,
189
+ treatmentOnlyFailures,
190
+ };
191
+ const rationale = [];
192
+ let recommendation = "defer";
193
+ if (hasLeakEvidence) {
194
+ rationale.push("Leak or reuse-boundary violation detected in supplied evidence.");
195
+ recommendation = "defer";
196
+ }
197
+ else if (missingApproval) {
198
+ rationale.push("Human approval for live benchmark expansion is missing or pending.");
199
+ recommendation = insufficientEvidence ? "defer" : "maintain-opt-in";
200
+ }
201
+ else if (insufficientEvidence) {
202
+ rationale.push("Insufficient off/on executor evidence for a live benchmark decision.");
203
+ recommendation = "defer";
204
+ }
205
+ else if (hasFailure || treatmentOnlyFailures) {
206
+ rationale.push("Failures or treatment-only regressions present; keep opt-in only.");
207
+ recommendation = "maintain-opt-in";
208
+ }
209
+ else {
210
+ rationale.push("Approval and balanced off/on evidence present with no leak/failure flags.");
211
+ recommendation = "eligible-for-human-review";
212
+ }
213
+ rationale.push("Default reuse mode remains off; this command never enables default-on.");
214
+ return {
215
+ command: "pi-reuse-benchmark",
216
+ defaultReuseMode: "off",
217
+ configuredReuseMode: input.configuredReuseMode,
218
+ liveCallsPerformed: false,
219
+ approval: input.approval,
220
+ evidenceCount,
221
+ metrics: {
222
+ off: input.off,
223
+ on: input.on,
224
+ delta: input.off && input.on
225
+ ? compareModeAggregates(input.off, input.on)
226
+ : undefined,
227
+ },
228
+ flags,
229
+ recommendation,
230
+ rationale,
231
+ };
232
+ }
233
+ export async function buildPiReuseBenchmarkResult(input) {
234
+ const env = input.env ?? process.env;
235
+ const configuredReuseMode = resolvePiReuseRuntimeMode(env);
236
+ let approval = { status: "unknown" };
237
+ if (input.approvalPath) {
238
+ const content = await readOptionalFile(input.approvalPath);
239
+ if (content) {
240
+ approval = parseApprovalJsonContent(content, input.approvalPath);
241
+ }
242
+ }
243
+ else if (input.reportPath) {
244
+ const content = await readOptionalFile(input.reportPath);
245
+ if (content) {
246
+ approval = parseApprovalFromReportMarkdown(content, input.reportPath);
247
+ }
248
+ }
249
+ const offPath = input.offExecutorPath ??
250
+ (input.offTaskId
251
+ ? resolveTaskExecutorPath(input.repoRoot, input.offTaskId)
252
+ : undefined);
253
+ const onPath = input.onExecutorPath ??
254
+ (input.onTaskId
255
+ ? resolveTaskExecutorPath(input.repoRoot, input.onTaskId)
256
+ : undefined);
257
+ let off;
258
+ let on;
259
+ if (offPath) {
260
+ const content = await readOptionalFile(offPath);
261
+ if (content) {
262
+ off = aggregateExecutorSteps(parseExecutorJsonlContent(content));
263
+ }
264
+ }
265
+ if (onPath) {
266
+ const content = await readOptionalFile(onPath);
267
+ if (content) {
268
+ on = aggregateExecutorSteps(parseExecutorJsonlContent(content));
269
+ }
270
+ }
271
+ return analyzePiReuseEvidence({
272
+ approval,
273
+ off,
274
+ on,
275
+ configuredReuseMode,
276
+ });
277
+ }
278
+ export function formatPiReuseBenchmarkMarkdown(result) {
279
+ const lines = [
280
+ "# Pi Runtime Reuse Benchmark Decision",
281
+ "",
282
+ `| Field | Value |`,
283
+ `|-------|-------|`,
284
+ `| recommendation | ${result.recommendation} |`,
285
+ `| approval.status | ${result.approval.status} |`,
286
+ `| evidenceCount | ${result.evidenceCount} |`,
287
+ `| defaultReuseMode | ${result.defaultReuseMode} |`,
288
+ `| configuredReuseMode | ${result.configuredReuseMode} |`,
289
+ `| liveCallsPerformed | ${result.liveCallsPerformed} |`,
290
+ `| hasLeakEvidence | ${result.flags.hasLeakEvidence} |`,
291
+ `| missingApproval | ${result.flags.missingApproval} |`,
292
+ "",
293
+ "## Rationale",
294
+ ...result.rationale.map((line) => `- ${line}`),
295
+ ];
296
+ if (result.metrics.off) {
297
+ lines.push("", "## Off metrics", `- steps: ${result.metrics.off.stepCount}`, `- totalDurationMs: ${result.metrics.off.totalDurationMs}`, `- totalParsedEvents: ${result.metrics.off.totalParsedEvents}`, `- totalTokensUsed: ${result.metrics.off.totalTokensUsed}`, `- reuseActiveSteps: ${result.metrics.off.reuseActiveSteps}`);
298
+ }
299
+ if (result.metrics.on) {
300
+ lines.push("", "## On metrics", `- steps: ${result.metrics.on.stepCount}`, `- totalDurationMs: ${result.metrics.on.totalDurationMs}`, `- totalParsedEvents: ${result.metrics.on.totalParsedEvents}`, `- totalTokensUsed: ${result.metrics.on.totalTokensUsed}`, `- reuseActiveSteps: ${result.metrics.on.reuseActiveSteps}`);
301
+ }
302
+ if (result.metrics.delta) {
303
+ lines.push("", "## Delta (on - off)", `- durationMs: ${result.metrics.delta.durationMs}`, `- parsedEvents: ${result.metrics.delta.parsedEvents}`, `- tokensUsed: ${result.metrics.delta.tokensUsed}`);
304
+ }
305
+ return `${lines.join("\n")}\n`;
306
+ }
307
+ /** Expose parser/env helpers for tests without live calls. */
308
+ export function summarizeConfiguredReuseMode(env = process.env) {
309
+ const mode = resolvePiReuseRuntimeMode(env);
310
+ return {
311
+ envVar: PI_REUSE_RUNTIME_ENV,
312
+ mode,
313
+ effectiveWithSdkFirst: isPiReuseRuntimeEffective(mode, "sdk-first"),
314
+ effectiveWithCliOnly: isPiReuseRuntimeEffective(mode, "cli-only"),
315
+ };
316
+ }
@@ -0,0 +1,29 @@
1
+ /** Environment variable controlling opt-in Pi SDK runtime reuse (default off). */
2
+ export const PI_REUSE_RUNTIME_ENV = 'CODE_AGENT_PI_REUSE_RUNTIME';
3
+ const AUTO_RUN_VALUE = 'auto-run';
4
+ /**
5
+ * Parse a raw `CODE_AGENT_PI_REUSE_RUNTIME` value into an effective reuse mode.
6
+ * Only the explicit `auto-run` token enables reuse; unset, off-like aliases, and
7
+ * unknown values all resolve to `off`.
8
+ */
9
+ export function parsePiReuseRuntimeMode(raw) {
10
+ const normalized = raw?.trim().toLowerCase();
11
+ if (!normalized || normalized === 'off' || normalized === 'false' || normalized === '0') {
12
+ return 'off';
13
+ }
14
+ if (normalized === AUTO_RUN_VALUE) {
15
+ return 'auto-run';
16
+ }
17
+ return 'off';
18
+ }
19
+ /** Resolve configured Pi SDK runtime reuse mode from the process environment. */
20
+ export function resolvePiReuseRuntimeMode(env = process.env) {
21
+ return parsePiReuseRuntimeMode(env[PI_REUSE_RUNTIME_ENV]);
22
+ }
23
+ /**
24
+ * True when reuse is opt-in (`auto-run`) and the Pi backend allows SDK execution.
25
+ * `cli-only` always forces reuse off regardless of the reuse env value.
26
+ */
27
+ export function isPiReuseRuntimeEffective(reuseMode, piBackend) {
28
+ return reuseMode === 'auto-run' && piBackend !== 'cli-only';
29
+ }
@@ -0,0 +1,255 @@
1
+ import { classifyPiFailure, DEFAULT_TIMEOUT_MS, extractAssistantTextFromPiJson, extractTokenUsageFromPiJson, } from './pi-executor.js';
2
+ import { serializeSessionEvent } from './pi-event-serializer.js';
3
+ let sdkSessionFactoryOverride;
4
+ let sdkImportOverrideForTests;
5
+ let sdkModuleOverrideForTests;
6
+ let activeReuseScopeState;
7
+ /**
8
+ * Begin a command/workflow-scoped SDK reuse scope.
9
+ * Callers must invoke endPiSdkReuseScope() at command end to prevent cross-command bleed.
10
+ */
11
+ export function beginPiSdkReuseScope() {
12
+ if (activeReuseScopeState && !activeReuseScopeState.disposed) {
13
+ return;
14
+ }
15
+ activeReuseScopeState = { disposed: false };
16
+ }
17
+ /** Return the active reuse scope, if any. */
18
+ export function getActivePiSdkReuseScope() {
19
+ if (!activeReuseScopeState || activeReuseScopeState.disposed) {
20
+ return undefined;
21
+ }
22
+ const state = activeReuseScopeState;
23
+ return {
24
+ isActive: () => !state.disposed,
25
+ getOrCreateResources: () => getOrCreateSharedResources(state),
26
+ };
27
+ }
28
+ /** Dispose the active reuse scope and clear cached shared resources. */
29
+ export async function endPiSdkReuseScope() {
30
+ if (!activeReuseScopeState)
31
+ return;
32
+ activeReuseScopeState.disposed = true;
33
+ activeReuseScopeState.resources = undefined;
34
+ activeReuseScopeState = undefined;
35
+ }
36
+ /** Test hook: reset command-scoped reuse state between isolated tests. */
37
+ export async function resetPiSdkReuseScopeForTests() {
38
+ await endPiSdkReuseScope();
39
+ }
40
+ /** Test hook: inject a mock session factory without loading the real SDK. */
41
+ export function setPiSdkSessionFactoryForTests(factory) {
42
+ sdkSessionFactoryOverride = factory;
43
+ }
44
+ /** Test hook: simulate SDK import success/failure without relying on optionalDependency install state. */
45
+ export function setPiSdkImportOverrideForTests(fn) {
46
+ sdkImportOverrideForTests = fn;
47
+ }
48
+ /** Test hook: inject a mock Pi SDK module to exercise resolveSdkSessionFactory without optionalDependency. */
49
+ export function setPiSdkModuleOverrideForTests(fn) {
50
+ sdkModuleOverrideForTests = fn;
51
+ }
52
+ /** Check whether the Pi SDK optional dependency is importable. */
53
+ export async function checkPiSdkAvailability(_repoRoot) {
54
+ if (sdkSessionFactoryOverride) {
55
+ return { ok: true, detail: 'pi SDK session factory override active' };
56
+ }
57
+ try {
58
+ if (sdkImportOverrideForTests) {
59
+ await sdkImportOverrideForTests();
60
+ }
61
+ else {
62
+ await import('@earendil-works/pi-coding-agent');
63
+ }
64
+ return { ok: true, detail: 'pi SDK available' };
65
+ }
66
+ catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ return { ok: false, detail: `pi SDK not available: ${message}` };
69
+ }
70
+ }
71
+ async function resolveModelViaRegistry(modelRegistry, provider, modelId) {
72
+ const fromRegistry = modelRegistry.find(provider, modelId);
73
+ if (fromRegistry)
74
+ return fromRegistry;
75
+ try {
76
+ const piAi = await import('@earendil-works/pi-ai');
77
+ return piAi.getModel?.(provider, modelId);
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ }
83
+ async function loadPiSdkModule() {
84
+ if (sdkModuleOverrideForTests) {
85
+ return sdkModuleOverrideForTests();
86
+ }
87
+ return await import('@earendil-works/pi-coding-agent');
88
+ }
89
+ async function getOrCreateSharedResources(state) {
90
+ if (state.resources)
91
+ return state.resources;
92
+ const sdk = await loadPiSdkModule();
93
+ const getAgentDir = sdk.getAgentDir;
94
+ const AuthStorage = sdk.AuthStorage;
95
+ const ModelRegistry = sdk.ModelRegistry;
96
+ const agentDir = getAgentDir();
97
+ const authStorage = AuthStorage.create();
98
+ const modelRegistry = ModelRegistry.create(authStorage);
99
+ state.resources = { agentDir, authStorage, modelRegistry };
100
+ return state.resources;
101
+ }
102
+ async function createSdkSession(sdk, input, shared) {
103
+ const createAgentSession = sdk.createAgentSession;
104
+ const SessionManager = sdk.SessionManager;
105
+ const DefaultResourceLoader = sdk.DefaultResourceLoader;
106
+ const getAgentDir = sdk.getAgentDir;
107
+ const AuthStorage = sdk.AuthStorage;
108
+ const ModelRegistry = sdk.ModelRegistry;
109
+ const agentDir = shared?.agentDir ?? getAgentDir();
110
+ const authStorage = shared?.authStorage ?? AuthStorage.create();
111
+ const modelRegistry = shared?.modelRegistry ?? ModelRegistry.create(authStorage);
112
+ const model = input.provider && input.model
113
+ ? await resolveModelViaRegistry(modelRegistry, input.provider, input.model)
114
+ : undefined;
115
+ const loader = new DefaultResourceLoader({
116
+ cwd: input.cwd,
117
+ agentDir,
118
+ noContextFiles: true,
119
+ noSkills: true,
120
+ appendSystemPromptOverride: (base) => [...base, input.appendSystemPrompt],
121
+ });
122
+ await loader.reload();
123
+ const created = await createAgentSession({
124
+ cwd: input.cwd,
125
+ sessionManager: SessionManager.inMemory(input.cwd),
126
+ resourceLoader: loader,
127
+ tools: input.toolNames,
128
+ authStorage,
129
+ modelRegistry,
130
+ ...(model ? { model } : {}),
131
+ ...(input.thinking ? { thinkingLevel: input.thinking } : {}),
132
+ });
133
+ return created.session;
134
+ }
135
+ async function resolveSdkSessionFactory(reuseScope) {
136
+ if (sdkSessionFactoryOverride)
137
+ return sdkSessionFactoryOverride;
138
+ const sdk = await loadPiSdkModule();
139
+ return async (input) => {
140
+ const shared = reuseScope ? await reuseScope.getOrCreateResources() : undefined;
141
+ return createSdkSession(sdk, input, shared);
142
+ };
143
+ }
144
+ /**
145
+ * Execute a single Pi step via the SDK.
146
+ * When reuseScope is active, only shared auth/model resources are reused; each attempt still
147
+ * creates and disposes its own session and resource loader. Session reuse across steps is
148
+ * deferred until a proven reset/isolation strategy exists.
149
+ */
150
+ export async function executeSingleSdkAttempt(options) {
151
+ const modelConfig = options.modelConfig;
152
+ const modelDisplay = modelConfig.provider && modelConfig.model
153
+ ? `${modelConfig.provider}/${modelConfig.model}`
154
+ : modelConfig.model ?? 'default';
155
+ const timeoutMs = options.timeoutMs ?? modelConfig.timeoutMs ?? DEFAULT_TIMEOUT_MS;
156
+ const piSdkArgs = [
157
+ '--provider', modelConfig.provider ?? '(default)',
158
+ '--model', modelConfig.model ?? '(default)',
159
+ ...(modelConfig.thinking ? ['--thinking', modelConfig.thinking] : []),
160
+ '--tools', options.toolNames.join(','),
161
+ '--append-system-prompt', options.prompt,
162
+ ...options.attachedFiles.map((file) => `@${file}`),
163
+ options.userMessage,
164
+ ];
165
+ const startedAt = Date.now();
166
+ let timedOut = false;
167
+ let stderr = '';
168
+ const stdoutLines = [];
169
+ let session;
170
+ let timeoutHandle;
171
+ try {
172
+ const createSession = await resolveSdkSessionFactory(options.reuseScope);
173
+ session = await createSession({
174
+ cwd: options.repoRoot,
175
+ toolNames: options.toolNames,
176
+ appendSystemPrompt: options.prompt,
177
+ provider: modelConfig.provider,
178
+ model: modelConfig.model,
179
+ thinking: modelConfig.thinking,
180
+ });
181
+ const unsubscribe = session.subscribe((event) => {
182
+ stdoutLines.push(serializeSessionEvent(event));
183
+ });
184
+ const filePrefix = options.attachedFiles.map((file) => `@${file}`).join(' ');
185
+ const promptMessage = filePrefix
186
+ ? `${filePrefix}\n${options.userMessage}`
187
+ : options.userMessage;
188
+ const promptPromise = session.prompt(promptMessage);
189
+ const timeoutPromise = timeoutMs > 0
190
+ ? new Promise((resolve) => {
191
+ timeoutHandle = setTimeout(() => {
192
+ timedOut = true;
193
+ void session?.abort();
194
+ resolve('timeout');
195
+ }, timeoutMs);
196
+ })
197
+ : null;
198
+ if (timeoutPromise) {
199
+ const raced = await Promise.race([
200
+ promptPromise.then(() => 'done'),
201
+ timeoutPromise,
202
+ ]);
203
+ if (raced === 'timeout') {
204
+ stderr = `pi SDK step timed out after ${timeoutMs}ms`;
205
+ }
206
+ }
207
+ else {
208
+ await promptPromise;
209
+ }
210
+ unsubscribe();
211
+ }
212
+ catch (error) {
213
+ const message = error instanceof Error ? error.message : String(error);
214
+ stderr = stderr ? `${stderr}\n${message}` : message;
215
+ }
216
+ finally {
217
+ if (timeoutHandle)
218
+ clearTimeout(timeoutHandle);
219
+ if (session) {
220
+ try {
221
+ await session.dispose();
222
+ }
223
+ catch (disposeError) {
224
+ const message = disposeError instanceof Error ? disposeError.message : String(disposeError);
225
+ stderr = stderr ? `${stderr}\n${message}` : message;
226
+ }
227
+ }
228
+ }
229
+ const stdout = stdoutLines.join('\n');
230
+ const durationMs = Date.now() - startedAt;
231
+ const { assistantText, parsedEvents } = extractAssistantTextFromPiJson(stdout);
232
+ const tokensUsed = extractTokenUsageFromPiJson(stdout);
233
+ const failureCategory = classifyPiFailure({
234
+ assistantText,
235
+ exitCode: timedOut ? 1 : stderr ? 1 : 0,
236
+ stderr,
237
+ stdout,
238
+ timedOut,
239
+ });
240
+ return {
241
+ assistantText,
242
+ backend: 'sdk',
243
+ command: ['pi-sdk', ...piSdkArgs],
244
+ durationMs,
245
+ exitCode: timedOut || stderr ? 1 : 0,
246
+ failureCategory,
247
+ modelDisplay,
248
+ ok: !timedOut && !stderr && assistantText.length > 0,
249
+ parsedEvents,
250
+ stderr,
251
+ stdout,
252
+ timedOut,
253
+ tokensUsed,
254
+ };
255
+ }
@@ -0,0 +1 @@
1
+ export * from "./pi-sdk-executor.js";
@@ -0,0 +1,3 @@
1
+ export * from "./pi-event-serializer.js";
2
+ export * from "./pi-executor.js";
3
+ export * from "./pi-reuse-benchmark.js";