feat(sdk): add --ws flag for workstream-aware execution#1443
feat(sdk): add --ws flag for workstream-aware execution#1443odmrs wants to merge 2 commits intogsd-build:mainfrom
Conversation
e9e5659 to
b2e77f4
Compare
trek-e
left a comment
There was a problem hiding this comment.
Adversarial Review: feat(sdk): add --ws flag for workstream-aware execution
Verdict: CHANGES REQUESTED
The --ws flag feature itself is well-implemented. However, this PR has scope creep that needs to be addressed.
BLOCKER 1: PR bundles 3 unrelated changes into one
This PR contains 4 commits spanning 3 distinct concerns:
- feat(sdk): add --ws flag (commit b2e77f4) -- the stated feature
- perf(sdk): skip completed steps when resuming phases (commit c01c26a) -- a performance optimization unrelated to workstreams
- fix(sdk): phase success based on verify outcome (commit c5d174c) -- a bug fix unrelated to workstreams
- fix(sdk): verify outcome gates advance correctly (commit 4bf9789) -- the fix from PR #1454 review feedback, also unrelated to workstreams
Commits 2-4 should be in separate PRs. The skip-completed-steps optimization and the verify-outcome fixes are independent concerns that happen to touch the same file but serve different purposes. Bundling them makes it impossible to merge the --ws feature independently or revert the verify fix without losing the workstream feature.
Fix: Split this into 3 PRs: (a) the --ws flag feature, (b) the step-skip optimization, (c) the verify outcome gate fixes. The verify fixes may already be covered by PR #1454 -- coordinate to avoid duplicate work.
Issue 2: context-engine.ts constructor overload is fragile
constructor(projectDir: string, loggerOrWorkstream?: GSDLogger | string, logger?: GSDLogger) {
if (typeof loggerOrWorkstream === 'string') { ... }This union-type parameter is a maintenance hazard. If GSDLogger ever gains a string form or if someone passes a string logger name, this silently takes the wrong branch. A cleaner approach:
constructor(projectDir: string, opts?: { workstream?: string; logger?: GSDLogger })Or keep the existing constructor signature and add a static factory:
static forWorkstream(projectDir: string, workstream: string, logger?: GSDLogger)This is non-blocking but worth addressing.
Positive observations
- The --ws flag threading through the full stack (CLI -> GSD -> GSDTools -> config -> context-engine -> init-runner) is thorough and consistent.
- The
relPlanningPath()helper in init-runner.ts is a clean abstraction that avoids scattered path concatenation. - The
--wsargument appending in gsd-tools.ts ([...args, ...wsArgs]) correctly leverages gsd-tools.cjs's existing--wssupport. - The init-runner.ts changes are all mechanical path replacements with no behavioral changes beyond routing.
- No security issues or prompt injection patterns detected.
Overlap note
This PR overlaps with PR #1454 (commits 3-4 contain the same verify-outcome fixes). Coordinate with that PR to avoid merge conflicts and duplicate test changes.
4bf9789 to
6e622e6
Compare
|
Thanks for the thorough review @trek-e! Addressed all feedback: BLOCKER 1 (scope creep): Removed all 3 unrelated commits. The branch now contains only the Issue 2 (constructor overload): Refactored constructor(projectDir: string, opts?: { workstream?: string; logger?: GSDLogger })Updated all call sites and tests accordingly. Build compiles, 675/675 tests pass. |
Re-Review: feat(sdk): add --ws flag for workstream-aware executionPrevious blockers — verificationBLOCKER 1 (scope creep): Verified in diff. The PR now contains only workstream-related changes. Issue 2 (constructor overload): Verified. Fresh adversarial review of current diffPath traversal in workstream name. The
No prompt injection patterns detected. Tests: The Mergeability: MERGEABLE. Verdict: CHANGES REQUESTEDOne remaining issue: the workstream name is used as a path component without validation. A name containing |
trek-e
left a comment
There was a problem hiding this comment.
Adversarial Re-Review — APPROVED
Author pushed updates after last review. All blocking issues have been addressed.
Prior Issue Tracking
1. PR bundles 3 unrelated changes: FIXED
The PR is now 2 commits across 9 files, all within sdk/src/. The unrelated commits (skip-completed-steps performance optimization, verify-outcome gate fixes) have been removed. The two remaining commits are:
feat(sdk): add --ws flag for workstream-aware execution(the stated feature)refactor(sdk): use options object for ContextEngine constructor(addresses issue #2 below)
Both are directly related to the --ws flag feature.
2. context-engine.ts constructor overload is fragile: FIXED
The constructor was refactored from constructor(projectDir: string, loggerOrWorkstream?: GSDLogger | string, logger?: GSDLogger) to constructor(projectDir: string, opts?: { workstream?: string; logger?: GSDLogger }). This is the exact fix suggested in the prior review (options object pattern). Clean, unambiguous, no union-type fragility. The test file was updated accordingly (new ContextEngine(projectDir, { logger })).
Implementation Review
The --ws flag threading is thorough and consistent:
cli.ts: parses--wsargument, passes to GSD constructor and InitRunnertypes.ts: addsworkstream?: stringto GSDOptionsindex.ts: stores workstream, passes to loadConfig, ContextEngine, GSDTools, runPhaseconfig.ts: loadConfig accepts optional workstream parameter, resolves to.planning/workstreams/<name>/config.jsoncontext-engine.ts: resolves planningDir to workstream subdirectorygsd-tools.ts: appends--ws <name>to all CLI tool invocationsinit-runner.ts: usesrelPlanningPath()helper for all path construction, creating workstream-aware paths throughout
The relPlanningPath() helper in init-runner.ts is clean — single source of truth for path construction, avoids scattered conditional path building.
sdk/package-lock.json
The diff includes a "license": "MIT" addition to package-lock.json. This is a harmless metadata change, likely from an npm install run.
No new issues found.
trek-e
left a comment
There was a problem hiding this comment.
Code Review
Verdict: APPROVE
Security
No issues found. The workstream name is passed as a CLI argument and used directly in filesystem path construction. There is no sanitization of the workstream name (e.g., rejecting path traversal characters like ../). A value like --ws ../../etc would resolve to .planning/workstreams/../../etc/, which is outside the .planning/ tree. This is low-severity given the CLI context (local developer tool, not a server), but worth hardening with a simple alphanumeric-plus-hyphen validation on the workstream value before it reaches path construction.
Logic / Correctness
gsd-tools.ts — wsArgs placement in execRaw:
In exec(), the injected args are appended after user args:
[gsdToolsPath, command, ...args, ...wsArgs]
In execRaw() the same order is used, but --raw is appended last:
[gsdToolsPath, command, ...args, ...wsArgs, '--raw']
If gsd-tools.cjs uses a positional-arg or stop-at-first-non-flag parser, the position of --ws relative to --raw could matter. This is fine as long as the underlying CLI parses flags anywhere — worth a quick confirmation that gsd-tools.cjs is not order-sensitive here.
init-runner.ts — relPlanningPath vs planningDir:
The helper relPlanningPath() constructs relative strings (used in prompts and artifact lists), while this.planningDir (an absolute path) is used for actual filesystem reads/writes. The two are kept in sync correctly — the division is intentional and clean.
Missing propagation in runPhase (index.ts):
GSD.runPhase() creates a ContextEngine with workstream and calls loadConfig with workstream. It also calls this.createTools() which propagates workstream to GSDTools. All three axes (config, context, tools) are covered. No gap found.
Test Coverage
The two updated context-engine.test.ts callsites are corrected to the new options-object constructor — good. However, there are no new tests for:
relPlanningPath()— trivial but worth a unit test for the workstream and non-workstream branches.loadConfig()with a workstream arg — should verify the path resolves to.planning/workstreams/<name>/config.json.GSDTools.exec()/execRaw()— no test that--ws <name>is injected into the args whenworkstreamis set.parseCliArgs()— no test that--ws foopopulatesargs.workstream.
The PR description notes "675 tests passing, zero breakage" which is encouraging. The missing tests are for the new code paths, not regressions. These are gaps, not blockers for merging.
Style
No issues found. The relPlanningPath() private helper is a clean abstraction that avoids string duplication. Constructor refactor of ContextEngine to an options object (addressed in the second commit) is the right pattern. JSDoc on workstream fields is consistent throughout.
Summary
Clean, well-scoped feature that correctly propagates workstream context through all five layers of the stack (CLI → GSD → GSDTools, loadConfig, ContextEngine, InitRunner). The one issue worth addressing before a follow-up is input validation on the workstream name to prevent path traversal; new unit tests for the four new code paths would also strengthen confidence.
trek-e
left a comment
There was a problem hiding this comment.
Code Review Update (Pass 2)
Verdict: APPROVE — prior review issues addressed
Prior Review Resolution
The prior review approved the --ws flag workstream support. The last review noted one minor security concern: workstream name used directly in path construction without sanitization (e.g., --ws ../../etc could escape .planning/). This is low-severity in a local developer tool context and was noted as advisory.
CI has no checks reported on the feat/workstream-support branch, but the prior review was APPROVED.
Summary
Ready to merge pending CI run. The path sanitization concern is advisory and acceptable for a local CLI tool.
|
This PR has been open for more than 24 hours without a linked issue. Please link a GitHub issue (e.g., |
trek-e
left a comment
There was a problem hiding this comment.
Code Review
Verdict: REQUEST CHANGES
BLOCKER 1: Path traversal via unsanitized workstream name
The --ws value flows directly into filesystem paths without validation. A value like --ws ../../etc resolves to .planning/workstreams/../../etc/, escaping the .planning/ tree entirely. This affects every path construction site:
config.ts:join(projectDir, '.planning', 'workstreams', workstream)context-engine.ts: same patterninit-runner.ts:this.planningDirandrelPlanningPath()gsd-tools.ts: passed as--wsarg togsd-tools.cjs(which does its own path resolution)
Prior reviews noted this as "advisory for a local CLI tool." That framing undersells the impact: gsd-sdk is designed for headless/autonomous execution. An agent passing a malformed workstream name to the SDK would write artifacts to arbitrary paths. This is not hypothetical — the SDK's own auto mode runs agent sessions that could propagate bad inputs.
Fix: Add validation in parseCliArgs() (cli.ts) immediately after parsing --ws:
if (workstream && !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(workstream)) {
console.error('Error: --ws name must be alphanumeric (hyphens, underscores, dots allowed)');
process.exit(1);
}This is a single point of validation that protects all downstream consumers.
BLOCKER 2: No linked issue / missing closing keyword
The PR body does not contain a Closes #N or Fixes #N reference. Per CONTRIBUTING.md, features require an issue with approved-feature or approved-enhancement label. Without a linked issue, there is no way to verify this requirement is met.
Fix: Add Closes #<issue-number> to the PR body, referencing an issue that has the appropriate label.
BLOCKER 3: Merge conflicts
The PR has a CONFLICTING mergeable status. Rebase onto main to resolve.
Issue 4: No tests for new code paths
Four new code paths have zero test coverage:
parseCliArgs()with--ws foo— verifyargs.workstream === 'foo'loadConfig(dir, 'my-ws')— verify path resolves to.planning/workstreams/my-ws/config.jsonGSDTools.exec()/execRaw()— verify--ws <name>is injected into child process argsrelPlanningPath()— verify both workstream and non-workstream branches
The existing 675 tests confirm no regressions, but the new feature itself is untested. At minimum, tests for items 1 and 4 are trivial to add and would catch future path-construction regressions.
Issue 5: --ws flag name collision risk with --ws-port
Both --ws and --ws-port are registered CLI options. While Node's parseArgs distinguishes them correctly, the UX is confusing — especially since ws in --ws-port refers to WebSocket. Consider --workstream as the long-form flag name to avoid ambiguity.
Positive observations
- The
--wsthreading through all five layers (CLI -> GSD -> GSDTools/Config/ContextEngine -> InitRunner) is thorough and consistent. relPlanningPath()is a clean single-source-of-truth abstraction.- The
ContextEngineconstructor refactor to an options object (addressing the prior review) is the right pattern. - The
gsd-tools.tsarg injection correctly leveragesgsd-tools.cjs's existing--wssupport. - All init-runner prompt builders correctly reference workstream-aware paths.
trek-e
left a comment
There was a problem hiding this comment.
Re-review — PR #1443
Verdict: REQUEST CHANGES
The diff reviewed is commit 6e622e6. Prior reviews (trek-e, 2026-04-04) resolved the scope creep and the ContextEngine constructor fragility. A CHANGES_REQUESTED review from 2026-04-05T13:14 raised three new blockers. None of the three are resolved in the current diff.
Prior issues confirmed resolved
Scope creep (3 unrelated commits bundled): Fixed. The PR is now 2 commits touching only sdk/src/, both directly related to the --ws flag feature plus the constructor refactor.
ContextEngine constructor union-type fragility: Fixed. Constructor now takes opts?: { workstream?: string; logger?: GSDLogger }. The context-engine.test.ts callsites were updated accordingly.
Still blocking
BLOCKER 1: No input validation on the workstream name — path traversal
The --ws value flows from parseCliArgs() directly into filesystem path construction at four sites:
config.ts:join(projectDir, '.planning', 'workstreams', workstream)context-engine.ts: same patterninit-runner.ts:this.planningDirandrelPlanningPath()gsd-tools.ts: passed as--wsarg togsd-tools.cjs
A value like --ws ../../etc resolves to .planning/workstreams/../../etc/, escaping the .planning/ tree. The prior code review (2026-04-04) classified this as low-severity advisory for a local CLI tool. The CHANGES_REQUESTED review (2026-04-05) upgraded it to a blocker because gsd-sdk is designed for headless/autonomous execution — agent sessions running in auto mode can propagate bad workstream names. The fix is a single validation in parseCliArgs():
if (args.workstream && !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(args.workstream)) {
console.error('Error: --ws name must be alphanumeric (hyphens, underscores, dots allowed)');
process.exit(1);
}The current diff adds no such validation. The workstream value reaches path construction unsanitized.
BLOCKER 2: No linked issue / missing closing keyword
The PR body contains no Closes #N, Fixes #N, or Resolves #N. Per CONTRIBUTING.md, features require an issue labeled approved-feature or approved-enhancement before the PR can be merged. Without a linked issue, there is no way to confirm label compliance, and the PR auto-close CI check will fail.
BLOCKER 3: Merge conflicts
The PR carries needs merge fixes label. The mergeStateStatus is UNKNOWN (conflicting). Rebase onto main before this can land.
Issue worth addressing before merge (non-blocking)
--ws / --ws-port name collision: Both --ws and --ws-port are registered CLI options. ws in --ws-port refers to WebSocket; --ws here means workstream. The similarity is confusing in a help string. Consider --workstream as the long-form flag to avoid ambiguity. This is a UX issue, not a correctness issue.
Missing tests for four new code paths: parseCliArgs() with --ws foo, loadConfig(dir, 'my-ws') path resolution, GSDTools.exec() --ws injection, and relPlanningPath() both branches. The existing 675 tests confirm no regressions, but the new code paths themselves are untested. The path traversal validation, once added, should also have a test.
Positive observations (unchanged)
The --ws threading through all five layers (CLI → GSD → GSDTools/Config/ContextEngine → InitRunner) is thorough and consistent. relPlanningPath() is a clean single-source-of-truth abstraction. The ContextEngine constructor refactor is correct. The gsd-tools.ts arg injection correctly leverages the existing --ws support in gsd-tools.cjs. No security issues beyond the path traversal item above. No prompt injection patterns.
Summary
Two of three prior blockers from the 2026-04-05 CHANGES_REQUESTED review remain unaddressed: (1) path traversal via unsanitized workstream name — add the regex validation in parseCliArgs(), and (2) no linked issue with the required label. Also rebase to resolve merge conflicts. Once those three items are done, the implementation itself is clean and ready.
trek-e
left a comment
There was a problem hiding this comment.
Code Review — Security Blocker Unresolved
Verdict: CHANGES REQUESTED
Reviewed the current diff (commits b2e77f4, 6e622e6). The two structural issues fixed in prior rounds (scope creep, ContextEngine constructor fragility) remain fixed. The three blockers from the 2026-04-05 CHANGES_REQUESTED are still unresolved.
BLOCKER 1: Path traversal — not fixed
The --ws value reaches filesystem path construction without any validation. Tracing the data flow in the current diff:
sdk/src/cli.ts, line ~72:
workstream: values.ws as string | undefined,No validation here. A value like ../../.ssh passes through.
sdk/src/config.ts:
join(projectDir, '.planning', 'workstreams', workstream)sdk/src/context-engine.ts:
join(projectDir, '.planning', 'workstreams', opts.workstream)sdk/src/init-runner.ts — relPlanningPath():
return this.workstream
? `.planning/workstreams/${this.workstream}/${filename}`
: `.planning/${filename}`;sdk/src/gsd-tools.ts:
const wsArgs = this.workstream ? ['--ws', this.workstream] : [];None of these sites validate the workstream name. --ws ../../.ssh resolves join(projectDir, '.planning', 'workstreams', '../../.ssh') to <projectDir>/.ssh, outside the .planning/ tree entirely.
The fix belongs in parseCliArgs() in cli.ts, immediately after the workstream field is assigned:
workstream: values.ws as string | undefined,Add before the return (or inline on assignment):
const workstream = values.ws as string | undefined;
if (workstream && !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(workstream)) {
console.error('Error: --ws name must start with a letter or digit and contain only letters, digits, hyphens, underscores, or dots.');
process.exit(1);
}This is one addition in one file. All four downstream path construction sites are then safe because the bad input never reaches them.
BLOCKER 2: No linked issue
The PR body contains no Closes #N, Fixes #N, or Resolves #N. Per CONTRIBUTING.md, enhancements require a linked issue labeled approved-enhancement. Without this, CI closes the PR automatically. Add the closing keyword referencing the approved issue before requesting re-review.
BLOCKER 3: Merge conflicts
The PR has needs merge fixes on it and the base has moved since the last commit (2026-03-30). Rebase onto main to resolve.
Non-blocking (worth fixing before merge)
- Add tests for the new code paths:
parseCliArgs()with--ws fooand--ws ../../etc(should exit 1),loadConfig()with workstream arg verifying the resolved path,GSDTools.exec()verifying--wsinjection,relPlanningPath()both branches. - Consider
--workstreamas the canonical long-form flag to avoid visual collision with--ws-port(WebSocket). Not a blocker, but the help string is confusing as-is.
What is working well
The threading of workstream through all five layers (CLI → GSD → GSDTools/Config/ContextEngine → InitRunner) is consistent. relPlanningPath() is a clean single-source-of-truth for path construction. The ContextEngine constructor refactor is correct. The scope is tight. Once the three blockers above are resolved, this is ready to merge.
Add --ws <name> CLI flag that routes all .planning/ paths to .planning/workstreams/<name>/, enabling the SDK to operate within existing multi-workstream projects without conflicting with other workstreams or requiring a fresh directory. Changes: - cli.ts: Parse --ws flag, pass to GSD and InitRunner - types.ts: Add workstream field to GSDOptions - gsd-tools.ts: Inject --ws into all gsd-tools.cjs invocations - config.ts: loadConfig() resolves workstream-aware config path - context-engine.ts: Constructor accepts optional workstream name - init-runner.ts: All artifact paths use workstream-aware resolution - index.ts: GSD class propagates workstream to tools, context, config The --ws flag leverages gsd-tools.cjs existing --ws support which sets GSD_WORKSTREAM env var, making planningDir() auto-resolve workstream paths.
Reject workstream names containing path traversal characters (../, etc). Only alphanumeric characters, hyphens, underscores, and dots are allowed. Validation happens in parseCliArgs() before the value reaches any path construction site. Closes gsd-build#1884
6e622e6 to
47eabdd
Compare
All blockers resolved — ready for re-reviewHey @trek-e, pushed updates addressing all three blockers from your last review: BLOCKER 1: Path traversal — FIXED ✅Added input validation in BLOCKER 2: No linked issue — FIXED ✅Created issue #1884 and added BLOCKER 3: Merge conflicts — FIXED ✅Rebased onto Verification
|
Closes #1884
Summary
Adds
--ws <name>CLI flag togsd-sdkthat routes all.planning/paths to.planning/workstreams/<name>/, enabling the SDK to operate within existing multi-workstream projects without requiring a fresh directory.Problem:
gsd-sdk initfails with "Project already exists" when.planning/PROJECT.mdexists at the root — even if the user wants to initialize a new workstream within the same project. This blocks headless SDK usage in multi-workstream repos.Solution: Propagate a
--wsflag through the entire SDK stack, leveraginggsd-tools.cjs's existing--wssupport (GSD_WORKSTREAMenv var →planningDir()resolution).Changes
SDK (
sdk/src/)--wsflag with input validation (alphanumeric + hyphens/underscores/dots only, prevents path traversal)workstream?: stringtoGSDOptions--ws <name>into allgsd-tools.cjsinvocations (exec()andexecRaw())loadConfig()resolves workstream-aware config path (.planning/workstreams/<name>/config.json){ workstream?, logger?, truncation? }for clean extensibilityrelPlanningPath()helper for workstream-aware resolutionGSDclass stores and propagates workstream to tools, context engine, and config loaderHow it works
The
--wsflag flows through the stack:--ws→ validates name (rejects path traversal) → passes toGSDconstructorcreateTools(),loadConfig(),ContextEngine--ws <name>to everygsd-tools.cjsinvocationGSD_WORKSTREAMenv var →planningDir()resolves to.planning/workstreams/<name>/Test plan
npx tsc --noEmit— compiles with zero errorsnpm test— 2649 pass, 7 fail (pre-existing on main, unrelated to this PR)../../etc--wsflag, behavior is unchanged (flat mode)