@united-workforce/cli 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -11
- package/dist/.build-fingerprint +1 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js +17 -7
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts +2 -0
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts.map +1 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js +30 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -0
- package/dist/__tests__/build-step-entry.test.d.ts +2 -0
- package/dist/__tests__/build-step-entry.test.d.ts.map +1 -0
- package/dist/__tests__/build-step-entry.test.js +173 -0
- package/dist/__tests__/build-step-entry.test.js.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts +2 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js +93 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js.map +1 -0
- package/dist/__tests__/concurrency.test.d.ts +2 -0
- package/dist/__tests__/concurrency.test.d.ts.map +1 -0
- package/dist/__tests__/concurrency.test.js +196 -0
- package/dist/__tests__/concurrency.test.js.map +1 -0
- package/dist/__tests__/config.test.js +26 -302
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/current-role.test.js +7 -6
- package/dist/__tests__/current-role.test.js.map +1 -1
- package/dist/__tests__/e2e-mock-agent.test.js +43 -30
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/format-text-default.test.d.ts +2 -0
- package/dist/__tests__/format-text-default.test.d.ts.map +1 -0
- package/dist/__tests__/format-text-default.test.js +43 -0
- package/dist/__tests__/format-text-default.test.js.map +1 -0
- package/dist/__tests__/format-text-registry.test.d.ts +2 -0
- package/dist/__tests__/format-text-registry.test.d.ts.map +1 -0
- package/dist/__tests__/format-text-registry.test.js +158 -0
- package/dist/__tests__/format-text-registry.test.js.map +1 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts +2 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts.map +1 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js +40 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js.map +1 -0
- package/dist/__tests__/log-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/log-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/log-text-renderer.test.js +265 -0
- package/dist/__tests__/log-text-renderer.test.js.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.js +9 -50
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
- package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts +2 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts.map +1 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.js +102 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.js.map +1 -0
- package/dist/__tests__/output-mapper-workflow-add.test.d.ts +2 -0
- package/dist/__tests__/output-mapper-workflow-add.test.d.ts.map +1 -0
- package/dist/__tests__/output-mapper-workflow-add.test.js +22 -0
- package/dist/__tests__/output-mapper-workflow-add.test.js.map +1 -0
- package/dist/__tests__/pid-recycling.test.d.ts +2 -0
- package/dist/__tests__/pid-recycling.test.d.ts.map +1 -0
- package/dist/__tests__/pid-recycling.test.js +273 -0
- package/dist/__tests__/pid-recycling.test.js.map +1 -0
- package/dist/__tests__/prompt.test.js +365 -2
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +12 -4
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
- package/dist/__tests__/setup-agent-discovery.test.js +21 -30
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
- package/dist/__tests__/setup-complexity.test.js +2 -168
- package/dist/__tests__/setup-complexity.test.js.map +1 -1
- package/dist/__tests__/setup-no-llm.test.d.ts +2 -0
- package/dist/__tests__/setup-no-llm.test.d.ts.map +1 -0
- package/dist/__tests__/setup-no-llm.test.js +52 -0
- package/dist/__tests__/setup-no-llm.test.js.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js +27 -28
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.d.ts +2 -0
- package/dist/__tests__/step-ask.test.d.ts.map +1 -0
- package/dist/__tests__/step-ask.test.js +507 -0
- package/dist/__tests__/step-ask.test.js.map +1 -0
- package/dist/__tests__/step-show-json.test.js +1 -0
- package/dist/__tests__/step-show-json.test.js.map +1 -1
- package/dist/__tests__/step-timing.test.js +2 -0
- package/dist/__tests__/step-timing.test.js.map +1 -1
- package/dist/__tests__/store-global-cas.test.js +2 -2
- package/dist/__tests__/store-global-cas.test.js.map +1 -1
- package/dist/__tests__/store-unified-threads.test.js +28 -26
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-status.test.js +25 -19
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.js +110 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.js.map +1 -0
- package/dist/__tests__/thread-list-filters.test.js +354 -17
- package/dist/__tests__/thread-list-filters.test.js.map +1 -1
- package/dist/__tests__/thread-list-template-ms-date.test.d.ts +2 -0
- package/dist/__tests__/thread-list-template-ms-date.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js +102 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts +2 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.js +157 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.js.map +1 -0
- package/dist/__tests__/thread-poke.test.d.ts +2 -0
- package/dist/__tests__/thread-poke.test.d.ts.map +1 -0
- package/dist/__tests__/thread-poke.test.js +422 -0
- package/dist/__tests__/thread-poke.test.js.map +1 -0
- package/dist/__tests__/thread-read-xml-tags.test.js +10 -9
- package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -1
- package/dist/__tests__/thread-resume.test.js +21 -15
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-show-status.test.js +17 -28
- package/dist/__tests__/thread-show-status.test.js.map +1 -1
- package/dist/__tests__/thread-start-cwd-cli.test.js +15 -3
- package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -1
- package/dist/__tests__/thread-stop-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/thread-stop-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/thread-stop-text-renderer.test.js +148 -0
- package/dist/__tests__/thread-stop-text-renderer.test.js.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.js +13 -16
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread-suspended-display.test.js +10 -22
- package/dist/__tests__/thread-suspended-display.test.js.map +1 -1
- package/dist/__tests__/thread-test-helpers.d.ts +7 -0
- package/dist/__tests__/thread-test-helpers.d.ts.map +1 -1
- package/dist/__tests__/thread-test-helpers.js +13 -0
- package/dist/__tests__/thread-test-helpers.js.map +1 -1
- package/dist/__tests__/thread.test.js +15 -13
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +105 -23
- package/dist/__tests__/validate-semantic.test.js.map +1 -1
- package/dist/__tests__/workflow-list-recursive.test.d.ts +2 -0
- package/dist/__tests__/workflow-list-recursive.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-list-recursive.test.js +286 -0
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +46 -28
- package/dist/__tests__/workflow-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-show-resolution.test.d.ts +2 -0
- package/dist/__tests__/workflow-show-resolution.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-show-resolution.test.js +213 -0
- package/dist/__tests__/workflow-show-resolution.test.js.map +1 -0
- package/dist/__tests__/workflow-validate.test.d.ts +2 -0
- package/dist/__tests__/workflow-validate.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-validate.test.js +707 -0
- package/dist/__tests__/workflow-validate.test.js.map +1 -0
- package/dist/__tests__/write-envelope.test.d.ts +2 -0
- package/dist/__tests__/write-envelope.test.d.ts.map +1 -0
- package/dist/__tests__/write-envelope.test.js +201 -0
- package/dist/__tests__/write-envelope.test.js.map +1 -0
- package/dist/background/background.d.ts +22 -1
- package/dist/background/background.d.ts.map +1 -1
- package/dist/background/background.js +83 -6
- package/dist/background/background.js.map +1 -1
- package/dist/background/index.d.ts +1 -1
- package/dist/background/index.d.ts.map +1 -1
- package/dist/background/index.js +1 -1
- package/dist/background/index.js.map +1 -1
- package/dist/background/types.d.ts +1 -0
- package/dist/background/types.d.ts.map +1 -1
- package/dist/cli.js +120 -62
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts +3 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +17 -31
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +57 -31
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +12 -39
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +72 -303
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +44 -1
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +255 -11
- package/dist/commands/step.js.map +1 -1
- package/dist/commands/thread.d.ts +16 -3
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +423 -142
- package/dist/commands/thread.js.map +1 -1
- package/dist/commands/workflow.d.ts +9 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +126 -6
- package/dist/commands/workflow.js.map +1 -1
- package/dist/concurrency/concurrency.d.ts +34 -0
- package/dist/concurrency/concurrency.d.ts.map +1 -0
- package/dist/concurrency/concurrency.js +216 -0
- package/dist/concurrency/concurrency.js.map +1 -0
- package/dist/concurrency/index.d.ts +3 -0
- package/dist/concurrency/index.d.ts.map +1 -0
- package/dist/concurrency/index.js +2 -0
- package/dist/concurrency/index.js.map +1 -0
- package/dist/concurrency/types.d.ts +19 -0
- package/dist/concurrency/types.d.ts.map +1 -0
- package/dist/concurrency/types.js +2 -0
- package/dist/concurrency/types.js.map +1 -0
- package/dist/format.d.ts +69 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +198 -1
- package/dist/format.js.map +1 -1
- package/dist/moderator/__tests__/evaluate.test.js +31 -17
- package/dist/moderator/__tests__/evaluate.test.js.map +1 -1
- package/dist/moderator/evaluate.d.ts.map +1 -1
- package/dist/moderator/evaluate.js +4 -16
- package/dist/moderator/evaluate.js.map +1 -1
- package/dist/moderator/index.d.ts +1 -2
- package/dist/moderator/index.d.ts.map +1 -1
- package/dist/moderator/index.js +0 -1
- package/dist/moderator/index.js.map +1 -1
- package/dist/moderator/types.d.ts +6 -10
- package/dist/moderator/types.d.ts.map +1 -1
- package/dist/moderator/types.js +1 -3
- package/dist/moderator/types.js.map +1 -1
- package/dist/output-mappers.d.ts +122 -0
- package/dist/output-mappers.d.ts.map +1 -0
- package/dist/output-mappers.js +134 -0
- package/dist/output-mappers.js.map +1 -0
- package/dist/schemas.d.ts +6 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +34 -5
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +28 -9
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +75 -16
- package/dist/store.js.map +1 -1
- package/dist/text-renderers.d.ts +30 -0
- package/dist/text-renderers.d.ts.map +1 -0
- package/dist/text-renderers.js +251 -0
- package/dist/text-renderers.js.map +1 -0
- package/dist/validate-semantic.d.ts.map +1 -1
- package/dist/validate-semantic.js +95 -61
- package/dist/validate-semantic.js.map +1 -1
- package/dist/validate.d.ts +6 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +24 -0
- package/dist/validate.js.map +1 -1
- package/examples/brainstorm.yaml +130 -0
- package/examples/debate.yaml +169 -0
- package/examples/socratic-questioning.yaml +112 -0
- package/package.json +9 -10
- package/src/__tests__/adapter-json-roundtrip.test.ts +16 -7
- package/src/__tests__/agent-resolution-llm-free.test.ts +39 -0
- package/src/__tests__/build-step-entry.test.ts +203 -0
- package/src/__tests__/clear-thread-failed-attempts.test.ts +122 -0
- package/src/__tests__/concurrency.test.ts +266 -0
- package/src/__tests__/config.test.ts +33 -321
- package/src/__tests__/current-role.test.ts +7 -6
- package/src/__tests__/e2e-mock-agent.test.ts +65 -30
- package/src/__tests__/fixtures/e2e-count.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-linear.workflow.yaml +1 -0
- package/src/__tests__/fixtures/{e2e-mustache.workflow.yaml → e2e-liquid.workflow.yaml} +3 -2
- package/src/__tests__/fixtures/e2e-loop.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-suspend.mock.yaml +2 -2
- package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +6 -10
- package/src/__tests__/format-text-default.test.ts +49 -0
- package/src/__tests__/format-text-registry.test.ts +173 -0
- package/src/__tests__/issue-180-workflow-ref-removed.test.ts +43 -0
- package/src/__tests__/log-text-renderer.test.ts +294 -0
- package/src/__tests__/moderator-evaluate.test.ts +9 -52
- package/src/__tests__/output-mapper-thread-list-startedat.test.ts +124 -0
- package/src/__tests__/output-mapper-workflow-add.test.ts +24 -0
- package/src/__tests__/pid-recycling.test.ts +329 -0
- package/src/__tests__/prompt.test.ts +443 -2
- package/src/__tests__/resolve-head-hash.test.ts +11 -4
- package/src/__tests__/setup-agent-discovery.test.ts +26 -51
- package/src/__tests__/setup-complexity.test.ts +1 -203
- package/src/__tests__/setup-no-llm.test.ts +68 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +27 -31
- package/src/__tests__/step-ask.test.ts +677 -0
- package/src/__tests__/step-show-json.test.ts +1 -0
- package/src/__tests__/step-timing.test.ts +2 -0
- package/src/__tests__/store-global-cas.test.ts +2 -2
- package/src/__tests__/store-unified-threads.test.ts +30 -27
- package/src/__tests__/thread-cancel-status.test.ts +27 -20
- package/src/__tests__/thread-cancel-text-renderer.test.ts +125 -0
- package/src/__tests__/thread-list-filters.test.ts +443 -17
- package/src/__tests__/thread-list-template-ms-date.test.ts +110 -0
- package/src/__tests__/thread-list-workflow-corrupt.test.ts +198 -0
- package/src/__tests__/thread-poke.test.ts +554 -0
- package/src/__tests__/thread-read-xml-tags.test.ts +9 -11
- package/src/__tests__/thread-resume.test.ts +20 -15
- package/src/__tests__/thread-show-status.test.ts +17 -29
- package/src/__tests__/thread-start-cwd-cli.test.ts +15 -3
- package/src/__tests__/thread-stop-text-renderer.test.ts +168 -0
- package/src/__tests__/thread-suspend-step.test.ts +13 -16
- package/src/__tests__/thread-suspended-display.test.ts +10 -22
- package/src/__tests__/thread-test-helpers.ts +15 -1
- package/src/__tests__/thread.test.ts +14 -14
- package/src/__tests__/validate-semantic.test.ts +118 -33
- package/src/__tests__/workflow-list-recursive.test.ts +370 -0
- package/src/__tests__/workflow-resolution.test.ts +48 -29
- package/src/__tests__/workflow-show-resolution.test.ts +286 -0
- package/src/__tests__/workflow-validate.test.ts +828 -0
- package/src/__tests__/write-envelope.test.ts +257 -0
- package/src/background/background.ts +88 -6
- package/src/background/index.ts +2 -0
- package/src/background/types.ts +1 -0
- package/src/cli.ts +184 -77
- package/src/commands/config.ts +16 -33
- package/src/commands/prompt.ts +57 -31
- package/src/commands/setup.ts +80 -358
- package/src/commands/step.ts +339 -12
- package/src/commands/thread.ts +511 -171
- package/src/commands/workflow.ts +155 -4
- package/src/concurrency/concurrency.ts +245 -0
- package/src/concurrency/index.ts +10 -0
- package/src/concurrency/types.ts +19 -0
- package/src/format.ts +282 -2
- package/src/moderator/__tests__/evaluate.test.ts +34 -17
- package/src/moderator/evaluate.ts +5 -17
- package/src/moderator/index.ts +1 -6
- package/src/moderator/types.ts +6 -14
- package/src/output-mappers.ts +254 -0
- package/src/schemas.ts +51 -5
- package/src/store.ts +86 -20
- package/src/text-renderers.ts +355 -0
- package/src/validate-semantic.ts +125 -73
- package/src/validate.ts +27 -0
- package/dist/__tests__/setup-validate.test.d.ts +0 -2
- package/dist/__tests__/setup-validate.test.d.ts.map +0 -1
- package/dist/__tests__/setup-validate.test.js +0 -108
- package/dist/__tests__/setup-validate.test.js.map +0 -1
- package/src/__tests__/setup-validate.test.ts +0 -148
- /package/src/__tests__/fixtures/{e2e-mustache.mock.yaml → e2e-liquid.mock.yaml} +0 -0
package/src/commands/workflow.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, resolve as resolvePath } from "node:path";
|
|
2
|
+
import { basename, dirname, isAbsolute, resolve as resolvePath } from "node:path";
|
|
3
3
|
|
|
4
4
|
import type { JSONSchema } from "@ocas/core";
|
|
5
5
|
import { putSchema, validate } from "@ocas/core";
|
|
@@ -12,11 +12,17 @@ import {
|
|
|
12
12
|
discoverProjectWorkflows,
|
|
13
13
|
findRegistryName,
|
|
14
14
|
loadWorkflowRegistry,
|
|
15
|
+
resolveProjectWorkflowFile,
|
|
15
16
|
resolveWorkflowHash,
|
|
16
17
|
saveWorkflowRegistry,
|
|
17
18
|
type UwfStore,
|
|
18
19
|
} from "../store.js";
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
checkWorkflowFilenameConsistency,
|
|
22
|
+
isCasRef,
|
|
23
|
+
isMissingVersion,
|
|
24
|
+
parseWorkflowPayload,
|
|
25
|
+
} from "../validate.js";
|
|
20
26
|
import { validateWorkflow } from "../validate-semantic.js";
|
|
21
27
|
|
|
22
28
|
export type WorkflowOrigin = "local" | "global";
|
|
@@ -105,6 +111,7 @@ export async function materializeWorkflowPayload(
|
|
|
105
111
|
};
|
|
106
112
|
}
|
|
107
113
|
return {
|
|
114
|
+
version: raw.version,
|
|
108
115
|
name: raw.name,
|
|
109
116
|
description: raw.description,
|
|
110
117
|
roles,
|
|
@@ -112,6 +119,43 @@ export async function materializeWorkflowPayload(
|
|
|
112
119
|
};
|
|
113
120
|
}
|
|
114
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Validate a workflow YAML file without registering it.
|
|
124
|
+
*
|
|
125
|
+
* CI-friendly: does not touch CAS or the workflow registry. On success,
|
|
126
|
+
* returns silently (no stdout/stderr) and exits 0. On any error, writes a
|
|
127
|
+
* single message to stderr and exits 1.
|
|
128
|
+
*/
|
|
129
|
+
export async function cmdWorkflowValidate(filePath: string): Promise<string[]> {
|
|
130
|
+
let text: string;
|
|
131
|
+
try {
|
|
132
|
+
text = await readFile(filePath, "utf8");
|
|
133
|
+
} catch {
|
|
134
|
+
fail(`file not found: ${filePath}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let raw: unknown;
|
|
138
|
+
try {
|
|
139
|
+
raw = parse(text, {
|
|
140
|
+
customTags: [createIncludeTag(dirname(resolvePath(filePath)))],
|
|
141
|
+
}) as unknown;
|
|
142
|
+
} catch (e) {
|
|
143
|
+
fail(`invalid YAML: ${e instanceof Error ? e.message : String(e)}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const payload = parseWorkflowPayload(raw);
|
|
147
|
+
if (payload === null) {
|
|
148
|
+
fail("invalid workflow YAML: expected WorkflowPayload shape");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const filenameError = checkWorkflowFilenameConsistency(filePath, payload);
|
|
152
|
+
if (filenameError !== null) {
|
|
153
|
+
return [filenameError];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return validateWorkflow(payload);
|
|
157
|
+
}
|
|
158
|
+
|
|
115
159
|
export async function cmdWorkflowAdd(
|
|
116
160
|
storageRoot: string,
|
|
117
161
|
filePath: string,
|
|
@@ -137,6 +181,12 @@ export async function cmdWorkflowAdd(
|
|
|
137
181
|
fail("invalid workflow YAML: expected WorkflowPayload shape");
|
|
138
182
|
}
|
|
139
183
|
|
|
184
|
+
if (isMissingVersion(raw)) {
|
|
185
|
+
process.stderr.write(
|
|
186
|
+
`warning: workflow YAML "${basename(filePath)}" is missing top-level \`version\` field; falling back to version 1. Add \`version: 1\` to silence this warning.\n`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
140
190
|
const filenameError = checkWorkflowFilenameConsistency(filePath, payload);
|
|
141
191
|
if (filenameError !== null) {
|
|
142
192
|
fail(filenameError);
|
|
@@ -161,13 +211,113 @@ export async function cmdWorkflowAdd(
|
|
|
161
211
|
return { name: materialized.name, hash };
|
|
162
212
|
}
|
|
163
213
|
|
|
214
|
+
// ── workflow show resolution helpers ──────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
function isFilePath(input: string): boolean {
|
|
217
|
+
return (
|
|
218
|
+
input.includes("/") || input.includes("\\") || input.endsWith(".yaml") || input.endsWith(".yml")
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function materializeLocalWorkflowForShow(uwf: UwfStore, filePath: string): Promise<CasRef> {
|
|
223
|
+
let text: string;
|
|
224
|
+
try {
|
|
225
|
+
text = await readFile(filePath, "utf8");
|
|
226
|
+
} catch {
|
|
227
|
+
fail(`project workflow file not found: ${filePath}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let raw: unknown;
|
|
231
|
+
try {
|
|
232
|
+
raw = parse(text, { customTags: [createIncludeTag(dirname(filePath))] }) as unknown;
|
|
233
|
+
} catch (e) {
|
|
234
|
+
fail(`invalid YAML in ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const payload = parseWorkflowPayload(raw);
|
|
238
|
+
if (payload === null) {
|
|
239
|
+
fail(`invalid workflow YAML in ${filePath}: expected WorkflowPayload shape`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const filenameError = checkWorkflowFilenameConsistency(filePath, payload);
|
|
243
|
+
if (filenameError !== null) {
|
|
244
|
+
fail(filenameError);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const semanticErrors = validateWorkflow(payload);
|
|
248
|
+
if (semanticErrors.length > 0) {
|
|
249
|
+
fail(`workflow validation failed:\n${semanticErrors.map((e) => ` - ${e}`).join("\n")}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const materialized = await materializeWorkflowPayload(uwf, payload);
|
|
253
|
+
const hash = await uwf.store.cas.put(uwf.schemas.workflow, materialized);
|
|
254
|
+
const stored = uwf.store.cas.get(hash);
|
|
255
|
+
if (stored === null || !validate(uwf.store, stored)) {
|
|
256
|
+
fail("stored local workflow failed schema validation");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return hash;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function resolveWorkflowCasRefForShow(
|
|
263
|
+
uwf: UwfStore,
|
|
264
|
+
workflowId: string,
|
|
265
|
+
projectRoot: string,
|
|
266
|
+
): Promise<CasRef> {
|
|
267
|
+
// Validate input
|
|
268
|
+
const trimmed = workflowId.trim();
|
|
269
|
+
if (trimmed === "") {
|
|
270
|
+
fail("workflow ID cannot be empty");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Strategy 1: Direct CAS hash
|
|
274
|
+
if (isCasRef(trimmed)) {
|
|
275
|
+
const node = uwf.store.cas.get(trimmed);
|
|
276
|
+
if (node === null) {
|
|
277
|
+
fail(`CAS node not found: ${trimmed}`);
|
|
278
|
+
}
|
|
279
|
+
if (node.type !== uwf.schemas.workflow) {
|
|
280
|
+
fail(`node ${trimmed} is not a Workflow (type ${node.type})`);
|
|
281
|
+
}
|
|
282
|
+
return trimmed;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Strategy 2: Explicit file path (relative or absolute)
|
|
286
|
+
if (isFilePath(trimmed)) {
|
|
287
|
+
const absolutePath = isAbsolute(trimmed) ? trimmed : resolvePath(projectRoot, trimmed);
|
|
288
|
+
return materializeLocalWorkflowForShow(uwf, absolutePath);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Strategy 3: Local discovery (reuses discoverProjectWorkflows from store.ts)
|
|
292
|
+
const localEntries = await discoverProjectWorkflows(projectRoot);
|
|
293
|
+
const localPath = resolveProjectWorkflowFile(localEntries, trimmed);
|
|
294
|
+
if (localPath !== null) {
|
|
295
|
+
return materializeLocalWorkflowForShow(uwf, localPath);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Strategy 4: Global registry fallback
|
|
299
|
+
const registry = loadWorkflowRegistry(uwf.varStore);
|
|
300
|
+
const hash = resolveWorkflowHash(registry, trimmed);
|
|
301
|
+
if (!isCasRef(hash)) {
|
|
302
|
+
fail(`workflow not found: ${trimmed}`);
|
|
303
|
+
}
|
|
304
|
+
const node = uwf.store.cas.get(hash);
|
|
305
|
+
if (node === null) {
|
|
306
|
+
fail(`CAS node not found: ${hash}`);
|
|
307
|
+
}
|
|
308
|
+
if (node.type !== uwf.schemas.workflow) {
|
|
309
|
+
fail(`node ${hash} is not a Workflow (type ${node.type})`);
|
|
310
|
+
}
|
|
311
|
+
return hash;
|
|
312
|
+
}
|
|
313
|
+
|
|
164
314
|
export async function cmdWorkflowShow(
|
|
165
315
|
storageRoot: string,
|
|
166
316
|
id: string,
|
|
317
|
+
projectRoot: string,
|
|
167
318
|
): Promise<WorkflowShowOutput> {
|
|
168
319
|
const uwf = await createUwfStore(storageRoot);
|
|
169
|
-
const
|
|
170
|
-
const hash = resolveWorkflowHash(registry, id);
|
|
320
|
+
const hash = await resolveWorkflowCasRefForShow(uwf, id, projectRoot);
|
|
171
321
|
|
|
172
322
|
const node = uwf.store.cas.get(hash);
|
|
173
323
|
if (node === null) {
|
|
@@ -178,6 +328,7 @@ export async function cmdWorkflowShow(
|
|
|
178
328
|
}
|
|
179
329
|
|
|
180
330
|
const payload = node.payload as WorkflowPayload;
|
|
331
|
+
const registry = loadWorkflowRegistry(uwf.varStore);
|
|
181
332
|
return {
|
|
182
333
|
hash,
|
|
183
334
|
name: findRegistryName(registry, hash),
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { unlinkSync } from "node:fs";
|
|
2
|
+
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { isPidAlive } from "../background/index.js";
|
|
5
|
+
import type { AcquireSlotOptions, SlotHandle } from "./types.js";
|
|
6
|
+
|
|
7
|
+
/** Default concurrency limit when no config or flag is provided. */
|
|
8
|
+
export const DEFAULT_MAX_RUNNING = 2;
|
|
9
|
+
|
|
10
|
+
/** Default poll interval (ms) for waiting on a slot. */
|
|
11
|
+
const DEFAULT_POLL_INTERVAL_MS = 2000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the path to the slots directory.
|
|
15
|
+
*/
|
|
16
|
+
export function getSlotsDir(storageRoot: string): string {
|
|
17
|
+
return join(storageRoot, "slots");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Count active slot files (alive PIDs only). Stale slots are skipped but not removed.
|
|
22
|
+
*/
|
|
23
|
+
export async function countActiveSlots(storageRoot: string): Promise<number> {
|
|
24
|
+
const slotsDir = getSlotsDir(storageRoot);
|
|
25
|
+
let files: string[];
|
|
26
|
+
try {
|
|
27
|
+
files = await readdir(slotsDir);
|
|
28
|
+
} catch {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let count = 0;
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
if (!file.endsWith(".slot")) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const pidStr = file.slice(0, -5);
|
|
38
|
+
const pid = Number(pidStr);
|
|
39
|
+
if (Number.isNaN(pid)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (isPidAlive(pid)) {
|
|
43
|
+
count++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return count;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Remove slot files whose PIDs are no longer alive.
|
|
51
|
+
* Returns the number of stale slots cleaned.
|
|
52
|
+
*/
|
|
53
|
+
export async function cleanStaleSlots(storageRoot: string): Promise<number> {
|
|
54
|
+
const slotsDir = getSlotsDir(storageRoot);
|
|
55
|
+
let files: string[];
|
|
56
|
+
try {
|
|
57
|
+
files = await readdir(slotsDir);
|
|
58
|
+
} catch {
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let cleaned = 0;
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
if (!file.endsWith(".slot")) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const pidStr = file.slice(0, -5);
|
|
68
|
+
const pid = Number(pidStr);
|
|
69
|
+
if (Number.isNaN(pid)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!isPidAlive(pid)) {
|
|
73
|
+
try {
|
|
74
|
+
await rm(join(slotsDir, file), { force: true });
|
|
75
|
+
cleaned++;
|
|
76
|
+
} catch {
|
|
77
|
+
// Ignore removal errors (race with another cleanup)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return cleaned;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create a slot file for the current process. Returns the path to the created file.
|
|
86
|
+
*/
|
|
87
|
+
async function writeSlotFile(storageRoot: string): Promise<string> {
|
|
88
|
+
const slotsDir = getSlotsDir(storageRoot);
|
|
89
|
+
await mkdir(slotsDir, { recursive: true });
|
|
90
|
+
const slotPath = join(slotsDir, `${process.pid}.slot`);
|
|
91
|
+
await writeFile(slotPath, "", "utf8");
|
|
92
|
+
return slotPath;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Remove a slot file. Idempotent — silently ignores missing file.
|
|
97
|
+
*/
|
|
98
|
+
async function removeSlotFile(slotPath: string): Promise<void> {
|
|
99
|
+
try {
|
|
100
|
+
await rm(slotPath, { force: true });
|
|
101
|
+
} catch {
|
|
102
|
+
// Already removed or race condition — safe to ignore
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sleep(ms: number, signal: AbortSignal | null): Promise<void> {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
if (signal?.aborted) {
|
|
109
|
+
reject(new Error("aborted"));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const timer = setTimeout(resolve, ms);
|
|
113
|
+
if (signal !== null) {
|
|
114
|
+
const onAbort = () => {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
reject(new Error("aborted"));
|
|
117
|
+
};
|
|
118
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Try to claim a slot. Returns the slot path on success, null if a race was
|
|
125
|
+
* detected (post-write count exceeds maxRunning → rolls back).
|
|
126
|
+
*/
|
|
127
|
+
async function tryClaimSlot(storageRoot: string, maxRunning: number): Promise<string | null> {
|
|
128
|
+
const slotPath = await writeSlotFile(storageRoot);
|
|
129
|
+
const postWriteCount = await countActiveSlots(storageRoot);
|
|
130
|
+
if (postWriteCount > maxRunning) {
|
|
131
|
+
await removeSlotFile(slotPath);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return slotPath;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function createSlotHandle(slotPath: string): SlotHandle {
|
|
138
|
+
let released = false;
|
|
139
|
+
return {
|
|
140
|
+
slotPath,
|
|
141
|
+
release: async () => {
|
|
142
|
+
if (released) return;
|
|
143
|
+
released = true;
|
|
144
|
+
await removeSlotFile(slotPath);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
type ResolvedOptions = {
|
|
150
|
+
onWaiting: ((info: string) => void) | null;
|
|
151
|
+
onAcquired: (() => void) | null;
|
|
152
|
+
pollIntervalMs: number;
|
|
153
|
+
signal: AbortSignal | null;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function resolveOptions(options: Partial<AcquireSlotOptions>): ResolvedOptions {
|
|
157
|
+
return {
|
|
158
|
+
onWaiting: options.onWaiting ?? null,
|
|
159
|
+
onAcquired: options.onAcquired ?? null,
|
|
160
|
+
pollIntervalMs: options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
|
161
|
+
signal: options.signal ?? null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function notifyWaiting(opts: ResolvedOptions, waited: boolean, message: string): boolean {
|
|
166
|
+
if (!waited && opts.onWaiting !== null) {
|
|
167
|
+
opts.onWaiting(message);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return waited;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Acquire a concurrency slot. If all slots are occupied, polls until one is available.
|
|
175
|
+
*
|
|
176
|
+
* Race protection: after writing the slot file, double-checks countActiveSlots.
|
|
177
|
+
* If the count exceeds maxRunning, rolls back (removes own slot) and retries.
|
|
178
|
+
*/
|
|
179
|
+
export async function acquireSlot(
|
|
180
|
+
storageRoot: string,
|
|
181
|
+
maxRunning: number,
|
|
182
|
+
options: Partial<AcquireSlotOptions> = {},
|
|
183
|
+
): Promise<SlotHandle> {
|
|
184
|
+
const opts = resolveOptions(options);
|
|
185
|
+
let waited = false;
|
|
186
|
+
|
|
187
|
+
while (true) {
|
|
188
|
+
await cleanStaleSlots(storageRoot);
|
|
189
|
+
|
|
190
|
+
const currentCount = await countActiveSlots(storageRoot);
|
|
191
|
+
if (currentCount >= maxRunning) {
|
|
192
|
+
waited = notifyWaiting(opts, waited, `${currentCount}/${maxRunning} running`);
|
|
193
|
+
await sleep(opts.pollIntervalMs, opts.signal);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const slotPath = await tryClaimSlot(storageRoot, maxRunning);
|
|
198
|
+
if (slotPath === null) {
|
|
199
|
+
waited = notifyWaiting(opts, waited, `race detected, retrying`);
|
|
200
|
+
await sleep(opts.pollIntervalMs, opts.signal);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (waited && opts.onAcquired !== null) {
|
|
205
|
+
opts.onAcquired();
|
|
206
|
+
}
|
|
207
|
+
return createSlotHandle(slotPath);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Alias for SlotHandle.release() — explicit function form for callers that
|
|
213
|
+
* prefer passing the handle as an argument.
|
|
214
|
+
*/
|
|
215
|
+
export async function releaseSlot(handle: SlotHandle): Promise<void> {
|
|
216
|
+
await handle.release();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Install process signal handlers that release the slot on SIGINT/SIGTERM.
|
|
221
|
+
* Returns a cleanup function that removes the handlers (call on normal exit).
|
|
222
|
+
*/
|
|
223
|
+
export function installSlotCleanup(handle: SlotHandle): () => void {
|
|
224
|
+
const cleanup = () => {
|
|
225
|
+
try {
|
|
226
|
+
unlinkSync(handle.slotPath);
|
|
227
|
+
} catch {
|
|
228
|
+
// Already removed
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const onSignal = () => {
|
|
233
|
+
cleanup();
|
|
234
|
+
process.exit(1);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
process.on("SIGINT", onSignal);
|
|
238
|
+
process.on("SIGTERM", onSignal);
|
|
239
|
+
|
|
240
|
+
// Return a function to uninstall the handlers
|
|
241
|
+
return () => {
|
|
242
|
+
process.removeListener("SIGINT", onSignal);
|
|
243
|
+
process.removeListener("SIGTERM", onSignal);
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Handle returned by acquireSlot; call release() to free the slot. */
|
|
2
|
+
export type SlotHandle = {
|
|
3
|
+
/** Remove the slot file. Idempotent — second call is a no-op. */
|
|
4
|
+
release: () => Promise<void>;
|
|
5
|
+
/** The slot file path (for signal-handler cleanup). */
|
|
6
|
+
slotPath: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** Options for acquireSlot polling behavior and callbacks. */
|
|
10
|
+
export type AcquireSlotOptions = {
|
|
11
|
+
/** Called when the function starts waiting (all slots occupied). */
|
|
12
|
+
onWaiting: ((info: string) => void) | null;
|
|
13
|
+
/** Called when a slot becomes available after waiting. */
|
|
14
|
+
onAcquired: (() => void) | null;
|
|
15
|
+
/** Poll interval in milliseconds (default: 2000). */
|
|
16
|
+
pollIntervalMs: number;
|
|
17
|
+
/** AbortSignal to cancel waiting. */
|
|
18
|
+
signal: AbortSignal | null;
|
|
19
|
+
};
|