@osolmaz/pi-workflows 0.12.0 → 0.12.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 (119) hide show
  1. package/README.md +4 -3
  2. package/dist/builtins/autoimplement.workflow.d.ts +485 -126
  3. package/dist/builtins/autoimplement.workflow.js +17 -105
  4. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  5. package/dist/builtins/catalog.js +4 -4
  6. package/dist/builtins/index.d.ts +2 -1
  7. package/dist/builtins/index.js +2 -1
  8. package/dist/builtins/index.js.map +1 -1
  9. package/dist/builtins/monitor.workflow.d.ts +2 -4
  10. package/dist/builtins/monitor.workflow.js +26 -128
  11. package/dist/builtins/monitor.workflow.js.map +1 -1
  12. package/dist/builtins/pi-agent-group.d.ts +72 -0
  13. package/dist/builtins/pi-agent-group.js +1087 -0
  14. package/dist/builtins/pi-agent-group.js.map +1 -0
  15. package/dist/builtins/plan-approval.workflow.d.ts +39 -5
  16. package/dist/builtins/plan-approval.workflow.js +92 -14
  17. package/dist/builtins/plan-approval.workflow.js.map +1 -1
  18. package/dist/builtins/plan-change.workflow.d.ts +301 -0
  19. package/dist/builtins/plan-change.workflow.js +256 -0
  20. package/dist/builtins/plan-change.workflow.js.map +1 -0
  21. package/dist/builtins/plan-presentation.js +2 -2
  22. package/dist/builtins/plan-presentation.js.map +1 -1
  23. package/dist/builtins/sanity-check.workflow.d.ts +5 -3
  24. package/dist/builtins/sanity-check.workflow.js +105 -21
  25. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  26. package/dist/extension/decision-channels.d.ts +2 -2
  27. package/dist/extension/decision-channels.js +22 -28
  28. package/dist/extension/decision-channels.js.map +1 -1
  29. package/dist/extension/index.js +62 -33
  30. package/dist/extension/index.js.map +1 -1
  31. package/dist/extension/session-events.d.ts +2 -2
  32. package/dist/extension/widget.js +23 -3
  33. package/dist/extension/widget.js.map +1 -1
  34. package/dist/render/graph-render.js +1 -2
  35. package/dist/render/graph-render.js.map +1 -1
  36. package/dist/viewer/render.js +7 -6
  37. package/dist/viewer/render.js.map +1 -1
  38. package/dist/workflows/catalog.js +7 -2
  39. package/dist/workflows/catalog.js.map +1 -1
  40. package/dist/workflows/composition.js +8 -0
  41. package/dist/workflows/composition.js.map +1 -1
  42. package/dist/workflows/decision-presentation.d.ts +1 -1
  43. package/dist/workflows/decision-presentation.js +51 -38
  44. package/dist/workflows/decision-presentation.js.map +1 -1
  45. package/dist/workflows/engine.d.ts +2 -2
  46. package/dist/workflows/engine.js +14 -13
  47. package/dist/workflows/engine.js.map +1 -1
  48. package/dist/workflows/errors.d.ts +13 -0
  49. package/dist/workflows/errors.js +15 -0
  50. package/dist/workflows/errors.js.map +1 -1
  51. package/dist/workflows/human-decision.d.ts +16 -4
  52. package/dist/workflows/human-decision.js +175 -72
  53. package/dist/workflows/human-decision.js.map +1 -1
  54. package/dist/workflows/index.d.ts +2 -2
  55. package/dist/workflows/index.js +1 -1
  56. package/dist/workflows/index.js.map +1 -1
  57. package/dist/workflows/progress.d.ts +1 -0
  58. package/dist/workflows/progress.js +15 -3
  59. package/dist/workflows/progress.js.map +1 -1
  60. package/dist/workflows/schema.js +10 -0
  61. package/dist/workflows/schema.js.map +1 -1
  62. package/dist/workflows/store.js +5 -0
  63. package/dist/workflows/store.js.map +1 -1
  64. package/dist/workflows/types.d.ts +33 -45
  65. package/docs/HUMAN_DECISIONS.md +25 -35
  66. package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
  67. package/docs/MONITOR.md +5 -11
  68. package/docs/WORKFLOW_COMPOSITION.md +8 -7
  69. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
  70. package/docs/plans/2026-08-21-sanity-check-plan.md +202 -94
  71. package/docs/run-bundles.md +6 -6
  72. package/docs/workflows.md +26 -6
  73. package/examples/workflows/approved-plan.workflow.ts +19 -46
  74. package/herdr-plugin.toml +1 -1
  75. package/package.json +7 -7
  76. package/schemas/human-decision-accepted-v1.schema.json +15 -3
  77. package/schemas/human-decision-continuation-v1.schema.json +10 -1
  78. package/schemas/human-decision-delivery-v1.schema.json +8 -0
  79. package/schemas/human-decision-receipt-v1.schema.json +8 -0
  80. package/schemas/human-decision-request-v1.schema.json +24 -4
  81. package/skills/autoimplement/SKILL.md +27 -0
  82. package/skills/monitor/SKILL.md +31 -3
  83. package/skills/pi-workflows/SKILL.md +2 -1
  84. package/skills/sanity-check/SKILL.md +44 -0
  85. package/src/builtins/autoimplement.workflow.ts +19 -118
  86. package/src/builtins/catalog.ts +4 -4
  87. package/src/builtins/index.ts +11 -0
  88. package/src/builtins/monitor.workflow.ts +27 -150
  89. package/src/builtins/pi-agent-group.ts +1407 -0
  90. package/src/builtins/plan-approval.workflow.ts +157 -24
  91. package/src/builtins/plan-change.workflow.ts +321 -0
  92. package/src/builtins/plan-presentation.ts +2 -2
  93. package/src/builtins/sanity-check.workflow.ts +186 -41
  94. package/src/extension/decision-channels.ts +29 -59
  95. package/src/extension/index.ts +79 -41
  96. package/src/extension/session-events.ts +2 -2
  97. package/src/extension/widget.ts +24 -5
  98. package/src/render/graph-render.ts +1 -2
  99. package/src/viewer/render.ts +7 -6
  100. package/src/workflows/catalog.ts +7 -2
  101. package/src/workflows/composition.ts +9 -0
  102. package/src/workflows/decision-presentation.ts +56 -43
  103. package/src/workflows/engine.ts +17 -15
  104. package/src/workflows/errors.ts +24 -0
  105. package/src/workflows/human-decision.ts +218 -101
  106. package/src/workflows/index.ts +5 -11
  107. package/src/workflows/progress.ts +18 -3
  108. package/src/workflows/schema.ts +17 -0
  109. package/src/workflows/store.ts +5 -0
  110. package/src/workflows/types.ts +39 -56
  111. package/dist/builtins/sanity-check-session.d.ts +0 -17
  112. package/dist/builtins/sanity-check-session.js +0 -168
  113. package/dist/builtins/sanity-check-session.js.map +0 -1
  114. package/schemas/human-decision-accepted-v2.schema.json +0 -50
  115. package/schemas/human-decision-delivery-v2.schema.json +0 -36
  116. package/schemas/human-decision-receipt-v2.schema.json +0 -39
  117. package/schemas/human-decision-request-v2.schema.json +0 -69
  118. package/schemas/human-decision-resolution-v2.schema.json +0 -27
  119. package/src/builtins/sanity-check-session.ts +0 -205
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osolmaz/pi-workflows",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Workflow and controller runtime with a live terminal viewer for the pi coding agent",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -69,9 +69,9 @@
69
69
  "jiti": "^2.7.0"
