@osolmaz/pi-workflows 0.11.0 → 0.11.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 +9 -2
- package/dist/builtins/autoimplement.workflow.d.ts +2 -0
- package/dist/builtins/autoimplement.workflow.js +186 -9
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/herdr/setup.d.ts +13 -1
- package/dist/herdr/setup.js +349 -36
- package/dist/herdr/setup.js.map +1 -1
- package/dist/viewer/cli.d.ts +1 -0
- package/dist/viewer/cli.js +21 -10
- package/dist/viewer/cli.js.map +1 -1
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +138 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +104 -0
- package/docs/workflows.md +4 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/builtins/autoimplement.workflow.ts +212 -9
- package/src/herdr/setup.ts +429 -39
- package/src/viewer/cli.ts +22 -10
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Confirm blockers before autoimplement stops
|
|
3
|
+
author: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
|
|
4
|
+
date: 2026-08-20
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Confirm blockers before autoimplement stops
|
|
8
|
+
|
|
9
|
+
Autoimplement must not stop only because one model says that work is blocked. A separate agent must challenge the claim and confirm that the blocker is real before the workflow uses its terminal blocked result.
|
|
10
|
+
|
|
11
|
+
The canonical workflow behavior is in [Workflow authoring reference](../workflows.md#built-in-planning-and-implementation).
|
|
12
|
+
|
|
13
|
+
## Outcome
|
|
14
|
+
|
|
15
|
+
Add one independent `challengeBlocker` agent node to the built-in autoimplement workflow. Use only existing public Pi Workflows primitives. Keep the graph explicit and reuse the existing redesign include and terminal blocked result.
|
|
16
|
+
|
|
17
|
+
The challenge asks these questions in plain terms:
|
|
18
|
+
|
|
19
|
+
- Are you really blocked?
|
|
20
|
+
- Is this really a blocker right now?
|
|
21
|
+
- Can you find a safe way to move forward and finish this?
|
|
22
|
+
- Are you getting stuck on something trivial, procedural, reversible, or already authorized?
|
|
23
|
+
|
|
24
|
+
The challenge inspects the task, approved plan, current result, evidence, scope, authority, previous attempts, and viable alternatives. It distinguishes a true external blocker from normal rollout work, local implementation work, a design adjustment, a missing verification step, or a reversible operational task.
|
|
25
|
+
|
|
26
|
+
## Output and validation
|
|
27
|
+
|
|
28
|
+
The challenge returns this bounded structured output:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"route": "continue | blocked",
|
|
33
|
+
"blockingNow": true,
|
|
34
|
+
"outsideAuthority": true,
|
|
35
|
+
"canProceed": false,
|
|
36
|
+
"reason": "concise reason",
|
|
37
|
+
"nextAction": "",
|
|
38
|
+
"alternativesChecked": ["checked alternative"],
|
|
39
|
+
"evidence": ["concrete evidence"]
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A terminal blocked result is valid only when all these conditions hold:
|
|
44
|
+
|
|
45
|
+
- `route` is `blocked`.
|
|
46
|
+
- `blockingNow` is `true`.
|
|
47
|
+
- `outsideAuthority` is `true`.
|
|
48
|
+
- `canProceed` is `false`.
|
|
49
|
+
- `nextAction` is empty.
|
|
50
|
+
- `alternativesChecked` contains checked practical alternatives.
|
|
51
|
+
- `evidence` contains concrete evidence.
|
|
52
|
+
|
|
53
|
+
A `continue` result must name the next practical action. Contradictory output, such as `route: blocked` with `canProceed: true`, cannot route to the terminal blocked result.
|
|
54
|
+
|
|
55
|
+
Keep the output concise and bounded.
|
|
56
|
+
|
|
57
|
+
## Routing
|
|
58
|
+
|
|
59
|
+
Route `classifyImplementation.blocked` to `challengeBlocker`. A `continue` challenge result routes to the existing redesign include. Redesign can revise and document the plan, then return through implementation, verification, review, comment handling, CI, and delivery. A valid `blocked` result routes to the existing terminal blocked result.
|
|
60
|
+
|
|
61
|
+
Inspect each later model-produced blocked exit in verification, review, comment handling, CI, delivery, and equivalent current nodes. Route operational or model-judged blockers through the same challenge when it is safe. Use one reusable challenge node and existing edges or includes. Do not copy the prompt into several nodes and do not add an engine-level blocker feature.
|
|
62
|
+
|
|
63
|
+
Preserve direct terminal stops for these hard boundaries:
|
|
64
|
+
|
|
65
|
+
- an explicit human stop;
|
|
66
|
+
- cancellation;
|
|
67
|
+
- an exhausted workflow safety or replan limit;
|
|
68
|
+
- a protected authorization gap;
|
|
69
|
+
- an independent blocked result from redesign when another challenge could make an unsafe or unbounded loop.
|
|
70
|
+
|
|
71
|
+
Limit blocker challenges to three attempts in one run. Include all earlier challenge outputs in each later challenge prompt. If a fourth challenge would be needed, stop with the normal safety-limit reason. Do not repeat an unsupported blocker assertion.
|
|
72
|
+
|
|
73
|
+
Include the latest challenge output in the workflow's latest-issue or context helper. A later redesign must receive the rejected blocker, its evidence, and its required next action.
|
|
74
|
+
|
|
75
|
+
## Required behavior
|
|
76
|
+
|
|
77
|
+
A supported cutover does not become blocked only because an artifact has an ownership or packaging mismatch. In the Bob artifact incident, authorized deployment and rollback make the safe rollout work part of the task. The challenge must return `continue`, name the next rollout action, and route to redesign.
|
|
78
|
+
|
|
79
|
+
A missing external authorization for a prohibited remote mutation can remain blocked when no non-mutating path completes the task.
|
|
80
|
+
|
|
81
|
+
A local test failure, stale package, packaging mismatch, rollback preparation, or deployment procedure is not, by itself, outside the granted authority.
|
|
82
|
+
|
|
83
|
+
## Tests
|
|
84
|
+
|
|
85
|
+
Add focused tests in `test/builtin-autoimplement.test.ts` or the best current autoimplement test file. Prove all these cases:
|
|
86
|
+
|
|
87
|
+
1. A false blocker routes to redesign and continued work.
|
|
88
|
+
2. A confirmed blocker reaches the terminal blocked result.
|
|
89
|
+
3. Contradictory blocked output is rejected or cannot route to the terminal blocked result.
|
|
90
|
+
4. The Bob artifact-ownership mismatch returns `continue` when rollout is authorized.
|
|
91
|
+
5. An explicit human stop bypasses the challenge.
|
|
92
|
+
6. A protected authorization gap remains a hard stop.
|
|
93
|
+
7. An independent `redesign.blocked` path does not create a loop.
|
|
94
|
+
8. Earlier challenge context is present and the three-attempt bound works.
|
|
95
|
+
9. All relevant late-stage model-generated blocker routes use the challenge.
|
|
96
|
+
10. Normal success paths remain unchanged.
|
|
97
|
+
|
|
98
|
+
Keep compute nodes pure. Put the independent reasoning in the challenge agent node.
|
|
99
|
+
|
|
100
|
+
## Documentation
|
|
101
|
+
|
|
102
|
+
Update `docs/workflows.md` with the blocker-confirmation rule, hard-stop exceptions, and bounded routing. Keep the public behavior concise. Do not add internal details that users do not need.
|
|
103
|
+
|
|
104
|
+
## Verification
|
|
105
|
+
|
|
106
|
+
Run focused tests during development. Before completion, run these exact checks:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm run check
|
|
110
|
+
npm run test:e2e
|
|
111
|
+
npx slophammer-ts@latest dry .
|
|
112
|
+
npx slophammer-ts@latest check . --only ts.dependency-boundaries-required
|
|
113
|
+
git diff --check
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Verify discovery and behavior through the installed package path or the repository's supported real-Pi end-to-end path. A fresh Pi process is required because built-in discovery is process-local.
|
|
117
|
+
|
|
118
|
+
Review the final diff for missing blocker routes, accidental unbounded loops, and false terminal blockers. Fix each valid finding.
|
|
119
|
+
|
|
120
|
+
## Boundaries
|
|
121
|
+
|
|
122
|
+
- Preserve current run-bundle schemas unless a schema change is required. Do not add a persistence layer.
|
|
123
|
+
- Use existing public Pi Workflows primitives only. Do not change Pi core.
|
|
124
|
+
- Keep explicit human and protected authorization boundaries intact.
|
|
125
|
+
- Use a hard cutover. Do not retain a legacy blocker route.
|
|
126
|
+
- Preserve unrelated work and do not modify other repositories.
|
|
127
|
+
- Make only the smallest source-based adjustment needed to preserve the approved behavior.
|
|
128
|
+
- Do not stop for a trivial, reversible, or already authorized issue.
|
|
129
|
+
- Commit coherent changes with a Conventional Commit message and push after all checks pass.
|
|
130
|
+
- Do not publish a package, create a release, or merge anything.
|
|
131
|
+
|
|
132
|
+
## Contract impact
|
|
133
|
+
|
|
134
|
+
- **Session state:** normal workflow messages and tool results only.
|
|
135
|
+
- **Other persistent data:** none beyond the existing run bundle records for normal node outputs.
|
|
136
|
+
- **Pi internals:** none.
|
|
137
|
+
- **Public Pi API:** existing documented extension APIs only.
|
|
138
|
+
- **Public Pi Workflows API:** existing agent, compute, edge, and included-workflow primitives only.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Keep the Herdr plugin linked after package updates
|
|
3
|
+
author: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
|
|
4
|
+
date: 2026-08-20
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Keep the Herdr plugin linked after package updates
|
|
8
|
+
|
|
9
|
+
Pi Workflows ships its Herdr plugin inside the npm package. Herdr records the package's absolute path. npm can move an installed package between nested and hoisted `node_modules` directories during an update, which leaves Herdr linked to a path that no longer exists.
|
|
10
|
+
|
|
11
|
+
Pi Workflows will own one explicit command that finds its own package and repairs this link. OnurPi will call that command after it installs an exact reviewed Pi Workflows release. OnurPi will not contain Herdr paths, manifests, or link-repair rules.
|
|
12
|
+
|
|
13
|
+
## Outcome
|
|
14
|
+
|
|
15
|
+
The canonical command is:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pi-workflows herdr sync --json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`pi-workflows herdr setup` remains an alias for compatibility.
|
|
22
|
+
|
|
23
|
+
The command validates the bundled package before it changes Herdr. It then creates a missing link, enables a disabled link, leaves a correct link unchanged, or replaces a link to another package path or version. A successful result requires a new Herdr query that shows the expected plugin ID, package version, package root, manifest path, enabled state, and no warning.
|
|
24
|
+
|
|
25
|
+
## Scope
|
|
26
|
+
|
|
27
|
+
### Pi Workflows
|
|
28
|
+
|
|
29
|
+
- Resolve the installed package root from the running CLI.
|
|
30
|
+
- Validate `package.json`, `herdr-plugin.toml`, and the bundled viewer before changing Herdr.
|
|
31
|
+
- Reconcile the current registration through Herdr's public CLI.
|
|
32
|
+
- Return a versioned JSON result with one of `linked`, `relinked`, `enabled`, `unchanged`, or `unavailable`.
|
|
33
|
+
- Return `unavailable` only when the Herdr executable is absent.
|
|
34
|
+
- Treat malformed package data, malformed Herdr output, command failures, identity conflicts, and failed verification as errors.
|
|
35
|
+
- Keep output bounded and use argument arrays instead of shell commands.
|
|
36
|
+
- Test source and packed-package layouts, including paths that contain spaces.
|
|
37
|
+
|
|
38
|
+
### OnurPi
|
|
39
|
+
|
|
40
|
+
- Keep the Pi Workflows dependency pinned to an exact reviewed release.
|
|
41
|
+
- Invoke the local `pi-workflows herdr sync --json` command from an explicit TypeScript sync script after dependency installation.
|
|
42
|
+
- Accept the versioned result and keep Herdr-specific behavior in Pi Workflows.
|
|
43
|
+
- Keep package installation free of `postinstall` side effects.
|
|
44
|
+
|
|
45
|
+
## Non-goals
|
|
46
|
+
|
|
47
|
+
- Do not create another Herdr plugin package or release.
|
|
48
|
+
- Do not duplicate the manifest or viewer in OnurPi.
|
|
49
|
+
- Do not add hard-coded `node_modules` paths.
|
|
50
|
+
- Do not edit Herdr state files directly.
|
|
51
|
+
- Do not change Herdr core or Pi core.
|
|
52
|
+
- Do not hot-reload running Pi processes. A running process still needs `/reload` or restart after a package update.
|
|
53
|
+
- Do not add a service, watcher, or implicit package-install mutation.
|
|
54
|
+
|
|
55
|
+
## Command contract
|
|
56
|
+
|
|
57
|
+
The JSON result uses schema `pi-workflows.herdr-sync.v1` and contains:
|
|
58
|
+
|
|
59
|
+
- the result status;
|
|
60
|
+
- whether Herdr state changed;
|
|
61
|
+
- the plugin ID;
|
|
62
|
+
- the expected and effective versions when available;
|
|
63
|
+
- the effective enabled state when available;
|
|
64
|
+
- a plain summary; and
|
|
65
|
+
- an advisory that running Pi processes must reload after a package update.
|
|
66
|
+
|
|
67
|
+
A missing Herdr executable returns `unavailable` with exit code zero because Herdr is an optional integration. Every other failure returns a nonzero exit code and does not claim success.
|
|
68
|
+
|
|
69
|
+
The command preflights the new package before unlinking an old registration. Herdr currently exposes separate unlink and link commands, so replacement cannot be atomic. If replacement fails, Pi Workflows makes one restore attempt only when the previous package root still passes the same validation. It then reports the state found by a fresh Herdr query.
|
|
70
|
+
|
|
71
|
+
Concurrent sync commands converge on the same target. After a failed or ambiguous mutation, the command queries Herdr and adopts the result only when another process already reached the exact expected state. It does not repeat the same mutation blindly.
|
|
72
|
+
|
|
73
|
+
## Compatibility
|
|
74
|
+
|
|
75
|
+
Existing `herdr setup` callers use the same implementation. Other Pi Workflows CLI commands do not change. The npm package remains the only source of the plugin manifest and viewer.
|
|
76
|
+
|
|
77
|
+
OnurPi adds only invocation timing and result handling. It does not parse the Herdr manifest or issue link commands.
|
|
78
|
+
|
|
79
|
+
## Verification
|
|
80
|
+
|
|
81
|
+
### Pi Workflows
|
|
82
|
+
|
|
83
|
+
- Test first link, unchanged link, disabled link, moved package path, stale path, and version update.
|
|
84
|
+
- Test missing Herdr, malformed manifests, malformed plugin records, command failures, post-action mismatches, and bounded restore behavior.
|
|
85
|
+
- Test concurrent adoption and paths with spaces.
|
|
86
|
+
- Run the CLI from `npm pack` contents in nested and hoisted layouts.
|
|
87
|
+
- Run `npm run check`, `npm run test:e2e`, Slophammer, SimpleDoc, and diff checks.
|
|
88
|
+
|
|
89
|
+
### OnurPi
|
|
90
|
+
|
|
91
|
+
- Test every structured result and invalid command output with a fake executable.
|
|
92
|
+
- Verify the wrapper contains no plugin ID, manifest copy, Herdr mutation command, or package path.
|
|
93
|
+
- Verify root and wrapper dependency pins remain equal.
|
|
94
|
+
- Run `npm run check`, `npm run slophammer`, SimpleDoc, and diff checks.
|
|
95
|
+
|
|
96
|
+
### Adoption
|
|
97
|
+
|
|
98
|
+
After a separately approved Pi Workflows release, update OnurPi to that exact version and run:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm run workflows:sync
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then verify the Herdr plugin list, open `piw`, and start or reload Pi to confirm resource discovery. Repeating the sync must return `unchanged`.
|
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
|
package/herdr-plugin.toml
CHANGED
package/package.json
CHANGED
|
@@ -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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: "
|
|
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: {
|
|
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: "
|
|
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: "
|
|
1312
|
+
blocked: "challengeBlockerGuard",
|
|
1113
1313
|
},
|
|
1114
1314
|
},
|
|
1115
1315
|
},
|
|
1116
1316
|
{
|
|
1117
1317
|
from: "finalizeDelivery",
|
|
1118
|
-
switch: {
|
|
1318
|
+
switch: {
|
|
1319
|
+
on: "$.status",
|
|
1320
|
+
cases: { completed: "finalize", blocked: "challengeBlockerGuard" },
|
|
1321
|
+
},
|
|
1119
1322
|
},
|
|
1120
1323
|
],
|
|
1121
1324
|
});
|