@wichayutdew/pi-workflows 2.3.0 → 2.4.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/agents/step.md +8 -0
- package/dist/index.js +32 -10
- package/examples/starter-kit/steps/mr-comment/plan.md +3 -2
- package/examples/starter-kit/steps/mr-comment/publish.md +2 -1
- package/examples/starter-kit/steps/mr-comment/verify.md +9 -1
- package/examples/starter-kit/steps/mr-review/publish.md +11 -0
- package/examples/starter-kit/steps/ticket/implement.md +2 -1
- package/examples/starter-kit/steps/ticket/plan.md +40 -0
- package/examples/starter-kit/steps/ticket/verify.md +50 -4
- package/examples/starter-kit/steps/work/verify.md +4 -0
- package/package.json +1 -1
- package/schemas/workflow.schema.json +14 -1
- package/src/config/types.ts +1 -1
- package/src/config/validation/step.ts +8 -7
- package/src/harness/workspace-directory.ts +13 -4
- package/src/integrations/subagents/child-policy-sections.ts +1 -2
- package/src/prompt/step-sections.ts +30 -0
- package/src/prompt/step-task.ts +2 -0
package/agents/step.md
CHANGED
|
@@ -22,3 +22,11 @@ pi-subagents' `structured_output`; `workflow_complete_step` belongs to
|
|
|
22
22
|
main-agent workflow steps. Never call `contact_supervisor`,
|
|
23
23
|
`subagent_supervisor`, or `intercom`. The workflow prompt defines artifact
|
|
24
24
|
content and format, acceptance criteria, and the meaning of every outcome.
|
|
25
|
+
|
|
26
|
+
For a non-success outcome, treat `summary` as an operator handoff, not a
|
|
27
|
+
diagnostic transcript: lead with a plain-language decision, list each
|
|
28
|
+
independent issue with its decisive evidence and the concrete action/owner, and
|
|
29
|
+
end with the safe next move. Omit policy narration, raw logs, successful-check
|
|
30
|
+
or clean-state notes, and assertions that the child lacks authority; state the
|
|
31
|
+
prerequisite that would unblock it. Mention a passed check only when it directly
|
|
32
|
+
explains the remaining issue.
|
package/dist/index.js
CHANGED
|
@@ -786,16 +786,16 @@ function parseWorkspaceRoots(value, path, errors) {
|
|
|
786
786
|
if (value === undefined)
|
|
787
787
|
return ["."];
|
|
788
788
|
if (!Array.isArray(value)) {
|
|
789
|
-
errors.push(`${path}: expected an array of
|
|
789
|
+
errors.push(`${path}: expected an array of workspace paths`);
|
|
790
790
|
return [];
|
|
791
791
|
}
|
|
792
792
|
if (value.length > MAX_WORKSPACE_ALLOWED_ROOTS) {
|
|
793
|
-
errors.push(`${path}: at most ${MAX_WORKSPACE_ALLOWED_ROOTS}
|
|
793
|
+
errors.push(`${path}: at most ${MAX_WORKSPACE_ALLOWED_ROOTS} workspace paths are allowed`);
|
|
794
794
|
}
|
|
795
795
|
const roots = value.slice(0, MAX_WORKSPACE_ALLOWED_ROOTS).reduce((result, item, index) => {
|
|
796
796
|
const itemPath = `${path}[${index}]`;
|
|
797
797
|
if (typeof item !== "string" || !item || item.trim() !== item) {
|
|
798
|
-
errors.push(`${itemPath}: expected a non-empty
|
|
798
|
+
errors.push(`${itemPath}: expected a non-empty workspace path`);
|
|
799
799
|
return result;
|
|
800
800
|
}
|
|
801
801
|
const root = item;
|
|
@@ -803,8 +803,8 @@ function parseWorkspaceRoots(value, path, errors) {
|
|
|
803
803
|
errors.push(`${itemPath}: path exceeds ${MAX_WORKSPACE_PATH_CHARS} characters`);
|
|
804
804
|
return result;
|
|
805
805
|
}
|
|
806
|
-
if (root.includes("\x00") ||
|
|
807
|
-
errors.push(`${itemPath}: expected a relative path`);
|
|
806
|
+
if (root.includes("\x00") || win32.parse(root).root !== "" && !isAbsolute(root)) {
|
|
807
|
+
errors.push(`${itemPath}: expected a relative, absolute, or home-relative path`);
|
|
808
808
|
return result;
|
|
809
809
|
}
|
|
810
810
|
if (result.includes(root)) {
|
|
@@ -814,7 +814,7 @@ function parseWorkspaceRoots(value, path, errors) {
|
|
|
814
814
|
return [...result, root];
|
|
815
815
|
}, []);
|
|
816
816
|
if (roots.length === 0) {
|
|
817
|
-
errors.push(`${path}: at least one
|
|
817
|
+
errors.push(`${path}: at least one workspace path is required`);
|
|
818
818
|
}
|
|
819
819
|
return roots;
|
|
820
820
|
}
|
|
@@ -2381,7 +2381,7 @@ var parseWorkspace2 = (value, outcomes) => {
|
|
|
2381
2381
|
if (!isStringArray(bindOn) || bindOn.length === 0 || new Set(bindOn).size !== bindOn.length || bindOn.some((outcome) => !outcomes.includes(outcome))) {
|
|
2382
2382
|
throw new Error("child policy workspace bindOn outcomes are invalid");
|
|
2383
2383
|
}
|
|
2384
|
-
if (!isStringArray(allowedRoots) || allowedRoots.length === 0 || allowedRoots.length > MAX_WORKSPACE_ALLOWED_ROOTS || new Set(allowedRoots).size !== allowedRoots.length || allowedRoots.some((root) => !root.trim() || root !== root.trim() || root.length > MAX_WORKSPACE_PATH_CHARS || root.includes("\x00") ||
|
|
2384
|
+
if (!isStringArray(allowedRoots) || allowedRoots.length === 0 || allowedRoots.length > MAX_WORKSPACE_ALLOWED_ROOTS || new Set(allowedRoots).size !== allowedRoots.length || allowedRoots.some((root) => !root.trim() || root !== root.trim() || root.length > MAX_WORKSPACE_PATH_CHARS || root.includes("\x00") || win322.parse(root).root !== "" && !isAbsolute4(root))) {
|
|
2385
2385
|
throw new Error("child policy workspace allowed roots are invalid");
|
|
2386
2386
|
}
|
|
2387
2387
|
return { workspace: { bindOn, allowedRoots } };
|
|
@@ -5316,11 +5316,13 @@ async function showWorkflowStatus(ctx, getSnapshot, statusShortcut = DEFAULT_STA
|
|
|
5316
5316
|
}
|
|
5317
5317
|
// src/harness/workspace-directory.ts
|
|
5318
5318
|
import { realpathSync, statSync } from "node:fs";
|
|
5319
|
+
import { homedir as homedir2 } from "node:os";
|
|
5319
5320
|
import { isAbsolute as isAbsolute10, relative as relative6, resolve as resolve10, sep as sep5, win32 as win323 } from "node:path";
|
|
5320
5321
|
var isWithin2 = (root, candidate) => {
|
|
5321
5322
|
const pathFromRoot = relative6(root, candidate);
|
|
5322
5323
|
return pathFromRoot === "" || pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep5}`) && !isAbsolute10(pathFromRoot);
|
|
5323
5324
|
};
|
|
5325
|
+
var resolveAllowedRoot = (startCwd, allowedRoot) => allowedRoot === "~" ? homedir2() : allowedRoot.startsWith("~/") ? resolve10(homedir2(), allowedRoot.slice(2)) : isAbsolute10(allowedRoot) ? allowedRoot : resolve10(startCwd, allowedRoot);
|
|
5324
5326
|
function resolveWorkspaceDirectory({
|
|
5325
5327
|
candidateCwd,
|
|
5326
5328
|
startCwd,
|
|
@@ -5337,10 +5339,10 @@ function resolveWorkspaceDirectory({
|
|
|
5337
5339
|
}
|
|
5338
5340
|
const canonicalStart = realpathSync(startCwd);
|
|
5339
5341
|
const canonicalRoots = allowedRoots.map((allowedRoot) => {
|
|
5340
|
-
if (!allowedRoot ||
|
|
5341
|
-
throw new Error("workspace allowed roots must be non-empty relative paths");
|
|
5342
|
+
if (!allowedRoot || win323.parse(allowedRoot).root !== "" && !isAbsolute10(allowedRoot) || allowedRoot.includes("\x00")) {
|
|
5343
|
+
throw new Error("workspace allowed roots must be non-empty relative, absolute, or home-relative paths");
|
|
5342
5344
|
}
|
|
5343
|
-
return realpathSync(
|
|
5345
|
+
return realpathSync(resolveAllowedRoot(canonicalStart, allowedRoot));
|
|
5344
5346
|
});
|
|
5345
5347
|
const canonicalCwd = realpathSync(candidateCwd);
|
|
5346
5348
|
if (!statSync(canonicalCwd).isDirectory()) {
|
|
@@ -7000,6 +7002,25 @@ function buildDelegatedCompletionInstructions() {
|
|
|
7000
7002
|
"Stay within the configured permissions and do not broaden mutation targets or external side effects."
|
|
7001
7003
|
];
|
|
7002
7004
|
}
|
|
7005
|
+
function buildNonSuccessSummaryInstructions(outcomes) {
|
|
7006
|
+
const nonSuccessOutcomes = outcomes.filter((outcome) => ["blocked", "failed", "retry"].includes(outcome));
|
|
7007
|
+
if (nonSuccessOutcomes.length === 0)
|
|
7008
|
+
return [];
|
|
7009
|
+
return [
|
|
7010
|
+
"## Human-readable non-success results",
|
|
7011
|
+
"",
|
|
7012
|
+
`For ${nonSuccessOutcomes.map((outcome) => `\`${outcome}\``).join(", ")}, write a decision-first summary. It is shown verbatim to the operator and handed to a fresh child. Use this format:`,
|
|
7013
|
+
"",
|
|
7014
|
+
"# <Failed | Blocked | Retry>: <one-sentence plain-language decision>",
|
|
7015
|
+
"1. **<short issue>** — <only the decisive evidence, including an exact command/error, path, or identifier when it enables action>.",
|
|
7016
|
+
" **Action:** <the specific owner or role> must <the concrete evidence, decision, or change needed>.",
|
|
7017
|
+
"2. Repeat only for other independent issues (at most three total).",
|
|
7018
|
+
"**Next:** <the exact safe next move, such as provide the listed evidence and run `/workflow-resume`>.",
|
|
7019
|
+
"",
|
|
7020
|
+
"Do not include a process narrative, raw logs, repeated policy constraints, successful checks, clean-state notes, or statements that merely say the child lacks authority. Mention a passed check only when it directly explains the remaining issue. Name the missing prerequisite and who can supply it. Keep only details needed to make the decision or complete the next action.",
|
|
7021
|
+
""
|
|
7022
|
+
];
|
|
7023
|
+
}
|
|
7003
7024
|
|
|
7004
7025
|
// src/prompt/template.ts
|
|
7005
7026
|
function currentStepHandoff(run) {
|
|
@@ -7131,6 +7152,7 @@ function buildStepTask(options) {
|
|
|
7131
7152
|
...contract.workspaceLines,
|
|
7132
7153
|
"",
|
|
7133
7154
|
"Put a self-contained compact handoff in `summary`; this is the only step context passed to the next fresh child.",
|
|
7155
|
+
...buildNonSuccessSummaryInstructions(contract.outcomes),
|
|
7134
7156
|
...isDelegated ? buildDelegatedCompletionInstructions() : [],
|
|
7135
7157
|
"Do not call the completion tool alongside other tool calls."
|
|
7136
7158
|
].join(`
|
|
@@ -25,8 +25,9 @@ to close evidence gaps.
|
|
|
25
25
|
Classify every unresolved comment as valid, partly valid, invalid, or already
|
|
26
26
|
addressed, with causal evidence. Define scoped code changes, exact
|
|
27
27
|
repository-native checks, an optional commit, and the public reply for each
|
|
28
|
-
comment. Include
|
|
29
|
-
host
|
|
28
|
+
comment. Include the matching non-force push whenever a committed code fix must
|
|
29
|
+
reach the host, and one same-host reply action for every comment that requires
|
|
30
|
+
a response. Never include approval, merge, resolution, closure, deletion,
|
|
30
31
|
force-push, cross-host mutation, or unrelated work.
|
|
31
32
|
|
|
32
33
|
This user-owned prompt defines the Plannotator artifact:
|
|
@@ -20,7 +20,8 @@ an exact already-present non-force push SHA or an exact reply by the current
|
|
|
20
20
|
user with the approved marker. Execute each remaining action once, in approved
|
|
21
21
|
order, through its exact configured MCP tool/input or standalone `git`, `glab`,
|
|
22
22
|
`gh`, or authenticated cURL command. Push before replies that describe the
|
|
23
|
-
code fix.
|
|
23
|
+
code fix. Automatically perform every required approved push and reply; never
|
|
24
|
+
ask the user to perform one.
|
|
24
25
|
|
|
25
26
|
Require a successful same-host, target-correlated response. Never change reply
|
|
26
27
|
meaning, target another comment, expose credentials, force-push, approve,
|
|
@@ -22,6 +22,13 @@ comment/anchor. Verify every remote action is same-host, non-force,
|
|
|
22
22
|
idempotently observable, and limited to the approved push and replies. Do not
|
|
23
23
|
execute remote actions here.
|
|
24
24
|
|
|
25
|
+
Any regression, lint failure, formatting failure, or other actionable local
|
|
26
|
+
finding is `failed`; the workflow sends that outcome directly back to
|
|
27
|
+
implementation. Do not use `blocked` for a fixable local finding. When a code
|
|
28
|
+
fix was committed, require the matching approved non-force push action before
|
|
29
|
+
the replies. A valid unresolved review comment requires its approved public
|
|
30
|
+
reply action.
|
|
31
|
+
|
|
25
32
|
Call `structured_output` alone with:
|
|
26
33
|
|
|
27
34
|
- `ready` when all criteria pass and approved remote actions remain;
|
|
@@ -32,4 +39,5 @@ Call `structured_output` alone with:
|
|
|
32
39
|
that cannot proceed safely.
|
|
33
40
|
|
|
34
41
|
For `ready` and `failed`, include complete fresh evidence and the unchanged
|
|
35
|
-
Execution contract in `summary`.
|
|
42
|
+
Execution contract in `summary`. A `ready` handoff automatically proceeds to
|
|
43
|
+
the publisher; do not ask the user to push or post a reply.
|
|
@@ -25,6 +25,17 @@ Never alter the approved body, target another head or anchor, expose
|
|
|
25
25
|
credentials, approve, merge, resolve, close, delete, push, cross hosts, or add
|
|
26
26
|
an unlisted action.
|
|
27
27
|
|
|
28
|
+
For a GitLab inline discussion, the API requires `position` as a nested object.
|
|
29
|
+
Prefer an exact configured GitLab MCP mutation whose schema accepts that object.
|
|
30
|
+
When using `glab api`, submit every discussion field as multipart form data with
|
|
31
|
+
`--form`, including `position[base_sha]`, `position[start_sha]`,
|
|
32
|
+
`position[head_sha]`, `position[position_type]`, both paths, and the applicable
|
|
33
|
+
line. Do not use `--field` or `--raw-field` for any `position[...]` key: those
|
|
34
|
+
flags serialize JSON scalar keys and do not construct GitLab's nested position,
|
|
35
|
+
which can create an unanchored general discussion. Before calling
|
|
36
|
+
`structured_output`, fetch the returned discussion and require its note to have
|
|
37
|
+
the approved position/path/line as well as the approved body and marker.
|
|
38
|
+
|
|
28
39
|
After a mutation-capable call is attempted, ambiguity is `blocked`; do not
|
|
29
40
|
blindly replay it. Call `structured_output` alone with outcome `published` only
|
|
30
41
|
after every approved effect succeeded now or was proven already present.
|
|
@@ -26,7 +26,8 @@ errors and state before trying a safe equivalent. Never weaken validation,
|
|
|
26
26
|
broaden scope, or mutate Jira.
|
|
27
27
|
|
|
28
28
|
Run the approved checks, and stage or commit only when the approved plan calls
|
|
29
|
-
for it. Do not push or publish in this step
|
|
29
|
+
for it. Do not push or publish in this step; independent verification publishes
|
|
30
|
+
only the reviewed Publication contract after it has passed.
|
|
30
31
|
|
|
31
32
|
Call `structured_output` alone with outcome `ready` when the result is ready for
|
|
32
33
|
independent review. Summarize ticket identity, changed files, commands/results,
|
|
@@ -56,11 +56,51 @@ This user-owned prompt defines the Plannotator artifact. Produce:
|
|
|
56
56
|
5. `## Acceptance criteria`
|
|
57
57
|
6. `## Validation commands`
|
|
58
58
|
7. `## Risks and unresolved decisions`
|
|
59
|
+
8. `## Publication contract`
|
|
59
60
|
|
|
60
61
|
Include exact target files and observable results. Derive every repository
|
|
61
62
|
command from current scripts or authoritative tool help. Do not assume a
|
|
62
63
|
language, framework, package manager, flag order, or cwd syntax.
|
|
63
64
|
|
|
65
|
+
The Publication contract is part of the reviewed artifact and authorizes the
|
|
66
|
+
post-verification publication. State the exact bound branch, remote, target
|
|
67
|
+
branch, merge-request title and description, and the GitLab project or hosted
|
|
68
|
+
remote evidence. The title must use this Conventional Commit format exactly:
|
|
69
|
+
`fix: [<JiraId>] <brief summary of the changes>`. The description must use this
|
|
70
|
+
format exactly, replacing placeholders with current evidence and omitting the
|
|
71
|
+
Experiment ID line when none exists:
|
|
72
|
+
|
|
73
|
+
```md
|
|
74
|
+
- Jira ID : {JiraId}
|
|
75
|
+
- Experiment ID : {ExperimentId, if any}
|
|
76
|
+
|
|
77
|
+
## Proposed changes
|
|
78
|
+
- {changes}
|
|
79
|
+
|
|
80
|
+
## Test added in this MR
|
|
81
|
+
- **Unit test**
|
|
82
|
+
- {test cases}
|
|
83
|
+
- **Functional test (If need)**
|
|
84
|
+
- {test cases}
|
|
85
|
+
- **Integration test (If need)**
|
|
86
|
+
- {test cases}
|
|
87
|
+
|
|
88
|
+
## Tested scenarios with screenshots
|
|
89
|
+
| Scenario | Production | This branch |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| Scenario 1 | paste screenshot here | paste screenshot here |
|
|
92
|
+
| Scenario 2 | paste screenshot here | paste screenshot here |
|
|
93
|
+
|
|
94
|
+
/assign me
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
It authorizes only a non-force push of the verified HEAD to that same branch
|
|
98
|
+
and creation of one merge request for this ticket. Do not include credentials,
|
|
99
|
+
arbitrary shell commands, history rewrites, branch deletion, Jira mutation,
|
|
100
|
+
merging, or any other remote mutation. If the remote, target branch, or
|
|
101
|
+
merge-request metadata cannot be established safely from current evidence, use
|
|
102
|
+
`blocked` rather than leaving a publish decision for the verification step.
|
|
103
|
+
|
|
64
104
|
Call `structured_output` alone with outcome `submit`, the complete Markdown in
|
|
65
105
|
`artifact`, and a self-contained execution handoff in `summary`. Use `blocked`
|
|
66
106
|
when ticket identity, access, or evidence is insufficient for a safe plan. Use
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
You independently verify the approved ticket work
|
|
2
|
-
|
|
1
|
+
You independently verify the approved ticket work, then publish its reviewed
|
|
2
|
+
Publication contract. Do not edit files, amend commits, change worktrees, or
|
|
3
|
+
mutate Jira. The only allowed external mutations are the contract's non-force
|
|
4
|
+
push and one GitLab merge-request creation.
|
|
3
5
|
|
|
4
6
|
Ticket input:
|
|
5
7
|
{{workflow.input}}
|
|
@@ -17,13 +19,57 @@ ticket acceptance criterion against current code and behavior. Run every exact
|
|
|
17
19
|
repository-native validation command from the approved plan. A skipped, stale,
|
|
18
20
|
unavailable, or failing required check is not passing.
|
|
19
21
|
|
|
22
|
+
Any regression, lint failure, formatting failure, or other actionable local
|
|
23
|
+
verification finding is `failed`; the workflow sends that outcome directly back
|
|
24
|
+
to implementation. Do not use `blocked` for a fixable local finding.
|
|
25
|
+
|
|
26
|
+
Only after all local criteria pass, parse the approved `## Publication contract`
|
|
27
|
+
and validate its branch, remote, target branch, project, title, and description
|
|
28
|
+
against the bound workspace and current remote evidence. The commit being
|
|
29
|
+
published must be the current verified `HEAD`; record its full SHA. Query the
|
|
30
|
+
remote branch and existing GitLab merge requests first. If the exact SHA is
|
|
31
|
+
already published, do not push again. Otherwise push only that current HEAD to
|
|
32
|
+
the contract branch with a non-force `git push`. Publish only committed code:
|
|
33
|
+
never stage, commit, stash, discard, or otherwise consider pending staged or
|
|
34
|
+
unstaged working-tree changes part of the publication. Those changes must not
|
|
35
|
+
change the exact `HEAD` SHA being pushed. Never use `--force`, `--set-upstream`,
|
|
36
|
+
refspec wildcards, another remote, or another branch. If the push is rejected,
|
|
37
|
+
ambiguous, or proves that the remote branch contains different history, use
|
|
38
|
+
`blocked` and do not attempt a workaround.
|
|
39
|
+
|
|
40
|
+
Use MCP only for an enabled, exact server/tool selector. Every MCP call must
|
|
41
|
+
name both `server` and `tool`; never use MCP discovery or proxy modes such as
|
|
42
|
+
`action`, `connect`, `describe`, `search`, `regex`, or a server-only call. Use
|
|
43
|
+
the configured Atlassian tool for Jira evidence. This ticket workflow does not
|
|
44
|
+
authorize GitLab MCP tools, so inspect and create GitLab merge requests with
|
|
45
|
+
the authenticated host CLI instead of attempting an MCP call.
|
|
46
|
+
|
|
47
|
+
Before the first remote query or push, run the contract's `git ls-remote`
|
|
48
|
+
branch check as one standalone Bash call, never as part of a command chain.
|
|
49
|
+
This is the SSH-authentication preflight and may display a 1Password approval.
|
|
50
|
+
If SSH authentication is unavailable (for example, the agent socket cannot be
|
|
51
|
+
reached, the agent refuses the signature, or approval is cancelled), do not try
|
|
52
|
+
alternate credentials or a workaround. Return `blocked` with the redacted
|
|
53
|
+
diagnostic and the precise recovery: unlock/approve the configured 1Password
|
|
54
|
+
SSH key for the remote host in an interactive session, then resume this step.
|
|
55
|
+
|
|
56
|
+
After the branch is confirmed remote, reuse an existing open merge request only
|
|
57
|
+
when its source branch, target branch, and ticket correlation match the contract.
|
|
58
|
+
Otherwise create exactly one GitLab merge request using the contract title and
|
|
59
|
+
description. Refresh it and confirm its URL, project, source branch, target
|
|
60
|
+
branch, and head SHA. Never merge, close, approve, alter an unrelated MR, or
|
|
61
|
+
retry an ambiguous mutation. A missing or materially incomplete Publication
|
|
62
|
+
contract is `blocked`, not permission to infer a publish action.
|
|
63
|
+
|
|
20
64
|
Call `structured_output` alone with:
|
|
21
65
|
|
|
22
|
-
- `passed` only when all criteria and checks pass
|
|
66
|
+
- `passed` only when all criteria and checks pass and the reviewed commit is
|
|
67
|
+
pushed and represented by the matching GitLab merge request;
|
|
23
68
|
- `failed` for an actionable implementation defect, with exact location,
|
|
24
69
|
evidence, and the smallest corrective handoff;
|
|
25
70
|
- `blocked` when ticket or repository evidence is stale or verification cannot
|
|
26
71
|
proceed safely.
|
|
27
72
|
|
|
28
73
|
Include the refreshed ticket identity, commands/results, per-criterion evidence,
|
|
29
|
-
diff/commit identity,
|
|
74
|
+
diff/commit identity, remote branch result, merge-request URL/identity, and
|
|
75
|
+
final status in the summary. Do not fix findings.
|
|
@@ -18,6 +18,10 @@ derive any necessary invocation-only correction from current scripts or tool
|
|
|
18
18
|
help without weakening the check. A skipped, stale, unavailable, or failing
|
|
19
19
|
required check is not passing.
|
|
20
20
|
|
|
21
|
+
Any regression, lint failure, formatting failure, or other actionable local
|
|
22
|
+
verification finding is `failed`; the workflow sends that outcome directly back
|
|
23
|
+
to implementation. Do not use `blocked` for a fixable local finding.
|
|
24
|
+
|
|
21
25
|
Call `structured_output` alone with:
|
|
22
26
|
|
|
23
27
|
- `passed` only when every criterion and required check passes;
|
package/package.json
CHANGED
|
@@ -124,6 +124,19 @@
|
|
|
124
124
|
}
|
|
125
125
|
]
|
|
126
126
|
},
|
|
127
|
+
"workspaceRoot": {
|
|
128
|
+
"type": "string",
|
|
129
|
+
"minLength": 1,
|
|
130
|
+
"maxLength": 4096,
|
|
131
|
+
"pattern": "^\\S(?:.*\\S)?$",
|
|
132
|
+
"allOf": [
|
|
133
|
+
{
|
|
134
|
+
"not": {
|
|
135
|
+
"pattern": "\\u0000"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
},
|
|
127
140
|
"resourceName": {
|
|
128
141
|
"type": "string",
|
|
129
142
|
"pattern": "^[A-Za-z0-9_@./:+-]+$"
|
|
@@ -575,7 +588,7 @@
|
|
|
575
588
|
"uniqueItems": true,
|
|
576
589
|
"default": ["."],
|
|
577
590
|
"items": {
|
|
578
|
-
"$ref": "#/$defs/
|
|
591
|
+
"$ref": "#/$defs/workspaceRoot"
|
|
579
592
|
}
|
|
580
593
|
}
|
|
581
594
|
}
|
package/src/config/types.ts
CHANGED
|
@@ -95,7 +95,7 @@ export type WorkflowGate = PromptGate | PlannotatorGate;
|
|
|
95
95
|
export type StepWorkspaceBinding = {
|
|
96
96
|
/** Outcomes whose result establishes the workspace for later steps. */
|
|
97
97
|
readonly bindOn: ReadonlyArray<string>;
|
|
98
|
-
/**
|
|
98
|
+
/** Relative, absolute, or ~/ home-relative paths that may contain the workspace. */
|
|
99
99
|
readonly allowedRoots: ReadonlyArray<string>;
|
|
100
100
|
};
|
|
101
101
|
|
|
@@ -159,12 +159,12 @@ function parseWorkspaceRoots(
|
|
|
159
159
|
): Array<string> {
|
|
160
160
|
if (value === undefined) return ['.'];
|
|
161
161
|
if (!Array.isArray(value)) {
|
|
162
|
-
errors.push(`${path}: expected an array of
|
|
162
|
+
errors.push(`${path}: expected an array of workspace paths`);
|
|
163
163
|
return [];
|
|
164
164
|
}
|
|
165
165
|
if (value.length > MAX_WORKSPACE_ALLOWED_ROOTS) {
|
|
166
166
|
errors.push(
|
|
167
|
-
`${path}: at most ${MAX_WORKSPACE_ALLOWED_ROOTS}
|
|
167
|
+
`${path}: at most ${MAX_WORKSPACE_ALLOWED_ROOTS} workspace paths are allowed`,
|
|
168
168
|
);
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -173,7 +173,7 @@ function parseWorkspaceRoots(
|
|
|
173
173
|
.reduce<Array<string>>((result, item, index) => {
|
|
174
174
|
const itemPath = `${path}[${index}]`;
|
|
175
175
|
if (typeof item !== 'string' || !item || item.trim() !== item) {
|
|
176
|
-
errors.push(`${itemPath}: expected a non-empty
|
|
176
|
+
errors.push(`${itemPath}: expected a non-empty workspace path`);
|
|
177
177
|
return result;
|
|
178
178
|
}
|
|
179
179
|
const root = item;
|
|
@@ -185,10 +185,11 @@ function parseWorkspaceRoots(
|
|
|
185
185
|
}
|
|
186
186
|
if (
|
|
187
187
|
root.includes('\0') ||
|
|
188
|
-
isAbsolute(root)
|
|
189
|
-
win32.parse(root).root !== ''
|
|
188
|
+
(win32.parse(root).root !== '' && !isAbsolute(root))
|
|
190
189
|
) {
|
|
191
|
-
errors.push(
|
|
190
|
+
errors.push(
|
|
191
|
+
`${itemPath}: expected a relative, absolute, or home-relative path`,
|
|
192
|
+
);
|
|
192
193
|
return result;
|
|
193
194
|
}
|
|
194
195
|
if (result.includes(root)) {
|
|
@@ -198,7 +199,7 @@ function parseWorkspaceRoots(
|
|
|
198
199
|
return [...result, root];
|
|
199
200
|
}, []);
|
|
200
201
|
if (roots.length === 0) {
|
|
201
|
-
errors.push(`${path}: at least one
|
|
202
|
+
errors.push(`${path}: at least one workspace path is required`);
|
|
202
203
|
}
|
|
203
204
|
return roots;
|
|
204
205
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
2
3
|
import { isAbsolute, relative, resolve, sep, win32 } from 'node:path';
|
|
3
4
|
|
|
4
5
|
export type ResolveWorkspaceDirectoryOptions = {
|
|
@@ -17,6 +18,15 @@ const isWithin = (root: string, candidate: string): boolean => {
|
|
|
17
18
|
);
|
|
18
19
|
};
|
|
19
20
|
|
|
21
|
+
const resolveAllowedRoot = (startCwd: string, allowedRoot: string): string =>
|
|
22
|
+
allowedRoot === '~'
|
|
23
|
+
? homedir()
|
|
24
|
+
: allowedRoot.startsWith('~/')
|
|
25
|
+
? resolve(homedir(), allowedRoot.slice(2))
|
|
26
|
+
: isAbsolute(allowedRoot)
|
|
27
|
+
? allowedRoot
|
|
28
|
+
: resolve(startCwd, allowedRoot);
|
|
29
|
+
|
|
20
30
|
/**
|
|
21
31
|
* Canonicalizes and validates one YAML-authorized execution directory.
|
|
22
32
|
*
|
|
@@ -46,15 +56,14 @@ export function resolveWorkspaceDirectory({
|
|
|
46
56
|
const canonicalRoots = allowedRoots.map((allowedRoot) => {
|
|
47
57
|
if (
|
|
48
58
|
!allowedRoot ||
|
|
49
|
-
isAbsolute(allowedRoot) ||
|
|
50
|
-
win32.parse(allowedRoot).root !== '' ||
|
|
59
|
+
(win32.parse(allowedRoot).root !== '' && !isAbsolute(allowedRoot)) ||
|
|
51
60
|
allowedRoot.includes('\0')
|
|
52
61
|
) {
|
|
53
62
|
throw new Error(
|
|
54
|
-
'workspace allowed roots must be non-empty relative paths',
|
|
63
|
+
'workspace allowed roots must be non-empty relative, absolute, or home-relative paths',
|
|
55
64
|
);
|
|
56
65
|
}
|
|
57
|
-
return realpathSync(
|
|
66
|
+
return realpathSync(resolveAllowedRoot(canonicalStart, allowedRoot));
|
|
58
67
|
});
|
|
59
68
|
const canonicalCwd = realpathSync(candidateCwd);
|
|
60
69
|
if (!statSync(canonicalCwd).isDirectory()) {
|
|
@@ -149,8 +149,7 @@ const parseWorkspace = (
|
|
|
149
149
|
root !== root.trim() ||
|
|
150
150
|
root.length > MAX_WORKSPACE_PATH_CHARS ||
|
|
151
151
|
root.includes('\0') ||
|
|
152
|
-
isAbsolute(root)
|
|
153
|
-
win32.parse(root).root !== '',
|
|
152
|
+
(win32.parse(root).root !== '' && !isAbsolute(root)),
|
|
154
153
|
)
|
|
155
154
|
) {
|
|
156
155
|
throw new Error('child policy workspace allowed roots are invalid');
|
|
@@ -85,3 +85,33 @@ export function buildDelegatedCompletionInstructions(): ReadonlyArray<string> {
|
|
|
85
85
|
'Stay within the configured permissions and do not broaden mutation targets or external side effects.',
|
|
86
86
|
];
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Builds the shared operator-facing format for non-successful step results.
|
|
91
|
+
*
|
|
92
|
+
* The summary is posted verbatim to chat and is the only context available to
|
|
93
|
+
* a fresh child, so it must remain actionable without becoming a transcript.
|
|
94
|
+
*/
|
|
95
|
+
export function buildNonSuccessSummaryInstructions(
|
|
96
|
+
outcomes: ReadonlyArray<string>,
|
|
97
|
+
): ReadonlyArray<string> {
|
|
98
|
+
const nonSuccessOutcomes = outcomes.filter((outcome) =>
|
|
99
|
+
['blocked', 'failed', 'retry'].includes(outcome),
|
|
100
|
+
);
|
|
101
|
+
if (nonSuccessOutcomes.length === 0) return [];
|
|
102
|
+
|
|
103
|
+
return [
|
|
104
|
+
'## Human-readable non-success results',
|
|
105
|
+
'',
|
|
106
|
+
`For ${nonSuccessOutcomes.map((outcome) => `\`${outcome}\``).join(', ')}, write a decision-first summary. It is shown verbatim to the operator and handed to a fresh child. Use this format:`,
|
|
107
|
+
'',
|
|
108
|
+
'# <Failed | Blocked | Retry>: <one-sentence plain-language decision>',
|
|
109
|
+
'1. **<short issue>** — <only the decisive evidence, including an exact command/error, path, or identifier when it enables action>.',
|
|
110
|
+
' **Action:** <the specific owner or role> must <the concrete evidence, decision, or change needed>.',
|
|
111
|
+
'2. Repeat only for other independent issues (at most three total).',
|
|
112
|
+
'**Next:** <the exact safe next move, such as provide the listed evidence and run `/workflow-resume`>.',
|
|
113
|
+
'',
|
|
114
|
+
'Do not include a process narrative, raw logs, repeated policy constraints, successful checks, clean-state notes, or statements that merely say the child lacks authority. Mention a passed check only when it directly explains the remaining issue. Name the missing prerequisite and who can supply it. Keep only details needed to make the decision or complete the next action.',
|
|
115
|
+
'',
|
|
116
|
+
];
|
|
117
|
+
}
|
package/src/prompt/step-task.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createStepContract } from './step-contract.ts';
|
|
|
4
4
|
import {
|
|
5
5
|
buildDelegatedCompletionInstructions,
|
|
6
6
|
buildDelegatedHandoffSection,
|
|
7
|
+
buildNonSuccessSummaryInstructions,
|
|
7
8
|
buildRestartWorkspaceSection,
|
|
8
9
|
buildResourceSection,
|
|
9
10
|
} from './step-sections.ts';
|
|
@@ -149,6 +150,7 @@ export function buildStepTask(options: BuildStepTaskOptions): string {
|
|
|
149
150
|
...contract.workspaceLines,
|
|
150
151
|
'',
|
|
151
152
|
'Put a self-contained compact handoff in `summary`; this is the only step context passed to the next fresh child.',
|
|
153
|
+
...buildNonSuccessSummaryInstructions(contract.outcomes),
|
|
152
154
|
...(isDelegated ? buildDelegatedCompletionInstructions() : []),
|
|
153
155
|
'Do not call the completion tool alongside other tool calls.',
|
|
154
156
|
].join('\n');
|