@wichayutdew/pi-workflows 0.3.0 → 1.0.1

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.
Files changed (144) hide show
  1. package/README.md +55 -39
  2. package/dist/index.js +5771 -4794
  3. package/examples/mr-comments.workflow.yaml +2 -0
  4. package/package.json +1 -1
  5. package/schemas/workflow.schema.json +10 -1
  6. package/src/command-names.ts +7 -1
  7. package/src/commands.ts +126 -52
  8. package/src/config/catalog.ts +162 -0
  9. package/src/config/ceiling.ts +120 -116
  10. package/src/config/command-conflicts.ts +4 -4
  11. package/src/config/diagnostics.ts +28 -0
  12. package/src/config/load-settings.ts +37 -0
  13. package/src/config/load-types.ts +53 -0
  14. package/src/config/load-workflows.ts +135 -0
  15. package/src/config/load.ts +43 -311
  16. package/src/config/types.ts +150 -137
  17. package/src/config/validate.ts +12 -1261
  18. package/src/config/validation/permissions.ts +312 -0
  19. package/src/config/validation/prompt.ts +21 -0
  20. package/src/config/validation/settings.ts +122 -0
  21. package/src/config/validation/shared.ts +129 -0
  22. package/src/config/validation/shortcut.ts +118 -0
  23. package/src/config/validation/step.ts +228 -0
  24. package/src/config/validation/subagent.ts +288 -0
  25. package/src/config/validation/workflow.ts +156 -0
  26. package/src/config/yaml.ts +29 -0
  27. package/src/digest.ts +38 -6
  28. package/src/engine/checkpoint.ts +13 -9
  29. package/src/engine/create-run.ts +43 -0
  30. package/src/engine/gate-transitions.ts +187 -0
  31. package/src/engine/reconciliation-history.ts +88 -0
  32. package/src/engine/resume.ts +19 -7
  33. package/src/engine/run-advance.ts +122 -0
  34. package/src/engine/run-lifecycle.ts +124 -0
  35. package/src/engine/run-reconciliation.ts +116 -0
  36. package/src/engine/run-validation.ts +129 -0
  37. package/src/engine/state-types.ts +66 -0
  38. package/src/engine/state.ts +10 -193
  39. package/src/engine/transition-helpers.ts +28 -0
  40. package/src/engine/transition-types.ts +8 -0
  41. package/src/engine/transitions.ts +17 -471
  42. package/src/harness/action-context.ts +181 -0
  43. package/src/harness/catalog.ts +45 -0
  44. package/src/harness/context-idle.ts +21 -0
  45. package/src/harness/core-actions.ts +299 -0
  46. package/src/harness/delegation-control-actions.ts +250 -0
  47. package/src/harness/delegation-failure.ts +215 -0
  48. package/src/harness/delegation-plan.ts +219 -0
  49. package/src/harness/delegation-recovery-validation.ts +159 -0
  50. package/src/harness/delegation-response-actions.ts +295 -0
  51. package/src/harness/delegation-retry-policy.ts +118 -0
  52. package/src/harness/dependencies.ts +165 -0
  53. package/src/harness/gate-submission-action.ts +154 -0
  54. package/src/harness/lifecycle-actions.ts +154 -0
  55. package/src/harness/pause-actions.ts +120 -0
  56. package/src/harness/plannotator-result-actions.ts +123 -0
  57. package/src/harness/prompt-gate-actions.ts +270 -0
  58. package/src/harness/resume-action.ts +261 -0
  59. package/src/harness/start-actions.ts +183 -0
  60. package/src/harness/status-actions.ts +191 -0
  61. package/src/harness/step-execution-actions.ts +283 -0
  62. package/src/harness/types.ts +76 -0
  63. package/src/harness.ts +236 -2303
  64. package/src/index.ts +73 -11
  65. package/src/integrations/plannotator-requests.ts +127 -0
  66. package/src/integrations/plannotator-responses.ts +141 -0
  67. package/src/integrations/plannotator-types.ts +44 -0
  68. package/src/integrations/plannotator.ts +16 -235
  69. package/src/integrations/prompt-gate.ts +17 -10
  70. package/src/integrations/subagents/child-policy-envelope.ts +119 -0
  71. package/src/integrations/subagents/child-policy-paths.ts +66 -0
  72. package/src/integrations/subagents/child-policy-sections.ts +186 -0
  73. package/src/integrations/subagents/child-policy-types.ts +32 -0
  74. package/src/integrations/subagents/child-policy-validation.ts +163 -0
  75. package/src/integrations/subagents/child-runtime-completion.ts +56 -0
  76. package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
  77. package/src/integrations/subagents/child-runtime-files.ts +156 -0
  78. package/src/integrations/subagents/child-runtime-policy.ts +83 -0
  79. package/src/integrations/subagents/child-runtime-types.ts +30 -0
  80. package/src/integrations/subagents/child-runtime.ts +140 -301
  81. package/src/integrations/subagents/client-delegation.ts +181 -0
  82. package/src/integrations/subagents/client-messages.ts +66 -0
  83. package/src/integrations/subagents/client-types.ts +36 -0
  84. package/src/integrations/subagents/client.ts +127 -219
  85. package/src/integrations/subagents/delegated-result.ts +31 -0
  86. package/src/integrations/subagents/diagnostic-format.ts +40 -0
  87. package/src/integrations/subagents/diagnostic-text.ts +114 -0
  88. package/src/integrations/subagents/diagnostic-types.ts +77 -0
  89. package/src/integrations/subagents/diagnostics.ts +24 -977
  90. package/src/integrations/subagents/failure-correlation.ts +235 -0
  91. package/src/integrations/subagents/failure-transcript.ts +192 -0
  92. package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
  93. package/src/integrations/subagents/protocol-events.ts +27 -0
  94. package/src/integrations/subagents/protocol.ts +30 -375
  95. package/src/integrations/subagents/replay-audit.ts +147 -0
  96. package/src/integrations/subagents/replay-safety.ts +82 -0
  97. package/src/integrations/subagents/session-diagnostics.ts +258 -0
  98. package/src/policy/approved-command-extraction.ts +71 -0
  99. package/src/policy/approved-commands.ts +15 -432
  100. package/src/policy/bash-authorization.ts +86 -0
  101. package/src/policy/bash-read-only.ts +161 -0
  102. package/src/policy/bash-types.ts +20 -0
  103. package/src/policy/bash.ts +4 -346
  104. package/src/policy/completion-batch.ts +21 -16
  105. package/src/policy/immutable-input.ts +5 -2
  106. package/src/policy/mcp-authorization.ts +71 -0
  107. package/src/policy/restricted-command.ts +141 -0
  108. package/src/policy/restricted-git.ts +30 -0
  109. package/src/policy/reviewed-artifact.ts +97 -0
  110. package/src/policy/reviewed-command-safety.ts +150 -0
  111. package/src/policy/reviewed-command-shape.ts +49 -0
  112. package/src/policy/reviewed-repository-contract.ts +138 -0
  113. package/src/policy/reviewed-repository-cwd.ts +102 -0
  114. package/src/policy/tool-call-authorization.ts +58 -0
  115. package/src/policy/tool-selection.ts +72 -0
  116. package/src/policy/tool-types.ts +14 -0
  117. package/src/policy/tools.ts +11 -150
  118. package/src/preflight.ts +96 -61
  119. package/src/prompt/main-workflow-notice.ts +41 -0
  120. package/src/prompt/retry-task.ts +83 -0
  121. package/src/prompt/step-contract.ts +101 -0
  122. package/src/prompt/step-sections.ts +75 -0
  123. package/src/prompt/step-task.ts +156 -0
  124. package/src/prompt/template.ts +83 -0
  125. package/src/prompt.ts +10 -254
  126. package/src/runtime/completion-tool.ts +3 -0
  127. package/src/runtime/main-step-completion.ts +78 -0
  128. package/src/runtime/main-step-lifecycle.ts +37 -0
  129. package/src/runtime/main-step-policy.ts +84 -0
  130. package/src/runtime/main-step-runtime-types.ts +64 -0
  131. package/src/runtime/main-step-runtime.ts +129 -200
  132. package/src/runtime/main-step-state.ts +85 -0
  133. package/src/runtime/serial-task-queue.ts +48 -13
  134. package/src/runtime/step-result.ts +48 -27
  135. package/src/workflow-list.ts +15 -6
  136. package/src/workflow-status/format-status.ts +52 -0
  137. package/src/workflow-status/formatting.ts +178 -0
  138. package/src/workflow-status/layout.ts +109 -0
  139. package/src/workflow-status/render-board.ts +101 -0
  140. package/src/workflow-status/render-path.ts +123 -0
  141. package/src/workflow-status/render-summary.ts +144 -0
  142. package/src/workflow-status/types.ts +40 -0
  143. package/src/workflow-status/view.ts +273 -0
  144. package/src/workflow-status.ts +20 -820
