@osolmaz/pi-workflows 0.11.0 → 0.11.2

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 (36) hide show
  1. package/README.md +14 -2
  2. package/dist/builtins/autoimplement.workflow.d.ts +2 -0
  3. package/dist/builtins/autoimplement.workflow.js +186 -9
  4. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  5. package/dist/controllers/sqlite.d.ts +55 -7
  6. package/dist/controllers/sqlite.js +195 -48
  7. package/dist/controllers/sqlite.js.map +1 -1
  8. package/dist/extension/index.js +246 -54
  9. package/dist/extension/index.js.map +1 -1
  10. package/dist/herdr/setup.d.ts +13 -1
  11. package/dist/herdr/setup.js +349 -36
  12. package/dist/herdr/setup.js.map +1 -1
  13. package/dist/host/runner.js +3 -0
  14. package/dist/host/runner.js.map +1 -1
  15. package/dist/viewer/cli.d.ts +1 -0
  16. package/dist/viewer/cli.js +21 -10
  17. package/dist/viewer/cli.js.map +1 -1
  18. package/dist/workflows/migrate-sources.d.ts +1 -1
  19. package/dist/workflows/migrate-sources.js.map +1 -1
  20. package/dist/workflows/tool-input.d.ts +1 -0
  21. package/dist/workflows/tool-input.js +2 -2
  22. package/dist/workflows/tool-input.js.map +1 -1
  23. package/docs/2026-08-20-durable-workflow-launch-plan.md +449 -0
  24. package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +138 -0
  25. package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +104 -0
  26. package/docs/workflows.md +11 -0
  27. package/herdr-plugin.toml +1 -1
  28. package/package.json +1 -1
  29. package/src/builtins/autoimplement.workflow.ts +212 -9
  30. package/src/controllers/sqlite.ts +308 -54
  31. package/src/extension/index.ts +303 -58
  32. package/src/herdr/setup.ts +429 -39
  33. package/src/host/runner.ts +3 -0
  34. package/src/viewer/cli.ts +22 -10
  35. package/src/workflows/migrate-sources.ts +5 -1
  36. package/src/workflows/tool-input.ts +5 -2
package/docs/workflows.md CHANGED
@@ -419,6 +419,10 @@ The built-in `plan-approval` workflow offers verified human `continue`, `stop`,
419
419
 
420
420
  Autoimplement writes and runs the exact Pi Reviewer command. It records P0 through P2 by review round. P0 or P1 work requires another review. P2-only work can be addressed and verified without another reviewer run. CI tracking commands are also explicit. One CI watch lasts at most five minutes, after which the model runs more useful local tests before checking CI again.
421
421
 
422
+ A model-generated blocker from implementation or a safe later stage does not end autoimplement by itself. A separate blocker-challenge agent checks the task, approved plan, current result, evidence, scope, authority, earlier attempts, and practical alternatives. It confirms a blocker only when the blocker exists now, is outside the granted authority, has no safe path forward, has an empty next action, and includes concrete evidence and checked alternatives. A rejected blocker must name the next practical action and routes through the existing redesign workflow before implementation and verification continue.
423
+
424
+ Autoimplement can run the blocker challenge at most three times in one run. Each later challenge receives the earlier challenge results. Reaching the limit stops with the normal workflow safety-limit reason. Explicit human stops, cancellation, exhausted workflow or replan limits, protected authorization gaps, and an independent blocked result from redesign remain direct stops. These hard boundaries do not enter the blocker challenge.
425
+
422
426
  ### Built-in monitor
423
427
 
424
428
  The built-in `monitor` workflow turns a plain request for repeated checks into
@@ -552,6 +556,13 @@ possible. Defaults worth knowing:
552
556
  held without nudges and the engine pauses at the next boundary. Node
553
557
  timeouts keep ticking while held, so a long-abandoned step still times out.
554
558
  `/workflow resume` re-delivers the pending step prompt.
