gsd-pi 2.41.0-dev.cac69f9 → 2.42.0-dev.1df898f
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 +23 -0
- package/dist/cli.js +18 -3
- package/dist/loader.js +3 -1
- package/dist/resource-loader.js +39 -6
- package/dist/resources/extensions/async-jobs/async-bash-tool.js +52 -4
- package/dist/resources/extensions/async-jobs/await-tool.js +5 -0
- package/dist/resources/extensions/async-jobs/index.js +2 -0
- package/dist/resources/extensions/gsd/auto/loop.js +80 -0
- package/dist/resources/extensions/gsd/auto/phases.js +3 -5
- 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-prompts.js +3 -16
- package/dist/resources/extensions/gsd/auto-start.js +8 -11
- package/dist/resources/extensions/gsd/auto.js +28 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +11 -5
- 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/detection.js +19 -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/doctor-checks.js +31 -1
- package/dist/resources/extensions/gsd/doctor-providers.js +10 -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/forensics.js +84 -0
- package/dist/resources/extensions/gsd/git-constants.js +1 -0
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/graph.js +225 -0
- package/dist/resources/extensions/gsd/native-git-bridge.js +1 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences.js +59 -8
- package/dist/resources/extensions/gsd/prompts/forensics.md +12 -5
- package/dist/resources/extensions/gsd/repo-identity.js +46 -5
- package/dist/resources/extensions/gsd/run-manager.js +134 -0
- package/dist/resources/extensions/gsd/service-tier.js +13 -4
- package/dist/resources/extensions/gsd/session-lock.js +2 -2
- package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +2 -2
- package/dist/resources/extensions/gsd/worktree.js +2 -2
- package/dist/resources/extensions/mcp-client/index.js +2 -1
- package/dist/resources/extensions/search-the-web/tool-search.js +3 -3
- 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 +15 -15
- 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/api/git/route.js +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 +15 -15
- package/dist/web/standalone/.next/server/chunks/229.js +2 -2
- 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/dist/web-mode.d.ts +2 -0
- package/dist/web-mode.js +40 -4
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +2 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +6 -0
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent.test.ts +53 -0
- package/packages/pi-agent-core/src/agent.ts +3 -0
- package/packages/pi-agent-core/src/types.ts +6 -0
- package/packages/pi-agent-core/tsconfig.json +1 -1
- package/packages/pi-ai/dist/models.d.ts +5 -3
- package/packages/pi-ai/dist/models.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +801 -1468
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +1135 -1588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/models.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js +60 -2
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
- package/packages/pi-ai/scripts/generate-models.ts +1543 -0
- package/packages/pi-ai/src/models.generated.ts +1140 -1593
- package/packages/pi-ai/src/models.ts +7 -4
- package/packages/pi-ai/src/utils/oauth/github-copilot.ts +74 -2
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +8 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +29 -2
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +60 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +18 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +23 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +63 -11
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +20 -6
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -5
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +9 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +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 +30 -10
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +7 -1
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +68 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -2
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +18 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +29 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +3 -0
- package/packages/pi-coding-agent/src/core/package-manager.ts +99 -58
- package/packages/pi-coding-agent/src/core/resource-loader.ts +24 -6
- package/packages/pi-coding-agent/src/core/system-prompt.ts +6 -5
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts +3 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +10 -6
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +31 -11
- package/pkg/package.json +1 -1
- package/src/resources/extensions/async-jobs/async-bash-timeout.test.ts +122 -0
- package/src/resources/extensions/async-jobs/async-bash-tool.ts +40 -4
- package/src/resources/extensions/async-jobs/await-tool.test.ts +47 -0
- package/src/resources/extensions/async-jobs/await-tool.ts +5 -0
- package/src/resources/extensions/async-jobs/index.ts +1 -0
- package/src/resources/extensions/async-jobs/job-manager.ts +2 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +0 -1
- package/src/resources/extensions/gsd/auto/loop.ts +91 -0
- package/src/resources/extensions/gsd/auto/phases.ts +3 -5
- 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-prompts.ts +2 -18
- package/src/resources/extensions/gsd/auto-start.ts +7 -10
- package/src/resources/extensions/gsd/auto.ts +31 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -5
- 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/detection.ts +19 -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/doctor-checks.ts +32 -1
- package/src/resources/extensions/gsd/doctor-providers.ts +13 -0
- package/src/resources/extensions/gsd/doctor-types.ts +1 -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/forensics.ts +92 -0
- package/src/resources/extensions/gsd/git-constants.ts +1 -0
- package/src/resources/extensions/gsd/git-service.ts +0 -1
- package/src/resources/extensions/gsd/gitignore.ts +1 -1
- package/src/resources/extensions/gsd/graph.ts +312 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +1 -0
- package/src/resources/extensions/gsd/preferences-types.ts +3 -0
- package/src/resources/extensions/gsd/preferences.ts +62 -6
- package/src/resources/extensions/gsd/prompts/forensics.md +12 -5
- package/src/resources/extensions/gsd/repo-identity.ts +48 -5
- package/src/resources/extensions/gsd/run-manager.ts +180 -0
- package/src/resources/extensions/gsd/service-tier.ts +17 -4
- package/src/resources/extensions/gsd/session-lock.ts +2 -2
- package/src/resources/extensions/gsd/tests/activity-log.test.ts +31 -69
- package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
- 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/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/forensics-dedup.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/git-locale.test.ts +133 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +44 -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/journal.test.ts +82 -127
- package/src/resources/extensions/gsd/tests/manifest-status.test.ts +73 -82
- package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
- package/src/resources/extensions/gsd/tests/service-tier.test.ts +30 -1
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +56 -3
- package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +156 -263
- package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +35 -78
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +81 -74
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +1 -2
- package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +2 -3
- package/src/resources/extensions/gsd/worktree.ts +2 -2
- package/src/resources/extensions/mcp-client/index.ts +5 -1
- package/src/resources/extensions/search-the-web/tool-search.ts +3 -3
- 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/{EnGUNqHeGbE0tuuUkTJVA → qw8qDHXOTLUXBq1vEknSz}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → qw8qDHXOTLUXBq1vEknSz}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-manager.ts — Create and list isolated workflow run directories.
|
|
3
|
+
*
|
|
4
|
+
* Each run lives under `.gsd/workflow-runs/<name>/<timestamp>/` and contains:
|
|
5
|
+
* - DEFINITION.yaml — frozen snapshot of the workflow definition at run-creation time
|
|
6
|
+
* - GRAPH.yaml — initialized step graph with all steps pending
|
|
7
|
+
* - PARAMS.json — (optional) parameter overrides used for this run
|
|
8
|
+
*
|
|
9
|
+
* Observability:
|
|
10
|
+
* - All run state is on disk in human-readable YAML/JSON — inspectable with cat/less.
|
|
11
|
+
* - `listRuns()` returns structured metadata including step counts and overall status.
|
|
12
|
+
* - Timestamp directory names are filesystem-safe (ISO with hyphens replacing colons).
|
|
13
|
+
* - Errors include the full path context for diagnosis.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdirSync, writeFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { stringify } from "yaml";
|
|
19
|
+
import { loadDefinition, substituteParams } from "./definition-loader.js";
|
|
20
|
+
import { initializeGraph, writeGraph, readGraph } from "./graph.js";
|
|
21
|
+
import type { WorkflowDefinition } from "./definition-loader.js";
|
|
22
|
+
import type { WorkflowGraph } from "./graph.js";
|
|
23
|
+
|
|
24
|
+
// ─── Types ───────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export interface RunMetadata {
|
|
27
|
+
/** Workflow definition name. */
|
|
28
|
+
name: string;
|
|
29
|
+
/** Filesystem-safe timestamp string used as dir name. */
|
|
30
|
+
timestamp: string;
|
|
31
|
+
/** Full path to the run directory. */
|
|
32
|
+
runDir: string;
|
|
33
|
+
/** Step counts derived from GRAPH.yaml. */
|
|
34
|
+
steps: { total: number; completed: number; pending: number; active: number };
|
|
35
|
+
/** Overall status derived from step states. */
|
|
36
|
+
status: "pending" | "running" | "complete";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Constants ───────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const RUNS_DIR = "workflow-runs";
|
|
42
|
+
const DEFS_DIR = "workflow-defs";
|
|
43
|
+
|
|
44
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate a filesystem-safe timestamp: `YYYY-MM-DDTHH-MM-SS`.
|
|
48
|
+
* Replaces colons with hyphens so the string is safe as a directory name
|
|
49
|
+
* on all platforms (Windows forbids colons in paths).
|
|
50
|
+
*/
|
|
51
|
+
function makeTimestamp(date: Date = new Date()): string {
|
|
52
|
+
return date.toISOString().replace(/:/g, "-").replace(/\.\d{3}Z$/, "");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Derive overall status from a graph's step statuses.
|
|
57
|
+
*/
|
|
58
|
+
function deriveStatus(graph: WorkflowGraph): "pending" | "running" | "complete" {
|
|
59
|
+
const hasActive = graph.steps.some((s) => s.status === "active");
|
|
60
|
+
const allDone = graph.steps.every(
|
|
61
|
+
(s) => s.status === "complete" || s.status === "expanded",
|
|
62
|
+
);
|
|
63
|
+
if (allDone) return "complete";
|
|
64
|
+
if (hasActive) return "running";
|
|
65
|
+
return "pending";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a new isolated run directory for a workflow definition.
|
|
72
|
+
*
|
|
73
|
+
* 1. Loads the definition from `<basePath>/.gsd/workflow-defs/<defName>.yaml`
|
|
74
|
+
* 2. Applies parameter substitution if overrides are provided
|
|
75
|
+
* 3. Creates `<basePath>/.gsd/workflow-runs/<defName>/<timestamp>/`
|
|
76
|
+
* 4. Writes frozen DEFINITION.yaml, initialized GRAPH.yaml, and optional PARAMS.json
|
|
77
|
+
*
|
|
78
|
+
* @param basePath — project root directory
|
|
79
|
+
* @param defName — definition filename (without .yaml extension)
|
|
80
|
+
* @param overrides — optional parameter overrides (merged with definition defaults)
|
|
81
|
+
* @returns Full path to the created run directory
|
|
82
|
+
* @throws Error if the definition file doesn't exist or is invalid
|
|
83
|
+
*/
|
|
84
|
+
export function createRun(
|
|
85
|
+
basePath: string,
|
|
86
|
+
defName: string,
|
|
87
|
+
overrides?: Record<string, string>,
|
|
88
|
+
): string {
|
|
89
|
+
const defsDir = join(basePath, ".gsd", DEFS_DIR);
|
|
90
|
+
|
|
91
|
+
// Load and validate the definition
|
|
92
|
+
const rawDef = loadDefinition(defsDir, defName);
|
|
93
|
+
|
|
94
|
+
// Apply parameter substitution if overrides provided
|
|
95
|
+
const def: WorkflowDefinition = overrides
|
|
96
|
+
? substituteParams(rawDef, overrides)
|
|
97
|
+
: substituteParams(rawDef); // still resolve default params if any
|
|
98
|
+
|
|
99
|
+
// Create the run directory
|
|
100
|
+
const timestamp = makeTimestamp();
|
|
101
|
+
const runDir = join(basePath, ".gsd", RUNS_DIR, defName, timestamp);
|
|
102
|
+
mkdirSync(runDir, { recursive: true });
|
|
103
|
+
|
|
104
|
+
// Freeze the definition as DEFINITION.yaml
|
|
105
|
+
writeFileSync(join(runDir, "DEFINITION.yaml"), stringify(def), "utf-8");
|
|
106
|
+
|
|
107
|
+
// Initialize and write GRAPH.yaml
|
|
108
|
+
const graph = initializeGraph(def);
|
|
109
|
+
writeGraph(runDir, graph);
|
|
110
|
+
|
|
111
|
+
// Write PARAMS.json if overrides were provided
|
|
112
|
+
if (overrides && Object.keys(overrides).length > 0) {
|
|
113
|
+
writeFileSync(
|
|
114
|
+
join(runDir, "PARAMS.json"),
|
|
115
|
+
JSON.stringify(overrides, null, 2),
|
|
116
|
+
"utf-8",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return runDir;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* List existing workflow runs with metadata.
|
|
125
|
+
*
|
|
126
|
+
* Scans `<basePath>/.gsd/workflow-runs/` for run directories. Each run's
|
|
127
|
+
* GRAPH.yaml is read to derive step counts and overall status.
|
|
128
|
+
*
|
|
129
|
+
* @param basePath — project root directory
|
|
130
|
+
* @param defName — optional filter: only list runs for this definition name
|
|
131
|
+
* @returns Array of run metadata, sorted newest-first within each definition
|
|
132
|
+
*/
|
|
133
|
+
export function listRuns(basePath: string, defName?: string): RunMetadata[] {
|
|
134
|
+
const runsRoot = join(basePath, ".gsd", RUNS_DIR);
|
|
135
|
+
if (!existsSync(runsRoot)) return [];
|
|
136
|
+
|
|
137
|
+
const results: RunMetadata[] = [];
|
|
138
|
+
|
|
139
|
+
// Get workflow name directories
|
|
140
|
+
const nameDirs = defName ? [defName] : readdirSync(runsRoot).filter((entry) => {
|
|
141
|
+
const full = join(runsRoot, entry);
|
|
142
|
+
return statSync(full).isDirectory();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
for (const name of nameDirs) {
|
|
146
|
+
const nameDir = join(runsRoot, name);
|
|
147
|
+
if (!existsSync(nameDir)) continue;
|
|
148
|
+
|
|
149
|
+
const timestamps = readdirSync(nameDir).filter((entry) => {
|
|
150
|
+
const full = join(nameDir, entry);
|
|
151
|
+
return statSync(full).isDirectory();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Sort newest-first (ISO strings sort lexicographically)
|
|
155
|
+
timestamps.sort().reverse();
|
|
156
|
+
|
|
157
|
+
for (const ts of timestamps) {
|
|
158
|
+
const runDir = join(nameDir, ts);
|
|
159
|
+
try {
|
|
160
|
+
const graph = readGraph(runDir);
|
|
161
|
+
const total = graph.steps.length;
|
|
162
|
+
const completed = graph.steps.filter((s) => s.status === "complete").length;
|
|
163
|
+
const pending = graph.steps.filter((s) => s.status === "pending").length;
|
|
164
|
+
const active = graph.steps.filter((s) => s.status === "active").length;
|
|
165
|
+
|
|
166
|
+
results.push({
|
|
167
|
+
name,
|
|
168
|
+
timestamp: ts,
|
|
169
|
+
runDir,
|
|
170
|
+
steps: { total, completed, pending, active },
|
|
171
|
+
status: deriveStatus(graph),
|
|
172
|
+
});
|
|
173
|
+
} catch {
|
|
174
|
+
// Skip runs with invalid/missing GRAPH.yaml
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return results;
|
|
180
|
+
}
|
|
@@ -23,6 +23,8 @@ import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./comm
|
|
|
23
23
|
|
|
24
24
|
export type ServiceTierSetting = "priority" | "flex" | undefined;
|
|
25
25
|
|
|
26
|
+
const SERVICE_TIER_SCOPE_NOTE = "Only affects gpt-5.4 models, regardless of provider.";
|
|
27
|
+
|
|
26
28
|
// ─── Gating ──────────────────────────────────────────────────────────────────
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -51,7 +53,7 @@ export function formatServiceTierStatus(tier: ServiceTierSetting): string {
|
|
|
51
53
|
" /gsd fast flex Set to flex (0.5x cost, slower)",
|
|
52
54
|
" /gsd fast off Disable service tier",
|
|
53
55
|
"",
|
|
54
|
-
|
|
56
|
+
SERVICE_TIER_SCOPE_NOTE,
|
|
55
57
|
].join("\n");
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -64,10 +66,18 @@ export function formatServiceTierStatus(tier: ServiceTierSetting): string {
|
|
|
64
66
|
" /gsd fast flex Set to flex (0.5x cost, slower)",
|
|
65
67
|
" /gsd fast off Disable service tier",
|
|
66
68
|
"",
|
|
67
|
-
|
|
69
|
+
SERVICE_TIER_SCOPE_NOTE,
|
|
68
70
|
].join("\n");
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
export function formatServiceTierFooterStatus(
|
|
74
|
+
tier: ServiceTierSetting,
|
|
75
|
+
modelId: string | undefined,
|
|
76
|
+
): string | undefined {
|
|
77
|
+
if (!tier || !modelId || !supportsServiceTier(modelId)) return undefined;
|
|
78
|
+
return tier === "priority" ? "fast: ⚡ priority" : "fast: 💰 flex";
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
// ─── Icon Resolution ─────────────────────────────────────────────────────────
|
|
72
82
|
|
|
73
83
|
/**
|
|
@@ -148,19 +158,22 @@ export async function handleFast(args: string, ctx: ExtensionCommandContext): Pr
|
|
|
148
158
|
|
|
149
159
|
if (trimmed === "on") {
|
|
150
160
|
await writeGlobalServiceTier(ctx, "priority");
|
|
151
|
-
ctx.ui.
|
|
161
|
+
ctx.ui.setStatus("gsd-fast", formatServiceTierFooterStatus("priority", ctx.model?.id));
|
|
162
|
+
ctx.ui.notify("Service tier set to priority (2x cost, faster responses). Only affects gpt-5.4 models, regardless of provider.", "info");
|
|
152
163
|
return;
|
|
153
164
|
}
|
|
154
165
|
|
|
155
166
|
if (trimmed === "off") {
|
|
156
167
|
await writeGlobalServiceTier(ctx, undefined);
|
|
168
|
+
ctx.ui.setStatus("gsd-fast", undefined);
|
|
157
169
|
ctx.ui.notify("Service tier disabled.", "info");
|
|
158
170
|
return;
|
|
159
171
|
}
|
|
160
172
|
|
|
161
173
|
if (trimmed === "flex") {
|
|
162
174
|
await writeGlobalServiceTier(ctx, "flex");
|
|
163
|
-
ctx.ui.
|
|
175
|
+
ctx.ui.setStatus("gsd-fast", formatServiceTierFooterStatus("flex", ctx.model?.id));
|
|
176
|
+
ctx.ui.notify("Service tier set to flex (0.5x cost, slower responses). Only affects gpt-5.4 models, regardless of provider.", "info");
|
|
164
177
|
return;
|
|
165
178
|
}
|
|
166
179
|
|
|
@@ -239,7 +239,7 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
|
|
|
239
239
|
const elapsed = Date.now() - _lockAcquiredAt;
|
|
240
240
|
if (elapsed < 1_800_000) {
|
|
241
241
|
process.stderr.write(
|
|
242
|
-
`[gsd] Lock heartbeat
|
|
242
|
+
`[gsd] Lock heartbeat caught up after ${Math.round(elapsed / 1000)}s — long LLM call, no action needed.\n`,
|
|
243
243
|
);
|
|
244
244
|
return; // Suppress false positive
|
|
245
245
|
}
|
|
@@ -299,7 +299,7 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
|
|
|
299
299
|
const elapsed = Date.now() - _lockAcquiredAt;
|
|
300
300
|
if (elapsed < 1_800_000) {
|
|
301
301
|
process.stderr.write(
|
|
302
|
-
`[gsd] Lock heartbeat
|
|
302
|
+
`[gsd] Lock heartbeat caught up after ${Math.round(elapsed / 1000)}s — long LLM call, no action needed.\n`,
|
|
303
303
|
);
|
|
304
304
|
return;
|
|
305
305
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* - activity-log-save.test.ts (caching, dedup, collision recovery)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import test from "node:test";
|
|
7
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
8
8
|
import assert from "node:assert/strict";
|
|
9
9
|
import { existsSync, mkdtempSync, mkdirSync, readdirSync, realpathSync, rmSync, utimesSync, writeFileSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join, dirname } from "node:path";
|
|
@@ -48,9 +48,12 @@ function createCtx(entries: unknown[]) {
|
|
|
48
48
|
|
|
49
49
|
// ── Pruning ──────────────────────────────────────────────────────────────────
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
describe("pruneActivityLogs", () => {
|
|
52
|
+
let dir: string;
|
|
53
|
+
beforeEach(() => { dir = createTmpDir(); });
|
|
54
|
+
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
|
55
|
+
|
|
56
|
+
test("deletes old files, keeps recent and highest-seq", () => {
|
|
54
57
|
const f001 = writeActivityFile(dir, "001", "execute-task-M001-S01-T01");
|
|
55
58
|
writeActivityFile(dir, "002", "execute-task-M001-S01-T02");
|
|
56
59
|
writeActivityFile(dir, "003", "execute-task-M001-S01-T03");
|
|
@@ -61,14 +64,9 @@ test("pruneActivityLogs deletes old files, keeps recent and highest-seq", () =>
|
|
|
61
64
|
assert.ok(!remaining.includes("001-execute-task-M001-S01-T01.jsonl"));
|
|
62
65
|
assert.ok(remaining.includes("002-execute-task-M001-S01-T02.jsonl"));
|
|
63
66
|
assert.ok(remaining.includes("003-execute-task-M001-S01-T03.jsonl"));
|
|
64
|
-
}
|
|
65
|
-
rmSync(dir, { recursive: true, force: true });
|
|
66
|
-
}
|
|
67
|
-
});
|
|
67
|
+
});
|
|
68
68
|
|
|
69
|
-
test("
|
|
70
|
-
const dir = createTmpDir();
|
|
71
|
-
try {
|
|
69
|
+
test("preserves highest-seq even when all files are old", () => {
|
|
72
70
|
const f001 = writeActivityFile(dir, "001", "t1");
|
|
73
71
|
const f002 = writeActivityFile(dir, "002", "t2");
|
|
74
72
|
const f003 = writeActivityFile(dir, "003", "t3");
|
|
@@ -78,14 +76,9 @@ test("pruneActivityLogs preserves highest-seq even when all files are old", () =
|
|
|
78
76
|
const remaining = listFiles(dir);
|
|
79
77
|
assert.equal(remaining.length, 1);
|
|
80
78
|
assert.ok(remaining[0].startsWith("003-"));
|
|
81
|
-
}
|
|
82
|
-
rmSync(dir, { recursive: true, force: true });
|
|
83
|
-
}
|
|
84
|
-
});
|
|
79
|
+
});
|
|
85
80
|
|
|
86
|
-
test("
|
|
87
|
-
const dir = createTmpDir();
|
|
88
|
-
try {
|
|
81
|
+
test("with retentionDays=0 keeps only highest-seq", () => {
|
|
89
82
|
writeActivityFile(dir, "001", "t1");
|
|
90
83
|
writeActivityFile(dir, "002", "t2");
|
|
91
84
|
writeActivityFile(dir, "003", "t3");
|
|
@@ -94,51 +87,31 @@ test("pruneActivityLogs with retentionDays=0 keeps only highest-seq", () => {
|
|
|
94
87
|
const remaining = listFiles(dir);
|
|
95
88
|
assert.equal(remaining.length, 1);
|
|
96
89
|
assert.ok(remaining[0].startsWith("003-"));
|
|
97
|
-
}
|
|
98
|
-
rmSync(dir, { recursive: true, force: true });
|
|
99
|
-
}
|
|
100
|
-
});
|
|
90
|
+
});
|
|
101
91
|
|
|
102
|
-
test("
|
|
103
|
-
const dir = createTmpDir();
|
|
104
|
-
try {
|
|
92
|
+
test("no-op when all files are recent", () => {
|
|
105
93
|
writeActivityFile(dir, "001", "t1");
|
|
106
94
|
writeActivityFile(dir, "002", "t2");
|
|
107
95
|
writeActivityFile(dir, "003", "t3");
|
|
108
96
|
|
|
109
97
|
pruneActivityLogs(dir, 30);
|
|
110
98
|
assert.equal(listFiles(dir).length, 3);
|
|
111
|
-
}
|
|
112
|
-
rmSync(dir, { recursive: true, force: true });
|
|
113
|
-
}
|
|
114
|
-
});
|
|
99
|
+
});
|
|
115
100
|
|
|
116
|
-
test("
|
|
117
|
-
const dir = createTmpDir();
|
|
118
|
-
try {
|
|
101
|
+
test("handles empty directory", () => {
|
|
119
102
|
assert.doesNotThrow(() => pruneActivityLogs(dir, 30));
|
|
120
103
|
assert.equal(readdirSync(dir).length, 0);
|
|
121
|
-
}
|
|
122
|
-
rmSync(dir, { recursive: true, force: true });
|
|
123
|
-
}
|
|
124
|
-
});
|
|
104
|
+
});
|
|
125
105
|
|
|
126
|
-
test("
|
|
127
|
-
const dir = createTmpDir();
|
|
128
|
-
try {
|
|
106
|
+
test("preserves single old file (it is highest-seq)", () => {
|
|
129
107
|
const f = writeActivityFile(dir, "001", "t1");
|
|
130
108
|
backdateFile(f, 100);
|
|
131
109
|
|
|
132
110
|
pruneActivityLogs(dir, 30);
|
|
133
111
|
assert.equal(listFiles(dir).length, 1);
|
|
134
|
-
}
|
|
135
|
-
rmSync(dir, { recursive: true, force: true });
|
|
136
|
-
}
|
|
137
|
-
});
|
|
112
|
+
});
|
|
138
113
|
|
|
139
|
-
test("
|
|
140
|
-
const dir = createTmpDir();
|
|
141
|
-
try {
|
|
114
|
+
test("ignores non-matching filenames", () => {
|
|
142
115
|
const f001 = writeActivityFile(dir, "001", "t1");
|
|
143
116
|
writeFileSync(join(dir, "notes.txt"), "some notes\n", "utf-8");
|
|
144
117
|
backdateFile(f001, 40);
|
|
@@ -148,16 +121,17 @@ test("pruneActivityLogs ignores non-matching filenames", () => {
|
|
|
148
121
|
assert.ok(remaining.includes("notes.txt"));
|
|
149
122
|
// 001 is the only seq file, so it's highest-seq and survives
|
|
150
123
|
assert.ok(remaining.includes("001-t1.jsonl"));
|
|
151
|
-
}
|
|
152
|
-
rmSync(dir, { recursive: true, force: true });
|
|
153
|
-
}
|
|
124
|
+
});
|
|
154
125
|
});
|
|
155
126
|
|
|
156
127
|
// ── Save: caching, dedup, collision recovery ─────────────────────────────────
|
|
157
128
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
129
|
+
describe("saveActivityLog", () => {
|
|
130
|
+
let baseDir: string;
|
|
131
|
+
beforeEach(() => { baseDir = createTmpDir(); });
|
|
132
|
+
afterEach(() => { rmSync(baseDir, { recursive: true, force: true }); });
|
|
133
|
+
|
|
134
|
+
test("caches sequence instead of rescanning", () => {
|
|
161
135
|
saveActivityLog(createCtx([{ kind: "first", n: 1 }]) as any, baseDir, "execute-task", "M001/S01/T01");
|
|
162
136
|
writeFileSync(join(activityDir(baseDir), "999-external.jsonl"), '{"x":1}\n', "utf-8");
|
|
163
137
|
saveActivityLog(createCtx([{ kind: "second", n: 2 }]) as any, baseDir, "execute-task", "M001/S01/T02");
|
|
@@ -166,14 +140,9 @@ test("saveActivityLog caches sequence instead of rescanning", () => {
|
|
|
166
140
|
assert.ok(files.includes("001-execute-task-M001-S01-T01.jsonl"));
|
|
167
141
|
assert.ok(files.includes("002-execute-task-M001-S01-T02.jsonl"));
|
|
168
142
|
assert.ok(!files.some(f => f.startsWith("1000-")));
|
|
169
|
-
}
|
|
170
|
-
rmSync(baseDir, { recursive: true, force: true });
|
|
171
|
-
}
|
|
172
|
-
});
|
|
143
|
+
});
|
|
173
144
|
|
|
174
|
-
test("
|
|
175
|
-
const baseDir = createTmpDir();
|
|
176
|
-
try {
|
|
145
|
+
test("deduplicates identical snapshots for same unit", () => {
|
|
177
146
|
const ctx = createCtx([{ role: "assistant", content: "same" }]);
|
|
178
147
|
saveActivityLog(ctx as any, baseDir, "plan-slice", "M002/S01");
|
|
179
148
|
saveActivityLog(ctx as any, baseDir, "plan-slice", "M002/S01");
|
|
@@ -184,14 +153,9 @@ test("saveActivityLog deduplicates identical snapshots for same unit", () => {
|
|
|
184
153
|
saveActivityLog(createCtx([{ role: "assistant", content: "changed" }]) as any, baseDir, "plan-slice", "M002/S01");
|
|
185
154
|
files = listFiles(activityDir(baseDir));
|
|
186
155
|
assert.equal(files.length, 2);
|
|
187
|
-
}
|
|
188
|
-
rmSync(baseDir, { recursive: true, force: true });
|
|
189
|
-
}
|
|
190
|
-
});
|
|
156
|
+
});
|
|
191
157
|
|
|
192
|
-
test("
|
|
193
|
-
const baseDir = createTmpDir();
|
|
194
|
-
try {
|
|
158
|
+
test("recovers on sequence collision", () => {
|
|
195
159
|
saveActivityLog(createCtx([{ turn: 1 }]) as any, baseDir, "execute-task", "M003/S02/T01");
|
|
196
160
|
writeFileSync(join(activityDir(baseDir), "002-execute-task-M003-S02-T02.jsonl"), '{"collision":true}\n', "utf-8");
|
|
197
161
|
saveActivityLog(createCtx([{ turn: 2 }]) as any, baseDir, "execute-task", "M003/S02/T02");
|
|
@@ -199,9 +163,7 @@ test("saveActivityLog recovers on sequence collision", () => {
|
|
|
199
163
|
const files = listFiles(activityDir(baseDir));
|
|
200
164
|
assert.ok(files.includes("002-execute-task-M003-S02-T02.jsonl"));
|
|
201
165
|
assert.ok(files.includes("003-execute-task-M003-S02-T02.jsonl"));
|
|
202
|
-
}
|
|
203
|
-
rmSync(baseDir, { recursive: true, force: true });
|
|
204
|
-
}
|
|
166
|
+
});
|
|
205
167
|
});
|
|
206
168
|
|
|
207
169
|
// ── Prompt text assertion ────────────────────────────────────────────────────
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled workflow definition validation tests.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that every example YAML in src/resources/skills/create-workflow/templates/
|
|
5
|
+
* passes validateDefinition() from definition-loader.ts with { valid: true, errors: [] }.
|
|
6
|
+
*
|
|
7
|
+
* Also validates scaffold template and structural properties of each example
|
|
8
|
+
* (step counts, feature usage) to guard against accidental regressions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { join, dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { parse } from "yaml";
|
|
17
|
+
|
|
18
|
+
import { validateDefinition } from "../definition-loader.ts";
|
|
19
|
+
|
|
20
|
+
// ─── Path resolution ─────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
// Navigate from tests/ → extensions/gsd/ → extensions/ → resources/ → skills/create-workflow/templates/
|
|
24
|
+
const templatesDir = join(
|
|
25
|
+
__dirname,
|
|
26
|
+
"..",
|
|
27
|
+
"..",
|
|
28
|
+
"..",
|
|
29
|
+
"skills",
|
|
30
|
+
"create-workflow",
|
|
31
|
+
"templates",
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
function loadYaml(filename: string): unknown {
|
|
35
|
+
const raw = readFileSync(join(templatesDir, filename), "utf-8");
|
|
36
|
+
return parse(raw);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Scaffold template ──────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
test("scaffold template (workflow-definition.yaml) passes validation", () => {
|
|
42
|
+
const parsed = loadYaml("workflow-definition.yaml");
|
|
43
|
+
const result = validateDefinition(parsed);
|
|
44
|
+
assert.equal(result.valid, true, `Scaffold invalid: ${result.errors.join("; ")}`);
|
|
45
|
+
assert.equal(result.errors.length, 0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ─── blog-post-pipeline.yaml ────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
test("blog-post-pipeline.yaml passes validation", () => {
|
|
51
|
+
const parsed = loadYaml("blog-post-pipeline.yaml");
|
|
52
|
+
const result = validateDefinition(parsed);
|
|
53
|
+
assert.equal(result.valid, true, `Invalid: ${result.errors.join("; ")}`);
|
|
54
|
+
assert.equal(result.errors.length, 0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("blog-post-pipeline.yaml: 3 steps, context_from, params, content-heuristic", () => {
|
|
58
|
+
const parsed = loadYaml("blog-post-pipeline.yaml") as Record<string, unknown>;
|
|
59
|
+
const steps = parsed.steps as Array<Record<string, unknown>>;
|
|
60
|
+
|
|
61
|
+
// 3 steps
|
|
62
|
+
assert.equal(steps.length, 3, "Expected 3 steps");
|
|
63
|
+
|
|
64
|
+
// params defined
|
|
65
|
+
assert.ok(parsed.params, "Expected params to be defined");
|
|
66
|
+
const params = parsed.params as Record<string, string>;
|
|
67
|
+
assert.ok("topic" in params, "Expected 'topic' param");
|
|
68
|
+
assert.ok("audience" in params, "Expected 'audience' param");
|
|
69
|
+
|
|
70
|
+
// At least one step uses context_from
|
|
71
|
+
const hasContextFrom = steps.some(
|
|
72
|
+
(s) => Array.isArray(s.context_from) && s.context_from.length > 0,
|
|
73
|
+
);
|
|
74
|
+
assert.ok(hasContextFrom, "Expected at least one step with context_from");
|
|
75
|
+
|
|
76
|
+
// All steps use content-heuristic verify
|
|
77
|
+
for (const step of steps) {
|
|
78
|
+
const verify = step.verify as Record<string, unknown> | undefined;
|
|
79
|
+
assert.ok(verify, `Step "${step.id}" missing verify`);
|
|
80
|
+
assert.equal(verify.policy, "content-heuristic", `Step "${step.id}" should use content-heuristic`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ─── code-audit.yaml ────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
test("code-audit.yaml passes validation", () => {
|
|
87
|
+
const parsed = loadYaml("code-audit.yaml");
|
|
88
|
+
const result = validateDefinition(parsed);
|
|
89
|
+
assert.equal(result.valid, true, `Invalid: ${result.errors.join("; ")}`);
|
|
90
|
+
assert.equal(result.errors.length, 0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("code-audit.yaml: iterate with capture group and shell-command verify", () => {
|
|
94
|
+
const parsed = loadYaml("code-audit.yaml") as Record<string, unknown>;
|
|
95
|
+
const steps = parsed.steps as Array<Record<string, unknown>>;
|
|
96
|
+
|
|
97
|
+
// Find step with iterate
|
|
98
|
+
const iterateStep = steps.find((s) => s.iterate != null);
|
|
99
|
+
assert.ok(iterateStep, "Expected a step with iterate config");
|
|
100
|
+
|
|
101
|
+
const iterate = iterateStep.iterate as Record<string, unknown>;
|
|
102
|
+
assert.equal(typeof iterate.source, "string", "iterate.source must be a string");
|
|
103
|
+
assert.equal(typeof iterate.pattern, "string", "iterate.pattern must be a string");
|
|
104
|
+
|
|
105
|
+
// Pattern has a capture group
|
|
106
|
+
const pattern = iterate.pattern as string;
|
|
107
|
+
assert.ok(/\((?!\?)/.test(pattern), "iterate.pattern must contain a capture group");
|
|
108
|
+
|
|
109
|
+
// Pattern is valid regex
|
|
110
|
+
assert.doesNotThrow(() => new RegExp(pattern), "iterate.pattern must be valid regex");
|
|
111
|
+
|
|
112
|
+
// Has shell-command verify
|
|
113
|
+
const verify = iterateStep.verify as Record<string, unknown>;
|
|
114
|
+
assert.equal(verify.policy, "shell-command");
|
|
115
|
+
assert.equal(typeof verify.command, "string");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ─── release-checklist.yaml ─────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
test("release-checklist.yaml passes validation", () => {
|
|
121
|
+
const parsed = loadYaml("release-checklist.yaml");
|
|
122
|
+
const result = validateDefinition(parsed);
|
|
123
|
+
assert.equal(result.valid, true, `Invalid: ${result.errors.join("; ")}`);
|
|
124
|
+
assert.equal(result.errors.length, 0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("release-checklist.yaml: diamond dependencies and human-review", () => {
|
|
128
|
+
const parsed = loadYaml("release-checklist.yaml") as Record<string, unknown>;
|
|
129
|
+
const steps = parsed.steps as Array<Record<string, unknown>>;
|
|
130
|
+
|
|
131
|
+
// 4 steps
|
|
132
|
+
assert.equal(steps.length, 4, "Expected 4 steps");
|
|
133
|
+
|
|
134
|
+
// Diamond pattern: two steps depend on the same parent
|
|
135
|
+
const changelog = steps.find((s) => s.id === "changelog");
|
|
136
|
+
const versionBump = steps.find((s) => s.id === "version-bump");
|
|
137
|
+
const testSuite = steps.find((s) => s.id === "test-suite");
|
|
138
|
+
const publish = steps.find((s) => s.id === "publish");
|
|
139
|
+
|
|
140
|
+
assert.ok(changelog, "Expected 'changelog' step");
|
|
141
|
+
assert.ok(versionBump, "Expected 'version-bump' step");
|
|
142
|
+
assert.ok(testSuite, "Expected 'test-suite' step");
|
|
143
|
+
assert.ok(publish, "Expected 'publish' step");
|
|
144
|
+
|
|
145
|
+
// Both version-bump and test-suite depend on changelog
|
|
146
|
+
const vbReqs = versionBump.requires as string[];
|
|
147
|
+
const tsReqs = testSuite.requires as string[];
|
|
148
|
+
assert.ok(vbReqs.includes("changelog"), "version-bump should require changelog");
|
|
149
|
+
assert.ok(tsReqs.includes("changelog"), "test-suite should require changelog");
|
|
150
|
+
|
|
151
|
+
// publish depends on both (diamond join)
|
|
152
|
+
const pubReqs = publish.requires as string[];
|
|
153
|
+
assert.ok(pubReqs.includes("version-bump"), "publish should require version-bump");
|
|
154
|
+
assert.ok(pubReqs.includes("test-suite"), "publish should require test-suite");
|
|
155
|
+
|
|
156
|
+
// publish uses human-review
|
|
157
|
+
const verify = publish.verify as Record<string, unknown>;
|
|
158
|
+
assert.equal(verify.policy, "human-review");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ─── Cross-cutting: no path traversal in produces ───────────────────────
|
|
162
|
+
|
|
163
|
+
test("no produces path contains '..'", () => {
|
|
164
|
+
const files = [
|
|
165
|
+
"blog-post-pipeline.yaml",
|
|
166
|
+
"code-audit.yaml",
|
|
167
|
+
"release-checklist.yaml",
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
for (const file of files) {
|
|
171
|
+
const parsed = loadYaml(file) as Record<string, unknown>;
|
|
172
|
+
const steps = parsed.steps as Array<Record<string, unknown>>;
|
|
173
|
+
for (const step of steps) {
|
|
174
|
+
const produces = (step.produces as string[]) ?? [];
|
|
175
|
+
for (const p of produces) {
|
|
176
|
+
assert.ok(!p.includes(".."), `${file} step "${step.id}" produces path contains '..': ${p}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|