@wichayutdew/pi-workflows 1.0.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.
package/README.md CHANGED
@@ -398,6 +398,8 @@ Supported sources are:
398
398
  | `verification-worker` | `repositories[].worker[].command` |
399
399
  | `verification-reviewer` | `repositories[].reviewer[].command` |
400
400
  | `remote-actions` | `actions[]` where `toolName` is `bash` and `input.command` is present |
401
+ | `remote-push` | Exact approved non-force `git push` command |
402
+ | `remote-drafts` | Parent-synthesized author-private review drafts |
401
403
 
402
404
  The JSON must be the whole reviewed artifact or appear in a fenced `json`
403
405
  block. The harness copies only exact strings into the correlated step policy.
package/dist/index.js CHANGED
@@ -29,7 +29,8 @@ function bashWithinCeiling(requested, ceiling) {
29
29
  return false;
30
30
  const allowedRules = new Set(ceiling.allow.map(ruleKey));
31
31
  const allowedSources = new Set(ceiling.approvedSources ?? []);
32
- return requested.allow.every((rule) => allowedRules.has(ruleKey(rule))) && (requested.approvedSources ?? []).every((source) => allowedSources.has(source));
32
+ const allowedHandoffSources = new Set(ceiling.handoffSources ?? []);
33
+ return requested.allow.every((rule) => allowedRules.has(ruleKey(rule))) && (requested.approvedSources ?? []).every((source) => allowedSources.has(source)) && (requested.handoffSources ?? []).every((source) => allowedHandoffSources.has(source));
33
34
  }
