@united-workforce/cli 0.2.1-rc.9 → 0.4.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 +15 -8
- package/dist/__tests__/adapter-json-roundtrip.test.js +1 -1
- 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__/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 +20 -23
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- 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__/moderator-evaluate.test.js +9 -50
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
- 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 +271 -0
- package/dist/__tests__/pid-recycling.test.js.map +1 -0
- package/dist/__tests__/prompt.test.js +321 -0
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +4 -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 +24 -27
- 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 +499 -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 +9 -9
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-status.test.js +6 -6
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
- package/dist/__tests__/thread-list-filters.test.js +344 -9
- package/dist/__tests__/thread-list-filters.test.js.map +1 -1
- 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 +412 -0
- package/dist/__tests__/thread-poke.test.js.map +1 -0
- package/dist/__tests__/thread-resume.test.js +10 -14
- 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-suspend-step.test.js +8 -14
- 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.js +4 -4
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +49 -21
- 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 +283 -0
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +36 -21
- 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 +210 -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 +687 -0
- package/dist/__tests__/workflow-validate.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 +66 -31
- 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 +7 -33
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +15 -2
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +7 -39
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +27 -302
- 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 +379 -140
- 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 +130 -6
- package/dist/commands/workflow.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/schemas.d.ts +2 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +5 -3
- 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/validate-semantic.d.ts.map +1 -1
- package/dist/validate-semantic.js +83 -66
- 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/package.json +8 -10
- package/src/__tests__/adapter-json-roundtrip.test.ts +1 -1
- 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__/config.test.ts +33 -321
- package/src/__tests__/current-role.test.ts +7 -6
- package/src/__tests__/e2e-mock-agent.test.ts +20 -23
- 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__/issue-180-workflow-ref-removed.test.ts +43 -0
- package/src/__tests__/moderator-evaluate.test.ts +9 -52
- package/src/__tests__/pid-recycling.test.ts +328 -0
- package/src/__tests__/prompt.test.ts +397 -0
- package/src/__tests__/resolve-head-hash.test.ts +4 -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 +24 -30
- package/src/__tests__/step-ask.test.ts +670 -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 +9 -9
- package/src/__tests__/thread-cancel-status.test.ts +6 -6
- package/src/__tests__/thread-list-filters.test.ts +434 -8
- package/src/__tests__/thread-poke.test.ts +545 -0
- package/src/__tests__/thread-resume.test.ts +10 -14
- package/src/__tests__/thread-show-status.test.ts +17 -29
- package/src/__tests__/thread-suspend-step.test.ts +8 -14
- package/src/__tests__/thread-suspended-display.test.ts +10 -22
- package/src/__tests__/thread.test.ts +4 -4
- package/src/__tests__/validate-semantic.test.ts +59 -31
- package/src/__tests__/workflow-list-recursive.test.ts +370 -0
- package/src/__tests__/workflow-resolution.test.ts +39 -21
- package/src/__tests__/workflow-show-resolution.test.ts +285 -0
- package/src/__tests__/workflow-validate.test.ts +806 -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 +97 -47
- package/src/commands/config.ts +7 -35
- package/src/commands/prompt.ts +15 -2
- package/src/commands/setup.ts +29 -357
- package/src/commands/step.ts +339 -12
- package/src/commands/thread.ts +463 -169
- package/src/commands/workflow.ts +159 -4
- 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/schemas.ts +13 -3
- package/src/store.ts +86 -20
- package/src/validate-semantic.ts +109 -78
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import type { RunningThreadItem, ThreadId } from "@united-workforce/protocol";
|
|
@@ -18,6 +19,42 @@ export function getMarkerPath(storageRoot: string, threadId: ThreadId): string {
|
|
|
18
19
|
return join(getRunningDir(storageRoot), `${threadId}.json`);
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Read the process start time from /proc/<pid>/stat (field 22, starttime).
|
|
24
|
+
* Returns the value in clock ticks since boot, or null on non-Linux systems
|
|
25
|
+
* or when the process does not exist.
|
|
26
|
+
*/
|
|
27
|
+
export function getProcessStartTime(pid: number): number | null {
|
|
28
|
+
try {
|
|
29
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
30
|
+
// /proc/<pid>/stat format: pid (comm) state ... field22_starttime ...
|
|
31
|
+
// The comm field can contain spaces and parentheses, so we find the last ')' first
|
|
32
|
+
const closeParenIdx = stat.lastIndexOf(")");
|
|
33
|
+
if (closeParenIdx === -1) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
// Fields after (comm) start at index 2 (state is field 3, index 2 in 0-based after split)
|
|
37
|
+
// starttime is field 22 (1-based), which is index 19 in the fields after ')'
|
|
38
|
+
const fieldsAfterComm = stat
|
|
39
|
+
.slice(closeParenIdx + 2)
|
|
40
|
+
.trim()
|
|
41
|
+
.split(" ");
|
|
42
|
+
// Field indices after comm (0-based): 0=state(3), 1=ppid(4), ..., 19=starttime(22)
|
|
43
|
+
const startTimeStr = fieldsAfterComm[19];
|
|
44
|
+
if (startTimeStr === undefined) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const startTime = Number(startTimeStr);
|
|
48
|
+
if (Number.isNaN(startTime)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return startTime;
|
|
52
|
+
} catch {
|
|
53
|
+
// /proc not available (non-Linux) or process doesn't exist
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
21
58
|
/**
|
|
22
59
|
* Check if a PID is still running.
|
|
23
60
|
* Returns true if the process exists, false otherwise.
|
|
@@ -33,6 +70,39 @@ export function isPidAlive(pid: number): boolean {
|
|
|
33
70
|
}
|
|
34
71
|
}
|
|
35
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Validate that a running marker still refers to the same process.
|
|
75
|
+
* Checks both that the PID is alive AND that its start time matches.
|
|
76
|
+
* Returns false if:
|
|
77
|
+
* - The PID is no longer alive
|
|
78
|
+
* - The PID is alive but its start time doesn't match (PID was recycled)
|
|
79
|
+
* Returns true if:
|
|
80
|
+
* - PID is alive AND start times match
|
|
81
|
+
* - PID is alive AND marker has null processStartTime (backward compat / non-Linux)
|
|
82
|
+
*/
|
|
83
|
+
export function isMarkerValid(marker: RunningMarker): boolean {
|
|
84
|
+
if (!isPidAlive(marker.pid)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If marker has no processStartTime (legacy marker or non-Linux at creation time),
|
|
89
|
+
// fall back to PID-alive-only check for backward compatibility
|
|
90
|
+
if (marker.processStartTime === null) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Verify process identity by comparing start times
|
|
95
|
+
const actualStartTime = getProcessStartTime(marker.pid);
|
|
96
|
+
|
|
97
|
+
// If we can't read the actual start time (non-Linux runtime), trust PID-alive check
|
|
98
|
+
if (actualStartTime === null) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Start times must match — if they differ, PID was recycled
|
|
103
|
+
return marker.processStartTime === actualStartTime;
|
|
104
|
+
}
|
|
105
|
+
|
|
36
106
|
/**
|
|
37
107
|
* Create a marker file for a running thread.
|
|
38
108
|
* Writes to a temp file in the same directory, then atomically renames.
|
|
@@ -63,6 +133,7 @@ export async function deleteMarker(storageRoot: string, threadId: ThreadId): Pro
|
|
|
63
133
|
|
|
64
134
|
/**
|
|
65
135
|
* Read a marker file. Returns null if file doesn't exist or is invalid.
|
|
136
|
+
* Handles legacy markers that lack processStartTime by defaulting to null.
|
|
66
137
|
*/
|
|
67
138
|
export async function readMarker(
|
|
68
139
|
storageRoot: string,
|
|
@@ -71,7 +142,15 @@ export async function readMarker(
|
|
|
71
142
|
const markerPath = getMarkerPath(storageRoot, threadId);
|
|
72
143
|
try {
|
|
73
144
|
const content = await readFile(markerPath, "utf8");
|
|
74
|
-
const
|
|
145
|
+
const raw = JSON.parse(content) as Record<string, unknown>;
|
|
146
|
+
// Normalize legacy markers that lack processStartTime
|
|
147
|
+
const marker: RunningMarker = {
|
|
148
|
+
thread: raw.thread as ThreadId,
|
|
149
|
+
workflow: raw.workflow as string,
|
|
150
|
+
pid: raw.pid as number,
|
|
151
|
+
startedAt: raw.startedAt as number,
|
|
152
|
+
processStartTime: typeof raw.processStartTime === "number" ? raw.processStartTime : null,
|
|
153
|
+
};
|
|
75
154
|
return marker;
|
|
76
155
|
} catch {
|
|
77
156
|
return null;
|
|
@@ -80,6 +159,8 @@ export async function readMarker(
|
|
|
80
159
|
|
|
81
160
|
/**
|
|
82
161
|
* List all running threads, filtering out stale markers.
|
|
162
|
+
* A marker is stale if the PID is dead or if the PID was recycled
|
|
163
|
+
* (processStartTime mismatch).
|
|
83
164
|
*/
|
|
84
165
|
export async function listRunningThreads(storageRoot: string): Promise<RunningThreadItem[]> {
|
|
85
166
|
const runningDir = getRunningDir(storageRoot);
|
|
@@ -107,8 +188,8 @@ export async function listRunningThreads(storageRoot: string): Promise<RunningTh
|
|
|
107
188
|
continue;
|
|
108
189
|
}
|
|
109
190
|
|
|
110
|
-
if (!
|
|
111
|
-
// Stale marker - process no longer exists
|
|
191
|
+
if (!isMarkerValid(marker)) {
|
|
192
|
+
// Stale marker - process no longer exists or PID was recycled
|
|
112
193
|
await deleteMarker(storageRoot, threadId);
|
|
113
194
|
continue;
|
|
114
195
|
}
|
|
@@ -126,7 +207,8 @@ export async function listRunningThreads(storageRoot: string): Promise<RunningTh
|
|
|
126
207
|
|
|
127
208
|
/**
|
|
128
209
|
* Check if a thread is currently executing in the background.
|
|
129
|
-
* Returns the marker if running, null otherwise.
|
|
210
|
+
* Returns the marker if running (and process identity is verified), null otherwise.
|
|
211
|
+
* Automatically deletes stale markers (dead PID or recycled PID).
|
|
130
212
|
*/
|
|
131
213
|
export async function isThreadRunning(
|
|
132
214
|
storageRoot: string,
|
|
@@ -137,8 +219,8 @@ export async function isThreadRunning(
|
|
|
137
219
|
return null;
|
|
138
220
|
}
|
|
139
221
|
|
|
140
|
-
if (!
|
|
141
|
-
// Stale marker
|
|
222
|
+
if (!isMarkerValid(marker)) {
|
|
223
|
+
// Stale marker — PID dead or recycled
|
|
142
224
|
await deleteMarker(storageRoot, threadId);
|
|
143
225
|
return null;
|
|
144
226
|
}
|
package/src/background/index.ts
CHANGED
package/src/background/types.ts
CHANGED
package/src/cli.ts
CHANGED
|
@@ -11,12 +11,13 @@ import {
|
|
|
11
11
|
cmdPromptUsage,
|
|
12
12
|
cmdPromptWorkflowAuthoring,
|
|
13
13
|
} from "./commands/prompt.js";
|
|
14
|
-
import { cmdSetup, cmdSetupInteractive
|
|
15
|
-
import { cmdStepFork, cmdStepList, cmdStepRead, cmdStepShow } from "./commands/step.js";
|
|
14
|
+
import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js";
|
|
15
|
+
import { cmdStepAsk, cmdStepFork, cmdStepList, cmdStepRead, cmdStepShow } from "./commands/step.js";
|
|
16
16
|
import {
|
|
17
17
|
cmdThreadCancel,
|
|
18
18
|
cmdThreadExec,
|
|
19
19
|
cmdThreadList,
|
|
20
|
+
cmdThreadPoke,
|
|
20
21
|
cmdThreadRead,
|
|
21
22
|
cmdThreadResume,
|
|
22
23
|
cmdThreadShow,
|
|
@@ -25,7 +26,12 @@ import {
|
|
|
25
26
|
THREAD_READ_DEFAULT_QUOTA,
|
|
26
27
|
} from "./commands/thread.js";
|
|
27
28
|
import { parseTimeInput } from "./commands/thread-time-parser.js";
|
|
28
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
cmdWorkflowAdd,
|
|
31
|
+
cmdWorkflowList,
|
|
32
|
+
cmdWorkflowShow,
|
|
33
|
+
cmdWorkflowValidate,
|
|
34
|
+
} from "./commands/workflow.js";
|
|
29
35
|
import { formatOutput, type OutputFormat } from "./format.js";
|
|
30
36
|
import { resolveStorageRoot } from "./store.js";
|
|
31
37
|
|
|
@@ -72,6 +78,17 @@ workflow
|
|
|
72
78
|
});
|
|
73
79
|
});
|
|
74
80
|
|
|
81
|
+
workflow
|
|
82
|
+
.command("validate")
|
|
83
|
+
.description("Validate a workflow YAML without registering it (CI-friendly)")
|
|
84
|
+
.argument("<file>", "Workflow YAML file")
|
|
85
|
+
.action((file: string) => {
|
|
86
|
+
runAction(async () => {
|
|
87
|
+
await cmdWorkflowValidate(file);
|
|
88
|
+
// silent on success — do not call writeOutput
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
75
92
|
workflow
|
|
76
93
|
.command("show")
|
|
77
94
|
.description("Show a workflow by name or CAS hash")
|
|
@@ -79,7 +96,7 @@ workflow
|
|
|
79
96
|
.action((id: string) => {
|
|
80
97
|
const storageRoot = resolveStorageRoot();
|
|
81
98
|
runAction(async () => {
|
|
82
|
-
const result = await cmdWorkflowShow(storageRoot, id);
|
|
99
|
+
const result = await cmdWorkflowShow(storageRoot, id, process.cwd());
|
|
83
100
|
writeOutput(result);
|
|
84
101
|
});
|
|
85
102
|
});
|
|
@@ -178,11 +195,18 @@ function parseStatusFilter(status: string | undefined): ThreadStatus[] | null {
|
|
|
178
195
|
if (raw === "active") return ["idle", "running"];
|
|
179
196
|
|
|
180
197
|
const parts = raw.split(",").map((s) => s.trim());
|
|
181
|
-
const validStatuses: ThreadStatus[] = [
|
|
198
|
+
const validStatuses: ThreadStatus[] = [
|
|
199
|
+
"idle",
|
|
200
|
+
"running",
|
|
201
|
+
"suspended",
|
|
202
|
+
"end",
|
|
203
|
+
"cancelled",
|
|
204
|
+
"corrupt",
|
|
205
|
+
];
|
|
182
206
|
for (const part of parts) {
|
|
183
207
|
if (!validStatuses.includes(part as ThreadStatus)) {
|
|
184
208
|
process.stderr.write(
|
|
185
|
-
`Invalid status: ${part}. Must be one of: idle, running, suspended,
|
|
209
|
+
`Invalid status: ${part}. Must be one of: idle, running, suspended, end, cancelled, active\n`,
|
|
186
210
|
);
|
|
187
211
|
process.exit(1);
|
|
188
212
|
}
|
|
@@ -232,11 +256,12 @@ function parsePaginationOptions(
|
|
|
232
256
|
|
|
233
257
|
thread
|
|
234
258
|
.command("list")
|
|
235
|
-
.description("List threads")
|
|
259
|
+
.description("List threads (defaults to active: idle + running + corrupt)")
|
|
236
260
|
.option(
|
|
237
261
|
"--status <status>",
|
|
238
|
-
"Filter by status: idle, running,
|
|
262
|
+
"Filter by status: idle, running, end, cancelled, active (idle+running), or comma-separated values",
|
|
239
263
|
)
|
|
264
|
+
.option("--all", "Show all threads regardless of status (overrides default active-only filter)")
|
|
240
265
|
.option("--after <date>", "Filter threads created after this date (ISO or relative like '7d')")
|
|
241
266
|
.option("--before <date>", "Filter threads created before this date (ISO or relative like '7d')")
|
|
242
267
|
.option("--skip <n>", "Skip first n threads")
|
|
@@ -244,6 +269,7 @@ thread
|
|
|
244
269
|
.action(
|
|
245
270
|
(opts: {
|
|
246
271
|
status: string | undefined;
|
|
272
|
+
all: boolean | undefined;
|
|
247
273
|
after: string | undefined;
|
|
248
274
|
before: string | undefined;
|
|
249
275
|
skip: string | undefined;
|
|
@@ -255,6 +281,7 @@ thread
|
|
|
255
281
|
const nowMs = Date.now();
|
|
256
282
|
const { afterMs, beforeMs } = parseTimeFilters(opts.after, opts.before, nowMs);
|
|
257
283
|
const { skip, take } = parsePaginationOptions(opts.skip, opts.take);
|
|
284
|
+
const showAll = opts.all === true;
|
|
258
285
|
|
|
259
286
|
const result = await cmdThreadList(
|
|
260
287
|
storageRoot,
|
|
@@ -263,6 +290,7 @@ thread
|
|
|
263
290
|
beforeMs,
|
|
264
291
|
skip,
|
|
265
292
|
take,
|
|
293
|
+
showAll,
|
|
266
294
|
);
|
|
267
295
|
writeOutput(result);
|
|
268
296
|
});
|
|
@@ -290,6 +318,26 @@ thread
|
|
|
290
318
|
});
|
|
291
319
|
});
|
|
292
320
|
|
|
321
|
+
thread
|
|
322
|
+
.command("poke")
|
|
323
|
+
.description("Re-run the head step's agent with a supplementary prompt (replaces head step)")
|
|
324
|
+
.argument("<thread-id>", "Thread ULID")
|
|
325
|
+
.requiredOption("-p, --prompt <text>", "Supplementary prompt for the agent")
|
|
326
|
+
.option("--agent <cmd>", "Override agent command (defaults to head step's agent)")
|
|
327
|
+
.action((threadId: string, opts: { prompt: string; agent: string | undefined }) => {
|
|
328
|
+
const storageRoot = resolveStorageRoot();
|
|
329
|
+
runAction(async () => {
|
|
330
|
+
const agentOverride = opts.agent ?? null;
|
|
331
|
+
const result = await cmdThreadPoke(
|
|
332
|
+
storageRoot,
|
|
333
|
+
threadId as ThreadId,
|
|
334
|
+
opts.prompt,
|
|
335
|
+
agentOverride,
|
|
336
|
+
);
|
|
337
|
+
writeOutput(result);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
293
341
|
thread
|
|
294
342
|
.command("stop")
|
|
295
343
|
.description("Stop background execution of a thread (keep thread active)")
|
|
@@ -369,6 +417,32 @@ step
|
|
|
369
417
|
});
|
|
370
418
|
});
|
|
371
419
|
|
|
420
|
+
step
|
|
421
|
+
.command("ask")
|
|
422
|
+
.description(
|
|
423
|
+
"Ask a follow-up question to a historical step's agent (read-only; no thread mutation)",
|
|
424
|
+
)
|
|
425
|
+
.argument("<step-hash>", "CAS hash of the StepNode to query")
|
|
426
|
+
.requiredOption("-p, --prompt <text>", "Question to ask the step's agent")
|
|
427
|
+
.option("--agent <cmd>", "Override agent command (defaults to the step's recorded agent)")
|
|
428
|
+
.option(
|
|
429
|
+
"--no-fork",
|
|
430
|
+
"Skip session-fork; spawn the agent in a fresh ask session and inject the step's detail ref for context",
|
|
431
|
+
)
|
|
432
|
+
.action(
|
|
433
|
+
(stepHash: string, opts: { prompt: string; agent: string | undefined; fork: boolean }) => {
|
|
434
|
+
const storageRoot = resolveStorageRoot();
|
|
435
|
+
runAction(async () => {
|
|
436
|
+
const stdout = await cmdStepAsk(storageRoot, stepHash as CasRef, {
|
|
437
|
+
prompt: opts.prompt,
|
|
438
|
+
agentOverride: opts.agent ?? null,
|
|
439
|
+
fork: opts.fork,
|
|
440
|
+
});
|
|
441
|
+
process.stdout.write(stdout.endsWith("\n") ? stdout : `${stdout}\n`);
|
|
442
|
+
});
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
|
|
372
446
|
step
|
|
373
447
|
.command("read")
|
|
374
448
|
.description("Read a step's turns as human-readable markdown")
|
|
@@ -542,46 +616,22 @@ prompt
|
|
|
542
616
|
|
|
543
617
|
program
|
|
544
618
|
.command("setup")
|
|
545
|
-
.description(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
.option("--model <name>", "Default model name")
|
|
619
|
+
.description(
|
|
620
|
+
"Configure the default agent. Run without --agent for interactive wizard.\n" +
|
|
621
|
+
"LLM provider/model configuration lives in <storage>/config.yaml under providers and models.",
|
|
622
|
+
)
|
|
550
623
|
.option("--agent <name>", "Default agent adapter (e.g. hermes → uwf-hermes)")
|
|
551
|
-
.action(
|
|
552
|
-
(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const resolvedBaseUrl =
|
|
563
|
-
opts.baseUrl ??
|
|
564
|
-
(opts.provider !== undefined ? resolvePresetBaseUrl(opts.provider) : null);
|
|
565
|
-
if (opts.provider && resolvedBaseUrl && opts.apiKey && opts.model) {
|
|
566
|
-
const result = await cmdSetup({
|
|
567
|
-
provider: opts.provider,
|
|
568
|
-
baseUrl: resolvedBaseUrl,
|
|
569
|
-
apiKey: opts.apiKey,
|
|
570
|
-
model: opts.model,
|
|
571
|
-
agent: opts.agent ?? undefined,
|
|
572
|
-
storageRoot,
|
|
573
|
-
});
|
|
574
|
-
writeOutput(result);
|
|
575
|
-
} else if (!opts.provider && !opts.baseUrl && !opts.apiKey && !opts.model) {
|
|
576
|
-
await cmdSetupInteractive(storageRoot);
|
|
577
|
-
} else {
|
|
578
|
-
throw new Error(
|
|
579
|
-
"Non-interactive setup requires: --provider, --api-key, --model (--base-url is optional for preset providers)",
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
});
|
|
583
|
-
},
|
|
584
|
-
);
|
|
624
|
+
.action((opts: { agent?: string }) => {
|
|
625
|
+
const storageRoot = resolveStorageRoot();
|
|
626
|
+
runAction(async () => {
|
|
627
|
+
if (opts.agent !== undefined && opts.agent !== "") {
|
|
628
|
+
const result = await cmdSetup({ agent: opts.agent, storageRoot });
|
|
629
|
+
writeOutput(result);
|
|
630
|
+
} else {
|
|
631
|
+
await cmdSetupInteractive(storageRoot);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
});
|
|
585
635
|
|
|
586
636
|
const log = program.command("log").description("Process-level debug logs");
|
|
587
637
|
|
package/src/commands/config.ts
CHANGED
|
@@ -3,20 +3,14 @@ import { join } from "node:path";
|
|
|
3
3
|
import { parse, stringify } from "yaml";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Valid configuration key schema
|
|
6
|
+
* Valid configuration key schema. Engine config is LLM-free — providers,
|
|
7
|
+
* models, defaultModel, and modelOverrides are no longer accepted here.
|
|
8
|
+
* Each adapter owns its own LLM configuration.
|
|
7
9
|
*/
|
|
8
10
|
const VALID_CONFIG_KEYS: Record<
|
|
9
11
|
string,
|
|
10
12
|
{ nested: boolean; knownFields?: string[]; minDepth?: number }
|
|
11
13
|
> = {
|
|
12
|
-
providers: {
|
|
13
|
-
nested: true,
|
|
14
|
-
knownFields: ["baseUrl", "apiKey"],
|
|
15
|
-
},
|
|
16
|
-
models: {
|
|
17
|
-
nested: true,
|
|
18
|
-
knownFields: ["provider", "name"],
|
|
19
|
-
},
|
|
20
14
|
agents: {
|
|
21
15
|
nested: true,
|
|
22
16
|
knownFields: ["command", "args"],
|
|
@@ -26,14 +20,7 @@ const VALID_CONFIG_KEYS: Record<
|
|
|
26
20
|
// agentOverrides.<workflowName>.<roleName> = agentAlias (string value)
|
|
27
21
|
// No knownFields — workflow/role names are user-defined
|
|
28
22
|
},
|
|
29
|
-
modelOverrides: {
|
|
30
|
-
nested: true,
|
|
31
|
-
minDepth: 2,
|
|
32
|
-
// modelOverrides.<scenario> = modelAlias (string value)
|
|
33
|
-
// No knownFields — scenarios are user-defined
|
|
34
|
-
},
|
|
35
23
|
defaultAgent: { nested: false },
|
|
36
|
-
defaultModel: { nested: false },
|
|
37
24
|
};
|
|
38
25
|
|
|
39
26
|
/**
|
|
@@ -175,27 +162,12 @@ export function setNestedValue(obj: Record<string, unknown>, path: string[], val
|
|
|
175
162
|
}
|
|
176
163
|
|
|
177
164
|
/**
|
|
178
|
-
* Deep clone
|
|
165
|
+
* Deep clone the config. Engine config is LLM-free, so there are no apiKey
|
|
166
|
+
* fields to mask — this function is preserved as a defensive deep-clone
|
|
167
|
+
* boundary used by `cmdConfigList`.
|
|
179
168
|
*/
|
|
180
169
|
export function maskApiKeys(config: Record<string, unknown>): Record<string, unknown> {
|
|
181
|
-
|
|
182
|
-
const cloned = JSON.parse(JSON.stringify(config)) as Record<string, unknown>;
|
|
183
|
-
|
|
184
|
-
// Mask apiKey values in providers
|
|
185
|
-
if (cloned.providers && typeof cloned.providers === "object") {
|
|
186
|
-
const providers = cloned.providers as Record<string, unknown>;
|
|
187
|
-
for (const providerName of Object.keys(providers)) {
|
|
188
|
-
const provider = providers[providerName];
|
|
189
|
-
if (provider && typeof provider === "object") {
|
|
190
|
-
const providerObj = provider as Record<string, unknown>;
|
|
191
|
-
if ("apiKey" in providerObj) {
|
|
192
|
-
providerObj.apiKey = "***MASKED***";
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return cloned;
|
|
170
|
+
return JSON.parse(JSON.stringify(config)) as Record<string, unknown>;
|
|
199
171
|
}
|
|
200
172
|
|
|
201
173
|
/**
|
package/src/commands/prompt.ts
CHANGED
|
@@ -241,7 +241,17 @@ uwf thread exec <thread-id>
|
|
|
241
241
|
uwf thread show <thread-id>
|
|
242
242
|
\`\`\`
|
|
243
243
|
|
|
244
|
-
If the thread reaches \`$END\` with status \`
|
|
244
|
+
If the thread reaches \`$END\` with status \`end\`, the setup is working.
|
|
245
|
+
|
|
246
|
+
To verify suspend/resume and poke:
|
|
247
|
+
|
|
248
|
+
\`\`\`bash
|
|
249
|
+
# After a role yields with $status: "$SUSPEND", resume the suspended thread:
|
|
250
|
+
uwf thread resume <thread-id> -p "Additional context for the agent"
|
|
251
|
+
|
|
252
|
+
# Re-run the head step's agent with a supplementary prompt (replaces head step):
|
|
253
|
+
uwf thread poke <thread-id> -p "Try again with this hint"
|
|
254
|
+
\`\`\`
|
|
245
255
|
|
|
246
256
|
## Scenario B: Upgrade from Previous Version
|
|
247
257
|
|
|
@@ -297,7 +307,7 @@ Check the changelog for breaking changes. Known migrations:
|
|
|
297
307
|
resume: { role: planner, prompt: "Review previous run and continue." }
|
|
298
308
|
\`\`\`
|
|
299
309
|
|
|
300
|
-
Update all \`.
|
|
310
|
+
Update all \`.workflows/\` and \`.workflow/\` YAML files in your projects. \`uwf workflow add\` will reject files with the old \`_\` syntax.
|
|
301
311
|
|
|
302
312
|
- **v0.2.1**: \`$status: { enum: [value] }\` → \`$status: { const: "value" }\`. The validator no longer accepts \`enum\` for \`$status\`. Update all workflow YAML files:
|
|
303
313
|
\`\`\`yaml
|
|
@@ -310,6 +320,9 @@ Update all \`.workflow/\` and \`.workflows/\` YAML files in your projects. \`uwf
|
|
|
310
320
|
# For multi-exit, use oneOf with const (unchanged)
|
|
311
321
|
\`\`\`
|
|
312
322
|
|
|
323
|
+
- **v0.4.0**: Thread status \`completed\` → \`end\`. Update scripts that filter \`--status completed\` to use \`--status end\`. Legacy on-disk \`status: completed\` is normalized to \`end\` on read.
|
|
324
|
+
- **v0.4.0**: \`$SUSPEND\` is now an engine-level coroutine yield, not a graph target. Workflows that routed to \`role: "$SUSPEND"\` must emit \`$status: "$SUSPEND"\` with a \`reason\` from the role output instead. The thread becomes \`suspended\`; continue with \`uwf thread resume\`.
|
|
325
|
+
|
|
313
326
|
### Step 4 — Verify
|
|
314
327
|
|
|
315
328
|
\`\`\`bash
|