@wichayutdew/pi-workflows 1.0.0 → 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 -221
- package/agents/step.md +4 -11
- package/dist/index.js +2556 -961
- package/examples/mr-comments.workflow.yaml +18 -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 -35
- package/src/command-names.ts +1 -0
- package/src/commands.ts +33 -4
- package/src/config/ceiling.ts +6 -8
- package/src/config/load-workflows.ts +12 -1
- package/src/config/step-digests.ts +19 -0
- package/src/config/types.ts +17 -11
- package/src/config/validation/permissions.ts +5 -45
- package/src/config/validation/prompt.ts +3 -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 +4 -0
- package/src/engine/gate-transitions.ts +58 -6
- 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 +236 -0
- package/src/engine/run-workflow-validation.ts +191 -0
- package/src/engine/state-types.ts +109 -4
- 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 -72
- 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 +5 -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
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import type { BashPermission, BashRule } from '../config/types.ts';
|
|
3
|
-
import {
|
|
4
|
-
authorizeHostedApiRead,
|
|
5
|
-
authorizeReadOnlyBash,
|
|
6
|
-
usesReadOnlyBashPreset,
|
|
7
|
-
} from './bash-read-only.ts';
|
|
8
3
|
import type { BashAuthorization } from './bash-types.ts';
|
|
9
4
|
import { tokenizeRestrictedCommand } from './restricted-command.ts';
|
|
10
5
|
|
|
@@ -34,23 +29,15 @@ const matchesRule = (tokens: ReadonlyArray<string>, rule: BashRule): boolean =>
|
|
|
34
29
|
/**
|
|
35
30
|
* Authorizes a Bash command for a workflow step.
|
|
36
31
|
*
|
|
37
|
-
* Exact reviewed commands are considered first, followed by the configured
|
|
38
|
-
* terminal mode and its static restrictions.
|
|
39
|
-
*
|
|
40
32
|
* @param command - Bash command text.
|
|
41
33
|
* @param permission - Bash permission configured for the active step.
|
|
42
|
-
* @param approvedCommands - Exact commands derived from a reviewed artifact.
|
|
43
34
|
* @returns The authorization decision.
|
|
44
35
|
*/
|
|
45
36
|
export const authorizeBash = (
|
|
46
37
|
command: string,
|
|
47
38
|
permission: BashPermission,
|
|
48
|
-
approvedCommands: ReadonlyArray<string> = [],
|
|
49
39
|
): BashAuthorization => {
|
|
50
|
-
|
|
51
|
-
(permission.approvedSources?.length ?? 0) > 0 &&
|
|
52
|
-
approvedCommands.includes(command);
|
|
53
|
-
if (isReviewedCommand || permission.mode === 'unrestricted') {
|
|
40
|
+
if (permission.mode === 'unrestricted') {
|
|
54
41
|
return { allowed: true };
|
|
55
42
|
}
|
|
56
43
|
if (permission.mode === 'deny') {
|
|
@@ -70,17 +57,11 @@ export const authorizeBash = (
|
|
|
70
57
|
return reject('environment assignments are not allowed in restricted mode');
|
|
71
58
|
}
|
|
72
59
|
|
|
73
|
-
if (permission.mode === 'read-only') {
|
|
74
|
-
return authorizeReadOnlyBash(parsed.tokens);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
60
|
const rule = permission.allow.find((candidate) =>
|
|
78
61
|
matchesRule(parsed.tokens, candidate),
|
|
79
62
|
);
|
|
80
63
|
if (!rule) {
|
|
81
64
|
return reject("command does not match this step's Bash allow-list");
|
|
82
65
|
}
|
|
83
|
-
return
|
|
84
|
-
? authorizeReadOnlyBash(parsed.tokens)
|
|
85
|
-
: authorizeHostedApiRead(parsed.tokens);
|
|
66
|
+
return { allowed: true, tokens: [...parsed.tokens] };
|
|
86
67
|
};
|
package/src/policy/bash-types.ts
CHANGED
package/src/policy/bash.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export { authorizeBash } from './bash-authorization.ts';
|
|
2
|
-
export type { BashAuthorization
|
|
2
|
+
export type { BashAuthorization } from './bash-types.ts';
|
|
3
3
|
export { tokenizeRestrictedCommand } from './restricted-command.ts';
|
|
4
|
-
export { parseRestrictedGitCommand } from './restricted-git.ts';
|
|
@@ -19,7 +19,6 @@ const reject = (reason: string): ToolAuthorization => ({
|
|
|
19
19
|
* @param input - Requested tool input.
|
|
20
20
|
* @param step - Active workflow step.
|
|
21
21
|
* @param inventory - Registered tool inventory.
|
|
22
|
-
* @param approvedBashCommands - Exact commands derived from human review.
|
|
23
22
|
* @returns The authorization decision.
|
|
24
23
|
*/
|
|
25
24
|
export const authorizeToolCall = (
|
|
@@ -27,7 +26,6 @@ export const authorizeToolCall = (
|
|
|
27
26
|
input: Readonly<Record<string, unknown>>,
|
|
28
27
|
step: WorkflowStep,
|
|
29
28
|
inventory: ReadonlyArray<ToolInventoryItem>,
|
|
30
|
-
approvedBashCommands: ReadonlyArray<string> = [],
|
|
31
29
|
): ToolAuthorization => {
|
|
32
30
|
if (toolName === 'mcp') {
|
|
33
31
|
return authorizeMcpProxy(input, step.permissions.mcp);
|
|
@@ -47,11 +45,7 @@ export const authorizeToolCall = (
|
|
|
47
45
|
if (typeof command !== 'string') {
|
|
48
46
|
return reject('Bash call is missing command text');
|
|
49
47
|
}
|
|
50
|
-
const result = authorizeBash(
|
|
51
|
-
command,
|
|
52
|
-
step.permissions.bash,
|
|
53
|
-
approvedBashCommands,
|
|
54
|
-
);
|
|
48
|
+
const result = authorizeBash(command, step.permissions.bash);
|
|
55
49
|
return result.allowed
|
|
56
50
|
? { allowed: true }
|
|
57
51
|
: reject(result.reason ?? 'Bash command is not allowed');
|
package/src/preflight.ts
CHANGED
|
@@ -87,11 +87,6 @@ export function preflightStep(
|
|
|
87
87
|
hasResource: (toolName) => toolNames.has(toolName),
|
|
88
88
|
resourceKind: 'tool',
|
|
89
89
|
}),
|
|
90
|
-
...(step.permissions.mcp.length > 0 && !toolNames.has('mcp')
|
|
91
|
-
? [
|
|
92
|
-
'MCP selectors are configured, but the "mcp" proxy tool is not installed',
|
|
93
|
-
]
|
|
94
|
-
: []),
|
|
95
90
|
...(isPlannotatorRequired && !hasExtension('plannotator')
|
|
96
91
|
? [
|
|
97
92
|
'Plannotator is required by this gate, but its extension is not installed or detectable',
|
package/src/prompt/retry-task.ts
CHANGED
|
@@ -46,6 +46,9 @@ export function automaticRecoveryTask(
|
|
|
46
46
|
maxAttempts: number,
|
|
47
47
|
): string {
|
|
48
48
|
const diagnostics = serializeDiagnostics(reasons);
|
|
49
|
+
const missingStructuredOutput = reasons.some((reason) =>
|
|
50
|
+
reason.includes('Missing structured_output call'),
|
|
51
|
+
);
|
|
49
52
|
|
|
50
53
|
return [
|
|
51
54
|
'## Automatic recovery after subagent failure',
|
|
@@ -57,10 +60,15 @@ export function automaticRecoveryTask(
|
|
|
57
60
|
'</pi-workflows-retry-diagnostic-v1>',
|
|
58
61
|
'',
|
|
59
62
|
'Diagnose and resolve the specific causes before completing the original step. Treat every listed approach as already attempted. When `Failed tool`, `Command` or `Arguments`, and `Tool error` are present, use them to choose a permitted alternative; do not repeat a failing call unchanged.',
|
|
60
|
-
'This is a continuation, not a blind replay. Inspect current
|
|
61
|
-
'Keep working after a successful recovery and complete the original step
|
|
63
|
+
'This is a continuation, not a blind replay. Inspect current state first, assume a prior call may already have applied its effect, and do not repeat a side effect that is already present.',
|
|
64
|
+
'Keep working after a successful recovery and complete the original step according to its configured prompt and outcomes.',
|
|
65
|
+
...(missingStructuredOutput
|
|
66
|
+
? [
|
|
67
|
+
'A prior child ended without the required `structured_output` call. After completing the original work, call `structured_output` exactly once as the only tool call in its message; a prose final response does not complete this workflow step.',
|
|
68
|
+
]
|
|
69
|
+
: []),
|
|
62
70
|
'Use only tools enabled for this step. If the named tool is unavailable, use an enabled alternative. In restricted Bash modes, use one allowed command per tool call; do not use shell operators, substitutions, escapes in double quotes, environment assignments, or wrappers.',
|
|
63
|
-
'If no permitted alternative resolves the failure, follow the step
|
|
71
|
+
'If no permitted alternative resolves the failure, follow the step prompt when choosing a configured outcome; the engine assigns no special meaning to outcome names. Include the exact failed call, exact error, alternatives attempted, and observed state in the handoff.',
|
|
64
72
|
].join('\n');
|
|
65
73
|
}
|
|
66
74
|
|
|
@@ -9,7 +9,7 @@ export type StepContract = {
|
|
|
9
9
|
readonly outcomes: ReadonlyArray<string>;
|
|
10
10
|
readonly transitionLines: string;
|
|
11
11
|
readonly gateLine: string;
|
|
12
|
-
readonly
|
|
12
|
+
readonly workspaceLines: ReadonlyArray<string>;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
type CreateStepContractOptions = {
|
|
@@ -18,57 +18,8 @@ type CreateStepContractOptions = {
|
|
|
18
18
|
readonly step: WorkflowStep;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
const pauseOutcomesFor = (
|
|
22
|
-
step: WorkflowStep,
|
|
23
|
-
allowedOutcomeSet: ReadonlySet<string>,
|
|
24
|
-
): ReadonlyArray<string> =>
|
|
25
|
-
Object.entries(step.transitions)
|
|
26
|
-
.filter(
|
|
27
|
-
([outcome, target]) =>
|
|
28
|
-
target === '$pause' && allowedOutcomeSet.has(outcome),
|
|
29
|
-
)
|
|
30
|
-
.map(([outcome]) => outcome);
|
|
31
|
-
|
|
32
|
-
type RecoveryInstructionsOptions = {
|
|
33
|
-
readonly allowedOutcomeSet: ReadonlySet<string>;
|
|
34
|
-
readonly pauseOutcomes: ReadonlyArray<string>;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const buildRecoveryInstructions = ({
|
|
38
|
-
allowedOutcomeSet,
|
|
39
|
-
pauseOutcomes,
|
|
40
|
-
}: RecoveryInstructionsOptions): ReadonlyArray<string> => {
|
|
41
|
-
const retryInstruction = allowedOutcomeSet.has('retry')
|
|
42
|
-
? [
|
|
43
|
-
'Use outcome `retry` when the execution contract remains valid and another bounded fresh attempt can safely continue from inspected state. Include the exact failure, attempts, observed state, and next alternative in `summary`.',
|
|
44
|
-
]
|
|
45
|
-
: [];
|
|
46
|
-
const replanInstruction = allowedOutcomeSet.has('replan')
|
|
47
|
-
? [
|
|
48
|
-
'Use outcome `replan` when recovery requires a material change to reviewed intent, commands, targets, or authority. Include the exact invalid contract evidence and proposed correction in `summary`.',
|
|
49
|
-
]
|
|
50
|
-
: [];
|
|
51
|
-
|
|
52
|
-
if (pauseOutcomes.length > 0) {
|
|
53
|
-
return [
|
|
54
|
-
...retryInstruction,
|
|
55
|
-
...replanInstruction,
|
|
56
|
-
`Use a pause outcome (${pauseOutcomes.join(', ')}) only when permitted alternatives and offered recovery outcomes cannot resolve the workflow definition, environment, or execution contract. Describe the exhausted recovery evidence declaratively in \`summary\`.`,
|
|
57
|
-
];
|
|
58
|
-
}
|
|
59
|
-
if (allowedOutcomeSet.has('retry') || allowedOutcomeSet.has('replan')) {
|
|
60
|
-
return [...retryInstruction, ...replanInstruction];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return [
|
|
64
|
-
...retryInstruction,
|
|
65
|
-
...replanInstruction,
|
|
66
|
-
'If the workflow definition, environment, or final execution contract is wrong, do not fabricate success or call the completion tool; end with a concise declarative error so the harness pauses the step.',
|
|
67
|
-
];
|
|
68
|
-
};
|
|
69
|
-
|
|
70
21
|
/**
|
|
71
|
-
* Derives the completion
|
|
22
|
+
* Derives the completion contract for an active workflow step.
|
|
72
23
|
*
|
|
73
24
|
* @param options - Workflow state used to resolve currently allowed outcomes.
|
|
74
25
|
* @returns Immutable text fragments for the step prompt.
|
|
@@ -80,7 +31,6 @@ export function createStepContract({
|
|
|
80
31
|
}: CreateStepContractOptions): StepContract {
|
|
81
32
|
const outcomes = allowedOutcomes(workflow, run);
|
|
82
33
|
const allowedOutcomeSet = new Set(outcomes);
|
|
83
|
-
const pauseOutcomes = pauseOutcomesFor(step, allowedOutcomeSet);
|
|
84
34
|
const transitionLines = Object.entries(step.transitions)
|
|
85
35
|
.filter(([outcome]) => allowedOutcomeSet.has(outcome))
|
|
86
36
|
.map(([outcome, target]) => `- ${outcome}: ${target}`)
|
|
@@ -88,14 +38,18 @@ export function createStepContract({
|
|
|
88
38
|
const gateLine = step.gate
|
|
89
39
|
? `- ${step.gate.submitOutcome}: submit the artifact to ${step.gate.provider}; include the full artifact argument`
|
|
90
40
|
: '';
|
|
41
|
+
const workspaceLines = step.workspace
|
|
42
|
+
? [
|
|
43
|
+
`Workspace-binding outcomes: ${step.workspace.bindOn.join(', ')}`,
|
|
44
|
+
`For those outcomes, include \`workspace.cwd\` as an absolute directory under one allowed root relative to the run-start directory: ${step.workspace.allowedRoots.join(', ')}`,
|
|
45
|
+
'For every other outcome, omit `workspace`.',
|
|
46
|
+
]
|
|
47
|
+
: ['This step cannot bind a workspace; omit `workspace`.'];
|
|
91
48
|
|
|
92
49
|
return {
|
|
93
50
|
outcomes,
|
|
94
51
|
transitionLines,
|
|
95
52
|
gateLine,
|
|
96
|
-
|
|
97
|
-
allowedOutcomeSet,
|
|
98
|
-
pauseOutcomes,
|
|
99
|
-
}),
|
|
53
|
+
workspaceLines,
|
|
100
54
|
};
|
|
101
55
|
}
|
|
@@ -30,6 +30,11 @@ export function buildResourceSection({
|
|
|
30
30
|
`Extension selectors: ${formatList(step.permissions.extensions)}`,
|
|
31
31
|
`Skills: ${formatList(step.permissions.skills)}`,
|
|
32
32
|
`Bash policy: ${step.permissions.bash.mode}`,
|
|
33
|
+
`Bash allow rules: ${
|
|
34
|
+
step.permissions.bash.allow.length > 0
|
|
35
|
+
? JSON.stringify(step.permissions.bash.allow)
|
|
36
|
+
: '(none)'
|
|
37
|
+
}`,
|
|
33
38
|
'',
|
|
34
39
|
`Use only the listed skills for this step. Tool calls are enforced ${isDelegated ? 'inside this child process' : 'by the workflow harness'}.`,
|
|
35
40
|
'',
|
|
@@ -59,17 +64,10 @@ export function buildDelegatedHandoffSection(
|
|
|
59
64
|
* @param step - Active delegated workflow step.
|
|
60
65
|
* @returns Delegated completion-guidance lines.
|
|
61
66
|
*/
|
|
62
|
-
export function buildDelegatedCompletionInstructions(
|
|
63
|
-
step: WorkflowStep,
|
|
64
|
-
): ReadonlyArray<string> {
|
|
65
|
-
const finalContractInstruction = step.gate
|
|
66
|
-
? 'Put every unresolved decision in the gate artifact with evidence, options, a recommendation, and an adopted default; do not ask a terminal question.'
|
|
67
|
-
: 'Treat the step instructions and incoming handoff as the final execution contract; do not ask a terminal question.';
|
|
68
|
-
|
|
67
|
+
export function buildDelegatedCompletionInstructions(): ReadonlyArray<string> {
|
|
69
68
|
return [
|
|
70
69
|
'This child is non-interactive. Never call `contact_supervisor`, `subagent_supervisor`, or `intercom`.',
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
finalContractInstruction,
|
|
70
|
+
'Follow the step instructions when choosing one valid outcome; outcome names have no built-in domain meaning.',
|
|
71
|
+
'Stay within the configured permissions and do not broaden mutation targets or external side effects.',
|
|
74
72
|
];
|
|
75
73
|
}
|
package/src/prompt/step-task.ts
CHANGED
|
@@ -45,6 +45,8 @@ type RenderStepPromptOptions = {
|
|
|
45
45
|
readonly execution: StepExecution;
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
const RESUME_INPUT_PLACEHOLDER = /\{\{\s*resume\.input\s*\}\}/;
|
|
49
|
+
|
|
48
50
|
const renderStepPrompt = ({
|
|
49
51
|
workflow,
|
|
50
52
|
run,
|
|
@@ -53,20 +55,44 @@ const renderStepPrompt = ({
|
|
|
53
55
|
}: RenderStepPromptOptions): string => {
|
|
54
56
|
const promptTemplate = workflow.prompts[run.currentStepId] ?? '';
|
|
55
57
|
const templateValues = createTemplateValues({ workflow, run, step });
|
|
56
|
-
const
|
|
58
|
+
const hidesRepeatedLastSummary =
|
|
57
59
|
execution === 'delegated' &&
|
|
58
60
|
/\{\{\s*last\.summary\s*\}\}/.test(promptTemplate);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
: templateValues;
|
|
61
|
+
const handoffReference =
|
|
62
|
+
'(Provided once in the Previous step handoff section below.)';
|
|
63
|
+
const values = {
|
|
64
|
+
...templateValues,
|
|
65
|
+
...(hidesRepeatedLastSummary ? { 'last.summary': handoffReference } : {}),
|
|
66
|
+
};
|
|
66
67
|
|
|
67
68
|
return renderTemplate(promptTemplate, values);
|
|
68
69
|
};
|
|
69
70
|
|
|
71
|
+
const buildResumeInputSection = (
|
|
72
|
+
run: WorkflowRun,
|
|
73
|
+
promptContainsResumeInput: boolean,
|
|
74
|
+
): ReadonlyArray<string> => {
|
|
75
|
+
if (!run.resumeInput) return [];
|
|
76
|
+
const authority =
|
|
77
|
+
'The user-supplied resume guidance for this attempt is authoritative when it conflicts with task instructions in the step prompt or previous handoff. Inspect current state before applying it. It does not change the workflow graph or the YAML-enforced tools, MCP, extensions, skills, Bash policy, or workspace boundary.';
|
|
78
|
+
if (promptContainsResumeInput) {
|
|
79
|
+
return ['## Resume guidance authority', '', authority, ''];
|
|
80
|
+
}
|
|
81
|
+
const serialized = JSON.stringify({ input: run.resumeInput }, null, 2)
|
|
82
|
+
.replaceAll('<', '\\u003c')
|
|
83
|
+
.replaceAll('>', '\\u003e');
|
|
84
|
+
return [
|
|
85
|
+
'## User guidance supplied with `/workflow-resume`',
|
|
86
|
+
'',
|
|
87
|
+
authority,
|
|
88
|
+
'',
|
|
89
|
+
'<pi-workflows-resume-input-v1>',
|
|
90
|
+
serialized,
|
|
91
|
+
'</pi-workflows-resume-input-v1>',
|
|
92
|
+
'',
|
|
93
|
+
];
|
|
94
|
+
};
|
|
95
|
+
|
|
70
96
|
/**
|
|
71
97
|
* Builds the complete task for either a main-agent or delegated workflow step.
|
|
72
98
|
*
|
|
@@ -77,6 +103,7 @@ export function buildStepTask(options: BuildStepTaskOptions): string {
|
|
|
77
103
|
const { execution, workflow, run } = options;
|
|
78
104
|
const step = resolveStep(workflow, run);
|
|
79
105
|
const isDelegated = execution === 'delegated';
|
|
106
|
+
const promptTemplate = workflow.prompts[run.currentStepId] ?? '';
|
|
80
107
|
const prompt = renderStepPrompt({ workflow, run, step, execution });
|
|
81
108
|
const handoff = currentStepHandoff(run);
|
|
82
109
|
const contract = createStepContract({ workflow, run, step });
|
|
@@ -105,6 +132,10 @@ export function buildStepTask(options: BuildStepTaskOptions): string {
|
|
|
105
132
|
prompt,
|
|
106
133
|
'',
|
|
107
134
|
...(isDelegated ? buildDelegatedHandoffSection(handoff) : []),
|
|
135
|
+
...buildResumeInputSection(
|
|
136
|
+
run,
|
|
137
|
+
RESUME_INPUT_PLACEHOLDER.test(promptTemplate),
|
|
138
|
+
),
|
|
108
139
|
...buildResourceSection({ execution, step }),
|
|
109
140
|
'## Completion contract',
|
|
110
141
|
'',
|
|
@@ -112,11 +143,11 @@ export function buildStepTask(options: BuildStepTaskOptions): string {
|
|
|
112
143
|
`Valid outcomes: ${contract.outcomes.join(', ')}`,
|
|
113
144
|
contract.transitionLines,
|
|
114
145
|
contract.gateLine,
|
|
146
|
+
...contract.workspaceLines,
|
|
115
147
|
'',
|
|
116
148
|
'Put a self-contained compact handoff in `summary`; this is the only step context passed to the next fresh child.',
|
|
117
|
-
...(isDelegated ? buildDelegatedCompletionInstructions(
|
|
149
|
+
...(isDelegated ? buildDelegatedCompletionInstructions() : []),
|
|
118
150
|
'Do not call the completion tool alongside other tool calls.',
|
|
119
|
-
...contract.recoveryInstructions,
|
|
120
151
|
].join('\n');
|
|
121
152
|
}
|
|
122
153
|
|
package/src/prompt/template.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { WorkflowRun } from '../engine/state.ts';
|
|
|
7
7
|
export type TemplateValues = Readonly<Record<string, string>>;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Combines the
|
|
10
|
+
* Combines the incoming handoff with the latest paused-attempt
|
|
11
11
|
* summary without duplicating identical content.
|
|
12
12
|
*
|
|
13
13
|
* @param run - Current workflow run.
|
|
@@ -24,7 +24,7 @@ export function currentStepHandoff(run: WorkflowRun): string {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
return [
|
|
27
|
-
'Incoming
|
|
27
|
+
'Incoming previous-step handoff:',
|
|
28
28
|
incomingHandoff,
|
|
29
29
|
'',
|
|
30
30
|
'Latest paused attempt:',
|
|
@@ -76,6 +76,9 @@ export function createTemplateValues({
|
|
|
76
76
|
'step.id': run.currentStepId,
|
|
77
77
|
'step.title': step.title,
|
|
78
78
|
'last.summary': currentStepHandoff(run),
|
|
79
|
+
'reviewed.artifact': run.reviewedArtifact ?? '',
|
|
80
|
+
'reviewed.feedback': run.reviewedFeedback ?? '',
|
|
79
81
|
'gate.feedback': run.gateFeedback,
|
|
82
|
+
'resume.input': run.resumeInput ?? '',
|
|
80
83
|
};
|
|
81
84
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Type } from 'typebox';
|
|
2
|
+
import { MAX_WORKSPACE_PATH_CHARS } from '../config/types.ts';
|
|
2
3
|
|
|
3
4
|
export const WORKFLOW_COMPLETION_TOOL = 'workflow_complete_step';
|
|
4
5
|
|
|
@@ -20,6 +21,19 @@ export const WORKFLOW_COMPLETION_PARAMETERS = Type.Object(
|
|
|
20
21
|
maxLength: 200_000,
|
|
21
22
|
}),
|
|
22
23
|
),
|
|
24
|
+
workspace: Type.Optional(
|
|
25
|
+
Type.Object(
|
|
26
|
+
{
|
|
27
|
+
cwd: Type.String({
|
|
28
|
+
description:
|
|
29
|
+
'Absolute working directory established for subsequent workflow steps',
|
|
30
|
+
minLength: 1,
|
|
31
|
+
maxLength: MAX_WORKSPACE_PATH_CHARS,
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
{ additionalProperties: false },
|
|
35
|
+
),
|
|
36
|
+
),
|
|
23
37
|
},
|
|
24
38
|
{ additionalProperties: false },
|
|
25
39
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { WORKFLOW_COMPLETION_TOOL } from './completion-tool.ts';
|
|
3
|
+
import { armMainStepTrace } from './main-step-trace.ts';
|
|
3
4
|
import type {
|
|
4
5
|
MainStepRuntimeDependencies,
|
|
5
6
|
MainStepRuntimeState,
|
|
@@ -26,6 +27,7 @@ export function registerMainStepPolicy({
|
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
pi.on('message_end', (event) => {
|
|
30
|
+
armMainStepTrace(state, event.message);
|
|
29
31
|
if (!state.active) {
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
@@ -70,7 +72,6 @@ export function registerMainStepPolicy({
|
|
|
70
72
|
Object.fromEntries(Object.entries(event.input)),
|
|
71
73
|
state.active.step,
|
|
72
74
|
pi.getAllTools(),
|
|
73
|
-
state.active.approvedBashCommands,
|
|
74
75
|
);
|
|
75
76
|
if (!authorization.allowed) {
|
|
76
77
|
return {
|
|
@@ -17,8 +17,14 @@ export type MainStepExecution = StepResultPolicy & {
|
|
|
17
17
|
readonly runId: string;
|
|
18
18
|
readonly stepId: string;
|
|
19
19
|
readonly stepDigest: string;
|
|
20
|
+
/** Exact task whose finalized user message arms this attempt's trace. */
|
|
21
|
+
readonly task: string;
|
|
20
22
|
readonly step: WorkflowStep;
|
|
21
|
-
|
|
23
|
+
/** Persists one finalized, already-redacted turn before Pi begins a later turn. */
|
|
24
|
+
readonly onTrace: (
|
|
25
|
+
lines: ReadonlyArray<string>,
|
|
26
|
+
context: ExtensionContext,
|
|
27
|
+
) => Promise<void> | void;
|
|
22
28
|
/** Handles the captured result after Pi fully settles the agent run. */
|
|
23
29
|
readonly onSettled: (
|
|
24
30
|
result: WorkflowStepResult | undefined,
|
|
@@ -60,5 +66,7 @@ export type MainStepRuntimeState = {
|
|
|
60
66
|
active: MainStepExecution | undefined;
|
|
61
67
|
pendingResult: WorkflowStepResult | undefined;
|
|
62
68
|
invalidCompletionCalls: ReadonlySet<string>;
|
|
69
|
+
traceArmed: boolean;
|
|
70
|
+
traceClosed: boolean;
|
|
63
71
|
isSuspended: boolean;
|
|
64
72
|
};
|
|
@@ -5,6 +5,7 @@ import { authorizeToolCall, resolveActiveTools } from '../policy/tools.ts';
|
|
|
5
5
|
import { registerMainStepCompletion } from './main-step-completion.ts';
|
|
6
6
|
import { registerMainStepLifecycle } from './main-step-lifecycle.ts';
|
|
7
7
|
import { registerMainStepPolicy } from './main-step-policy.ts';
|
|
8
|
+
import { registerMainStepTrace } from './main-step-trace.ts';
|
|
8
9
|
import type {
|
|
9
10
|
MainStepExecution,
|
|
10
11
|
MainStepRuntimeController,
|
|
@@ -84,6 +85,7 @@ export function createMainStepRuntime({
|
|
|
84
85
|
|
|
85
86
|
registerMainStepLifecycle({ pi, state });
|
|
86
87
|
registerMainStepPolicy({ pi, state, dependencies });
|
|
88
|
+
registerMainStepTrace({ pi, state });
|
|
87
89
|
registerMainStepCompletion({ pi, state, dependencies });
|
|
88
90
|
|
|
89
91
|
return {
|
|
@@ -14,6 +14,8 @@ export function createMainStepRuntimeState(): MainStepRuntimeState {
|
|
|
14
14
|
active: undefined,
|
|
15
15
|
pendingResult: undefined,
|
|
16
16
|
invalidCompletionCalls: new Set(),
|
|
17
|
+
traceArmed: false,
|
|
18
|
+
traceClosed: false,
|
|
17
19
|
isSuspended: false,
|
|
18
20
|
};
|
|
19
21
|
}
|
|
@@ -43,6 +45,8 @@ export function activateMainStep({
|
|
|
43
45
|
state.active = execution;
|
|
44
46
|
state.pendingResult = undefined;
|
|
45
47
|
state.invalidCompletionCalls = new Set();
|
|
48
|
+
state.traceArmed = false;
|
|
49
|
+
state.traceClosed = false;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
@@ -56,6 +60,8 @@ export function deactivateMainStep(state: MainStepRuntimeState): boolean {
|
|
|
56
60
|
state.active = undefined;
|
|
57
61
|
state.pendingResult = undefined;
|
|
58
62
|
state.invalidCompletionCalls = new Set();
|
|
63
|
+
state.traceArmed = false;
|
|
64
|
+
state.traceClosed = false;
|
|
59
65
|
return wasActive;
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { stepLogLinesFromTurn, textOnlyUserMessage } from '../step-log.ts';
|
|
3
|
+
import type { MainStepRuntimeState } from './main-step-runtime-types.ts';
|
|
4
|
+
|
|
5
|
+
type RegisterMainStepTraceOptions = {
|
|
6
|
+
readonly pi: ExtensionAPI;
|
|
7
|
+
readonly state: MainStepRuntimeState;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Arms a trace only after Pi finalizes the exact extension-supplied task. */
|
|
11
|
+
export function armMainStepTrace(
|
|
12
|
+
state: MainStepRuntimeState,
|
|
13
|
+
message: unknown,
|
|
14
|
+
): void {
|
|
15
|
+
if (!state.active || state.traceArmed || state.traceClosed) return;
|
|
16
|
+
if (textOnlyUserMessage(message) === state.active.task) {
|
|
17
|
+
state.traceArmed = true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Persists finalized main-agent turns while one exact workflow attempt is armed.
|
|
23
|
+
*
|
|
24
|
+
* Successful completion is closed only after its assistant call and tool result
|
|
25
|
+
* are captured, preventing queued parent-session follow-ups from leaking in.
|
|
26
|
+
*/
|
|
27
|
+
export function registerMainStepTrace({
|
|
28
|
+
pi,
|
|
29
|
+
state,
|
|
30
|
+
}: RegisterMainStepTraceOptions): void {
|
|
31
|
+
pi.on('turn_end', async (event, context) => {
|
|
32
|
+
const active = state.active;
|
|
33
|
+
if (!active || !state.traceArmed || state.traceClosed) return;
|
|
34
|
+
const lines = stepLogLinesFromTurn(event.message, event.toolResults);
|
|
35
|
+
try {
|
|
36
|
+
if (lines.length > 0) await active.onTrace(lines, context);
|
|
37
|
+
} catch {
|
|
38
|
+
// Status evidence is best-effort and must never interrupt the agent loop.
|
|
39
|
+
} finally {
|
|
40
|
+
if (state.active === active && state.pendingResult) {
|
|
41
|
+
state.traceClosed = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { isAbsolute } from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
MAX_WORKSPACE_PATH_CHARS,
|
|
4
|
+
type StepWorkspaceBinding,
|
|
5
|
+
} from '../config/types.ts';
|
|
6
|
+
|
|
1
7
|
const MAX_ARTIFACT_CHARS = 200_000;
|
|
2
8
|
const RESULT_KEYS = new Set([
|
|
3
9
|
'version',
|
|
@@ -5,6 +11,7 @@ const RESULT_KEYS = new Set([
|
|
|
5
11
|
'outcome',
|
|
6
12
|
'summary',
|
|
7
13
|
'artifact',
|
|
14
|
+
'workspace',
|
|
8
15
|
]);
|
|
9
16
|
|
|
10
17
|
/**
|
|
@@ -15,6 +22,11 @@ export type StepResultPolicy = {
|
|
|
15
22
|
readonly outcomes: ReadonlyArray<string>;
|
|
16
23
|
readonly summaryMaxChars: number;
|
|
17
24
|
readonly gateSubmitOutcome?: string;
|
|
25
|
+
readonly workspace?: StepWorkspaceBinding;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type WorkflowResultWorkspace = {
|
|
29
|
+
readonly cwd: string;
|
|
18
30
|
};
|
|
19
31
|
|
|
20
32
|
/**
|
|
@@ -26,12 +38,51 @@ export type WorkflowStepResult = {
|
|
|
26
38
|
readonly outcome: string;
|
|
27
39
|
readonly summary: string;
|
|
28
40
|
readonly artifact?: string;
|
|
41
|
+
readonly workspace?: WorkflowResultWorkspace;
|
|
29
42
|
};
|
|
30
43
|
|
|
31
44
|
const isObject = (value: unknown): value is Record<string, unknown> => {
|
|
32
45
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
33
46
|
};
|
|
34
47
|
|
|
48
|
+
function parseResultWorkspace(
|
|
49
|
+
value: unknown,
|
|
50
|
+
outcome: string,
|
|
51
|
+
policy: StepResultPolicy,
|
|
52
|
+
): WorkflowResultWorkspace | undefined {
|
|
53
|
+
const requiresWorkspace = policy.workspace?.bindOn.includes(outcome) === true;
|
|
54
|
+
if (!requiresWorkspace) {
|
|
55
|
+
if (value !== undefined) {
|
|
56
|
+
throw new Error('workflow step workspace is forbidden for this outcome');
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (!isObject(value)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`workflow step outcome "${outcome}" requires workspace.cwd`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const unknownKey = Object.keys(value).find((key) => key !== 'cwd');
|
|
66
|
+
if (unknownKey) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`workflow step workspace has unknown property "${unknownKey}"`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (typeof value.cwd !== 'string') {
|
|
72
|
+
throw new Error('workflow step workspace cwd must be a string');
|
|
73
|
+
}
|
|
74
|
+
const cwd = value.cwd;
|
|
75
|
+
if (!cwd || cwd.includes('\0') || !isAbsolute(cwd)) {
|
|
76
|
+
throw new Error('workflow step workspace cwd must be an absolute path');
|
|
77
|
+
}
|
|
78
|
+
if (cwd.length > MAX_WORKSPACE_PATH_CHARS) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`workflow step workspace cwd exceeds ${MAX_WORKSPACE_PATH_CHARS} characters`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return { cwd };
|
|
84
|
+
}
|
|
85
|
+
|
|
35
86
|
/**
|
|
36
87
|
* Validates and normalizes the structured result returned by a workflow step.
|
|
37
88
|
*
|
|
@@ -96,11 +147,17 @@ export function parseWorkflowStepResult(
|
|
|
96
147
|
) {
|
|
97
148
|
throw new Error('workflow gate outcome requires a non-empty artifact');
|
|
98
149
|
}
|
|
150
|
+
const workspace = parseResultWorkspace(
|
|
151
|
+
value.workspace,
|
|
152
|
+
value.outcome,
|
|
153
|
+
policy,
|
|
154
|
+
);
|
|
99
155
|
return {
|
|
100
156
|
version: 1,
|
|
101
157
|
policyDigest: policy.policyDigest,
|
|
102
158
|
outcome: value.outcome,
|
|
103
159
|
summary,
|
|
104
160
|
...(artifact !== undefined ? { artifact } : {}),
|
|
161
|
+
...(workspace ? { workspace } : {}),
|
|
105
162
|
};
|
|
106
163
|
}
|