@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 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 relative paths`);
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} relative paths are allowed`);
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 relative path`);
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") || isAbsolute(root) || win32.parse(root).root !== "") {
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 relative path is required`);
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") || isAbsolute4(root) || win322.parse(root).root !== "")) {
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 || isAbsolute10(allowedRoot) || win323.parse(allowedRoot).root !== "" || allowedRoot.includes("\x00")) {
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(resolve10(canonicalStart, allowedRoot));
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}\` to show or hide the workflow status overlay, or \`/workflow-pause\` to cancel the child and repair the workflow before resuming.`
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 a non-force push only when verified local code must reach the
29
- host. Never include approval, merge, resolution, closure, deletion,
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. Do not edit files, amend
2
- commits, change worktrees, or mutate Jira or any other external service.
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, and final status in the summary. Do not fix findings.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wichayutdew/pi-workflows",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "A declarative, pauseable workflow harness for Pi",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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/relativePath"
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: {
@@ -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
- /** Paths relative to the run-start directory that may contain the workspace. */
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 relative paths`);
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} relative paths are allowed`,
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 relative path`);
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(`${itemPath}: expected a relative path`);
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 relative path is required`);
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(resolve(canonicalStart, allowedRoot));
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}\` to show or hide the workflow status overlay, or \`/workflow-pause\` to cancel the child and repair the workflow before resuming.`,
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
  }