70
70
  },
71
71
  "devDependencies": {
72
- "@earendil-works/pi-ai": "^0.80.10",
73
- "@earendil-works/pi-coding-agent": "^0.80.10",
74
- "@earendil-works/pi-tui": "^0.80.10",
72
+ "@earendil-works/pi-ai": "0.84.2",
73
+ "@earendil-works/pi-coding-agent": "0.84.2",
74
+ "@earendil-works/pi-tui": "0.84.2",
75
75
  "@types/better-sqlite3": "^7.6.13",
76
76
  "@types/node": "^26.1.1",
77
77
  "@vitest/coverage-istanbul": "^4.1.10",
@@ -83,9 +83,9 @@
83
83
  "vitest": "^4.1.10"
84
84
  },
85
85
  "peerDependencies": {
86
- "@earendil-works/pi-ai": "*",
87
- "@earendil-works/pi-coding-agent": "*",
88
- "@earendil-works/pi-tui": "*",
86
+ "@earendil-works/pi-ai": ">=0.84.2 <0.85.0",
87
+ "@earendil-works/pi-coding-agent": ">=0.84.2 <0.85.0",
88
+ "@earendil-works/pi-tui": ">=0.84.2 <0.85.0",
89
89
  "typebox": "*"
90
90
  },
91
91
  "engines": {
@@ -8,9 +8,11 @@
8
8
  "schema",
9
9
  "decisionId",
10
10
  "requestDigest",
11
+ "subjectDigest",
12
+ "presentationDigest",
13
+ "revision",
11
14
  "response",
12
- "source",
13
- "idempotencyKey",
15
+ "provenance",
14
16
  "acceptedAt",
15
17
  "answerDigest"
16
18
  ],
@@ -18,6 +20,9 @@
18
20
  "schema": { "const": "pi-workflows.human-decision-accepted.v1" },
19
21
  "decisionId": { "type": "string" },
20
22
  "requestDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
23
+ "subjectDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
24
+ "presentationDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
25
+ "revision": { "type": "integer", "minimum": 1 },
21
26
  "response": {
22
27
  "type": "object",
23
28
  "additionalProperties": false,
@@ -27,6 +32,7 @@
27
32
  "input": { "type": "object", "additionalProperties": { "type": "string" } }
28
33
  }
