@united-workforce/cli 0.4.0 → 0.6.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 +30 -3
- package/dist/.build-fingerprint +1 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js +16 -6
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
- 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-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/config-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/config-text-renderer.test.js +137 -0
- package/dist/__tests__/config-text-renderer.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +23 -7
- 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.js +1 -1
- 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__/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.js +9 -7
- package/dist/__tests__/pid-recycling.test.js.map +1 -1
- package/dist/__tests__/prompt.test.js +46 -4
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +8 -0
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
- package/dist/__tests__/solve-issue-tea-worktree.test.js +3 -1
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.js +9 -1
- package/dist/__tests__/step-ask.test.js.map +1 -1
- package/dist/__tests__/store-unified-threads.test.js +19 -17
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-agent-failure-suspended.test.d.ts +2 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.d.ts.map +1 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.js +332 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -0
- package/dist/__tests__/thread-cancel-status.test.js +19 -13
- 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-join.test.d.ts +2 -0
- package/dist/__tests__/thread-join.test.d.ts.map +1 -0
- package/dist/__tests__/thread-join.test.js +77 -0
- package/dist/__tests__/thread-join.test.js.map +1 -0
- package/dist/__tests__/thread-list-filters.test.js +10 -8
- 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.js +15 -2
- package/dist/__tests__/thread-poke.test.js.map +1 -1
- 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 +11 -1
- package/dist/__tests__/thread-resume.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 +5 -2
- package/dist/__tests__/thread-suspend-step.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 +11 -9
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +56 -2
- package/dist/__tests__/validate-semantic.test.js.map +1 -1
- package/dist/__tests__/workflow-list-recursive.test.js +10 -7
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -1
- package/dist/__tests__/workflow-paths.test.d.ts +2 -0
- package/dist/__tests__/workflow-paths.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-paths.test.js +261 -0
- package/dist/__tests__/workflow-paths.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +10 -7
- package/dist/__tests__/workflow-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-show-resolution.test.js +10 -7
- package/dist/__tests__/workflow-show-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-validate.test.js +75 -55
- package/dist/__tests__/workflow-validate.test.js.map +1 -1
- 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/cli.js +76 -36
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +81 -3
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +42 -29
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +9 -4
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +51 -7
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/thread.d.ts +12 -0
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +226 -9
- package/dist/commands/thread.js.map +1 -1
- package/dist/commands/workflow.d.ts +2 -2
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +26 -10
- 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/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 +4 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +31 -4
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +11 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +20 -1
- 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 +28 -11
- package/dist/validate-semantic.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 +12 -11
- package/src/__tests__/adapter-json-roundtrip.test.ts +15 -6
- package/src/__tests__/concurrency.test.ts +266 -0
- package/src/__tests__/config-text-renderer.test.ts +156 -0
- package/src/__tests__/e2e-mock-agent.test.ts +45 -7
- 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 +1 -1
- package/src/__tests__/log-text-renderer.test.ts +294 -0
- 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 +9 -8
- package/src/__tests__/prompt.test.ts +48 -4
- package/src/__tests__/resolve-head-hash.test.ts +7 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +3 -1
- package/src/__tests__/step-ask.test.ts +8 -1
- package/src/__tests__/store-unified-threads.test.ts +21 -18
- package/src/__tests__/thread-agent-failure-suspended.test.ts +406 -0
- package/src/__tests__/thread-cancel-status.test.ts +21 -14
- package/src/__tests__/thread-cancel-text-renderer.test.ts +125 -0
- package/src/__tests__/thread-join.test.ts +103 -0
- package/src/__tests__/thread-list-filters.test.ts +9 -9
- 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 +14 -2
- package/src/__tests__/thread-read-xml-tags.test.ts +9 -11
- package/src/__tests__/thread-resume.test.ts +10 -1
- 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 +5 -2
- package/src/__tests__/thread-test-helpers.ts +15 -1
- package/src/__tests__/thread.test.ts +10 -10
- package/src/__tests__/validate-semantic.test.ts +59 -2
- package/src/__tests__/workflow-list-recursive.test.ts +9 -9
- package/src/__tests__/workflow-paths.test.ts +337 -0
- package/src/__tests__/workflow-resolution.test.ts +9 -8
- package/src/__tests__/workflow-show-resolution.test.ts +9 -8
- package/src/__tests__/workflow-validate.test.ts +78 -56
- package/src/__tests__/write-envelope.test.ts +257 -0
- package/src/cli.ts +111 -35
- package/src/commands/config.ts +85 -3
- package/src/commands/prompt.ts +42 -29
- package/src/commands/setup.ts +57 -7
- package/src/commands/thread.ts +280 -9
- package/src/commands/workflow.ts +32 -11
- 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/output-mappers.ts +255 -0
- package/src/schemas.ts +39 -3
- package/src/store.ts +25 -1
- package/src/text-renderers.ts +355 -0
- package/src/validate-semantic.ts +33 -12
- package/LICENSE +0 -21
package/src/commands/thread.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
isThreadRunning,
|
|
44
44
|
readMarker,
|
|
45
45
|
} from "../background/index.js";
|
|
46
|
+
import { acquireSlot, DEFAULT_MAX_RUNNING, installSlotCleanup } from "../concurrency/index.js";
|
|
46
47
|
import { createIncludeTag } from "../include.js";
|
|
47
48
|
import { evaluate } from "../moderator/index.js";
|
|
48
49
|
import {
|
|
@@ -60,6 +61,13 @@ import {
|
|
|
60
61
|
} from "../store.js";
|
|
61
62
|
import { checkWorkflowFilenameConsistency, isCasRef, parseWorkflowPayload } from "../validate.js";
|
|
62
63
|
import { validateWorkflow } from "../validate-semantic.js";
|
|
64
|
+
import {
|
|
65
|
+
getConfigPath,
|
|
66
|
+
getNestedValue,
|
|
67
|
+
loadConfig,
|
|
68
|
+
loadWorkflowPaths,
|
|
69
|
+
parseDotPath,
|
|
70
|
+
} from "./config.js";
|
|
63
71
|
import {
|
|
64
72
|
type ChainState,
|
|
65
73
|
collectOrderedSteps,
|
|
@@ -166,6 +174,13 @@ async function resolveActiveThreadStatus(
|
|
|
166
174
|
return "running";
|
|
167
175
|
}
|
|
168
176
|
|
|
177
|
+
// Check the persisted entry status first — agent failure suspends the thread
|
|
178
|
+
// via markThreadSuspended() without producing a $SUSPEND output in CAS.
|
|
179
|
+
const entry = getThread(uwf.varStore, threadId);
|
|
180
|
+
if (entry !== null && entry.status === "suspended") {
|
|
181
|
+
return "suspended";
|
|
182
|
+
}
|
|
183
|
+
|
|
169
184
|
const chain = walkChain(uwf, head);
|
|
170
185
|
const { lastOutput } = resolveEvaluateArgs(uwf, chain);
|
|
171
186
|
if (readSuspendReason(lastOutput) !== null) {
|
|
@@ -225,9 +240,20 @@ function buildResumePrompt(graphPrompt: string, supplement: string | null): stri
|
|
|
225
240
|
return `${graphPrompt}\n\n${supplement}`;
|
|
226
241
|
}
|
|
227
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Error thrown by failStep so that callers can catch it, persist
|
|
245
|
+
* thread state (e.g. suspend), and then re-throw / exit.
|
|
246
|
+
*/
|
|
247
|
+
class StepFailureError extends Error {
|
|
248
|
+
constructor(message: string) {
|
|
249
|
+
super(message);
|
|
250
|
+
this.name = "StepFailureError";
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
228
254
|
function failStep(plog: ProcessLogger, message: string): never {
|
|
229
255
|
plog.log(PL_STEP_ERROR, message, null);
|
|
230
|
-
|
|
256
|
+
throw new StepFailureError(message);
|
|
231
257
|
}
|
|
232
258
|
|
|
233
259
|
/**
|
|
@@ -344,6 +370,49 @@ async function findWorkflowInParents(startDir: string, name: string): Promise<st
|
|
|
344
370
|
return null;
|
|
345
371
|
}
|
|
346
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Search for a workflow by name directly in a directory (not inside .workflows/).
|
|
375
|
+
* Used for workflowPaths resolution — each path dir contains YAMLs at top level.
|
|
376
|
+
* Checks flat files (<name>.yaml/.yml) and folder layout (<name>/index.yaml/.yml).
|
|
377
|
+
*/
|
|
378
|
+
async function findWorkflowInPath(dir: string, name: string): Promise<string | null> {
|
|
379
|
+
// Check flat YAML files
|
|
380
|
+
for (const ext of [".yaml", ".yml"]) {
|
|
381
|
+
const result = await workflowFileExists(dir, name, ext);
|
|
382
|
+
if (result !== null) {
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Check folder-based layout (<name>/index.yaml)
|
|
387
|
+
for (const indexName of ["index.yaml", "index.yml"]) {
|
|
388
|
+
const candidate = resolvePath(dir, name, indexName);
|
|
389
|
+
try {
|
|
390
|
+
await access(candidate);
|
|
391
|
+
return candidate;
|
|
392
|
+
} catch {
|
|
393
|
+
/* not found */
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Search workflowPaths directories for a workflow by name.
|
|
401
|
+
* Searches each directory in order; first match wins.
|
|
402
|
+
*/
|
|
403
|
+
async function findWorkflowInPaths(
|
|
404
|
+
dirs: ReadonlyArray<string>,
|
|
405
|
+
name: string,
|
|
406
|
+
): Promise<string | null> {
|
|
407
|
+
for (const dir of dirs) {
|
|
408
|
+
const found = await findWorkflowInPath(dir, name);
|
|
409
|
+
if (found !== null) {
|
|
410
|
+
return found;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
347
416
|
async function materializeLocalWorkflow(uwf: UwfStore, filePath: string): Promise<CasRef> {
|
|
348
417
|
let text: string;
|
|
349
418
|
try {
|
|
@@ -419,6 +488,13 @@ async function resolveWorkflowCasRef(
|
|
|
419
488
|
return materializeLocalWorkflow(uwf, localPath);
|
|
420
489
|
}
|
|
421
490
|
|
|
491
|
+
// Strategy 3.5: workflowPaths global directories
|
|
492
|
+
const workflowPaths = loadWorkflowPaths(uwf.storageRoot);
|
|
493
|
+
const pathsFile = await findWorkflowInPaths(workflowPaths, trimmed);
|
|
494
|
+
if (pathsFile !== null) {
|
|
495
|
+
return materializeLocalWorkflow(uwf, pathsFile);
|
|
496
|
+
}
|
|
497
|
+
|
|
422
498
|
// Strategy 4: Global registry fallback
|
|
423
499
|
const registry = loadWorkflowRegistry(uwf.varStore);
|
|
424
500
|
const hash = resolveWorkflowHash(registry, trimmed);
|
|
@@ -992,6 +1068,15 @@ type EvaluateLastOutput = Record<string, unknown>;
|
|
|
992
1068
|
|
|
993
1069
|
const STATUS_KEY = "$status";
|
|
994
1070
|
|
|
1071
|
+
/**
|
|
1072
|
+
* Strip YAML frontmatter (---...---) from a raw markdown string,
|
|
1073
|
+
* returning only the body portion.
|
|
1074
|
+
*/
|
|
1075
|
+
function stripFrontmatter(raw: string): string {
|
|
1076
|
+
const match = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
1077
|
+
return match ? raw.slice(match[0].length).trim() : raw.trim();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
995
1080
|
function resolveEvaluateArgs(
|
|
996
1081
|
uwf: UwfStore,
|
|
997
1082
|
chain: ChainState,
|
|
@@ -1011,6 +1096,13 @@ function resolveEvaluateArgs(
|
|
|
1011
1096
|
? (raw as Record<string, unknown>)
|
|
1012
1097
|
: {};
|
|
1013
1098
|
|
|
1099
|
+
// Inject _body — the markdown body (after frontmatter) from the last step's
|
|
1100
|
+
// assistant output. Workflow edge prompts can reference it via {{ _body }}.
|
|
1101
|
+
const content = extractLastAssistantContent(uwf, lastStep.detail);
|
|
1102
|
+
if (content !== null) {
|
|
1103
|
+
base._body = stripFrontmatter(content);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1014
1106
|
return {
|
|
1015
1107
|
lastRole: lastStep.role,
|
|
1016
1108
|
lastOutput: base,
|
|
@@ -1020,10 +1112,10 @@ function resolveEvaluateArgs(
|
|
|
1020
1112
|
function loadWorkflowPayload(uwf: UwfStore, workflowRef: CasRef): WorkflowPayload {
|
|
1021
1113
|
const node = uwf.store.cas.get(workflowRef);
|
|
1022
1114
|
if (node === null) {
|
|
1023
|
-
|
|
1115
|
+
throw new Error(`workflow CAS node not found: ${workflowRef}`);
|
|
1024
1116
|
}
|
|
1025
1117
|
if (node.type !== uwf.schemas.workflow) {
|
|
1026
|
-
|
|
1118
|
+
throw new Error(`node ${workflowRef} is not a Workflow`);
|
|
1027
1119
|
}
|
|
1028
1120
|
return node.payload as WorkflowPayload;
|
|
1029
1121
|
}
|
|
@@ -1360,7 +1452,18 @@ export async function cmdThreadPoke(
|
|
|
1360
1452
|
// Spawn the agent. The agent will create a new StepNode with prev=oldHead (it reads
|
|
1361
1453
|
// the active thread head). After the agent returns, we rewrite that node's prev so
|
|
1362
1454
|
// that the new head replaces the old head instead of appending after it.
|
|
1363
|
-
|
|
1455
|
+
let agentResult: AdapterOutput;
|
|
1456
|
+
try {
|
|
1457
|
+
agentResult = spawnAgent(plog, agent, threadId, role, prompt, effectiveCwd);
|
|
1458
|
+
} catch (e) {
|
|
1459
|
+
if (e instanceof StepFailureError) {
|
|
1460
|
+
// Fatal agent failure in poke — persist suspended state before propagating
|
|
1461
|
+
const uwfErr = await createUwfStore(storageRoot);
|
|
1462
|
+
const errEntry = getThread(uwfErr.varStore, threadId) ?? entry;
|
|
1463
|
+
setThread(uwfErr.varStore, threadId, markThreadSuspended(errEntry, role, e.message));
|
|
1464
|
+
}
|
|
1465
|
+
throw e;
|
|
1466
|
+
}
|
|
1364
1467
|
const agentStepHash = agentResult.stepHash as CasRef;
|
|
1365
1468
|
|
|
1366
1469
|
plog.log(PL_AGENT_DONE, `agent returned head=${agentStepHash}`, null);
|
|
@@ -1406,6 +1509,25 @@ export function validateCount(count: number): void {
|
|
|
1406
1509
|
}
|
|
1407
1510
|
}
|
|
1408
1511
|
|
|
1512
|
+
/**
|
|
1513
|
+
* Resolve the effective maxRunning limit.
|
|
1514
|
+
* Priority: config file > DEFAULT_MAX_RUNNING (2).
|
|
1515
|
+
*/
|
|
1516
|
+
async function resolveMaxRunning(storageRoot: string): Promise<number> {
|
|
1517
|
+
try {
|
|
1518
|
+
const configPath = getConfigPath(storageRoot);
|
|
1519
|
+
const config = loadConfig(configPath);
|
|
1520
|
+
const path = parseDotPath("concurrency.maxRunning");
|
|
1521
|
+
const value = getNestedValue(config, path);
|
|
1522
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
1523
|
+
return value;
|
|
1524
|
+
}
|
|
1525
|
+
} catch {
|
|
1526
|
+
// Config file missing or invalid — fall through to default
|
|
1527
|
+
}
|
|
1528
|
+
return DEFAULT_MAX_RUNNING;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1409
1531
|
export async function cmdThreadExec(
|
|
1410
1532
|
storageRoot: string,
|
|
1411
1533
|
threadId: ThreadId,
|
|
@@ -1446,6 +1568,13 @@ export async function cmdThreadExec(
|
|
|
1446
1568
|
processStartTime: getProcessStartTime(process.pid),
|
|
1447
1569
|
});
|
|
1448
1570
|
|
|
1571
|
+
// Resolve concurrency limit: config > default
|
|
1572
|
+
const effectiveMaxRunning = await resolveMaxRunning(storageRoot);
|
|
1573
|
+
|
|
1574
|
+
// Acquire concurrency slot (blocks if at capacity)
|
|
1575
|
+
const slotHandle = await acquireSlot(storageRoot, effectiveMaxRunning);
|
|
1576
|
+
const uninstallCleanup = installSlotCleanup(slotHandle);
|
|
1577
|
+
|
|
1449
1578
|
try {
|
|
1450
1579
|
const results: StepOutput[] = [];
|
|
1451
1580
|
for (let i = 0; i < count; i++) {
|
|
@@ -1457,10 +1586,101 @@ export async function cmdThreadExec(
|
|
|
1457
1586
|
}
|
|
1458
1587
|
return results;
|
|
1459
1588
|
} finally {
|
|
1589
|
+
uninstallCleanup();
|
|
1590
|
+
await slotHandle.release();
|
|
1460
1591
|
await deleteMarker(storageRoot, threadId);
|
|
1461
1592
|
}
|
|
1462
1593
|
}
|
|
1463
1594
|
|
|
1595
|
+
const JOIN_POLL_INTERVAL_MS = 1000;
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* Block until a running thread finishes (marker disappears), then return the
|
|
1599
|
+
* final thread state in the same `StepOutput[]` format that `cmdThreadExec`
|
|
1600
|
+
* produces.
|
|
1601
|
+
*
|
|
1602
|
+
* - If the thread is currently running → poll until it stops, then return
|
|
1603
|
+
* its final state.
|
|
1604
|
+
* - If the thread is not running → return its current state immediately.
|
|
1605
|
+
*
|
|
1606
|
+
* An optional `timeoutMs` aborts the wait with an error when exceeded.
|
|
1607
|
+
*/
|
|
1608
|
+
export async function cmdThreadJoin(
|
|
1609
|
+
storageRoot: string,
|
|
1610
|
+
threadId: ThreadId,
|
|
1611
|
+
timeoutMs: number | null,
|
|
1612
|
+
): Promise<StepOutput[]> {
|
|
1613
|
+
const uwf = await createUwfStore(storageRoot);
|
|
1614
|
+
const entry = getThread(uwf.varStore, threadId);
|
|
1615
|
+
if (entry === null) {
|
|
1616
|
+
fail(`thread not found: ${threadId}`);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// Wait for running marker to disappear
|
|
1620
|
+
const deadline = timeoutMs !== null ? Date.now() + timeoutMs : null;
|
|
1621
|
+
|
|
1622
|
+
while ((await isThreadRunning(storageRoot, threadId)) !== null) {
|
|
1623
|
+
if (deadline !== null && Date.now() >= deadline) {
|
|
1624
|
+
fail(`join timed out after ${timeoutMs}ms — thread ${threadId} is still running`);
|
|
1625
|
+
}
|
|
1626
|
+
await new Promise<void>((resolve) => {
|
|
1627
|
+
setTimeout(resolve, JOIN_POLL_INTERVAL_MS);
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Thread is no longer running — read final state.
|
|
1632
|
+
// Re-open the store to get the latest state written by the worker.
|
|
1633
|
+
const freshUwf = await createUwfStore(storageRoot);
|
|
1634
|
+
const freshEntry = getThread(freshUwf.varStore, threadId);
|
|
1635
|
+
if (freshEntry === null) {
|
|
1636
|
+
fail(`thread disappeared after join: ${threadId}`);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
const activeHead = freshEntry.head;
|
|
1640
|
+
const workflowHash = resolveWorkflowFromHead(freshUwf, activeHead);
|
|
1641
|
+
if (workflowHash === null) {
|
|
1642
|
+
fail(`failed to resolve workflow from head: ${activeHead}`);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// Build the StepOutput matching exec's format
|
|
1646
|
+
if (freshEntry.status === "end" || freshEntry.status === "cancelled") {
|
|
1647
|
+
return [
|
|
1648
|
+
{
|
|
1649
|
+
workflow: workflowHash,
|
|
1650
|
+
thread: threadId,
|
|
1651
|
+
head: activeHead,
|
|
1652
|
+
status: freshEntry.status,
|
|
1653
|
+
currentRole: null,
|
|
1654
|
+
suspendedRole: null,
|
|
1655
|
+
suspendMessage: null,
|
|
1656
|
+
done: true,
|
|
1657
|
+
background: null,
|
|
1658
|
+
error: null,
|
|
1659
|
+
},
|
|
1660
|
+
];
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Active thread — resolve detailed status
|
|
1664
|
+
const status = await resolveActiveThreadStatus(storageRoot, threadId, freshUwf, activeHead);
|
|
1665
|
+
const currentRole = resolveCurrentRole(freshUwf, activeHead, workflowHash);
|
|
1666
|
+
const suspendFields = resolveSuspendFieldsForShow(freshEntry, status, freshUwf, activeHead);
|
|
1667
|
+
|
|
1668
|
+
return [
|
|
1669
|
+
{
|
|
1670
|
+
workflow: workflowHash,
|
|
1671
|
+
thread: threadId,
|
|
1672
|
+
head: activeHead,
|
|
1673
|
+
status,
|
|
1674
|
+
currentRole,
|
|
1675
|
+
suspendedRole: suspendFields.suspendedRole,
|
|
1676
|
+
suspendMessage: suspendFields.suspendMessage,
|
|
1677
|
+
done: false,
|
|
1678
|
+
background: null,
|
|
1679
|
+
error: null,
|
|
1680
|
+
},
|
|
1681
|
+
];
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1464
1684
|
async function resolveActiveThreadWorkflowHash(
|
|
1465
1685
|
storageRoot: string,
|
|
1466
1686
|
threadId: ThreadId,
|
|
@@ -1715,6 +1935,51 @@ async function cmdThreadStepOnce(
|
|
|
1715
1935
|
});
|
|
1716
1936
|
|
|
1717
1937
|
loadDotenv({ path: getEnvPath(storageRoot) });
|
|
1938
|
+
|
|
1939
|
+
// Wrap agent execution in a try-catch: when the agent command crashes
|
|
1940
|
+
// (non-zero exit, unparseable output, invalid CAS node, etc.), failStep throws
|
|
1941
|
+
// StepFailureError. We catch it to persist suspended state before re-throwing
|
|
1942
|
+
// so the CLI still exits non-zero.
|
|
1943
|
+
try {
|
|
1944
|
+
return await executeAndProcessAgentStep(
|
|
1945
|
+
storageRoot,
|
|
1946
|
+
threadId,
|
|
1947
|
+
headHash,
|
|
1948
|
+
workflowHash,
|
|
1949
|
+
workflow,
|
|
1950
|
+
role,
|
|
1951
|
+
edgePrompt,
|
|
1952
|
+
effectiveCwd,
|
|
1953
|
+
agent,
|
|
1954
|
+
plog,
|
|
1955
|
+
);
|
|
1956
|
+
} catch (e) {
|
|
1957
|
+
if (e instanceof StepFailureError) {
|
|
1958
|
+
// Fatal agent failure — persist suspended state before propagating
|
|
1959
|
+
const uwfErr = await createUwfStore(storageRoot);
|
|
1960
|
+
const errEntry = getThread(uwfErr.varStore, threadId) ?? createThreadIndexEntry(headHash);
|
|
1961
|
+
setThread(uwfErr.varStore, threadId, markThreadSuspended(errEntry, role, e.message));
|
|
1962
|
+
}
|
|
1963
|
+
throw e;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* Execute the agent command and process the result. Separated from cmdThreadStepOnce
|
|
1969
|
+
* so that fatal failures (StepFailureError) can be caught and handled by the caller.
|
|
1970
|
+
*/
|
|
1971
|
+
async function executeAndProcessAgentStep(
|
|
1972
|
+
storageRoot: string,
|
|
1973
|
+
threadId: ThreadId,
|
|
1974
|
+
headHash: CasRef,
|
|
1975
|
+
workflowHash: CasRef,
|
|
1976
|
+
workflow: WorkflowPayload,
|
|
1977
|
+
role: string,
|
|
1978
|
+
edgePrompt: string,
|
|
1979
|
+
effectiveCwd: string,
|
|
1980
|
+
agent: AgentConfig,
|
|
1981
|
+
plog: ProcessLogger,
|
|
1982
|
+
): Promise<StepOutput> {
|
|
1718
1983
|
const agentResult = spawnAgent(plog, agent, threadId, role, edgePrompt, effectiveCwd);
|
|
1719
1984
|
const newHead = agentResult.stepHash as CasRef;
|
|
1720
1985
|
|
|
@@ -1732,22 +1997,28 @@ async function cmdThreadStepOnce(
|
|
|
1732
1997
|
// next exec (until eventual success records `previousAttempts` linking the
|
|
1733
1998
|
// failed step hashes).
|
|
1734
1999
|
if (agentResult.isError === true) {
|
|
2000
|
+
const errorMsg = agentResult.errorMessage ?? "agent reported error";
|
|
1735
2001
|
plog.log(
|
|
1736
2002
|
PL_AGENT_ERROR,
|
|
1737
|
-
`agent reported recoverable failure stepHash=${newHead} message=${
|
|
2003
|
+
`agent reported recoverable failure stepHash=${newHead} message=${errorMsg}`,
|
|
1738
2004
|
null,
|
|
1739
2005
|
);
|
|
2006
|
+
|
|
2007
|
+
// Persist suspended state so `thread list --status suspended` reflects the failure.
|
|
2008
|
+
const priorEntry = getThread(uwfAfter.varStore, threadId) ?? createThreadIndexEntry(headHash);
|
|
2009
|
+
setThread(uwfAfter.varStore, threadId, markThreadSuspended(priorEntry, role, errorMsg));
|
|
2010
|
+
|
|
1740
2011
|
return {
|
|
1741
2012
|
workflow: workflowHash,
|
|
1742
2013
|
thread: threadId,
|
|
1743
2014
|
head: headHash,
|
|
1744
|
-
status: "
|
|
2015
|
+
status: "suspended",
|
|
1745
2016
|
currentRole: role,
|
|
1746
|
-
suspendedRole:
|
|
1747
|
-
suspendMessage:
|
|
2017
|
+
suspendedRole: role,
|
|
2018
|
+
suspendMessage: errorMsg,
|
|
1748
2019
|
done: false,
|
|
1749
2020
|
background: null,
|
|
1750
|
-
error: { stepHash: newHead, message:
|
|
2021
|
+
error: { stepHash: newHead, message: errorMsg },
|
|
1751
2022
|
};
|
|
1752
2023
|
}
|
|
1753
2024
|
|
package/src/commands/workflow.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { createIncludeTag } from "../include.js";
|
|
|
10
10
|
import {
|
|
11
11
|
createUwfStore,
|
|
12
12
|
discoverProjectWorkflows,
|
|
13
|
+
discoverWorkflowPathsEntries,
|
|
13
14
|
findRegistryName,
|
|
14
15
|
loadWorkflowRegistry,
|
|
15
16
|
resolveProjectWorkflowFile,
|
|
@@ -24,8 +25,9 @@ import {
|
|
|
24
25
|
parseWorkflowPayload,
|
|
25
26
|
} from "../validate.js";
|
|
26
27
|
import { validateWorkflow } from "../validate-semantic.js";
|
|
28
|
+
import { loadWorkflowPaths } from "./config.js";
|
|
27
29
|
|
|
28
|
-
export type WorkflowOrigin = "local" | "global";
|
|
30
|
+
export type WorkflowOrigin = "local" | "paths" | "global";
|
|
29
31
|
|
|
30
32
|
export type WorkflowListEntry = {
|
|
31
33
|
name: string;
|
|
@@ -126,7 +128,7 @@ export async function materializeWorkflowPayload(
|
|
|
126
128
|
* returns silently (no stdout/stderr) and exits 0. On any error, writes a
|
|
127
129
|
* single message to stderr and exits 1.
|
|
128
130
|
*/
|
|
129
|
-
export async function cmdWorkflowValidate(filePath: string): Promise<
|
|
131
|
+
export async function cmdWorkflowValidate(filePath: string): Promise<string[]> {
|
|
130
132
|
let text: string;
|
|
131
133
|
try {
|
|
132
134
|
text = await readFile(filePath, "utf8");
|
|
@@ -150,20 +152,19 @@ export async function cmdWorkflowValidate(filePath: string): Promise<void> {
|
|
|
150
152
|
|
|
151
153
|
const filenameError = checkWorkflowFilenameConsistency(filePath, payload);
|
|
152
154
|
if (filenameError !== null) {
|
|
153
|
-
|
|
155
|
+
return [filenameError];
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
if (semanticErrors.length > 0) {
|
|
158
|
-
fail(`workflow validation failed:\n${semanticErrors.map((e) => ` - ${e}`).join("\n")}`);
|
|
159
|
-
}
|
|
160
|
-
// success: silent return
|
|
158
|
+
return validateWorkflow(payload);
|
|
161
159
|
}
|
|
162
160
|
|
|
163
161
|
export async function cmdWorkflowAdd(
|
|
164
162
|
storageRoot: string,
|
|
165
163
|
filePath: string,
|
|
166
164
|
): Promise<WorkflowAddOutput> {
|
|
165
|
+
process.stderr.write(
|
|
166
|
+
"warning: `uwf workflow add` is deprecated. Use workflowPaths in ~/.uwf/config.yaml instead. See issue #360.\n",
|
|
167
|
+
);
|
|
167
168
|
let text: string;
|
|
168
169
|
try {
|
|
169
170
|
text = await readFile(filePath, "utf8");
|
|
@@ -299,6 +300,14 @@ async function resolveWorkflowCasRefForShow(
|
|
|
299
300
|
return materializeLocalWorkflowForShow(uwf, localPath);
|
|
300
301
|
}
|
|
301
302
|
|
|
303
|
+
// Strategy 3.5: workflowPaths global directories
|
|
304
|
+
const workflowPaths = loadWorkflowPaths(uwf.storageRoot);
|
|
305
|
+
const pathsEntries = await discoverWorkflowPathsEntries(workflowPaths);
|
|
306
|
+
const pathsFile = resolveProjectWorkflowFile(pathsEntries, trimmed);
|
|
307
|
+
if (pathsFile !== null) {
|
|
308
|
+
return materializeLocalWorkflowForShow(uwf, pathsFile);
|
|
309
|
+
}
|
|
310
|
+
|
|
302
311
|
// Strategy 4: Global registry fallback
|
|
303
312
|
const registry = loadWorkflowRegistry(uwf.varStore);
|
|
304
313
|
const hash = resolveWorkflowHash(registry, trimmed);
|
|
@@ -348,18 +357,30 @@ export async function cmdWorkflowList(
|
|
|
348
357
|
): Promise<WorkflowListEntry[]> {
|
|
349
358
|
const uwf = await createUwfStore(storageRoot);
|
|
350
359
|
const localEntries = await discoverProjectWorkflows(projectRoot);
|
|
360
|
+
const workflowPaths = loadWorkflowPaths(storageRoot);
|
|
361
|
+
const pathsEntries = await discoverWorkflowPathsEntries(workflowPaths);
|
|
351
362
|
const registry = loadWorkflowRegistry(uwf.varStore);
|
|
352
363
|
|
|
353
364
|
const result: WorkflowListEntry[] = [];
|
|
354
|
-
const
|
|
365
|
+
const seenNames = new Set<string>();
|
|
355
366
|
|
|
367
|
+
// Layer 1: local .workflows/ (highest priority)
|
|
356
368
|
for (const entry of localEntries) {
|
|
357
|
-
|
|
369
|
+
seenNames.add(entry.name);
|
|
358
370
|
result.push({ name: entry.name, hash: "(local)", origin: "local" });
|
|
359
371
|
}
|
|
360
372
|
|
|
373
|
+
// Layer 2: workflowPaths directories
|
|
374
|
+
for (const entry of pathsEntries) {
|
|
375
|
+
if (!seenNames.has(entry.name)) {
|
|
376
|
+
seenNames.add(entry.name);
|
|
377
|
+
result.push({ name: entry.name, hash: "(paths)", origin: "paths" });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Layer 3: global registry (lowest priority)
|
|
361
382
|
for (const [name, hash] of Object.entries(registry)) {
|
|
362
|
-
if (!
|
|
383
|
+
if (!seenNames.has(name)) {
|
|
363
384
|
result.push({ name, hash, origin: "global" });
|
|
364
385
|
}
|
|
365
386
|
}
|