@united-workforce/cli 0.1.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/LICENSE +21 -0
- package/README.md +221 -0
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js +147 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +685 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/current-role.test.d.ts +2 -0
- package/dist/__tests__/current-role.test.d.ts.map +1 -0
- package/dist/__tests__/current-role.test.js +401 -0
- package/dist/__tests__/current-role.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.d.ts +2 -0
- package/dist/__tests__/e2e-mock-agent.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +401 -0
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -0
- package/dist/__tests__/include-tag.test.d.ts +2 -0
- package/dist/__tests__/include-tag.test.d.ts.map +1 -0
- package/dist/__tests__/include-tag.test.js +69 -0
- package/dist/__tests__/include-tag.test.js.map +1 -0
- package/dist/__tests__/log.test.d.ts +2 -0
- package/dist/__tests__/log.test.d.ts.map +1 -0
- package/dist/__tests__/log.test.js +161 -0
- package/dist/__tests__/log.test.js.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.d.ts +2 -0
- package/dist/__tests__/moderator-evaluate.test.d.ts.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.js +170 -0
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -0
- package/dist/__tests__/preload.d.ts +3 -0
- package/dist/__tests__/preload.d.ts.map +1 -0
- package/dist/__tests__/preload.js +6 -0
- package/dist/__tests__/preload.js.map +1 -0
- package/dist/__tests__/prompt.test.d.ts +2 -0
- package/dist/__tests__/prompt.test.d.ts.map +1 -0
- package/dist/__tests__/prompt.test.js +111 -0
- package/dist/__tests__/prompt.test.js.map +1 -0
- package/dist/__tests__/resolve-head-hash.test.d.ts +2 -0
- package/dist/__tests__/resolve-head-hash.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-head-hash.test.js +66 -0
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -0
- package/dist/__tests__/setup-agent-discovery.test.d.ts +2 -0
- package/dist/__tests__/setup-agent-discovery.test.d.ts.map +1 -0
- package/dist/__tests__/setup-agent-discovery.test.js +119 -0
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -0
- package/dist/__tests__/setup-complexity.test.d.ts +2 -0
- package/dist/__tests__/setup-complexity.test.d.ts.map +1 -0
- package/dist/__tests__/setup-complexity.test.js +314 -0
- package/dist/__tests__/setup-complexity.test.js.map +1 -0
- package/dist/__tests__/setup-validate.test.d.ts +2 -0
- package/dist/__tests__/setup-validate.test.d.ts.map +1 -0
- package/dist/__tests__/setup-validate.test.js +108 -0
- package/dist/__tests__/setup-validate.test.js.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.d.ts +2 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.d.ts.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js +107 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -0
- package/dist/__tests__/spawn-agent-json.test.d.ts +2 -0
- package/dist/__tests__/spawn-agent-json.test.d.ts.map +1 -0
- package/dist/__tests__/spawn-agent-json.test.js +79 -0
- package/dist/__tests__/spawn-agent-json.test.js.map +1 -0
- package/dist/__tests__/step-read.test.d.ts +2 -0
- package/dist/__tests__/step-read.test.d.ts.map +1 -0
- package/dist/__tests__/step-read.test.js +561 -0
- package/dist/__tests__/step-read.test.js.map +1 -0
- package/dist/__tests__/step-show-json.test.d.ts +2 -0
- package/dist/__tests__/step-show-json.test.d.ts.map +1 -0
- package/dist/__tests__/step-show-json.test.js +311 -0
- package/dist/__tests__/step-show-json.test.js.map +1 -0
- package/dist/__tests__/step-timing.test.d.ts +2 -0
- package/dist/__tests__/step-timing.test.d.ts.map +1 -0
- package/dist/__tests__/step-timing.test.js +345 -0
- package/dist/__tests__/step-timing.test.js.map +1 -0
- package/dist/__tests__/store-global-cas.test.d.ts +2 -0
- package/dist/__tests__/store-global-cas.test.d.ts.map +1 -0
- package/dist/__tests__/store-global-cas.test.js +235 -0
- package/dist/__tests__/store-global-cas.test.js.map +1 -0
- package/dist/__tests__/store-storage-root.test.d.ts +2 -0
- package/dist/__tests__/store-storage-root.test.d.ts.map +1 -0
- package/dist/__tests__/store-storage-root.test.js +43 -0
- package/dist/__tests__/store-storage-root.test.js.map +1 -0
- package/dist/__tests__/store-unified-threads.test.d.ts +2 -0
- package/dist/__tests__/store-unified-threads.test.d.ts.map +1 -0
- package/dist/__tests__/store-unified-threads.test.js +189 -0
- package/dist/__tests__/store-unified-threads.test.js.map +1 -0
- package/dist/__tests__/thread-cancel-status.test.d.ts +2 -0
- package/dist/__tests__/thread-cancel-status.test.d.ts.map +1 -0
- package/dist/__tests__/thread-cancel-status.test.js +111 -0
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -0
- package/dist/__tests__/thread-list-filters.test.d.ts +2 -0
- package/dist/__tests__/thread-list-filters.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-filters.test.js +442 -0
- package/dist/__tests__/thread-list-filters.test.js.map +1 -0
- package/dist/__tests__/thread-location.test.d.ts +2 -0
- package/dist/__tests__/thread-location.test.d.ts.map +1 -0
- package/dist/__tests__/thread-location.test.js +159 -0
- package/dist/__tests__/thread-location.test.js.map +1 -0
- package/dist/__tests__/thread-read-quota.test.d.ts +2 -0
- package/dist/__tests__/thread-read-quota.test.d.ts.map +1 -0
- package/dist/__tests__/thread-read-quota.test.js +546 -0
- package/dist/__tests__/thread-read-quota.test.js.map +1 -0
- package/dist/__tests__/thread-read-xml-tags.test.d.ts +2 -0
- package/dist/__tests__/thread-read-xml-tags.test.d.ts.map +1 -0
- package/dist/__tests__/thread-read-xml-tags.test.js +610 -0
- package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -0
- package/dist/__tests__/thread-resume.test.d.ts +2 -0
- package/dist/__tests__/thread-resume.test.d.ts.map +1 -0
- package/dist/__tests__/thread-resume.test.js +592 -0
- package/dist/__tests__/thread-resume.test.js.map +1 -0
- package/dist/__tests__/thread-show-status.test.d.ts +2 -0
- package/dist/__tests__/thread-show-status.test.d.ts.map +1 -0
- package/dist/__tests__/thread-show-status.test.js +267 -0
- package/dist/__tests__/thread-show-status.test.js.map +1 -0
- package/dist/__tests__/thread-start-cwd-cli.test.d.ts +2 -0
- package/dist/__tests__/thread-start-cwd-cli.test.d.ts.map +1 -0
- package/dist/__tests__/thread-start-cwd-cli.test.js +130 -0
- package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -0
- package/dist/__tests__/thread-step-count.test.d.ts +2 -0
- package/dist/__tests__/thread-step-count.test.d.ts.map +1 -0
- package/dist/__tests__/thread-step-count.test.js +55 -0
- package/dist/__tests__/thread-step-count.test.js.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.d.ts +2 -0
- package/dist/__tests__/thread-suspend-step.test.d.ts.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.js +155 -0
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -0
- package/dist/__tests__/thread-suspended-display.test.d.ts +2 -0
- package/dist/__tests__/thread-suspended-display.test.d.ts.map +1 -0
- package/dist/__tests__/thread-suspended-display.test.js +247 -0
- package/dist/__tests__/thread-suspended-display.test.js.map +1 -0
- package/dist/__tests__/thread-test-helpers.d.ts +4 -0
- package/dist/__tests__/thread-test-helpers.d.ts.map +1 -0
- package/dist/__tests__/thread-test-helpers.js +23 -0
- package/dist/__tests__/thread-test-helpers.js.map +1 -0
- package/dist/__tests__/thread.test.d.ts +2 -0
- package/dist/__tests__/thread.test.d.ts.map +1 -0
- package/dist/__tests__/thread.test.js +883 -0
- package/dist/__tests__/thread.test.js.map +1 -0
- package/dist/__tests__/validate-semantic.test.d.ts +2 -0
- package/dist/__tests__/validate-semantic.test.d.ts.map +1 -0
- package/dist/__tests__/validate-semantic.test.js +408 -0
- package/dist/__tests__/validate-semantic.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.d.ts +2 -0
- package/dist/__tests__/workflow-resolution.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +308 -0
- package/dist/__tests__/workflow-resolution.test.js.map +1 -0
- package/dist/background/background.d.ts +38 -0
- package/dist/background/background.d.ts.map +1 -0
- package/dist/background/background.js +123 -0
- package/dist/background/background.js.map +1 -0
- package/dist/background/index.d.ts +3 -0
- package/dist/background/index.d.ts.map +1 -0
- package/dist/background/index.js +2 -0
- package/dist/background/index.js.map +1 -0
- package/dist/background/types.d.ts +9 -0
- package/dist/background/types.d.ts.map +1 -0
- package/dist/background/types.js +2 -0
- package/dist/background/types.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +535 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.d.ts +41 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +252 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/log.d.ts +26 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +79 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/prompt.d.ts +6 -0
- package/dist/commands/prompt.d.ts.map +1 -0
- package/dist/commands/prompt.js +67 -0
- package/dist/commands/prompt.js.map +1 -0
- package/dist/commands/setup.d.ts +73 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +522 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/shared.d.ts +31 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +154 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/step.d.ts +18 -0
- package/dist/commands/step.d.ts.map +1 -0
- package/dist/commands/step.js +257 -0
- package/dist/commands/step.js.map +1 -0
- package/dist/commands/thread-time-parser.d.ts +6 -0
- package/dist/commands/thread-time-parser.d.ts.map +1 -0
- package/dist/commands/thread-time-parser.js +22 -0
- package/dist/commands/thread-time-parser.js.map +1 -0
- package/dist/commands/thread.d.ts +38 -0
- package/dist/commands/thread.d.ts.map +1 -0
- package/dist/commands/thread.js +1087 -0
- package/dist/commands/thread.js.map +1 -0
- package/dist/commands/workflow.d.ts +24 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +138 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +10 -0
- package/dist/format.js.map +1 -0
- package/dist/include.d.ts +12 -0
- package/dist/include.d.ts.map +1 -0
- package/dist/include.js +35 -0
- package/dist/include.js.map +1 -0
- package/dist/moderator/__tests__/evaluate.test.d.ts +2 -0
- package/dist/moderator/__tests__/evaluate.test.d.ts.map +1 -0
- package/dist/moderator/__tests__/evaluate.test.js +167 -0
- package/dist/moderator/__tests__/evaluate.test.js.map +1 -0
- package/dist/moderator/evaluate.d.ts +6 -0
- package/dist/moderator/evaluate.d.ts.map +1 -0
- package/dist/moderator/evaluate.js +65 -0
- package/dist/moderator/evaluate.js.map +1 -0
- package/dist/moderator/index.d.ts +4 -0
- package/dist/moderator/index.d.ts.map +1 -0
- package/dist/moderator/index.js +3 -0
- package/dist/moderator/index.js.map +1 -0
- package/dist/moderator/types.d.ts +25 -0
- package/dist/moderator/types.d.ts.map +1 -0
- package/dist/moderator/types.js +4 -0
- package/dist/moderator/types.js.map +1 -0
- package/dist/schemas.d.ts +16 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +17 -0
- package/dist/schemas.js.map +1 -0
- package/dist/store.d.ts +77 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +392 -0
- package/dist/store.js.map +1 -0
- package/dist/validate-semantic.d.ts +7 -0
- package/dist/validate-semantic.d.ts.map +1 -0
- package/dist/validate-semantic.js +263 -0
- package/dist/validate-semantic.js.map +1 -0
- package/dist/validate.d.ts +16 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +115 -0
- package/dist/validate.js.map +1 -0
- package/package.json +44 -0
- package/src/__tests__/adapter-json-roundtrip.test.ts +181 -0
- package/src/__tests__/config.test.ts +740 -0
- package/src/__tests__/current-role.test.ts +438 -0
- package/src/__tests__/e2e-mock-agent.test.ts +498 -0
- package/src/__tests__/fixtures/e2e-completed-resume.mock.yaml +15 -0
- package/src/__tests__/fixtures/e2e-count.mock.yaml +19 -0
- package/src/__tests__/fixtures/e2e-count.workflow.yaml +45 -0
- package/src/__tests__/fixtures/e2e-linear.mock.yaml +13 -0
- package/src/__tests__/fixtures/e2e-linear.workflow.yaml +32 -0
- package/src/__tests__/fixtures/e2e-loop.mock.yaml +25 -0
- package/src/__tests__/fixtures/e2e-loop.workflow.yaml +36 -0
- package/src/__tests__/fixtures/e2e-mismatch.mock.yaml +16 -0
- package/src/__tests__/fixtures/e2e-mustache.mock.yaml +15 -0
- package/src/__tests__/fixtures/e2e-mustache.workflow.yaml +34 -0
- package/src/__tests__/fixtures/e2e-suspend.mock.yaml +14 -0
- package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +24 -0
- package/src/__tests__/include-tag.test.ts +84 -0
- package/src/__tests__/log.test.ts +181 -0
- package/src/__tests__/moderator-evaluate.test.ts +186 -0
- package/src/__tests__/preload.ts +7 -0
- package/src/__tests__/prompt.test.ts +129 -0
- package/src/__tests__/resolve-head-hash.test.ts +86 -0
- package/src/__tests__/setup-agent-discovery.test.ts +167 -0
- package/src/__tests__/setup-complexity.test.ts +381 -0
- package/src/__tests__/setup-validate.test.ts +148 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +144 -0
- package/src/__tests__/spawn-agent-json.test.ts +100 -0
- package/src/__tests__/step-read.test.ts +632 -0
- package/src/__tests__/step-show-json.test.ts +373 -0
- package/src/__tests__/step-timing.test.ts +392 -0
- package/src/__tests__/store-global-cas.test.ts +308 -0
- package/src/__tests__/store-storage-root.test.ts +49 -0
- package/src/__tests__/store-unified-threads.test.ts +235 -0
- package/src/__tests__/thread-cancel-status.test.ts +138 -0
- package/src/__tests__/thread-list-filters.test.ts +572 -0
- package/src/__tests__/thread-location.test.ts +186 -0
- package/src/__tests__/thread-read-quota.test.ts +613 -0
- package/src/__tests__/thread-read-xml-tags.test.ts +717 -0
- package/src/__tests__/thread-resume.test.ts +710 -0
- package/src/__tests__/thread-show-status.test.ts +317 -0
- package/src/__tests__/thread-start-cwd-cli.test.ts +164 -0
- package/src/__tests__/thread-step-count.test.ts +70 -0
- package/src/__tests__/thread-suspend-step.test.ts +181 -0
- package/src/__tests__/thread-suspended-display.test.ts +287 -0
- package/src/__tests__/thread-test-helpers.ts +37 -0
- package/src/__tests__/thread.test.ts +1025 -0
- package/src/__tests__/validate-semantic.test.ts +474 -0
- package/src/__tests__/workflow-resolution.test.ts +421 -0
- package/src/background/background.ts +147 -0
- package/src/background/index.ts +11 -0
- package/src/background/types.ts +9 -0
- package/src/cli.ts +692 -0
- package/src/commands/config.ts +304 -0
- package/src/commands/log.ts +116 -0
- package/src/commands/prompt.ts +81 -0
- package/src/commands/setup.ts +603 -0
- package/src/commands/shared.ts +227 -0
- package/src/commands/step.ts +343 -0
- package/src/commands/thread-time-parser.ts +23 -0
- package/src/commands/thread.ts +1575 -0
- package/src/commands/workflow.ts +213 -0
- package/src/format.ts +12 -0
- package/src/include.ts +37 -0
- package/src/moderator/__tests__/evaluate.test.ts +199 -0
- package/src/moderator/evaluate.ts +80 -0
- package/src/moderator/index.ts +7 -0
- package/src/moderator/types.ts +24 -0
- package/src/schemas.ts +26 -0
- package/src/store.ts +479 -0
- package/src/validate-semantic.ts +304 -0
- package/src/validate.ts +137 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import type { WorkflowPayload } from "@united-workforce/protocol";
|
|
2
|
+
|
|
3
|
+
type SchemaObj = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
const RESERVED_NAMES = new Set(["$START", "$END", "$SUSPEND"]);
|
|
6
|
+
const PSEUDO_TARGETS = new Set(["$END", "$SUSPEND"]);
|
|
7
|
+
|
|
8
|
+
/** Extract mustache variable names from a prompt string. */
|
|
9
|
+
function extractMustacheVars(prompt: string): string[] {
|
|
10
|
+
const vars: string[] = [];
|
|
11
|
+
const re = /\{\{\{?([^}]+)\}\}\}?/g;
|
|
12
|
+
let m: RegExpExecArray | null = re.exec(prompt);
|
|
13
|
+
while (m !== null) {
|
|
14
|
+
vars.push(m[1]);
|
|
15
|
+
m = re.exec(prompt);
|
|
16
|
+
}
|
|
17
|
+
return vars;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Check if a frontmatter schema is a oneOf (multi-exit) type. */
|
|
21
|
+
function isOneOfSchema(fm: unknown): fm is SchemaObj & { oneOf: SchemaObj[] } {
|
|
22
|
+
if (typeof fm !== "object" || fm === null) return false;
|
|
23
|
+
const obj = fm as SchemaObj;
|
|
24
|
+
return Array.isArray(obj.oneOf);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Check if a frontmatter schema declares "$status" as an enum (the required form for user roles). */
|
|
28
|
+
function hasStatusEnum(fm: unknown): boolean {
|
|
29
|
+
if (typeof fm !== "object" || fm === null) return false;
|
|
30
|
+
const obj = fm as SchemaObj;
|
|
31
|
+
const props = obj.properties as Record<string, SchemaObj> | undefined;
|
|
32
|
+
if (!props?.$status) return false;
|
|
33
|
+
return Array.isArray(props.$status.enum);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Extract status values from an enum-based $status field. */
|
|
37
|
+
function getEnumStatuses(fm: SchemaObj): string[] {
|
|
38
|
+
const props = fm.properties as Record<string, SchemaObj> | undefined;
|
|
39
|
+
if (!props?.$status) return [];
|
|
40
|
+
const statusDef = props.$status;
|
|
41
|
+
if (!Array.isArray(statusDef.enum)) return [];
|
|
42
|
+
return statusDef.enum as string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Get property names from a schema object. */
|
|
46
|
+
function getPropertyNames(schema: SchemaObj): Set<string> {
|
|
47
|
+
const props = schema.properties;
|
|
48
|
+
if (typeof props !== "object" || props === null) return new Set();
|
|
49
|
+
return new Set(Object.keys(props as Record<string, unknown>));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Extract $status const values from oneOf variants. */
|
|
53
|
+
function getOneOfStatuses(variants: SchemaObj[]): string[] {
|
|
54
|
+
const statuses: string[] = [];
|
|
55
|
+
for (const variant of variants) {
|
|
56
|
+
const props = variant.properties as Record<string, SchemaObj> | undefined;
|
|
57
|
+
if (props?.$status) {
|
|
58
|
+
const statusDef = props.$status;
|
|
59
|
+
if (typeof statusDef.const === "string") {
|
|
60
|
+
statuses.push(statusDef.const);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return statuses;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Check reserved names and role/graph reference integrity. */
|
|
68
|
+
function checkRoleReferences(payload: WorkflowPayload, errors: string[]): void {
|
|
69
|
+
const roleNames = new Set(Object.keys(payload.roles));
|
|
70
|
+
const graphNodes = new Set(Object.keys(payload.graph));
|
|
71
|
+
|
|
72
|
+
for (const name of roleNames) {
|
|
73
|
+
if (RESERVED_NAMES.has(name)) {
|
|
74
|
+
errors.push(`reserved name "${name}" must not appear in roles`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const node of graphNodes) {
|
|
79
|
+
if (!RESERVED_NAMES.has(node) && !roleNames.has(node)) {
|
|
80
|
+
errors.push(`graph references unknown role "${node}"`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const name of roleNames) {
|
|
85
|
+
if (RESERVED_NAMES.has(name)) continue;
|
|
86
|
+
if (!graphNodes.has(name)) {
|
|
87
|
+
errors.push(`role "${name}" is defined but not referenced in graph`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Check $START/$END constraints, edge targets, and reachability. */
|
|
93
|
+
function checkGraphStructure(payload: WorkflowPayload, errors: string[]): void {
|
|
94
|
+
const roleNames = new Set(Object.keys(payload.roles));
|
|
95
|
+
const graphNodes = new Set(Object.keys(payload.graph));
|
|
96
|
+
|
|
97
|
+
if (!graphNodes.has("$START")) {
|
|
98
|
+
errors.push("$START must be defined in graph");
|
|
99
|
+
} else {
|
|
100
|
+
const startKeys = Object.keys(payload.graph.$START);
|
|
101
|
+
if (startKeys.length !== 1 || startKeys[0] !== "_") {
|
|
102
|
+
errors.push('$START must have exactly one edge with status "_"');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (graphNodes.has("$END")) {
|
|
107
|
+
errors.push("$END must not have outgoing edges");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (graphNodes.has("$SUSPEND")) {
|
|
111
|
+
errors.push("$SUSPEND must not have outgoing edges");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const [node, statusMap] of Object.entries(payload.graph)) {
|
|
115
|
+
for (const [status, target] of Object.entries(statusMap)) {
|
|
116
|
+
if (!PSEUDO_TARGETS.has(target.role) && !roleNames.has(target.role)) {
|
|
117
|
+
errors.push(`edge ${node}→${status}: unknown target role "${target.role}"`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
checkReachability(roleNames, collectReachableRoles(payload.graph), errors);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** BFS to collect all roles reachable from $START. */
|
|
126
|
+
function collectReachableRoles(graph: WorkflowPayload["graph"]): Set<string> {
|
|
127
|
+
const reachable = new Set<string>();
|
|
128
|
+
const startEdges = graph.$START;
|
|
129
|
+
if (!startEdges) return reachable;
|
|
130
|
+
|
|
131
|
+
const queue: string[] = [];
|
|
132
|
+
for (const target of Object.values(startEdges)) {
|
|
133
|
+
if (!PSEUDO_TARGETS.has(target.role) && !reachable.has(target.role)) {
|
|
134
|
+
reachable.add(target.role);
|
|
135
|
+
queue.push(target.role);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
while (queue.length > 0) {
|
|
140
|
+
const current = queue.shift() as string;
|
|
141
|
+
const edges = graph[current];
|
|
142
|
+
if (!edges) continue;
|
|
143
|
+
for (const target of Object.values(edges)) {
|
|
144
|
+
if (!PSEUDO_TARGETS.has(target.role) && !reachable.has(target.role)) {
|
|
145
|
+
reachable.add(target.role);
|
|
146
|
+
queue.push(target.role);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return reachable;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Check that all defined roles are reachable from $START. */
|
|
155
|
+
function checkReachability(roleNames: Set<string>, reachable: Set<string>, errors: string[]): void {
|
|
156
|
+
for (const name of roleNames) {
|
|
157
|
+
if (RESERVED_NAMES.has(name)) continue;
|
|
158
|
+
if (!reachable.has(name)) {
|
|
159
|
+
errors.push(`role "${name}" is not reachable from $START`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Check oneOf discriminant validity for a role. */
|
|
165
|
+
function checkOneOfDiscriminant(
|
|
166
|
+
roleName: string,
|
|
167
|
+
variants: SchemaObj[],
|
|
168
|
+
statuses: string[],
|
|
169
|
+
errors: string[],
|
|
170
|
+
): void {
|
|
171
|
+
if (statuses.length === variants.length) return;
|
|
172
|
+
|
|
173
|
+
let foundMissing = false;
|
|
174
|
+
for (const variant of variants) {
|
|
175
|
+
const props = variant.properties as Record<string, SchemaObj> | undefined;
|
|
176
|
+
if (!props?.$status) {
|
|
177
|
+
errors.push(`role "${roleName}": oneOf variants must have "$status" as const discriminant`);
|
|
178
|
+
foundMissing = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
if (typeof props.$status.const !== "string") {
|
|
182
|
+
errors.push(`role "${roleName}": oneOf variant $status must be a const value`);
|
|
183
|
+
foundMissing = true;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!foundMissing) {
|
|
189
|
+
errors.push(`role "${roleName}": oneOf variant $status must be a const value`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Check status-edge consistency for a user role. "_" is reserved for $START and rejected here. */
|
|
194
|
+
function checkStatusEdges(
|
|
195
|
+
roleName: string,
|
|
196
|
+
graphKeys: Set<string>,
|
|
197
|
+
statusSet: Set<string>,
|
|
198
|
+
errors: string[],
|
|
199
|
+
): void {
|
|
200
|
+
if (graphKeys.has("_")) {
|
|
201
|
+
errors.push(`role "${roleName}" must use explicit $status keys in graph, not "_"`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (statusSet.has("_")) {
|
|
205
|
+
errors.push(`role "${roleName}" $status enum must use explicit values, not "_"`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const extraKeys = [...graphKeys].filter((k) => !statusSet.has(k));
|
|
210
|
+
const missingKeys = [...statusSet].filter((k) => !graphKeys.has(k));
|
|
211
|
+
if (extraKeys.length > 0) {
|
|
212
|
+
errors.push(`role "${roleName}" graph has extra status keys: ${extraKeys.join(", ")}`);
|
|
213
|
+
}
|
|
214
|
+
if (missingKeys.length > 0) {
|
|
215
|
+
errors.push(`role "${roleName}" graph is missing status keys: ${missingKeys.join(", ")}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Check mustache variables for multi-exit role. */
|
|
220
|
+
function checkMultiExitMustache(
|
|
221
|
+
roleName: string,
|
|
222
|
+
graphEntry: Record<string, { role: string; prompt: string }>,
|
|
223
|
+
variants: SchemaObj[],
|
|
224
|
+
errors: string[],
|
|
225
|
+
): void {
|
|
226
|
+
for (const [status, target] of Object.entries(graphEntry)) {
|
|
227
|
+
const vars = extractMustacheVars(target.prompt);
|
|
228
|
+
const variant = variants.find((v) => {
|
|
229
|
+
const props = v.properties as Record<string, SchemaObj> | undefined;
|
|
230
|
+
return props?.$status?.const === status;
|
|
231
|
+
});
|
|
232
|
+
if (!variant) continue;
|
|
233
|
+
const propNames = getPropertyNames(variant);
|
|
234
|
+
for (const v of vars) {
|
|
235
|
+
if (v === "$status") continue;
|
|
236
|
+
if (!propNames.has(v)) {
|
|
237
|
+
errors.push(`prompt variable "${v}" not found in role "${roleName}" variant "${status}"`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Check status-edge consistency and mustache for each role. */
|
|
244
|
+
function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void {
|
|
245
|
+
for (const [roleName, role] of Object.entries(payload.roles)) {
|
|
246
|
+
if (RESERVED_NAMES.has(roleName)) continue;
|
|
247
|
+
const graphEntry = payload.graph[roleName];
|
|
248
|
+
if (!graphEntry) continue;
|
|
249
|
+
|
|
250
|
+
const fm = role.frontmatter as unknown;
|
|
251
|
+
const graphKeys = new Set(Object.keys(graphEntry));
|
|
252
|
+
|
|
253
|
+
if (isOneOfSchema(fm)) {
|
|
254
|
+
const variants = fm.oneOf as SchemaObj[];
|
|
255
|
+
const statuses = getOneOfStatuses(variants);
|
|
256
|
+
|
|
257
|
+
checkOneOfDiscriminant(roleName, variants, statuses, errors);
|
|
258
|
+
checkStatusEdges(roleName, graphKeys, new Set(statuses), errors);
|
|
259
|
+
checkMultiExitMustache(roleName, graphEntry, variants, errors);
|
|
260
|
+
} else if (hasStatusEnum(fm)) {
|
|
261
|
+
const statuses = getEnumStatuses(fm as SchemaObj);
|
|
262
|
+
checkStatusEdges(roleName, graphKeys, new Set(statuses), errors);
|
|
263
|
+
// For enum-based schemas, mustache vars come from the flat properties
|
|
264
|
+
checkEnumMustache(roleName, graphEntry, fm as SchemaObj, errors);
|
|
265
|
+
} else {
|
|
266
|
+
errors.push(
|
|
267
|
+
`role "${roleName}" must define "$status" as an enum (or oneOf const) in frontmatter`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Check mustache vars in all edge prompts against flat schema properties. */
|
|
274
|
+
function checkEnumMustache(
|
|
275
|
+
roleName: string,
|
|
276
|
+
graphEntry: Record<string, { role: string; prompt: string }>,
|
|
277
|
+
fm: SchemaObj,
|
|
278
|
+
errors: string[],
|
|
279
|
+
): void {
|
|
280
|
+
const propNames = getPropertyNames(fm);
|
|
281
|
+
for (const [status, target] of Object.entries(graphEntry)) {
|
|
282
|
+
const vars = extractMustacheVars(target.prompt);
|
|
283
|
+
for (const v of vars) {
|
|
284
|
+
if (v === "$status") continue;
|
|
285
|
+
if (!propNames.has(v)) {
|
|
286
|
+
errors.push(
|
|
287
|
+
`prompt variable "${v}" in graph[${roleName}][${status}] not found in role "${roleName}" frontmatter`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Validate a parsed WorkflowPayload for semantic correctness.
|
|
296
|
+
* Returns an array of error messages. Empty array = valid.
|
|
297
|
+
*/
|
|
298
|
+
export function validateWorkflow(payload: WorkflowPayload): string[] {
|
|
299
|
+
const errors: string[] = [];
|
|
300
|
+
checkRoleReferences(payload, errors);
|
|
301
|
+
checkGraphStructure(payload, errors);
|
|
302
|
+
checkRoleConsistency(payload, errors);
|
|
303
|
+
return errors;
|
|
304
|
+
}
|
package/src/validate.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { basename, dirname } from "node:path";
|
|
2
|
+
import type { CasRef, WorkflowPayload } from "@united-workforce/protocol";
|
|
3
|
+
|
|
4
|
+
const CAS_REF_PATTERN = /^[0-9A-HJKMNP-TV-Z]{13}$/;
|
|
5
|
+
|
|
6
|
+
export function isCasRef(value: string): value is CasRef {
|
|
7
|
+
return CAS_REF_PATTERN.test(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isRoleDefinition(value: unknown): boolean {
|
|
15
|
+
if (!isRecord(value)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const frontmatter = value.frontmatter;
|
|
19
|
+
const frontmatterOk =
|
|
20
|
+
isRecord(frontmatter) &&
|
|
21
|
+
(typeof frontmatter.type === "string" || Array.isArray(frontmatter.oneOf));
|
|
22
|
+
const capabilities = value.capabilities;
|
|
23
|
+
const capabilitiesOk =
|
|
24
|
+
Array.isArray(capabilities) && capabilities.every((c) => typeof c === "string");
|
|
25
|
+
return (
|
|
26
|
+
typeof value.description === "string" &&
|
|
27
|
+
typeof value.goal === "string" &&
|
|
28
|
+
capabilitiesOk &&
|
|
29
|
+
typeof value.procedure === "string" &&
|
|
30
|
+
typeof value.output === "string" &&
|
|
31
|
+
frontmatterOk
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isTarget(value: unknown): boolean {
|
|
36
|
+
if (!isRecord(value)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const hasValidLocation =
|
|
40
|
+
value.location === undefined || value.location === null || typeof value.location === "string";
|
|
41
|
+
return (
|
|
42
|
+
typeof value.role === "string" &&
|
|
43
|
+
typeof value.prompt === "string" &&
|
|
44
|
+
value.prompt.trim() !== "" &&
|
|
45
|
+
hasValidLocation
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isStringRecord(value: unknown, itemCheck: (item: unknown) => boolean): boolean {
|
|
50
|
+
if (!isRecord(value)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return Object.values(value).every(itemCheck);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isGraph(value: unknown): boolean {
|
|
57
|
+
if (!isRecord(value)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return Object.entries(value).every(([node, statusMap]) => {
|
|
61
|
+
if (!isRecord(statusMap)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return Object.entries(statusMap).every(([status, target]) => {
|
|
65
|
+
// "_" is only valid as a status key for the $START entry node.
|
|
66
|
+
if (status === "_" && node !== "$START") {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return isTarget(target);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Derive the expected workflow name from a file path (stem without extension).
|
|
76
|
+
* Returns the stem for `.yaml` / `.yml` files.
|
|
77
|
+
*/
|
|
78
|
+
export function workflowNameFromPath(filePath: string): string {
|
|
79
|
+
const base = basename(filePath);
|
|
80
|
+
const stem = base.endsWith(".yaml")
|
|
81
|
+
? base.slice(0, -5)
|
|
82
|
+
: base.endsWith(".yml")
|
|
83
|
+
? base.slice(0, -4)
|
|
84
|
+
: base;
|
|
85
|
+
if (stem === "index") {
|
|
86
|
+
return basename(dirname(filePath));
|
|
87
|
+
}
|
|
88
|
+
return stem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check that the `name` field in a parsed payload matches the expected name
|
|
93
|
+
* derived from the file path. Returns an error message string on mismatch,
|
|
94
|
+
* or null when the names are consistent.
|
|
95
|
+
*/
|
|
96
|
+
export function checkWorkflowFilenameConsistency(
|
|
97
|
+
filePath: string,
|
|
98
|
+
payload: WorkflowPayload,
|
|
99
|
+
): string | null {
|
|
100
|
+
const expected = workflowNameFromPath(filePath);
|
|
101
|
+
if (payload.name !== expected) {
|
|
102
|
+
return `workflow name mismatch: file "${basename(filePath)}" implies name "${expected}" but YAML declares name "${payload.name}"`;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Validate YAML-parsed workflow document shape (outputSchema may be inline JSON Schema). */
|
|
108
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: validation function with many field checks
|
|
109
|
+
export function parseWorkflowPayload(raw: unknown): WorkflowPayload | null {
|
|
110
|
+
if (!isRecord(raw)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (typeof raw.name !== "string" || typeof raw.description !== "string") {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (!isStringRecord(raw.roles, isRoleDefinition) || !isGraph(raw.graph)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Normalize location field: undefined → null
|
|
121
|
+
const normalized = { ...raw } as WorkflowPayload;
|
|
122
|
+
for (const roleName of Object.keys(normalized.graph)) {
|
|
123
|
+
const statusMap = normalized.graph[roleName];
|
|
124
|
+
if (statusMap !== undefined) {
|
|
125
|
+
for (const status of Object.keys(statusMap)) {
|
|
126
|
+
const target = statusMap[status];
|
|
127
|
+
if (target !== undefined) {
|
|
128
|
+
if (target.location === undefined) {
|
|
129
|
+
target.location = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return normalized;
|
|
137
|
+
}
|