@wichayutdew/pi-workflows 1.0.1 → 2.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/README.md +312 -223
- package/agents/step.md +4 -11
- package/dist/index.js +2554 -976
- package/examples/mr-comments.workflow.yaml +16 -8
- package/examples/prompts/mr-comments/implement.md +13 -9
- package/examples/prompts/mr-comments/plan.md +44 -15
- package/examples/prompts/mr-comments/verify.md +8 -7
- package/examples/settings.yaml +7 -1
- package/examples/starter-kit/mr-comment.workflow.yaml +149 -0
- package/examples/starter-kit/mr-review.workflow.yaml +130 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +33 -0
- package/examples/starter-kit/steps/mr-comment/implement.md +38 -0
- package/examples/starter-kit/steps/mr-comment/plan.md +62 -0
- package/examples/starter-kit/steps/mr-comment/publish.md +36 -0
- package/examples/starter-kit/steps/mr-comment/verify.md +35 -0
- package/examples/starter-kit/steps/mr-review/fetch.md +25 -0
- package/examples/starter-kit/steps/mr-review/publish.md +30 -0
- package/examples/starter-kit/steps/mr-review/review.md +69 -0
- package/examples/starter-kit/steps/mr-review/verify.md +24 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +57 -0
- package/examples/starter-kit/steps/ticket/implement.md +35 -0
- package/examples/starter-kit/steps/ticket/plan.md +40 -0
- package/examples/starter-kit/steps/ticket/verify.md +29 -0
- package/examples/starter-kit/steps/work/implement.md +34 -0
- package/examples/starter-kit/steps/work/plan.md +38 -0
- package/examples/starter-kit/steps/work/verify.md +30 -0
- package/examples/starter-kit/ticket.workflow.yaml +100 -0
- package/examples/starter-kit/work.workflow.yaml +97 -0
- package/package.json +3 -3
- package/schemas/workflow.schema.json +78 -44
- package/src/command-names.ts +1 -0
- package/src/commands.ts +33 -4
- package/src/config/ceiling.ts +6 -12
- package/src/config/load-workflows.ts +12 -1
- package/src/config/step-digests.ts +19 -0
- package/src/config/types.ts +17 -22
- package/src/config/validation/permissions.ts +5 -66
- package/src/config/validation/prompt.ts +1 -0
- package/src/config/validation/shared.ts +0 -2
- package/src/config/validation/step.ts +106 -2
- package/src/config/validation/workflow.ts +76 -0
- package/src/engine/create-run.ts +3 -0
- package/src/engine/gate-transitions.ts +58 -9
- package/src/engine/reconciliation-history.ts +61 -24
- package/src/engine/run-advance.ts +37 -4
- package/src/engine/run-lifecycle.ts +11 -0
- package/src/engine/run-reconciliation.ts +51 -21
- package/src/engine/run-validation.ts +235 -0
- package/src/engine/run-workflow-validation.ts +191 -0
- package/src/engine/state-types.ts +108 -5
- package/src/engine/state.ts +15 -1
- package/src/engine/step-trace.ts +495 -0
- package/src/engine/transitions.ts +1 -0
- package/src/harness/action-context.ts +31 -6
- package/src/harness/core-actions.ts +30 -1
- package/src/harness/delegation-control-actions.ts +27 -2
- package/src/harness/delegation-failure.ts +42 -9
- package/src/harness/delegation-plan.ts +96 -40
- package/src/harness/delegation-recovery-validation.ts +3 -1
- package/src/harness/delegation-response-actions.ts +73 -3
- package/src/harness/delegation-retry-policy.ts +5 -3
- package/src/harness/dependencies.ts +11 -2
- package/src/harness/gate-submission-action.ts +5 -31
- package/src/harness/lifecycle-actions.ts +4 -5
- package/src/harness/pause-actions.ts +10 -1
- package/src/harness/plannotator-result-actions.ts +20 -11
- package/src/harness/prompt-gate-actions.ts +40 -2
- package/src/harness/resume-action.ts +75 -1
- package/src/harness/start-actions.ts +77 -3
- package/src/harness/status-actions.ts +7 -17
- package/src/harness/step-effects.ts +61 -0
- package/src/harness/step-execution-actions.ts +181 -25
- package/src/harness/step-reporting.ts +168 -0
- package/src/harness/types.ts +1 -0
- package/src/harness/workspace-directory.ts +67 -0
- package/src/harness.ts +37 -9
- package/src/integrations/plannotator-requests.ts +40 -5
- package/src/integrations/subagents/child-policy-sections.ts +71 -84
- package/src/integrations/subagents/child-policy-types.ts +7 -7
- package/src/integrations/subagents/child-policy-validation.ts +10 -4
- package/src/integrations/subagents/child-runtime-completion.ts +1 -0
- package/src/integrations/subagents/child-runtime-files.ts +29 -86
- package/src/integrations/subagents/child-runtime-policy.ts +8 -33
- package/src/integrations/subagents/child-runtime.ts +3 -15
- package/src/integrations/subagents/delegated-result.ts +1 -0
- package/src/integrations/subagents/diagnostic-format.ts +5 -0
- package/src/integrations/subagents/diagnostic-types.ts +7 -1
- package/src/integrations/subagents/diagnostics.ts +2 -0
- package/src/integrations/subagents/failure-correlation.ts +52 -2
- package/src/integrations/subagents/failure-transcript.ts +60 -1
- package/src/integrations/subagents/replay-audit.ts +0 -1
- package/src/integrations/subagents/replay-safety.ts +12 -27
- package/src/integrations/subagents/session-diagnostics.ts +102 -3
- package/src/policy/bash-authorization.ts +2 -21
- package/src/policy/bash-types.ts +0 -5
- package/src/policy/bash.ts +1 -2
- package/src/policy/tool-call-authorization.ts +1 -7
- package/src/preflight.ts +0 -5
- package/src/prompt/retry-task.ts +11 -3
- package/src/prompt/step-contract.ts +10 -56
- package/src/prompt/step-sections.ts +8 -10
- package/src/prompt/step-task.ts +41 -10
- package/src/prompt/template.ts +3 -2
- package/src/runtime/completion-tool.ts +14 -0
- package/src/runtime/main-step-completion.ts +3 -0
- package/src/runtime/main-step-policy.ts +2 -1
- package/src/runtime/main-step-runtime-types.ts +9 -1
- package/src/runtime/main-step-runtime.ts +2 -0
- package/src/runtime/main-step-state.ts +6 -0
- package/src/runtime/main-step-trace.ts +45 -0
- package/src/runtime/step-result.ts +57 -0
- package/src/step-log.ts +178 -0
- package/src/workflow-doctor.ts +254 -0
- package/src/workflow-status/format-status.ts +32 -3
- package/src/workflow-status/render-board.ts +7 -4
- package/src/workflow-status/render-path.ts +51 -24
- package/src/workflow-status/render-step-detail.ts +379 -0
- package/src/workflow-status/render-summary.ts +22 -0
- package/src/workflow-status/transcript-reader.ts +231 -0
- package/src/workflow-status/types.ts +8 -0
- package/src/workflow-status/view.ts +161 -14
- package/src/workflow-status.ts +6 -0
- package/src/policy/approved-command-extraction.ts +0 -71
- package/src/policy/approved-commands.ts +0 -15
- package/src/policy/bash-read-only.ts +0 -161
- package/src/policy/restricted-git.ts +0 -30
- package/src/policy/reviewed-artifact.ts +0 -97
- package/src/policy/reviewed-command-safety.ts +0 -150
- package/src/policy/reviewed-command-shape.ts +0 -49
- package/src/policy/reviewed-repository-contract.ts +0 -138
- package/src/policy/reviewed-repository-cwd.ts +0 -102
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
2
|
+
import type {
|
|
3
|
+
StepExecutionAttempt,
|
|
4
|
+
StepHistoryEntry,
|
|
5
|
+
} from '../engine/state.ts';
|
|
6
|
+
import { redactStepDetailText } from '../step-log.ts';
|
|
7
|
+
import { boxed, keyValueLines } from './layout.ts';
|
|
8
|
+
import { buildPathEntries } from './render-path.ts';
|
|
9
|
+
import type { StepTranscriptLog } from './transcript-reader.ts';
|
|
10
|
+
import type { WorkflowStatusSnapshot, WorkflowStatusTheme } from './types.ts';
|
|
11
|
+
|
|
12
|
+
const MAX_DETAIL_ARTIFACT_CHARS = 20_000;
|
|
13
|
+
|
|
14
|
+
export type StepTranscriptViewState =
|
|
15
|
+
{ readonly status: 'loading' } | StepTranscriptLog;
|
|
16
|
+
|
|
17
|
+
export type StepTranscriptViewCache = ReadonlyMap<
|
|
18
|
+
string,
|
|
19
|
+
StepTranscriptViewState
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
export type SelectedStepDetail = {
|
|
23
|
+
readonly entryIndex: number;
|
|
24
|
+
readonly historyEntry?: StepHistoryEntry;
|
|
25
|
+
readonly attempts: ReadonlyArray<StepExecutionAttempt>;
|
|
26
|
+
readonly omittedAttempts: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function stepTranscriptCacheKey(
|
|
30
|
+
runId: string,
|
|
31
|
+
attempt: StepExecutionAttempt,
|
|
32
|
+
): string {
|
|
33
|
+
return `${runId}\0${attempt.requestId}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Resolves one displayed path row back to its durable checkpoint evidence. */
|
|
37
|
+
export function selectedStepDetail(
|
|
38
|
+
snapshot: WorkflowStatusSnapshot,
|
|
39
|
+
selectedIndex: number,
|
|
40
|
+
): SelectedStepDetail | undefined {
|
|
41
|
+
const entries = buildPathEntries(snapshot);
|
|
42
|
+
const entry = entries[selectedIndex];
|
|
43
|
+
if (!entry) return undefined;
|
|
44
|
+
if (entry.historyIndex !== undefined) {
|
|
45
|
+
const historyEntry = snapshot.run.history[entry.historyIndex];
|
|
46
|
+
if (!historyEntry) return undefined;
|
|
47
|
+
return {
|
|
48
|
+
entryIndex: entry.index,
|
|
49
|
+
historyEntry,
|
|
50
|
+
attempts: historyEntry.attempts ?? [],
|
|
51
|
+
omittedAttempts: historyEntry.omittedAttempts ?? 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
entryIndex: entry.index,
|
|
56
|
+
attempts: snapshot.run.currentStepAttempts ?? [],
|
|
57
|
+
omittedAttempts: snapshot.run.currentStepOmittedAttempts ?? 0,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function wrapPlain(
|
|
62
|
+
value: string,
|
|
63
|
+
width: number,
|
|
64
|
+
theme: WorkflowStatusTheme,
|
|
65
|
+
color: 'text' | 'muted' | 'dim' | 'warning' = 'text',
|
|
66
|
+
): Array<string> {
|
|
67
|
+
const sanitized = redactStepDetailText(value);
|
|
68
|
+
const logicalLines = sanitized.split('\n');
|
|
69
|
+
const wrapped = logicalLines.flatMap((line) => {
|
|
70
|
+
const themed = theme.fg(color, line || ' ');
|
|
71
|
+
return wrapTextWithAnsi(themed, Math.max(1, width));
|
|
72
|
+
});
|
|
73
|
+
return wrapped.length > 0 ? wrapped : [theme.fg(color, ' ')];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function boundedArtifact(value: string): {
|
|
77
|
+
readonly value: string;
|
|
78
|
+
readonly truncated: boolean;
|
|
79
|
+
} {
|
|
80
|
+
return value.length <= MAX_DETAIL_ARTIFACT_CHARS
|
|
81
|
+
? { value, truncated: false }
|
|
82
|
+
: {
|
|
83
|
+
value: value.slice(0, MAX_DETAIL_ARTIFACT_CHARS),
|
|
84
|
+
truncated: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderResult(
|
|
89
|
+
theme: WorkflowStatusTheme,
|
|
90
|
+
attempt: StepExecutionAttempt,
|
|
91
|
+
width: number,
|
|
92
|
+
): Array<string> {
|
|
93
|
+
const result = attempt.result;
|
|
94
|
+
if (!result) return [];
|
|
95
|
+
const artifact =
|
|
96
|
+
result.artifact === undefined
|
|
97
|
+
? undefined
|
|
98
|
+
: boundedArtifact(result.artifact);
|
|
99
|
+
return [
|
|
100
|
+
'',
|
|
101
|
+
theme.bold(theme.fg('accent', 'Submitted result')),
|
|
102
|
+
...keyValueLines(theme, 'outcome', result.outcome, width, 'success'),
|
|
103
|
+
...keyValueLines(
|
|
104
|
+
theme,
|
|
105
|
+
'summary',
|
|
106
|
+
`${result.summary}${result.summaryTruncated ? ' … [trace truncated]' : ''}`,
|
|
107
|
+
width,
|
|
108
|
+
),
|
|
109
|
+
...(result.workspaceCwd
|
|
110
|
+
? keyValueLines(theme, 'workspace', result.workspaceCwd, width)
|
|
111
|
+
: []),
|
|
112
|
+
...(artifact
|
|
113
|
+
? [
|
|
114
|
+
theme.fg('muted', 'artifact'),
|
|
115
|
+
...wrapPlain(
|
|
116
|
+
`${artifact.value}${
|
|
117
|
+
artifact.truncated || result.artifactTruncated
|
|
118
|
+
? '\n… [artifact truncated in status trace]'
|
|
119
|
+
: ''
|
|
120
|
+
}`,
|
|
121
|
+
width,
|
|
122
|
+
theme,
|
|
123
|
+
),
|
|
124
|
+
]
|
|
125
|
+
: []),
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderGateDecision(
|
|
130
|
+
theme: WorkflowStatusTheme,
|
|
131
|
+
attempt: StepExecutionAttempt,
|
|
132
|
+
width: number,
|
|
133
|
+
): Array<string> {
|
|
134
|
+
const decision = attempt.gateDecision;
|
|
135
|
+
if (!decision) return [];
|
|
136
|
+
return [
|
|
137
|
+
'',
|
|
138
|
+
theme.bold(theme.fg('accent', 'Human gate decision')),
|
|
139
|
+
...keyValueLines(theme, 'provider', decision.provider, width),
|
|
140
|
+
...keyValueLines(
|
|
141
|
+
theme,
|
|
142
|
+
'decision',
|
|
143
|
+
decision.approved ? 'approved' : 'rejected',
|
|
144
|
+
width,
|
|
145
|
+
decision.approved ? 'success' : 'warning',
|
|
146
|
+
),
|
|
147
|
+
...(decision.reviewId
|
|
148
|
+
? keyValueLines(theme, 'review', decision.reviewId, width, 'muted')
|
|
149
|
+
: []),
|
|
150
|
+
...(decision.feedback
|
|
151
|
+
? keyValueLines(
|
|
152
|
+
theme,
|
|
153
|
+
'feedback',
|
|
154
|
+
`${decision.feedback}${
|
|
155
|
+
decision.feedbackTruncated ? ' … [trace truncated]' : ''
|
|
156
|
+
}`,
|
|
157
|
+
width,
|
|
158
|
+
)
|
|
159
|
+
: []),
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function renderTranscript(
|
|
164
|
+
theme: WorkflowStatusTheme,
|
|
165
|
+
snapshot: WorkflowStatusSnapshot,
|
|
166
|
+
attempt: StepExecutionAttempt,
|
|
167
|
+
cache: StepTranscriptViewCache,
|
|
168
|
+
width: number,
|
|
169
|
+
): Array<string> {
|
|
170
|
+
if (attempt.kind === 'main') {
|
|
171
|
+
const events = (attempt.log ?? []).flatMap((event, index) => [
|
|
172
|
+
theme.fg('muted', `#${index + 1}`),
|
|
173
|
+
...wrapPlain(event, width, theme),
|
|
174
|
+
]);
|
|
175
|
+
return [
|
|
176
|
+
...(events.length > 0
|
|
177
|
+
? events
|
|
178
|
+
: [
|
|
179
|
+
theme.fg(
|
|
180
|
+
'muted',
|
|
181
|
+
'No main-agent reaction log was recorded for this attempt. Legacy checkpoints still show the exact task and result.',
|
|
182
|
+
),
|
|
183
|
+
]),
|
|
184
|
+
...(attempt.logTruncated
|
|
185
|
+
? [
|
|
186
|
+
theme.fg(
|
|
187
|
+
'warning',
|
|
188
|
+
`… Main-agent log was bounded; ${attempt.omittedLogEvents ?? 0} later event${attempt.omittedLogEvents === 1 ? '' : 's'} omitted.`,
|
|
189
|
+
),
|
|
190
|
+
]
|
|
191
|
+
: []),
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
if (!attempt.transcript) {
|
|
195
|
+
return wrapPlain(
|
|
196
|
+
'No trusted child transcript reference was recorded for this attempt.',
|
|
197
|
+
width,
|
|
198
|
+
theme,
|
|
199
|
+
'muted',
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const state = cache.get(stepTranscriptCacheKey(snapshot.run.runId, attempt));
|
|
203
|
+
if (!state || state.status === 'loading') {
|
|
204
|
+
return [theme.fg('muted', 'Loading trusted child transcript…')];
|
|
205
|
+
}
|
|
206
|
+
if (state.status === 'unavailable') {
|
|
207
|
+
return wrapPlain(state.reason, width, theme, 'warning');
|
|
208
|
+
}
|
|
209
|
+
const events = state.lines.flatMap((event, index) => [
|
|
210
|
+
theme.fg('muted', `#${index + 1}`),
|
|
211
|
+
...wrapPlain(event, width, theme),
|
|
212
|
+
]);
|
|
213
|
+
return [
|
|
214
|
+
...(events.length > 0
|
|
215
|
+
? events
|
|
216
|
+
: [theme.fg('muted', 'No displayable assistant or tool events found.')]),
|
|
217
|
+
...(state.truncated
|
|
218
|
+
? [
|
|
219
|
+
theme.fg(
|
|
220
|
+
'warning',
|
|
221
|
+
'… Transcript display was bounded; additional events were omitted.',
|
|
222
|
+
),
|
|
223
|
+
]
|
|
224
|
+
: []),
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function renderAttempt(
|
|
229
|
+
theme: WorkflowStatusTheme,
|
|
230
|
+
snapshot: WorkflowStatusSnapshot,
|
|
231
|
+
attempt: StepExecutionAttempt,
|
|
232
|
+
attemptNumber: number,
|
|
233
|
+
cache: StepTranscriptViewCache,
|
|
234
|
+
width: number,
|
|
235
|
+
): Array<string> {
|
|
236
|
+
const actor =
|
|
237
|
+
attempt.kind === 'subagent' ? `subagent · ${attempt.agent}` : 'main agent';
|
|
238
|
+
const truncation = attempt.taskTruncated
|
|
239
|
+
? `\n… [${attempt.omittedTaskChars ?? 0} task characters omitted from the bounded checkpoint trace]`
|
|
240
|
+
: '';
|
|
241
|
+
return [
|
|
242
|
+
theme.bold(theme.fg('accent', `Attempt ${attemptNumber} · ${actor}`)),
|
|
243
|
+
...keyValueLines(theme, 'request', attempt.requestId, width, 'muted'),
|
|
244
|
+
'',
|
|
245
|
+
theme.bold('Requirement fed to the agent'),
|
|
246
|
+
...wrapPlain(`${attempt.task}${truncation}`, width, theme),
|
|
247
|
+
'',
|
|
248
|
+
theme.bold('Chronological reaction and tool log'),
|
|
249
|
+
...renderTranscript(theme, snapshot, attempt, cache, width),
|
|
250
|
+
...renderResult(theme, attempt, width),
|
|
251
|
+
...renderGateDecision(theme, attempt, width),
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function attemptDisplayOrdinal(
|
|
256
|
+
attempt: StepExecutionAttempt,
|
|
257
|
+
index: number,
|
|
258
|
+
omittedAttempts: number,
|
|
259
|
+
): number {
|
|
260
|
+
if (attempt.ordinal !== undefined) return attempt.ordinal;
|
|
261
|
+
if (omittedAttempts === 0 || index === 0) return index + 1;
|
|
262
|
+
return omittedAttempts + index + 1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Renders one selected execution-path row and its persisted evidence. */
|
|
266
|
+
export function renderStepDetail(
|
|
267
|
+
theme: WorkflowStatusTheme,
|
|
268
|
+
snapshot: WorkflowStatusSnapshot,
|
|
269
|
+
selectedIndex: number,
|
|
270
|
+
cache: StepTranscriptViewCache,
|
|
271
|
+
width: number,
|
|
272
|
+
): Array<string> {
|
|
273
|
+
const entries = buildPathEntries(snapshot);
|
|
274
|
+
const entry = entries[selectedIndex];
|
|
275
|
+
const detail = selectedStepDetail(snapshot, selectedIndex);
|
|
276
|
+
if (!entry || !detail) {
|
|
277
|
+
return boxed(
|
|
278
|
+
theme,
|
|
279
|
+
'Step Explorer',
|
|
280
|
+
width,
|
|
281
|
+
[theme.fg('muted', 'The selected step is no longer available.')],
|
|
282
|
+
'borderAccent',
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const history = detail.historyEntry;
|
|
287
|
+
const finalArtifact =
|
|
288
|
+
history?.artifact ??
|
|
289
|
+
(entry.isCurrent ? snapshot.run.pendingGate?.artifact : undefined);
|
|
290
|
+
const artifact =
|
|
291
|
+
finalArtifact === undefined ? undefined : boundedArtifact(finalArtifact);
|
|
292
|
+
const attempts = detail.attempts.flatMap((attempt, index) => [
|
|
293
|
+
...(index > 0 ? ['', theme.fg('dim', '─'.repeat(Math.max(1, width)))] : []),
|
|
294
|
+
...renderAttempt(
|
|
295
|
+
theme,
|
|
296
|
+
snapshot,
|
|
297
|
+
attempt,
|
|
298
|
+
attemptDisplayOrdinal(attempt, index, detail.omittedAttempts),
|
|
299
|
+
cache,
|
|
300
|
+
width,
|
|
301
|
+
),
|
|
302
|
+
]);
|
|
303
|
+
const currentReason =
|
|
304
|
+
entry.isCurrent && snapshot.run.pauseReason
|
|
305
|
+
? snapshot.run.pauseReason
|
|
306
|
+
: undefined;
|
|
307
|
+
const body = [
|
|
308
|
+
...keyValueLines(theme, 'workflow', snapshot.run.workflowId, width),
|
|
309
|
+
...keyValueLines(theme, 'run', snapshot.run.runId, width, 'muted'),
|
|
310
|
+
...keyValueLines(theme, 'step', entry.stepId, width),
|
|
311
|
+
...keyValueLines(theme, 'visit', String(entry.visit), width),
|
|
312
|
+
...keyValueLines(theme, 'status', entry.status, width),
|
|
313
|
+
...(history
|
|
314
|
+
? [
|
|
315
|
+
...keyValueLines(theme, 'outcome', history.outcome, width, 'success'),
|
|
316
|
+
...keyValueLines(theme, 'summary', history.summary, width),
|
|
317
|
+
]
|
|
318
|
+
: []),
|
|
319
|
+
...(currentReason
|
|
320
|
+
? keyValueLines(theme, 'reason', currentReason, width, 'warning')
|
|
321
|
+
: []),
|
|
322
|
+
...(entry.isCurrent && snapshot.run.gateFeedback
|
|
323
|
+
? keyValueLines(
|
|
324
|
+
theme,
|
|
325
|
+
'feedback',
|
|
326
|
+
snapshot.run.gateFeedback,
|
|
327
|
+
width,
|
|
328
|
+
'warning',
|
|
329
|
+
)
|
|
330
|
+
: []),
|
|
331
|
+
...(history?.workspaceCwd
|
|
332
|
+
? keyValueLines(theme, 'workspace', history.workspaceCwd, width)
|
|
333
|
+
: entry.isCurrent && snapshot.run.cwd
|
|
334
|
+
? keyValueLines(theme, 'workspace', snapshot.run.cwd, width)
|
|
335
|
+
: []),
|
|
336
|
+
...(artifact
|
|
337
|
+
? [
|
|
338
|
+
'',
|
|
339
|
+
theme.bold('Final/review artifact'),
|
|
340
|
+
...wrapPlain(
|
|
341
|
+
`${artifact.value}${
|
|
342
|
+
artifact.truncated
|
|
343
|
+
? '\n… [artifact truncated in status display]'
|
|
344
|
+
: ''
|
|
345
|
+
}`,
|
|
346
|
+
width,
|
|
347
|
+
theme,
|
|
348
|
+
),
|
|
349
|
+
]
|
|
350
|
+
: []),
|
|
351
|
+
'',
|
|
352
|
+
...(detail.omittedAttempts > 0
|
|
353
|
+
? [
|
|
354
|
+
theme.fg(
|
|
355
|
+
'warning',
|
|
356
|
+
`${detail.omittedAttempts} attempt${
|
|
357
|
+
detail.omittedAttempts === 1 ? ' was' : 's were'
|
|
358
|
+
} compacted from the bounded checkpoint trace; retained attempt numbers preserve the chronological gap.`,
|
|
359
|
+
),
|
|
360
|
+
'',
|
|
361
|
+
]
|
|
362
|
+
: []),
|
|
363
|
+
...(attempts.length > 0
|
|
364
|
+
? attempts
|
|
365
|
+
: [
|
|
366
|
+
theme.fg(
|
|
367
|
+
'muted',
|
|
368
|
+
'No execution trace was recorded. This step may predate the step-explorer feature.',
|
|
369
|
+
),
|
|
370
|
+
]),
|
|
371
|
+
];
|
|
372
|
+
return boxed(
|
|
373
|
+
theme,
|
|
374
|
+
`Step Explorer · ${entry.title} · visit ${entry.visit}`,
|
|
375
|
+
width,
|
|
376
|
+
body,
|
|
377
|
+
'borderAccent',
|
|
378
|
+
);
|
|
379
|
+
}
|
|
@@ -25,9 +25,11 @@ export function renderHeaderLines(
|
|
|
25
25
|
width: number,
|
|
26
26
|
): Array<string> {
|
|
27
27
|
const { run } = snapshot;
|
|
28
|
+
const command = snapshot.workflow?.definition.command;
|
|
28
29
|
const firstLine = [
|
|
29
30
|
statusGlyph(theme, runDisplayStatus(run), snapshot.now),
|
|
30
31
|
theme.bold(inline(run.workflowId)),
|
|
32
|
+
...(command ? [theme.fg('accent', `/${inline(command)}`)] : []),
|
|
31
33
|
statusBadge(theme, run.status),
|
|
32
34
|
theme.fg('muted', '·'),
|
|
33
35
|
theme.fg(
|
|
@@ -62,6 +64,23 @@ export function renderSummaryLines(
|
|
|
62
64
|
const { run, workflow } = snapshot;
|
|
63
65
|
const lines = [
|
|
64
66
|
...keyValueLines(theme, 'workflow', run.workflowId, width),
|
|
67
|
+
...(workflow
|
|
68
|
+
? [
|
|
69
|
+
...keyValueLines(
|
|
70
|
+
theme,
|
|
71
|
+
'command',
|
|
72
|
+
`/${workflow.definition.command}`,
|
|
73
|
+
width,
|
|
74
|
+
'accent',
|
|
75
|
+
),
|
|
76
|
+
...keyValueLines(
|
|
77
|
+
theme,
|
|
78
|
+
'about',
|
|
79
|
+
workflow.definition.description,
|
|
80
|
+
width,
|
|
81
|
+
),
|
|
82
|
+
]
|
|
83
|
+
: []),
|
|
65
84
|
...keyValueLines(theme, 'run', run.runId, width),
|
|
66
85
|
...keyValueLines(
|
|
67
86
|
theme,
|
|
@@ -91,6 +110,9 @@ export function renderSummaryLines(
|
|
|
91
110
|
),
|
|
92
111
|
];
|
|
93
112
|
|
|
113
|
+
if (run.cwd && run.startCwd && run.cwd !== run.startCwd) {
|
|
114
|
+
lines.push(...keyValueLines(theme, 'workspace', run.cwd, width, 'accent'));
|
|
115
|
+
}
|
|
94
116
|
const execution = formatExecution(snapshot.execution);
|
|
95
117
|
if (execution) {
|
|
96
118
|
lines.push(
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open, realpath } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
MAX_STEP_TRACE_LOG_CHARS,
|
|
6
|
+
MAX_STEP_TRACE_LOG_EVENTS,
|
|
7
|
+
type SubagentTranscriptReference,
|
|
8
|
+
} from '../engine/state.ts';
|
|
9
|
+
import {
|
|
10
|
+
redactStepLogText,
|
|
11
|
+
redactStepLogValue,
|
|
12
|
+
sanitizeStepLogText,
|
|
13
|
+
stepLogLinesFromMessage,
|
|
14
|
+
} from '../step-log.ts';
|
|
15
|
+
|
|
16
|
+
const MAX_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
export type StepTranscriptLog =
|
|
19
|
+
| {
|
|
20
|
+
readonly status: 'available';
|
|
21
|
+
readonly lines: ReadonlyArray<string>;
|
|
22
|
+
readonly truncated: boolean;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
readonly status: 'unavailable';
|
|
26
|
+
readonly reason: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type UnknownRecord = Readonly<Record<string, unknown>>;
|
|
30
|
+
|
|
31
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
32
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isWithin(root: string, candidate: string): boolean {
|
|
36
|
+
const pathFromRoot = relative(resolve(root), resolve(candidate));
|
|
37
|
+
return (
|
|
38
|
+
pathFromRoot !== '' &&
|
|
39
|
+
pathFromRoot !== '..' &&
|
|
40
|
+
!pathFromRoot.startsWith(`..${sep}`) &&
|
|
41
|
+
!isAbsolute(pathFromRoot)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasSafeIdentity(reference: SubagentTranscriptReference): boolean {
|
|
46
|
+
return (
|
|
47
|
+
isAbsolute(reference.trustedRoot) &&
|
|
48
|
+
isAbsolute(reference.sessionFile) &&
|
|
49
|
+
!reference.trustedRoot.includes('\0') &&
|
|
50
|
+
!reference.sessionFile.includes('\0') &&
|
|
51
|
+
reference.runId.length > 0 &&
|
|
52
|
+
!reference.runId.includes('\0') &&
|
|
53
|
+
!reference.runId.includes('/') &&
|
|
54
|
+
!reference.runId.includes('\\') &&
|
|
55
|
+
reference.runId !== '.' &&
|
|
56
|
+
reference.runId !== '..' &&
|
|
57
|
+
Number.isSafeInteger(reference.childIndex) &&
|
|
58
|
+
reference.childIndex >= 0 &&
|
|
59
|
+
resolve(reference.sessionFile) ===
|
|
60
|
+
resolve(
|
|
61
|
+
reference.trustedRoot,
|
|
62
|
+
reference.runId,
|
|
63
|
+
`run-${reference.childIndex}`,
|
|
64
|
+
'session.jsonl',
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
redactStepDetailText as redactStatusDetailText,
|
|
71
|
+
redactStepLogText as redactStatusLogText,
|
|
72
|
+
sanitizeStepLogText as sanitizeStatusLogText,
|
|
73
|
+
} from '../step-log.ts';
|
|
74
|
+
|
|
75
|
+
function transcriptEntryLines(entry: unknown): Array<string> {
|
|
76
|
+
if (!isRecord(entry)) return [];
|
|
77
|
+
if (entry.type === 'custom_message') {
|
|
78
|
+
const customType =
|
|
79
|
+
typeof entry.customType === 'string' ? entry.customType : 'custom';
|
|
80
|
+
const content =
|
|
81
|
+
typeof entry.content === 'string'
|
|
82
|
+
? redactStepLogText(entry.content)
|
|
83
|
+
: redactStepLogValue(entry.details);
|
|
84
|
+
return content
|
|
85
|
+
? [`event ${sanitizeStepLogText(customType)}\n${content}`]
|
|
86
|
+
: [];
|
|
87
|
+
}
|
|
88
|
+
if (entry.type !== 'message' || !isRecord(entry.message)) return [];
|
|
89
|
+
return stepLogLinesFromMessage(entry.message);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseTranscript(
|
|
93
|
+
content: string,
|
|
94
|
+
sourceTruncated: boolean,
|
|
95
|
+
): StepTranscriptLog {
|
|
96
|
+
const lines: Array<string> = [];
|
|
97
|
+
let chars = 0;
|
|
98
|
+
let truncated = sourceTruncated;
|
|
99
|
+
for (const rawLine of content.split('\n')) {
|
|
100
|
+
if (!rawLine.trim()) continue;
|
|
101
|
+
let entry: unknown;
|
|
102
|
+
try {
|
|
103
|
+
entry = JSON.parse(rawLine);
|
|
104
|
+
} catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for (const line of transcriptEntryLines(entry)) {
|
|
108
|
+
if (
|
|
109
|
+
lines.length >= MAX_STEP_TRACE_LOG_EVENTS ||
|
|
110
|
+
chars + line.length > MAX_STEP_TRACE_LOG_CHARS
|
|
111
|
+
) {
|
|
112
|
+
truncated = true;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
lines.push(line);
|
|
116
|
+
chars += line.length;
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
lines.length >= MAX_STEP_TRACE_LOG_EVENTS ||
|
|
120
|
+
chars >= MAX_STEP_TRACE_LOG_CHARS
|
|
121
|
+
) {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { status: 'available', lines, truncated };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Reads one child transcript through a confined, no-follow, stable prefix.
|
|
130
|
+
*
|
|
131
|
+
* The private child-policy user message is never rendered. Callers display
|
|
132
|
+
* the separately persisted policy-envelope-free task body instead.
|
|
133
|
+
*/
|
|
134
|
+
export async function readStepTranscript(
|
|
135
|
+
reference: SubagentTranscriptReference,
|
|
136
|
+
): Promise<StepTranscriptLog> {
|
|
137
|
+
if (!hasSafeIdentity(reference)) {
|
|
138
|
+
return {
|
|
139
|
+
status: 'unavailable',
|
|
140
|
+
reason: 'The recorded child transcript identity is invalid.',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const runDirectory = resolve(reference.trustedRoot, reference.runId);
|
|
145
|
+
const childDirectory = resolve(runDirectory, `run-${reference.childIndex}`);
|
|
146
|
+
const [runDirectoryInfo, childDirectoryInfo] = await Promise.all([
|
|
147
|
+
lstat(runDirectory),
|
|
148
|
+
lstat(childDirectory),
|
|
149
|
+
]);
|
|
150
|
+
if (
|
|
151
|
+
runDirectoryInfo.isSymbolicLink() ||
|
|
152
|
+
!runDirectoryInfo.isDirectory() ||
|
|
153
|
+
childDirectoryInfo.isSymbolicLink() ||
|
|
154
|
+
!childDirectoryInfo.isDirectory()
|
|
155
|
+
) {
|
|
156
|
+
return {
|
|
157
|
+
status: 'unavailable',
|
|
158
|
+
reason:
|
|
159
|
+
'The recorded child transcript directory identity is not trusted.',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const inspected = await lstat(reference.sessionFile);
|
|
163
|
+
if (inspected.isSymbolicLink() || !inspected.isFile()) {
|
|
164
|
+
return {
|
|
165
|
+
status: 'unavailable',
|
|
166
|
+
reason: 'The recorded child transcript is not a regular file.',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const [canonicalRoot, canonicalSession] = await Promise.all([
|
|
170
|
+
realpath(reference.trustedRoot),
|
|
171
|
+
realpath(reference.sessionFile),
|
|
172
|
+
]);
|
|
173
|
+
const canonicalExpected = resolve(
|
|
174
|
+
canonicalRoot,
|
|
175
|
+
reference.runId,
|
|
176
|
+
`run-${reference.childIndex}`,
|
|
177
|
+
'session.jsonl',
|
|
178
|
+
);
|
|
179
|
+
if (
|
|
180
|
+
!isWithin(canonicalRoot, canonicalSession) ||
|
|
181
|
+
canonicalSession !== canonicalExpected
|
|
182
|
+
) {
|
|
183
|
+
return {
|
|
184
|
+
status: 'unavailable',
|
|
185
|
+
reason:
|
|
186
|
+
'The recorded child transcript does not match its trusted canonical identity.',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const handle = await open(
|
|
191
|
+
canonicalSession,
|
|
192
|
+
constants.O_RDONLY | constants.O_NOFOLLOW,
|
|
193
|
+
);
|
|
194
|
+
try {
|
|
195
|
+
const before = await handle.stat();
|
|
196
|
+
if (!before.isFile()) {
|
|
197
|
+
return {
|
|
198
|
+
status: 'unavailable',
|
|
199
|
+
reason: 'The recorded child transcript is not readable.',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const bytesToRead = Math.min(before.size, MAX_TRANSCRIPT_BYTES);
|
|
203
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
204
|
+
const { bytesRead } = await handle.read(buffer, 0, bytesToRead, 0);
|
|
205
|
+
const after = await handle.stat();
|
|
206
|
+
if (
|
|
207
|
+
after.dev !== before.dev ||
|
|
208
|
+
after.ino !== before.ino ||
|
|
209
|
+
after.size !== before.size ||
|
|
210
|
+
after.mtimeMs !== before.mtimeMs
|
|
211
|
+
) {
|
|
212
|
+
return {
|
|
213
|
+
status: 'unavailable',
|
|
214
|
+
reason: 'The child transcript changed while it was being read.',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return parseTranscript(
|
|
218
|
+
buffer.subarray(0, bytesRead).toString('utf8'),
|
|
219
|
+
before.size > bytesToRead,
|
|
220
|
+
);
|
|
221
|
+
} finally {
|
|
222
|
+
await handle.close();
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
return {
|
|
226
|
+
status: 'unavailable',
|
|
227
|
+
reason:
|
|
228
|
+
'The recorded child transcript is missing or cannot be read safely.',
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -2,6 +2,8 @@ import type { Theme, ThemeColor } from '@earendil-works/pi-coding-agent';
|
|
|
2
2
|
import type { TUI } from '@earendil-works/pi-tui';
|
|
3
3
|
import type { LoadedWorkflow } from '../config/types.ts';
|
|
4
4
|
import type { WorkflowRun, WorkflowRunStatus } from '../engine/state.ts';
|
|
5
|
+
import type { StepExecutionAttempt } from '../engine/state.ts';
|
|
6
|
+
import type { StepTranscriptLog } from './transcript-reader.ts';
|
|
5
7
|
|
|
6
8
|
export type WorkflowStatusExecution =
|
|
7
9
|
| {
|
|
@@ -28,6 +30,8 @@ export type StatusViewTui = Pick<TUI, 'requestRender'> & {
|
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
export type PathEntry = {
|
|
33
|
+
readonly index: number;
|
|
34
|
+
readonly historyIndex?: number;
|
|
31
35
|
readonly stepId: string;
|
|
32
36
|
readonly title: string;
|
|
33
37
|
readonly status: StepDisplayStatus;
|
|
@@ -36,5 +40,9 @@ export type PathEntry = {
|
|
|
36
40
|
readonly isCurrent: boolean;
|
|
37
41
|
};
|
|
38
42
|
|
|
43
|
+
export type StepTranscriptLoader = (
|
|
44
|
+
attempt: Extract<StepExecutionAttempt, { readonly kind: 'subagent' }>,
|
|
45
|
+
) => Promise<StepTranscriptLog>;
|
|
46
|
+
|
|
39
47
|
export type WorkflowStatusThemeColor = ThemeColor;
|
|
40
48
|
export type WorkflowStatusTheme = Pick<Theme, 'fg' | 'bg' | 'bold'>;
|