pi-workflow-engine 0.13.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/pi-workflow-engine/index.ts +152 -416
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +17 -14
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +7 -1
- 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 +12 -13
- 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 +131 -117
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +4 -18
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +3 -0
- 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/classify.ts +107 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +25 -80
- 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 +58 -117
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +41 -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/host-tools.ts +76 -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 +0 -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/model-ref.ts +19 -0
- 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/pi-compat.ts +2 -2
- 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 +29 -31
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +75 -139
- 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 +3 -4
- 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 +7 -9
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -117
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +8 -13
- 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 +17 -4
- 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 +30 -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 -129
- 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 +41 -34
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +162 -22
- 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 +48 -110
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +64 -93
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +25 -67
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +19 -53
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +21 -49
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
- package/README.md +1 -1
- package/USAGE.md +79 -20
- 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 -24
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
- package/.pi/settings.json +0 -3
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { WorkflowPauseError } from "./cancellation.ts";
|
|
2
|
+
import { isRecord } from "./guards.ts";
|
|
2
3
|
import {
|
|
3
4
|
WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
|
|
4
5
|
type ResolvedWorkflowRunOptions,
|
|
5
6
|
} from "./options.ts";
|
|
7
|
+
import { truncateText } from "./text.ts";
|
|
6
8
|
import type { WorkflowProviderUsageLimitPause } from "./workflow-run-record.ts";
|
|
7
9
|
|
|
8
10
|
export const WORKFLOW_PROVIDER_USAGE_LIMIT_CODE = "WORKFLOW_PROVIDER_USAGE_LIMIT";
|
|
@@ -43,25 +45,33 @@ export class WorkflowProviderUsageLimitError extends WorkflowPauseError {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
now = Date.now(),
|
|
49
|
-
): WorkflowProviderUsageLimitError | undefined {
|
|
48
|
+
/** The last assistant message when it ended its turn with a provider error, its error text ("" if absent), and its non-empty provider metadata. */
|
|
49
|
+
export function lastAssistantError(messages: readonly unknown[]) {
|
|
50
50
|
const message = messages.findLast(isAssistantMessage);
|
|
51
51
|
if (!message || message.stopReason !== "error") return undefined;
|
|
52
|
-
const providerMessage = typeof message.errorMessage === "string" ? message.errorMessage.trim() : "";
|
|
53
|
-
if (!providerMessage || !isUsageLimitMessage(providerMessage)) return undefined;
|
|
54
|
-
const reset = parseProviderResetHint(providerMessage, now);
|
|
55
52
|
const provider = stringDetail(message.provider);
|
|
56
53
|
const model = stringDetail(message.model);
|
|
57
54
|
const api = stringDetail(message.api);
|
|
55
|
+
return {
|
|
56
|
+
message,
|
|
57
|
+
errorMessage: typeof message.errorMessage === "string" ? message.errorMessage : "",
|
|
58
|
+
details: { ...(provider ? { provider } : {}), ...(model ? { model } : {}), ...(api ? { api } : {}) },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function providerUsageLimitFromMessages(
|
|
63
|
+
messages: readonly unknown[],
|
|
64
|
+
now = Date.now(),
|
|
65
|
+
): WorkflowProviderUsageLimitError | undefined {
|
|
66
|
+
const failure = lastAssistantError(messages);
|
|
67
|
+
if (!failure) return undefined;
|
|
68
|
+
const providerMessage = failure.errorMessage.trim();
|
|
69
|
+
if (!providerMessage || !isUsageLimitMessage(providerMessage)) return undefined;
|
|
58
70
|
return new WorkflowProviderUsageLimitError({
|
|
59
71
|
stopReason: "error",
|
|
60
72
|
providerMessage,
|
|
61
|
-
...
|
|
62
|
-
...(
|
|
63
|
-
...(api ? { api } : {}),
|
|
64
|
-
...reset,
|
|
73
|
+
...failure.details,
|
|
74
|
+
...parseProviderResetHint(providerMessage, now),
|
|
65
75
|
});
|
|
66
76
|
}
|
|
67
77
|
|
|
@@ -119,8 +129,9 @@ export function parseProviderResetHint(
|
|
|
119
129
|
: { resetHint: boundedHint(iso) };
|
|
120
130
|
}
|
|
121
131
|
|
|
132
|
+
// Units must be whole words on the hint's line ("6m0s" stays compact; "1 month" and "30\nmessage" are not durations).
|
|
122
133
|
const labelled = message.match(
|
|
123
|
-
/(?:retry[- ]after|try again in|resets? in|available again in|x-ratelimit-reset(?:-[a-z-]+)?)\s*[:=]?\s*((?:\d+(?:\.\d+)
|
|
134
|
+
/(?:retry[- ]after|try again in|resets? in|available again in|x-ratelimit-reset(?:-[a-z-]+)?)\s*[:=]?\s*((?:\d+(?:\.\d+)?[ \t]*(?:milliseconds?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)(?![a-z])[ \t]*)+)/i,
|
|
124
135
|
);
|
|
125
136
|
if (labelled?.[1]) {
|
|
126
137
|
const delayMs = parseDurationMs(labelled[1]);
|
|
@@ -150,27 +161,24 @@ function isUsageLimitMessage(message: string): boolean {
|
|
|
150
161
|
|| /\b(?:usage|rate)\s+limit\s+(?:(?:has\s+been|was)\s+)?(?:reached|exceeded)\b/i.test(message);
|
|
151
162
|
}
|
|
152
163
|
|
|
164
|
+
/** Sum a duration already captured by parseProviderResetHint's labelled grammar. */
|
|
153
165
|
function parseDurationMs(value: string): number | undefined {
|
|
154
166
|
const unitPattern = /(\d+(?:\.\d+)?)\s*(milliseconds?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)/gi;
|
|
155
167
|
let total = 0;
|
|
156
|
-
let matched = "";
|
|
157
168
|
for (const part of value.matchAll(unitPattern)) {
|
|
158
169
|
const amount = Number(part[1]);
|
|
159
|
-
const unit = part[2]
|
|
160
|
-
if (!Number.isFinite(amount) || !unit) return undefined;
|
|
161
|
-
matched += part[0];
|
|
170
|
+
const unit = (part[2] ?? "").toLowerCase();
|
|
162
171
|
if (unit === "ms" || unit.startsWith("millisecond")) total += amount;
|
|
163
172
|
else if (unit === "s" || unit.startsWith("sec")) total += amount * 1_000;
|
|
164
173
|
else if (unit === "m" || unit.startsWith("min")) total += amount * 60_000;
|
|
165
174
|
else if (unit === "h" || unit.startsWith("hr") || unit.startsWith("hour")) total += amount * 3_600_000;
|
|
166
175
|
else total += amount * 86_400_000;
|
|
167
176
|
}
|
|
168
|
-
|
|
169
|
-
return Number.isFinite(total) && total >= 0 ? total : undefined;
|
|
177
|
+
return Number.isFinite(total) ? total : undefined;
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
function boundedHint(value: string): string {
|
|
173
|
-
return value
|
|
181
|
+
return truncateText(value, MAX_RESET_HINT_LENGTH);
|
|
174
182
|
}
|
|
175
183
|
|
|
176
184
|
function isAssistantMessage(value: unknown): value is {
|
|
@@ -181,7 +189,7 @@ function isAssistantMessage(value: unknown): value is {
|
|
|
181
189
|
readonly model?: unknown;
|
|
182
190
|
readonly api?: unknown;
|
|
183
191
|
} {
|
|
184
|
-
return
|
|
192
|
+
return isRecord(value) && value.role === "assistant";
|
|
185
193
|
}
|
|
186
194
|
|
|
187
195
|
function stringDetail(value: unknown): string | undefined {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isPathWithin } from "./tree-fingerprint.ts";
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { isPathWithin, portableRelativePath } from "./tree-fingerprint.ts";
|
|
3
3
|
|
|
4
4
|
export interface ReplayWorkspaceRoots {
|
|
5
5
|
readonly sessionCwd: string;
|
|
@@ -20,11 +20,6 @@ export function normalizeWorkspaceReferences(value: string, roots: ReplayWorkspa
|
|
|
20
20
|
return paths.reduce((normalized, path) => replacePathPrefix(normalized, path, "<workspace>"), value);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function portableRelativePath(root: string, path: string): string {
|
|
24
|
-
const value = relative(root, path).split(sep).join("/");
|
|
25
|
-
return value.length === 0 ? "." : value;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
23
|
function resolvedWorkspaceRoots(roots: ReplayWorkspaceRoots): readonly string[] {
|
|
29
24
|
return [...new Set([resolve(roots.sessionCwd), resolve(roots.workspaceRoot)])];
|
|
30
25
|
}
|
|
@@ -39,7 +39,6 @@ export const ResearchEvidenceSchema = Type.Object({
|
|
|
39
39
|
export type ResearchEvidence = Static<typeof ResearchEvidenceSchema>;
|
|
40
40
|
|
|
41
41
|
export const ResearchLaneResultSchema = Type.Object({
|
|
42
|
-
laneId: Type.String({ minLength: 1 }),
|
|
43
42
|
evidence: Type.Array(ResearchEvidenceSchema, { maxItems: MAX_LANE_EVIDENCE }),
|
|
44
43
|
gaps: Type.Array(Type.String()),
|
|
45
44
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
MAX_RESEARCH_LANES,
|
|
3
2
|
MAX_VERIFICATION_CLAIMS,
|
|
4
3
|
type ResearchClaimCandidate,
|
|
5
4
|
type ResearchEvidence,
|
|
@@ -22,31 +21,21 @@ export function normalizeResearchLanes(plan: ResearchPlan): ResearchLane[] {
|
|
|
22
21
|
const id = normalizedText(lane.id);
|
|
23
22
|
const title = normalizedText(lane.title);
|
|
24
23
|
const objective = normalizedText(lane.objective);
|
|
25
|
-
const queries = dedupeText(lane.queries)
|
|
24
|
+
const queries = dedupeText(lane.queries);
|
|
26
25
|
if (!id || !title || !objective || queries.length === 0 || seen.has(id.toLowerCase())) continue;
|
|
27
26
|
seen.add(id.toLowerCase());
|
|
28
27
|
lanes.push({ id, title, objective, queries });
|
|
29
|
-
if (lanes.length >= MAX_RESEARCH_LANES) break;
|
|
30
28
|
}
|
|
31
29
|
return lanes;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
export function sanitizeLaneResults(results: readonly ResearchLaneResult[]): ResearchLaneResult[] {
|
|
35
|
-
return results.map((result) => ({
|
|
36
|
-
laneId: normalizedText(result.laneId),
|
|
37
|
-
gaps: dedupeText(result.gaps),
|
|
38
|
-
evidence: dedupeEvidence(result.evidence.map(sanitizeEvidence).filter(isDefined)),
|
|
39
|
-
}));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
32
|
export function buildClaimCandidates(results: readonly ResearchLaneResult[]): ResearchClaimCandidate[] {
|
|
43
33
|
const byClaim = new Map<string, { claim: string; importance: ResearchEvidence["importance"]; evidence: ResearchEvidence[] }>();
|
|
44
|
-
for (const item of results.flatMap((result) => result.evidence)) {
|
|
45
|
-
const key =
|
|
46
|
-
if (!key) continue;
|
|
34
|
+
for (const item of results.flatMap((result) => result.evidence.map(sanitizeEvidence).filter(isDefined))) {
|
|
35
|
+
const key = item.claim.toLowerCase();
|
|
47
36
|
const current = byClaim.get(key);
|
|
48
37
|
if (!current) {
|
|
49
|
-
byClaim.set(key, { claim:
|
|
38
|
+
byClaim.set(key, { claim: item.claim, importance: item.importance, evidence: [item] });
|
|
50
39
|
continue;
|
|
51
40
|
}
|
|
52
41
|
if (IMPORTANCE_RANK[item.importance] < IMPORTANCE_RANK[current.importance]) current.importance = item.importance;
|
|
@@ -142,10 +131,7 @@ function verificationSources(
|
|
|
142
131
|
return byClaim;
|
|
143
132
|
}
|
|
144
133
|
|
|
145
|
-
export function fallbackResearchReport(
|
|
146
|
-
verifications: readonly ResearchVerification[],
|
|
147
|
-
limitation: string,
|
|
148
|
-
): ResearchReport {
|
|
134
|
+
export function fallbackResearchReport(verifications: readonly ResearchVerification[]): ResearchReport {
|
|
149
135
|
const entry = (verification: ResearchVerification): ResearchReportEntry => ({
|
|
150
136
|
claim: verification.claim,
|
|
151
137
|
explanation: verification.explanation,
|
|
@@ -158,22 +144,34 @@ export function fallbackResearchReport(
|
|
|
158
144
|
uncertainties: verifications.filter((item) => item.verdict === "UNCERTAIN").map(entry),
|
|
159
145
|
inferences: verifications.filter((item) => item.verdict === "INFERENCE").map(entry),
|
|
160
146
|
sources: dedupeSources(verifications.flatMap((verification) => verification.sources)),
|
|
161
|
-
limitations: [
|
|
147
|
+
limitations: ["The synthesis stage failed; verified claims are listed without a narrative answer."],
|
|
162
148
|
nextSteps: ["Review the verified claim groups and rerun synthesis if a narrative answer is required."],
|
|
163
149
|
};
|
|
164
150
|
}
|
|
165
151
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
152
|
+
const UNAVAILABLE_RESEARCH = {
|
|
153
|
+
"empty-question": {
|
|
154
|
+
message: "No research question was provided.",
|
|
155
|
+
nextStep: "Run `/workflow research <question>` and include any source, date, or geography constraints in the arguments.",
|
|
156
|
+
},
|
|
157
|
+
"missing-capability": {
|
|
158
|
+
message: "Research could not start because its agents could reach no external web-search or URL-extraction tool.",
|
|
159
|
+
nextStep:
|
|
160
|
+
"Install or enable a pi tool that can search the web or extract HTTP(S) pages, then rerun the workflow. " +
|
|
161
|
+
"An MCP web search server counts only when the host agent runs research through the workflow tool, for example after `dynamax`.",
|
|
162
|
+
},
|
|
163
|
+
"lanes-failed": {
|
|
164
|
+
message: "Research could not gather direct-page evidence because one or more research lanes failed.",
|
|
165
|
+
nextStep: "Check the installed external-search tool and its provider, then rerun the workflow.",
|
|
166
|
+
},
|
|
167
|
+
"no-evidence": {
|
|
168
|
+
message: "Research completed without enough direct-page evidence to support an answer.",
|
|
169
|
+
nextStep: "Check the installed external-search tool, narrow the question, or provide preferred source domains.",
|
|
170
|
+
},
|
|
171
|
+
} as const;
|
|
172
|
+
|
|
173
|
+
export function unavailableResearchReport(reason: keyof typeof UNAVAILABLE_RESEARCH): ResearchReport {
|
|
174
|
+
const { message, nextStep } = UNAVAILABLE_RESEARCH[reason];
|
|
177
175
|
return {
|
|
178
176
|
answer: message,
|
|
179
177
|
supportedClaims: [],
|
|
@@ -3,15 +3,19 @@ import { lstat, stat } from "node:fs/promises";
|
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { throwIfAborted } from "./cancellation.ts";
|
|
5
5
|
import { isMissingPathError } from "./filesystem-error.ts";
|
|
6
|
+
import { isGitObjectId, parseGitTopLevel, runGit } from "./git.ts";
|
|
7
|
+
import { isRecord } from "./guards.ts";
|
|
6
8
|
import type {
|
|
7
9
|
EffectiveAgentSessionIdentity,
|
|
8
10
|
EffectiveToolIdentity,
|
|
9
11
|
} from "./agent-session-identity.ts";
|
|
10
|
-
import {
|
|
12
|
+
import type { BoundedProcessResult } from "./process-runner.ts";
|
|
11
13
|
import {
|
|
12
14
|
BoundedFingerprint,
|
|
13
15
|
captureDeclaredInputFingerprint,
|
|
14
|
-
|
|
16
|
+
captureSourceTreeFingerprint,
|
|
17
|
+
FINGERPRINT_EXCLUDED_RELATIVE_PATHS,
|
|
18
|
+
isExcludedDeclaredInput,
|
|
15
19
|
isPathWithin,
|
|
16
20
|
resolveDeclaredInputPaths,
|
|
17
21
|
validateTreeFile,
|
|
@@ -58,11 +62,8 @@ export interface AgentResumeBaseContext {
|
|
|
58
62
|
readonly workflow: WorkflowResumeContext;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
export interface
|
|
65
|
+
export interface AgentResumeContext {
|
|
62
66
|
readonly workflow: VerifiedWorkflowResumeContext;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface AgentResumeContext extends VerifiedAgentResumeBaseContext {
|
|
66
67
|
readonly repository: VerifiedRepositoryResumeContext;
|
|
67
68
|
readonly session: EffectiveAgentSessionIdentity;
|
|
68
69
|
readonly skills: readonly ResolvedSkillIdentity[];
|
|
@@ -86,7 +87,6 @@ type BoundedProcessFailureResult = Extract<BoundedProcessResult, { readonly ok:
|
|
|
86
87
|
const GIT_CONTEXT_TIMEOUT_MS = 15_000;
|
|
87
88
|
const GIT_CONTEXT_MAX_BYTES = 32 << 20;
|
|
88
89
|
const CONTENT_FINGERPRINT_MAX_BYTES = 32 << 20;
|
|
89
|
-
const SOURCE_TREE_MAX_FILES = 4096;
|
|
90
90
|
const REPOSITORY_INPUT_MAX_ENTRIES = 4096;
|
|
91
91
|
const GIT_UNTRACKED_MAX_ENTRIES = 32_768;
|
|
92
92
|
const GIT_VISIBLE_PATHS = [
|
|
@@ -94,14 +94,6 @@ const GIT_VISIBLE_PATHS = [
|
|
|
94
94
|
":(exclude).pi/.workflow-runs/**",
|
|
95
95
|
":(glob,exclude)**/.pi/.workflow-runs/**",
|
|
96
96
|
] as const;
|
|
97
|
-
export const FINGERPRINT_EXCLUDED_RELATIVE_PATHS = new Set([
|
|
98
|
-
".git",
|
|
99
|
-
".pi/.workflow-runs",
|
|
100
|
-
]);
|
|
101
|
-
|
|
102
|
-
export function unverifiableRepositoryResumeContext(reason: string): RepositoryResumeContext {
|
|
103
|
-
return { kind: "unverifiable", reason };
|
|
104
|
-
}
|
|
105
97
|
|
|
106
98
|
export function unverifiableWorkflowResumeContext(name: string, reason: string): WorkflowResumeContext {
|
|
107
99
|
return { kind: "unverifiable", name, reason };
|
|
@@ -152,9 +144,9 @@ export async function captureIsolatedRepositoryContext(
|
|
|
152
144
|
return { kind: "unverifiable", reason: "isolated baseline is not a full commit object ID" };
|
|
153
145
|
}
|
|
154
146
|
const [commit, tree, entries] = await Promise.all([
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
runContextGit(cwd, ["rev-parse", "--verify", `${baselineOid}^{commit}`], signal),
|
|
148
|
+
runContextGit(cwd, ["rev-parse", "--verify", `${baselineOid}^{tree}`], signal),
|
|
149
|
+
runContextGit(cwd, ["ls-tree", "-r", "-z", "--full-tree", baselineOid], signal),
|
|
158
150
|
]);
|
|
159
151
|
throwIfAborted(signal);
|
|
160
152
|
if (!commit.ok) return { kind: "unverifiable", reason: processFailureReason("isolated baseline commit probe", commit) };
|
|
@@ -189,7 +181,7 @@ async function captureNonGitRepositoryContext(
|
|
|
189
181
|
const capture = await captureRepositoryInputs(cwd, inputs, signal);
|
|
190
182
|
return capture.kind === "verified"
|
|
191
183
|
? { kind: "verified", state: "non-git", workingTreeFingerprint: capture.fingerprint }
|
|
192
|
-
:
|
|
184
|
+
: capture;
|
|
193
185
|
}
|
|
194
186
|
|
|
195
187
|
async function captureRepositoryInputs(
|
|
@@ -240,7 +232,7 @@ async function inspectRepository(
|
|
|
240
232
|
signal: AbortSignal | undefined,
|
|
241
233
|
): Promise<RepositoryInspection> {
|
|
242
234
|
throwIfAborted(signal);
|
|
243
|
-
const probe = await
|
|
235
|
+
const probe = await runContextGit(cwd, ["rev-parse", "--is-inside-work-tree"], signal);
|
|
244
236
|
throwIfAborted(signal);
|
|
245
237
|
if (!probe.ok) {
|
|
246
238
|
if (probe.failure.kind === "exit") {
|
|
@@ -258,8 +250,8 @@ async function inspectRepository(
|
|
|
258
250
|
}
|
|
259
251
|
|
|
260
252
|
const [rootResult, headResult] = await Promise.all([
|
|
261
|
-
|
|
262
|
-
|
|
253
|
+
runContextGit(cwd, ["rev-parse", "--show-toplevel"], signal),
|
|
254
|
+
runContextGit(cwd, ["rev-parse", "--verify", "--quiet", "HEAD"], signal),
|
|
263
255
|
]);
|
|
264
256
|
throwIfAborted(signal);
|
|
265
257
|
if (!rootResult.ok) {
|
|
@@ -283,57 +275,50 @@ async function captureGitVisibleState(
|
|
|
283
275
|
signal: AbortSignal | undefined,
|
|
284
276
|
): Promise<FingerprintCapture> {
|
|
285
277
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
278
|
+
const diffFlags = [
|
|
279
|
+
"--binary",
|
|
280
|
+
"--full-index",
|
|
281
|
+
"--no-ext-diff",
|
|
282
|
+
"--no-textconv",
|
|
283
|
+
"--no-color",
|
|
284
|
+
"--ignore-submodules=none",
|
|
285
|
+
] as const;
|
|
286
|
+
const [unstaged, unstagedRaw, staged, untrackedPaths, indexEntries, trackedFlags] = await Promise.all([
|
|
287
|
+
runContextGit(cwd, ["diff", ...diffFlags, "--", ...GIT_VISIBLE_PATHS], signal),
|
|
288
|
+
runContextGit(cwd, ["diff", "--raw", "-z", "--no-abbrev", "--no-renames", ...diffFlags.slice(2), "--", ...GIT_VISIBLE_PATHS], signal),
|
|
289
|
+
runContextGit(cwd, ["diff", "--cached", ...diffFlags, "--", ...GIT_VISIBLE_PATHS], signal),
|
|
290
|
+
runContextGit(cwd, ["ls-files", "--others", "--exclude-standard", "-z", "--", ...GIT_VISIBLE_PATHS], signal),
|
|
291
|
+
runContextGit(cwd, ["ls-files", "--stage", "--full-name", "-z"], signal),
|
|
292
|
+
runContextGit(cwd, ["ls-files", "-v", "--full-name", "-z"], signal),
|
|
293
|
+
]);
|
|
288
294
|
throwIfAborted(signal);
|
|
289
|
-
return { kind: "unverifiable", reason: unknownErrorMessage(error) };
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
async function captureGitVisibleStateUnchecked(
|
|
294
|
-
cwd: string,
|
|
295
|
-
signal: AbortSignal | undefined,
|
|
296
|
-
): Promise<FingerprintCapture> {
|
|
297
|
-
const diffFlags = [
|
|
298
|
-
"--binary",
|
|
299
|
-
"--full-index",
|
|
300
|
-
"--no-ext-diff",
|
|
301
|
-
"--no-textconv",
|
|
302
|
-
"--no-color",
|
|
303
|
-
"--ignore-submodules=none",
|
|
304
|
-
] as const;
|
|
305
|
-
const [unstaged, unstagedRaw, staged, untrackedPaths, indexEntries, trackedFlags] = await Promise.all([
|
|
306
|
-
runGit(cwd, ["diff", ...diffFlags, "--", ...GIT_VISIBLE_PATHS], signal),
|
|
307
|
-
runGit(cwd, ["diff", "--raw", "-z", "--no-abbrev", "--no-renames", ...diffFlags.slice(2), "--", ...GIT_VISIBLE_PATHS], signal),
|
|
308
|
-
runGit(cwd, ["diff", "--cached", ...diffFlags, "--", ...GIT_VISIBLE_PATHS], signal),
|
|
309
|
-
runGit(cwd, ["ls-files", "--others", "--exclude-standard", "-z", "--", ...GIT_VISIBLE_PATHS], signal),
|
|
310
|
-
runGit(cwd, ["ls-files", "--stage", "--full-name", "-z"], signal),
|
|
311
|
-
runGit(cwd, ["ls-files", "-v", "--full-name", "-z"], signal),
|
|
312
|
-
]);
|
|
313
|
-
throwIfAborted(signal);
|
|
314
295
|
|
|
315
|
-
|
|
316
|
-
|
|
296
|
+
const validated = validateGitVisibleOutputs({ unstaged, unstagedRaw, staged, untrackedPaths, indexEntries, trackedFlags });
|
|
297
|
+
if (!validated.ok) return { kind: "unverifiable", reason: validated.reason };
|
|
317
298
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
299
|
+
const untracked = await captureDeclaredInputFingerprint({
|
|
300
|
+
root: cwd,
|
|
301
|
+
inputs: validated.untrackedPaths.filter((path) => !isExcludedDeclaredInput(path, FINGERPRINT_EXCLUDED_RELATIVE_PATHS)),
|
|
302
|
+
excludedRelativePaths: FINGERPRINT_EXCLUDED_RELATIVE_PATHS,
|
|
303
|
+
maxBytes: CONTENT_FINGERPRINT_MAX_BYTES,
|
|
304
|
+
maxEntries: GIT_UNTRACKED_MAX_ENTRIES,
|
|
305
|
+
signal,
|
|
306
|
+
});
|
|
307
|
+
if (untracked.kind === "unverifiable") return untracked;
|
|
327
308
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
309
|
+
return {
|
|
310
|
+
kind: "verified",
|
|
311
|
+
fingerprint: combineFingerprints([
|
|
312
|
+
["unstaged", validated.unstaged],
|
|
313
|
+
["staged", validated.staged],
|
|
314
|
+
["index", validated.indexEntries],
|
|
315
|
+
["untracked", untracked.fingerprint],
|
|
316
|
+
]),
|
|
317
|
+
};
|
|
318
|
+
} catch (error) {
|
|
319
|
+
throwIfAborted(signal);
|
|
320
|
+
return { kind: "unverifiable", reason: unknownErrorMessage(error) };
|
|
321
|
+
}
|
|
337
322
|
}
|
|
338
323
|
|
|
339
324
|
interface GitVisibleProcessOutputs {
|
|
@@ -474,13 +459,7 @@ export async function captureWorkflowResumeContext(
|
|
|
474
459
|
return unverifiableWorkflowResumeContext(mod.meta.name, `workflow source file is not part of its source tree: ${validation.reason}`);
|
|
475
460
|
}
|
|
476
461
|
|
|
477
|
-
const current = await
|
|
478
|
-
root: sourceRoot,
|
|
479
|
-
excludedRelativePaths: FINGERPRINT_EXCLUDED_RELATIVE_PATHS,
|
|
480
|
-
maxBytes: CONTENT_FINGERPRINT_MAX_BYTES,
|
|
481
|
-
maxFiles: SOURCE_TREE_MAX_FILES,
|
|
482
|
-
signal,
|
|
483
|
-
});
|
|
462
|
+
const current = await captureSourceTreeFingerprint(sourceRoot, signal);
|
|
484
463
|
if (current.kind === "unverifiable") {
|
|
485
464
|
return unverifiableWorkflowResumeContext(mod.meta.name, `workflow source tree could not be verified: ${current.reason}`);
|
|
486
465
|
}
|
|
@@ -490,18 +469,20 @@ export async function captureWorkflowResumeContext(
|
|
|
490
469
|
return { kind: "verified", name: mod.meta.name, sourceFingerprint: mod.source.fingerprint };
|
|
491
470
|
}
|
|
492
471
|
|
|
493
|
-
export function
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
472
|
+
export function repositoryMismatchReason(
|
|
473
|
+
stored: VerifiedRepositoryResumeContext,
|
|
474
|
+
current: VerifiedRepositoryResumeContext,
|
|
475
|
+
): string | undefined {
|
|
476
|
+
if (stored.state !== current.state) return "repository state changed";
|
|
477
|
+
if (
|
|
478
|
+
(stored.state === "git" || stored.state === "isolated") &&
|
|
479
|
+
(current.state === "git" || current.state === "isolated") &&
|
|
480
|
+
stored.head !== current.head
|
|
481
|
+
) {
|
|
482
|
+
return stored.state === "git" ? "repository HEAD changed" : "isolated baseline changed";
|
|
483
|
+
}
|
|
484
|
+
if (stored.workingTreeFingerprint !== current.workingTreeFingerprint) return "working tree contents changed";
|
|
485
|
+
return undefined;
|
|
505
486
|
}
|
|
506
487
|
|
|
507
488
|
export function resumeContextMismatchReason(
|
|
@@ -509,17 +490,8 @@ export function resumeContextMismatchReason(
|
|
|
509
490
|
current: AgentResumeContext,
|
|
510
491
|
options: ResumeContextComparisonOptions = {},
|
|
511
492
|
): string | undefined {
|
|
512
|
-
|
|
513
|
-
if (
|
|
514
|
-
(stored.repository.state === "git" || stored.repository.state === "isolated") &&
|
|
515
|
-
(current.repository.state === "git" || current.repository.state === "isolated") &&
|
|
516
|
-
stored.repository.head !== current.repository.head
|
|
517
|
-
) {
|
|
518
|
-
return stored.repository.state === "git" ? "repository HEAD changed" : "isolated baseline changed";
|
|
519
|
-
}
|
|
520
|
-
if (stored.repository.workingTreeFingerprint !== current.repository.workingTreeFingerprint) {
|
|
521
|
-
return "working tree contents changed";
|
|
522
|
-
}
|
|
493
|
+
const repository = repositoryMismatchReason(stored.repository, current.repository);
|
|
494
|
+
if (repository) return repository;
|
|
523
495
|
if (stored.workflow.name !== current.workflow.name) return "workflow name changed";
|
|
524
496
|
if (
|
|
525
497
|
!options.allowWorkflowSourceMismatch
|
|
@@ -560,19 +532,14 @@ export function isAgentResumeContext(value: unknown): value is AgentResumeContex
|
|
|
560
532
|
);
|
|
561
533
|
}
|
|
562
534
|
|
|
563
|
-
async function
|
|
564
|
-
return await
|
|
565
|
-
file: "git",
|
|
566
|
-
args,
|
|
535
|
+
async function runContextGit(cwd: string, args: readonly string[], signal: AbortSignal | undefined): Promise<BoundedProcessResult> {
|
|
536
|
+
return await runGit({
|
|
567
537
|
cwd,
|
|
568
|
-
|
|
538
|
+
args,
|
|
539
|
+
label: "git resume-context capture",
|
|
569
540
|
signal,
|
|
570
541
|
timeoutMs: GIT_CONTEXT_TIMEOUT_MS,
|
|
571
542
|
maxBufferBytes: GIT_CONTEXT_MAX_BYTES,
|
|
572
|
-
abortError: "git resume-context capture aborted",
|
|
573
|
-
timeoutError: `git resume-context capture timed out after ${GIT_CONTEXT_TIMEOUT_MS}ms`,
|
|
574
|
-
maxBufferError: `git resume-context capture exceeded ${GIT_CONTEXT_MAX_BYTES} bytes`,
|
|
575
|
-
exitError: (stderr, code, exitSignal) => stderr.trim() || `git exited with code ${code ?? `signal ${exitSignal ?? "unknown"}`}`,
|
|
576
543
|
});
|
|
577
544
|
}
|
|
578
545
|
|
|
@@ -715,37 +682,6 @@ function parseNullTerminatedRecords(output: string, operation: string): string[]
|
|
|
715
682
|
return output.slice(0, -1).split("\0");
|
|
716
683
|
}
|
|
717
684
|
|
|
718
|
-
function parseGitTopLevel(output: string, cwd: string): string | undefined {
|
|
719
|
-
const withoutLf = output.endsWith("\n") ? output.slice(0, -1) : output;
|
|
720
|
-
const value = withoutLf.endsWith("\r") ? withoutLf.slice(0, -1) : withoutLf;
|
|
721
|
-
if (value.length === 0 || value.includes("\n") || value.includes("\0")) return undefined;
|
|
722
|
-
const root = resolve(cwd, value);
|
|
723
|
-
return isPathWithin(root, cwd) ? root : undefined;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
function isGitObjectId(value: string): boolean {
|
|
727
|
-
return /^[0-9a-f]{40,64}$/i.test(value);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function isFingerprintExcludedPath(path: string): boolean {
|
|
731
|
-
const normalized = path.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
732
|
-
for (const excluded of FINGERPRINT_EXCLUDED_RELATIVE_PATHS) {
|
|
733
|
-
if (
|
|
734
|
-
normalized === excluded ||
|
|
735
|
-
normalized.startsWith(`${excluded}/`) ||
|
|
736
|
-
normalized.endsWith(`/${excluded}`) ||
|
|
737
|
-
normalized.includes(`/${excluded}/`)
|
|
738
|
-
) {
|
|
739
|
-
return true;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
return false;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
746
|
-
return typeof value === "object" && value !== null;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
685
|
function sanitizeProcessMessage(message: string): string {
|
|
750
686
|
return message.trim().replace(/\s+/g, " ").slice(0, 500);
|
|
751
687
|
}
|