29
34
  },
35
+ "provenance": { "enum": ["human", "timeout"] },
30
36
  "source": {
31
37
  "type": "object",
32
38
  "additionalProperties": false,
@@ -40,5 +46,11 @@
40
46
  "idempotencyKey": { "type": "string" },
41
47
  "acceptedAt": { "type": "string", "format": "date-time" },
42
48
  "answerDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
43
- }
49
+ },
50
+ "allOf": [
51
+ {
52
+ "if": { "properties": { "provenance": { "const": "human" } } },
53
+ "then": { "required": ["source", "idempotencyKey"] }
54
+ }
55
+ ]
44
56
  }
@@ -4,11 +4,20 @@
4
4
  "title": "pi-workflows human decision continuation v1",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
- "required": ["schema", "decisionId", "requestDigest", "parentRunId", "runId", "createdAt"],
7
+ "required": [
8
+ "schema",
9
+ "decisionId",
10
+ "requestDigest",
11
+ "provenance",
12
+ "parentRunId",
13
+ "runId",
14
+ "createdAt"
15
+ ],
8
16
  "properties": {
9
17
  "schema": { "const": "pi-workflows.human-decision-continuation.v1" },
10
18
  "decisionId": { "type": "string" },
11
19
  "requestDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
20
+ "provenance": { "enum": ["human", "timeout"] },
12
21
  "parentRunId": { "type": "string" },
13
22
  "runId": { "type": "string" },
14
23
  "createdAt": { "type": "string", "format": "date-time" }
@@ -9,7 +9,9 @@
9
9
  "attemptId",
10
10
  "decisionId",
11
11
  "requestDigest",
12
+ "presentationDigest",
12
13
  "channel",
14
+ "phase",
13
15
  "state",
14
16
  "createdAt"
15
17
  ],
@@ -18,10 +20,16 @@
18
20
  "attemptId": { "type": "string" },
19
21
  "decisionId": { "type": "string" },
20
22
  "requestDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
23
+ "presentationDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
21
24
  "channel": { "type": "string" },
25
+ "phase": { "enum": ["intent", "part", "complete"] },
22
26
  "state": { "enum": ["intent", "confirmed", "failed", "unknown"] },
23
27
  "createdAt": { "type": "string", "format": "date-time" },
24
28
  "finishedAt": { "type": "string", "format": "date-time" },
29
+ "recipientIndex": { "type": "integer", "minimum": 1 },
30
+ "partIndex": { "type": "integer", "minimum": 1 },
31
+ "partCount": { "type": "integer", "minimum": 1, "maximum": 20 },
32
+ "contentDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
25
33
  "messageCount": { "type": "integer", "minimum": 0 },
26
34
  "errorCode": { "type": "string" }
27
35
  }
@@ -8,8 +8,12 @@
8
8
  "schema",
9
9
  "decisionId",
10
10
  "requestDigest",
11
+ "subjectDigest",
12
+ "presentationDigest",
13
+ "revision",
11
14
  "nodeId",
12
15
  "response",
16
+ "provenance",
13
17
  "acceptedAt",
14
18
  "answerDigest"
15
19
  ],
@@ -17,6 +21,9 @@
17
21
  "schema": { "const": "pi-workflows.human-decision-receipt.v1" },
18
22
  "decisionId": { "type": "string" },
19
23
  "requestDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
24
+ "subjectDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
25
+ "presentationDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
26
+ "revision": { "type": "integer", "minimum": 1 },
20
27
  "nodeId": { "type": "string" },
21
28
  "response": {
22
29
  "type": "object",
@@ -27,6 +34,7 @@
27
34
  "input": { "type": "object", "additionalProperties": { "type": "string" } }
28
35
  }
29
36
  },
37
+ "provenance": { "enum": ["human", "timeout"] },
30
38
  "acceptedAt": { "type": "string", "format": "date-time" },
31
39
  "answerDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
32
40
  }
@@ -8,13 +8,17 @@
8
8
  "schema",
9
9
  "decisionId",
10
10
  "requestDigest",
11
+ "subjectDigest",
12
+ "presentationDigest",
11
13
  "runId",
12
14
  "workflowName",
13
15
  "nodeId",
14
16
  "attemptId",
15
17
  "audience",
16
18
  "title",
17
- "body",
19
+ "subject",
20
+ "presentation",
21
+ "revision",
18
22
  "choices",
19
23
  "createdAt"
20
24
  ],
@@ -22,13 +26,19 @@
22
26
  "schema": { "const": "pi-workflows.human-decision-request.v1" },
23
27
  "decisionId": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$" },
24
28
  "requestDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
29
+ "subjectDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
30
+ "presentationDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
25
31
  "runId": { "type": "string" },
26
32
  "workflowName": { "type": "string" },
27
33
  "nodeId": { "type": "string" },
28
34
  "attemptId": { "type": "string" },
