agentplane 0.2.24 → 0.2.26
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/bin/agentplane.js +91 -54
- package/dist/.build-manifest.json +11 -0
- package/dist/backends/task-backend/local-backend.d.ts +2 -0
- package/dist/backends/task-backend/local-backend.d.ts.map +1 -1
- package/dist/backends/task-backend/local-backend.js +12 -1
- package/dist/backends/task-backend/redmine/mapping.d.ts.map +1 -1
- package/dist/backends/task-backend/redmine/mapping.js +26 -1
- package/dist/backends/task-backend/redmine-backend.d.ts +4 -0
- package/dist/backends/task-backend/redmine-backend.d.ts.map +1 -1
- package/dist/backends/task-backend/redmine-backend.js +92 -9
- package/dist/backends/task-backend/shared/types.d.ts +1 -0
- package/dist/backends/task-backend/shared/types.d.ts.map +1 -1
- package/dist/backends/task-index.d.ts.map +1 -1
- package/dist/backends/task-index.js +8 -1
- package/dist/cli/command-guide.d.ts.map +1 -1
- package/dist/cli/command-guide.js +21 -8
- package/dist/cli/command-snippets.d.ts +24 -0
- package/dist/cli/command-snippets.d.ts.map +1 -0
- package/dist/cli/command-snippets.js +23 -0
- package/dist/cli/reason-codes.d.ts +9 -0
- package/dist/cli/reason-codes.d.ts.map +1 -0
- package/dist/cli/reason-codes.js +79 -0
- package/dist/cli/recipes-bundled.d.ts +1 -0
- package/dist/cli/recipes-bundled.d.ts.map +1 -1
- package/dist/cli/recipes-bundled.js +4 -1
- package/dist/cli/run-cli/command-catalog.d.ts +1 -1
- package/dist/cli/run-cli/command-catalog.d.ts.map +1 -1
- package/dist/cli/run-cli/command-catalog.js +40 -1
- package/dist/cli/run-cli/commands/config.d.ts +5 -0
- package/dist/cli/run-cli/commands/config.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/config.js +86 -1
- package/dist/cli/run-cli/commands/core.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/core.js +55 -0
- package/dist/cli/run-cli/commands/init/recipes.d.ts +5 -1
- package/dist/cli/run-cli/commands/init/recipes.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/init/recipes.js +24 -4
- package/dist/cli/run-cli/commands/init/write-workflow.d.ts +7 -0
- package/dist/cli/run-cli/commands/init/write-workflow.d.ts.map +1 -0
- package/dist/cli/run-cli/commands/init/write-workflow.js +52 -0
- package/dist/cli/run-cli/commands/init.d.ts +2 -1
- package/dist/cli/run-cli/commands/init.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/init.js +104 -54
- package/dist/cli/run-cli.d.ts.map +1 -1
- package/dist/cli/run-cli.js +70 -1
- package/dist/commands/backend/sync.command.d.ts.map +1 -1
- package/dist/commands/backend/sync.command.js +7 -6
- package/dist/commands/backend.d.ts.map +1 -1
- package/dist/commands/backend.js +2 -0
- package/dist/commands/block.spec.d.ts.map +1 -1
- package/dist/commands/block.spec.js +23 -2
- package/dist/commands/commit.spec.d.ts.map +1 -1
- package/dist/commands/commit.spec.js +18 -6
- package/dist/commands/doctor.run.d.ts.map +1 -1
- package/dist/commands/doctor.run.js +96 -10
- package/dist/commands/finish.spec.d.ts.map +1 -1
- package/dist/commands/finish.spec.js +53 -4
- package/dist/commands/guard/commit.command.d.ts.map +1 -1
- package/dist/commands/guard/commit.command.js +26 -20
- package/dist/commands/guard/impl/allow.d.ts.map +1 -1
- package/dist/commands/guard/impl/allow.js +8 -1
- package/dist/commands/guard/impl/commands.d.ts.map +1 -1
- package/dist/commands/guard/impl/commands.js +19 -21
- package/dist/commands/guard/impl/comment-commit.d.ts.map +1 -1
- package/dist/commands/guard/impl/comment-commit.js +8 -17
- package/dist/commands/recipes/impl/commands/install.d.ts.map +1 -1
- package/dist/commands/recipes/impl/commands/install.js +36 -13
- package/dist/commands/recipes/impl/scenario.d.ts.map +1 -1
- package/dist/commands/recipes/impl/scenario.js +25 -0
- package/dist/commands/recipes/impl/types.d.ts +4 -0
- package/dist/commands/recipes/impl/types.d.ts.map +1 -1
- package/dist/commands/scenario/impl/commands.d.ts.map +1 -1
- package/dist/commands/scenario/impl/commands.js +74 -3
- package/dist/commands/scenario/impl/report.d.ts +8 -0
- package/dist/commands/scenario/impl/report.d.ts.map +1 -1
- package/dist/commands/scenario/impl/report.js +1 -0
- package/dist/commands/shared/reconcile-check.d.ts +7 -0
- package/dist/commands/shared/reconcile-check.d.ts.map +1 -0
- package/dist/commands/shared/reconcile-check.js +60 -0
- package/dist/commands/start.spec.d.ts.map +1 -1
- package/dist/commands/start.spec.js +23 -2
- package/dist/commands/sync.command.d.ts.map +1 -1
- package/dist/commands/sync.command.js +9 -2
- package/dist/commands/task/finish.d.ts.map +1 -1
- package/dist/commands/task/finish.js +34 -10
- package/dist/commands/task/list.d.ts.map +1 -1
- package/dist/commands/task/list.js +2 -1
- package/dist/commands/task/list.spec.d.ts.map +1 -1
- package/dist/commands/task/list.spec.js +7 -0
- package/dist/commands/task/next.d.ts.map +1 -1
- package/dist/commands/task/next.js +2 -1
- package/dist/commands/task/next.spec.d.ts.map +1 -1
- package/dist/commands/task/next.spec.js +7 -0
- package/dist/commands/task/search.d.ts.map +1 -1
- package/dist/commands/task/search.js +2 -1
- package/dist/commands/task/search.spec.d.ts.map +1 -1
- package/dist/commands/task/search.spec.js +7 -0
- package/dist/commands/task/set-status.command.d.ts.map +1 -1
- package/dist/commands/task/set-status.command.js +22 -2
- package/dist/commands/task/shared.d.ts +7 -0
- package/dist/commands/task/shared.d.ts.map +1 -1
- package/dist/commands/task/shared.js +21 -1
- package/dist/commands/task/verify-record.d.ts.map +1 -1
- package/dist/commands/task/verify-record.js +2 -0
- package/dist/commands/workflow-build.command.d.ts +8 -0
- package/dist/commands/workflow-build.command.d.ts.map +1 -0
- package/dist/commands/workflow-build.command.js +96 -0
- package/dist/commands/workflow-playbook.command.d.ts +10 -0
- package/dist/commands/workflow-playbook.command.d.ts.map +1 -0
- package/dist/commands/workflow-playbook.command.js +174 -0
- package/dist/commands/workflow-restore.command.d.ts +5 -0
- package/dist/commands/workflow-restore.command.d.ts.map +1 -0
- package/dist/commands/workflow-restore.command.js +30 -0
- package/dist/commands/workflow.command.d.ts +6 -0
- package/dist/commands/workflow.command.d.ts.map +1 -0
- package/dist/commands/workflow.command.js +36 -0
- package/dist/harness/dynamic-tool-contract.d.ts +29 -0
- package/dist/harness/dynamic-tool-contract.d.ts.map +1 -0
- package/dist/harness/dynamic-tool-contract.js +86 -0
- package/dist/harness/hooks-lifecycle.d.ts +27 -0
- package/dist/harness/hooks-lifecycle.d.ts.map +1 -0
- package/dist/harness/hooks-lifecycle.js +67 -0
- package/dist/harness/index.d.ts +9 -0
- package/dist/harness/index.d.ts.map +1 -0
- package/dist/harness/index.js +8 -0
- package/dist/harness/reconcile.d.ts +37 -0
- package/dist/harness/reconcile.d.ts.map +1 -0
- package/dist/harness/reconcile.js +42 -0
- package/dist/harness/retry-policy.d.ts +31 -0
- package/dist/harness/retry-policy.d.ts.map +1 -0
- package/dist/harness/retry-policy.js +33 -0
- package/dist/harness/scheduler.d.ts +18 -0
- package/dist/harness/scheduler.d.ts.map +1 -0
- package/dist/harness/scheduler.js +55 -0
- package/dist/harness/state-machine.d.ts +17 -0
- package/dist/harness/state-machine.d.ts.map +1 -0
- package/dist/harness/state-machine.js +70 -0
- package/dist/harness/token-accounting.d.ts +19 -0
- package/dist/harness/token-accounting.d.ts.map +1 -0
- package/dist/harness/token-accounting.js +77 -0
- package/dist/harness/workspace-safety.d.ts +14 -0
- package/dist/harness/workspace-safety.d.ts.map +1 -0
- package/dist/harness/workspace-safety.js +62 -0
- package/dist/policy/rules/allowlist.d.ts.map +1 -1
- package/dist/policy/rules/allowlist.js +9 -0
- package/dist/recipes/bundled-recipes.d.ts +4 -0
- package/dist/recipes/bundled-recipes.d.ts.map +1 -1
- package/dist/recipes/bundled-recipes.js +11 -0
- package/dist/shared/allow-prefix-policy.d.ts +3 -0
- package/dist/shared/allow-prefix-policy.d.ts.map +1 -0
- package/dist/shared/allow-prefix-policy.js +8 -0
- package/dist/shared/errors.d.ts +6 -0
- package/dist/shared/errors.d.ts.map +1 -1
- package/dist/shared/errors.js +1 -0
- package/dist/workflow-runtime/build.d.ts +4 -0
- package/dist/workflow-runtime/build.d.ts.map +1 -0
- package/dist/workflow-runtime/build.js +114 -0
- package/dist/workflow-runtime/enforcement.d.ts +3 -0
- package/dist/workflow-runtime/enforcement.d.ts.map +1 -0
- package/dist/workflow-runtime/enforcement.js +10 -0
- package/dist/workflow-runtime/file-ops.d.ts +11 -0
- package/dist/workflow-runtime/file-ops.d.ts.map +1 -0
- package/dist/workflow-runtime/file-ops.js +248 -0
- package/dist/workflow-runtime/fix.d.ts +9 -0
- package/dist/workflow-runtime/fix.d.ts.map +1 -0
- package/dist/workflow-runtime/fix.js +107 -0
- package/dist/workflow-runtime/index.d.ts +11 -0
- package/dist/workflow-runtime/index.d.ts.map +1 -0
- package/dist/workflow-runtime/index.js +10 -0
- package/dist/workflow-runtime/markdown.d.ts +10 -0
- package/dist/workflow-runtime/markdown.d.ts.map +1 -0
- package/dist/workflow-runtime/markdown.js +147 -0
- package/dist/workflow-runtime/observability.d.ts +12 -0
- package/dist/workflow-runtime/observability.d.ts.map +1 -0
- package/dist/workflow-runtime/observability.js +14 -0
- package/dist/workflow-runtime/paths.d.ts +3 -0
- package/dist/workflow-runtime/paths.d.ts.map +1 -0
- package/dist/workflow-runtime/paths.js +11 -0
- package/dist/workflow-runtime/template.d.ts +7 -0
- package/dist/workflow-runtime/template.d.ts.map +1 -0
- package/dist/workflow-runtime/template.js +94 -0
- package/dist/workflow-runtime/types.d.ts +68 -0
- package/dist/workflow-runtime/types.d.ts.map +1 -0
- package/dist/workflow-runtime/types.js +1 -0
- package/dist/workflow-runtime/validate.d.ts +8 -0
- package/dist/workflow-runtime/validate.d.ts.map +1 -0
- package/dist/workflow-runtime/validate.js +331 -0
- package/package.json +3 -3
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveProject } from "@agentplaneorg/core";
|
|
3
|
+
import { successMessage, warnMessage } from "../cli/output.js";
|
|
4
|
+
import { resolveWorkflowPaths, restoreWorkflowFromLastKnownGood, } from "../workflow-runtime/index.js";
|
|
5
|
+
export const workflowRestoreSpec = {
|
|
6
|
+
id: ["workflow", "restore"],
|
|
7
|
+
group: "Workflow",
|
|
8
|
+
summary: "Restore WORKFLOW.md from last-known-good snapshot.",
|
|
9
|
+
parse: () => ({}),
|
|
10
|
+
examples: [
|
|
11
|
+
{
|
|
12
|
+
cmd: "agentplane workflow restore",
|
|
13
|
+
why: "Restore active workflow from .agentplane/workflows/last-known-good.md.",
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
export const runWorkflowRestore = async (ctx) => {
|
|
18
|
+
const resolved = await resolveProject({ cwd: ctx.cwd, rootOverride: ctx.rootOverride ?? null });
|
|
19
|
+
const workflowPaths = resolveWorkflowPaths(resolved.gitRoot);
|
|
20
|
+
const result = await restoreWorkflowFromLastKnownGood(resolved.gitRoot);
|
|
21
|
+
if (!result.ok) {
|
|
22
|
+
process.stderr.write(warnMessage("workflow restore failed") + "\n");
|
|
23
|
+
for (const diagnostic of result.diagnostics) {
|
|
24
|
+
process.stderr.write(`- [${diagnostic.severity}] ${diagnostic.code} ${diagnostic.path}: ${diagnostic.message}\n`);
|
|
25
|
+
}
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
process.stdout.write(successMessage("workflow restore", undefined, `Restored ${path.relative(resolved.gitRoot, workflowPaths.workflowPath)} from snapshot.`) + "\n");
|
|
29
|
+
return 0;
|
|
30
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CommandHandler, CommandSpec } from "../cli/spec/spec.js";
|
|
2
|
+
type WorkflowParsed = Record<string, never>;
|
|
3
|
+
export declare const workflowSpec: CommandSpec<WorkflowParsed>;
|
|
4
|
+
export declare const runWorkflow: CommandHandler<WorkflowParsed>;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=workflow.command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow.command.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAInF,KAAK,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAI5C,eAAO,MAAM,YAAY,EAAE,WAAW,CAAC,cAAc,CAyBpD,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,cAAc,CAAC,cAAc,CAMtD,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { usageError } from "../cli/spec/errors.js";
|
|
2
|
+
import { suggestOne } from "../cli/spec/suggest.js";
|
|
3
|
+
const WORKFLOW_SUBCOMMANDS = ["build", "restore", "debug", "sync", "land"];
|
|
4
|
+
export const workflowSpec = {
|
|
5
|
+
id: ["workflow"],
|
|
6
|
+
group: "Workflow",
|
|
7
|
+
summary: "Workflow contract commands.",
|
|
8
|
+
synopsis: ["agentplane workflow <subcommand> [options]"],
|
|
9
|
+
args: [{ name: "subcommand", required: false, variadic: true, valueHint: "<subcommand>" }],
|
|
10
|
+
parse: () => ({}),
|
|
11
|
+
validateRaw: (raw) => {
|
|
12
|
+
const rest = Array.isArray(raw.args.subcommand) ? raw.args.subcommand : [];
|
|
13
|
+
const sub = rest[0];
|
|
14
|
+
if (!sub) {
|
|
15
|
+
throw usageError({
|
|
16
|
+
spec: workflowSpec,
|
|
17
|
+
command: "workflow",
|
|
18
|
+
message: "Missing workflow subcommand.",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const suggestion = suggestOne(String(sub), [...WORKFLOW_SUBCOMMANDS]);
|
|
22
|
+
const suffix = suggestion ? ` Did you mean: ${suggestion}?` : "";
|
|
23
|
+
throw usageError({
|
|
24
|
+
spec: workflowSpec,
|
|
25
|
+
command: "workflow",
|
|
26
|
+
message: `Unknown workflow subcommand: ${String(sub)}.${suffix}`,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
export const runWorkflow = (_ctx) => {
|
|
31
|
+
throw usageError({
|
|
32
|
+
spec: workflowSpec,
|
|
33
|
+
command: "workflow",
|
|
34
|
+
message: "Missing workflow subcommand.",
|
|
35
|
+
});
|
|
36
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type DynamicToolInputSchema = {
|
|
2
|
+
required?: string[];
|
|
3
|
+
properties?: Record<string, {
|
|
4
|
+
type: "string" | "number" | "boolean" | "object" | "array";
|
|
5
|
+
}>;
|
|
6
|
+
additionalProperties?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type DynamicToolSpec = {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
inputSchema: DynamicToolInputSchema;
|
|
12
|
+
};
|
|
13
|
+
export type DynamicToolRegistry = Record<string, DynamicToolSpec>;
|
|
14
|
+
export type DynamicToolResponse = {
|
|
15
|
+
success: boolean;
|
|
16
|
+
code: "TOOL_OK" | "TOOL_UNSUPPORTED" | "TOOL_INVALID_ARGS" | "TOOL_EXECUTION_FAILED";
|
|
17
|
+
data?: unknown;
|
|
18
|
+
error?: {
|
|
19
|
+
message: string;
|
|
20
|
+
reason?: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export declare function executeDynamicTool(opts: {
|
|
24
|
+
registry: DynamicToolRegistry;
|
|
25
|
+
handlers: Record<string, (args: Record<string, unknown>) => unknown>;
|
|
26
|
+
toolName: string;
|
|
27
|
+
args: unknown;
|
|
28
|
+
}): Promise<DynamicToolResponse>;
|
|
29
|
+
//# sourceMappingURL=dynamic-tool-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-tool-contract.d.ts","sourceRoot":"","sources":["../../src/harness/dynamic-tool-contract.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5F,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,sBAAsB,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;AAElE,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,SAAS,GAAG,kBAAkB,GAAG,mBAAmB,GAAG,uBAAuB,CAAC;IACrF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,CAAC;AA+CF,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAiD/B"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
function isObject(value) {
|
|
2
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function matchesType(value, type) {
|
|
5
|
+
if (type === "array")
|
|
6
|
+
return Array.isArray(value);
|
|
7
|
+
if (type === "object")
|
|
8
|
+
return isObject(value);
|
|
9
|
+
return typeof value === type;
|
|
10
|
+
}
|
|
11
|
+
function validateInput(schema, args) {
|
|
12
|
+
if (!isObject(args)) {
|
|
13
|
+
return { ok: false, message: "Tool arguments must be an object." };
|
|
14
|
+
}
|
|
15
|
+
const required = schema.required ?? [];
|
|
16
|
+
for (const key of required) {
|
|
17
|
+
if (!(key in args)) {
|
|
18
|
+
return { ok: false, message: `Missing required field: ${key}` };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const properties = schema.properties ?? {};
|
|
22
|
+
for (const [key, value] of Object.entries(args)) {
|
|
23
|
+
const propSchema = properties[key];
|
|
24
|
+
if (!propSchema) {
|
|
25
|
+
if (schema.additionalProperties === false) {
|
|
26
|
+
return { ok: false, message: `Unknown argument: ${key}` };
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!matchesType(value, propSchema.type)) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
message: `Invalid type for ${key}: expected ${propSchema.type}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { ok: true };
|
|
38
|
+
}
|
|
39
|
+
export async function executeDynamicTool(opts) {
|
|
40
|
+
const spec = opts.registry[opts.toolName];
|
|
41
|
+
if (!spec) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
code: "TOOL_UNSUPPORTED",
|
|
45
|
+
error: {
|
|
46
|
+
message: `Unsupported tool: ${opts.toolName}`,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const valid = validateInput(spec.inputSchema, opts.args);
|
|
51
|
+
if (!valid.ok) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
code: "TOOL_INVALID_ARGS",
|
|
55
|
+
error: {
|
|
56
|
+
message: valid.message ?? "Invalid tool arguments.",
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const handler = opts.handlers[opts.toolName];
|
|
61
|
+
if (!handler) {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
code: "TOOL_UNSUPPORTED",
|
|
65
|
+
error: { message: `No handler registered for tool: ${opts.toolName}` },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const data = await handler(opts.args);
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
code: "TOOL_OK",
|
|
73
|
+
data,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
80
|
+
error: {
|
|
81
|
+
message: "Tool execution failed.",
|
|
82
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type HookName = "after_create" | "before_run" | "after_run" | "before_remove";
|
|
2
|
+
export type HookPolicy = {
|
|
3
|
+
command?: string;
|
|
4
|
+
timeoutMs: number;
|
|
5
|
+
blocking: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type HookResult = {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
hook: HookName;
|
|
10
|
+
exitCode: number | null;
|
|
11
|
+
timedOut: boolean;
|
|
12
|
+
output: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function runLifecycleHook(opts: {
|
|
15
|
+
hook: HookName;
|
|
16
|
+
policy: HookPolicy;
|
|
17
|
+
cwd: string;
|
|
18
|
+
}): Promise<HookResult>;
|
|
19
|
+
export declare function runLifecycleHooks(opts: {
|
|
20
|
+
cwd: string;
|
|
21
|
+
hooks: Partial<Record<HookName, HookPolicy>>;
|
|
22
|
+
order: HookName[];
|
|
23
|
+
}): Promise<{
|
|
24
|
+
ok: boolean;
|
|
25
|
+
results: HookResult[];
|
|
26
|
+
}>;
|
|
27
|
+
//# sourceMappingURL=hooks-lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks-lifecycle.d.ts","sourceRoot":"","sources":["../../src/harness/hooks-lifecycle.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,eAAe,CAAC;AAErF,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,UAAU,CAAC,CAyDtB;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;IAC7C,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC,CAclD"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export async function runLifecycleHook(opts) {
|
|
3
|
+
if (!opts.policy.command || opts.policy.command.trim().length === 0) {
|
|
4
|
+
return {
|
|
5
|
+
ok: true,
|
|
6
|
+
hook: opts.hook,
|
|
7
|
+
exitCode: 0,
|
|
8
|
+
timedOut: false,
|
|
9
|
+
output: "",
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
return await new Promise((resolve) => {
|
|
13
|
+
const child = spawn("sh", ["-lc", opts.policy.command], {
|
|
14
|
+
cwd: opts.cwd,
|
|
15
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
16
|
+
});
|
|
17
|
+
let output = "";
|
|
18
|
+
let finished = false;
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
if (finished)
|
|
21
|
+
return;
|
|
22
|
+
finished = true;
|
|
23
|
+
child.kill("SIGKILL");
|
|
24
|
+
resolve({
|
|
25
|
+
ok: !opts.policy.blocking,
|
|
26
|
+
hook: opts.hook,
|
|
27
|
+
exitCode: null,
|
|
28
|
+
timedOut: true,
|
|
29
|
+
output,
|
|
30
|
+
});
|
|
31
|
+
}, Math.max(1, opts.policy.timeoutMs));
|
|
32
|
+
child.stdout.on("data", (chunk) => {
|
|
33
|
+
output += String(chunk);
|
|
34
|
+
});
|
|
35
|
+
child.stderr.on("data", (chunk) => {
|
|
36
|
+
output += String(chunk);
|
|
37
|
+
});
|
|
38
|
+
child.on("close", (code) => {
|
|
39
|
+
if (finished)
|
|
40
|
+
return;
|
|
41
|
+
finished = true;
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
const success = code === 0;
|
|
44
|
+
resolve({
|
|
45
|
+
ok: success || !opts.policy.blocking,
|
|
46
|
+
hook: opts.hook,
|
|
47
|
+
exitCode: code,
|
|
48
|
+
timedOut: false,
|
|
49
|
+
output,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export async function runLifecycleHooks(opts) {
|
|
55
|
+
const results = [];
|
|
56
|
+
for (const hook of opts.order) {
|
|
57
|
+
const policy = opts.hooks[hook];
|
|
58
|
+
if (!policy)
|
|
59
|
+
continue;
|
|
60
|
+
const result = await runLifecycleHook({ hook, policy, cwd: opts.cwd });
|
|
61
|
+
results.push(result);
|
|
62
|
+
if (!result.ok && policy.blocking) {
|
|
63
|
+
return { ok: false, results };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, results };
|
|
67
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./state-machine.js";
|
|
2
|
+
export * from "./scheduler.js";
|
|
3
|
+
export * from "./reconcile.js";
|
|
4
|
+
export * from "./retry-policy.js";
|
|
5
|
+
export * from "./workspace-safety.js";
|
|
6
|
+
export * from "./hooks-lifecycle.js";
|
|
7
|
+
export * from "./dynamic-tool-contract.js";
|
|
8
|
+
export * from "./token-accounting.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/harness/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./state-machine.js";
|
|
2
|
+
export * from "./scheduler.js";
|
|
3
|
+
export * from "./reconcile.js";
|
|
4
|
+
export * from "./retry-policy.js";
|
|
5
|
+
export * from "./workspace-safety.js";
|
|
6
|
+
export * from "./hooks-lifecycle.js";
|
|
7
|
+
export * from "./dynamic-tool-contract.js";
|
|
8
|
+
export * from "./token-accounting.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { OrchestrationState } from "./state-machine.js";
|
|
2
|
+
export type ReconcileRunningEntry = {
|
|
3
|
+
issueId: string;
|
|
4
|
+
issueIdentifier: string;
|
|
5
|
+
state: string;
|
|
6
|
+
orchestrationState: OrchestrationState;
|
|
7
|
+
startedAt: Date;
|
|
8
|
+
lastActivityAt: Date;
|
|
9
|
+
};
|
|
10
|
+
export type ReconcileObservedIssue = {
|
|
11
|
+
id: string;
|
|
12
|
+
state: string;
|
|
13
|
+
assignedToWorker: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type ReconcileConfig = {
|
|
16
|
+
activeStates: string[];
|
|
17
|
+
terminalStates: string[];
|
|
18
|
+
stallTimeoutMs: number;
|
|
19
|
+
};
|
|
20
|
+
export type ReconcileAction = {
|
|
21
|
+
type: "stop_running";
|
|
22
|
+
issueId: string;
|
|
23
|
+
reason: "terminal" | "non_active" | "not_routed";
|
|
24
|
+
} | {
|
|
25
|
+
type: "restart_stalled";
|
|
26
|
+
issueId: string;
|
|
27
|
+
elapsedMs: number;
|
|
28
|
+
} | {
|
|
29
|
+
type: "keep_running";
|
|
30
|
+
issueId: string;
|
|
31
|
+
};
|
|
32
|
+
export type ReconcileResult = {
|
|
33
|
+
nextRunning: ReconcileRunningEntry[];
|
|
34
|
+
actions: ReconcileAction[];
|
|
35
|
+
};
|
|
36
|
+
export declare function reconcileFirst(running: readonly ReconcileRunningEntry[], observed: readonly ReconcileObservedIssue[], config: ReconcileConfig, now: Date): ReconcileResult;
|
|
37
|
+
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/harness/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE7D,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,SAAS,EAAE,IAAI,CAAC;IAChB,cAAc,EAAE,IAAI,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,GAAG,YAAY,GAAG,YAAY,CAAA;CAAE,GAC3F;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,qBAAqB,EAAE,CAAC;IACrC,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B,CAAC;AAWF,wBAAgB,cAAc,CAC5B,OAAO,EAAE,SAAS,qBAAqB,EAAE,EACzC,QAAQ,EAAE,SAAS,sBAAsB,EAAE,EAC3C,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,IAAI,GACR,eAAe,CA0CjB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function normalizeState(value) {
|
|
2
|
+
return value.trim().toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
function isStateInSet(value, set) {
|
|
5
|
+
const normalized = normalizeState(value);
|
|
6
|
+
return set.some((item) => normalizeState(item) === normalized);
|
|
7
|
+
}
|
|
8
|
+
export function reconcileFirst(running, observed, config, now) {
|
|
9
|
+
const observedById = new Map(observed.map((issue) => [issue.id, issue]));
|
|
10
|
+
const nextRunning = [];
|
|
11
|
+
const actions = [];
|
|
12
|
+
for (const entry of running) {
|
|
13
|
+
const observedIssue = observedById.get(entry.issueId);
|
|
14
|
+
if (!observedIssue) {
|
|
15
|
+
actions.push({ type: "stop_running", issueId: entry.issueId, reason: "non_active" });
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (isStateInSet(observedIssue.state, config.terminalStates)) {
|
|
19
|
+
actions.push({ type: "stop_running", issueId: entry.issueId, reason: "terminal" });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!observedIssue.assignedToWorker) {
|
|
23
|
+
actions.push({ type: "stop_running", issueId: entry.issueId, reason: "not_routed" });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!isStateInSet(observedIssue.state, config.activeStates)) {
|
|
27
|
+
actions.push({ type: "stop_running", issueId: entry.issueId, reason: "non_active" });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const elapsedMs = Math.max(0, now.getTime() - entry.lastActivityAt.getTime());
|
|
31
|
+
if (config.stallTimeoutMs > 0 && elapsedMs > config.stallTimeoutMs) {
|
|
32
|
+
actions.push({ type: "restart_stalled", issueId: entry.issueId, elapsedMs });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
actions.push({ type: "keep_running", issueId: entry.issueId });
|
|
36
|
+
nextRunning.push({ ...entry, state: observedIssue.state });
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
nextRunning,
|
|
40
|
+
actions,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type RetryReasonCode = "continuation_needed" | "worker_exit_abnormal" | "worker_spawn_failed" | "stalled_timeout" | "poll_failed" | "no_slots" | "reconcile_conflict";
|
|
2
|
+
export type RetryDelayType = "continuation" | "failure";
|
|
3
|
+
export type RetryPolicy = {
|
|
4
|
+
continuationDelayMs: number;
|
|
5
|
+
failureBaseDelayMs: number;
|
|
6
|
+
maxBackoffMs: number;
|
|
7
|
+
maxAttempts: number;
|
|
8
|
+
};
|
|
9
|
+
export type RetryEntry = {
|
|
10
|
+
issueId: string;
|
|
11
|
+
attempt: number;
|
|
12
|
+
dueAtMs: number;
|
|
13
|
+
reasonCode: RetryReasonCode;
|
|
14
|
+
reasonMessage?: string;
|
|
15
|
+
delayType: RetryDelayType;
|
|
16
|
+
};
|
|
17
|
+
export declare function computeRetryDelayMs(attempt: number, delayType: RetryDelayType, policy?: RetryPolicy): number;
|
|
18
|
+
export declare function scheduleRetryEntry(opts: {
|
|
19
|
+
issueId: string;
|
|
20
|
+
attempt: number;
|
|
21
|
+
reasonCode: RetryReasonCode;
|
|
22
|
+
reasonMessage?: string;
|
|
23
|
+
delayType: RetryDelayType;
|
|
24
|
+
nowMs: number;
|
|
25
|
+
policy?: RetryPolicy;
|
|
26
|
+
}): RetryEntry;
|
|
27
|
+
export declare function canRetry(attempt: number, policy?: RetryPolicy): {
|
|
28
|
+
allowed: boolean;
|
|
29
|
+
terminal: boolean;
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=retry-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-policy.d.ts","sourceRoot":"","sources":["../../src/harness/retry-policy.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,qBAAqB,GACrB,sBAAsB,GACtB,qBAAqB,GACrB,iBAAiB,GACjB,aAAa,GACb,UAAU,GACV,oBAAoB,CAAC;AAEzB,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,SAAS,CAAC;AAExD,MAAM,MAAM,WAAW,GAAG;IACxB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,eAAe,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,cAAc,CAAC;CAC3B,CAAC;AASF,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,cAAc,EACzB,MAAM,GAAE,WAA4B,GACnC,MAAM,CAOR;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,eAAe,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,cAAc,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,UAAU,CAYb;AAED,wBAAgB,QAAQ,CACtB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,WAA4B,GACnC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAKzC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const DEFAULT_POLICY = {
|
|
2
|
+
continuationDelayMs: 1000,
|
|
3
|
+
failureBaseDelayMs: 10_000,
|
|
4
|
+
maxBackoffMs: 300_000,
|
|
5
|
+
maxAttempts: 5,
|
|
6
|
+
};
|
|
7
|
+
export function computeRetryDelayMs(attempt, delayType, policy = DEFAULT_POLICY) {
|
|
8
|
+
if (delayType === "continuation" && attempt <= 1) {
|
|
9
|
+
return policy.continuationDelayMs;
|
|
10
|
+
}
|
|
11
|
+
const power = Math.max(0, Math.min(attempt - 1, 10));
|
|
12
|
+
const delayed = policy.failureBaseDelayMs * 2 ** power;
|
|
13
|
+
return Math.min(delayed, policy.maxBackoffMs);
|
|
14
|
+
}
|
|
15
|
+
export function scheduleRetryEntry(opts) {
|
|
16
|
+
const policy = opts.policy ?? DEFAULT_POLICY;
|
|
17
|
+
const safeAttempt = Math.max(1, opts.attempt);
|
|
18
|
+
const delayMs = computeRetryDelayMs(safeAttempt, opts.delayType, policy);
|
|
19
|
+
return {
|
|
20
|
+
issueId: opts.issueId,
|
|
21
|
+
attempt: safeAttempt,
|
|
22
|
+
dueAtMs: opts.nowMs + delayMs,
|
|
23
|
+
reasonCode: opts.reasonCode,
|
|
24
|
+
reasonMessage: opts.reasonMessage,
|
|
25
|
+
delayType: opts.delayType,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function canRetry(attempt, policy = DEFAULT_POLICY) {
|
|
29
|
+
if (attempt < policy.maxAttempts) {
|
|
30
|
+
return { allowed: true, terminal: false };
|
|
31
|
+
}
|
|
32
|
+
return { allowed: false, terminal: true };
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type SchedulerIssue = {
|
|
2
|
+
id: string;
|
|
3
|
+
state: string;
|
|
4
|
+
priority?: number | null;
|
|
5
|
+
createdAt?: string | null;
|
|
6
|
+
};
|
|
7
|
+
export type SchedulerEntry = {
|
|
8
|
+
issue: SchedulerIssue;
|
|
9
|
+
workerId: string;
|
|
10
|
+
};
|
|
11
|
+
export type SchedulerLimits = {
|
|
12
|
+
maxConcurrent: number;
|
|
13
|
+
maxConcurrentByState?: Record<string, number>;
|
|
14
|
+
};
|
|
15
|
+
export declare function sortIssuesForDispatch(issues: readonly SchedulerIssue[]): SchedulerIssue[];
|
|
16
|
+
export declare function canDispatchIssue(issue: SchedulerIssue, running: readonly SchedulerEntry[], limits: SchedulerLimits): boolean;
|
|
17
|
+
export declare function planDispatch(candidates: readonly SchedulerIssue[], running: readonly SchedulerEntry[], limits: SchedulerLimits): SchedulerIssue[];
|
|
18
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/harness/scheduler.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,cAAc,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/C,CAAC;AAoBF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,GAAG,cAAc,EAAE,CAYzF;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,SAAS,cAAc,EAAE,EAClC,MAAM,EAAE,eAAe,GACtB,OAAO,CAaT;AAED,wBAAgB,YAAY,CAC1B,UAAU,EAAE,SAAS,cAAc,EAAE,EACrC,OAAO,EAAE,SAAS,cAAc,EAAE,EAClC,MAAM,EAAE,eAAe,GACtB,cAAc,EAAE,CAYlB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function normalizeState(value) {
|
|
2
|
+
return value.trim().toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
function createdAtMicros(value) {
|
|
5
|
+
if (!value)
|
|
6
|
+
return Number.MAX_SAFE_INTEGER;
|
|
7
|
+
const parsed = Date.parse(value);
|
|
8
|
+
if (Number.isNaN(parsed))
|
|
9
|
+
return Number.MAX_SAFE_INTEGER;
|
|
10
|
+
return parsed * 1000;
|
|
11
|
+
}
|
|
12
|
+
function priorityRank(priority) {
|
|
13
|
+
if (typeof priority !== "number")
|
|
14
|
+
return 5;
|
|
15
|
+
if (!Number.isInteger(priority))
|
|
16
|
+
return 5;
|
|
17
|
+
if (priority < 1 || priority > 4)
|
|
18
|
+
return 5;
|
|
19
|
+
return priority;
|
|
20
|
+
}
|
|
21
|
+
export function sortIssuesForDispatch(issues) {
|
|
22
|
+
return issues.toSorted((a, b) => {
|
|
23
|
+
const aPriority = priorityRank(a.priority);
|
|
24
|
+
const bPriority = priorityRank(b.priority);
|
|
25
|
+
if (aPriority !== bPriority)
|
|
26
|
+
return aPriority - bPriority;
|
|
27
|
+
const aCreated = createdAtMicros(a.createdAt);
|
|
28
|
+
const bCreated = createdAtMicros(b.createdAt);
|
|
29
|
+
if (aCreated !== bCreated)
|
|
30
|
+
return aCreated - bCreated;
|
|
31
|
+
return a.id.localeCompare(b.id);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function canDispatchIssue(issue, running, limits) {
|
|
35
|
+
const totalRunning = running.length;
|
|
36
|
+
if (totalRunning >= limits.maxConcurrent)
|
|
37
|
+
return false;
|
|
38
|
+
const stateKey = normalizeState(issue.state);
|
|
39
|
+
const stateLimitRaw = limits.maxConcurrentByState?.[stateKey];
|
|
40
|
+
const stateLimit = typeof stateLimitRaw === "number" && stateLimitRaw > 0 ? stateLimitRaw : limits.maxConcurrent;
|
|
41
|
+
const stateUsed = running.filter((entry) => normalizeState(entry.issue.state) === stateKey).length;
|
|
42
|
+
return stateUsed < stateLimit;
|
|
43
|
+
}
|
|
44
|
+
export function planDispatch(candidates, running, limits) {
|
|
45
|
+
const ordered = sortIssuesForDispatch(candidates);
|
|
46
|
+
const accepted = [];
|
|
47
|
+
const shadowRunning = [...running];
|
|
48
|
+
for (const issue of ordered) {
|
|
49
|
+
if (!canDispatchIssue(issue, shadowRunning, limits))
|
|
50
|
+
continue;
|
|
51
|
+
accepted.push(issue);
|
|
52
|
+
shadowRunning.push({ issue, workerId: `planned:${issue.id}` });
|
|
53
|
+
}
|
|
54
|
+
return accepted;
|
|
55
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type OrchestrationState = "unclaimed" | "claimed" | "running" | "retry_queued" | "released";
|
|
2
|
+
export type OrchestrationEvent = "claim" | "start" | "queue_retry" | "release" | "mark_released" | "reset";
|
|
3
|
+
export type TransitionResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
next: OrchestrationState;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
code: "ORCH_INVALID_TRANSITION";
|
|
9
|
+
message: string;
|
|
10
|
+
current: OrchestrationState;
|
|
11
|
+
event: OrchestrationEvent;
|
|
12
|
+
};
|
|
13
|
+
export declare function transitionOrchestrationState(current: OrchestrationState, event: OrchestrationEvent, opts?: {
|
|
14
|
+
strict?: boolean;
|
|
15
|
+
}): TransitionResult;
|
|
16
|
+
export declare function assertTransitionOrThrow(current: OrchestrationState, event: OrchestrationEvent): OrchestrationState;
|
|
17
|
+
//# sourceMappingURL=state-machine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-machine.d.ts","sourceRoot":"","sources":["../../src/harness/state-machine.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,GAAG,UAAU,CAAC;AAEnG,MAAM,MAAM,kBAAkB,GAC1B,OAAO,GACP,OAAO,GACP,aAAa,GACb,SAAS,GACT,eAAe,GACf,OAAO,CAAC;AAEZ,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,kBAAkB,CAAA;CAAE,GACtC;IACE,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,KAAK,EAAE,kBAAkB,CAAC;CAC3B,CAAC;AAqDN,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,kBAAkB,EACzB,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC1B,gBAAgB,CAalB;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,CAMpB"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const TRANSITIONS = {
|
|
2
|
+
unclaimed: {
|
|
3
|
+
claim: "claimed",
|
|
4
|
+
start: "unclaimed",
|
|
5
|
+
queue_retry: "unclaimed",
|
|
6
|
+
release: "released",
|
|
7
|
+
mark_released: "released",
|
|
8
|
+
reset: "unclaimed",
|
|
9
|
+
},
|
|
10
|
+
claimed: {
|
|
11
|
+
claim: "claimed",
|
|
12
|
+
start: "running",
|
|
13
|
+
queue_retry: "retry_queued",
|
|
14
|
+
release: "released",
|
|
15
|
+
mark_released: "released",
|
|
16
|
+
reset: "unclaimed",
|
|
17
|
+
},
|
|
18
|
+
running: {
|
|
19
|
+
claim: "running",
|
|
20
|
+
start: "running",
|
|
21
|
+
queue_retry: "retry_queued",
|
|
22
|
+
release: "released",
|
|
23
|
+
mark_released: "released",
|
|
24
|
+
reset: "unclaimed",
|
|
25
|
+
},
|
|
26
|
+
retry_queued: {
|
|
27
|
+
claim: "claimed",
|
|
28
|
+
start: "running",
|
|
29
|
+
queue_retry: "retry_queued",
|
|
30
|
+
release: "released",
|
|
31
|
+
mark_released: "released",
|
|
32
|
+
reset: "unclaimed",
|
|
33
|
+
},
|
|
34
|
+
released: {
|
|
35
|
+
claim: "claimed",
|
|
36
|
+
start: "released",
|
|
37
|
+
queue_retry: "released",
|
|
38
|
+
release: "released",
|
|
39
|
+
mark_released: "released",
|
|
40
|
+
reset: "unclaimed",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const STRICT_ALLOWED = {
|
|
44
|
+
unclaimed: ["claim", "mark_released", "reset"],
|
|
45
|
+
claimed: ["start", "queue_retry", "release", "reset"],
|
|
46
|
+
running: ["queue_retry", "release", "reset"],
|
|
47
|
+
retry_queued: ["claim", "start", "release", "reset"],
|
|
48
|
+
released: ["claim", "reset"],
|
|
49
|
+
};
|
|
50
|
+
export function transitionOrchestrationState(current, event, opts) {
|
|
51
|
+
const strict = opts?.strict ?? true;
|
|
52
|
+
if (strict && !STRICT_ALLOWED[current].includes(event)) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
code: "ORCH_INVALID_TRANSITION",
|
|
56
|
+
message: `Invalid transition: ${current} --${event}--> ?`,
|
|
57
|
+
current,
|
|
58
|
+
event,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const next = TRANSITIONS[current][event];
|
|
62
|
+
return { ok: true, next };
|
|
63
|
+
}
|
|
64
|
+
export function assertTransitionOrThrow(current, event) {
|
|
65
|
+
const result = transitionOrchestrationState(current, event, { strict: true });
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
throw new Error(`${result.code}: ${result.message}`);
|
|
68
|
+
}
|
|
69
|
+
return result.next;
|
|
70
|
+
}
|