@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,257 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { openStore } from "@ocas/fs";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
6
|
+
import { isOutputFormat, type OutputFormat, SUPPORTED_FORMATS, writeEnvelope } from "../format.js";
|
|
7
|
+
import { registerUwfSchemas, type UwfSchemaHashes } from "../schemas.js";
|
|
8
|
+
|
|
9
|
+
let tmp: string;
|
|
10
|
+
let store: Awaited<ReturnType<typeof openStore>>;
|
|
11
|
+
let schemas: UwfSchemaHashes;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
tmp = await mkdtemp(join(tmpdir(), "uwf-write-envelope-"));
|
|
15
|
+
store = await openStore(tmp);
|
|
16
|
+
schemas = await registerUwfSchemas(store);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await rm(tmp, { recursive: true, force: true });
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function captureStdout<T>(fn: () => Promise<T>): { result: Promise<T>; output: string[] } {
|
|
25
|
+
const buf: string[] = [];
|
|
26
|
+
const spy = vi.spyOn(process.stdout, "write").mockImplementation(((
|
|
27
|
+
chunk: string | Uint8Array,
|
|
28
|
+
): boolean => {
|
|
29
|
+
buf.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
30
|
+
return true;
|
|
31
|
+
}) as typeof process.stdout.write);
|
|
32
|
+
return {
|
|
33
|
+
result: (async () => {
|
|
34
|
+
try {
|
|
35
|
+
return await fn();
|
|
36
|
+
} finally {
|
|
37
|
+
spy.mockRestore();
|
|
38
|
+
}
|
|
39
|
+
})(),
|
|
40
|
+
output: buf,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("isOutputFormat type guard", () => {
|
|
45
|
+
test("accepts every supported format", () => {
|
|
46
|
+
for (const fmt of SUPPORTED_FORMATS) {
|
|
47
|
+
expect(isOutputFormat(fmt)).toBe(true);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("rejects unknown formats", () => {
|
|
52
|
+
expect(isOutputFormat("xml")).toBe(false);
|
|
53
|
+
expect(isOutputFormat("")).toBe(false);
|
|
54
|
+
expect(isOutputFormat("JSON")).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("SUPPORTED_FORMATS", () => {
|
|
59
|
+
test("contains exactly the five formats specified in cli-envelope-writer.md", () => {
|
|
60
|
+
expect([...SUPPORTED_FORMATS].sort()).toEqual(["json", "raw-json", "raw-yaml", "text", "yaml"]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("writeEnvelope — json format", () => {
|
|
65
|
+
test("emits {type,value} JSON envelope with trailing newline", async () => {
|
|
66
|
+
const payload = { valid: true, errors: [] };
|
|
67
|
+
const { result, output } = captureStdout(async () =>
|
|
68
|
+
writeEnvelope(payload, "validate-result", { format: "json", store, schemas }),
|
|
69
|
+
);
|
|
70
|
+
await result;
|
|
71
|
+
|
|
72
|
+
const out = output.join("");
|
|
73
|
+
expect(out.endsWith("\n")).toBe(true);
|
|
74
|
+
const parsed = JSON.parse(out);
|
|
75
|
+
expect(parsed).toEqual({
|
|
76
|
+
type: schemas.outputs["validate-result"],
|
|
77
|
+
value: { valid: true, errors: [] },
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("writeEnvelope — yaml format", () => {
|
|
83
|
+
test("emits envelope yaml with type then value keys", async () => {
|
|
84
|
+
const payload = { valid: false, errors: ["a", "b"] };
|
|
85
|
+
const { result, output } = captureStdout(async () =>
|
|
86
|
+
writeEnvelope(payload, "validate-result", { format: "yaml", store, schemas }),
|
|
87
|
+
);
|
|
88
|
+
await result;
|
|
89
|
+
|
|
90
|
+
const out = output.join("");
|
|
91
|
+
expect(out.endsWith("\n")).toBe(true);
|
|
92
|
+
expect(out).toContain(`type: ${schemas.outputs["validate-result"]}`);
|
|
93
|
+
expect(out).toContain("value:");
|
|
94
|
+
expect(out).toContain("valid: false");
|
|
95
|
+
// type must precede value
|
|
96
|
+
expect(out.indexOf("type:")).toBeLessThan(out.indexOf("value:"));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("writeEnvelope — raw-json format", () => {
|
|
101
|
+
test("emits bare value JSON without envelope (legacy 0.5.0 shape)", async () => {
|
|
102
|
+
const payload = { valid: true, errors: [] };
|
|
103
|
+
const { result, output } = captureStdout(async () =>
|
|
104
|
+
writeEnvelope(payload, "validate-result", { format: "raw-json", store, schemas }),
|
|
105
|
+
);
|
|
106
|
+
await result;
|
|
107
|
+
|
|
108
|
+
const out = output.join("");
|
|
109
|
+
expect(out.endsWith("\n")).toBe(true);
|
|
110
|
+
const parsed = JSON.parse(out);
|
|
111
|
+
expect(parsed).toEqual({ valid: true, errors: [] });
|
|
112
|
+
// Must NOT contain envelope keys
|
|
113
|
+
expect(parsed.type).toBeUndefined();
|
|
114
|
+
expect(parsed.value).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("writeEnvelope — raw-yaml format", () => {
|
|
119
|
+
test("emits bare value YAML without envelope (legacy 0.5.0 shape)", async () => {
|
|
120
|
+
const payload = { valid: true, errors: [] };
|
|
121
|
+
const { result, output } = captureStdout(async () =>
|
|
122
|
+
writeEnvelope(payload, "validate-result", { format: "raw-yaml", store, schemas }),
|
|
123
|
+
);
|
|
124
|
+
await result;
|
|
125
|
+
|
|
126
|
+
const out = output.join("");
|
|
127
|
+
expect(out.endsWith("\n")).toBe(true);
|
|
128
|
+
expect(out).toContain("valid: true");
|
|
129
|
+
expect(out).toContain("errors:");
|
|
130
|
+
expect(out).not.toContain("type:");
|
|
131
|
+
expect(out).not.toContain("value:");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("writeEnvelope — text format (Liquid template)", () => {
|
|
136
|
+
test("renders validate-result valid case as `✓ valid`", async () => {
|
|
137
|
+
const payload = { valid: true, errors: [] };
|
|
138
|
+
const { result, output } = captureStdout(async () =>
|
|
139
|
+
writeEnvelope(payload, "validate-result", { format: "text", store, schemas }),
|
|
140
|
+
);
|
|
141
|
+
await result;
|
|
142
|
+
|
|
143
|
+
const out = output.join("");
|
|
144
|
+
expect(out.trim()).toBe("✓ valid");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("renders validate-result invalid case with bulleted errors", async () => {
|
|
148
|
+
const payload = {
|
|
149
|
+
valid: false,
|
|
150
|
+
errors: ['unknown role "bogus"', "$START missing resume edge"],
|
|
151
|
+
};
|
|
152
|
+
const { result, output } = captureStdout(async () =>
|
|
153
|
+
writeEnvelope(payload, "validate-result", { format: "text", store, schemas }),
|
|
154
|
+
);
|
|
155
|
+
await result;
|
|
156
|
+
|
|
157
|
+
const out = output.join("");
|
|
158
|
+
expect(out).toContain("✗ invalid (2 errors)");
|
|
159
|
+
expect(out).toContain(' - unknown role "bogus"');
|
|
160
|
+
expect(out).toContain(" - $START missing resume edge");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("renders workflow-add as Registered/Hash key-value lines", async () => {
|
|
164
|
+
const payload = { name: "review-pr", hash: "2TBP6T37TZAJZ" };
|
|
165
|
+
const { result, output } = captureStdout(async () =>
|
|
166
|
+
writeEnvelope(payload, "workflow-add", { format: "text", store, schemas }),
|
|
167
|
+
);
|
|
168
|
+
await result;
|
|
169
|
+
|
|
170
|
+
const out = output.join("");
|
|
171
|
+
expect(out).toBe("Registered review-pr\nHash 2TBP6T37TZAJZ\n");
|
|
172
|
+
// No JSON envelope leakage
|
|
173
|
+
expect(out).not.toContain("{");
|
|
174
|
+
expect(out).not.toContain("undefined");
|
|
175
|
+
// Single trailing newline
|
|
176
|
+
expect(out.endsWith("\n")).toBe(true);
|
|
177
|
+
expect(out.endsWith("\n\n")).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("workflow-add tolerates empty hash field without throwing", async () => {
|
|
181
|
+
const payload = { name: "review-pr", hash: "" };
|
|
182
|
+
const { result, output } = captureStdout(async () =>
|
|
183
|
+
writeEnvelope(payload, "workflow-add", { format: "text", store, schemas }),
|
|
184
|
+
);
|
|
185
|
+
await result;
|
|
186
|
+
|
|
187
|
+
const out = output.join("");
|
|
188
|
+
// Renders without throwing; empty hash leaves trailing whitespace
|
|
189
|
+
expect(out).toContain("Registered review-pr");
|
|
190
|
+
expect(out).toContain("Hash ");
|
|
191
|
+
expect(out).not.toContain("undefined");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("writeEnvelope — workflow-add formats", () => {
|
|
196
|
+
test("json format wraps payload in {type,value} envelope", async () => {
|
|
197
|
+
const payload = { name: "review-pr", hash: "2TBP6T37TZAJZ" };
|
|
198
|
+
const { result, output } = captureStdout(async () =>
|
|
199
|
+
writeEnvelope(payload, "workflow-add", { format: "json", store, schemas }),
|
|
200
|
+
);
|
|
201
|
+
await result;
|
|
202
|
+
|
|
203
|
+
const out = output.join("");
|
|
204
|
+
const parsed = JSON.parse(out);
|
|
205
|
+
expect(parsed).toEqual({
|
|
206
|
+
type: schemas.outputs["workflow-add"],
|
|
207
|
+
value: { name: "review-pr", hash: "2TBP6T37TZAJZ" },
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("raw-json format emits bare payload (legacy 0.5.0 shape)", async () => {
|
|
212
|
+
const payload = { name: "review-pr", hash: "2TBP6T37TZAJZ" };
|
|
213
|
+
const { result, output } = captureStdout(async () =>
|
|
214
|
+
writeEnvelope(payload, "workflow-add", { format: "raw-json", store, schemas }),
|
|
215
|
+
);
|
|
216
|
+
await result;
|
|
217
|
+
|
|
218
|
+
const out = output.join("");
|
|
219
|
+
expect(out).toBe('{"name":"review-pr","hash":"2TBP6T37TZAJZ"}\n');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("yaml format emits envelope with type and value keys", async () => {
|
|
223
|
+
const payload = { name: "review-pr", hash: "2TBP6T37TZAJZ" };
|
|
224
|
+
const { result, output } = captureStdout(async () =>
|
|
225
|
+
writeEnvelope(payload, "workflow-add", { format: "yaml", store, schemas }),
|
|
226
|
+
);
|
|
227
|
+
await result;
|
|
228
|
+
|
|
229
|
+
const out = output.join("");
|
|
230
|
+
expect(out).toContain(`type: ${schemas.outputs["workflow-add"]}`);
|
|
231
|
+
expect(out).toContain("value:");
|
|
232
|
+
expect(out).toContain("name: review-pr");
|
|
233
|
+
expect(out).toContain("hash: 2TBP6T37TZAJZ");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("writeEnvelope — schema lookup", () => {
|
|
238
|
+
test("throws when schema short name is unknown", async () => {
|
|
239
|
+
await expect(
|
|
240
|
+
// @ts-expect-error invalid schema name on purpose
|
|
241
|
+
writeEnvelope({}, "not-a-real-schema", { format: "json", store, schemas }),
|
|
242
|
+
).rejects.toThrow(/output schema not registered/);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("each format calls in to the same registered schema hash", async () => {
|
|
246
|
+
const payload = { valid: true, errors: [] };
|
|
247
|
+
const formats: OutputFormat[] = ["json", "yaml"];
|
|
248
|
+
for (const fmt of formats) {
|
|
249
|
+
const { result, output } = captureStdout(async () =>
|
|
250
|
+
writeEnvelope(payload, "validate-result", { format: fmt, store, schemas }),
|
|
251
|
+
);
|
|
252
|
+
await result;
|
|
253
|
+
const out = output.join("");
|
|
254
|
+
expect(out).toContain(schemas.outputs["validate-result"]);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -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