29
35
  "audience": { "type": "string" },
30
36
  "title": { "type": "string", "minLength": 1 },
31
- "body": true,
37
+ "subject": true,
38
+ "presentation": {
39
+ "$ref": "decision-presentation-v1.schema.json"
40
+ },
41
+ "revision": { "type": "integer", "minimum": 1 },
32
42
  "choices": {
33
43
  "type": "object",
34
44
  "minProperties": 1,
@@ -54,6 +64,16 @@
54
64
  }
55
65
  },
56
66
  "createdAt": { "type": "string", "format": "date-time" },
57
- "expiresAt": { "type": "string", "format": "date-time" }
58
- }
67
+ "expiresAt": { "type": "string", "format": "date-time" },
68
+ "defaultResponse": {
69
+ "type": "object",
70
+ "additionalProperties": false,
71
+ "required": ["choice"],
72
+ "properties": {
73
+ "choice": { "type": "string" },
74
+ "input": { "type": "object", "additionalProperties": { "type": "string" } }
75
+ }
76
+ }
77
+ },
78
+ "dependentRequired": { "defaultResponse": ["expiresAt"] }
59
79
  }
@@ -20,6 +20,7 @@ Build the input as follows:
20
20
  - `baseBranch`: Use the requested base or the repository default branch.
21
21
  - `merge`: Set `true` only when the user explicitly requested merge or an applicable standing instruction authorizes it. Otherwise set `false`.
22
22
  - `documents`: Include known canonical plan or specification paths. Use an empty array when none are known.
23
+ - `approval`: Omit it for the default behavior: ask on each new plan and continue after 10 minutes without an answer. Use `{ "mode": "required" }` when the user says to block on plan changes. Use `{ "mode": "skip" }` when the user says to continue without asking about plan changes.
23
24
  - `concurrency`: Include it only when the conversation gives explicit limits.
24
25
 
25
26
  When one repository is clearly named, derive the scope without asking the user to restate it. A safe derived scope permits only work needed for the task in that repository, including local verification and normal branch and pull-request publication. It excludes unrelated repositories, merge, release, deployment, credentials, and policy changes unless those actions are explicitly authorized.
@@ -51,6 +52,32 @@ Replace the example values below with facts from the conversation, then make one
51
52
  }
52
53
  ```
53
54
 
55
+ ### Plan-change decisions
56
+
57
+ The workflow gates only plans that it creates or changes after the run starts. A supplied or discovered existing plan does not receive another decision.
58
+
59
+ Use required approval when the user says to block on plan changes:
60
+
61
+ ```json
62
+ {
63
+ "approval": {
64
+ "mode": "required"
65
+ }
66
+ }
67
+ ```
68
+
69
+ Skip plan decisions when the user says to accept every new plan immediately:
70
+
71
+ ```json
72
+ {
73
+ "approval": {
74
+ "mode": "skip"
75
+ }
76
+ }
77
+ ```
78
+
79
+ Omit `approval` for autonomous mode. It asks the `operator` audience and continues with the exact presented plan after 10 minutes without an accepted answer. The workflow owns this decision. The model must not answer the protected decision through the workflow tool.
80
+
54
81
  Do not manually duplicate stages already owned by the workflow. Autoimplement runs independent pi-reviewer commands, pending CI watches, and local verification commands from separate repositories in bounded batches. It keeps model turns, fixes, pushes, comment changes, merges, and releases ordered. One repository uses the same batch path with concurrency one.
55
82
 
56
83
  When this skill is loaded inside an active workflow step, do not start another workflow. Complete the current step contract with the available tools.
@@ -29,7 +29,7 @@ Derive the workflow input from the full conversation:
29
29
  - `task`: State the complete objective, the exact current target and stable identifiers, authoritative status sources, durable progress and final-output surfaces, routine actions authorized by the monitor request, other recorded approvals, immutable boundaries, cost and credential rules, and required validation or downstream operations.
30
30
  - `everyMinutes`: Use the user's interval when present. Use `30` when the user gives no interval. The built-in workflow accepts intervals from 1 minute through 24 hours.
31
31
  - `stopWhen`: Infer verified completion from the full conversation. Describe completion of the complete objective, not only the end of one physical process. Also name material blockers that require human intervention.
32
- - `repair`: Include this object only when the request or an existing approval authorizes mutation. Set `authorized: true` and record the repository, scope, base branch, merge policy, and constraints that apply. Omit it for observation-only work.
32
+ - `repair`: Include this object only when the request or an existing approval authorizes mutation. Set `authorized: true` and record the repository, scope, base branch, merge policy, and constraints that apply. Omit it for observation-only work. Omit `repair.approval` for the default behavior: ask on each new repair plan and continue after 10 minutes without an answer. Use `{ "mode": "required" }` to block on plan changes or `{ "mode": "skip" }` to continue without asking.
33
33
 
34
34
  Replace the example values below with facts from the conversation, then make one start call:
35
35
 
@@ -65,9 +65,37 @@ When the conversation gives no clear finish criterion, set `stopWhen` to `Stop o
65
65
 
