pi-workflow-engine 0.12.0 → 1.0.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.
- package/.pi/extensions/pi-workflow-engine/index.ts +106 -391
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +20 -2
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +5 -11
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +93 -100
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +18 -18
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +37 -86
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +39 -98
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +37 -80
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
- package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
- package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +1 -12
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
- package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +128 -142
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +119 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +41 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -116
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +9 -14
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
- package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
- package/.pi/extensions/pi-workflow-engine/src/types.ts +9 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -126
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +21 -32
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +178 -188
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
- package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +55 -110
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +101 -203
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +51 -187
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +33 -109
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +36 -106
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
- package/USAGE.md +90 -17
- package/package.json +1 -2
- package/skills/workflow-code-review-actions/SKILL.md +12 -23
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -49
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
- package/.pi/settings.json +0 -3
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
import { collectAdvisoryStage, type AdvisoryStageCoverage } from "./advisory-evidence.ts";
|
|
3
|
+
import { DEFAULT_ADVISORY_TOOLS, DEFAULT_ADVISORY_TOOL_HINTS, type AdvisoryVerified } from "./workflow-advisory-utils.ts";
|
|
4
|
+
import type { WorkflowApi } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
const ChallengeSchema = Type.Object({
|
|
7
|
+
outcome: Type.Union([Type.Literal("counterexample"), Type.Literal("alternative-explanation"), Type.Literal("supports-original"), Type.Literal("no-counterexample")]),
|
|
8
|
+
evidence: Type.Array(Type.String()),
|
|
9
|
+
experiment: Type.String({ description: "Smallest distinguishing experiment, with observed result if actually run." }),
|
|
10
|
+
});
|
|
11
|
+
const AdjudicationSchema = Type.Object({
|
|
12
|
+
outcome: Type.Union([Type.Literal("upheld"), Type.Literal("refuted"), Type.Literal("unresolved")]),
|
|
13
|
+
evidence: Type.Array(Type.String()),
|
|
14
|
+
reason: Type.String(),
|
|
15
|
+
});
|
|
16
|
+
export type ChallengeRecord =
|
|
17
|
+
| { status: "complete"; challenge: Static<typeof ChallengeSchema>; adjudication: Static<typeof AdjudicationSchema> }
|
|
18
|
+
| { status: "failed"; challenge?: Static<typeof ChallengeSchema> };
|
|
19
|
+
export interface AdvisoryChallengeOptions {
|
|
20
|
+
maxChallenges: number;
|
|
21
|
+
shouldChallenge?: (finding: AdvisoryVerified) => boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Opt in with --challenge or --challenge=N (hard bound of 10). */
|
|
25
|
+
export function parseChallengeArgs(args: string): { args: string; options: AdvisoryChallengeOptions } {
|
|
26
|
+
let maxChallenges = 0;
|
|
27
|
+
const remaining = args.replace(/(?:^|\s)--challenge(?:=([^\s]*))?(?=\s|$)/g, (_match, limit: string | undefined) => {
|
|
28
|
+
if (limit !== undefined && !/^\d+$/.test(limit)) {
|
|
29
|
+
throw new Error(`Invalid --challenge value "${limit}". Use --challenge or --challenge=N with a non-negative integer; zero disables challenges.`);
|
|
30
|
+
}
|
|
31
|
+
maxChallenges = Math.min(10, limit === undefined ? 3 : Number(limit));
|
|
32
|
+
return " ";
|
|
33
|
+
});
|
|
34
|
+
return { args: remaining.trim(), options: { maxChallenges } };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function needsChallenge(finding: AdvisoryVerified): boolean {
|
|
38
|
+
return finding.verdict === "PLAUSIBLE" || finding.verdict === "NOT_SUBSTANTIATED" || finding.evidence.length === 0 ||
|
|
39
|
+
/\b(security|concurren\w*|race|persist\w*|cancel\w*|retr(?:y|ies)|data loss|corrupt\w*|high impact)\b/i.test(`${finding.category} ${finding.summary} ${finding.impact}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A bounded recipe using existing agent/parallel calls, not a new runtime primitive. */
|
|
43
|
+
export async function challengeFindings<T extends AdvisoryVerified>(
|
|
44
|
+
api: Pick<WorkflowApi, "agent" | "parallel">,
|
|
45
|
+
findings: T[],
|
|
46
|
+
context: string,
|
|
47
|
+
options: AdvisoryChallengeOptions,
|
|
48
|
+
coverage: AdvisoryStageCoverage[],
|
|
49
|
+
): Promise<T[]> {
|
|
50
|
+
const limit = Number.isFinite(options.maxChallenges) ? Math.max(0, Math.min(10, Math.trunc(options.maxChallenges))) : 0;
|
|
51
|
+
if (limit === 0) return findings;
|
|
52
|
+
const selected = findings.filter((finding) => finding.verdict !== "REFUTED" && (options.shouldChallenge ?? needsChallenge)(finding)).slice(0, limit);
|
|
53
|
+
const replacements = new Map<string, T>();
|
|
54
|
+
await collectAdvisoryStage(api, "Challenge", selected.map((finding) => ({ id: finding.candidateId, run: async () => {
|
|
55
|
+
// Preserve the candidate as unresolved if either independent stage fails.
|
|
56
|
+
replacements.set(finding.candidateId, { ...finding, verdict: "NOT_SUBSTANTIATED", challenge: { status: "failed" } });
|
|
57
|
+
const challenge = await api.agent(
|
|
58
|
+
`Assume this finding is a false positive. Try to DISPROVE it. Find the strongest concrete counterexample or alternative root cause. Inspect callers, invariants, tests and control flow. For a repair, seek an input, race or error path that still fails. State the smallest experiment distinguishing explanations. Do not edit files or claim tests you did not run. No counterexample found is not proof.\n\nExact review context:\n${context}\n\nCandidate and verifier evidence:\n${JSON.stringify(finding)}`,
|
|
59
|
+
{ label: `challenge:${finding.candidateId}`, phase: "Challenge", profile: "medium", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, schema: ChallengeSchema },
|
|
60
|
+
);
|
|
61
|
+
replacements.set(finding.candidateId, { ...finding, verdict: "NOT_SUBSTANTIATED", challenge: { status: "failed", challenge } });
|
|
62
|
+
const adjudication = await api.agent(
|
|
63
|
+
`Adjudicate the original finding, independent verifier evidence and falsification attempt below. Preserve unresolved conflict; do not force consensus. A missing counterexample alone cannot upgrade a plausible claim. Cite concrete evidence and observed test results; never invent experiments.\nContext:\n${context}\nOriginal and verifier:\n${JSON.stringify(finding)}\nChallenger:\n${JSON.stringify(challenge)}`,
|
|
64
|
+
{ label: `adjudicate:${finding.candidateId}`, phase: "Challenge", profile: "medium", tools: [], skills: [], schema: AdjudicationSchema },
|
|
65
|
+
);
|
|
66
|
+
replacements.set(finding.candidateId, {
|
|
67
|
+
...finding,
|
|
68
|
+
verdict: adjudication.outcome === "refuted" ? "REFUTED" : adjudication.outcome === "unresolved" ? "NOT_SUBSTANTIATED" : finding.verdict,
|
|
69
|
+
evidence: [...finding.evidence, ...challenge.evidence, ...(challenge.experiment ? [challenge.experiment] : []), ...adjudication.evidence, adjudication.reason],
|
|
70
|
+
challenge: { status: "complete", challenge, adjudication },
|
|
71
|
+
});
|
|
72
|
+
return finding.candidateId;
|
|
73
|
+
} })), coverage);
|
|
74
|
+
return findings.map((finding) => replacements.get(finding.candidateId) ?? finding);
|
|
75
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
|
+
import { AdvisorySeveritySchema, type IdentifiedAdvisoryCandidate, type AdvisoryCandidate, type AdvisoryReport } from "./advisory-schema.ts";
|
|
4
|
+
import type { WorkflowApi } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export type AdvisoryStageCoverage = NonNullable<AdvisoryReport["coverage"]>[number];
|
|
7
|
+
|
|
8
|
+
/** Settled results are required here: a missing branch is not a negative finding. */
|
|
9
|
+
export async function collectAdvisoryStage<T>(
|
|
10
|
+
api: Pick<WorkflowApi, "parallel">,
|
|
11
|
+
stage: string,
|
|
12
|
+
branches: { id: string; candidate?: AdvisoryCandidate; run(): Promise<T> }[],
|
|
13
|
+
coverage: AdvisoryStageCoverage[],
|
|
14
|
+
): Promise<T[]> {
|
|
15
|
+
const results = await api.parallel(branches.map((branch) => branch.run), { settled: true });
|
|
16
|
+
const failures = results.flatMap((result, index) => result.ok ? [] : [{ branch: branches[index]!.id, reason: result.error.message, ...(branches[index]!.candidate ? { candidate: branches[index]!.candidate } : {}) }]);
|
|
17
|
+
coverage.push({ stage, expected: branches.length, completed: results.length - failures.length, failed: failures.length, failures });
|
|
18
|
+
return results.flatMap((result) => result.ok ? [result.value] : []);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function identifyCandidates(candidates: readonly AdvisoryCandidate[], lens: string): IdentifiedAdvisoryCandidate[] {
|
|
22
|
+
return candidates.map((candidate, index) => {
|
|
23
|
+
const candidateId = createHash("sha256").update(JSON.stringify([lens, index, candidate])).digest("hex").slice(0, 20);
|
|
24
|
+
// Identities are assigned by the workflow, never accepted from a finder.
|
|
25
|
+
return { ...candidate, candidateId, sourceCandidateIds: [candidateId] };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizedSummary(summary: string): string {
|
|
30
|
+
return summary.toLowerCase().replace(/\s+/g, " ").trim().replace(/[.!?]+$/, "");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Repo-relative readings of a reported path, most literal first: without a stray `./`, then also without a git `a/`/`b/` prefix. */
|
|
34
|
+
export function repoPathCandidates(path: string): [literal: string, unprefixed: string] {
|
|
35
|
+
const literal = path.replace(/^\.\//, "");
|
|
36
|
+
return [literal, literal.replace(/^[ab]\//, "")];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function candidateDedupKey(candidate: AdvisoryCandidate): string {
|
|
40
|
+
const location = candidate.locations[0];
|
|
41
|
+
return JSON.stringify([candidate.category, location && repoPathCandidates(location.file)[1],
|
|
42
|
+
location?.symbol ?? location?.line ?? null, normalizedSummary(candidate.summary)]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Collapse only equivalent claims at the same anchor; keep every discovery ID and location. */
|
|
46
|
+
export function dedupeCandidates<T extends IdentifiedAdvisoryCandidate>(candidates: readonly T[]): T[] {
|
|
47
|
+
const seen = new Map<string, T>();
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
const key = candidateDedupKey(candidate);
|
|
50
|
+
const previous = seen.get(key);
|
|
51
|
+
if (!previous) seen.set(key, { ...candidate });
|
|
52
|
+
else {
|
|
53
|
+
previous.sourceCandidateIds = [...new Set([...previous.sourceCandidateIds, ...candidate.sourceCandidateIds])];
|
|
54
|
+
previous.locations = uniqueLocations([...previous.locations, ...candidate.locations]);
|
|
55
|
+
previous.impact = [...new Set([previous.impact, candidate.impact])].join("\n");
|
|
56
|
+
previous.discoveryEvidence = [...new Set([...(previous.discoveryEvidence ?? []), ...(candidate.discoveryEvidence ?? [])])];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return [...seen.values()];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function uniqueLocations(locations: AdvisoryCandidate["locations"]): AdvisoryCandidate["locations"] {
|
|
63
|
+
return [...new Map(locations.map((location) => [JSON.stringify(location), location])).values()];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Synthesis selects evidence records. It has no fields with which to replace their evidence.
|
|
67
|
+
export const AdvisorySynthesisSchema = Type.Object({
|
|
68
|
+
summary: Type.String(),
|
|
69
|
+
findings: Type.Array(Type.Object({
|
|
70
|
+
sourceCandidateIds: Type.Array(Type.String(), { minItems: 1 }),
|
|
71
|
+
severity: AdvisorySeveritySchema,
|
|
72
|
+
recommendation: Type.String(),
|
|
73
|
+
})),
|
|
74
|
+
nextSteps: Type.Array(Type.String()),
|
|
75
|
+
});
|
|
76
|
+
export type AdvisorySynthesis = Static<typeof AdvisorySynthesisSchema>;
|
|
77
|
+
export const SYNTHESIS_ID_INSTRUCTIONS = "Select or merge findings only by sourceCandidateIds shown below. Return severity and an advisory recommendation per selection. Never select an ID absent from these verified records. ";
|
|
78
|
+
|
|
79
|
+
export function withAdvisoryCoverage<T extends AdvisoryReport>(report: T, coverage: AdvisoryStageCoverage[]) {
|
|
80
|
+
const incomplete = coverage.some((stage) => stage.failed > 0);
|
|
81
|
+
const gaps = coverage.flatMap((stage) => stage.failures.map((failure) => `${stage.stage}/${failure.branch}: ${failure.reason}`));
|
|
82
|
+
return {
|
|
83
|
+
...report,
|
|
84
|
+
status: incomplete ? "incomplete" as const : "complete" as const,
|
|
85
|
+
summary: incomplete ? `Incomplete review: ${gaps.length} branch(es) failed. ${report.findings.length} finding(s) available; no clean conclusion is possible.` : report.summary,
|
|
86
|
+
nextSteps: incomplete ? ["Inspect failed branches and rerun missing work.", ...(report.findings.length > 0 ? report.nextSteps : [])] : report.nextSteps,
|
|
87
|
+
coverage,
|
|
88
|
+
gaps,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -9,7 +9,7 @@ export const AdvisoryConfidenceSchema = Type.Union([Type.Literal("low"), Type.Li
|
|
|
9
9
|
description: "Confidence that the finding is actionable.",
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
export const AdvisoryVerifierVerdictSchema = Type.Union([Type.Literal("CONFIRMED"), Type.Literal("PLAUSIBLE"), Type.Literal("REFUTED")], {
|
|
12
|
+
export const AdvisoryVerifierVerdictSchema = Type.Union([Type.Literal("CONFIRMED"), Type.Literal("PLAUSIBLE"), Type.Literal("REFUTED"), Type.Literal("NOT_SUBSTANTIATED")], {
|
|
13
13
|
description: "Verifier judgment for a candidate finding.",
|
|
14
14
|
});
|
|
15
15
|
|
|
@@ -20,6 +20,8 @@ export const AdvisoryLocationSchema = Type.Object({
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
export const AdvisoryCandidateSchema = Type.Object({
|
|
23
|
+
discoveryEvidence: Type.Optional(Type.Array(Type.String())),
|
|
24
|
+
reviewAnchor: Type.Optional(AdvisoryLocationSchema),
|
|
23
25
|
summary: Type.String({ description: "One-line candidate finding or hypothesis." }),
|
|
24
26
|
category: Type.String({ description: "Workflow-specific category such as bug, duplication, root-cause, or io." }),
|
|
25
27
|
locations: Type.Array(AdvisoryLocationSchema, { description: "Relevant code or configuration locations." }),
|
|
@@ -34,10 +36,12 @@ export const AdvisoryCandidatesSchema = Type.Object({
|
|
|
34
36
|
export const AdvisoryVerdictSchema = Type.Object({
|
|
35
37
|
verdict: AdvisoryVerifierVerdictSchema,
|
|
36
38
|
evidence: Type.Array(Type.String({ description: "Quoted or cited evidence supporting the verdict." })),
|
|
37
|
-
confidence: Type.Optional(AdvisoryConfidenceSchema),
|
|
38
39
|
});
|
|
39
40
|
|
|
40
41
|
export const AdvisoryFindingSchema = Type.Object({
|
|
42
|
+
verdict: Type.Optional(AdvisoryVerifierVerdictSchema),
|
|
43
|
+
sourceCandidateIds: Type.Optional(Type.Array(Type.String())),
|
|
44
|
+
reviewAnchor: Type.Optional(AdvisoryLocationSchema),
|
|
41
45
|
summary: Type.String({ description: "One-line final finding." }),
|
|
42
46
|
category: Type.String({ description: "Workflow-specific category for grouping and display." }),
|
|
43
47
|
severity: AdvisorySeveritySchema,
|
|
@@ -49,6 +53,12 @@ export const AdvisoryFindingSchema = Type.Object({
|
|
|
49
53
|
});
|
|
50
54
|
|
|
51
55
|
export const AdvisoryReportSchema = Type.Object({
|
|
56
|
+
status: Type.Optional(Type.Union([Type.Literal("complete"), Type.Literal("incomplete")])),
|
|
57
|
+
gaps: Type.Optional(Type.Array(Type.String())),
|
|
58
|
+
coverage: Type.Optional(Type.Array(Type.Object({
|
|
59
|
+
stage: Type.String(), expected: Type.Number(), completed: Type.Number(), failed: Type.Number(),
|
|
60
|
+
failures: Type.Array(Type.Object({ branch: Type.String(), reason: Type.String(), candidate: Type.Optional(AdvisoryCandidateSchema) })),
|
|
61
|
+
}))),
|
|
52
62
|
summary: Type.String({ description: "Short overall advisory summary." }),
|
|
53
63
|
findings: Type.Array(AdvisoryFindingSchema, { description: "Verified and ranked advisory findings." }),
|
|
54
64
|
nextSteps: Type.Array(Type.String({ description: "Concrete follow-up commands, inspections, or decisions." })),
|
|
@@ -61,6 +71,7 @@ export const AdvisoryReportWithStatsSchema = Type.Object({
|
|
|
61
71
|
|
|
62
72
|
export type AdvisoryLocation = Static<typeof AdvisoryLocationSchema>;
|
|
63
73
|
export type AdvisoryCandidate = Static<typeof AdvisoryCandidateSchema>;
|
|
74
|
+
export type IdentifiedAdvisoryCandidate = AdvisoryCandidate & { candidateId: string; sourceCandidateIds: string[] };
|
|
64
75
|
export type AdvisoryVerdict = Static<typeof AdvisoryVerdictSchema>;
|
|
65
76
|
export type AdvisoryFinding = Static<typeof AdvisoryFindingSchema>;
|
|
66
77
|
export type AdvisoryReport = Static<typeof AdvisoryReportSchema>;
|
|
@@ -69,3 +80,10 @@ export type AdvisoryReportWithStats = Static<typeof AdvisoryReportWithStatsSchem
|
|
|
69
80
|
export function isAdvisoryReport(value: unknown): value is AdvisoryReportWithStats {
|
|
70
81
|
return Value.Check(AdvisoryReportWithStatsSchema, value);
|
|
71
82
|
}
|
|
83
|
+
|
|
84
|
+
/** `file:line (symbol)`, omitting whichever of line and symbol is unknown. */
|
|
85
|
+
export function formatAdvisoryLocation(location: AdvisoryLocation): string {
|
|
86
|
+
const line = location.line != null ? `:${location.line}` : "";
|
|
87
|
+
const symbol = location.symbol ? ` (${location.symbol})` : "";
|
|
88
|
+
return `${location.file}${line}${symbol}`;
|
|
89
|
+
}
|
|
@@ -2,28 +2,23 @@ import type { Api, Model } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import { assertWorkflowBudgetAvailable } from "./budget.ts";
|
|
3
3
|
import {
|
|
4
4
|
type AgentExecutionOptions,
|
|
5
|
-
type AgentRunTags,
|
|
6
5
|
type RunContext,
|
|
7
6
|
} from "./agent-runner-types.ts";
|
|
8
7
|
import {
|
|
9
8
|
captureReplayIdentity,
|
|
10
9
|
captureIsolatedRepositoryAfterSetup,
|
|
11
10
|
captureSharedRepositoryBeforeSetup,
|
|
12
|
-
isReplayEnabled,
|
|
13
11
|
lookupReplayResult,
|
|
14
|
-
|
|
12
|
+
validateMutationGuard,
|
|
15
13
|
validateReplayIdentity,
|
|
16
14
|
type AgentAttemptResult,
|
|
17
|
-
type
|
|
15
|
+
type AgentReplayContract,
|
|
18
16
|
type AgentReplayPlan,
|
|
17
|
+
type ArmedReplayPlan,
|
|
19
18
|
} from "./agent-replay.ts";
|
|
20
19
|
import { openAgentSession, promptAgentSession, type AgentSessionHandle } from "./agent-session.ts";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
disposeAgentWorkspace,
|
|
24
|
-
type AgentWorkspace,
|
|
25
|
-
} from "./agent-workspace.ts";
|
|
26
|
-
import type { AgentResumeBaseContext, AgentResumeContext, RepositoryResumeContext } from "./resume-context.ts";
|
|
20
|
+
import { createAgentWorkspace, type AgentWorkspace } from "./agent-workspace.ts";
|
|
21
|
+
import type { AgentResumeBaseContext, RepositoryResumeContext } from "./resume-context.ts";
|
|
27
22
|
import { captureRepositoryMutationGuard } from "./resume-context.ts";
|
|
28
23
|
|
|
29
24
|
/** Execute one fully bracketed workspace/session attempt. Cleanup always precedes settlement. */
|
|
@@ -36,38 +31,33 @@ export async function executeAgentAttempt(input: {
|
|
|
36
31
|
readonly replay: AgentReplayPlan;
|
|
37
32
|
readonly label: string;
|
|
38
33
|
readonly rowId: number;
|
|
39
|
-
readonly
|
|
34
|
+
readonly phase: string;
|
|
40
35
|
readonly admitLiveAgent: () => void;
|
|
41
36
|
}): Promise<AgentAttemptResult> {
|
|
42
|
-
const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId,
|
|
37
|
+
const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId, phase, admitLiveAgent } = input;
|
|
38
|
+
let armed: ArmedReplayPlan | undefined;
|
|
43
39
|
let repositoryBefore: RepositoryResumeContext | undefined;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
evidence = { kind: "isolated", mutationGuard: mutationGuard.fingerprint };
|
|
52
|
-
}
|
|
53
|
-
} else {
|
|
54
|
-
repositoryBefore = await captureSharedRepositoryBeforeSetup(rc, prompt, opts, replay);
|
|
55
|
-
evidence = { kind: "shared" };
|
|
56
|
-
}
|
|
40
|
+
if (replay.kind === "isolated") {
|
|
41
|
+
const mutationGuard = await captureRepositoryMutationGuard(rc.cwd, rc.signal);
|
|
42
|
+
if (mutationGuard.kind === "verified") armed = { ...replay, mutationGuard: mutationGuard.fingerprint };
|
|
43
|
+
else rc.progress.log(`${label}: resume disabled for this call (${mutationGuard.reason})`);
|
|
44
|
+
} else if (replay.kind === "shared") {
|
|
45
|
+
armed = replay;
|
|
46
|
+
repositoryBefore = await captureSharedRepositoryBeforeSetup(rc, prompt, opts, replay);
|
|
57
47
|
}
|
|
58
48
|
let workspace: AgentWorkspace | undefined;
|
|
59
49
|
let handle: AgentSessionHandle | undefined;
|
|
60
50
|
|
|
61
51
|
try {
|
|
62
52
|
workspace = await createAgentWorkspace(rc, opts, label);
|
|
63
|
-
if (
|
|
53
|
+
if (armed?.kind === "isolated") {
|
|
64
54
|
if (workspace.kind !== "isolated") throw new Error("Isolated replay created a shared workspace.");
|
|
65
55
|
repositoryBefore = await captureIsolatedRepositoryAfterSetup(rc, prompt, opts, workspace);
|
|
66
56
|
}
|
|
67
|
-
handle = await openAgentSession({ rc, prompt, opts, cwd: workspace.cwd, model, label
|
|
57
|
+
handle = await openAgentSession({ rc, prompt, opts, cwd: workspace.cwd, model, label });
|
|
68
58
|
|
|
69
|
-
let
|
|
70
|
-
if (
|
|
59
|
+
let contract: AgentReplayContract | undefined;
|
|
60
|
+
if (armed && repositoryBefore) {
|
|
71
61
|
const capture = await captureReplayIdentity({
|
|
72
62
|
rc,
|
|
73
63
|
base: resumeBaseContext,
|
|
@@ -79,23 +69,12 @@ export async function executeAgentAttempt(input: {
|
|
|
79
69
|
if (capture.kind === "unverifiable") {
|
|
80
70
|
rc.progress.log(`${label}: resume disabled for this call (${capture.reason})`);
|
|
81
71
|
} else {
|
|
82
|
-
|
|
83
|
-
const cached = await lookupReplayResult({ rc, key:
|
|
72
|
+
contract = { ...armed, identity: capture.identity };
|
|
73
|
+
const cached = await lookupReplayResult({ rc, key: contract.key, identity: contract.identity, opts, workspace });
|
|
84
74
|
if (cached.hit) {
|
|
85
|
-
const
|
|
86
|
-
rc,
|
|
87
|
-
identity,
|
|
88
|
-
selectedSkills: handle.selectedSkills,
|
|
89
|
-
session: handle.session,
|
|
90
|
-
sessionCwd: workspace.cwd,
|
|
91
|
-
replay,
|
|
92
|
-
workspace,
|
|
93
|
-
});
|
|
94
|
-
const validation = contract.ok
|
|
95
|
-
? await validateReplayEvidence(rc, evidence)
|
|
96
|
-
: contract;
|
|
75
|
+
const validation = await validateReplayContract(rc, contract, handle, workspace);
|
|
97
76
|
if (validation.ok) {
|
|
98
|
-
return { kind: "cache-hit", result: cached.result,
|
|
77
|
+
return { kind: "cache-hit", result: cached.result, contract };
|
|
99
78
|
}
|
|
100
79
|
rc.progress.log(`${label}: cached result invalidated (${validation.reason})`);
|
|
101
80
|
} else if (cached.reason) {
|
|
@@ -113,35 +92,42 @@ export async function executeAgentAttempt(input: {
|
|
|
113
92
|
opts,
|
|
114
93
|
label,
|
|
115
94
|
rowId,
|
|
116
|
-
|
|
95
|
+
phase,
|
|
117
96
|
});
|
|
118
97
|
const result = await workspace.wrapResult(rawResult);
|
|
119
|
-
if (!
|
|
120
|
-
if (!isReplayEnabled(replay) || !evidence) throw new Error("Replay identity produced without complete replay evidence.");
|
|
98
|
+
if (!contract) return { kind: "live-unrecordable", result };
|
|
121
99
|
|
|
122
|
-
const
|
|
123
|
-
rc,
|
|
124
|
-
identity,
|
|
125
|
-
selectedSkills: handle.selectedSkills,
|
|
126
|
-
session: handle.session,
|
|
127
|
-
sessionCwd: workspace.cwd,
|
|
128
|
-
replay,
|
|
129
|
-
workspace,
|
|
130
|
-
});
|
|
131
|
-
const validation = contract.ok
|
|
132
|
-
? await validateReplayEvidence(rc, evidence)
|
|
133
|
-
: contract;
|
|
100
|
+
const validation = await validateReplayContract(rc, contract, handle, workspace);
|
|
134
101
|
if (validation.ok) {
|
|
135
|
-
return { kind: "live-recordable", result,
|
|
102
|
+
return { kind: "live-recordable", result, contract };
|
|
136
103
|
}
|
|
137
104
|
rc.progress.log(`${label}: read-only resume contract was not recorded (${validation.reason})`);
|
|
138
105
|
return { kind: "live-unrecordable", result };
|
|
139
106
|
} finally {
|
|
140
107
|
try {
|
|
141
108
|
const session = handle?.session;
|
|
142
|
-
if (session) rc.perf.timeSync("agent.dispose_ms", () => session.dispose()
|
|
109
|
+
if (session) rc.perf.timeSync("agent.dispose_ms", () => session.dispose());
|
|
143
110
|
} finally {
|
|
144
|
-
await
|
|
111
|
+
await workspace?.dispose();
|
|
145
112
|
}
|
|
146
113
|
}
|
|
147
114
|
}
|
|
115
|
+
|
|
116
|
+
/** Re-check a captured replay identity and its repository evidence against the session and workspace as they are now. */
|
|
117
|
+
async function validateReplayContract(
|
|
118
|
+
rc: RunContext,
|
|
119
|
+
contract: AgentReplayContract,
|
|
120
|
+
handle: AgentSessionHandle,
|
|
121
|
+
workspace: AgentWorkspace,
|
|
122
|
+
): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
|
|
123
|
+
const identity = await validateReplayIdentity({
|
|
124
|
+
rc,
|
|
125
|
+
contract,
|
|
126
|
+
selectedSkills: handle.selectedSkills,
|
|
127
|
+
session: handle.session,
|
|
128
|
+
sessionCwd: workspace.cwd,
|
|
129
|
+
workspace,
|
|
130
|
+
});
|
|
131
|
+
if (!identity.ok || contract.kind === "shared") return identity;
|
|
132
|
+
return await validateMutationGuard(rc, contract.mutationGuard);
|
|
133
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Skill } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { captureAgentSkillIdentities, extractSkillSelectorsFromText } from "./agent-skills.ts";
|
|
4
|
-
import type { AgentExecutionOptions, AgentRunnerSession,
|
|
4
|
+
import type { AgentExecutionOptions, AgentRunnerSession, RunContext } from "./agent-runner-types.ts";
|
|
5
5
|
import { captureEffectiveAgentSessionIdentity } from "./agent-session-identity.ts";
|
|
6
|
+
import { isRecord } from "./guards.ts";
|
|
6
7
|
import { FINAL_TOOL } from "./agent-session.ts";
|
|
7
8
|
import type { AgentWorkspace, IsolatedAgentWorkspace } from "./agent-workspace.ts";
|
|
8
9
|
import { captureAgentJournalKey } from "./journal.ts";
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
captureRepositoryMutationGuard,
|
|
12
13
|
captureIsolatedRepositoryContext,
|
|
13
14
|
captureRepositoryResumeContext,
|
|
14
|
-
|
|
15
|
+
repositoryMismatchReason,
|
|
15
16
|
resumeContextMismatchReason,
|
|
16
17
|
type AgentResumeBaseContext,
|
|
17
18
|
type AgentResumeContext,
|
|
@@ -26,13 +27,17 @@ export type AgentReplayPlan =
|
|
|
26
27
|
| { readonly kind: "shared"; readonly key: string; readonly additionalInputs: readonly string[] }
|
|
27
28
|
| { readonly kind: "isolated"; readonly key: string };
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
| { readonly kind: "
|
|
30
|
+
/** An enabled replay plan with its main-workspace evidence: isolated replays carry the mutation guard taken before setup. */
|
|
31
|
+
export type ArmedReplayPlan =
|
|
32
|
+
| Extract<AgentReplayPlan, { readonly kind: "shared" }>
|
|
33
|
+
| (Extract<AgentReplayPlan, { readonly kind: "isolated" }> & { readonly mutationGuard: string });
|
|
34
|
+
|
|
35
|
+
/** An armed replay plan plus the resume identity captured for one attempt. */
|
|
36
|
+
export type AgentReplayContract = ArmedReplayPlan & { readonly identity: AgentResumeContext };
|
|
32
37
|
|
|
33
38
|
export type AgentAttemptResult =
|
|
34
|
-
| { readonly kind: "cache-hit"; readonly result: unknown; readonly
|
|
35
|
-
| { readonly kind: "live-recordable"; readonly result: unknown; readonly
|
|
39
|
+
| { readonly kind: "cache-hit"; readonly result: unknown; readonly contract: AgentReplayContract }
|
|
40
|
+
| { readonly kind: "live-recordable"; readonly result: unknown; readonly contract: AgentReplayContract }
|
|
36
41
|
| { readonly kind: "live-unrecordable"; readonly result: unknown };
|
|
37
42
|
|
|
38
43
|
export type AgentAttemptSettlement =
|
|
@@ -129,28 +134,28 @@ export async function lookupReplayResult(input: {
|
|
|
129
134
|
|
|
130
135
|
export async function validateReplayIdentity(input: {
|
|
131
136
|
readonly rc: RunContext;
|
|
132
|
-
readonly
|
|
137
|
+
readonly contract: AgentReplayContract;
|
|
133
138
|
readonly selectedSkills: readonly Skill[];
|
|
134
139
|
readonly session: AgentRunnerSession;
|
|
135
140
|
readonly sessionCwd: string;
|
|
136
|
-
readonly replay: Extract<AgentReplayPlan, { readonly kind: "shared" | "isolated" }>;
|
|
137
141
|
readonly workspace: AgentWorkspace;
|
|
138
142
|
}): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
|
|
139
|
-
const
|
|
143
|
+
const { identity } = input.contract;
|
|
144
|
+
const repository = identity.repository.state === "inaccessible"
|
|
140
145
|
? INACCESSIBLE_REPOSITORY_CONTEXT
|
|
141
|
-
: await captureCurrentRepository(input.rc, input.
|
|
146
|
+
: await captureCurrentRepository(input.rc, input.contract, input.workspace);
|
|
142
147
|
if (repository.kind === "unverifiable") return { ok: false, reason: repository.reason };
|
|
143
148
|
|
|
144
149
|
const current = await captureVerifiedReplayIdentity({
|
|
145
150
|
rc: input.rc,
|
|
146
151
|
repository,
|
|
147
|
-
workflow:
|
|
152
|
+
workflow: identity.workflow,
|
|
148
153
|
selectedSkills: input.selectedSkills,
|
|
149
154
|
session: input.session,
|
|
150
155
|
sessionCwd: input.sessionCwd,
|
|
151
156
|
});
|
|
152
157
|
if (current.kind === "unverifiable") return { ok: false, reason: current.reason };
|
|
153
|
-
const mismatch = resumeContextMismatchReason(
|
|
158
|
+
const mismatch = resumeContextMismatchReason(identity, current.identity, {
|
|
154
159
|
allowWorkflowSourceMismatch: input.rc.resumeEditedWorkflow,
|
|
155
160
|
});
|
|
156
161
|
return mismatch ? { ok: false, reason: mismatch } : { ok: true };
|
|
@@ -159,23 +164,16 @@ export async function validateReplayIdentity(input: {
|
|
|
159
164
|
export async function settleAgentAttempt(input: {
|
|
160
165
|
readonly rc: RunContext;
|
|
161
166
|
readonly label: string;
|
|
162
|
-
readonly tags: AgentRunTags;
|
|
163
|
-
readonly replay: AgentReplayPlan;
|
|
164
167
|
readonly outcome: AgentAttemptResult;
|
|
165
168
|
}): Promise<AgentAttemptSettlement> {
|
|
166
|
-
const { rc, label,
|
|
169
|
+
const { rc, label, outcome } = input;
|
|
167
170
|
if (outcome.kind === "live-unrecordable") {
|
|
168
171
|
recordAgentResultSource(rc, "live");
|
|
169
172
|
return { kind: "done", result: outcome.result };
|
|
170
173
|
}
|
|
171
|
-
if (!isReplayEnabled(replay)) throw new Error("Replayable agent outcome produced without an enabled replay plan.");
|
|
172
174
|
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
outcome.identity,
|
|
176
|
-
replay,
|
|
177
|
-
outcome.evidence,
|
|
178
|
-
);
|
|
175
|
+
const { contract } = outcome;
|
|
176
|
+
const repository = await validateRepositoryAfterCleanup(rc, contract);
|
|
179
177
|
if (!repository.ok) {
|
|
180
178
|
if (outcome.kind === "cache-hit") {
|
|
181
179
|
rc.progress.log(`${label}: cached result invalidated after cleanup (${repository.reason})`);
|
|
@@ -185,10 +183,10 @@ export async function settleAgentAttempt(input: {
|
|
|
185
183
|
return { kind: "done", result: outcome.result };
|
|
186
184
|
}
|
|
187
185
|
|
|
188
|
-
await recordJournalResult(rc, label,
|
|
186
|
+
await recordJournalResult(rc, label, contract.key, outcome.result, contract.identity);
|
|
189
187
|
if (outcome.kind === "cache-hit") {
|
|
190
188
|
rc.progress.log(`${label}: using cached result from workflow journal`);
|
|
191
|
-
rc.perf.counter("agent.cache_hit"
|
|
189
|
+
rc.perf.counter("agent.cache_hit");
|
|
192
190
|
recordAgentResultSource(rc, "cached");
|
|
193
191
|
} else {
|
|
194
192
|
recordAgentResultSource(rc, "live");
|
|
@@ -236,12 +234,12 @@ async function captureVerifiedReplayIdentity(input: {
|
|
|
236
234
|
}
|
|
237
235
|
return {
|
|
238
236
|
kind: "verified",
|
|
239
|
-
identity:
|
|
240
|
-
|
|
241
|
-
input.repository,
|
|
242
|
-
effectiveSession.identity,
|
|
243
|
-
skills.skills,
|
|
244
|
-
|
|
237
|
+
identity: {
|
|
238
|
+
workflow: input.workflow,
|
|
239
|
+
repository: input.repository,
|
|
240
|
+
session: effectiveSession.identity,
|
|
241
|
+
skills: skills.skills,
|
|
242
|
+
},
|
|
245
243
|
};
|
|
246
244
|
}
|
|
247
245
|
|
|
@@ -259,7 +257,7 @@ async function validateCachedResult(
|
|
|
259
257
|
if (!patch.ok) {
|
|
260
258
|
return {
|
|
261
259
|
ok: false,
|
|
262
|
-
reason: `cached isolated patch does not apply to the current baseline (${
|
|
260
|
+
reason: `cached isolated patch does not apply to the current baseline (${patch.error})`,
|
|
263
261
|
};
|
|
264
262
|
}
|
|
265
263
|
result = isolated.wrapper.result;
|
|
@@ -271,7 +269,6 @@ async function validateCachedResult(
|
|
|
271
269
|
? { ok: true }
|
|
272
270
|
: { ok: false, reason: "cached text result is not a string" };
|
|
273
271
|
}
|
|
274
|
-
if (result === null) return { ok: true };
|
|
275
272
|
try {
|
|
276
273
|
return Value.Check(opts.schema, result)
|
|
277
274
|
? { ok: true }
|
|
@@ -287,63 +284,45 @@ function isolatedCachedResult(value: unknown):
|
|
|
287
284
|
readonly wrapper: { readonly result: unknown; readonly patch: string; readonly changed: boolean };
|
|
288
285
|
}
|
|
289
286
|
| { readonly ok: false; readonly reason: string } {
|
|
290
|
-
if (
|
|
287
|
+
if (!isRecord(value)) {
|
|
291
288
|
return { ok: false, reason: "cached isolated result is not an object" };
|
|
292
289
|
}
|
|
293
|
-
|
|
294
|
-
if (!("result" in candidate) || typeof candidate.patch !== "string" || typeof candidate.changed !== "boolean") {
|
|
290
|
+
if (!("result" in value) || typeof value.patch !== "string" || typeof value.changed !== "boolean") {
|
|
295
291
|
return { ok: false, reason: "cached isolated result has an invalid wrapper" };
|
|
296
292
|
}
|
|
297
|
-
const wrapper = { result:
|
|
293
|
+
const wrapper = { result: value.result, patch: value.patch, changed: value.changed };
|
|
298
294
|
return { ok: true, wrapper };
|
|
299
295
|
}
|
|
300
296
|
|
|
301
297
|
async function validateRepositoryAfterCleanup(
|
|
302
298
|
rc: RunContext,
|
|
303
|
-
|
|
304
|
-
replay: Extract<AgentReplayPlan, { readonly kind: "shared" | "isolated" }>,
|
|
305
|
-
evidence: AgentReplayEvidence,
|
|
299
|
+
contract: AgentReplayContract,
|
|
306
300
|
): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
|
|
307
|
-
if (
|
|
308
|
-
return evidence.kind === "isolated"
|
|
309
|
-
? await validateReplayEvidence(rc, evidence)
|
|
310
|
-
: { ok: false, reason: "isolated replay omitted its main-workspace mutation guard" };
|
|
311
|
-
}
|
|
301
|
+
if (contract.kind === "isolated") return await validateMutationGuard(rc, contract.mutationGuard);
|
|
312
302
|
|
|
313
|
-
const repository = identity.repository.state === "inaccessible"
|
|
303
|
+
const repository = contract.identity.repository.state === "inaccessible"
|
|
314
304
|
? INACCESSIBLE_REPOSITORY_CONTEXT
|
|
315
|
-
: await captureRepositoryResumeContext(rc.cwd,
|
|
305
|
+
: await captureRepositoryResumeContext(rc.cwd, contract.additionalInputs, rc.signal);
|
|
316
306
|
if (repository.kind === "unverifiable") return { ok: false, reason: repository.reason };
|
|
317
|
-
const mismatch =
|
|
318
|
-
|
|
319
|
-
return evidence.kind === "shared"
|
|
320
|
-
? { ok: true }
|
|
321
|
-
: { ok: false, reason: "shared replay carried isolated mutation evidence" };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export async function validateReplayEvidence(
|
|
325
|
-
rc: RunContext,
|
|
326
|
-
evidence: AgentReplayEvidence,
|
|
327
|
-
): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
|
|
328
|
-
return evidence.kind === "shared"
|
|
329
|
-
? { ok: true }
|
|
330
|
-
: await validateRepositoryMutationGuard(rc, evidence.mutationGuard);
|
|
307
|
+
const mismatch = repositoryMismatchReason(contract.identity.repository, repository);
|
|
308
|
+
return mismatch ? { ok: false, reason: mismatch } : { ok: true };
|
|
331
309
|
}
|
|
332
310
|
|
|
333
|
-
|
|
311
|
+
/** Check that the main workspace is unchanged since an isolated replay took its mutation guard. */
|
|
312
|
+
export async function validateMutationGuard(
|
|
334
313
|
rc: RunContext,
|
|
335
|
-
|
|
314
|
+
mutationGuard: string,
|
|
336
315
|
): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
|
|
337
316
|
const current = await captureRepositoryMutationGuard(rc.cwd, rc.signal);
|
|
338
317
|
if (current.kind === "unverifiable") return { ok: false, reason: current.reason };
|
|
339
|
-
return current.fingerprint ===
|
|
318
|
+
return current.fingerprint === mutationGuard
|
|
340
319
|
? { ok: true }
|
|
341
320
|
: { ok: false, reason: "main workspace changed outside the isolated worktree" };
|
|
342
321
|
}
|
|
343
322
|
|
|
344
323
|
async function captureCurrentRepository(
|
|
345
324
|
rc: RunContext,
|
|
346
|
-
replay:
|
|
325
|
+
replay: ArmedReplayPlan,
|
|
347
326
|
workspace: AgentWorkspace,
|
|
348
327
|
): Promise<RepositoryResumeContext> {
|
|
349
328
|
if (replay.kind === "shared") return await captureRepositoryResumeContext(rc.cwd, replay.additionalInputs, rc.signal);
|
|
@@ -375,8 +354,8 @@ function declaresNoWorkspaceCapability(prompt: string, opts: AgentExecutionOptio
|
|
|
375
354
|
opts.tools !== undefined &&
|
|
376
355
|
opts.tools.length === 0 &&
|
|
377
356
|
(opts.toolHints?.length ?? 0) === 0 &&
|
|
378
|
-
|
|
379
|
-
extractSkillSelectorsFromText(prompt).length === 0
|
|
357
|
+
// Explicit skills suppress prompt inference, matching resolveAgentSkillRequest.
|
|
358
|
+
(opts.skills !== undefined ? opts.skills.length === 0 : extractSkillSelectorsFromText(prompt).length === 0)
|
|
380
359
|
);
|
|
381
360
|
}
|
|
382
361
|
|