@@ -0,0 +1,47 @@
1
+ import { randomUUID, timingSafeEqual } from 'node:crypto';
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ readFileSync,
6
+ realpathSync,
7
+ renameSync,
8
+ statSync,
9
+ unlinkSync,
10
+ writeFileSync,
11
+ } from 'node:fs';
12
+ import { tmpdir } from 'node:os';
13
+ import type { SubagentChildRuntimeDependencies } from './child-runtime-types.ts';
14
+
15
+ const tokensAreEqual = (actual: string, expected: string): boolean => {
16
+ const actualBytes = Buffer.from(actual, 'utf8');
17
+ const expectedBytes = Buffer.from(expected, 'utf8');
18
+ return (
19
+ actualBytes.length === expectedBytes.length &&
20
+ timingSafeEqual(actualBytes, expectedBytes)
21
+ );
22
+ };
23
+
24
+ export const DEFAULT_CHILD_RUNTIME_DEPENDENCIES = {
25
+ fileSystem: {
26
+ exists: existsSync,
27
+ inspect: lstatSync,
28
+ readText: (path: string) => readFileSync(path, 'utf8'),
29
+ realPath: realpathSync,
30
+ rename: renameSync,
31
+ stat: statSync,
32
+ unlink: unlinkSync,
33
+ writeExclusive: (path: string, content: string) => {
34
+ writeFileSync(path, content, {
35
+ encoding: 'utf8',
36
+ flag: 'wx',
37
+ mode: 0o600,
38
+ });
39
+ },
40
+ },
41
+ createUniqueId: randomUUID,
42
+ currentWorkingDirectory: () => process.cwd(),
43
+ environmentChildAgent: () =>
44
+ process.env.PI_SUBAGENT_CHILD_AGENT?.trim() || undefined,
45
+ temporaryDirectory: tmpdir,
46
+ tokensAreEqual,
47
+ } as const satisfies SubagentChildRuntimeDependencies;
@@ -0,0 +1,156 @@
1
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
2
+ import type { ChildStepPolicy } from './child-policy-types.ts';
3
+ import type { SubagentChildRuntimeDependencies } from './child-runtime-types.ts';
4
+
5
+ const FILE_MUTATION_TOOLS: ReadonlySet<string> = new Set(['edit', 'write']);
6
+
7
+ const errorCode = (error: unknown): unknown =>
8
+ error !== null && typeof error === 'object' && 'code' in error
9
+ ? error.code
10
+ : undefined;
11
+
12
+ const pathIsInside = (root: string, candidate: string): boolean => {
13
+ const relativePath = relative(root, candidate);
14
+ return (
15
+ relativePath !== '..' &&
16
+ !relativePath.startsWith(`..${sep}`) &&
17
+ !isAbsolute(relativePath)
18
+ );
19
+ };
20
+
21
+ const nearestCanonicalAncestor = (
22
+ path: string,
23
+ dependencies: SubagentChildRuntimeDependencies,
24
+ ): string | undefined => {
25
+ let candidate = path;
26
+ while (true) {
27
+ try {
28
+ dependencies.fileSystem.inspect(candidate);
29
+ } catch (error) {
30
+ if (errorCode(error) !== 'ENOENT') return undefined;
31
+ const parent = dirname(candidate);
32
+ if (parent === candidate) return undefined;
33
+ candidate = parent;
34
+ continue;
35
+ }
36
+ try {
37
+ return dependencies.fileSystem.realPath(candidate);
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ };
43
+
44
+ type VerifyChildCapabilityOptions = {
45
+ readonly policy: ChildStepPolicy;
46
+ readonly childAgent: string;
47
+ readonly dependencies: SubagentChildRuntimeDependencies;
48
+ };
49
+
50
+ /**
51
+ * Verifies and consumes the one-time capability for a delegated child.
52
+ *
53
+ * @throws When the child identity or capability is invalid.
54
+ */
55
+ export const verifyChildCapability = ({
56
+ policy,
57
+ childAgent,
58
+ dependencies,
59
+ }: VerifyChildCapabilityOptions): void => {
60
+ if (childAgent !== policy.agent) {
61
+ throw new Error('child agent does not match the delegated workflow policy');
62
+ }
63
+
64
+ let actual: string;
65
+ try {
66
+ actual = dependencies.fileSystem.readText(policy.capabilityPath);
67
+ } catch {
68
+ throw new Error('delegated workflow capability is missing');
69
+ }
70
+ if (!dependencies.tokensAreEqual(actual, policy.capabilityToken)) {
71
+ throw new Error('delegated workflow capability is invalid');
72
+ }
73
+ dependencies.fileSystem.unlink(policy.capabilityPath);
74
+ };
75
+
76
+ type WriteChildResultOptions = {
77
+ readonly policy: ChildStepPolicy;
78
+ readonly result: unknown;
79
+ readonly dependencies: SubagentChildRuntimeDependencies;
80
+ };
81
+
82
+ /**
83
+ * Atomically writes a delegated result through injected file-system
84
+ * operations.
85
+ */
86
+ export const writeChildResult = ({
87
+ policy,
88
+ result,
89
+ dependencies,
90
+ }: WriteChildResultOptions): void => {
91
+ if (dependencies.fileSystem.exists(policy.resultPath)) {
92
+ throw new Error('Delegated workflow step already produced a result');
93
+ }
94
+
95
+ const temporaryPath = `${policy.resultPath}.${dependencies.createUniqueId()}.tmp`;
96
+ try {
97
+ dependencies.fileSystem.writeExclusive(
98
+ temporaryPath,
99
+ JSON.stringify(result),
100
+ );
101
+ dependencies.fileSystem.rename(temporaryPath, policy.resultPath);
102
+ } catch (error) {
103
+ try {
104
+ dependencies.fileSystem.unlink(temporaryPath);
105
+ } catch {
106
+ // The temporary file may not have been created.
107
+ }
108
+ throw error;
109
+ }
110
+ };
111
+
112
+ type RepositoryMutationOptions = {
113
+ readonly toolName: string;
114
+ readonly input: Readonly<Record<string, unknown>>;
115
+ readonly policy: ChildStepPolicy;
116
+ readonly dependencies: SubagentChildRuntimeDependencies;
117
+ };
118
+
119
+ /**
120
+ * Returns a policy error when a file mutation escapes the reviewed repository.
121
+ */
122
+ export const repositoryMutationError = ({
123
+ toolName,
124
+ input,
125
+ policy,
126
+ dependencies,
127
+ }: RepositoryMutationOptions): string | undefined => {
128
+ if (!policy.repositoryCwd || !FILE_MUTATION_TOOLS.has(toolName)) return;
129
+ if (typeof input.path !== 'string' || !input.path.trim()) {
130
+ return `${toolName} must name a path inside the reviewed repository root`;
131
+ }
132
+
133
+ const candidate = resolve(dependencies.currentWorkingDirectory(), input.path);
134
+ const root = resolve(policy.repositoryCwd);
135
+ if (!pathIsInside(root, candidate)) {
136
+ return `${toolName} path is outside the reviewed repository root "${policy.repositoryCwd}"`;
137
+ }
138
+
139
+ let canonicalRoot: string;
140
+ try {
141
+ if (!dependencies.fileSystem.stat(root).isDirectory()) {
142
+ throw new Error('not a directory');
143
+ }
144
+ canonicalRoot = dependencies.fileSystem.realPath(root);
145
+ } catch {
146
+ return `reviewed repository root is not an existing directory: ${policy.repositoryCwd}`;
147
+ }
148
+ const canonicalAncestor = nearestCanonicalAncestor(candidate, dependencies);
149
+ if (
150
+ canonicalAncestor === undefined ||
151
+ !pathIsInside(canonicalRoot, canonicalAncestor)
152
+ ) {
153
+ return `${toolName} path is outside the reviewed repository root "${policy.repositoryCwd}"`;
154
+ }
155
+ return;
156
+ };
@@ -0,0 +1,83 @@
1
+ import type { WorkflowStep } from '../../config/types.ts';
2
+ import type { ChildStepPolicy } from './child-policy-types.ts';
3
+
4
+ /**
5
+ * Projects a delegated policy into the workflow step shape used by policy
6
+ * authorization.
7
+ */
8
+ export const childPolicyStep = (policy: ChildStepPolicy): WorkflowStep => ({
9
+ title: policy.stepTitle,
10
+ prompt: { inline: 'Delegated workflow step' },
11
+ subagent: {
12
+ agent: policy.agent,
13
+ context: 'fresh',
14
+ timeoutMs: 900_000,
15
+ artifacts: false,
16
+ retryToolFailures: false,
17
+ },
18
+ permissions: policy.permissions,
19
+ requires: { tools: [], extensions: [], skills: [] },
20
+ transitions: {},
21
+ });
22
+
23
+ /**
24
+ * Builds the deterministic system prompt that describes the active child
25
+ * policy.
26
+ */
27
+ export const childSystemPrompt = (policy: ChildStepPolicy): string => {
28
+ const hasPauseOutcome = policy.pauseOutcomes.length > 0;
29
+ return [
30
+ '# Pi Workflows delegated step',
31
+ '',
32
+ `Workflow: ${policy.workflowId}`,
33
+ `Run: ${policy.runId}`,
34
+ `Step: ${policy.stepId} (${policy.stepTitle})`,
35
+ '',
36
+ 'The parent workflow harness owns orchestration and state transitions.',
37
+ 'Perform only this delegated step. Its child-side tool policy is enforced.',
38
+ 'When finished, call `structured_output` exactly once and as the only tool call in that message.',
39
+ 'Pass the workflow result as its `value`: outcome, summary, and optional artifact.',
40
+ `Valid outcomes: ${policy.outcomes.join(', ')}`,
41
+ `Pause outcomes: ${policy.pauseOutcomes.join(', ') || '(none)'}`,
42
+ `Summary limit: ${policy.summaryMaxChars} characters`,
43
+ ...(policy.gateSubmitOutcome
44
+ ? [
45
+ `Outcome "${policy.gateSubmitOutcome}" requires the complete gate artifact.`,
46
+ ]
47
+ : []),
48
+ ...(hasPauseOutcome
49
+ ? [
50
+ `If the workflow definition or environment is wrong, choose a pause outcome (${policy.pauseOutcomes.join(', ')}).`,
51
+ ]
52
+ : [
53
+ 'If the workflow definition or environment is wrong, do not fabricate success or call the completion tool; end with a concise declarative error so the parent pauses the step.',
54
+ ]),
55
+ 'This is a non-interactive workflow child. Never call contact_supervisor, subagent_supervisor, or intercom.',
56
+ ...(policy.gateSubmitOutcome
57
+ ? [
58
+ 'Put every unresolved decision in the gate artifact with evidence, options, a recommendation, and an adopted default; do not ask a terminal question.',
59
+ ]
60
+ : hasPauseOutcome
61
+ ? [
62
+ 'Treat the step instructions and incoming handoff as the final execution contract.',
63
+ 'If that contract is missing, stale, or contradictory, finish with a pause outcome and describe the unresolved contract and evidence declaratively in the summary; do not ask a terminal question.',
64
+ ]
65
+ : [
66
+ 'Treat the step instructions and incoming handoff as the final execution contract.',
67
+ 'If that contract is missing, stale, or contradictory, do not fabricate success or call the completion tool; end with a concise declarative error so the parent pauses the step. Do not ask a terminal question.',
68
+ ]),
69
+ ...(policy.repositoryCwd
70
+ ? [
71
+ `Reviewed repository root: ${policy.repositoryCwd}`,
72
+ ...(policy.bootstrapCwd
73
+ ? [
74
+ `Bootstrap directory: ${policy.bootstrapCwd}`,
75
+ 'The reviewed repository root does not exist yet. Run only its exact approved setup command first, then use absolute paths under the reviewed repository root for every edit and write. Never mutate the bootstrap directory.',
76
+ ]
77
+ : [
78
+ 'Keep every edit and write inside the reviewed repository root.',
79
+ ]),
80
+ ]
81
+ : []),
82
+ ].join('\n');
83
+ };
@@ -0,0 +1,30 @@
1
+ export type ChildRuntimePathInspection = {
2
+ readonly isDirectory: () => boolean;
3
+ readonly isFile: () => boolean;
4
+ readonly isSymbolicLink: () => boolean;
5
+ };
6
+
7
+ export type ChildRuntimeFileSystem = {
8
+ readonly exists: (path: string) => boolean;
9
+ readonly inspect: (path: string) => ChildRuntimePathInspection;
10
+ readonly readText: (path: string) => string;
11
+ readonly realPath: (path: string) => string;
12
+ readonly rename: (source: string, destination: string) => void;
13
+ readonly stat: (path: string) => ChildRuntimePathInspection;
14
+ readonly unlink: (path: string) => void;
15
+ readonly writeExclusive: (path: string, content: string) => void;
16
+ };
17
+
18
+ export type SubagentChildRuntimeDependencies = {
19
+ readonly fileSystem: ChildRuntimeFileSystem;
20
+ readonly createUniqueId: () => string;
21
+ readonly currentWorkingDirectory: () => string;
22
+ readonly environmentChildAgent: () => string | undefined;
23
+ readonly temporaryDirectory: () => string;
24
+ readonly tokensAreEqual: (actual: string, expected: string) => boolean;
25
+ };
26
+
27
+ export type SubagentChildRuntimeOptions = {
28
+ readonly childAgent?: string;
29
+ readonly dependencies?: SubagentChildRuntimeDependencies;
30
+ };