66
66
  Do not invent a finite check count. Omit `maxChecks` unless the user explicitly requests one. The workflow host can apply its own safety upper bound. Disclose that bound if it appears.
67
67
 
68
- When repair is authorized, route a concrete code or design defect through the monitor's composed repair path. Supply the problem, observed evidence, and a stable fingerprint of the issue plus target state. The workflow runs outer `autoplan`, standalone `autodoc`, optional `plan-approval`, `autoimplement`, and internal redesign when needed, then checks the target again. Do not copy their prompts into the monitor task.
68
+ When repair is authorized, route a concrete code or design defect through the monitor's shared plan-change path. Supply the problem, observed evidence, and a stable fingerprint of the issue plus target state. The path runs Autoplan, Autodoc, the configured plan decision, and Autoimplement, then checks the target again. Autoimplement does not ask again for the plan selected by Monitor. It uses the same shared path if later evidence requires another plan. Do not copy their prompts into the monitor task.
69
69
 
70
- Add `repair.approval` only when the user requests a human plan decision. Set its named `audience` and a bounded `maxReplans`. A verified continue answer starts implementation. Stop ends the repair truthfully. Replan preserves the exact operator text, sends it back to autoplan, documents the revised plan, and asks again. The model-facing workflow answer tool cannot answer this gate.
70
+ ### Repair plan decisions
71
+
72
+ Omit `repair.approval` for autonomous mode. It asks the `operator` audience and continues with the exact presented plan after 10 minutes without an accepted answer.
73
+
74
+ Block until the operator answers:
75
+
76
+ ```json
77
+ {
78
+ "repair": {
79
+ "approval": {
80
+ "mode": "required"
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ Continue without asking:
87
+
88
+ ```json
89
+ {
90
+ "repair": {
91
+ "approval": {
92
+ "mode": "skip"
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ `continue` starts implementation. `stop` ends the repair truthfully. `replan` preserves the exact operator text, sends it through the shared plan-change workflow, records the revised plan, and asks again. The model-facing workflow answer tool cannot answer this gate.
71
99
 
72
100
  ## Keep routine work moving
73
101
 
@@ -71,7 +71,8 @@ Follow these rules:
71
71
  - Give included workflows named exits, map their input explicitly, and keep parent edges out of child internals.
72
72
  - Keep `compute` pure. Put external effects in agent, function-action, or shell-action nodes.
73
73
  - Use structured node outputs for routing.
74
- - Use an ordinary checkpoint for external continuation data that the model may submit. Use `humanDecision()` for a verified human choice, and use the included `plan-approval` workflow for standard continue, stop, and exact-text replan routing.
74
+ - Use an ordinary checkpoint for external continuation data that the model may submit. Use `humanDecision()` for a verified human choice. Use its typed `onTimeout` policy only when the workflow may supply a named automatic response after a durable deadline. A timeout response is recorded as policy provenance, not as a human answer.
75
+ - Use the shared internal plan-change workflow for Autoplan, Autodoc, plan approval, and bounded exact-text replanning. Do not copy that sequence into Autoimplement, Monitor, or another workflow.
75
76
  - Set explicit step and command timeouts.
76
77
  - Bound ordinary loops with `maxSteps` or another clear finish rule.
77
78
  - Use a controller instead of a workflow for indefinite resource reconciliation.
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: sanity-check
3
+ description: Use when the user asks for a read-only sanity check of a repository contribution, including whether it is necessary, duplicates existing code, adds justified contracts, or has proportionate scope and tests. Starts the built-in sanity-check workflow and returns an evidence-backed keep, simplify, refactor, drop, or needs-evidence verdict.
4
+ compatibility: Requires pi-workflows and the built-in sanity-check workflow.
5
+ ---
6
+
7
+ # Sanity Check
8
+
9
+ ## Start the workflow
10
+
11
+ Use the built-in `sanity-check` workflow when it is available. At top level, list workflows, build the complete input, and start `sanity-check` once.
12
+
13
+ The workflow reviews the repository of the current Pi session. Before starting, confirm that Pi's current working directory is inside the repository that owns the contribution. Do not claim to check a different repository. If the current directory is wrong, start or use a Pi session in the correct repository first.
14
+
15
+ Build the input as follows:
16
+
17
+ - `mode`: Use `serial` unless the user explicitly requests parallel review. Serial mode reviews all four areas in one isolated session. Parallel mode runs one isolated review per area with concurrency four. Both modes run a separate verification review.
18
+ - `baseRef`: Use the requested base Git reference. When the base is clear, pass it explicitly, such as `origin/main`. Omit it only when the workflow should derive the base from `origin/HEAD`, the current branch upstream, or `HEAD^`.
19
+
20
+ Replace the example values below with facts from the conversation, then make one start call:
21
+
22
+ ```json
23
+ {
24
+ "action": "start",
25
+ "workflow": "sanity-check",
26
+ "input": {
27
+ "mode": "serial",
28
+ "baseRef": "origin/main"
29
+ }
30
+ }
31
+ ```
32
+
33
+ The workflow is read-only. It collects committed and working-tree evidence, reads matching pull-request metadata when available, runs isolated reviews, verifies their claims, and reports one verdict. It does not edit files, post comments, or fix findings.
34
+
35
+ When this skill is loaded inside an active workflow step, do not start another workflow. Complete the current step contract.
36
+
37
+ Outside Pi, or when the workflow is unavailable:
38
+
39
+ 1. Collect the committed diff from the selected base through `HEAD`, the working-tree diff, untracked files, and matching pull-request context.
40
+ 2. Review necessity, duplication, contracts, and scope and tests.
41
+ 3. For each area, report pass, concern, or unclear with exact file and symbol evidence.
42
+ 4. State the strongest evidence-based case for accepting the contribution.
43
+ 5. Verify the findings, remove unsupported claims, and return keep, simplify, refactor, drop, or needs evidence.
44
+ 6. Stay read-only unless the user separately asks to implement the required changes.
@@ -27,8 +27,8 @@ import {
27
27
  type PublishedRepository,
28
28
  type VerificationCommandPlan,
29
29
  } from "./autoimplement-command-batches.js";
30
- import autoplanWorkflow, { type AutoplanInput } from "./autoplan.workflow.js";
31
- import planApprovalWorkflow, { type PlanApprovalInput } from "./plan-approval.workflow.js";
30
+ import { parsePlanApprovalPolicy, type PlanApprovalPolicy } from "./plan-approval.workflow.js";
31
+ import planChangeWorkflow, { type NormalizedPlanChangeInput } from "./plan-change.workflow.js";
32
32
 
33
33
  export type AutoimplementInput = {
34
34
  task: string;
@@ -44,10 +44,7 @@ export type AutoimplementInput = {
44
44
  planDigest: string;
45
45
  documents: string[];
46
46
  };
47
- approval?: {
48
- audience: string;
49
- maxReplans: number;
50
- };
47
+ approval?: PlanApprovalPolicy;
51
48
  concurrency?: Partial<AutoimplementConcurrency>;
52
49
  };
53
50
 
@@ -465,22 +462,7 @@ function parseInput(value: unknown): AutoimplementInput {
465
462
  };
466
463
  }
467
464
  const concurrency = parseAutoimplementConcurrency(input.concurrency);
468
- let approval: AutoimplementInput["approval"];
469
- if (input.approval !== undefined) {
470
- const raw = requireRecord(input.approval, "autoimplement approval");
471
- const maxReplans = raw.maxReplans ?? 3;
472
- if (
473
- !Number.isInteger(maxReplans) ||
474
- (maxReplans as number) < 1 ||
475
- (maxReplans as number) > 20
476
- ) {
477
- throw new Error("autoimplement approval maxReplans must be from 1 through 20");
478
- }
479
- approval = {
480
- audience: requireString(raw.audience, "autoimplement approval audience"),
481
- maxReplans: maxReplans as number,
482
- };
483
- }
465
+ const approval = parsePlanApprovalPolicy(input.approval);
484
466
  return {
485
467
  task: requireString(input.task, "autoimplement task"),
486
468
  ...(input.plan !== undefined ? { plan: input.plan } : {}),
@@ -495,7 +477,7 @@ function parseInput(value: unknown): AutoimplementInput {
495
477
  merge: input.merge === true,
496
478
  ...(documents !== undefined ? { documents: [...documents] as string[] } : {}),
497
479
  ...(documentation !== undefined ? { documentation } : {}),
498
- ...(approval !== undefined ? { approval } : {}),
480
+ approval,
499
481
  concurrency,
500
482
  };
501
483
  }
@@ -637,20 +619,6 @@ function currentPlan(context: WorkflowNodeContext): unknown {
637
619
  return (context.input as AutoimplementInput).plan;
638
620
  }
639
621
 
640
- function currentPlanDigest(context: WorkflowNodeContext): string {
641
- const adopted = context.outputs.adoptPlan as { planDigest?: unknown } | undefined;
642
- if (typeof adopted?.planDigest === "string") return adopted.planDigest;
643
- const documented = context.outputs.documentation as
644
- | { exit?: string; output?: { planDigest?: unknown } }
645
- | undefined;
646
- if (documented?.exit === "ready" && typeof documented.output?.planDigest === "string") {
647
- return documented.output.planDigest;
648
- }
649
- const plan = currentPlan(context);
650
- if (plan === undefined) throw new Error("autoimplement does not have a selected plan");
651
- return digest(plan);
652
- }
653
-
654
622
  function blockerChallenges(context: WorkflowNodeContext): BlockerChallenge[] {
655
623
  return context.state.steps
656
624
  .filter((step) => step.nodeId === "challengeBlocker" && step.outcome === "ok")
@@ -688,16 +656,6 @@ function recentWorkflowAttempts(context: WorkflowNodeContext): unknown[] {
688
656
  }
689
657
 
690
658
  function latestIssue(context: WorkflowNodeContext): unknown {
691
- const approval = context.outputs.approval as
692
- | { exit?: string; output?: { instructions?: unknown } }
693
- | undefined;
694
- if (approval?.exit === "replan" && typeof approval.output?.instructions === "string") {
695
- return {
696
- source: "human-replan",
697
- instructions: approval.output.instructions,
698
- priorPlanDigest: currentPlanDigest(context),
699
- };
700
- }
701
659
  const ids = [
702
660
  "challengeBlocker",
703
661
  "classifyImplementation",
@@ -1145,11 +1103,10 @@ function latestBlockedReason(context: WorkflowNodeContext): { reason: string; ev
1145
1103
  "classifyImplementation",
1146
1104
  "classifyVerification",
1147
1105
  "triageReview",
1148
- "replanGuard",
1106
+ "redesign",
1149
1107
  "adoptPlan",
1150
1108
  "findPlan",
1151
1109
  "documentation",
1152
- "approval",
1153
1110
  ];
1154
1111
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
1155
1112
  const step = context.state.steps[index];
@@ -1191,37 +1148,18 @@ export const autoimplementWorkflow = defineWorkflow({
1191
1148
  };
1192
1149
  },
1193
1150
  }),
1194
- approval: includeWorkflow(planApprovalWorkflow, {
1195
- input: (context): PlanApprovalInput => {
1151
+ redesign: includeWorkflow(planChangeWorkflow, {
1152
+ input: (context): NormalizedPlanChangeInput => {
1196
1153
  const request = context.input as AutoimplementInput;
1197
- if (request.approval === undefined) {
1198
- throw new Error("autoimplement approval was entered without an approval policy");
1199
- }
1200
- const plan = currentPlan(context);
1201
- if (plan === undefined) throw new Error("autoimplement approval is missing a plan");
1202
- const revisions = context.state.steps.filter(
1203
- (step) => step.nodeId === "approval/approve",
1204
- ).length;
1205
1154
  return {
1206
1155
  task: request.task,
1207
- plan,
1208
- planDigest: currentPlanDigest(context),
1209
- audience: request.approval.audience,
1210
- revision: revisions + 1,
1211
- };
1212
- },
1213
- }),
1214
- redesign: includeWorkflow({
1215
- workflow: "autoplan",
1216
- contract: autoplanWorkflow,
1217
- input: (context): AutoplanInput => {
1218
- const request = context.input as AutoimplementInput;
1219
- return {
1220
- problem: request.task,
1221
1156
  ...(request.scope !== undefined ? { scope: request.scope } : {}),
1222
1157
  ...(request.constraints !== undefined ? { constraints: request.constraints } : {}),
1158
+ ...(request.repository !== undefined ? { repository: request.repository } : {}),
1159
+ documents: request.documents ?? request.documentation?.documents ?? [],
1223
1160
  ...(currentPlan(context) !== undefined ? { previousPlan: currentPlan(context) } : {}),
1224
1161
  newEvidence: latestIssue(context),
1162
+ approval: parsePlanApprovalPolicy(request.approval),
1225
1163
  };
1226
1164
  },
1227
1165
  }),
@@ -1285,40 +1223,17 @@ export const autoimplementWorkflow = defineWorkflow({
1285
1223
  }),
1286
1224
  adoptPlan: compute({
1287
1225
  run: ({ outputs }) => {
1288
- const result = includedResult(autoplanWorkflow, outputs.redesign);
1226
+ const result = includedResult(planChangeWorkflow, outputs.redesign);
1289
1227
  if (result.exit !== "ready") throw new Error("redesign did not return a ready plan");
1290
1228
  return {
1291
- route: result.output.changed ? "document" : "blocked",
1292
1229
  plan: result.output.plan,
1293
1230
  planDigest: result.output.planDigest,
1294
- changed: result.output.changed,
1295
- reason: result.output.changed
1296
- ? "The plan changed in response to new evidence and must be documented."
1297
- : "Redesign returned the same plan for the same unresolved evidence.",
1231
+ documents: result.output.documents,
1232
+ approval: result.output.approval,
1233
+ reason: "The changed plan was documented and passed its approval policy.",
1298
1234
  };
1299
1235
  },
1300
1236
  }),
1301
- maybeApproval: compute({
1302
- run: ({ input }) => ({
1303
- route: (input as AutoimplementInput).approval === undefined ? "implement" : "approve",
1304
- }),
1305
- }),
1306
- replanGuard: compute({
1307
- run: (context) => {
1308
- const request = context.input as AutoimplementInput;
1309
- const limit = request.approval?.maxReplans ?? 3;
1310
- const replans = context.state.steps.filter(
1311
- (step) => step.nodeId === "approval/replan",
1312
- ).length;
1313
- return replans > limit
1314
- ? {
1315
- route: "blocked",
1316
- reason: `Plan approval reached the ${limit}-replan safety limit.`,
1317
- evidence: context.outputs.approval,
1318
- }
1319
- : { route: "redesign", replans, limit };
1320
- },
1321
- }),
1322
1237
  timeoutFallbackGuard: compute({
1323
1238
  run: timeoutFallbackGuard,
1324
1239
  }),
@@ -1808,7 +1723,7 @@ export const autoimplementWorkflow = defineWorkflow({
1808
1723
  from: "prepare",
1809
1724
  switch: {
1810
1725
  on: "$.route",
1811
- cases: { find: "findPlan", document: "documentation", ready: "maybeApproval" },
1726
+ cases: { find: "findPlan", document: "documentation", ready: "implement" },
1812
1727
  },
1813
1728
  },
1814
1729
  {
@@ -1819,28 +1734,14 @@ export const autoimplementWorkflow = defineWorkflow({
1819
1734
  from: "routeFoundPlan",
1820
1735
  switch: {
1821
1736
  on: "$.route",
1822
- cases: { ready: "maybeApproval", document: "documentation", blocked: "blocked" },
1737
+ cases: { ready: "implement", document: "documentation", blocked: "blocked" },
1823
1738
  },
1824
1739
  },
1825
1740
  { from: "redesign.ready", to: "adoptPlan" },
1826
1741
  { from: "redesign.blocked", to: "blocked" },
1827
- {
1828
- from: "adoptPlan",
1829
- switch: { on: "$.route", cases: { document: "documentation", blocked: "blocked" } },
1830
- },
1831
- { from: "documentation.ready", to: "maybeApproval" },
1742
+ { from: "adoptPlan", to: "implement" },
1743
+ { from: "documentation.ready", to: "implement" },
1832
1744
  { from: "documentation.blocked", to: "blocked" },
1833
- {
1834
- from: "maybeApproval",
1835
- switch: { on: "$.route", cases: { approve: "approval", implement: "implement" } },
1836
- },
1837
- { from: "approval.continue", to: "implement" },
1838
- { from: "approval.stop", to: "blocked" },
1839
- { from: "approval.replan", to: "replanGuard" },
1840
- {
1841
- from: "replanGuard",
1842
- switch: { on: "$.route", cases: { redesign: "redesign", blocked: "blocked" } },
1843
- },
1844
1745
  {
1845
1746
  from: "timeoutFallbackGuard",
1846
1747
  switch: { on: "$.route", cases: { recover: "timeoutFallback", blocked: "blocked" } },
@@ -9,12 +9,12 @@ import sanityCheckWorkflow from "./sanity-check.workflow.js";
9
9
  export const builtinWorkflowCatalog = new BuiltinWorkflowCatalog([
10
10
  { id: "autoplan", revision: "1", definition: autoplanWorkflow },
11
11
  { id: "autodoc", revision: "1", definition: autodocWorkflow },
12
- { id: "autoimplement", revision: "6", definition: autoimplementWorkflow },
13
- { id: "plan-approval", revision: "2", definition: planApprovalWorkflow },
14
- { id: "sanity-check", revision: "1", definition: sanityCheckWorkflow },
12
+ { id: "autoimplement", revision: "8", definition: autoimplementWorkflow },
13
+ { id: "plan-approval", revision: "4", definition: planApprovalWorkflow },
14
+ { id: "sanity-check", revision: "3", definition: sanityCheckWorkflow },
15
15
  {
16
16
  id: "monitor",
17
- revision: "7",
17
+ revision: "9",
18
18
  definition: monitorWorkflow,
19
19
  legacySources: [
20
20
  {
@@ -23,6 +23,12 @@ export {
23
23
  type MonitorInput,
24
24
  type MonitorRepairPolicy,
25
25
  } from "./monitor.workflow.js";
26
+ export {
27
+ planChangeWorkflow,
28
+ type PlanChangeBlocked,
29
+ type PlanChangeInput,
30
+ type PlanChangeReady,
31
+ } from "./plan-change.workflow.js";
26
32
  export { presentPlan } from "./plan-presentation.js";
27
33
  export {
28
34
  sanityCheckWorkflow,
@@ -38,8 +44,13 @@ export {
38
44
  } from "./sanity-check.workflow.js";
39
45
  export {
40
46
  planApprovalWorkflow,
47
+ parsePlanApprovalPolicy,
41
48
  type PlanApprovalContinue,
42
49
  type PlanApprovalInput,
50
+ type PlanApprovalMode,
51
+ type PlanApprovalPolicy,
43
52
  type PlanApprovalReplan,
53
+ type PlanApprovalResolution,
44
54
  type PlanApprovalStop,
55
+ type ResolvedPlanApprovalPolicy,
45
56
  } from "./plan-approval.workflow.js";