34
35
  function turnBudgetErrors(subagent, ceiling, path) {
35
36
  if (!subagent.turnBudget) {
@@ -135,6 +136,8 @@ var PROMPT_VARIABLES = new Set([
135
136
  "step.id",
136
137
  "step.title",
137
138
  "last.summary",
139
+ "reviewed.artifact",
140
+ "reviewed.feedback",
138
141
  "gate.feedback"
139
142
  ]);
140
143
  function validatePromptText(text, path) {
@@ -150,7 +153,7 @@ var TOOL_PATTERN = /^[A-Za-z0-9_.:-]+$/;
150
153
  var RESOURCE_SELECTOR_PATTERN = /^[A-Za-z0-9_@./:+-]+$/;
151
154
  var MCP_SELECTOR_PATTERN = /^[A-Za-z0-9_.:-]+(?:\/[A-Za-z0-9_.:-]+)?$/;
152
155
  var EXECUTABLE_PATTERN = /^[A-Za-z0-9_./+-]+$/;
153
- var BASH_APPROVAL_SOURCE_PATTERN = /^(verification-worker|verification-reviewer|remote-actions)$/;
156
+ var BASH_APPROVAL_SOURCE_PATTERN = /^(verification-worker|verification-reviewer|remote-actions|remote-push|remote-drafts)$/;
154
157
  function isJsonObject(value) {
155
158
  return value !== null && typeof value === "object" && !Array.isArray(value);
156
159
  }
@@ -215,7 +218,9 @@ function readStringList(value, path, errors, pattern) {
215
218
  var BASH_APPROVAL_SOURCES = [
216
219
  "verification-worker",
217
220
  "verification-reviewer",
218
- "remote-actions"
221
+ "remote-actions",
222
+ "remote-push",
223
+ "remote-drafts"
219
224
  ];
220
225
  function isBashApprovalSource(value) {
221
226
  return BASH_APPROVAL_SOURCES.some((source) => source === value);
@@ -282,7 +287,7 @@ function parseBashPermission(value, path, errors) {
282
287
  errors.push(`${path}: expected an object`);
283
288
  return { ...EMPTY_PERMISSIONS.bash, allow: [] };
284
289
  }
285
- rejectUnknownKeys(value, ["mode", "allow", "approvedSources"], path, errors);
290
+ rejectUnknownKeys(value, ["mode", "allow", "approvedSources", "handoffSources"], path, errors);
286
291
  const mode = readString(value.mode, `${path}.mode`, errors);
287
292
  const isValidMode = mode === "deny" || mode === "read-only" || mode === "allow-list" || mode === "unrestricted";
288
293
  if (!isValidMode) {
@@ -293,6 +298,7 @@ function parseBashPermission(value, path, errors) {
293
298
  errors.push(`${path}.allow: expected an array`);
294
299
  }
295
300
  const approvedSources = (value.approvedSources === undefined ? [] : readStringList(value.approvedSources, `${path}.approvedSources`, errors, BASH_APPROVAL_SOURCE_PATTERN)).filter(isBashApprovalSource);
301
+ const handoffSources = value.handoffSources === undefined ? [] : readStringList(value.handoffSources, `${path}.handoffSources`, errors, /^(verification-worker|verification-reviewer)$/);
296
302
  const normalizedMode = isValidMode ? mode : "deny";
297
303
  if (normalizedMode !== "allow-list" && allow.length > 0) {
298
304
  errors.push(`${path}.allow: only valid when mode is "allow-list"`);
@@ -303,10 +309,14 @@ function parseBashPermission(value, path, errors) {
303
309
  if (normalizedMode !== "allow-list" && approvedSources.length > 0) {
304
310
  errors.push(`${path}.approvedSources: only valid when mode is "allow-list"`);
305
311
  }
312
+ if (normalizedMode !== "allow-list" && handoffSources.length > 0) {
313
+ errors.push(`${path}.handoffSources: only valid when mode is "allow-list"`);
314
+ }
306
315
  return {
307
316
  mode: normalizedMode,
308
317
  allow,
309
- ...approvedSources.length > 0 ? { approvedSources } : {}
318
+ ...approvedSources.length > 0 ? { approvedSources } : {},
319
+ ...handoffSources.length > 0 ? { handoffSources } : {}
310
320
  };
311
321
  }
312
322
  function parsePermissions(value, path, errors) {
@@ -2212,9 +2222,10 @@ var isStepPermissions = (value) => {
2212
2222
  const bashRules = Array.isArray(bash.allow) ? bash.allow : undefined;
2213
2223
  const isValidMode = bash.mode === "deny" || bash.mode === "read-only" || bash.mode === "allow-list" || bash.mode === "unrestricted";
2214
2224
  const hasValidRules = bashRules !== undefined && bashRules.every((rule) => isRecord(rule) && typeof rule.executable === "string" && isStringArray(rule.argsPrefix));
2215
- const hasValidApprovedSources = bash.approvedSources === undefined || isStringArray(bash.approvedSources) && new Set(bash.approvedSources).size === bash.approvedSources.length && bash.approvedSources.every((source) => source === "verification-worker" || source === "verification-reviewer" || source === "remote-actions");
2225
+ const hasValidApprovedSources = bash.approvedSources === undefined || isStringArray(bash.approvedSources) && new Set(bash.approvedSources).size === bash.approvedSources.length && bash.approvedSources.every((source) => source === "verification-worker" || source === "verification-reviewer" || source === "remote-actions" || source === "remote-push" || source === "remote-drafts");
2226
+ const hasValidHandoffSources = bash.handoffSources === undefined || isStringArray(bash.handoffSources) && new Set(bash.handoffSources).size === bash.handoffSources.length && bash.handoffSources.every((source) => source === "verification-worker" || source === "verification-reviewer");
2216
2227
  const hasValidApprovalShape = bash.mode === "allow-list" ? (bashRules?.length ?? 0) > 0 || Array.isArray(bash.approvedSources) && bash.approvedSources.length > 0 : bash.approvedSources === undefined;
2217
- return isStringArray(value.tools) && isStringArray(value.mcp) && isStringArray(value.extensions) && isStringArray(value.skills) && isValidMode && hasValidRules && hasValidApprovedSources && hasValidApprovalShape;
2228
+ return isStringArray(value.tools) && isStringArray(value.mcp) && isStringArray(value.extensions) && isStringArray(value.skills) && isValidMode && hasValidRules && hasValidApprovedSources && hasValidHandoffSources && hasValidApprovalShape;
2218
2229
  };
2219
2230
  var parsePermissions2 = (value) => {
2220
2231
  if (!isStepPermissions(value.permissions)) {
@@ -4238,6 +4249,7 @@ var createRun = (workflow, input, baselineTools, runId, now) => {
4238
4249
  startedAt: now,
4239
4250
  updatedAt: now,
4240
4251
  reviewedArtifact: "",
4252
+ reviewedFeedback: "",
4241
4253
  stepHandoff: "",
4242
4254
  lastSummary: "",
4243
4255
  gateFeedback: ""
@@ -4260,7 +4272,7 @@ var isWorkflowRun = (value) => {
4260
4272
  const hasValidRequiredFields = value.stateVersion === RUN_STATE_VERSION && typeof value.runId === "string" && typeof value.workflowId === "string" && typeof value.workflowDigest === "string" && typeof value.input === "string" && isWorkflowRunStatus(value.status) && typeof value.currentStepId === "string" && typeof value.currentStepDigest === "string" && Array.isArray(value.baselineTools) && value.baselineTools.every((tool) => typeof tool === "string") && Array.isArray(value.history) && value.history.every(isStepHistoryEntry) && isVisitCounts(value.visits) && typeof value.startedAt === "number" && typeof value.updatedAt === "number" && typeof value.lastSummary === "string" && typeof value.gateFeedback === "string";
4261
4273
  if (!hasValidRequiredFields)
4262
4274
  return false;
4263
- const hasValidOptionalFields = isOptionalString(value.reviewedArtifact) && isOptionalString(value.stepHandoff) && isOptionalString(value.pauseReason) && isOptionalString(value.failedStepId) && (value.pausedFrom === undefined || value.pausedFrom === "running" || value.pausedFrom === "awaiting-gate");
4275
+ const hasValidOptionalFields = isOptionalString(value.reviewedArtifact) && isOptionalString(value.reviewedFeedback) && isOptionalString(value.stepHandoff) && isOptionalString(value.pauseReason) && isOptionalString(value.failedStepId) && (value.pausedFrom === undefined || value.pausedFrom === "running" || value.pausedFrom === "awaiting-gate");
4264
4276
  if (!hasValidOptionalFields)
4265
4277
  return false;
4266
4278
  const pendingGate = value.pendingGate;
@@ -4426,7 +4438,7 @@ var advanceRun = (workflow, run, outcome, summary, now) => {
4426
4438
  stepHandoff: summary,
4427
4439
  lastSummary: summary,
4428
4440
  gateFeedback: "",
4429
- ...nextStep.gate ? { reviewedArtifact: "" } : {}
4441
+ ...nextStep.gate ? { reviewedArtifact: "", reviewedFeedback: "" } : {}
4430
4442
  }, now);
4431
4443
  };
4432
4444
 
@@ -4489,7 +4501,10 @@ var resolveGate = (workflow, run, resolution, now) => {
4489
4501
  const runnableRun = withRunUpdate(run, {
4490
4502
  status: "running",
4491
4503
  pendingGate: undefined,
4492
- ...resolution.approved ? { reviewedArtifact: pendingGate.artifact } : {},
4504
+ ...resolution.approved ? {
4505
+ reviewedArtifact: pendingGate.artifact,
4506
+ reviewedFeedback: resolution.feedback
4507
+ } : {},
4493
4508
  pausedFrom: undefined,
4494
4509
  pauseReason: undefined,
4495
4510
  gateFeedback: resolution.feedback
@@ -5029,6 +5044,8 @@ function createTemplateValues({
5029
5044
  "step.id": run.currentStepId,
5030
5045
  "step.title": step.title,
5031
5046
  "last.summary": currentStepHandoff(run),
5047
+ "reviewed.artifact": run.reviewedArtifact ?? "",
5048
+ "reviewed.feedback": run.reviewedFeedback ?? "",
5032
5049
  "gate.feedback": run.gateFeedback
5033
5050
  };
5034
5051
  }
@@ -85,6 +85,7 @@ steps:
85
85
  skills: [superpowers:test-driven-development]
86
86
  transitions:
87
87
  ready: verify
88
+ retry: implement
88
89
  blocked: $pause
89
90
  verify:
90
91
  title: Verify the result
@@ -112,4 +113,5 @@ steps:
112
113
  transitions:
113
114
  passed: $done
114
115
  failed: implement
116
+ retry: verify
115
117
  blocked: $pause
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wichayutdew/pi-workflows",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A declarative, pauseable workflow harness for Pi",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -223,9 +223,18 @@
223
223
  "enum": [
224
224
  "verification-worker",
225
225
  "verification-reviewer",
226
- "remote-actions"
226
+ "remote-actions",
227
+ "remote-push",
228
+ "remote-drafts"
227
229
  ]
228
230
  }
231
+ },
232
+ "handoffSources": {
233
+ "type": "array",
234
+ "uniqueItems": true,
235
+ "items": {
236
+ "enum": ["verification-worker", "verification-reviewer"]
237
+ }
229
238
  }
230
239
  }
231
240
  },
@@ -33,10 +33,14 @@ function bashWithinCeiling(
33
33
 
34
34
  const allowedRules = new Set(ceiling.allow.map(ruleKey));
35
35
  const allowedSources = new Set(ceiling.approvedSources ?? []);
36
+ const allowedHandoffSources = new Set(ceiling.handoffSources ?? []);
36
37
  return (
37
38
  requested.allow.every((rule) => allowedRules.has(ruleKey(rule))) &&
38
39
  (requested.approvedSources ?? []).every((source) =>
39
40
  allowedSources.has(source),
41
+ ) &&
42
+ (requested.handoffSources ?? []).every((source) =>
43
+ allowedHandoffSources.has(source),
40
44
  )
41
45
  );
42
46
  }
@@ -11,7 +11,11 @@ export type StepTarget = string;
11
11
 
12
12
  export type BashMode = 'deny' | 'read-only' | 'allow-list' | 'unrestricted';
13
13
  export type BashApprovalSource =
14
- 'verification-worker' | 'verification-reviewer' | 'remote-actions';
14
+ | 'verification-worker'
15
+ | 'verification-reviewer'
16
+ | 'remote-actions'
17
+ | 'remote-push'
18
+ | 'remote-drafts';
15
19
 
16
20
  export type BashRule = {
17
21
  readonly executable: string;
@@ -26,6 +30,13 @@ export type BashPermission = {
26
30
  * They supplement `allow` only inside the correlated step execution.
27
31
  */
28
32
  readonly approvedSources?: ReadonlyArray<BashApprovalSource>;
33
+ /**
34
+ * Sources whose command-only retry handoffs may refine an already approved
35
+ * command. The original reviewed identity and safety envelope remain fixed.
36
+ */
37
+ readonly handoffSources?: ReadonlyArray<
38
+ 'verification-worker' | 'verification-reviewer'
39
+ >;
29
40
  };
30
41
 
31
42
  export type StepPermissions = {
@@ -24,6 +24,8 @@ const BASH_APPROVAL_SOURCES = [
24
24
  'verification-worker',
25
25
  'verification-reviewer',
26
26
  'remote-actions',
27
+ 'remote-push',
28
+ 'remote-drafts',
27
29
  ] as const satisfies ReadonlyArray<BashApprovalSource>;
28
30
 
29
31
  function isBashApprovalSource(value: string): value is BashApprovalSource {
@@ -129,7 +131,12 @@ function parseBashPermission(
129
131
  errors.push(`${path}: expected an object`);
130
132
  return { ...EMPTY_PERMISSIONS.bash, allow: [] };
131
133
  }
132
- rejectUnknownKeys(value, ['mode', 'allow', 'approvedSources'], path, errors);
134
+ rejectUnknownKeys(
135
+ value,
136
+ ['mode', 'allow', 'approvedSources', 'handoffSources'],
137
+ path,
138
+ errors,
139
+ );
133
140
 
134
141
  const mode = readString(value.mode, `${path}.mode`, errors);
135
142
  const isValidMode =
@@ -162,6 +169,16 @@ function parseBashPermission(
162
169
  BASH_APPROVAL_SOURCE_PATTERN,
163
170
  )
164
171
  ).filter(isBashApprovalSource);
172
+ const handoffSources = (
173
+ value.handoffSources === undefined
174
+ ? []
175
+ : readStringList(
176
+ value.handoffSources,
177
+ `${path}.handoffSources`,
178
+ errors,
179
+ /^(verification-worker|verification-reviewer)$/,
180
+ )
181
+ ) as Array<'verification-worker' | 'verification-reviewer'>;
165
182
 
166
183
  const normalizedMode: BashMode = isValidMode ? mode : 'deny';
167
184
  if (normalizedMode !== 'allow-list' && allow.length > 0) {
@@ -181,11 +198,15 @@ function parseBashPermission(
181
198
  `${path}.approvedSources: only valid when mode is "allow-list"`,
182
199
  );
183
200
  }
201
+ if (normalizedMode !== 'allow-list' && handoffSources.length > 0) {
202
+ errors.push(`${path}.handoffSources: only valid when mode is "allow-list"`);
203
+ }
184
204
 
185
205
  return {
186
206
  mode: normalizedMode,
187
207
  allow,
188
208
  ...(approvedSources.length > 0 ? { approvedSources } : {}),
209
+ ...(handoffSources.length > 0 ? { handoffSources } : {}),
189
210
  };
190
211
  }
191
212
 
@@ -5,6 +5,8 @@ const PROMPT_VARIABLES = new Set([
5
5
  'step.id',
6
6
  'step.title',
7
7
  'last.summary',
8
+ 'reviewed.artifact',
9
+ 'reviewed.feedback',
8
10
  'gate.feedback',
9
11
  ]);
10
12
 
@@ -13,7 +13,7 @@ export const RESOURCE_SELECTOR_PATTERN = /^[A-Za-z0-9_@./:+-]+$/;
13
13
  export const MCP_SELECTOR_PATTERN = /^[A-Za-z0-9_.:-]+(?:\/[A-Za-z0-9_.:-]+)?$/;
14
14
  export const EXECUTABLE_PATTERN = /^[A-Za-z0-9_./+-]+$/;
15
15
  export const BASH_APPROVAL_SOURCE_PATTERN =
16
- /^(verification-worker|verification-reviewer|remote-actions)$/;
16
+ /^(verification-worker|verification-reviewer|remote-actions|remote-push|remote-drafts)$/;
17
17
 
18
18
  type StringOptions = {
19
19
  readonly pattern?: RegExp;
@@ -35,6 +35,7 @@ export const createRun = (
35
35
  startedAt: now,
36
36
  updatedAt: now,
37
37
  reviewedArtifact: '',
38
+ reviewedFeedback: '',
38
39
  stepHandoff: '',
39
40
  lastSummary: '',
40
41
  gateFeedback: '',
@@ -169,7 +169,10 @@ export const resolveGate = (
169
169
  status: 'running',
170
170
  pendingGate: undefined,
171
171
  ...(resolution.approved
172
- ? { reviewedArtifact: pendingGate.artifact }
172
+ ? {
173
+ reviewedArtifact: pendingGate.artifact,
174
+ reviewedFeedback: resolution.feedback,
175
+ }
173
176
  : {}),
174
177
  pausedFrom: undefined,
175
178
  pauseReason: undefined,
@@ -115,7 +115,7 @@ export const advanceRun = (
115
115
  stepHandoff: summary,
116
116
  lastSummary: summary,
117
117
  gateFeedback: '',
118
- ...(nextStep.gate ? { reviewedArtifact: '' } : {}),
118
+ ...(nextStep.gate ? { reviewedArtifact: '', reviewedFeedback: '' } : {}),
119
119
  },
120
120
  now,
121
121
  );
@@ -109,6 +109,7 @@ export const isWorkflowRun = (value: unknown): value is WorkflowRun => {
109
109
 
110
110
  const hasValidOptionalFields =
111
111
  isOptionalString(value.reviewedArtifact) &&
112
+ isOptionalString(value.reviewedFeedback) &&
112
113
  isOptionalString(value.stepHandoff) &&
113
114
  isOptionalString(value.pauseReason) &&
114
115
  isOptionalString(value.failedStepId) &&
@@ -49,6 +49,8 @@ export type WorkflowRun = {
49
49
  * source from which reviewed Bash capabilities may be derived.
50
50
  */
51
51
  readonly reviewedArtifact?: string | undefined;
52
+ /** Final feedback paired with the immutable approved artifact. */
53
+ readonly reviewedFeedback?: string | undefined;
52
54
  /**
53
55
  * Input inherited from the previous completed step. Unlike `lastSummary`,
54
56
  * this survives a paused attempt of the current step.
@@ -46,7 +46,18 @@ const isStepPermissions = (value: unknown): value is StepPermissions => {
46
46
  (source) =>
47
47
  source === 'verification-worker' ||
48
48
  source === 'verification-reviewer' ||
49
- source === 'remote-actions',
49
+ source === 'remote-actions' ||
50
+ source === 'remote-push' ||
51
+ source === 'remote-drafts',
52
+ ));
53
+ const hasValidHandoffSources =
54
+ bash.handoffSources === undefined ||
55
+ (isStringArray(bash.handoffSources) &&
56
+ new Set(bash.handoffSources).size === bash.handoffSources.length &&
57
+ bash.handoffSources.every(
58
+ (source) =>
59
+ source === 'verification-worker' ||
60
+ source === 'verification-reviewer',
50
61
  ));
51
62
  const hasValidApprovalShape =
52
63
  bash.mode === 'allow-list'
@@ -62,6 +73,7 @@ const isStepPermissions = (value: unknown): value is StepPermissions => {
62
73
  isValidMode &&
63
74
  hasValidRules &&
64
75
  hasValidApprovedSources &&
76
+ hasValidHandoffSources &&
65
77
  hasValidApprovalShape
66
78
  );
67
79
  };
@@ -76,6 +76,8 @@ 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,
80
82
  };
81
83
  }