@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
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-command text renderers — the per-command registry from spec
|
|
3
|
+
* `cli-format-text-renderer-registry.md`.
|
|
4
|
+
*
|
|
5
|
+
* Each renderer accepts the command's payload (already mapped via the
|
|
6
|
+
* output-mappers module) and returns a human-readable string. Renderers must
|
|
7
|
+
* never throw on partial/missing data and must never return `undefined`.
|
|
8
|
+
*
|
|
9
|
+
* Distinct from the existing Liquid template registry in `format.ts`: this
|
|
10
|
+
* registry is plain JS functions (the spec contract is
|
|
11
|
+
* `Record<string, (data: unknown) => string>`). The Liquid templates remain
|
|
12
|
+
* the primary rendering path inside `writeEnvelope`; these renderers are the
|
|
13
|
+
* fallback contract surface so callers can resolve `text` rendering without
|
|
14
|
+
* needing access to a CAS store.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
type ThreadListItem = {
|
|
18
|
+
threadId: string;
|
|
19
|
+
workflowHash: string;
|
|
20
|
+
workflowName: string | null;
|
|
21
|
+
status: string;
|
|
22
|
+
currentRole: string | null;
|
|
23
|
+
startedAt: number | null;
|
|
24
|
+
completedAt: number | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ThreadListPayload = { items: ThreadListItem[] };
|
|
28
|
+
|
|
29
|
+
type ThreadStatusPayload = {
|
|
30
|
+
threadId: string;
|
|
31
|
+
workflowHash: string;
|
|
32
|
+
head: string | null;
|
|
33
|
+
status: string;
|
|
34
|
+
currentRole: string | null;
|
|
35
|
+
suspendedRole: string | null;
|
|
36
|
+
suspendMessage: string | null;
|
|
37
|
+
done: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type ThreadStartPayload = {
|
|
41
|
+
threadId: string;
|
|
42
|
+
workflowHash: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type WorkflowListItem = {
|
|
46
|
+
name: string;
|
|
47
|
+
hash: string;
|
|
48
|
+
source: string;
|
|
49
|
+
description: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type WorkflowListPayload = { items: WorkflowListItem[] };
|
|
53
|
+
|
|
54
|
+
type WorkflowDetailPayload = {
|
|
55
|
+
name: string;
|
|
56
|
+
hash: string;
|
|
57
|
+
version: number;
|
|
58
|
+
description: string;
|
|
59
|
+
roles: Record<string, { description: string; goal: string }>;
|
|
60
|
+
graph: Record<string, Record<string, { role: string; prompt: string }>>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type StepListItem = {
|
|
64
|
+
hash: string;
|
|
65
|
+
role: string;
|
|
66
|
+
durationMs: number | null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type StepListPayload = {
|
|
70
|
+
threadId: string;
|
|
71
|
+
items: StepListItem[];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type ThreadCancelPayload = {
|
|
75
|
+
thread: string;
|
|
76
|
+
cancelled: boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ThreadStopPayload = {
|
|
80
|
+
thread: string;
|
|
81
|
+
stopped: boolean;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type StepDetailPayload = {
|
|
85
|
+
hash: string;
|
|
86
|
+
role: string;
|
|
87
|
+
agent: string;
|
|
88
|
+
status: string;
|
|
89
|
+
startedAtMs: number | null;
|
|
90
|
+
completedAtMs: number | null;
|
|
91
|
+
durationMs: number | null;
|
|
92
|
+
frontmatter: Record<string, unknown>;
|
|
93
|
+
turns: Array<{ role: string; content: string; timestamp: number | null }>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function asObject(data: unknown): Record<string, unknown> {
|
|
97
|
+
if (data !== null && typeof data === "object" && !Array.isArray(data)) {
|
|
98
|
+
return data as Record<string, unknown>;
|
|
99
|
+
}
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function asString(value: unknown, fallback = "-"): string {
|
|
104
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function asArray(value: unknown): unknown[] {
|
|
109
|
+
return Array.isArray(value) ? value : [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatDuration(durationMs: unknown): string {
|
|
113
|
+
if (typeof durationMs !== "number" || !Number.isFinite(durationMs)) return "-";
|
|
114
|
+
if (durationMs >= 1000) return `${(durationMs / 1000).toFixed(1)}s`;
|
|
115
|
+
return `${durationMs}ms`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function pad(s: string, width: number): string {
|
|
119
|
+
if (s.length >= width) return s.slice(0, width);
|
|
120
|
+
return s + " ".repeat(width - s.length);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatTimestamp(ts: unknown): string {
|
|
124
|
+
if (typeof ts !== "number" || !Number.isFinite(ts)) return "-";
|
|
125
|
+
const d = new Date(ts);
|
|
126
|
+
if (Number.isNaN(d.getTime())) return "-";
|
|
127
|
+
const pad2 = (n: number): string => n.toString().padStart(2, "0");
|
|
128
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function renderThreadList(data: unknown): string {
|
|
132
|
+
const payload = asObject(data) as Partial<ThreadListPayload>;
|
|
133
|
+
const items = asArray(payload.items) as ThreadListItem[];
|
|
134
|
+
const lines: string[] = [
|
|
135
|
+
"THREAD WORKFLOW STATUS ROLE STARTED",
|
|
136
|
+
];
|
|
137
|
+
for (const item of items) {
|
|
138
|
+
const it = asObject(item);
|
|
139
|
+
const threadId = asString(it.threadId);
|
|
140
|
+
const workflowHash = asString(it.workflowHash);
|
|
141
|
+
const status = pad(asString(it.status), 9);
|
|
142
|
+
const role = pad(asString(it.currentRole), 10);
|
|
143
|
+
const started = formatTimestamp(it.startedAt);
|
|
144
|
+
lines.push(`${threadId} ${workflowHash} ${status} ${role} ${started}`);
|
|
145
|
+
}
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function renderThreadShow(data: unknown): string {
|
|
150
|
+
const p = asObject(data) as Partial<ThreadStatusPayload>;
|
|
151
|
+
const status = asString(p.status);
|
|
152
|
+
const role =
|
|
153
|
+
status === "suspended" && typeof p.suspendedRole === "string" && p.suspendedRole.length > 0
|
|
154
|
+
? p.suspendedRole
|
|
155
|
+
: asString(p.currentRole);
|
|
156
|
+
const head = asString(p.head);
|
|
157
|
+
const lines = [
|
|
158
|
+
`Thread ${asString(p.threadId)}`,
|
|
159
|
+
`Workflow ${asString(p.workflowHash)}`,
|
|
160
|
+
`Status ${status}`,
|
|
161
|
+
`Role ${role}`,
|
|
162
|
+
`Head ${head}`,
|
|
163
|
+
];
|
|
164
|
+
if (
|
|
165
|
+
status === "suspended" &&
|
|
166
|
+
typeof p.suspendMessage === "string" &&
|
|
167
|
+
p.suspendMessage.length > 0
|
|
168
|
+
) {
|
|
169
|
+
lines.push(`Suspend ${p.suspendMessage}`);
|
|
170
|
+
}
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function renderThreadStart(data: unknown): string {
|
|
175
|
+
const p = asObject(data) as Partial<ThreadStartPayload>;
|
|
176
|
+
return `Thread ${asString(p.threadId)}\nWorkflow ${asString(p.workflowHash)}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function renderWorkflowList(data: unknown): string {
|
|
180
|
+
const payload = asObject(data) as Partial<WorkflowListPayload>;
|
|
181
|
+
const items = asArray(payload.items) as WorkflowListItem[];
|
|
182
|
+
const lines: string[] = ["NAME HASH SOURCE DESCRIPTION"];
|
|
183
|
+
for (const item of items) {
|
|
184
|
+
const it = asObject(item);
|
|
185
|
+
const name = pad(asString(it.name), 13);
|
|
186
|
+
const hash = asString(it.hash);
|
|
187
|
+
const source = pad(asString(it.source), 10);
|
|
188
|
+
const description = asString(it.description, "");
|
|
189
|
+
lines.push(`${name} ${hash} ${source} ${description}`);
|
|
190
|
+
}
|
|
191
|
+
return lines.join("\n");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function renderWorkflowShow(data: unknown): string {
|
|
195
|
+
const p = asObject(data) as Partial<WorkflowDetailPayload>;
|
|
196
|
+
const roles =
|
|
197
|
+
p.roles !== null && typeof p.roles === "object" && !Array.isArray(p.roles)
|
|
198
|
+
? Object.keys(p.roles)
|
|
199
|
+
: [];
|
|
200
|
+
const lines = [
|
|
201
|
+
`Workflow ${asString(p.name)}`,
|
|
202
|
+
`Version ${typeof p.version === "number" ? p.version : "-"}`,
|
|
203
|
+
`Hash ${asString(p.hash)}`,
|
|
204
|
+
`Roles ${roles.join(", ")}`,
|
|
205
|
+
];
|
|
206
|
+
if (typeof p.description === "string" && p.description.length > 0) {
|
|
207
|
+
lines.push(`Description ${p.description}`);
|
|
208
|
+
}
|
|
209
|
+
return lines.join("\n");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function renderStepList(data: unknown): string {
|
|
213
|
+
const payload = asObject(data) as Partial<StepListPayload>;
|
|
214
|
+
const items = asArray(payload.items) as StepListItem[];
|
|
215
|
+
const lines: string[] = ["HASH ROLE DURATION"];
|
|
216
|
+
for (const item of items) {
|
|
217
|
+
const it = asObject(item);
|
|
218
|
+
const hash = asString(it.hash);
|
|
219
|
+
const role = pad(asString(it.role), 10);
|
|
220
|
+
const dur = formatDuration(it.durationMs);
|
|
221
|
+
lines.push(`${hash} ${role} ${dur}`);
|
|
222
|
+
}
|
|
223
|
+
return lines.join("\n");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function renderStepShow(data: unknown): string {
|
|
227
|
+
const p = asObject(data) as Partial<StepDetailPayload>;
|
|
228
|
+
return [
|
|
229
|
+
`Step ${asString(p.hash)}`,
|
|
230
|
+
`Role ${asString(p.role)}`,
|
|
231
|
+
`Agent ${asString(p.agent)}`,
|
|
232
|
+
`Status ${asString(p.status)}`,
|
|
233
|
+
`Duration ${formatDuration(p.durationMs)}`,
|
|
234
|
+
].join("\n");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function renderThreadCancel(data: unknown): string {
|
|
238
|
+
const p = asObject(data) as Partial<ThreadCancelPayload>;
|
|
239
|
+
const cancelled = typeof p.cancelled === "boolean" ? (p.cancelled ? "yes" : "no") : "-";
|
|
240
|
+
return [
|
|
241
|
+
`Thread ${asString(p.thread)}`,
|
|
242
|
+
`Status cancelled`,
|
|
243
|
+
`Cancelled ${cancelled}`,
|
|
244
|
+
].join("\n");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function renderThreadStop(data: unknown): string {
|
|
248
|
+
const p = asObject(data) as Partial<ThreadStopPayload>;
|
|
249
|
+
const stopped = typeof p.stopped === "boolean" ? (p.stopped ? "yes" : "no") : "-";
|
|
250
|
+
return [`Thread ${asString(p.thread)}`, `Stopped ${stopped}`].join("\n");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Config renderers ────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Flatten a nested object into dot-notation key-value lines.
|
|
257
|
+
* Arrays are rendered as compact JSON; scalars as strings.
|
|
258
|
+
*/
|
|
259
|
+
function flattenConfig(obj: Record<string, unknown>, prefix: string): string[] {
|
|
260
|
+
const lines: string[] = [];
|
|
261
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
262
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
263
|
+
if (Array.isArray(value)) {
|
|
264
|
+
lines.push(`${fullKey}\t${JSON.stringify(value)}`);
|
|
265
|
+
} else if (value !== null && typeof value === "object") {
|
|
266
|
+
lines.push(...flattenConfig(value as Record<string, unknown>, fullKey));
|
|
267
|
+
} else {
|
|
268
|
+
lines.push(`${fullKey}\t${String(value)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return lines;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function renderConfigList(data: unknown): string {
|
|
275
|
+
const obj = asObject(data);
|
|
276
|
+
if (Object.keys(obj).length === 0) return "";
|
|
277
|
+
return flattenConfig(obj as Record<string, unknown>, "").join("\n");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function renderConfigGet(data: unknown): string {
|
|
281
|
+
const obj = asObject(data) as Record<string, unknown>;
|
|
282
|
+
const value = obj.value;
|
|
283
|
+
if (value === null || value === undefined) return "";
|
|
284
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
285
|
+
return flattenConfig(value as Record<string, unknown>, "").join("\n");
|
|
286
|
+
}
|
|
287
|
+
if (Array.isArray(value)) return JSON.stringify(value);
|
|
288
|
+
return String(value);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function renderConfigSet(data: unknown): string {
|
|
292
|
+
const obj = asObject(data) as Record<string, unknown>;
|
|
293
|
+
const key = asString(obj.key as string | undefined);
|
|
294
|
+
const value = obj.value;
|
|
295
|
+
const rendered = Array.isArray(value) ? JSON.stringify(value) : String(value ?? "");
|
|
296
|
+
return `${key} = ${rendered}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── Log renderers ───────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
type LogListItem = {
|
|
302
|
+
name: string;
|
|
303
|
+
size: number;
|
|
304
|
+
date: string;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
type LogEntry = {
|
|
308
|
+
ts: string;
|
|
309
|
+
pid: string;
|
|
310
|
+
tag: string;
|
|
311
|
+
msg: string;
|
|
312
|
+
thread: string | null;
|
|
313
|
+
workflow: string | null;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
function formatSize(bytes: unknown): string {
|
|
317
|
+
if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0) return "-";
|
|
318
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
319
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
|
|
320
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
321
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function renderLogList(data: unknown): string {
|
|
325
|
+
const items = asArray(data) as LogListItem[];
|
|
326
|
+
if (items.length === 0) return "No log files.";
|
|
327
|
+
const lines: string[] = ["DATE SIZE NAME"];
|
|
328
|
+
for (const item of items) {
|
|
329
|
+
const it = asObject(item);
|
|
330
|
+
const date = pad(asString(it.date), 11);
|
|
331
|
+
const size = pad(formatSize(it.size), 9);
|
|
332
|
+
const name = asString(it.name);
|
|
333
|
+
lines.push(`${date} ${size} ${name}`);
|
|
334
|
+
}
|
|
335
|
+
return lines.join("\n");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function renderLogShow(data: unknown): string {
|
|
339
|
+
const items = asArray(data) as LogEntry[];
|
|
340
|
+
if (items.length === 0) return "No log entries.";
|
|
341
|
+
const lines: string[] = [];
|
|
342
|
+
for (const item of items) {
|
|
343
|
+
const it = asObject(item);
|
|
344
|
+
const ts = asString(it.ts);
|
|
345
|
+
const pid = asString(it.pid);
|
|
346
|
+
const tag = asString(it.tag);
|
|
347
|
+
const msg = asString(it.msg, "");
|
|
348
|
+
const thread = typeof it.thread === "string" && it.thread.length > 0 ? it.thread : null;
|
|
349
|
+
const parts = [ts, `pid=${pid}`, tag];
|
|
350
|
+
if (thread !== null) parts.push(`thread=${thread}`);
|
|
351
|
+
parts.push(msg);
|
|
352
|
+
lines.push(parts.join(" "));
|
|
353
|
+
}
|
|
354
|
+
return lines.join("\n");
|
|
355
|
+
}
|
package/src/validate-semantic.ts
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import type { WorkflowPayload } from "@united-workforce/protocol";
|
|
2
|
+
import { Liquid } from "liquidjs";
|
|
2
3
|
|
|
3
4
|
type SchemaObj = Record<string, unknown>;
|
|
4
5
|
|
|
5
6
|
const RESERVED_NAMES = new Set(["$START", "$END", "$SUSPEND"]);
|
|
6
|
-
const PSEUDO_TARGETS = new Set(["$END"
|
|
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
|
-
}
|
|
7
|
+
const PSEUDO_TARGETS = new Set(["$END"]);
|
|
8
|
+
const SUSPEND_TARGET = "$SUSPEND";
|
|
19
9
|
|
|
20
10
|
/** Check if a frontmatter schema is a oneOf (multi-exit) type. */
|
|
21
11
|
function isOneOfSchema(fm: unknown): fm is SchemaObj & { oneOf: SchemaObj[] } {
|
|
@@ -42,13 +32,6 @@ function getConstStatuses(fm: SchemaObj): string[] {
|
|
|
42
32
|
return [];
|
|
43
33
|
}
|
|
44
34
|
|
|
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
35
|
/** Extract $status const values from oneOf variants. */
|
|
53
36
|
function getOneOfStatuses(variants: SchemaObj[]): string[] {
|
|
54
37
|
const statuses: string[] = [];
|
|
@@ -64,6 +47,76 @@ function getOneOfStatuses(variants: SchemaObj[]): string[] {
|
|
|
64
47
|
return statuses;
|
|
65
48
|
}
|
|
66
49
|
|
|
50
|
+
/** Generate mock data from schema property names for template rendering. */
|
|
51
|
+
function generateMockData(schema: SchemaObj): Record<string, string> {
|
|
52
|
+
const mock: Record<string, string> = {};
|
|
53
|
+
const props = schema.properties as Record<string, SchemaObj> | undefined;
|
|
54
|
+
if (!props) return mock;
|
|
55
|
+
for (const key of Object.keys(props)) {
|
|
56
|
+
mock[key] = `mock_${key}`;
|
|
57
|
+
}
|
|
58
|
+
// _body is engine-injected (markdown body after frontmatter), not in the schema.
|
|
59
|
+
// Always provide it so templates referencing {{ _body }} pass strict validation.
|
|
60
|
+
mock._body = "mock__body";
|
|
61
|
+
return mock;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Extract variable name from a LiquidJS UndefinedVariableError message. */
|
|
65
|
+
function extractVarName(err: unknown): string {
|
|
66
|
+
const msg = String(err);
|
|
67
|
+
const match = msg.match(/undefined variable: ([^,\s]+)/);
|
|
68
|
+
return match ? match[1] : "unknown";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Validate edge templates using LiquidJS strict-render for a multi-exit role. */
|
|
72
|
+
function validateMultiExitTemplates(
|
|
73
|
+
roleName: string,
|
|
74
|
+
graphEntry: Record<string, { role: string; prompt: string }>,
|
|
75
|
+
variants: SchemaObj[],
|
|
76
|
+
errors: string[],
|
|
77
|
+
): void {
|
|
78
|
+
const strictEngine = new Liquid({ strictVariables: true });
|
|
79
|
+
|
|
80
|
+
for (const [status, target] of Object.entries(graphEntry)) {
|
|
81
|
+
const variant = variants.find((v) => {
|
|
82
|
+
const props = v.properties as Record<string, SchemaObj> | undefined;
|
|
83
|
+
return props?.$status?.const === status;
|
|
84
|
+
});
|
|
85
|
+
if (!variant) continue;
|
|
86
|
+
const mockData = generateMockData(variant);
|
|
87
|
+
try {
|
|
88
|
+
strictEngine.parseAndRenderSync(target.prompt, mockData);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const varName = extractVarName(err);
|
|
91
|
+
errors.push(
|
|
92
|
+
`template variable "${varName}" not found in role "${roleName}" variant "${status}"`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Validate edge templates using LiquidJS strict-render for a flat schema. */
|
|
99
|
+
function validateFlatTemplates(
|
|
100
|
+
roleName: string,
|
|
101
|
+
graphEntry: Record<string, { role: string; prompt: string }>,
|
|
102
|
+
fm: SchemaObj,
|
|
103
|
+
errors: string[],
|
|
104
|
+
): void {
|
|
105
|
+
const strictEngine = new Liquid({ strictVariables: true });
|
|
106
|
+
const mockData = generateMockData(fm);
|
|
107
|
+
|
|
108
|
+
for (const [status, target] of Object.entries(graphEntry)) {
|
|
109
|
+
try {
|
|
110
|
+
strictEngine.parseAndRenderSync(target.prompt, mockData);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const varName = extractVarName(err);
|
|
113
|
+
errors.push(
|
|
114
|
+
`template variable "${varName}" in graph[${roleName}][${status}] not found in role "${roleName}" frontmatter`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
67
120
|
/** Check reserved names and role/graph reference integrity. */
|
|
68
121
|
function checkRoleReferences(payload: WorkflowPayload, errors: string[]): void {
|
|
69
122
|
const roleNames = new Set(Object.keys(payload.roles));
|
|
@@ -89,6 +142,27 @@ function checkRoleReferences(payload: WorkflowPayload, errors: string[]): void {
|
|
|
89
142
|
}
|
|
90
143
|
}
|
|
91
144
|
|
|
145
|
+
/** Validate each graph edge's target role, including the removed $SUSPEND target. */
|
|
146
|
+
function checkEdgeTargets(
|
|
147
|
+
payload: WorkflowPayload,
|
|
148
|
+
roleNames: Set<string>,
|
|
149
|
+
errors: string[],
|
|
150
|
+
): void {
|
|
151
|
+
for (const [node, statusMap] of Object.entries(payload.graph)) {
|
|
152
|
+
for (const [status, target] of Object.entries(statusMap)) {
|
|
153
|
+
if (target.role === SUSPEND_TARGET) {
|
|
154
|
+
errors.push(
|
|
155
|
+
`edge ${node}→${status}: "${SUSPEND_TARGET}" is no longer a valid graph target. Emit $status: "${SUSPEND_TARGET}" from the "${node}" role output instead.`,
|
|
156
|
+
);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!PSEUDO_TARGETS.has(target.role) && !roleNames.has(target.role)) {
|
|
160
|
+
errors.push(`edge ${node}→${status}: unknown target role "${target.role}"`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
92
166
|
/** Check $START/$END constraints, edge targets, and reachability. */
|
|
93
167
|
function checkGraphStructure(payload: WorkflowPayload, errors: string[]): void {
|
|
94
168
|
const roleNames = new Set(Object.keys(payload.roles));
|
|
@@ -107,17 +181,13 @@ function checkGraphStructure(payload: WorkflowPayload, errors: string[]): void {
|
|
|
107
181
|
errors.push("$END must not have outgoing edges");
|
|
108
182
|
}
|
|
109
183
|
|
|
110
|
-
if (graphNodes.has(
|
|
111
|
-
errors.push(
|
|
184
|
+
if (graphNodes.has(SUSPEND_TARGET)) {
|
|
185
|
+
errors.push(
|
|
186
|
+
`"${SUSPEND_TARGET}" is no longer a valid graph node — it is now an engine-level reserved $status. Emit $status: "${SUSPEND_TARGET}" from a role output instead.`,
|
|
187
|
+
);
|
|
112
188
|
}
|
|
113
189
|
|
|
114
|
-
|
|
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
|
-
}
|
|
190
|
+
checkEdgeTargets(payload, roleNames, errors);
|
|
121
191
|
|
|
122
192
|
checkReachability(roleNames, collectReachableRoles(payload.graph), errors);
|
|
123
193
|
}
|
|
@@ -207,31 +277,33 @@ function checkStatusEdges(
|
|
|
207
277
|
}
|
|
208
278
|
}
|
|
209
279
|
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (
|
|
228
|
-
errors.push(
|
|
280
|
+
/** Reserved property names that must not appear in frontmatter schemas. */
|
|
281
|
+
const RESERVED_PROPERTIES = new Set(["_body"]);
|
|
282
|
+
|
|
283
|
+
/** Check that frontmatter schemas do not define reserved property names. */
|
|
284
|
+
function checkReservedProperties(roleName: string, fm: unknown, errors: string[]): void {
|
|
285
|
+
const schemasToCheck: SchemaObj[] = [];
|
|
286
|
+
|
|
287
|
+
if (isOneOfSchema(fm)) {
|
|
288
|
+
schemasToCheck.push(...(fm.oneOf as SchemaObj[]));
|
|
289
|
+
} else if (typeof fm === "object" && fm !== null) {
|
|
290
|
+
schemasToCheck.push(fm as SchemaObj);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const schema of schemasToCheck) {
|
|
294
|
+
const props = schema.properties as Record<string, unknown> | undefined;
|
|
295
|
+
if (!props) continue;
|
|
296
|
+
for (const key of Object.keys(props)) {
|
|
297
|
+
if (RESERVED_PROPERTIES.has(key)) {
|
|
298
|
+
errors.push(
|
|
299
|
+
`role "${roleName}" frontmatter must not define reserved property "${key}" — it is injected by the engine`,
|
|
300
|
+
);
|
|
229
301
|
}
|
|
230
302
|
}
|
|
231
303
|
}
|
|
232
304
|
}
|
|
233
305
|
|
|
234
|
-
/** Check status-edge consistency and
|
|
306
|
+
/** Check status-edge consistency and template vars for each role. */
|
|
235
307
|
function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void {
|
|
236
308
|
for (const [roleName, role] of Object.entries(payload.roles)) {
|
|
237
309
|
if (RESERVED_NAMES.has(roleName)) continue;
|
|
@@ -241,18 +313,19 @@ function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void
|
|
|
241
313
|
const fm = role.frontmatter as unknown;
|
|
242
314
|
const graphKeys = new Set(Object.keys(graphEntry));
|
|
243
315
|
|
|
316
|
+
checkReservedProperties(roleName, fm, errors);
|
|
317
|
+
|
|
244
318
|
if (isOneOfSchema(fm)) {
|
|
245
319
|
const variants = fm.oneOf as SchemaObj[];
|
|
246
320
|
const statuses = getOneOfStatuses(variants);
|
|
247
321
|
|
|
248
322
|
checkOneOfDiscriminant(roleName, variants, statuses, errors);
|
|
249
323
|
checkStatusEdges(roleName, graphKeys, new Set(statuses), errors);
|
|
250
|
-
|
|
324
|
+
validateMultiExitTemplates(roleName, graphEntry, variants, errors);
|
|
251
325
|
} else if (hasStatusConst(fm)) {
|
|
252
326
|
const statuses = getConstStatuses(fm as SchemaObj);
|
|
253
327
|
checkStatusEdges(roleName, graphKeys, new Set(statuses), errors);
|
|
254
|
-
|
|
255
|
-
checkFlatMustache(roleName, graphEntry, fm as SchemaObj, errors);
|
|
328
|
+
validateFlatTemplates(roleName, graphEntry, fm as SchemaObj, errors);
|
|
256
329
|
} else {
|
|
257
330
|
errors.push(
|
|
258
331
|
`role "${roleName}" must define "$status" as const (or oneOf with const) in frontmatter`,
|
|
@@ -261,27 +334,6 @@ function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void
|
|
|
261
334
|
}
|
|
262
335
|
}
|
|
263
336
|
|
|
264
|
-
/** Check mustache vars in all edge prompts against flat schema properties. */
|
|
265
|
-
function checkFlatMustache(
|
|
266
|
-
roleName: string,
|
|
267
|
-
graphEntry: Record<string, { role: string; prompt: string }>,
|
|
268
|
-
fm: SchemaObj,
|
|
269
|
-
errors: string[],
|
|
270
|
-
): void {
|
|
271
|
-
const propNames = getPropertyNames(fm);
|
|
272
|
-
for (const [status, target] of Object.entries(graphEntry)) {
|
|
273
|
-
const vars = extractMustacheVars(target.prompt);
|
|
274
|
-
for (const v of vars) {
|
|
275
|
-
if (v === "$status") continue;
|
|
276
|
-
if (!propNames.has(v)) {
|
|
277
|
-
errors.push(
|
|
278
|
-
`prompt variable "${v}" in graph[${roleName}][${status}] not found in role "${roleName}" frontmatter`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
337
|
/**
|
|
286
338
|
* Validate a parsed WorkflowPayload for semantic correctness.
|
|
287
339
|
* Returns an array of error messages. Empty array = valid.
|
package/src/validate.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { basename, dirname } from "node:path";
|
|
2
2
|
import type { CasRef, WorkflowPayload } from "@united-workforce/protocol";
|
|
3
|
+
import { CURRENT_WORKFLOW_VERSION } from "@united-workforce/protocol";
|
|
3
4
|
|
|
4
5
|
const CAS_REF_PATTERN = /^[0-9A-HJKMNP-TV-Z]{13}$/;
|
|
5
6
|
|
|
@@ -113,12 +114,26 @@ export function parseWorkflowPayload(raw: unknown): WorkflowPayload | null {
|
|
|
113
114
|
if (typeof raw.name !== "string" || typeof raw.description !== "string") {
|
|
114
115
|
return null;
|
|
115
116
|
}
|
|
117
|
+
// version is optional in legacy YAML — falls back to CURRENT_WORKFLOW_VERSION.
|
|
118
|
+
// When present, it MUST be an integer (booleans, strings, floats are rejected).
|
|
119
|
+
if (raw.version !== undefined) {
|
|
120
|
+
if (
|
|
121
|
+
typeof raw.version !== "number" ||
|
|
122
|
+
!Number.isInteger(raw.version) ||
|
|
123
|
+
typeof raw.version === "boolean"
|
|
124
|
+
) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
116
128
|
if (!isStringRecord(raw.roles, isRoleDefinition) || !isGraph(raw.graph)) {
|
|
117
129
|
return null;
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
// Normalize location field: undefined → null
|
|
121
133
|
const normalized = { ...raw } as WorkflowPayload;
|
|
134
|
+
if (normalized.version === undefined || normalized.version === null) {
|
|
135
|
+
normalized.version = CURRENT_WORKFLOW_VERSION;
|
|
136
|
+
}
|
|
122
137
|
for (const roleName of Object.keys(normalized.graph)) {
|
|
123
138
|
const statusMap = normalized.graph[roleName];
|
|
124
139
|
if (statusMap !== undefined) {
|
|
@@ -135,3 +150,15 @@ export function parseWorkflowPayload(raw: unknown): WorkflowPayload | null {
|
|
|
135
150
|
|
|
136
151
|
return normalized;
|
|
137
152
|
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Returns true when the parsed YAML document had no top-level `version` field.
|
|
156
|
+
* Used by `uwf workflow add` to emit a deprecation warning while still
|
|
157
|
+
* accepting legacy workflow YAML.
|
|
158
|
+
*/
|
|
159
|
+
export function isMissingVersion(raw: unknown): boolean {
|
|
160
|
+
if (!isRecord(raw)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return raw.version === undefined;
|
|
164
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"setup-validate.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/setup-validate.test.ts"],"names":[],"mappings":""}
|