@wichayutdew/pi-workflows 2.4.0 → 2.5.0
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/dist/index.js +30 -11
- 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/commands.ts +9 -0
- 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/harness.ts +11 -0
- package/src/integrations/subagents/child-policy-sections.ts +1 -2
- package/src/prompt/main-workflow-notice.ts +1 -1
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
|
}
|
|
@@ -1374,6 +1374,13 @@ function createHarnessCommands(controller) {
|
|
|
1374
1374
|
handler: async (reason, context) => controller.abort(reason.trim(), context)
|
|
1375
1375
|
}
|
|
1376
1376
|
},
|
|
1377
|
+
{
|
|
1378
|
+
name: "workflow-status",
|
|
1379
|
+
options: {
|
|
1380
|
+
description: "Toggle workflow status",
|
|
1381
|
+
handler: async (_args, context) => controller.status(context)
|
|
1382
|
+
}
|
|
1383
|
+
},
|
|
1377
1384
|
{
|
|
1378
1385
|
name: "workflow-reload",
|
|
1379
1386
|
options: {
|
|
@@ -2381,7 +2388,7 @@ var parseWorkspace2 = (value, outcomes) => {
|
|
|
2381
2388
|
if (!isStringArray(bindOn) || bindOn.length === 0 || new Set(bindOn).size !== bindOn.length || bindOn.some((outcome) => !outcomes.includes(outcome))) {
|
|
2382
2389
|
throw new Error("child policy workspace bindOn outcomes are invalid");
|
|
2383
2390
|
}
|
|
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") ||
|
|
2391
|
+
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
2392
|
throw new Error("child policy workspace allowed roots are invalid");
|
|
2386
2393
|
}
|
|
2387
2394
|
return { workspace: { bindOn, allowedRoots } };
|
|
@@ -5316,11 +5323,13 @@ async function showWorkflowStatus(ctx, getSnapshot, statusShortcut = DEFAULT_STA
|
|
|
5316
5323
|
}
|
|
5317
5324
|
// src/harness/workspace-directory.ts
|
|
5318
5325
|
import { realpathSync, statSync } from "node:fs";
|
|
5326
|
+
import { homedir as homedir2 } from "node:os";
|
|
5319
5327
|
import { isAbsolute as isAbsolute10, relative as relative6, resolve as resolve10, sep as sep5, win32 as win323 } from "node:path";
|
|
5320
5328
|
var isWithin2 = (root, candidate) => {
|
|
5321
5329
|
const pathFromRoot = relative6(root, candidate);
|
|
5322
5330
|
return pathFromRoot === "" || pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep5}`) && !isAbsolute10(pathFromRoot);
|
|
5323
5331
|
};
|
|
5332
|
+
var resolveAllowedRoot = (startCwd, allowedRoot) => allowedRoot === "~" ? homedir2() : allowedRoot.startsWith("~/") ? resolve10(homedir2(), allowedRoot.slice(2)) : isAbsolute10(allowedRoot) ? allowedRoot : resolve10(startCwd, allowedRoot);
|
|
5324
5333
|
function resolveWorkspaceDirectory({
|
|
5325
5334
|
candidateCwd,
|
|
5326
5335
|
startCwd,
|
|
@@ -5337,10 +5346,10 @@ function resolveWorkspaceDirectory({
|
|
|
5337
5346
|
}
|
|
5338
5347
|
const canonicalStart = realpathSync(startCwd);
|
|
5339
5348
|
const canonicalRoots = allowedRoots.map((allowedRoot) => {
|
|
5340
|
-
if (!allowedRoot ||
|
|
5341
|
-
throw new Error("workspace allowed roots must be non-empty relative paths");
|
|
5349
|
+
if (!allowedRoot || win323.parse(allowedRoot).root !== "" && !isAbsolute10(allowedRoot) || allowedRoot.includes("\x00")) {
|
|
5350
|
+
throw new Error("workspace allowed roots must be non-empty relative, absolute, or home-relative paths");
|
|
5342
5351
|
}
|
|
5343
|
-
return realpathSync(
|
|
5352
|
+
return realpathSync(resolveAllowedRoot(canonicalStart, allowedRoot));
|
|
5344
5353
|
});
|
|
5345
5354
|
const canonicalCwd = realpathSync(candidateCwd);
|
|
5346
5355
|
if (!statSync(canonicalCwd).isDirectory()) {
|
|
@@ -6882,7 +6891,7 @@ function buildMainWorkflowNotice(workflow, run, statusShortcutLabel = "Ctrl+Alt+
|
|
|
6882
6891
|
"",
|
|
6883
6892
|
`Workflow "${workflow.definition.id}" is running step "${run.currentStepId}" (${step.title}) in a separate pi-subagents child process.`,
|
|
6884
6893
|
"Do not perform the workflow step in this main session.",
|
|
6885
|
-
`Use \`${statusShortcutLabel}\`
|
|
6894
|
+
`Use \`${statusShortcutLabel}\` or \`/workflow-status\` to open the workflow status overlay, or \`/workflow-pause\` to cancel the child and repair the workflow before resuming.`
|
|
6886
6895
|
].join(`
|
|
6887
6896
|
`);
|
|
6888
6897
|
}
|
|
@@ -8573,6 +8582,16 @@ class WorkflowHarness {
|
|
|
8573
8582
|
abort(reason, context) {
|
|
8574
8583
|
return this.enqueueMutation(context, () => this.abortNow(reason, context));
|
|
8575
8584
|
}
|
|
8585
|
+
async status(context) {
|
|
8586
|
+
this.latestContext = context;
|
|
8587
|
+
if (this.isStatusOverlayOpen)
|
|
8588
|
+
return;
|
|
8589
|
+
if (!this.run) {
|
|
8590
|
+
context.ui.notify("No workflow checkpoint in this session", "info");
|
|
8591
|
+
return;
|
|
8592
|
+
}
|
|
8593
|
+
await this.showWorkflowStatus(context);
|
|
8594
|
+
}
|
|
8576
8595
|
reload(context) {
|
|
8577
8596
|
return this.enqueueMutation(context, () => this.reloadNow(context));
|
|
8578
8597
|
}
|
|
@@ -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/commands.ts
CHANGED
|
@@ -43,6 +43,8 @@ export type WorkflowCommandController = {
|
|
|
43
43
|
reason: string,
|
|
44
44
|
context: ExtensionCommandContext,
|
|
45
45
|
) => Promise<void>;
|
|
46
|
+
/** Opens the active workflow status overlay. */
|
|
47
|
+
readonly status: (context: ExtensionCommandContext) => Promise<void>;
|
|
46
48
|
/** Reloads workflow configuration. */
|
|
47
49
|
readonly reload: (context: ExtensionCommandContext) => Promise<void>;
|
|
48
50
|
};
|
|
@@ -165,6 +167,13 @@ export function createHarnessCommands(
|
|
|
165
167
|
controller.abort(reason.trim(), context),
|
|
166
168
|
},
|
|
167
169
|
},
|
|
170
|
+
{
|
|
171
|
+
name: 'workflow-status',
|
|
172
|
+
options: {
|
|
173
|
+
description: 'Toggle workflow status',
|
|
174
|
+
handler: async (_args, context) => controller.status(context),
|
|
175
|
+
},
|
|
176
|
+
},
|
|
168
177
|
{
|
|
169
178
|
name: 'workflow-reload',
|
|
170
179
|
options: {
|
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()) {
|
package/src/harness.ts
CHANGED
|
@@ -380,6 +380,17 @@ export class WorkflowHarness implements WorkflowCommandController {
|
|
|
380
380
|
return this.enqueueMutation(context, () => this.abortNow(reason, context));
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
/** Opens the workflow status overlay, matching the configured shortcut. */
|
|
384
|
+
async status(context: ExtensionCommandContext): Promise<void> {
|
|
385
|
+
this.latestContext = context;
|
|
386
|
+
if (this.isStatusOverlayOpen) return;
|
|
387
|
+
if (!this.run) {
|
|
388
|
+
context.ui.notify('No workflow checkpoint in this session', 'info');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
await this.showWorkflowStatus(context);
|
|
392
|
+
}
|
|
393
|
+
|
|
383
394
|
/** Reloads workflow configuration while no workflow is executing. */
|
|
384
395
|
reload(context: ExtensionCommandContext): Promise<void> {
|
|
385
396
|
return this.enqueueMutation(context, () => this.reloadNow(context));
|
|
@@ -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');
|
|
@@ -36,6 +36,6 @@ export function buildMainWorkflowNotice(
|
|
|
36
36
|
'',
|
|
37
37
|
`Workflow "${workflow.definition.id}" is running step "${run.currentStepId}" (${step.title}) in a separate pi-subagents child process.`,
|
|
38
38
|
'Do not perform the workflow step in this main session.',
|
|
39
|
-
`Use \`${statusShortcutLabel}\`
|
|
39
|
+
`Use \`${statusShortcutLabel}\` or \`/workflow-status\` to open the workflow status overlay, or \`/workflow-pause\` to cancel the child and repair the workflow before resuming.`,
|
|
40
40
|
].join('\n');
|
|
41
41
|
}
|