559
+ - A model-started workflow is persisted as `queued` with its final run ID before the start tool
560
+ returns. Activation waits for the initiating agent turn to settle, then moves through `starting`
561
+ and `running`. `workflow status` and `workflow cancel` accept the run ID before a run bundle
562
+ exists.
563
+ - If deferred activation fails, the queue stores a bounded safe error, releases the session
564
+ reservation, and sends one follow-up turn to the initiating model. The model can correct the
565
+ cause and make a new explicit start call. Pi Workflows does not retry blindly.
555
566
  - `/workflow cancel` aborts the current node and marks the run `cancelled`.
556
567
  When no run is live but the widget still shows a parked or finished run,
557
568
  the same command clears the widget.
package/herdr-plugin.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  id = "osolmaz.pi-workflows"
2
2
  name = "Pi Workflows"
3
- version = "0.11.0"
3
+ version = "0.11.2"
4
4
  min_herdr_version = "0.7.0"
5
5
  description = "Open the active Pi Workflows run in piw from a managed Herdr pane."
6
6
  platforms = ["linux", "macos"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osolmaz/pi-workflows",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Workflow and controller runtime with a live terminal viewer for the pi coding agent",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -87,8 +87,22 @@ export type AutoimplementBlocked = {
87
87
  evidence: unknown;
88
88
  };
89
89
 
90
+ type BlockerChallenge = {
91
+ route: "continue" | "blocked";
92
+ blockingNow: boolean;
93
+ outsideAuthority: boolean;
94
+ canProceed: boolean;
95
+ reason: string;
96
+ nextAction: string;
97
+ alternativesChecked: string[];
98
+ evidence: string[];
99
+ };
100
+
90
101
  const FIVE_MINUTES_MS = 5 * 60_000;
91
102
  const TEN_MINUTES_MS = 10 * 60_000;
103
+ const MAX_BLOCKER_CHALLENGES = 3;
104
+ const MAX_CHALLENGE_ITEMS = 5;
105
+ const MAX_CHALLENGE_TEXT = 500;
92
106
 
93
107
  function requireRecord(value: unknown, label: string): Record<string, unknown> {
94
108
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -104,6 +118,80 @@ function requireString(value: unknown, label: string): string {
104
118
  return value.trim();
105
119
  }
106
120
 
121
+ function boundedChallengeItems(value: unknown, label: string): string[] {
122
+ if (!Array.isArray(value) || value.length > MAX_CHALLENGE_ITEMS) {
123
+ throw new Error(`${label} must be an array with at most ${MAX_CHALLENGE_ITEMS} items`);
124
+ }
125
+ return value.map((item, index) => {
126
+ const text = requireString(item, `${label}[${index}]`);
127
+ if (text.length > MAX_CHALLENGE_TEXT) {
128
+ throw new Error(`${label}[${index}] must be at most ${MAX_CHALLENGE_TEXT} characters`);
129
+ }
130
+ return text;
131
+ });
132
+ }
133
+
134
+ function parseBlockerChallenge(value: unknown): BlockerChallenge {
135
+ const result = requireRecord(value, "blocker challenge");
136
+ if (result.route !== "continue" && result.route !== "blocked") {
137
+ throw new Error("blocker challenge route must be continue or blocked");
138
+ }
139
+ for (const key of ["blockingNow", "outsideAuthority", "canProceed"] as const) {
140
+ if (typeof result[key] !== "boolean") {
141
+ throw new Error(`blocker challenge ${key} must be a boolean`);
142
+ }
143
+ }
144
+ const blockingNow = result.blockingNow as boolean;
145
+ const outsideAuthority = result.outsideAuthority as boolean;
146
+ const canProceed = result.canProceed as boolean;
147
+ const reason = requireString(result.reason, "blocker challenge reason");
148
+ if (reason.length > MAX_CHALLENGE_TEXT) {
149
+ throw new Error(`blocker challenge reason must be at most ${MAX_CHALLENGE_TEXT} characters`);
150
+ }
151
+ if (typeof result.nextAction !== "string") {
152
+ throw new Error("blocker challenge nextAction must be a string");
153
+ }
154
+ const nextAction = result.nextAction.trim();
155
+ if (nextAction.length > MAX_CHALLENGE_TEXT) {
156
+ throw new Error(
157
+ `blocker challenge nextAction must be at most ${MAX_CHALLENGE_TEXT} characters`,
158
+ );
159
+ }
160
+ const alternativesChecked = boundedChallengeItems(
161
+ result.alternativesChecked,
162
+ "blocker challenge alternativesChecked",
163
+ );
164
+ const evidence = boundedChallengeItems(result.evidence, "blocker challenge evidence");
165
+
166
+ if (result.route === "blocked") {
167
+ if (
168
+ blockingNow !== true ||
169
+ outsideAuthority !== true ||
170
+ canProceed !== false ||
171
+ nextAction.length > 0 ||
172
+ alternativesChecked.length === 0 ||
173
+ evidence.length === 0
174
+ ) {
175
+ throw new Error(
176
+ "blocked challenge requires blockingNow=true, outsideAuthority=true, canProceed=false, an empty nextAction, and concrete alternatives and evidence",
177
+ );
178
+ }
179
+ } else if (canProceed !== true || nextAction.length === 0) {
180
+ throw new Error("continue challenge requires canProceed=true and a practical nextAction");
181
+ }
182
+
183
+ return {
184
+ route: result.route,
185
+ blockingNow,
186
+ outsideAuthority,
187
+ canProceed,
188
+ reason,
189
+ nextAction,
190
+ alternativesChecked,
191
+ evidence,
192
+ };
193
+ }
194
+
107
195
  function parseInput(value: unknown): AutoimplementInput {
108
196
  const input = requireRecord(value, "autoimplement input");
109
197
  const constraints = input.constraints;
@@ -339,6 +427,41 @@ function currentPlanDigest(context: WorkflowNodeContext): string {
339
427
  return digest(plan);
340
428
  }
341
429
 
430
+ function blockerChallenges(context: WorkflowNodeContext): BlockerChallenge[] {
431
+ return context.state.steps
432
+ .filter((step) => step.nodeId === "challengeBlocker" && step.outcome === "ok")
433
+ .map((step) => step.output as BlockerChallenge);
434
+ }
435
+
436
+ function latestBlockerClaim(context: WorkflowNodeContext): unknown {
437
+ const ids = [
438
+ "classifyImplementation",
439
+ "classifyVerification",
440
+ "repairReviewCommand",
441
+ "inspectComments",
442
+ "inspectCi",
443
+ "repairCiCommand",
444
+ "assessTrackedCi",
445
+ "classifyCi",
446
+ "finalizeDelivery",
447
+ ];
448
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
449
+ const step = context.state.steps[index];
450
+ if (step && ids.includes(step.nodeId)) {
451
+ return { source: step.nodeId, result: step.output };
452
+ }
453
+ }
454
+ throw new Error("No model-generated blocker claim is available to challenge");
455
+ }
456
+
457
+ function recentWorkflowAttempts(context: WorkflowNodeContext): unknown[] {
458
+ return context.state.steps.slice(-12).map((step) => ({
459
+ nodeId: step.nodeId,
460
+ outcome: step.outcome,
461
+ output: step.output,
462
+ }));
463
+ }
464
+
342
465
  function latestIssue(context: WorkflowNodeContext): unknown {
343
466
  const approval = context.outputs.approval as
344
467
  | { exit?: string; output?: { instructions?: unknown } }
@@ -351,6 +474,7 @@ function latestIssue(context: WorkflowNodeContext): unknown {
351
474
  };
352
475
  }
353
476
  const ids = [
477
+ "challengeBlocker",
354
478
  "classifyImplementation",
355
479
  "classifyVerification",
356
480
  "triageReview",
@@ -415,6 +539,8 @@ function reviewRounds(context: WorkflowNodeContext): ReviewAssessment[] {
415
539
 
416
540
  function latestBlockedReason(context: WorkflowNodeContext): { reason: string; evidence: unknown } {
417
541
  const candidates = [
542
+ "challengeBlockerGuard",
543
+ "challengeBlocker",
418
544
  "finalizeDelivery",
419
545
  "inspectCi",
420
546
  "assessTrackedCi",
@@ -634,6 +760,51 @@ export const autoimplementWorkflow = defineWorkflow({
634
760
  "implementation assessment",
635
761
  ),
636
762
  }),
763
+ challengeBlockerGuard: compute({
764
+ run: (context) => {
765
+ const challenges = blockerChallenges(context);
766
+ return challenges.length >= MAX_BLOCKER_CHALLENGES
767
+ ? {
768
+ route: "blocked",
769
+ reason: `Blocker challenge reached the ${MAX_BLOCKER_CHALLENGES}-attempt workflow safety limit.`,
770
+ evidence: { attempts: challenges.length, challenges },
771
+ }
772
+ : {
773
+ route: "challenge",
774
+ attempt: challenges.length + 1,
775
+ limit: MAX_BLOCKER_CHALLENGES,
776
+ };
777
+ },
778
+ }),
779
+ challengeBlocker: agent({
780
+ statusDetail: "challenging blocker claim",
781
+ prompt: (context) => {
782
+ const request = context.input as AutoimplementInput;
783
+ return [
784
+ "Independently challenge the latest claim that autoimplement is blocked.",
785
+ "Are you really blocked?",
786
+ "Is this really a blocker right now?",
787
+ "Can you find a safe way to move forward and finish this?",
788
+ "Are you getting stuck on something trivial, procedural, reversible, or already authorized?",
789
+ "Inspect the task, approved plan, current result, evidence, scope, authority, previous attempts, and viable alternatives.",
790
+ "Distinguish a true external blocker from ordinary rollout work, local implementation work, a design adjustment, a missing verification step, or a reversible operational task.",
791
+ "A local test failure, stale package, packaging or artifact mismatch, rollback preparation, or deployment procedure is not by itself outside authority.",
792
+ "If a safe deployment and rollback path is already authorized, a supported cutover is work to do, not a blocker.",
793
+ "Confirm blocked only when the issue blocks progress now, is outside authority, and has no safe practical path forward.",
794
+ "Return continue with the next practical action when work can proceed. Keep text concise, with at most five alternatives and five evidence items.",
795
+ `Task: ${request.task}`,
796
+ `Approved plan: ${JSON.stringify(currentPlan(context))}`,
797
+ `Current result and claimed blocker: ${JSON.stringify(latestBlockerClaim(context))}`,
798
+ `Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
799
+ `Constraints and authority: ${JSON.stringify(request.constraints ?? [])}`,
800
+ `Merge authorized: ${request.merge === true}`,
801
+ `Previous blocker challenges: ${JSON.stringify(blockerChallenges(context))}`,
802
+ `Recent workflow attempts: ${JSON.stringify(recentWorkflowAttempts(context))}`,
803
+ ].join("\n");
804
+ },
805
+ expectedOutput: `{ "route": "continue" | "blocked", "blockingNow": true | false, "outsideAuthority": true | false, "canProceed": true | false, "reason": "concise reason", "nextAction": "practical action or empty when blocked", "alternativesChecked": ["checked alternative"], "evidence": ["concrete evidence"] }`,
806
+ validate: parseBlockerChallenge,
807
+ }),
637
808
  verify: agent({
638
809
  timeoutMs: 45 * 60_000,
639
810
  statusDetail: "verifying",
@@ -1008,15 +1179,33 @@ export const autoimplementWorkflow = defineWorkflow({
1008
1179
  from: "classifyImplementation",
1009
1180
  switch: {
1010
1181
  on: "$.route",
1011
- cases: { verify: "verify", redesign: "redesign", fix: "fix", blocked: "blocked" },
1182
+ cases: {
1183
+ verify: "verify",
1184
+ redesign: "redesign",
1185
+ fix: "fix",
1186
+ blocked: "challengeBlockerGuard",
1187
+ },
1012
1188
  },
1013
1189
  },
1190
+ {
1191
+ from: "challengeBlockerGuard",
1192
+ switch: { on: "$.route", cases: { challenge: "challengeBlocker", blocked: "blocked" } },
1193
+ },
1194
+ {
1195
+ from: "challengeBlocker",
1196
+ switch: { on: "$.route", cases: { continue: "redesign", blocked: "blocked" } },
1197
+ },
1014
1198
  { from: "verify", to: "classifyVerification" },
1015
1199
  {
1016
1200
  from: "classifyVerification",
1017
1201
  switch: {
1018
1202
  on: "$.route",
1019
- cases: { publish: "publish", redesign: "redesign", fix: "fix", blocked: "blocked" },
1203
+ cases: {
1204
+ publish: "publish",
1205
+ redesign: "redesign",
1206
+ fix: "fix",
1207
+ blocked: "challengeBlockerGuard",
1208
+ },
1020
1209
  },
1021
1210
  },
1022
1211
  { from: "fix", to: "verify" },
@@ -1035,7 +1224,10 @@ export const autoimplementWorkflow = defineWorkflow({
1035
1224
  },
1036
1225
  {
1037
1226
  from: "repairReviewCommand",
1038
- switch: { on: "$.route", cases: { retry: "runReview", blocked: "blocked" } },
1227
+ switch: {
1228
+ on: "$.route",
1229
+ cases: { retry: "runReview", blocked: "challengeBlockerGuard" },
1230
+ },
1039
1231
  },
1040
1232
  {
1041
1233
  from: "assessReview",
@@ -1062,7 +1254,12 @@ export const autoimplementWorkflow = defineWorkflow({
1062
1254
  from: "inspectComments",
1063
1255
  switch: {
1064
1256
  on: "$.route",
1065
- cases: { redesign: "redesign", fix: "fix", ci: "inspectCi", blocked: "blocked" },
1257
+ cases: {
1258
+ redesign: "redesign",
1259
+ fix: "fix",
1260
+ ci: "inspectCi",
1261
+ blocked: "challengeBlockerGuard",
1262
+ },
1066
1263
  },
1067
1264
  },
1068
1265
  {
@@ -1073,7 +1270,7 @@ export const autoimplementWorkflow = defineWorkflow({
1073
1270
  green: "finalizeDelivery",
1074
1271
  failed: "classifyCi",
1075
1272
  pending: "trackCi",
1076
- unavailable: "blocked",
1273
+ unavailable: "challengeBlockerGuard",
1077
1274
  },
1078
1275
  },
1079
1276
  },
@@ -1086,7 +1283,10 @@ export const autoimplementWorkflow = defineWorkflow({
1086
1283
  },
1087
1284
  {
1088
1285
  from: "repairCiCommand",
1089
- switch: { on: "$.route", cases: { retry: "trackCi", blocked: "blocked" } },
1286
+ switch: {
1287
+ on: "$.route",
1288
+ cases: { retry: "trackCi", blocked: "challengeBlockerGuard" },
1289
+ },
1090
1290
  },
1091
1291
  {
1092
1292
  from: "assessTrackedCi",
@@ -1096,7 +1296,7 @@ export const autoimplementWorkflow = defineWorkflow({
1096
1296
  green: "finalizeDelivery",
1097
1297
  failed: "classifyCi",
1098
1298
  pending: "opportunisticTest",
1099
- unavailable: "blocked",
1299
+ unavailable: "challengeBlockerGuard",
1100
1300
  },
1101
1301
  },
1102
1302
  },
@@ -1109,13 +1309,16 @@ export const autoimplementWorkflow = defineWorkflow({
1109
1309
  redesign: "redesign",
1110
1310
  fix: "fix",
1111
1311
  unrelated: "finalizeDelivery",
1112
- blocked: "blocked",
1312
+ blocked: "challengeBlockerGuard",
1113
1313
  },
1114
1314
  },
1115
1315
  },
1116
1316
  {
1117
1317
  from: "finalizeDelivery",
1118
- switch: { on: "$.status", cases: { completed: "finalize", blocked: "blocked" } },
1318
+ switch: {
1319
+ on: "$.status",
1320
+ cases: { completed: "finalize", blocked: "challengeBlockerGuard" },
1321
+ },
1119
1322
  },
1120
1323
  ],
1121
1324
  });