gsd-pi 2.41.0-dev.3557dc4 → 2.41.0-dev.5a170d0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +80 -0
- package/dist/resources/extensions/gsd/auto/phases.js +2 -2
- package/dist/resources/extensions/gsd/auto/session.js +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +2 -0
- package/dist/resources/extensions/gsd/auto.js +28 -1
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
- package/dist/resources/extensions/gsd/commands/catalog.js +32 -0
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
- package/dist/resources/extensions/gsd/context-injector.js +74 -0
- package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
- package/dist/resources/extensions/gsd/custom-verification.js +145 -0
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
- package/dist/resources/extensions/gsd/definition-loader.js +352 -0
- package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
- package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
- package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
- package/dist/resources/extensions/gsd/engine-types.js +8 -0
- package/dist/resources/extensions/gsd/execution-policy.js +8 -0
- package/dist/resources/extensions/gsd/graph.js +225 -0
- package/dist/resources/extensions/gsd/run-manager.js +134 -0
- package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
- package/dist/resources/skills/create-workflow/SKILL.md +103 -0
- package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -0
- package/src/resources/extensions/gsd/auto/loop.ts +91 -0
- package/src/resources/extensions/gsd/auto/phases.ts +2 -2
- package/src/resources/extensions/gsd/auto/session.ts +6 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto.ts +31 -1
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
- package/src/resources/extensions/gsd/commands/catalog.ts +32 -0
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
- package/src/resources/extensions/gsd/context-injector.ts +100 -0
- package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
- package/src/resources/extensions/gsd/custom-verification.ts +180 -0
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
- package/src/resources/extensions/gsd/definition-loader.ts +462 -0
- package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
- package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
- package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
- package/src/resources/extensions/gsd/engine-types.ts +71 -0
- package/src/resources/extensions/gsd/execution-policy.ts +43 -0
- package/src/resources/extensions/gsd/graph.ts +312 -0
- package/src/resources/extensions/gsd/run-manager.ts +180 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +100 -118
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/captures.test.ts +12 -1
- package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +20 -20
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
- package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
- package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
- package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
- package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
- package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
- package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
- package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
- package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +195 -105
- package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
- package/src/resources/skills/create-workflow/SKILL.md +103 -0
- package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- /package/dist/web/standalone/.next/static/{JBSIr4fSfHXs5g5x2ZBSC → K7GYOOPvQWX6TKYEKhODM}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{JBSIr4fSfHXs5g5x2ZBSC → K7GYOOPvQWX6TKYEKhODM}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ One command. Walk away. Come back to a built project with clean git history.
|
|
|
28
28
|
|
|
29
29
|
### New Features
|
|
30
30
|
|
|
31
|
-
- **Browser-based web interface** — run GSD from the browser with `
|
|
31
|
+
- **Browser-based web interface** — run GSD from the browser with `gsd --web`. Full project management, real-time progress, and multi-project support via server-sent events. (#1717)
|
|
32
32
|
- **Doctor: worktree lifecycle checks** — `/gsd doctor` now validates worktree health, detects orphaned worktrees, consolidates cleanup, and enhances `/worktree list` with lifecycle status. (#1814)
|
|
33
33
|
- **CI: docs-only PR detection** — PRs that only change documentation skip build and test steps, with a new prompt injection scan for security. (#1699)
|
|
34
34
|
- **Custom Models guide** — new documentation for adding custom providers (Ollama, vLLM, LM Studio, proxies) via `models.json`. (#1670)
|
|
@@ -12,6 +12,7 @@ import { _clearCurrentResolve } from "./resolve.js";
|
|
|
12
12
|
import { runPreDispatch, runDispatch, runGuards, runUnitPhase, runFinalize, } from "./phases.js";
|
|
13
13
|
import { debugLog } from "../debug-logger.js";
|
|
14
14
|
import { isInfrastructureError } from "./infra-errors.js";
|
|
15
|
+
import { resolveEngine } from "../engine-resolver.js";
|
|
15
16
|
/**
|
|
16
17
|
* Main auto-mode execution loop. Iterates: derive → dispatch → guards →
|
|
17
18
|
* runUnit → finalize → repeat. Exits when s.active becomes false or a
|
|
@@ -82,6 +83,85 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
82
83
|
const ic = { ctx, pi, s, deps, prefs, iteration, flowId, nextSeq };
|
|
83
84
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-start", data: { iteration } });
|
|
84
85
|
let iterData;
|
|
86
|
+
// ── Custom engine path ──────────────────────────────────────────────
|
|
87
|
+
// When activeEngineId is a non-dev value, bypass runPreDispatch and
|
|
88
|
+
// runDispatch entirely — the custom engine drives its own state via
|
|
89
|
+
// GRAPH.yaml. Shares runGuards and runUnitPhase with the dev path.
|
|
90
|
+
// After unit execution, verifies then reconciles via the engine layer.
|
|
91
|
+
//
|
|
92
|
+
// GSD_ENGINE_BYPASS=1 skips the engine layer entirely — falls through
|
|
93
|
+
// to the dev path below.
|
|
94
|
+
if (s.activeEngineId != null && s.activeEngineId !== "dev" && !sidecarItem && process.env.GSD_ENGINE_BYPASS !== "1") {
|
|
95
|
+
debugLog("autoLoop", { phase: "custom-engine-derive", iteration, engineId: s.activeEngineId });
|
|
96
|
+
const { engine, policy } = resolveEngine({
|
|
97
|
+
activeEngineId: s.activeEngineId,
|
|
98
|
+
activeRunDir: s.activeRunDir,
|
|
99
|
+
});
|
|
100
|
+
const engineState = await engine.deriveState(s.basePath);
|
|
101
|
+
if (engineState.isComplete) {
|
|
102
|
+
await deps.stopAuto(ctx, pi, "Workflow complete");
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
debugLog("autoLoop", { phase: "custom-engine-dispatch", iteration });
|
|
106
|
+
const dispatch = await engine.resolveDispatch(engineState, { basePath: s.basePath });
|
|
107
|
+
if (dispatch.action === "stop") {
|
|
108
|
+
await deps.stopAuto(ctx, pi, dispatch.reason ?? "Engine stopped");
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (dispatch.action === "skip") {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// dispatch.action === "dispatch"
|
|
115
|
+
const step = dispatch.step;
|
|
116
|
+
const gsdState = await deps.deriveState(s.basePath);
|
|
117
|
+
iterData = {
|
|
118
|
+
unitType: step.unitType,
|
|
119
|
+
unitId: step.unitId,
|
|
120
|
+
prompt: step.prompt,
|
|
121
|
+
finalPrompt: step.prompt,
|
|
122
|
+
pauseAfterUatDispatch: false,
|
|
123
|
+
observabilityIssues: [],
|
|
124
|
+
state: gsdState,
|
|
125
|
+
mid: s.currentMilestoneId ?? "workflow",
|
|
126
|
+
midTitle: "Workflow",
|
|
127
|
+
isRetry: false,
|
|
128
|
+
previousTier: undefined,
|
|
129
|
+
};
|
|
130
|
+
// ── Progress widget (mirrors dev path in runDispatch) ──
|
|
131
|
+
deps.updateProgressWidget(ctx, iterData.unitType, iterData.unitId, iterData.state);
|
|
132
|
+
// ── Guards (shared with dev path) ──
|
|
133
|
+
const guardsResult = await runGuards(ic, s.currentMilestoneId ?? "workflow");
|
|
134
|
+
if (guardsResult.action === "break")
|
|
135
|
+
break;
|
|
136
|
+
// ── Unit execution (shared with dev path) ──
|
|
137
|
+
const unitPhaseResult = await runUnitPhase(ic, iterData, loopState);
|
|
138
|
+
if (unitPhaseResult.action === "break")
|
|
139
|
+
break;
|
|
140
|
+
// ── Verify first, then reconcile (only mark complete on pass) ──
|
|
141
|
+
debugLog("autoLoop", { phase: "custom-engine-verify", iteration, unitId: iterData.unitId });
|
|
142
|
+
const verifyResult = await policy.verify(iterData.unitType, iterData.unitId, { basePath: s.basePath });
|
|
143
|
+
if (verifyResult === "pause") {
|
|
144
|
+
await deps.pauseAuto(ctx, pi);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
if (verifyResult === "retry") {
|
|
148
|
+
debugLog("autoLoop", { phase: "custom-engine-verify-retry", iteration, unitId: iterData.unitId });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// Verification passed — mark step complete
|
|
152
|
+
debugLog("autoLoop", { phase: "custom-engine-reconcile", iteration, unitId: iterData.unitId });
|
|
153
|
+
await engine.reconcile(engineState, {
|
|
154
|
+
unitType: iterData.unitType,
|
|
155
|
+
unitId: iterData.unitId,
|
|
156
|
+
startedAt: s.currentUnit?.startedAt ?? Date.now(),
|
|
157
|
+
finishedAt: Date.now(),
|
|
158
|
+
});
|
|
159
|
+
deps.clearUnitTimeout();
|
|
160
|
+
consecutiveErrors = 0;
|
|
161
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
|
|
162
|
+
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
85
165
|
if (!sidecarItem) {
|
|
86
166
|
// ── Phase 1: Pre-dispatch ─────────────────────────────────────────
|
|
87
167
|
const preDispatchResult = await runPreDispatch(ic, loopState);
|
|
@@ -776,8 +776,8 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
776
776
|
if (s.currentUnitRouting) {
|
|
777
777
|
deps.recordOutcome(unitType, s.currentUnitRouting.tier, true);
|
|
778
778
|
}
|
|
779
|
-
const
|
|
780
|
-
const artifactVerified =
|
|
779
|
+
const skipArtifactVerification = unitType.startsWith("hook/") || unitType === "custom-step";
|
|
780
|
+
const artifactVerified = skipArtifactVerification ||
|
|
781
781
|
deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
782
782
|
if (artifactVerified) {
|
|
783
783
|
s.completedUnits.push({
|
|
@@ -27,6 +27,8 @@ export class AutoSession {
|
|
|
27
27
|
paused = false;
|
|
28
28
|
stepMode = false;
|
|
29
29
|
verbose = false;
|
|
30
|
+
activeEngineId = null;
|
|
31
|
+
activeRunDir = null;
|
|
30
32
|
cmdCtx = null;
|
|
31
33
|
// ── Paths ────────────────────────────────────────────────────────────────
|
|
32
34
|
basePath = "";
|
|
@@ -113,6 +115,8 @@ export class AutoSession {
|
|
|
113
115
|
this.paused = false;
|
|
114
116
|
this.stepMode = false;
|
|
115
117
|
this.verbose = false;
|
|
118
|
+
this.activeEngineId = null;
|
|
119
|
+
this.activeRunDir = null;
|
|
116
120
|
this.cmdCtx = null;
|
|
117
121
|
// Paths
|
|
118
122
|
this.basePath = "";
|
|
@@ -156,6 +160,8 @@ export class AutoSession {
|
|
|
156
160
|
paused: this.paused,
|
|
157
161
|
stepMode: this.stepMode,
|
|
158
162
|
basePath: this.basePath,
|
|
163
|
+
activeEngineId: this.activeEngineId,
|
|
164
|
+
activeRunDir: this.activeRunDir,
|
|
159
165
|
currentMilestoneId: this.currentMilestoneId,
|
|
160
166
|
currentUnit: this.currentUnit,
|
|
161
167
|
completedUnits: this.completedUnits.length,
|
|
@@ -46,6 +46,7 @@ export function unitVerb(unitType) {
|
|
|
46
46
|
case "rewrite-docs": return "rewriting";
|
|
47
47
|
case "reassess-roadmap": return "reassessing";
|
|
48
48
|
case "run-uat": return "running UAT";
|
|
49
|
+
case "custom-step": return "executing workflow step";
|
|
49
50
|
default: return unitType;
|
|
50
51
|
}
|
|
51
52
|
}
|
|
@@ -64,6 +65,7 @@ export function unitPhaseLabel(unitType) {
|
|
|
64
65
|
case "rewrite-docs": return "REWRITE";
|
|
65
66
|
case "reassess-roadmap": return "REASSESS";
|
|
66
67
|
case "run-uat": return "UAT";
|
|
68
|
+
case "custom-step": return "WORKFLOW";
|
|
67
69
|
default: return unitType.toUpperCase();
|
|
68
70
|
}
|
|
69
71
|
}
|
|
@@ -161,6 +161,18 @@ export function isAutoActive() {
|
|
|
161
161
|
export function isAutoPaused() {
|
|
162
162
|
return s.paused;
|
|
163
163
|
}
|
|
164
|
+
export function setActiveEngineId(id) {
|
|
165
|
+
s.activeEngineId = id;
|
|
166
|
+
}
|
|
167
|
+
export function getActiveEngineId() {
|
|
168
|
+
return s.activeEngineId;
|
|
169
|
+
}
|
|
170
|
+
export function setActiveRunDir(runDir) {
|
|
171
|
+
s.activeRunDir = runDir;
|
|
172
|
+
}
|
|
173
|
+
export function getActiveRunDir() {
|
|
174
|
+
return s.activeRunDir;
|
|
175
|
+
}
|
|
164
176
|
/**
|
|
165
177
|
* Return the model captured at auto-mode start for this session.
|
|
166
178
|
* Used by error-recovery to fall back to the session's own model
|
|
@@ -531,6 +543,8 @@ export async function pauseAuto(ctx, _pi) {
|
|
|
531
543
|
stepMode: s.stepMode,
|
|
532
544
|
pausedAt: new Date().toISOString(),
|
|
533
545
|
sessionFile: s.pausedSessionFile,
|
|
546
|
+
activeEngineId: s.activeEngineId,
|
|
547
|
+
activeRunDir: s.activeRunDir,
|
|
534
548
|
};
|
|
535
549
|
const runtimeDir = join(gsdRoot(s.originalBasePath || s.basePath), "runtime");
|
|
536
550
|
mkdirSync(runtimeDir, { recursive: true });
|
|
@@ -725,7 +739,20 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
725
739
|
const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
|
|
726
740
|
if (existsSync(pausedPath)) {
|
|
727
741
|
const meta = JSON.parse(readFileSync(pausedPath, "utf-8"));
|
|
728
|
-
if (meta.
|
|
742
|
+
if (meta.activeEngineId && meta.activeEngineId !== "dev") {
|
|
743
|
+
// Custom workflow resume — restore engine state
|
|
744
|
+
s.activeEngineId = meta.activeEngineId;
|
|
745
|
+
s.activeRunDir = meta.activeRunDir ?? null;
|
|
746
|
+
s.originalBasePath = meta.originalBasePath || base;
|
|
747
|
+
s.stepMode = meta.stepMode ?? requestedStepMode;
|
|
748
|
+
s.paused = true;
|
|
749
|
+
try {
|
|
750
|
+
unlinkSync(pausedPath);
|
|
751
|
+
}
|
|
752
|
+
catch { /* non-fatal */ }
|
|
753
|
+
ctx.ui.notify(`Resuming paused custom workflow${meta.activeRunDir ? ` (${meta.activeRunDir})` : ""}.`, "info");
|
|
754
|
+
}
|
|
755
|
+
else if (meta.milestoneId) {
|
|
729
756
|
// Validate the milestone still exists and isn't already complete (#1664).
|
|
730
757
|
const mDir = resolveMilestonePath(base, meta.milestoneId);
|
|
731
758
|
const summaryFile = resolveMilestoneFile(base, meta.milestoneId, "SUMMARY");
|
|
@@ -20,8 +20,13 @@ let enabled = true;
|
|
|
20
20
|
function hashToolCall(toolName, args) {
|
|
21
21
|
const h = createHash("sha256");
|
|
22
22
|
h.update(toolName);
|
|
23
|
-
// Sort keys for deterministic hashing regardless of object key order
|
|
24
|
-
h.update(JSON.stringify(args,
|
|
23
|
+
// Sort keys recursively for deterministic hashing regardless of object key order
|
|
24
|
+
h.update(JSON.stringify(args, (_key, value) => value && typeof value === "object" && !Array.isArray(value)
|
|
25
|
+
? Object.keys(value).sort().reduce((o, k) => {
|
|
26
|
+
o[k] = value[k];
|
|
27
|
+
return o;
|
|
28
|
+
}, {})
|
|
29
|
+
: value));
|
|
25
30
|
return h.digest("hex").slice(0, 16);
|
|
26
31
|
}
|
|
27
32
|
/**
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { loadRegistry } from "../workflow-templates.js";
|
|
5
|
+
import { resolveProjectRoot } from "../worktree.js";
|
|
5
6
|
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
6
7
|
export const GSD_COMMAND_DESCRIPTION = "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast";
|
|
7
8
|
export const TOP_LEVEL_SUBCOMMANDS = [
|
|
@@ -53,6 +54,7 @@ export const TOP_LEVEL_SUBCOMMANDS = [
|
|
|
53
54
|
{ cmd: "templates", desc: "List available workflow templates" },
|
|
54
55
|
{ cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" },
|
|
55
56
|
{ cmd: "fast", desc: "Toggle OpenAI service tier (on/off/flex/status)" },
|
|
57
|
+
{ cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" },
|
|
56
58
|
];
|
|
57
59
|
const NESTED_COMPLETIONS = {
|
|
58
60
|
auto: [
|
|
@@ -193,6 +195,14 @@ const NESTED_COMPLETIONS = {
|
|
|
193
195
|
{ cmd: "ok", desc: "Model was appropriate for this task" },
|
|
194
196
|
{ cmd: "under", desc: "Model was underqualified for this task" },
|
|
195
197
|
],
|
|
198
|
+
workflow: [
|
|
199
|
+
{ cmd: "new", desc: "Create a new workflow definition (via skill)" },
|
|
200
|
+
{ cmd: "run", desc: "Create a run and start auto-mode" },
|
|
201
|
+
{ cmd: "list", desc: "List workflow runs" },
|
|
202
|
+
{ cmd: "validate", desc: "Validate a workflow definition YAML" },
|
|
203
|
+
{ cmd: "pause", desc: "Pause custom workflow auto-mode" },
|
|
204
|
+
{ cmd: "resume", desc: "Resume paused custom workflow auto-mode" },
|
|
205
|
+
],
|
|
196
206
|
};
|
|
197
207
|
function filterOptions(partial, options, prefix = "") {
|
|
198
208
|
const normalizedPrefix = prefix ? `${prefix} ` : "";
|
|
@@ -287,6 +297,28 @@ export function getGsdArgumentCompletions(prefix) {
|
|
|
287
297
|
if (command === "undo" && parts.length <= 2) {
|
|
288
298
|
return [{ value: "undo --force", label: "--force", description: "Skip confirmation prompt" }];
|
|
289
299
|
}
|
|
300
|
+
// Workflow definition-name completion for `workflow run <name>` and `workflow validate <name>`
|
|
301
|
+
if (command === "workflow" && (subcommand === "run" || subcommand === "validate") && parts.length <= 3) {
|
|
302
|
+
try {
|
|
303
|
+
const defsDir = join(resolveProjectRoot(process.cwd()), ".gsd", "workflow-defs");
|
|
304
|
+
if (existsSync(defsDir)) {
|
|
305
|
+
return readdirSync(defsDir)
|
|
306
|
+
.filter((f) => f.endsWith(".yaml") && f.startsWith(third))
|
|
307
|
+
.map((f) => {
|
|
308
|
+
const name = f.replace(/\.yaml$/, "");
|
|
309
|
+
return {
|
|
310
|
+
value: `workflow ${subcommand} ${name}`,
|
|
311
|
+
label: name,
|
|
312
|
+
description: `Workflow definition: ${name}`,
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// ignore filesystem errors during completion
|
|
319
|
+
}
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
290
322
|
const nested = NESTED_COMPLETIONS[command];
|
|
291
323
|
if (nested && parts.length <= 2) {
|
|
292
324
|
return filterOptions(subcommand, nested, command);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
3
4
|
import { handleQuick } from "../../quick.js";
|
|
4
5
|
import { showDiscuss, showHeadlessMilestoneCreation, showQueue } from "../../guided-flow.js";
|
|
5
6
|
import { handleStart, handleTemplates } from "../../commands-workflow-templates.js";
|
|
@@ -10,7 +11,152 @@ import { loadEffectiveGSDPreferences } from "../../preferences.js";
|
|
|
10
11
|
import { nextMilestoneId } from "../../milestone-ids.js";
|
|
11
12
|
import { findMilestoneIds } from "../../guided-flow.js";
|
|
12
13
|
import { projectRoot } from "../context.js";
|
|
14
|
+
import { createRun, listRuns } from "../../run-manager.js";
|
|
15
|
+
import { setActiveEngineId, setActiveRunDir, startAuto, pauseAuto, isAutoActive, getActiveEngineId, } from "../../auto.js";
|
|
16
|
+
import { validateDefinition } from "../../definition-loader.js";
|
|
17
|
+
// ─── Custom Workflow Subcommands ─────────────────────────────────────────
|
|
18
|
+
const WORKFLOW_USAGE = [
|
|
19
|
+
"Usage: /gsd workflow <subcommand>",
|
|
20
|
+
"",
|
|
21
|
+
" new — Create a new workflow definition (via skill)",
|
|
22
|
+
" run <name> [k=v] — Create a run and start auto-mode",
|
|
23
|
+
" list [name] — List workflow runs (optionally filtered by name)",
|
|
24
|
+
" validate <name> — Validate a workflow definition YAML",
|
|
25
|
+
" pause — Pause custom workflow auto-mode",
|
|
26
|
+
" resume — Resume paused custom workflow auto-mode",
|
|
27
|
+
].join("\n");
|
|
28
|
+
async function handleCustomWorkflow(sub, ctx, pi) {
|
|
29
|
+
// Bare `/gsd workflow` — show usage
|
|
30
|
+
if (!sub) {
|
|
31
|
+
ctx.ui.notify(WORKFLOW_USAGE, "info");
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
// ── new ──
|
|
35
|
+
if (sub === "new") {
|
|
36
|
+
ctx.ui.notify("Use the create-workflow skill: /skill create-workflow", "info");
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
// ── run <name> [param=value ...] ──
|
|
40
|
+
if (sub === "run" || sub.startsWith("run ")) {
|
|
41
|
+
const args = sub.slice("run".length).trim();
|
|
42
|
+
if (!args) {
|
|
43
|
+
ctx.ui.notify("Usage: /gsd workflow run <name> [param=value ...]", "warning");
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const parts = args.split(/\s+/);
|
|
47
|
+
const defName = parts[0];
|
|
48
|
+
const overrides = {};
|
|
49
|
+
for (let i = 1; i < parts.length; i++) {
|
|
50
|
+
const eqIdx = parts[i].indexOf("=");
|
|
51
|
+
if (eqIdx > 0) {
|
|
52
|
+
overrides[parts[i].slice(0, eqIdx)] = parts[i].slice(eqIdx + 1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const base = projectRoot();
|
|
57
|
+
const runDir = createRun(base, defName, Object.keys(overrides).length > 0 ? overrides : undefined);
|
|
58
|
+
setActiveEngineId("custom");
|
|
59
|
+
setActiveRunDir(runDir);
|
|
60
|
+
ctx.ui.notify(`Created workflow run: ${defName}\nRun dir: ${runDir}`, "info");
|
|
61
|
+
await startAuto(ctx, pi, base, false);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
// Clean up engine state so a failed workflow run doesn't pollute the next /gsd auto
|
|
65
|
+
setActiveEngineId(null);
|
|
66
|
+
setActiveRunDir(null);
|
|
67
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
68
|
+
ctx.ui.notify(`Failed to run workflow "${defName}": ${msg}`, "error");
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
// ── list [name] ──
|
|
73
|
+
if (sub === "list" || sub.startsWith("list ")) {
|
|
74
|
+
const filterName = sub.slice("list".length).trim() || undefined;
|
|
75
|
+
const base = projectRoot();
|
|
76
|
+
const runs = listRuns(base, filterName);
|
|
77
|
+
if (runs.length === 0) {
|
|
78
|
+
ctx.ui.notify("No workflow runs found.", "info");
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const lines = runs.map((r) => {
|
|
82
|
+
const stepInfo = `${r.steps.completed}/${r.steps.total} steps`;
|
|
83
|
+
return `• ${r.name} [${r.timestamp}] — ${r.status} (${stepInfo})`;
|
|
84
|
+
});
|
|
85
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
// ── validate <name> ──
|
|
89
|
+
if (sub === "validate" || sub.startsWith("validate ")) {
|
|
90
|
+
const defName = sub.slice("validate".length).trim();
|
|
91
|
+
if (!defName) {
|
|
92
|
+
ctx.ui.notify("Usage: /gsd workflow validate <name>", "warning");
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const base = projectRoot();
|
|
96
|
+
const defPath = join(base, ".gsd", "workflow-defs", `${defName}.yaml`);
|
|
97
|
+
if (!existsSync(defPath)) {
|
|
98
|
+
ctx.ui.notify(`Definition not found: ${defPath}`, "error");
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const raw = readFileSync(defPath, "utf-8");
|
|
103
|
+
const parsed = parseYaml(raw);
|
|
104
|
+
const result = validateDefinition(parsed);
|
|
105
|
+
if (result.valid) {
|
|
106
|
+
ctx.ui.notify(`✓ "${defName}" is a valid workflow definition.`, "info");
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
ctx.ui.notify(`✗ "${defName}" has errors:\n - ${result.errors.join("\n - ")}`, "error");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
114
|
+
ctx.ui.notify(`Failed to validate "${defName}": ${msg}`, "error");
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
// ── pause ──
|
|
119
|
+
if (sub === "pause") {
|
|
120
|
+
const engineId = getActiveEngineId();
|
|
121
|
+
if (engineId === "dev" || engineId === null) {
|
|
122
|
+
ctx.ui.notify("No custom workflow is running. Use /gsd pause for dev workflow.", "warning");
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (!isAutoActive()) {
|
|
126
|
+
ctx.ui.notify("Auto-mode is not active.", "warning");
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
await pauseAuto(ctx, pi);
|
|
130
|
+
ctx.ui.notify("Custom workflow paused.", "info");
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
// ── resume ──
|
|
134
|
+
if (sub === "resume") {
|
|
135
|
+
const engineId = getActiveEngineId();
|
|
136
|
+
if (engineId === "dev" || engineId === null) {
|
|
137
|
+
ctx.ui.notify("No custom workflow to resume. Use /gsd auto for dev workflow.", "warning");
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
await startAuto(ctx, pi, projectRoot(), false);
|
|
142
|
+
ctx.ui.notify("Custom workflow resumed.", "info");
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
146
|
+
ctx.ui.notify(`Failed to resume workflow: ${msg}`, "error");
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
// Unknown subcommand — show usage
|
|
151
|
+
ctx.ui.notify(`Unknown workflow subcommand: "${sub}"\n\n${WORKFLOW_USAGE}`, "warning");
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
13
154
|
export async function handleWorkflowCommand(trimmed, ctx, pi) {
|
|
155
|
+
// ── Custom workflow commands (`/gsd workflow ...`) ──
|
|
156
|
+
if (trimmed === "workflow" || trimmed.startsWith("workflow ")) {
|
|
157
|
+
const sub = trimmed.slice("workflow".length).trim();
|
|
158
|
+
return handleCustomWorkflow(sub, ctx, pi);
|
|
159
|
+
}
|
|
14
160
|
if (trimmed === "queue") {
|
|
15
161
|
await showQueue(ctx, pi, projectRoot());
|
|
16
162
|
return true;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-injector.ts — Inject prior step artifacts as context into step prompts.
|
|
3
|
+
*
|
|
4
|
+
* Reads the frozen DEFINITION.yaml from a run directory, finds the current step's
|
|
5
|
+
* `contextFrom` references, locates each referenced step's `produces` artifacts
|
|
6
|
+
* on disk, reads their content (truncated to 10k chars), and prepends formatted
|
|
7
|
+
* context blocks to the step prompt.
|
|
8
|
+
*
|
|
9
|
+
* Observability:
|
|
10
|
+
* - Truncation is logged via console.warn when it occurs, preventing silent overflow.
|
|
11
|
+
* - Missing artifact files are skipped silently (the step may not have produced them yet).
|
|
12
|
+
* - Unknown step IDs in contextFrom produce a console.warn for diagnosis.
|
|
13
|
+
* - The frozen DEFINITION.yaml on disk is the single source of truth for contextFrom config.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
16
|
+
import { resolve, sep } from "node:path";
|
|
17
|
+
import { readFrozenDefinition } from "./custom-workflow-engine.js";
|
|
18
|
+
/** Maximum characters per artifact to prevent context window blowout. */
|
|
19
|
+
const MAX_CONTEXT_CHARS = 10_000;
|
|
20
|
+
/**
|
|
21
|
+
* Inject context from prior step artifacts into a step's prompt.
|
|
22
|
+
*
|
|
23
|
+
* Reads the frozen DEFINITION.yaml from `runDir`, finds the step matching
|
|
24
|
+
* `stepId`, and for each step ID in its `contextFrom` array, looks up that
|
|
25
|
+
* step's `produces` paths, reads them from disk (relative to `runDir`),
|
|
26
|
+
* truncates to MAX_CONTEXT_CHARS, and prepends as labeled context blocks.
|
|
27
|
+
*
|
|
28
|
+
* @param runDir — absolute path to the workflow run directory
|
|
29
|
+
* @param stepId — the step ID whose prompt to enrich
|
|
30
|
+
* @param prompt — the original step prompt
|
|
31
|
+
* @returns The prompt with context blocks prepended, or unchanged if no context applies
|
|
32
|
+
* @throws Error if DEFINITION.yaml is missing or unreadable
|
|
33
|
+
*/
|
|
34
|
+
export function injectContext(runDir, stepId, prompt) {
|
|
35
|
+
const def = readFrozenDefinition(runDir);
|
|
36
|
+
const step = def.steps.find((s) => s.id === stepId);
|
|
37
|
+
if (!step || !step.contextFrom || step.contextFrom.length === 0) {
|
|
38
|
+
return prompt;
|
|
39
|
+
}
|
|
40
|
+
const contextBlocks = [];
|
|
41
|
+
for (const refStepId of step.contextFrom) {
|
|
42
|
+
const refStep = def.steps.find((s) => s.id === refStepId);
|
|
43
|
+
if (!refStep) {
|
|
44
|
+
console.warn(`context-injector: step "${stepId}" references unknown step "${refStepId}" in contextFrom — skipping`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!refStep.produces || refStep.produces.length === 0) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const relPath of refStep.produces) {
|
|
51
|
+
const absPath = resolve(runDir, relPath);
|
|
52
|
+
// Path traversal guard: ensure resolved path stays within runDir
|
|
53
|
+
if (!absPath.startsWith(resolve(runDir) + sep) && absPath !== resolve(runDir)) {
|
|
54
|
+
console.warn(`context-injector: artifact path "${relPath}" resolves outside runDir — skipping`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!existsSync(absPath)) {
|
|
58
|
+
// Artifact not yet produced or optional — skip silently
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
let content = readFileSync(absPath, "utf-8");
|
|
62
|
+
if (content.length > MAX_CONTEXT_CHARS) {
|
|
63
|
+
console.warn(`context-injector: truncating artifact "${relPath}" from step "${refStepId}" ` +
|
|
64
|
+
`(${content.length} chars → ${MAX_CONTEXT_CHARS} chars)`);
|
|
65
|
+
content = content.slice(0, MAX_CONTEXT_CHARS) + "\n...[truncated]";
|
|
66
|
+
}
|
|
67
|
+
contextBlocks.push(`--- Context from step "${refStepId}" (file: ${relPath}) ---\n${content}\n---`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (contextBlocks.length === 0) {
|
|
71
|
+
return prompt;
|
|
72
|
+
}
|
|
73
|
+
return contextBlocks.join("\n\n") + "\n\n" + prompt;
|
|
74
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* custom-execution-policy.ts — ExecutionPolicy for custom workflows.
|
|
3
|
+
*
|
|
4
|
+
* Delegates verification to the step-level verification module which reads
|
|
5
|
+
* the frozen DEFINITION.yaml and dispatches to the appropriate policy handler.
|
|
6
|
+
*
|
|
7
|
+
* Observability:
|
|
8
|
+
* - verify() returns the outcome from runCustomVerification() — four policies
|
|
9
|
+
* are supported: content-heuristic, shell-command, prompt-verify, human-review.
|
|
10
|
+
* - selectModel() returns null — defers to loop defaults.
|
|
11
|
+
* - recover() returns retry — simple default recovery strategy.
|
|
12
|
+
*/
|
|
13
|
+
import { runCustomVerification } from "./custom-verification.js";
|
|
14
|
+
export class CustomExecutionPolicy {
|
|
15
|
+
runDir;
|
|
16
|
+
constructor(runDir) {
|
|
17
|
+
this.runDir = runDir;
|
|
18
|
+
}
|
|
19
|
+
/** No workspace preparation needed for custom workflows. */
|
|
20
|
+
async prepareWorkspace(_basePath, _milestoneId) {
|
|
21
|
+
// No-op — custom workflows don't need worktree setup
|
|
22
|
+
}
|
|
23
|
+
/** Defer model selection to loop defaults. */
|
|
24
|
+
async selectModel(_unitType, _unitId, _context) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Verify step output by dispatching to the step's configured verification policy.
|
|
29
|
+
*
|
|
30
|
+
* Extracts the step ID from unitId (format: "<workflowName>/<stepId>")
|
|
31
|
+
* and calls runCustomVerification() which reads the frozen DEFINITION.yaml
|
|
32
|
+
* to determine which policy to apply.
|
|
33
|
+
*/
|
|
34
|
+
async verify(_unitType, unitId, _context) {
|
|
35
|
+
const parts = unitId.split("/");
|
|
36
|
+
const stepId = parts[parts.length - 1];
|
|
37
|
+
return runCustomVerification(this.runDir, stepId);
|
|
38
|
+
}
|
|
39
|
+
/** Default recovery: retry the step. */
|
|
40
|
+
async recover(_unitType, _unitId, _context) {
|
|
41
|
+
return { outcome: "retry", reason: "Default retry" };
|
|
42
|
+
}
|
|
43
|
+
/** No-op closeout — no commits or artifact capture. */
|
|
44
|
+
async closeout(_unitType, _unitId, _context) {
|
|
45
|
+
return { committed: false, artifacts: [] };
|
|
46
|
+
}
|
|
47
|
+
}
|