@fitchmultz/pi-subagents 0.0.0-stage → 0.44.4
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.
- package/CHANGELOG.md +1645 -0
- package/LICENSE +21 -0
- package/README.md +1529 -2
- package/agents/context-builder.md +97 -0
- package/agents/debugger.md +51 -0
- package/agents/delegate.md +13 -0
- package/agents/fixer.md +37 -0
- package/agents/oracle.md +71 -0
- package/agents/planner.md +72 -0
- package/agents/researcher.md +51 -0
- package/agents/reviewer-claude.md +69 -0
- package/agents/reviewer-gpt.md +68 -0
- package/agents/reviewer-ponytail.md +82 -0
- package/agents/reviewer-security.md +81 -0
- package/agents/reviewer.md +69 -0
- package/agents/scout.md +66 -0
- package/agents/ui-designer.md +51 -0
- package/agents/watcher.md +26 -0
- package/agents/worker.md +46 -0
- package/agents/writer.md +36 -0
- package/dist/agents/agent-management.js +923 -0
- package/dist/agents/agent-scope.js +7 -0
- package/dist/agents/agent-selection.js +20 -0
- package/dist/agents/agent-serializer.js +100 -0
- package/dist/agents/agents.js +752 -0
- package/dist/agents/chain-serializer.js +367 -0
- package/dist/agents/frontmatter.js +21 -0
- package/dist/agents/identity.js +29 -0
- package/dist/agents/skills.js +570 -0
- package/dist/extension/build-info.js +1 -0
- package/dist/extension/compact-tools.js +112 -0
- package/dist/extension/config.js +22 -0
- package/dist/extension/control-notices.js +29 -0
- package/dist/extension/doctor.js +143 -0
- package/dist/extension/fanout-child.js +326 -0
- package/dist/extension/index.js +577 -0
- package/dist/extension/schemas.js +416 -0
- package/dist/extension/tool-input.js +20 -0
- package/dist/extension/tool-result.js +14 -0
- package/dist/history/canonical-result.js +81 -0
- package/dist/history/database.js +72 -0
- package/dist/history/index.js +194 -0
- package/dist/history/ingest.js +447 -0
- package/dist/history/queries.js +350 -0
- package/dist/history/store.js +216 -0
- package/dist/history/types.js +4 -0
- package/dist/history/worker.js +461 -0
- package/dist/intercom/intercom-bridge.js +112 -0
- package/dist/intercom/live-intercom.js +94 -0
- package/dist/intercom/result-intercom.js +373 -0
- package/dist/pi-intercom/agent-dir.js +10 -0
- package/dist/pi-intercom/broker/broker.js +426 -0
- package/dist/pi-intercom/broker/client.js +492 -0
- package/dist/pi-intercom/broker/framing.js +65 -0
- package/dist/pi-intercom/broker/paths.js +42 -0
- package/dist/pi-intercom/broker/pid.js +66 -0
- package/dist/pi-intercom/broker/spawn.js +221 -0
- package/dist/pi-intercom/config.js +102 -0
- package/dist/pi-intercom/index.js +2942 -0
- package/dist/pi-intercom/reply-tracker.js +182 -0
- package/dist/pi-intercom/session-targets.js +128 -0
- package/dist/pi-intercom/subagent-live-events.js +132 -0
- package/dist/pi-intercom/topics.js +157 -0
- package/dist/pi-intercom/types.js +163 -0
- package/dist/pi-intercom/ui/compose.js +269 -0
- package/dist/pi-intercom/ui/inline-message.js +80 -0
- package/dist/pi-intercom/ui/session-list.js +124 -0
- package/dist/runs/background/async-control.js +95 -0
- package/dist/runs/background/async-execution.js +676 -0
- package/dist/runs/background/async-job-tracker.js +383 -0
- package/dist/runs/background/async-result-file.js +58 -0
- package/dist/runs/background/async-resume.js +305 -0
- package/dist/runs/background/async-status.js +442 -0
- package/dist/runs/background/completion-dedupe.js +69 -0
- package/dist/runs/background/completion-delivery.js +245 -0
- package/dist/runs/background/notify.js +71 -0
- package/dist/runs/background/parallel-groups.js +55 -0
- package/dist/runs/background/result-watcher.js +426 -0
- package/dist/runs/background/run-id-resolver.js +64 -0
- package/dist/runs/background/run-status.js +382 -0
- package/dist/runs/background/stale-run-reconciler.js +335 -0
- package/dist/runs/background/subagent-runner-launcher.js +55 -0
- package/dist/runs/background/subagent-runner.js +1872 -0
- package/dist/runs/background/top-level-async.js +5 -0
- package/dist/runs/foreground/chain-clarify.js +1152 -0
- package/dist/runs/foreground/clarify-invocation.js +72 -0
- package/dist/runs/foreground/execution-input.js +423 -0
- package/dist/runs/foreground/foreground-control.js +817 -0
- package/dist/runs/foreground/question-control.js +77 -0
- package/dist/runs/foreground/run-async-path.js +205 -0
- package/dist/runs/foreground/subagent-executor.js +442 -0
- package/dist/runs/foreground/subagent-params.js +165 -0
- package/dist/runs/foreground/wait-run.js +139 -0
- package/dist/runs/shared/acceptance-contract.js +253 -0
- package/dist/runs/shared/acceptance-evaluation.js +256 -0
- package/dist/runs/shared/acceptance-finalization.js +280 -0
- package/dist/runs/shared/acceptance-reports.js +147 -0
- package/dist/runs/shared/acceptance.js +4 -0
- package/dist/runs/shared/chain-outputs.js +110 -0
- package/dist/runs/shared/child-attempt.js +693 -0
- package/dist/runs/shared/child-execution-cwd.js +96 -0
- package/dist/runs/shared/child-observations.js +114 -0
- package/dist/runs/shared/claude-code.js +199 -0
- package/dist/runs/shared/completion-guard.js +31 -0
- package/dist/runs/shared/dynamic-fanout.js +274 -0
- package/dist/runs/shared/history-index.js +72 -0
- package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
- package/dist/runs/shared/model-fallback.js +185 -0
- package/dist/runs/shared/mutating-tool-guard.js +170 -0
- package/dist/runs/shared/native-finalization.js +101 -0
- package/dist/runs/shared/native-usage.js +153 -0
- package/dist/runs/shared/nested-events.js +700 -0
- package/dist/runs/shared/nested-path.js +48 -0
- package/dist/runs/shared/nested-render.js +108 -0
- package/dist/runs/shared/parallel-utils.js +61 -0
- package/dist/runs/shared/parent-receipts.js +68 -0
- package/dist/runs/shared/parent-usage.js +121 -0
- package/dist/runs/shared/pi-args.js +310 -0
- package/dist/runs/shared/pi-spawn.js +52 -0
- package/dist/runs/shared/run-history-queries.js +45 -0
- package/dist/runs/shared/run-history.js +103 -0
- package/dist/runs/shared/run-id-ambiguity.js +4 -0
- package/dist/runs/shared/run-records.js +679 -0
- package/dist/runs/shared/session-cwd-preload.js +33 -0
- package/dist/runs/shared/single-output.js +222 -0
- package/dist/runs/shared/streaming-text.js +15 -0
- package/dist/runs/shared/structured-output.js +55 -0
- package/dist/runs/shared/subagent-control.js +179 -0
- package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
- package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
- package/dist/runs/shared/supervisor-questions.js +320 -0
- package/dist/runs/shared/workflow-graph.js +214 -0
- package/dist/runs/shared/workflow-policy.js +84 -0
- package/dist/runs/shared/worktree.js +525 -0
- package/dist/shared/agent-context-policy.js +126 -0
- package/dist/shared/artifacts.js +96 -0
- package/dist/shared/atomic-json.js +13 -0
- package/dist/shared/cooperative.js +20 -0
- package/dist/shared/execution-cwd.js +14 -0
- package/dist/shared/file-coalescer.js +21 -0
- package/dist/shared/fork-context.js +49 -0
- package/dist/shared/formatters.js +85 -0
- package/dist/shared/journal-reader.js +407 -0
- package/dist/shared/lazy-tools.js +32 -0
- package/dist/shared/model-info.js +68 -0
- package/dist/shared/native-session.js +15 -0
- package/dist/shared/native-tui.js +18 -0
- package/dist/shared/native-typebox.js +20 -0
- package/dist/shared/post-exit-stdio-guard.js +197 -0
- package/dist/shared/prompt-sections.js +8 -0
- package/dist/shared/session-entries.js +52 -0
- package/dist/shared/session-identity.js +14 -0
- package/dist/shared/settings.js +254 -0
- package/dist/shared/status-format.js +111 -0
- package/dist/shared/temp-root.js +114 -0
- package/dist/shared/types.js +207 -0
- package/dist/shared/ui-mode.js +3 -0
- package/dist/shared/utils.js +334 -0
- package/dist/slash/prompt-template-bridge.js +298 -0
- package/dist/slash/slash-bridge.js +116 -0
- package/dist/slash/slash-commands.js +607 -0
- package/dist/slash/slash-live-state.js +246 -0
- package/dist/tui/action-hints.js +75 -0
- package/dist/tui/agent-history.js +197 -0
- package/dist/tui/agent-view.js +1656 -0
- package/dist/tui/render.js +1515 -0
- package/docs/intercom.md +579 -0
- package/docs/performance.md +121 -0
- package/package.json +101 -4
- package/prompts/gather-context-and-clarify.md +14 -0
- package/prompts/parallel-cleanup.md +62 -0
- package/prompts/parallel-context-build.md +56 -0
- package/prompts/parallel-handoff-plan.md +62 -0
- package/prompts/parallel-research.md +51 -0
- package/prompts/parallel-review.md +59 -0
- package/prompts/review-loop.md +42 -0
- package/scripts/build.mjs +134 -0
- package/scripts/check-compat.mjs +48 -0
- package/scripts/compat-host.mjs +19 -0
- package/scripts/linux-smoke.sh +51 -0
- package/scripts/local-install-smoke.mjs +66 -0
- package/scripts/native-package-smoke.mjs +82 -0
- package/scripts/package-smoke.mjs +205 -0
- package/scripts/prepare.mjs +67 -0
- package/scripts/real-pi-smoke.mjs +415 -0
- package/scripts/run-tests.mjs +149 -0
- package/skills/pi-intercom/SKILL.md +113 -0
- package/skills/pi-intercom/evals/evals.json +29 -0
- package/skills/pi-intercom/evals/trigger-evals.json +47 -0
- package/skills/pi-intercom/references/peer-sessions.md +69 -0
- package/skills/pi-subagents/SKILL.md +76 -0
- package/skills/pi-subagents/evals/evals.json +29 -0
- package/skills/pi-subagents/evals/trigger-evals.json +42 -0
- package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
- package/src/agents/agent-management.ts +813 -0
- package/src/agents/agent-scope.ts +7 -0
- package/src/agents/agent-selection.ts +23 -0
- package/src/agents/agent-serializer.ts +99 -0
- package/src/agents/agents.ts +916 -0
- package/src/agents/chain-serializer.ts +329 -0
- package/src/agents/frontmatter.ts +21 -0
- package/src/agents/identity.ts +30 -0
- package/src/agents/skills.ts +637 -0
- package/src/extension/build-info.ts +2 -0
- package/src/extension/compact-tools.ts +114 -0
- package/src/extension/config.ts +23 -0
- package/src/extension/control-notices.ts +47 -0
- package/src/extension/doctor.ts +171 -0
- package/src/extension/fanout-child.ts +312 -0
- package/src/extension/index.ts +649 -0
- package/src/extension/schemas.ts +444 -0
- package/src/extension/tool-input.ts +19 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/history/canonical-result.ts +62 -0
- package/src/history/database.ts +59 -0
- package/src/history/index.ts +152 -0
- package/src/history/ingest.ts +332 -0
- package/src/history/queries.ts +241 -0
- package/src/history/store.ts +203 -0
- package/src/history/types.ts +166 -0
- package/src/history/worker.ts +317 -0
- package/src/intercom/intercom-bridge.ts +123 -0
- package/src/intercom/live-intercom.ts +104 -0
- package/src/intercom/result-intercom.ts +429 -0
- package/src/pi-intercom/agent-dir.ts +9 -0
- package/src/pi-intercom/broker/broker.ts +463 -0
- package/src/pi-intercom/broker/client.ts +558 -0
- package/src/pi-intercom/broker/framing.ts +77 -0
- package/src/pi-intercom/broker/paths.ts +47 -0
- package/src/pi-intercom/broker/pid.ts +59 -0
- package/src/pi-intercom/broker/spawn.ts +248 -0
- package/src/pi-intercom/config.ts +142 -0
- package/src/pi-intercom/index.ts +2929 -0
- package/src/pi-intercom/reply-tracker.ts +211 -0
- package/src/pi-intercom/session-targets.ts +172 -0
- package/src/pi-intercom/subagent-live-events.ts +133 -0
- package/src/pi-intercom/topics.ts +125 -0
- package/src/pi-intercom/types.ts +271 -0
- package/src/pi-intercom/ui/compose.ts +289 -0
- package/src/pi-intercom/ui/inline-message.ts +92 -0
- package/src/pi-intercom/ui/session-list.ts +139 -0
- package/src/runs/background/async-control.ts +89 -0
- package/src/runs/background/async-execution.ts +850 -0
- package/src/runs/background/async-job-tracker.ts +385 -0
- package/src/runs/background/async-result-file.ts +62 -0
- package/src/runs/background/async-resume.ts +333 -0
- package/src/runs/background/async-status.ts +481 -0
- package/src/runs/background/completion-dedupe.ts +79 -0
- package/src/runs/background/completion-delivery.ts +195 -0
- package/src/runs/background/notify.ts +123 -0
- package/src/runs/background/parallel-groups.ts +53 -0
- package/src/runs/background/result-watcher.ts +423 -0
- package/src/runs/background/run-id-resolver.ts +69 -0
- package/src/runs/background/run-status.ts +384 -0
- package/src/runs/background/stale-run-reconciler.ts +369 -0
- package/src/runs/background/subagent-runner-launcher.ts +56 -0
- package/src/runs/background/subagent-runner.ts +2053 -0
- package/src/runs/background/top-level-async.ts +13 -0
- package/src/runs/foreground/chain-clarify.ts +1340 -0
- package/src/runs/foreground/clarify-invocation.ts +77 -0
- package/src/runs/foreground/execution-input.ts +467 -0
- package/src/runs/foreground/foreground-control.ts +821 -0
- package/src/runs/foreground/question-control.ts +70 -0
- package/src/runs/foreground/run-async-path.ts +243 -0
- package/src/runs/foreground/subagent-executor.ts +501 -0
- package/src/runs/foreground/subagent-params.ts +320 -0
- package/src/runs/foreground/wait-run.ts +121 -0
- package/src/runs/shared/acceptance-contract.ts +257 -0
- package/src/runs/shared/acceptance-evaluation.ts +275 -0
- package/src/runs/shared/acceptance-finalization.ts +353 -0
- package/src/runs/shared/acceptance-reports.ts +152 -0
- package/src/runs/shared/acceptance.ts +33 -0
- package/src/runs/shared/chain-outputs.ts +114 -0
- package/src/runs/shared/child-attempt.ts +623 -0
- package/src/runs/shared/child-execution-cwd.ts +91 -0
- package/src/runs/shared/child-observations.ts +99 -0
- package/src/runs/shared/claude-code.ts +243 -0
- package/src/runs/shared/completion-guard.ts +35 -0
- package/src/runs/shared/dynamic-fanout.ts +295 -0
- package/src/runs/shared/history-index.ts +69 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
- package/src/runs/shared/model-fallback.ts +219 -0
- package/src/runs/shared/mutating-tool-guard.ts +194 -0
- package/src/runs/shared/native-finalization.ts +128 -0
- package/src/runs/shared/native-usage.ts +113 -0
- package/src/runs/shared/nested-events.ts +752 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +119 -0
- package/src/runs/shared/parallel-utils.ts +153 -0
- package/src/runs/shared/parent-receipts.ts +59 -0
- package/src/runs/shared/parent-usage.ts +121 -0
- package/src/runs/shared/pi-args.ts +350 -0
- package/src/runs/shared/pi-spawn.ts +52 -0
- package/src/runs/shared/run-history-queries.ts +44 -0
- package/src/runs/shared/run-history.ts +93 -0
- package/src/runs/shared/run-id-ambiguity.ts +4 -0
- package/src/runs/shared/run-records.ts +607 -0
- package/src/runs/shared/session-cwd-preload.ts +27 -0
- package/src/runs/shared/single-output.ts +267 -0
- package/src/runs/shared/streaming-text.ts +18 -0
- package/src/runs/shared/structured-output.ts +68 -0
- package/src/runs/shared/subagent-control.ts +219 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
- package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
- package/src/runs/shared/supervisor-questions.ts +372 -0
- package/src/runs/shared/workflow-graph.ts +220 -0
- package/src/runs/shared/workflow-policy.ts +125 -0
- package/src/runs/shared/worktree.ts +635 -0
- package/src/shared/agent-context-policy.ts +184 -0
- package/src/shared/artifacts.ts +95 -0
- package/src/shared/atomic-json.ts +16 -0
- package/src/shared/cooperative.ts +21 -0
- package/src/shared/execution-cwd.ts +14 -0
- package/src/shared/file-coalescer.ts +29 -0
- package/src/shared/fork-context.ts +68 -0
- package/src/shared/formatters.ts +84 -0
- package/src/shared/journal-reader.ts +333 -0
- package/src/shared/lazy-tools.ts +36 -0
- package/src/shared/model-info.ts +81 -0
- package/src/shared/native-session.ts +8 -0
- package/src/shared/native-tui.ts +10 -0
- package/src/shared/native-typebox.ts +14 -0
- package/src/shared/post-exit-stdio-guard.ts +204 -0
- package/src/shared/prompt-sections.ts +10 -0
- package/src/shared/session-entries.ts +49 -0
- package/src/shared/session-identity.ts +18 -0
- package/src/shared/settings.ts +384 -0
- package/src/shared/status-format.ts +123 -0
- package/src/shared/temp-root.ts +107 -0
- package/src/shared/types.ts +1259 -0
- package/src/shared/ui-mode.ts +5 -0
- package/src/shared/utils.ts +342 -0
- package/src/slash/prompt-template-bridge.ts +408 -0
- package/src/slash/slash-bridge.ts +178 -0
- package/src/slash/slash-commands.ts +617 -0
- package/src/slash/slash-live-state.ts +292 -0
- package/src/tui/action-hints.ts +69 -0
- package/src/tui/agent-history.ts +210 -0
- package/src/tui/agent-view.ts +1323 -0
- package/src/tui/render.ts +1550 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { writeAsyncControlRequest } from "../background/async-control.js";
|
|
3
|
+
import { ownedRunExecutionResult, ownedRunProgressResult, ownedRunStatusResult, ownedRunView, resolveOwnedRun } from "../shared/run-records.js";
|
|
4
|
+
import { getRunMetadataDir, listOwnedRunQuestions, questionProcessAlive, readRunJson } from "../shared/supervisor-questions.js";
|
|
5
|
+
import { getSingleResultOutput, readStatus } from "../../shared/utils.js";
|
|
6
|
+
import { INTERCOM_DETACH_REQUEST_EVENT, INTERCOM_DETACH_RESPONSE_EVENT, POLL_INTERVAL_MS } from "../../shared/types.js";
|
|
7
|
+
import { resolveSubagentRunId } from "../background/run-id-resolver.js";
|
|
8
|
+
import { resolveNestedAsyncDir } from "../shared/nested-events.js";
|
|
9
|
+
import { nestedResolutionScopeForExecutor } from "./foreground-control.js";
|
|
10
|
+
/** Wait on the saved outcome, not the transient notification file or an early terminal status. */
|
|
11
|
+
export async function waitForOwnedRun(input) {
|
|
12
|
+
const { deps, id, index, ctx } = input;
|
|
13
|
+
const owner = ctx.sessionManager.getSessionId(), session = deps.state.currentSessionId;
|
|
14
|
+
let run, nested = false;
|
|
15
|
+
try {
|
|
16
|
+
run = resolveOwnedRun(deps.state, id);
|
|
17
|
+
if (!run) {
|
|
18
|
+
const resolved = resolveSubagentRunId(id, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
|
|
19
|
+
if (resolved?.kind === "nested") {
|
|
20
|
+
const asyncDir = resolveNestedAsyncDir(resolved.match.rootRunId, resolved.match.run);
|
|
21
|
+
const status = asyncDir ? readStatus(asyncDir) : null;
|
|
22
|
+
if (status?.runId === resolved.id) {
|
|
23
|
+
const savedOwner = readRunJson(path.join(getRunMetadataDir(resolved.id), "question-owner.json"));
|
|
24
|
+
// Read through the authorized route without adopting the descendant or attributing its usage to this caller.
|
|
25
|
+
run = { runId: resolved.id, rootRunId: resolved.match.rootRunId, ownerSessionId: savedOwner?.sessionId ?? status.sessionId ?? "",
|
|
26
|
+
source: "async", mode: status.mode, cwd: status.cwd ?? "", task: "Nested delegated run", startedAt: status.startedAt, asyncDir, pid: status.pid,
|
|
27
|
+
children: (status.steps ?? []).map((step, index) => ({ agent: step.agent, index, sessionFile: step.sessionFile })) };
|
|
28
|
+
nested = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return { content: [{ type: "text", text: String(error) }], isError: true, details: { mode: "management", results: [] } };
|
|
35
|
+
}
|
|
36
|
+
if (!run || (!nested && run.ownerSessionId !== owner))
|
|
37
|
+
return { content: [{ type: "text", text: "Run is not available to wait on in this session. This wait did not start or stop work." }], isError: true, details: { mode: "management", results: [] } };
|
|
38
|
+
const target = run;
|
|
39
|
+
const waiting = deps.state.waitingRuns ??= new Map();
|
|
40
|
+
waiting.set(target.runId, (waiting.get(target.runId) ?? 0) + 1);
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let finished = false, timer, unsubscribe;
|
|
43
|
+
let previous = "";
|
|
44
|
+
const finish = (status, text, result) => {
|
|
45
|
+
if (finished)
|
|
46
|
+
return;
|
|
47
|
+
finished = true;
|
|
48
|
+
const remaining = (waiting.get(target.runId) ?? 1) - 1;
|
|
49
|
+
if (remaining)
|
|
50
|
+
waiting.set(target.runId, remaining);
|
|
51
|
+
else
|
|
52
|
+
waiting.delete(target.runId);
|
|
53
|
+
if (timer)
|
|
54
|
+
clearInterval(timer);
|
|
55
|
+
unsubscribe?.();
|
|
56
|
+
input.signal?.removeEventListener("abort", abort);
|
|
57
|
+
const execution = input.executionResult && status === "completed" ? ownedRunExecutionResult(target, deps.state, index, input.includeProgress) : undefined;
|
|
58
|
+
const saved = status === "completed" ? readRunJson(path.join(getRunMetadataDir(target.runId), "result.json")) : undefined;
|
|
59
|
+
resolve({ ...result, ...execution, content: status === "completed" && execution ? execution.content : [{ type: "text", text }],
|
|
60
|
+
...(status === "unavailable" || status === "cancelled" ? { isError: true } : {}),
|
|
61
|
+
details: { mode: "management", results: [], ...result?.details, ...execution?.details, wait: { runId: target.runId, completionId: saved?.completionId, index, status } } });
|
|
62
|
+
};
|
|
63
|
+
const abort = () => {
|
|
64
|
+
if (input.cancelNewRun && target.asyncDir) {
|
|
65
|
+
writeAsyncControlRequest(target.asyncDir, target.runId, "cancel");
|
|
66
|
+
}
|
|
67
|
+
finish("cancelled", input.cancelNewRun ? `Wait cancelled; cancellation requested for newly launched run ${target.runId}. Process exit is not yet confirmed.` : `Stopped waiting for ${target.runId}. The child was not stopped; completion will arrive automatically.`);
|
|
68
|
+
};
|
|
69
|
+
const check = () => {
|
|
70
|
+
if (deps.state.currentSessionId !== session || ctx.sessionManager.getSessionId() !== owner) {
|
|
71
|
+
finish("unavailable", "The owning session changed. This wait ended without stopping the child.");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const runQuestions = listOwnedRunQuestions(target.ownerSessionId, target.runId);
|
|
76
|
+
// Poll execution facts once. Native conversation configuration is only
|
|
77
|
+
// needed when returning an inspection, never for live progress.
|
|
78
|
+
const view = ownedRunView(target, deps.state, {
|
|
79
|
+
pendingInput: runQuestions.some((question) => question.state === "awaiting_input" || question.state === "answer_pending"),
|
|
80
|
+
readConfiguration: false, includeContinuations: false,
|
|
81
|
+
});
|
|
82
|
+
const children = view.children.filter((child) => index === undefined || child.index === index);
|
|
83
|
+
if (index !== undefined && !children.length) {
|
|
84
|
+
finish("unavailable", `Run ${target.runId} has no child at index ${index}.`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const questions = runQuestions.filter((question) => question.ownerSessionId === owner && (index === undefined || question.index === index) && question.state === "awaiting_input");
|
|
88
|
+
if (questions.length) {
|
|
89
|
+
const result = ownedRunStatusResult(target, deps.state);
|
|
90
|
+
result.details.questions = questions;
|
|
91
|
+
finish("awaiting_input", `Run ${target.runId} needs input; waiting ended without stopping it.\n\n${questions.map((question) => `Question ${question.questionId}: ${question.message}`).join("\n\n")}`, result);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if ((index !== undefined || view.resultPath) && children.every((child) => child.result && child.state !== "live" && child.state !== "unknown") && (index !== undefined || view.state !== "live")) {
|
|
95
|
+
const result = ownedRunStatusResult(target, deps.state);
|
|
96
|
+
result.isError = children.some((child) => child.state === "failed") || (index === undefined && view.state === "failed") || undefined;
|
|
97
|
+
finish("completed", [`Saved result for ${target.runId}${index !== undefined ? ` child ${index}` : ""}: ${index !== undefined ? children[0].state : view.state}`, ...children.map((child) => `\n${child.agent}: ${child.state}\n${getSingleResultOutput(child.result) || child.result?.error || "(no output)"}`), result.content.map((part) => part.type === "text" ? part.text : "").join("\n")].join("\n"), result);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const pid = target.pid ?? (target.asyncDir ? readStatus(target.asyncDir)?.pid : undefined);
|
|
101
|
+
const producerAlive = pid ? questionProcessAlive({ pid }) : children.some((child) => child.state === "live");
|
|
102
|
+
if (!producerAlive) {
|
|
103
|
+
finish("unavailable", `No saved final result is available for ${target.runId}; completion is unconfirmed. Inspect the saved session. No work was started.`, ownedRunStatusResult(target, deps.state));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const update = `Waiting for ${target.runId}${index !== undefined ? ` child ${index}` : ""}: ${children.map((child) => child.state).join(", ")}. ${input.cancelNewRun ? "Cancelling requests cancellation of this newly launched run; process exit still needs confirmation." : "Cancelling this wait leaves existing work alive."}`;
|
|
107
|
+
if (input.onUpdate) {
|
|
108
|
+
const progress = ownedRunProgressResult(target, deps.state, index, view);
|
|
109
|
+
const signature = JSON.stringify(progress.details.progress?.map(({ durationMs: _duration, ...activity }) => activity));
|
|
110
|
+
if (signature !== previous) {
|
|
111
|
+
previous = signature;
|
|
112
|
+
input.onUpdate({ ...progress, content: [{ type: "text", text: update }, ...progress.content] });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
finish("unavailable", `Wait could not read ${target.runId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
unsubscribe = deps.pi.events.on(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
|
|
121
|
+
if (finished || !payload || typeof payload !== "object")
|
|
122
|
+
return;
|
|
123
|
+
const requestId = payload.requestId;
|
|
124
|
+
if (typeof requestId !== "string")
|
|
125
|
+
return;
|
|
126
|
+
deps.pi.events.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted: true });
|
|
127
|
+
finish("yielded", `Released the wait for an incoming Intercom message. Run ${target.runId} is unchanged. Continue useful work or end the turn; completion will arrive automatically.`);
|
|
128
|
+
});
|
|
129
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
130
|
+
if (input.signal?.aborted)
|
|
131
|
+
abort();
|
|
132
|
+
else
|
|
133
|
+
check();
|
|
134
|
+
// Unlike passive background tracking, this foreground call owes a result.
|
|
135
|
+
// The detached runner cannot keep this process alive; finish() releases the timer.
|
|
136
|
+
if (!finished)
|
|
137
|
+
timer = setInterval(check, POLL_INTERVAL_MS);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Compile } from "../../shared/native-typebox.js";
|
|
2
|
+
import { AcceptanceOverride } from "../../extension/schemas.js";
|
|
3
|
+
import { formatAcceptanceReportExample } from "./acceptance-reports.js";
|
|
4
|
+
const DEFAULT_FINALIZATION_MAX_TURNS = 3;
|
|
5
|
+
const MAX_FINALIZATION_TURNS = 10;
|
|
6
|
+
const VALID_EVIDENCE = new Set([
|
|
7
|
+
"changed-files",
|
|
8
|
+
"tests-added",
|
|
9
|
+
"commands-run",
|
|
10
|
+
"validation-output",
|
|
11
|
+
"residual-risks",
|
|
12
|
+
"no-staged-files",
|
|
13
|
+
"diff-summary",
|
|
14
|
+
"review-findings",
|
|
15
|
+
"manual-notes",
|
|
16
|
+
]);
|
|
17
|
+
const ACCEPTANCE_KEYS = new Set(Object.keys(AcceptanceOverride.properties));
|
|
18
|
+
const REMOVED_ACCEPTANCE_KEYS = new Set(["level", "finalization", "reason", "review"]);
|
|
19
|
+
const ACCEPTANCE_VALIDATOR = Compile(AcceptanceOverride);
|
|
20
|
+
const EVIDENCE_REPORT_FIELDS = {
|
|
21
|
+
"changed-files": "changedFiles: array of changed file paths",
|
|
22
|
+
"tests-added": "testsAddedOrUpdated: array of test files, suites, or cases added/updated",
|
|
23
|
+
"commands-run": "commandsRun: array of commands with result passed/failed/not-run and a short summary",
|
|
24
|
+
"validation-output": "validationOutput: array of relevant validation output summaries",
|
|
25
|
+
"residual-risks": "residualRisks: array of remaining risks or blockers; use [] when none remain",
|
|
26
|
+
"no-staged-files": "noStagedFiles: boolean",
|
|
27
|
+
"diff-summary": "diffSummary: non-empty string summarizing changed behavior and important files",
|
|
28
|
+
"review-findings": "reviewFindings: array of reviewer findings as strings or objects; use [] when no findings remain",
|
|
29
|
+
"manual-notes": "manualNotes: string for manual notes or external evidence",
|
|
30
|
+
};
|
|
31
|
+
export function formatEvidenceReportFieldMapping(evidence) {
|
|
32
|
+
return evidence.map((kind) => `- ${kind} -> ${EVIDENCE_REPORT_FIELDS[kind]}`);
|
|
33
|
+
}
|
|
34
|
+
function hasArrayItems(value) {
|
|
35
|
+
return Array.isArray(value) && value.length > 0;
|
|
36
|
+
}
|
|
37
|
+
function acceptanceErrorPath(pathLabel, instancePath) {
|
|
38
|
+
return instancePath.split("/").slice(1).reduce((result, part) => {
|
|
39
|
+
const decoded = part.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
40
|
+
return /^\d+$/.test(decoded) ? `${result}[${decoded}]` : `${result}.${decoded}`;
|
|
41
|
+
}, pathLabel);
|
|
42
|
+
}
|
|
43
|
+
export function validateAcceptanceInput(input, pathLabel = "acceptance") {
|
|
44
|
+
if (input === undefined)
|
|
45
|
+
return [];
|
|
46
|
+
if (input === false || typeof input === "string") {
|
|
47
|
+
return [`${pathLabel} must be an object. Public acceptance levels and false disables are no longer supported.`];
|
|
48
|
+
}
|
|
49
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
50
|
+
return [`${pathLabel} must be an object.`];
|
|
51
|
+
const value = input;
|
|
52
|
+
const errors = [];
|
|
53
|
+
if (Object.hasOwn(value, "level"))
|
|
54
|
+
errors.push(`${pathLabel}.level is no longer supported; configure criteria, evidence, and verify directly.`);
|
|
55
|
+
if (Object.hasOwn(value, "review"))
|
|
56
|
+
errors.push(`${pathLabel}.review is not supported; launch a separate parent-controlled reviewer after the worker completes.`);
|
|
57
|
+
if (Object.hasOwn(value, "finalization"))
|
|
58
|
+
errors.push(`${pathLabel}.finalization is not supported; acceptance contracts always run the self-review loop.`);
|
|
59
|
+
if (Object.hasOwn(value, "reason"))
|
|
60
|
+
errors.push(`${pathLabel}.reason is not supported because acceptance is disabled by omitting the field.`);
|
|
61
|
+
for (const key of Object.keys(value)) {
|
|
62
|
+
if (!ACCEPTANCE_KEYS.has(key) && !REMOVED_ACCEPTANCE_KEYS.has(key))
|
|
63
|
+
errors.push(`${pathLabel}.${key} is not supported.`);
|
|
64
|
+
}
|
|
65
|
+
const structuralValue = Object.fromEntries(Object.entries(value).filter(([key]) => ACCEPTANCE_KEYS.has(key)));
|
|
66
|
+
const schemaErrors = [...ACCEPTANCE_VALIDATOR.Errors(structuralValue)];
|
|
67
|
+
for (const error of schemaErrors) {
|
|
68
|
+
const criterionMatch = error.instancePath.match(/^\/criteria\/(\d+)$/);
|
|
69
|
+
if (criterionMatch) {
|
|
70
|
+
const criterion = Array.isArray(value.criteria) ? value.criteria[Number(criterionMatch[1])] : undefined;
|
|
71
|
+
if (criterion && typeof criterion === "object" && !Array.isArray(criterion)) {
|
|
72
|
+
if (error.keyword === "anyOf" || error.schemaPath.includes("/anyOf/0"))
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
else if (typeof criterion === "string") {
|
|
76
|
+
if (error.keyword === "anyOf" || error.schemaPath.includes("/anyOf/1"))
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
else if (error.keyword !== "anyOf") {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (error.keyword === "minLength")
|
|
84
|
+
continue;
|
|
85
|
+
const errorPath = acceptanceErrorPath(pathLabel, error.instancePath);
|
|
86
|
+
if (error.keyword === "anyOf" && criterionMatch) {
|
|
87
|
+
errors.push(`${errorPath} must be a string or object.`);
|
|
88
|
+
}
|
|
89
|
+
else if (error.keyword === "required") {
|
|
90
|
+
for (const property of error.params.requiredProperties)
|
|
91
|
+
errors.push(`${errorPath}.${property} is required.`);
|
|
92
|
+
}
|
|
93
|
+
else if (error.keyword === "enum" && error.instancePath.includes("/evidence/")) {
|
|
94
|
+
errors.push(`${errorPath} is not a supported evidence kind.`);
|
|
95
|
+
}
|
|
96
|
+
else if (error.keyword === "enum" && error.instancePath.endsWith("/severity")) {
|
|
97
|
+
errors.push(`${errorPath} must be required or recommended.`);
|
|
98
|
+
}
|
|
99
|
+
else if (error.instancePath.endsWith("/timeoutMs")) {
|
|
100
|
+
errors.push(`${errorPath} must be a positive integer.`);
|
|
101
|
+
}
|
|
102
|
+
else if (error.instancePath === "/maxFinalizationTurns") {
|
|
103
|
+
errors.push(`${errorPath} must be an integer from 1 to ${MAX_FINALIZATION_TURNS}.`);
|
|
104
|
+
}
|
|
105
|
+
else if (error.keyword === "additionalProperties") {
|
|
106
|
+
for (const property of error.params.additionalProperties) {
|
|
107
|
+
errors.push(error.instancePath.endsWith("/env") ? `${errorPath}.${property} must be a string.` : `${errorPath}.${property} is not supported.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (error.keyword === "type") {
|
|
111
|
+
const expected = String(error.params.type);
|
|
112
|
+
const suffix = error.instancePath.match(/^\/stopRules\/\d+$/) ? "a non-empty string" : expected === "array" || expected === "object" ? `an ${expected}` : `a ${expected}`;
|
|
113
|
+
errors.push(`${errorPath} must be ${suffix}.`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
errors.push(`${errorPath} ${error.message}.`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const [index, criterion] of Array.isArray(value.criteria) ? value.criteria.entries() : []) {
|
|
120
|
+
if (typeof criterion === "string" && !criterion.trim())
|
|
121
|
+
errors.push(`${pathLabel}.criteria[${index}] must not be empty.`);
|
|
122
|
+
if (criterion && typeof criterion === "object" && !Array.isArray(criterion)) {
|
|
123
|
+
const item = criterion;
|
|
124
|
+
if (typeof item.id === "string" && !item.id.trim())
|
|
125
|
+
errors.push(`${pathLabel}.criteria[${index}].id is required.`);
|
|
126
|
+
if (typeof item.must === "string" && !item.must.trim())
|
|
127
|
+
errors.push(`${pathLabel}.criteria[${index}].must is required.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const [index, command] of Array.isArray(value.verify) ? value.verify.entries() : []) {
|
|
131
|
+
if (!command || typeof command !== "object" || Array.isArray(command))
|
|
132
|
+
continue;
|
|
133
|
+
const item = command;
|
|
134
|
+
if (typeof item.id === "string" && !item.id.trim())
|
|
135
|
+
errors.push(`${pathLabel}.verify[${index}].id is required.`);
|
|
136
|
+
if (typeof item.command === "string" && !item.command.trim())
|
|
137
|
+
errors.push(`${pathLabel}.verify[${index}].command is required.`);
|
|
138
|
+
}
|
|
139
|
+
for (const [index, rule] of Array.isArray(value.stopRules) ? value.stopRules.entries() : []) {
|
|
140
|
+
if (typeof rule === "string" && !rule.trim())
|
|
141
|
+
errors.push(`${pathLabel}.stopRules[${index}] must be a non-empty string.`);
|
|
142
|
+
}
|
|
143
|
+
if (!hasArrayItems(value.criteria) && !hasArrayItems(value.evidence) && !hasArrayItems(value.verify) && !hasArrayItems(value.stopRules)) {
|
|
144
|
+
errors.push(`${pathLabel} must include at least one of criteria, evidence, verify, or stopRules.`);
|
|
145
|
+
}
|
|
146
|
+
return [...new Set(errors)];
|
|
147
|
+
}
|
|
148
|
+
function normalizeCriteria(criteria, evidence) {
|
|
149
|
+
return (criteria ?? []).map((criterion, index) => {
|
|
150
|
+
if (typeof criterion === "string") {
|
|
151
|
+
return { id: `criterion-${index + 1}`, must: criterion, evidence, severity: "required" };
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
id: criterion.id.trim(),
|
|
155
|
+
must: criterion.must,
|
|
156
|
+
evidence: criterion.evidence?.filter((item) => VALID_EVIDENCE.has(item)) ?? evidence,
|
|
157
|
+
severity: criterion.severity ?? "required",
|
|
158
|
+
};
|
|
159
|
+
}).filter((criterion) => criterion.must.trim());
|
|
160
|
+
}
|
|
161
|
+
function deriveAcceptanceLevel(config) {
|
|
162
|
+
if ((config.verify?.length ?? 0) > 0)
|
|
163
|
+
return "verified";
|
|
164
|
+
return "checked";
|
|
165
|
+
}
|
|
166
|
+
export function resolveEffectiveAcceptance(input) {
|
|
167
|
+
if (input.explicit === undefined) {
|
|
168
|
+
return {
|
|
169
|
+
level: "none",
|
|
170
|
+
explicit: false,
|
|
171
|
+
inferredReason: ["acceptance not configured"],
|
|
172
|
+
criteria: [],
|
|
173
|
+
evidence: [],
|
|
174
|
+
verify: [],
|
|
175
|
+
stopRules: [],
|
|
176
|
+
finalization: { mode: "none", maxTurns: 0 },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const validationErrors = validateAcceptanceInput(input.explicit);
|
|
180
|
+
if (validationErrors.length > 0)
|
|
181
|
+
throw new Error(validationErrors.join(" "));
|
|
182
|
+
const explicit = input.explicit;
|
|
183
|
+
const evidence = [...new Set(explicit.evidence ?? [])];
|
|
184
|
+
const criteria = normalizeCriteria(explicit.criteria, evidence);
|
|
185
|
+
const verify = explicit.verify ?? [];
|
|
186
|
+
const stopRules = explicit.stopRules ?? [];
|
|
187
|
+
return {
|
|
188
|
+
level: deriveAcceptanceLevel(explicit),
|
|
189
|
+
explicit: true,
|
|
190
|
+
inferredReason: ["explicit acceptance contract"],
|
|
191
|
+
criteria,
|
|
192
|
+
evidence,
|
|
193
|
+
verify,
|
|
194
|
+
stopRules,
|
|
195
|
+
finalization: { mode: "self-review-loop", maxTurns: explicit.maxFinalizationTurns ?? DEFAULT_FINALIZATION_MAX_TURNS },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export function acceptanceInputFromResolved(acceptance) {
|
|
199
|
+
if (!acceptance?.explicit)
|
|
200
|
+
return undefined;
|
|
201
|
+
const maxTurns = acceptance.finalization?.maxTurns;
|
|
202
|
+
return {
|
|
203
|
+
criteria: acceptance.criteria,
|
|
204
|
+
evidence: acceptance.evidence,
|
|
205
|
+
verify: acceptance.verify,
|
|
206
|
+
stopRules: acceptance.stopRules,
|
|
207
|
+
...(maxTurns !== undefined ? { maxFinalizationTurns: maxTurns } : {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export function shouldRunAcceptanceFinalization(acceptance) {
|
|
211
|
+
return acceptance.explicit && acceptance.finalization.mode === "self-review-loop" && acceptance.finalization.maxTurns > 0;
|
|
212
|
+
}
|
|
213
|
+
export function acceptanceSelfReviewConfig(acceptance) {
|
|
214
|
+
if (acceptance.verify.length === 0)
|
|
215
|
+
return acceptance;
|
|
216
|
+
return {
|
|
217
|
+
...acceptance,
|
|
218
|
+
level: "checked",
|
|
219
|
+
verify: [],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export function formatAcceptancePrompt(acceptance, nativeReport = false) {
|
|
223
|
+
if (acceptance.level === "none")
|
|
224
|
+
return "";
|
|
225
|
+
const evidence = [...new Set([...acceptance.evidence, ...acceptance.criteria.flatMap((criterion) => criterion.evidence)])];
|
|
226
|
+
const lines = [
|
|
227
|
+
"",
|
|
228
|
+
"## Acceptance Contract",
|
|
229
|
+
nativeReport
|
|
230
|
+
? "Completion is not accepted from prose alone. Finish the initial response with a sole structured_output tool call containing {value:{answer,report}}. answer is the complete standalone visible answer; report is a typed object, never JSON embedded in a string."
|
|
231
|
+
: "Completion is not accepted from prose alone. End the initial response with a structured acceptance report.",
|
|
232
|
+
"After the initial response, the runtime will continue this same session for a bounded self-review/repair loop before accepting the run.",
|
|
233
|
+
"For an observed human-only boundary (such as Touch ID or an unavailable MFA code), report the affected criterion as blocked with concrete evidence and a nonempty humanAction stating the exact user action. Retain completed evidence. This leaves acceptance incomplete and stops finalization/verification until explicit Continue. Do not use blocked for ordinary errors, missing unrelated evidence, or work you can fix.",
|
|
234
|
+
"",
|
|
235
|
+
"Criteria:",
|
|
236
|
+
...(acceptance.criteria.length ? acceptance.criteria.map((criterion) => `- ${criterion.id}: ${criterion.must}${criterion.evidence.length ? ` (evidence: ${criterion.evidence.join(", ")})` : ""}`) : ["- No explicit criteria were configured; satisfy the requested task and the required evidence/checks below."]),
|
|
237
|
+
"",
|
|
238
|
+
`Required evidence: ${evidence.join(", ") || "none explicitly requested"}`,
|
|
239
|
+
];
|
|
240
|
+
if (evidence.length > 0) {
|
|
241
|
+
lines.push("", `Structured evidence must be present in the ${nativeReport ? "typed report object" : "`acceptance-report` JSON fields"}. Markdown sections in your visible answer do not satisfy required evidence by themselves. If you already described evidence in prose, copy or summarize it into the matching JSON field.`, "Evidence field mapping:", ...formatEvidenceReportFieldMapping(evidence));
|
|
242
|
+
}
|
|
243
|
+
if (acceptance.verify.length > 0) {
|
|
244
|
+
lines.push("", "Runtime verification commands configured by parent:");
|
|
245
|
+
for (const command of acceptance.verify)
|
|
246
|
+
lines.push(`- ${command.id}: ${command.command}`);
|
|
247
|
+
}
|
|
248
|
+
if (acceptance.stopRules.length > 0) {
|
|
249
|
+
lines.push("", "Stop rules:", ...acceptance.stopRules.map((rule) => `- ${rule}`));
|
|
250
|
+
}
|
|
251
|
+
lines.push("", nativeReport ? "Submit this shape through structured_output; do not emit a fenced acceptance-report:" : "Finish with a fenced JSON block tagged `acceptance-report` in this shape:", formatAcceptanceReportExample(nativeReport));
|
|
252
|
+
return lines.join("\n");
|
|
253
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { addAbortListener } from "node:events";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { attachChildProcessLifecycle } from "../../shared/post-exit-stdio-guard.js";
|
|
5
|
+
import { parseAcceptanceReport, validateAcceptanceReportShape } from "./acceptance-reports.js";
|
|
6
|
+
const LEVEL_RANK = {
|
|
7
|
+
none: 0,
|
|
8
|
+
attested: 1,
|
|
9
|
+
checked: 2,
|
|
10
|
+
verified: 3,
|
|
11
|
+
};
|
|
12
|
+
function isStringArray(value) {
|
|
13
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
14
|
+
}
|
|
15
|
+
function checkCriteriaSatisfied(criteria, report) {
|
|
16
|
+
const reports = new Map((report.criteriaSatisfied ?? []).filter((item) => item.id).map((item) => [item.id, item]));
|
|
17
|
+
const checks = [];
|
|
18
|
+
for (const criterion of criteria.filter((item) => item.severity !== "recommended")) {
|
|
19
|
+
const item = reports.get(criterion.id);
|
|
20
|
+
if (!item)
|
|
21
|
+
checks.push({ id: `criterion:${criterion.id}`, status: "failed", message: `Required criterion '${criterion.id}' was not reported.` });
|
|
22
|
+
else if (item.status === "blocked")
|
|
23
|
+
checks.push({ id: `criterion:${criterion.id}`, status: "blocked", message: `Needs human action: ${item.humanAction}. Evidence: ${item.evidence}` });
|
|
24
|
+
else if (item.status !== "satisfied")
|
|
25
|
+
checks.push({ id: `criterion:${criterion.id}`, status: "failed", message: `Required criterion '${criterion.id}' was reported as ${item.status}.` });
|
|
26
|
+
else
|
|
27
|
+
checks.push({ id: `criterion:${criterion.id}`, status: "passed", message: `Required criterion '${criterion.id}' satisfied.` });
|
|
28
|
+
for (const kind of criterion.evidence ?? []) {
|
|
29
|
+
const present = reportEvidencePresent(report, kind);
|
|
30
|
+
checks.push({
|
|
31
|
+
id: `criterion:${criterion.id}:evidence:${kind}`,
|
|
32
|
+
status: present ? "passed" : item?.status === "blocked" ? "blocked" : "failed",
|
|
33
|
+
message: present ? `${kind} evidence present for '${criterion.id}'.` : `${kind} evidence missing for required criterion '${criterion.id}'.`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return checks;
|
|
38
|
+
}
|
|
39
|
+
function reportEvidencePresent(report, kind) {
|
|
40
|
+
switch (kind) {
|
|
41
|
+
case "changed-files": return isStringArray(report.changedFiles) && report.changedFiles.length > 0;
|
|
42
|
+
case "tests-added": return isStringArray(report.testsAddedOrUpdated) && report.testsAddedOrUpdated.length > 0;
|
|
43
|
+
case "commands-run": return Array.isArray(report.commandsRun) && report.commandsRun.some((command) => command.result !== "not-run");
|
|
44
|
+
case "validation-output": return isStringArray(report.validationOutput) && report.validationOutput.length > 0;
|
|
45
|
+
case "residual-risks": return isStringArray(report.residualRisks);
|
|
46
|
+
case "no-staged-files": return report.noStagedFiles === true;
|
|
47
|
+
case "diff-summary": return typeof report.diffSummary === "string" && report.diffSummary.trim().length > 0;
|
|
48
|
+
case "review-findings": return Array.isArray(report.reviewFindings);
|
|
49
|
+
case "manual-notes": return Boolean((report.manualNotes ?? report.notes)?.trim());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function checkNoStagedFiles(cwd) {
|
|
53
|
+
const result = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8" });
|
|
54
|
+
if (result.status !== 0) {
|
|
55
|
+
return { id: "no-staged-files", status: "not-applicable", message: "git status unavailable; no staged-files check skipped" };
|
|
56
|
+
}
|
|
57
|
+
const staged = result.stdout.split(/\r?\n/).filter((line) => line.length >= 2 && line[0] !== " " && line[0] !== "?");
|
|
58
|
+
return staged.length === 0
|
|
59
|
+
? { id: "no-staged-files", status: "passed", message: "No staged files detected." }
|
|
60
|
+
: { id: "no-staged-files", status: "failed", message: `Staged files present: ${staged.join(", ")}` };
|
|
61
|
+
}
|
|
62
|
+
function runStructuralChecks(acceptance, report) {
|
|
63
|
+
const checks = [];
|
|
64
|
+
checks.push(...checkCriteriaSatisfied(acceptance.criteria, report));
|
|
65
|
+
if (!acceptance.criteria.length)
|
|
66
|
+
for (const item of report.criteriaSatisfied ?? []) {
|
|
67
|
+
if (item.status === "blocked")
|
|
68
|
+
checks.push({ id: `criterion:${item.id ?? "human-action"}`, status: "blocked", message: `Needs human action: ${item.humanAction}. Evidence: ${item.evidence}` });
|
|
69
|
+
}
|
|
70
|
+
const blockedCriteria = new Set((report.criteriaSatisfied ?? []).filter((criterion) => criterion.status === "blocked").map((criterion) => criterion.id));
|
|
71
|
+
const blockedEvidence = new Set(acceptance.criteria.filter((criterion) => blockedCriteria.has(criterion.id)).flatMap((criterion) => criterion.evidence));
|
|
72
|
+
for (const kind of acceptance.evidence) {
|
|
73
|
+
const present = reportEvidencePresent(report, kind);
|
|
74
|
+
const deferred = blockedEvidence.has(kind) || (!acceptance.criteria.length && blockedCriteria.size > 0);
|
|
75
|
+
checks.push({
|
|
76
|
+
id: `evidence:${kind}`,
|
|
77
|
+
status: present ? "passed" : deferred ? "blocked" : "failed",
|
|
78
|
+
message: present ? `${kind} evidence present.` : `${kind} evidence missing from child report.`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return checks;
|
|
82
|
+
}
|
|
83
|
+
const MAX_VERIFY_OUTPUT_CHARS = 12_000;
|
|
84
|
+
function appendVerifyOutput(current, chunk) {
|
|
85
|
+
const text = chunk.toString();
|
|
86
|
+
const remaining = Math.max(0, MAX_VERIFY_OUTPUT_CHARS - current.length);
|
|
87
|
+
return { value: current + text.slice(0, remaining), truncated: text.length > remaining };
|
|
88
|
+
}
|
|
89
|
+
function trimOutput(value, truncated = false) {
|
|
90
|
+
const trimmed = value.trim();
|
|
91
|
+
if (!trimmed && !truncated)
|
|
92
|
+
return undefined;
|
|
93
|
+
return truncated || trimmed.length > MAX_VERIFY_OUTPUT_CHARS
|
|
94
|
+
? `${trimmed.slice(0, MAX_VERIFY_OUTPUT_CHARS)}${trimmed ? "\n" : ""}...[truncated]`
|
|
95
|
+
: trimmed;
|
|
96
|
+
}
|
|
97
|
+
function runVerifyCommand(command, defaultCwd, signal) {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const startedAt = Date.now();
|
|
100
|
+
const cwd = command.cwd ? path.resolve(defaultCwd, command.cwd) : defaultCwd;
|
|
101
|
+
let stdout = "";
|
|
102
|
+
let stderr = "";
|
|
103
|
+
let stdoutTruncated = false;
|
|
104
|
+
let stderrTruncated = false;
|
|
105
|
+
let timedOut = false;
|
|
106
|
+
let cancelled = false;
|
|
107
|
+
const child = spawn(command.command, {
|
|
108
|
+
cwd,
|
|
109
|
+
env: { ...process.env, ...(command.env ?? {}) },
|
|
110
|
+
shell: true,
|
|
111
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
112
|
+
detached: true,
|
|
113
|
+
});
|
|
114
|
+
const lifecycle = attachChildProcessLifecycle(child);
|
|
115
|
+
const abortListener = signal && addAbortListener(signal, () => {
|
|
116
|
+
cancelled = true;
|
|
117
|
+
lifecycle.terminate();
|
|
118
|
+
});
|
|
119
|
+
const timeout = setTimeout(() => {
|
|
120
|
+
timedOut = true;
|
|
121
|
+
lifecycle.terminate();
|
|
122
|
+
}, command.timeoutMs ?? 120_000);
|
|
123
|
+
timeout.unref?.();
|
|
124
|
+
child.stdout.on("data", (chunk) => {
|
|
125
|
+
const next = appendVerifyOutput(stdout, chunk);
|
|
126
|
+
stdout = next.value;
|
|
127
|
+
stdoutTruncated ||= next.truncated;
|
|
128
|
+
});
|
|
129
|
+
child.stderr.on("data", (chunk) => {
|
|
130
|
+
const next = appendVerifyOutput(stderr, chunk);
|
|
131
|
+
stderr = next.value;
|
|
132
|
+
stderrTruncated ||= next.truncated;
|
|
133
|
+
});
|
|
134
|
+
const cleanup = () => {
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
abortListener?.[Symbol.dispose]();
|
|
137
|
+
};
|
|
138
|
+
child.on("close", (exitCode) => {
|
|
139
|
+
cleanup();
|
|
140
|
+
const durationMs = Date.now() - startedAt;
|
|
141
|
+
const passed = exitCode === 0 && !timedOut && !cancelled;
|
|
142
|
+
resolve({
|
|
143
|
+
id: command.id,
|
|
144
|
+
command: command.command,
|
|
145
|
+
cwd,
|
|
146
|
+
exitCode,
|
|
147
|
+
status: cancelled ? "failed" : timedOut ? "timed-out" : passed ? "passed" : command.allowFailure ? "allowed-failure" : "failed",
|
|
148
|
+
stdout: trimOutput(stdout, stdoutTruncated),
|
|
149
|
+
stderr: cancelled ? "Verification cancelled." : trimOutput(stderr, stderrTruncated),
|
|
150
|
+
durationMs,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
child.on("error", (error) => {
|
|
154
|
+
cleanup();
|
|
155
|
+
resolve({
|
|
156
|
+
id: command.id,
|
|
157
|
+
command: command.command,
|
|
158
|
+
cwd,
|
|
159
|
+
exitCode: 1,
|
|
160
|
+
status: command.allowFailure ? "allowed-failure" : "failed",
|
|
161
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
162
|
+
durationMs: Date.now() - startedAt,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** Structural evidence only. The run owner must still perform runtime checks before acceptance. */
|
|
168
|
+
export function evaluateAcceptanceReport(input) {
|
|
169
|
+
const acceptance = input.acceptance;
|
|
170
|
+
const ledger = {
|
|
171
|
+
status: acceptance.level === "none" ? "not-required" : "claimed",
|
|
172
|
+
explicit: acceptance.explicit,
|
|
173
|
+
effectiveAcceptance: input.governing ?? acceptance,
|
|
174
|
+
inferredReason: acceptance.inferredReason,
|
|
175
|
+
criteria: acceptance.criteria,
|
|
176
|
+
runtimeChecks: [],
|
|
177
|
+
verifyRuns: [],
|
|
178
|
+
};
|
|
179
|
+
if (acceptance.level === "none")
|
|
180
|
+
return ledger;
|
|
181
|
+
const shapeError = input.report !== undefined ? validateAcceptanceReportShape(input.report) : undefined;
|
|
182
|
+
const parsed = input.report === undefined ? parseAcceptanceReport(input.output)
|
|
183
|
+
: shapeError ? { error: `Acceptance report is invalid: ${shapeError}` } : { report: input.report };
|
|
184
|
+
if (parsed.report) {
|
|
185
|
+
ledger.childReport = parsed.report;
|
|
186
|
+
ledger.status = "attested";
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
ledger.childReportParseError = parsed.error;
|
|
190
|
+
ledger.runtimeChecks.push({ id: "attestation", status: "failed", message: parsed.error ?? "Structured acceptance report missing." });
|
|
191
|
+
ledger.status = "rejected";
|
|
192
|
+
return ledger;
|
|
193
|
+
}
|
|
194
|
+
if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked || parsed.report.criteriaSatisfied?.some((item) => item.status === "blocked")) {
|
|
195
|
+
ledger.runtimeChecks = runStructuralChecks(acceptance, parsed.report);
|
|
196
|
+
if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
197
|
+
ledger.status = "rejected";
|
|
198
|
+
return ledger;
|
|
199
|
+
}
|
|
200
|
+
if (ledger.runtimeChecks.some((check) => check.status === "blocked")) {
|
|
201
|
+
ledger.status = "blocked";
|
|
202
|
+
return ledger;
|
|
203
|
+
}
|
|
204
|
+
ledger.status = "checked";
|
|
205
|
+
}
|
|
206
|
+
return ledger;
|
|
207
|
+
}
|
|
208
|
+
export async function evaluateAcceptance(input) {
|
|
209
|
+
const { acceptance } = input;
|
|
210
|
+
const ledger = evaluateAcceptanceReport(input);
|
|
211
|
+
if (!ledger.childReport)
|
|
212
|
+
return ledger;
|
|
213
|
+
if ((LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked || ledger.childReport.criteriaSatisfied?.some((item) => item.status === "blocked"))
|
|
214
|
+
&& (acceptance.evidence.includes("no-staged-files") || acceptance.criteria.some((criterion) => criterion.evidence?.includes("no-staged-files")))) {
|
|
215
|
+
const check = checkNoStagedFiles(input.cwd);
|
|
216
|
+
ledger.runtimeChecks.push(check);
|
|
217
|
+
if (check.status === "failed")
|
|
218
|
+
ledger.status = "rejected";
|
|
219
|
+
}
|
|
220
|
+
if (ledger.status === "rejected" || ledger.status === "blocked")
|
|
221
|
+
return ledger;
|
|
222
|
+
if (acceptance.verify.length > 0) {
|
|
223
|
+
ledger.verifyRuns = [];
|
|
224
|
+
for (const command of acceptance.verify) {
|
|
225
|
+
if (input.signal?.aborted) {
|
|
226
|
+
ledger.runtimeChecks.push({ id: "cancelled", status: "failed", message: "Acceptance verification cancelled." });
|
|
227
|
+
ledger.status = "rejected";
|
|
228
|
+
return ledger;
|
|
229
|
+
}
|
|
230
|
+
ledger.verifyRuns.push(await runVerifyCommand(command, input.cwd, input.signal));
|
|
231
|
+
}
|
|
232
|
+
if (ledger.verifyRuns.some((run) => run.status === "failed" || run.status === "timed-out")) {
|
|
233
|
+
ledger.status = "rejected";
|
|
234
|
+
return ledger;
|
|
235
|
+
}
|
|
236
|
+
ledger.status = "verified";
|
|
237
|
+
}
|
|
238
|
+
return ledger;
|
|
239
|
+
}
|
|
240
|
+
export function acceptanceHumanAction(ledger) {
|
|
241
|
+
if (ledger?.status !== "blocked")
|
|
242
|
+
return undefined;
|
|
243
|
+
return ledger.childReport?.criteriaSatisfied?.filter((criterion) => criterion.status === "blocked")
|
|
244
|
+
.map((criterion) => `${criterion.id ?? "Criterion"}: ${criterion.humanAction}\nEvidence: ${criterion.evidence}`).join("\n");
|
|
245
|
+
}
|
|
246
|
+
export function acceptanceFailureMessage(ledger) {
|
|
247
|
+
if (ledger.status !== "rejected")
|
|
248
|
+
return undefined;
|
|
249
|
+
const failedCheck = ledger.runtimeChecks.find((check) => check.status === "failed");
|
|
250
|
+
if (failedCheck)
|
|
251
|
+
return `Acceptance rejected: ${failedCheck.message}`;
|
|
252
|
+
const failedVerify = ledger.verifyRuns.find((run) => run.status === "failed" || run.status === "timed-out");
|
|
253
|
+
if (failedVerify)
|
|
254
|
+
return `Acceptance verification '${failedVerify.id}' ${failedVerify.status}.`;
|
|
255
|
+
return "Acceptance rejected.";
|
|
256
|
+
}
|