sentinelayer-cli 0.6.2 → 0.8.1
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 +1009 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +64 -63
- package/src/agents/ai-governance/index.js +12 -0
- package/src/agents/ai-governance/tools/base.js +171 -0
- package/src/agents/ai-governance/tools/eval-regression.js +47 -0
- package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
- package/src/agents/ai-governance/tools/index.js +52 -0
- package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
- package/src/agents/ai-governance/tools/provenance-check.js +69 -0
- package/src/agents/backend/index.js +12 -0
- package/src/agents/backend/tools/base.js +189 -0
- package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
- package/src/agents/backend/tools/idempotency-audit.js +105 -0
- package/src/agents/backend/tools/index.js +87 -0
- package/src/agents/backend/tools/retry-audit.js +132 -0
- package/src/agents/backend/tools/timeout-audit.js +144 -0
- package/src/agents/code-quality/index.js +12 -0
- package/src/agents/code-quality/tools/base.js +159 -0
- package/src/agents/code-quality/tools/complexity-measure.js +197 -0
- package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
- package/src/agents/code-quality/tools/cycle-detect.js +49 -0
- package/src/agents/code-quality/tools/dep-graph.js +196 -0
- package/src/agents/code-quality/tools/index.js +89 -0
- package/src/agents/data-layer/index.js +12 -0
- package/src/agents/data-layer/tools/base.js +181 -0
- package/src/agents/data-layer/tools/index-audit.js +165 -0
- package/src/agents/data-layer/tools/index.js +83 -0
- package/src/agents/data-layer/tools/migration-scan.js +135 -0
- package/src/agents/data-layer/tools/query-explain.js +120 -0
- package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
- package/src/agents/documentation/index.js +12 -0
- package/src/agents/documentation/tools/api-diff.js +91 -0
- package/src/agents/documentation/tools/base.js +151 -0
- package/src/agents/documentation/tools/dead-link-check.js +58 -0
- package/src/agents/documentation/tools/docstring-coverage.js +78 -0
- package/src/agents/documentation/tools/index.js +52 -0
- package/src/agents/documentation/tools/readme-freshness.js +61 -0
- package/src/agents/envelope/fix-cycle.js +45 -0
- package/src/agents/envelope/index.js +31 -0
- package/src/agents/envelope/loop.js +150 -0
- package/src/agents/envelope/pulse.js +18 -0
- package/src/agents/envelope/stream.js +40 -0
- package/src/agents/infrastructure/index.js +12 -0
- package/src/agents/infrastructure/tools/base.js +171 -0
- package/src/agents/infrastructure/tools/checkov-run.js +32 -0
- package/src/agents/infrastructure/tools/drift-detect.js +59 -0
- package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
- package/src/agents/infrastructure/tools/index.js +52 -0
- package/src/agents/infrastructure/tools/tflint-run.js +31 -0
- package/src/agents/jules/config/definition.js +160 -160
- package/src/agents/jules/config/system-prompt.js +182 -182
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +17 -17
- package/src/agents/jules/loop.js +460 -450
- package/src/agents/jules/pulse.js +10 -10
- package/src/agents/jules/stream.js +187 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +315 -309
- package/src/agents/jules/tools/aidenid-email.js +189 -189
- package/src/agents/jules/tools/auth-audit.js +1708 -1691
- package/src/agents/jules/tools/dispatch.js +340 -335
- package/src/agents/jules/tools/file-edit.js +2 -2
- package/src/agents/jules/tools/file-read.js +2 -2
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +2 -2
- package/src/agents/jules/tools/grep.js +2 -2
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +2 -2
- package/src/agents/jules/tools/runtime-audit.js +507 -507
- package/src/agents/jules/tools/shell.js +2 -2
- package/src/agents/jules/tools/url-policy.js +100 -100
- package/src/agents/mode.js +113 -0
- package/src/agents/observability/index.js +12 -0
- package/src/agents/observability/tools/alert-audit.js +39 -0
- package/src/agents/observability/tools/base.js +181 -0
- package/src/agents/observability/tools/dashboard-gap.js +42 -0
- package/src/agents/observability/tools/index.js +54 -0
- package/src/agents/observability/tools/log-schema-check.js +74 -0
- package/src/agents/observability/tools/span-coverage.js +74 -0
- package/src/agents/persona-visuals.js +102 -61
- package/src/agents/release/index.js +12 -0
- package/src/agents/release/tools/base.js +181 -0
- package/src/agents/release/tools/changelog-diff.js +86 -0
- package/src/agents/release/tools/feature-flag-audit.js +126 -0
- package/src/agents/release/tools/index.js +61 -0
- package/src/agents/release/tools/rollback-verify.js +129 -0
- package/src/agents/release/tools/semver-check.js +109 -0
- package/src/agents/reliability/index.js +12 -0
- package/src/agents/reliability/tools/backpressure-check.js +129 -0
- package/src/agents/reliability/tools/base.js +181 -0
- package/src/agents/reliability/tools/chaos-probe.js +109 -0
- package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
- package/src/agents/reliability/tools/health-check-audit.js +111 -0
- package/src/agents/reliability/tools/index.js +87 -0
- package/src/agents/run-persona.js +109 -0
- package/src/agents/security/index.js +12 -0
- package/src/agents/security/tools/authz-audit.js +134 -0
- package/src/agents/security/tools/base.js +190 -0
- package/src/agents/security/tools/crypto-review.js +175 -0
- package/src/agents/security/tools/index.js +97 -0
- package/src/agents/security/tools/sast-scan.js +175 -0
- package/src/agents/security/tools/secrets-scan.js +216 -0
- package/src/agents/shared-tools/dispatch-core.js +320 -315
- package/src/agents/shared-tools/file-edit.js +180 -180
- package/src/agents/shared-tools/file-read.js +100 -100
- package/src/agents/shared-tools/glob.js +168 -168
- package/src/agents/shared-tools/grep.js +228 -228
- package/src/agents/shared-tools/index.js +46 -46
- package/src/agents/shared-tools/path-guards.js +161 -161
- package/src/agents/shared-tools/shell.js +383 -383
- package/src/agents/supply-chain/index.js +12 -0
- package/src/agents/supply-chain/tools/attestation-check.js +42 -0
- package/src/agents/supply-chain/tools/base.js +151 -0
- package/src/agents/supply-chain/tools/index.js +52 -0
- package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
- package/src/agents/supply-chain/tools/package-verify.js +56 -0
- package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
- package/src/agents/testing/index.js +12 -0
- package/src/agents/testing/tools/base.js +202 -0
- package/src/agents/testing/tools/coverage-gap.js +144 -0
- package/src/agents/testing/tools/flake-detect.js +125 -0
- package/src/agents/testing/tools/index.js +85 -0
- package/src/agents/testing/tools/mutation-test.js +143 -0
- package/src/agents/testing/tools/snapshot-diff.js +103 -0
- package/src/ai/aidenid.js +1021 -1009
- package/src/ai/client.js +553 -553
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/proxy.js +137 -137
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/gate.js +428 -371
- package/src/auth/http.js +681 -611
- package/src/auth/service.js +1106 -1106
- package/src/auth/session-store.js +813 -813
- package/src/cli.js +257 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1338
- package/src/commands/ai/provision-governance.js +1272 -1272
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1171 -1166
- package/src/commands/auth.js +419 -419
- package/src/commands/chat.js +184 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +20 -10
- package/src/commands/mcp.js +461 -461
- package/src/commands/omargate.js +63 -29
- package/src/commands/persona.js +65 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +865 -872
- package/src/commands/session.js +1238 -0
- package/src/commands/spec.js +771 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +511 -511
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/coord/events-log.js +141 -0
- package/src/coord/handshake.js +719 -0
- package/src/coord/index.js +35 -0
- package/src/coord/paths.js +84 -0
- package/src/coord/priority.js +62 -0
- package/src/coord/tarjan.js +157 -0
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tokenizer.js +160 -0
- package/src/cost/tracker.js +232 -171
- package/src/daemon/artifact-lineage.js +896 -534
- package/src/daemon/assignment-ledger.js +1083 -770
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +1209 -626
- package/src/daemon/fix-cycle.js +384 -377
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/ingest-refresh.js +79 -11
- package/src/daemon/jira-lifecycle.js +767 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/pulse.js +327 -327
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/scope-engine.js +1068 -0
- package/src/daemon/watchdog.js +971 -971
- package/src/events/schema.js +190 -0
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +933 -918
- package/src/ingest/ownership.js +380 -0
- package/src/interactive/index.js +97 -97
- package/src/legacy-cli.js +3228 -2994
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +136 -118
- package/src/review/ai-review.js +672 -679
- package/src/review/compliance-pack.js +389 -0
- package/src/review/investor-dd-config.js +54 -0
- package/src/review/investor-dd-file-loop.js +303 -0
- package/src/review/investor-dd-file-router.js +406 -0
- package/src/review/investor-dd-html-report.js +233 -0
- package/src/review/investor-dd-notification.js +120 -0
- package/src/review/investor-dd-orchestrator.js +405 -0
- package/src/review/investor-dd-persona-runner.js +275 -0
- package/src/review/live-validator.js +253 -0
- package/src/review/local-review.js +1351 -1305
- package/src/review/omargate-interactive.js +68 -68
- package/src/review/omargate-orchestrator.js +492 -300
- package/src/review/persona-prompts.js +484 -296
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +147 -42
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -67
- package/src/scaffold/templates.js +150 -150
- package/src/scan/generator.js +418 -418
- package/src/scan/gh-secrets.js +107 -107
- package/src/session/agent-registry.js +359 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +1396 -0
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +37 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +762 -0
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +704 -0
- package/src/session/stream.js +333 -0
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- package/src/spec/generator.js +619 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +569 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/session-tracker.js +234 -234
- package/src/telemetry/sync.js +203 -203
- package/src/ui/command-hints.js +13 -13
- package/src/ui/markdown.js +220 -220
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ensureWorkItemQueued,
|
|
9
|
+
heartbeatLease,
|
|
10
|
+
leaseWorkItem,
|
|
11
|
+
listAssignments,
|
|
12
|
+
releaseLease,
|
|
13
|
+
} from "../daemon/assignment-ledger.js";
|
|
14
|
+
import { createAgentEvent } from "../events/schema.js";
|
|
15
|
+
import { listAgents } from "./agent-registry.js";
|
|
16
|
+
import { resolveSessionPaths } from "./paths.js";
|
|
17
|
+
import { buildAgentAnalyticsSnapshot, rankAgentsByScore } from "./scoring.js";
|
|
18
|
+
import { appendToStream, readStream } from "./stream.js";
|
|
19
|
+
|
|
20
|
+
const TASK_REGISTRY_SCHEMA_VERSION = "1.0.0";
|
|
21
|
+
const DEFAULT_TASK_LEASE_TTL_MS = 30 * 60 * 1000;
|
|
22
|
+
const DEFAULT_TASK_LIST_LIMIT = 200;
|
|
23
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 10_000;
|
|
24
|
+
const DEFAULT_LOCK_STALE_MS = 30_000;
|
|
25
|
+
const DEFAULT_LOCK_POLL_MS = 25;
|
|
26
|
+
const SCORING_EVENT_WINDOW = 2_000;
|
|
27
|
+
|
|
28
|
+
const TASK_STATUSES = Object.freeze(["PENDING", "ACCEPTED", "COMPLETED", "BLOCKED"]);
|
|
29
|
+
const TASK_STATUS_SET = new Set(TASK_STATUSES);
|
|
30
|
+
const TASK_PRIORITIES = Object.freeze(["P0", "P1", "P2", "when-free"]);
|
|
31
|
+
const TASK_PRIORITY_SET = new Set(["P0", "P1", "P2", "WHEN-FREE"]);
|
|
32
|
+
|
|
33
|
+
function normalizeString(value) {
|
|
34
|
+
return String(value || "").trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeIsoTimestamp(value, fallbackIso = new Date().toISOString()) {
|
|
38
|
+
const normalized = normalizeString(value);
|
|
39
|
+
if (!normalized) {
|
|
40
|
+
return fallbackIso;
|
|
41
|
+
}
|
|
42
|
+
const epoch = Date.parse(normalized);
|
|
43
|
+
if (!Number.isFinite(epoch)) {
|
|
44
|
+
return fallbackIso;
|
|
45
|
+
}
|
|
46
|
+
return new Date(epoch).toISOString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizePositiveInteger(value, fallbackValue) {
|
|
50
|
+
if (value === undefined || value === null || normalizeString(value) === "") {
|
|
51
|
+
return fallbackValue;
|
|
52
|
+
}
|
|
53
|
+
const normalized = Number(value);
|
|
54
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
55
|
+
throw new Error("Value must be a positive integer.");
|
|
56
|
+
}
|
|
57
|
+
return Math.floor(normalized);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeTaskStatus(value, fallbackValue = "PENDING") {
|
|
61
|
+
const normalized = normalizeString(value).toUpperCase();
|
|
62
|
+
if (TASK_STATUS_SET.has(normalized)) {
|
|
63
|
+
return normalized;
|
|
64
|
+
}
|
|
65
|
+
return fallbackValue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeTaskPriority(value, fallbackValue = "when-free") {
|
|
69
|
+
const normalized = normalizeString(value).toUpperCase().replace(/_/g, "-");
|
|
70
|
+
if (!normalized) {
|
|
71
|
+
return fallbackValue;
|
|
72
|
+
}
|
|
73
|
+
if (!TASK_PRIORITY_SET.has(normalized)) {
|
|
74
|
+
return fallbackValue;
|
|
75
|
+
}
|
|
76
|
+
if (normalized === "WHEN-FREE") {
|
|
77
|
+
return "when-free";
|
|
78
|
+
}
|
|
79
|
+
return normalized;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeContext(value) {
|
|
83
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
return { ...value };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeTaskId(value) {
|
|
90
|
+
const normalized = normalizeString(value);
|
|
91
|
+
return normalized || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildTaskId() {
|
|
95
|
+
return `task-${randomUUID().slice(0, 8)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildWorkItemId(taskId) {
|
|
99
|
+
return `session-task-${normalizeString(taskId)}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizeTaskRecord(raw = {}, nowIso = new Date().toISOString()) {
|
|
103
|
+
const createdAt = normalizeIsoTimestamp(raw.createdAt, nowIso);
|
|
104
|
+
const updatedAt = normalizeIsoTimestamp(raw.updatedAt, createdAt);
|
|
105
|
+
return {
|
|
106
|
+
taskId: normalizeTaskId(raw.taskId) || buildTaskId(),
|
|
107
|
+
workItemId: normalizeString(raw.workItemId),
|
|
108
|
+
sessionId: normalizeString(raw.sessionId),
|
|
109
|
+
fromAgentId: normalizeString(raw.fromAgentId),
|
|
110
|
+
toAgentId: normalizeString(raw.toAgentId),
|
|
111
|
+
requestedToAgentId: normalizeString(raw.requestedToAgentId),
|
|
112
|
+
roleFilter: normalizeString(raw.roleFilter).toLowerCase() || null,
|
|
113
|
+
task: normalizeString(raw.task),
|
|
114
|
+
priority: normalizeTaskPriority(raw.priority, "when-free"),
|
|
115
|
+
context: normalizeContext(raw.context),
|
|
116
|
+
status: normalizeTaskStatus(raw.status, "PENDING"),
|
|
117
|
+
createdAt,
|
|
118
|
+
acceptedAt: raw.acceptedAt ? normalizeIsoTimestamp(raw.acceptedAt, updatedAt) : null,
|
|
119
|
+
completedAt: raw.completedAt ? normalizeIsoTimestamp(raw.completedAt, updatedAt) : null,
|
|
120
|
+
result: normalizeString(raw.result) || null,
|
|
121
|
+
updatedAt,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeTaskRegistry(raw = {}, { sessionId, nowIso = new Date().toISOString() } = {}) {
|
|
126
|
+
const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
127
|
+
const tasks = Array.isArray(source.tasks)
|
|
128
|
+
? source.tasks
|
|
129
|
+
.map((item) => normalizeTaskRecord(item, nowIso))
|
|
130
|
+
.filter((item) => normalizeString(item.taskId))
|
|
131
|
+
: [];
|
|
132
|
+
return {
|
|
133
|
+
schemaVersion: TASK_REGISTRY_SCHEMA_VERSION,
|
|
134
|
+
sessionId: normalizeString(source.sessionId) || normalizeString(sessionId),
|
|
135
|
+
updatedAt: normalizeIsoTimestamp(source.updatedAt, nowIso),
|
|
136
|
+
tasks,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function readJsonFile(filePath, { allowMissing = true } = {}) {
|
|
141
|
+
try {
|
|
142
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
143
|
+
return JSON.parse(raw);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (allowMissing && error && typeof error === "object" && error.code === "ENOENT") {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function writeJsonFile(filePath, payload) {
|
|
153
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
154
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
155
|
+
await fsp.writeFile(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
156
|
+
await fsp.rename(tmpPath, filePath);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function ensureSessionExists(paths) {
|
|
160
|
+
try {
|
|
161
|
+
await fsp.access(paths.metadataPath);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
164
|
+
throw new Error(`Session '${paths.sessionId}' was not found.`);
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function acquireLock(
|
|
171
|
+
lockPath,
|
|
172
|
+
{
|
|
173
|
+
timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
|
|
174
|
+
staleMs = DEFAULT_LOCK_STALE_MS,
|
|
175
|
+
pollMs = DEFAULT_LOCK_POLL_MS,
|
|
176
|
+
} = {}
|
|
177
|
+
) {
|
|
178
|
+
const startedAt = Date.now();
|
|
179
|
+
while (true) {
|
|
180
|
+
try {
|
|
181
|
+
await fsp.mkdir(lockPath);
|
|
182
|
+
return;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
const code = error && typeof error === "object" ? error.code : "";
|
|
185
|
+
if (!(code === "EEXIST" || code === "EPERM" || code === "EACCES")) {
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const stat = await fsp.stat(lockPath);
|
|
190
|
+
const ageMs = Date.now() - Number(stat.mtimeMs || 0);
|
|
191
|
+
if (Number.isFinite(ageMs) && ageMs > staleMs) {
|
|
192
|
+
await fsp.rm(lockPath, { recursive: true, force: true });
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// Continue waiting.
|
|
197
|
+
}
|
|
198
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
199
|
+
throw new Error("Timed out waiting for session task lock.");
|
|
200
|
+
}
|
|
201
|
+
await sleep(pollMs);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function releaseLock(lockPath) {
|
|
207
|
+
await fsp.rm(lockPath, { recursive: true, force: true }).catch(() => {});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function mutateTaskRegistry(
|
|
211
|
+
sessionId,
|
|
212
|
+
{
|
|
213
|
+
targetPath = process.cwd(),
|
|
214
|
+
nowIso = new Date().toISOString(),
|
|
215
|
+
} = {},
|
|
216
|
+
mutator = async () => ({})
|
|
217
|
+
) {
|
|
218
|
+
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
219
|
+
await ensureSessionExists(paths);
|
|
220
|
+
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
221
|
+
|
|
222
|
+
await acquireLock(paths.tasksLockPath);
|
|
223
|
+
try {
|
|
224
|
+
const raw = await readJsonFile(paths.tasksPath, { allowMissing: true });
|
|
225
|
+
const registry = normalizeTaskRegistry(raw || {}, {
|
|
226
|
+
sessionId: paths.sessionId,
|
|
227
|
+
nowIso: normalizedNow,
|
|
228
|
+
});
|
|
229
|
+
const result = await mutator(registry, {
|
|
230
|
+
nowIso: normalizedNow,
|
|
231
|
+
paths,
|
|
232
|
+
});
|
|
233
|
+
registry.updatedAt = normalizedNow;
|
|
234
|
+
await writeJsonFile(paths.tasksPath, registry);
|
|
235
|
+
return {
|
|
236
|
+
result,
|
|
237
|
+
registry,
|
|
238
|
+
paths,
|
|
239
|
+
nowIso: normalizedNow,
|
|
240
|
+
};
|
|
241
|
+
} finally {
|
|
242
|
+
await releaseLock(paths.tasksLockPath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function parseStatusFilters(status, statuses = []) {
|
|
247
|
+
const source = Array.isArray(statuses) && statuses.length > 0 ? statuses : status ? [status] : [];
|
|
248
|
+
if (source.length === 0) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const normalized = source
|
|
252
|
+
.map((item) => normalizeTaskStatus(item, ""))
|
|
253
|
+
.filter(Boolean);
|
|
254
|
+
if (normalized.length === 0) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
return new Set(normalized);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function severityForPriority(priority) {
|
|
261
|
+
const normalized = normalizeTaskPriority(priority, "when-free");
|
|
262
|
+
if (normalized === "P0" || normalized === "P1" || normalized === "P2") {
|
|
263
|
+
return normalized;
|
|
264
|
+
}
|
|
265
|
+
return "P3";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function normalizeAssigneeToken(toAgentId) {
|
|
269
|
+
return normalizeString(toAgentId).replace(/^@+/, "");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizeRoleFilter(roleFilter = "") {
|
|
273
|
+
const normalized = normalizeString(roleFilter).toLowerCase();
|
|
274
|
+
return normalized || null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseAssignmentTargetToken(rawToken) {
|
|
278
|
+
const token = normalizeAssigneeToken(rawToken);
|
|
279
|
+
if (!token) {
|
|
280
|
+
throw new Error("assign target is required.");
|
|
281
|
+
}
|
|
282
|
+
const lower = token.toLowerCase();
|
|
283
|
+
if (lower === "*") {
|
|
284
|
+
return {
|
|
285
|
+
wildcard: true,
|
|
286
|
+
requestedToAgentId: "*",
|
|
287
|
+
roleFilter: null,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (lower.startsWith("*:")) {
|
|
291
|
+
return {
|
|
292
|
+
wildcard: true,
|
|
293
|
+
requestedToAgentId: "*",
|
|
294
|
+
roleFilter: normalizeRoleFilter(token.slice(2)),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (lower.endsWith(":*")) {
|
|
298
|
+
return {
|
|
299
|
+
wildcard: true,
|
|
300
|
+
requestedToAgentId: "*",
|
|
301
|
+
roleFilter: normalizeRoleFilter(token.slice(0, -2)),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (lower.startsWith("role:")) {
|
|
305
|
+
return {
|
|
306
|
+
wildcard: true,
|
|
307
|
+
requestedToAgentId: "*",
|
|
308
|
+
roleFilter: normalizeRoleFilter(token.slice(5)),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
wildcard: false,
|
|
313
|
+
requestedToAgentId: token,
|
|
314
|
+
roleFilter: null,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function parseEpoch(value, fallbackIso = new Date().toISOString()) {
|
|
319
|
+
return Date.parse(normalizeIsoTimestamp(value, fallbackIso)) || 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function statusWeight(status = "") {
|
|
323
|
+
const normalized = normalizeString(status).toLowerCase();
|
|
324
|
+
if (normalized === "idle") return 0;
|
|
325
|
+
if (normalized === "watching") return 1;
|
|
326
|
+
if (normalized === "coding" || normalized === "reviewing" || normalized === "testing") return 2;
|
|
327
|
+
if (normalized === "blocked") return 3;
|
|
328
|
+
return 2;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function resolveExplicitAgent(sessionId, requestedAgentId, { targetPath = process.cwd(), roleFilter = null } = {}) {
|
|
332
|
+
const agents = await listAgents(sessionId, {
|
|
333
|
+
targetPath,
|
|
334
|
+
includeInactive: false,
|
|
335
|
+
});
|
|
336
|
+
const normalizedRequested = normalizeString(requestedAgentId).toLowerCase();
|
|
337
|
+
const match = agents.find((agent) => normalizeString(agent.agentId).toLowerCase() === normalizedRequested);
|
|
338
|
+
if (!match) {
|
|
339
|
+
throw new Error(`Target agent '${requestedAgentId}' is not active in session '${sessionId}'.`);
|
|
340
|
+
}
|
|
341
|
+
if (roleFilter && normalizeString(match.role).toLowerCase() !== roleFilter) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`Target agent '${requestedAgentId}' role '${match.role}' does not match required role '${roleFilter}'.`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
...match,
|
|
348
|
+
assignmentCount: 0,
|
|
349
|
+
statusWeight: statusWeight(match.status),
|
|
350
|
+
activityEpoch: parseEpoch(match.lastActivityAt),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function resolveHighestScoringAgent(
|
|
355
|
+
sessionId,
|
|
356
|
+
{
|
|
357
|
+
targetPath = process.cwd(),
|
|
358
|
+
roleFilter = null,
|
|
359
|
+
nowIso = new Date().toISOString(),
|
|
360
|
+
} = {}
|
|
361
|
+
) {
|
|
362
|
+
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
363
|
+
const [agents, activeAssignments, events, rawTaskRegistry] = await Promise.all([
|
|
364
|
+
listAgents(sessionId, {
|
|
365
|
+
targetPath,
|
|
366
|
+
includeInactive: false,
|
|
367
|
+
}),
|
|
368
|
+
listAssignments({
|
|
369
|
+
targetPath,
|
|
370
|
+
sessionId,
|
|
371
|
+
statuses: ["CLAIMED", "IN_PROGRESS"],
|
|
372
|
+
includeExpired: false,
|
|
373
|
+
limit: 500,
|
|
374
|
+
nowIso,
|
|
375
|
+
}),
|
|
376
|
+
readStream(sessionId, {
|
|
377
|
+
targetPath,
|
|
378
|
+
tail: SCORING_EVENT_WINDOW,
|
|
379
|
+
}),
|
|
380
|
+
readJsonFile(paths.tasksPath, { allowMissing: true }),
|
|
381
|
+
]);
|
|
382
|
+
|
|
383
|
+
const taskRegistry = normalizeTaskRegistry(rawTaskRegistry || {}, {
|
|
384
|
+
sessionId,
|
|
385
|
+
nowIso,
|
|
386
|
+
});
|
|
387
|
+
const counts = new Map();
|
|
388
|
+
for (const assignment of activeAssignments.assignments) {
|
|
389
|
+
const agentId = normalizeString(assignment.assignedAgentIdentity);
|
|
390
|
+
if (!agentId) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
counts.set(agentId, Number(counts.get(agentId) || 0) + 1);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const candidates = agents
|
|
397
|
+
.filter((agent) => normalizeString(agent.agentId).toLowerCase() !== "senti")
|
|
398
|
+
.filter((agent) => !roleFilter || normalizeString(agent.role).toLowerCase() === roleFilter)
|
|
399
|
+
.map((agent) => ({
|
|
400
|
+
...agent,
|
|
401
|
+
assignmentCount: Number(counts.get(normalizeString(agent.agentId)) || 0),
|
|
402
|
+
statusWeight: statusWeight(agent.status),
|
|
403
|
+
activityEpoch: parseEpoch(agent.lastActivityAt, nowIso),
|
|
404
|
+
}));
|
|
405
|
+
|
|
406
|
+
if (candidates.length === 0) {
|
|
407
|
+
if (roleFilter) {
|
|
408
|
+
throw new Error(`No active agent matches role '${roleFilter}' in session '${sessionId}'.`);
|
|
409
|
+
}
|
|
410
|
+
throw new Error(`No active agents available for wildcard task routing in session '${sessionId}'.`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const analyticsByAgent = buildAgentAnalyticsSnapshot({
|
|
414
|
+
events: Array.isArray(events) ? events : [],
|
|
415
|
+
tasks: taskRegistry.tasks,
|
|
416
|
+
activeAssignments: activeAssignments.assignments,
|
|
417
|
+
nowIso,
|
|
418
|
+
});
|
|
419
|
+
const rankedCandidates = rankAgentsByScore(candidates, analyticsByAgent);
|
|
420
|
+
return {
|
|
421
|
+
...rankedCandidates[0],
|
|
422
|
+
rankedCandidates: rankedCandidates.slice(0, 5).map((candidate) => ({
|
|
423
|
+
agentId: candidate.agentId,
|
|
424
|
+
role: candidate.role,
|
|
425
|
+
overallScore: candidate.score.overallScore,
|
|
426
|
+
taskCompletionRate: candidate.score.taskCompletionRate,
|
|
427
|
+
reviewAccuracy: candidate.score.reviewAccuracy,
|
|
428
|
+
assignmentCount: candidate.assignmentCount,
|
|
429
|
+
})),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function resolveTaskAssignee(
|
|
434
|
+
sessionId,
|
|
435
|
+
{
|
|
436
|
+
toAgentId,
|
|
437
|
+
roleFilter = null,
|
|
438
|
+
targetPath = process.cwd(),
|
|
439
|
+
nowIso = new Date().toISOString(),
|
|
440
|
+
} = {}
|
|
441
|
+
) {
|
|
442
|
+
const parsedTarget = parseAssignmentTargetToken(toAgentId);
|
|
443
|
+
const resolvedRoleFilter = normalizeRoleFilter(roleFilter || parsedTarget.roleFilter);
|
|
444
|
+
if (parsedTarget.wildcard) {
|
|
445
|
+
const agent = await resolveHighestScoringAgent(sessionId, {
|
|
446
|
+
targetPath,
|
|
447
|
+
roleFilter: resolvedRoleFilter,
|
|
448
|
+
nowIso,
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
wildcard: true,
|
|
452
|
+
requestedToAgentId: "*",
|
|
453
|
+
roleFilter: resolvedRoleFilter,
|
|
454
|
+
strategy: "score",
|
|
455
|
+
routedAgent: agent,
|
|
456
|
+
rankedCandidates: Array.isArray(agent.rankedCandidates) ? agent.rankedCandidates : [],
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
const agent = await resolveExplicitAgent(sessionId, parsedTarget.requestedToAgentId, {
|
|
460
|
+
targetPath,
|
|
461
|
+
roleFilter: resolvedRoleFilter,
|
|
462
|
+
});
|
|
463
|
+
return {
|
|
464
|
+
wildcard: false,
|
|
465
|
+
requestedToAgentId: parsedTarget.requestedToAgentId,
|
|
466
|
+
roleFilter: resolvedRoleFilter,
|
|
467
|
+
strategy: "explicit",
|
|
468
|
+
routedAgent: agent,
|
|
469
|
+
rankedCandidates: [],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function findTaskById(registry, taskId) {
|
|
474
|
+
const normalizedTaskId = normalizeTaskId(taskId);
|
|
475
|
+
if (!normalizedTaskId) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
return registry.tasks.find((task) => task.taskId === normalizedTaskId) || null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function findLatestTaskForAgent(registry, agentId, allowedStatuses = []) {
|
|
482
|
+
const normalizedAgentId = normalizeString(agentId).toLowerCase();
|
|
483
|
+
const statusOrder = Array.isArray(allowedStatuses) ? [...allowedStatuses] : [];
|
|
484
|
+
for (const status of statusOrder) {
|
|
485
|
+
const match = registry.tasks
|
|
486
|
+
.filter((task) => normalizeString(task.toAgentId).toLowerCase() === normalizedAgentId)
|
|
487
|
+
.filter((task) => normalizeTaskStatus(task.status, "") === normalizeTaskStatus(status, ""))
|
|
488
|
+
.sort((left, right) => parseEpoch(right.updatedAt) - parseEpoch(left.updatedAt))[0];
|
|
489
|
+
if (match) {
|
|
490
|
+
return match;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function parsePriorityAndTask(rawTask) {
|
|
497
|
+
const normalizedTask = normalizeString(rawTask);
|
|
498
|
+
if (!normalizedTask) {
|
|
499
|
+
throw new Error("task is required.");
|
|
500
|
+
}
|
|
501
|
+
const bracketPriority = normalizedTask.match(/^\[(P0|P1|P2|when-free)\]\s*(.+)$/i);
|
|
502
|
+
if (bracketPriority) {
|
|
503
|
+
return {
|
|
504
|
+
priority: normalizeTaskPriority(bracketPriority[1], "when-free"),
|
|
505
|
+
task: normalizeString(bracketPriority[2]),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const inlinePriority = normalizedTask.match(/^(P0|P1|P2|when-free)\s*[:\-]\s*(.+)$/i);
|
|
509
|
+
if (inlinePriority) {
|
|
510
|
+
return {
|
|
511
|
+
priority: normalizeTaskPriority(inlinePriority[1], "when-free"),
|
|
512
|
+
task: normalizeString(inlinePriority[2]),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
priority: "when-free",
|
|
517
|
+
task: normalizedTask,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function parseFilesFromTaskText(taskText) {
|
|
522
|
+
const match = /\bfiles?\s*:\s*(.+)$/i.exec(normalizeString(taskText));
|
|
523
|
+
if (!match) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
return match[1]
|
|
527
|
+
.split(",")
|
|
528
|
+
.map((item) => normalizeString(item))
|
|
529
|
+
.filter(Boolean);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function extractTaskIdFromText(text) {
|
|
533
|
+
const normalized = normalizeString(text);
|
|
534
|
+
if (!normalized) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const explicit = /\btask(?:\s+id)?\s*[:#-]?\s*([a-z0-9][a-z0-9._-]{5,})\b/i.exec(normalized);
|
|
538
|
+
if (explicit) {
|
|
539
|
+
return normalizeTaskId(explicit[1]);
|
|
540
|
+
}
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function buildTaskAssignPayload(taskRecord, routing = {}) {
|
|
545
|
+
return {
|
|
546
|
+
taskId: taskRecord.taskId,
|
|
547
|
+
workItemId: taskRecord.workItemId,
|
|
548
|
+
from: taskRecord.fromAgentId,
|
|
549
|
+
to: taskRecord.toAgentId,
|
|
550
|
+
requestedTo: taskRecord.requestedToAgentId,
|
|
551
|
+
wildcardRouted: Boolean(routing.wildcard),
|
|
552
|
+
roleFilter: taskRecord.roleFilter,
|
|
553
|
+
task: taskRecord.task,
|
|
554
|
+
priority: taskRecord.priority,
|
|
555
|
+
context: taskRecord.context,
|
|
556
|
+
routingStrategy: normalizeString(routing.strategy) || (routing.wildcard ? "score" : "explicit"),
|
|
557
|
+
selectedScore:
|
|
558
|
+
routing &&
|
|
559
|
+
routing.routedAgent &&
|
|
560
|
+
routing.routedAgent.score &&
|
|
561
|
+
Number.isFinite(Number(routing.routedAgent.score.overallScore))
|
|
562
|
+
? Number(routing.routedAgent.score.overallScore)
|
|
563
|
+
: null,
|
|
564
|
+
scoreModelVersion:
|
|
565
|
+
routing && routing.routedAgent && routing.routedAgent.score
|
|
566
|
+
? normalizeString(routing.routedAgent.score.scoreModelVersion) || null
|
|
567
|
+
: null,
|
|
568
|
+
rankedCandidates:
|
|
569
|
+
Array.isArray(routing.rankedCandidates) && routing.rankedCandidates.length > 0
|
|
570
|
+
? routing.rankedCandidates
|
|
571
|
+
: [],
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export async function assignTask(
|
|
576
|
+
sessionId,
|
|
577
|
+
{
|
|
578
|
+
fromAgentId,
|
|
579
|
+
toAgentId,
|
|
580
|
+
task,
|
|
581
|
+
priority = "when-free",
|
|
582
|
+
context = {},
|
|
583
|
+
roleFilter = null,
|
|
584
|
+
leaseTtlMs = DEFAULT_TASK_LEASE_TTL_MS,
|
|
585
|
+
targetPath = process.cwd(),
|
|
586
|
+
nowIso = new Date().toISOString(),
|
|
587
|
+
} = {}
|
|
588
|
+
) {
|
|
589
|
+
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
590
|
+
const normalizedFromAgentId = normalizeString(fromAgentId);
|
|
591
|
+
if (!normalizedFromAgentId) {
|
|
592
|
+
throw new Error("fromAgentId is required.");
|
|
593
|
+
}
|
|
594
|
+
const parsedTask = parsePriorityAndTask(task);
|
|
595
|
+
const normalizedPriority = normalizeTaskPriority(priority || parsedTask.priority, parsedTask.priority);
|
|
596
|
+
const normalizedTaskText = normalizeString(parsedTask.task);
|
|
597
|
+
const normalizedContext = normalizeContext(context);
|
|
598
|
+
|
|
599
|
+
const routing = await resolveTaskAssignee(sessionId, {
|
|
600
|
+
toAgentId,
|
|
601
|
+
roleFilter,
|
|
602
|
+
targetPath,
|
|
603
|
+
nowIso: normalizedNow,
|
|
604
|
+
});
|
|
605
|
+
const taskId = buildTaskId();
|
|
606
|
+
const workItemId = buildWorkItemId(taskId);
|
|
607
|
+
const mergedContext = {
|
|
608
|
+
...normalizedContext,
|
|
609
|
+
files:
|
|
610
|
+
Array.isArray(normalizedContext.files) && normalizedContext.files.length > 0
|
|
611
|
+
? [...normalizedContext.files]
|
|
612
|
+
: parseFilesFromTaskText(normalizedTaskText),
|
|
613
|
+
};
|
|
614
|
+
const taskRecord = normalizeTaskRecord(
|
|
615
|
+
{
|
|
616
|
+
taskId,
|
|
617
|
+
workItemId,
|
|
618
|
+
sessionId: normalizeString(sessionId),
|
|
619
|
+
fromAgentId: normalizedFromAgentId,
|
|
620
|
+
toAgentId: routing.routedAgent.agentId,
|
|
621
|
+
requestedToAgentId: routing.requestedToAgentId,
|
|
622
|
+
roleFilter: routing.roleFilter,
|
|
623
|
+
task: normalizedTaskText,
|
|
624
|
+
priority: normalizedPriority,
|
|
625
|
+
context: mergedContext,
|
|
626
|
+
status: "PENDING",
|
|
627
|
+
createdAt: normalizedNow,
|
|
628
|
+
updatedAt: normalizedNow,
|
|
629
|
+
},
|
|
630
|
+
normalizedNow
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
await ensureWorkItemQueued({
|
|
634
|
+
targetPath,
|
|
635
|
+
sessionId,
|
|
636
|
+
workItemId,
|
|
637
|
+
severity: severityForPriority(normalizedPriority),
|
|
638
|
+
message: normalizedTaskText,
|
|
639
|
+
metadata: {
|
|
640
|
+
taskId: taskRecord.taskId,
|
|
641
|
+
fromAgentId: taskRecord.fromAgentId,
|
|
642
|
+
toAgentId: taskRecord.toAgentId,
|
|
643
|
+
priority: taskRecord.priority,
|
|
644
|
+
roleFilter: taskRecord.roleFilter,
|
|
645
|
+
},
|
|
646
|
+
nowIso: normalizedNow,
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
const lease = await leaseWorkItem({
|
|
650
|
+
targetPath,
|
|
651
|
+
sessionId,
|
|
652
|
+
workItemId,
|
|
653
|
+
agentIdentity: taskRecord.toAgentId,
|
|
654
|
+
leaseTtlMs: normalizePositiveInteger(leaseTtlMs, DEFAULT_TASK_LEASE_TTL_MS),
|
|
655
|
+
stage: "session_task_assigned",
|
|
656
|
+
budgetSnapshot: {
|
|
657
|
+
taskId: taskRecord.taskId,
|
|
658
|
+
priority: taskRecord.priority,
|
|
659
|
+
scope: "session_task",
|
|
660
|
+
},
|
|
661
|
+
nowIso: normalizedNow,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
await mutateTaskRegistry(
|
|
665
|
+
sessionId,
|
|
666
|
+
{
|
|
667
|
+
targetPath,
|
|
668
|
+
nowIso: normalizedNow,
|
|
669
|
+
},
|
|
670
|
+
async (registry) => {
|
|
671
|
+
registry.tasks.push(taskRecord);
|
|
672
|
+
return taskRecord;
|
|
673
|
+
}
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
const event = await appendToStream(
|
|
677
|
+
sessionId,
|
|
678
|
+
createAgentEvent({
|
|
679
|
+
event: "task_assign",
|
|
680
|
+
agentId: normalizedFromAgentId,
|
|
681
|
+
sessionId,
|
|
682
|
+
workItemId: taskRecord.workItemId,
|
|
683
|
+
ts: normalizedNow,
|
|
684
|
+
payload: buildTaskAssignPayload(taskRecord, routing),
|
|
685
|
+
}),
|
|
686
|
+
{
|
|
687
|
+
targetPath,
|
|
688
|
+
}
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
task: taskRecord,
|
|
693
|
+
lease: lease.assignment,
|
|
694
|
+
event,
|
|
695
|
+
routing: {
|
|
696
|
+
wildcard: routing.wildcard,
|
|
697
|
+
strategy: routing.strategy || (routing.wildcard ? "score" : "explicit"),
|
|
698
|
+
roleFilter: routing.roleFilter,
|
|
699
|
+
selectedAgentId: routing.routedAgent.agentId,
|
|
700
|
+
selectedAgentRole: routing.routedAgent.role,
|
|
701
|
+
assignmentCount: routing.routedAgent.assignmentCount,
|
|
702
|
+
selectedScore:
|
|
703
|
+
routing.routedAgent && routing.routedAgent.score
|
|
704
|
+
? Number(routing.routedAgent.score.overallScore)
|
|
705
|
+
: null,
|
|
706
|
+
scoreModelVersion:
|
|
707
|
+
routing.routedAgent && routing.routedAgent.score
|
|
708
|
+
? normalizeString(routing.routedAgent.score.scoreModelVersion) || null
|
|
709
|
+
: null,
|
|
710
|
+
rankedCandidates: Array.isArray(routing.rankedCandidates) ? routing.rankedCandidates : [],
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export async function acceptTask(
|
|
716
|
+
sessionId,
|
|
717
|
+
agentId,
|
|
718
|
+
taskId = null,
|
|
719
|
+
{
|
|
720
|
+
note = "",
|
|
721
|
+
leaseTtlMs = DEFAULT_TASK_LEASE_TTL_MS,
|
|
722
|
+
targetPath = process.cwd(),
|
|
723
|
+
nowIso = new Date().toISOString(),
|
|
724
|
+
} = {}
|
|
725
|
+
) {
|
|
726
|
+
const normalizedAgentId = normalizeString(agentId);
|
|
727
|
+
if (!normalizedAgentId) {
|
|
728
|
+
throw new Error("agentId is required.");
|
|
729
|
+
}
|
|
730
|
+
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
731
|
+
const normalizedNote = normalizeString(note);
|
|
732
|
+
const mutation = await mutateTaskRegistry(
|
|
733
|
+
sessionId,
|
|
734
|
+
{
|
|
735
|
+
targetPath,
|
|
736
|
+
nowIso: normalizedNow,
|
|
737
|
+
},
|
|
738
|
+
async (registry) => {
|
|
739
|
+
const explicitTask = findTaskById(registry, taskId);
|
|
740
|
+
const candidate =
|
|
741
|
+
explicitTask ||
|
|
742
|
+
findLatestTaskForAgent(registry, normalizedAgentId, ["PENDING", "ACCEPTED"]);
|
|
743
|
+
if (!candidate) {
|
|
744
|
+
throw new Error(`No pending task found for agent '${normalizedAgentId}'.`);
|
|
745
|
+
}
|
|
746
|
+
if (normalizeString(candidate.toAgentId).toLowerCase() !== normalizedAgentId.toLowerCase()) {
|
|
747
|
+
throw new Error(
|
|
748
|
+
`Task '${candidate.taskId}' is assigned to '${candidate.toAgentId}', not '${normalizedAgentId}'.`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
if (candidate.status !== "PENDING" && candidate.status !== "ACCEPTED") {
|
|
752
|
+
throw new Error(`Task '${candidate.taskId}' cannot transition from status '${candidate.status}'.`);
|
|
753
|
+
}
|
|
754
|
+
candidate.status = "ACCEPTED";
|
|
755
|
+
candidate.acceptedAt = candidate.acceptedAt || normalizedNow;
|
|
756
|
+
candidate.updatedAt = normalizedNow;
|
|
757
|
+
if (normalizedNote) {
|
|
758
|
+
candidate.context = {
|
|
759
|
+
...normalizeContext(candidate.context),
|
|
760
|
+
acceptedNote: normalizedNote,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
return normalizeTaskRecord(candidate, normalizedNow);
|
|
764
|
+
}
|
|
765
|
+
);
|
|
766
|
+
const acceptedTask = mutation.result;
|
|
767
|
+
|
|
768
|
+
const lease = await heartbeatLease({
|
|
769
|
+
targetPath,
|
|
770
|
+
sessionId,
|
|
771
|
+
workItemId: acceptedTask.workItemId,
|
|
772
|
+
agentIdentity: normalizedAgentId,
|
|
773
|
+
leaseTtlMs: normalizePositiveInteger(leaseTtlMs, DEFAULT_TASK_LEASE_TTL_MS),
|
|
774
|
+
stage: "session_task_in_progress",
|
|
775
|
+
budgetSnapshot: {
|
|
776
|
+
taskId: acceptedTask.taskId,
|
|
777
|
+
priority: acceptedTask.priority,
|
|
778
|
+
scope: "session_task",
|
|
779
|
+
},
|
|
780
|
+
nowIso: normalizedNow,
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const event = await appendToStream(
|
|
784
|
+
sessionId,
|
|
785
|
+
createAgentEvent({
|
|
786
|
+
event: "task_accepted",
|
|
787
|
+
agentId: normalizedAgentId,
|
|
788
|
+
sessionId,
|
|
789
|
+
workItemId: acceptedTask.workItemId,
|
|
790
|
+
ts: normalizedNow,
|
|
791
|
+
payload: {
|
|
792
|
+
taskId: acceptedTask.taskId,
|
|
793
|
+
workItemId: acceptedTask.workItemId,
|
|
794
|
+
from: acceptedTask.fromAgentId,
|
|
795
|
+
to: acceptedTask.toAgentId,
|
|
796
|
+
note: normalizedNote || null,
|
|
797
|
+
},
|
|
798
|
+
}),
|
|
799
|
+
{
|
|
800
|
+
targetPath,
|
|
801
|
+
}
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
return {
|
|
805
|
+
task: acceptedTask,
|
|
806
|
+
lease: lease.assignment,
|
|
807
|
+
event,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export async function completeTask(
|
|
812
|
+
sessionId,
|
|
813
|
+
agentId,
|
|
814
|
+
taskId = null,
|
|
815
|
+
{
|
|
816
|
+
result = "",
|
|
817
|
+
targetPath = process.cwd(),
|
|
818
|
+
nowIso = new Date().toISOString(),
|
|
819
|
+
} = {}
|
|
820
|
+
) {
|
|
821
|
+
const normalizedAgentId = normalizeString(agentId);
|
|
822
|
+
if (!normalizedAgentId) {
|
|
823
|
+
throw new Error("agentId is required.");
|
|
824
|
+
}
|
|
825
|
+
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
826
|
+
const normalizedResult = normalizeString(result);
|
|
827
|
+
const mutation = await mutateTaskRegistry(
|
|
828
|
+
sessionId,
|
|
829
|
+
{
|
|
830
|
+
targetPath,
|
|
831
|
+
nowIso: normalizedNow,
|
|
832
|
+
},
|
|
833
|
+
async (registry) => {
|
|
834
|
+
const explicitTask = findTaskById(registry, taskId);
|
|
835
|
+
const candidate =
|
|
836
|
+
explicitTask ||
|
|
837
|
+
findLatestTaskForAgent(registry, normalizedAgentId, ["ACCEPTED", "PENDING"]);
|
|
838
|
+
if (!candidate) {
|
|
839
|
+
throw new Error(`No active task found for agent '${normalizedAgentId}'.`);
|
|
840
|
+
}
|
|
841
|
+
if (normalizeString(candidate.toAgentId).toLowerCase() !== normalizedAgentId.toLowerCase()) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
`Task '${candidate.taskId}' is assigned to '${candidate.toAgentId}', not '${normalizedAgentId}'.`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
if (candidate.status !== "PENDING" && candidate.status !== "ACCEPTED") {
|
|
847
|
+
throw new Error(`Task '${candidate.taskId}' cannot transition from status '${candidate.status}'.`);
|
|
848
|
+
}
|
|
849
|
+
candidate.status = "COMPLETED";
|
|
850
|
+
candidate.completedAt = normalizedNow;
|
|
851
|
+
candidate.updatedAt = normalizedNow;
|
|
852
|
+
candidate.result = normalizedResult || null;
|
|
853
|
+
return normalizeTaskRecord(candidate, normalizedNow);
|
|
854
|
+
}
|
|
855
|
+
);
|
|
856
|
+
const completedTask = mutation.result;
|
|
857
|
+
|
|
858
|
+
const lease = await releaseLease({
|
|
859
|
+
targetPath,
|
|
860
|
+
sessionId,
|
|
861
|
+
workItemId: completedTask.workItemId,
|
|
862
|
+
agentIdentity: normalizedAgentId,
|
|
863
|
+
status: "DONE",
|
|
864
|
+
reason: "session_task_completed",
|
|
865
|
+
budgetSnapshot: {
|
|
866
|
+
taskId: completedTask.taskId,
|
|
867
|
+
priority: completedTask.priority,
|
|
868
|
+
scope: "session_task",
|
|
869
|
+
result: normalizedResult || null,
|
|
870
|
+
},
|
|
871
|
+
nowIso: normalizedNow,
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
const event = await appendToStream(
|
|
875
|
+
sessionId,
|
|
876
|
+
createAgentEvent({
|
|
877
|
+
event: "task_completed",
|
|
878
|
+
agentId: normalizedAgentId,
|
|
879
|
+
sessionId,
|
|
880
|
+
workItemId: completedTask.workItemId,
|
|
881
|
+
ts: normalizedNow,
|
|
882
|
+
payload: {
|
|
883
|
+
taskId: completedTask.taskId,
|
|
884
|
+
workItemId: completedTask.workItemId,
|
|
885
|
+
from: completedTask.fromAgentId,
|
|
886
|
+
to: completedTask.toAgentId,
|
|
887
|
+
result: normalizedResult || null,
|
|
888
|
+
},
|
|
889
|
+
}),
|
|
890
|
+
{
|
|
891
|
+
targetPath,
|
|
892
|
+
}
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
task: completedTask,
|
|
897
|
+
lease: lease.assignment,
|
|
898
|
+
event,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export async function listSessionTasks(
|
|
903
|
+
sessionId,
|
|
904
|
+
{
|
|
905
|
+
status = null,
|
|
906
|
+
statuses = [],
|
|
907
|
+
limit = DEFAULT_TASK_LIST_LIMIT,
|
|
908
|
+
targetPath = process.cwd(),
|
|
909
|
+
} = {}
|
|
910
|
+
) {
|
|
911
|
+
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
912
|
+
const nowIso = new Date().toISOString();
|
|
913
|
+
const raw = await readJsonFile(paths.tasksPath, { allowMissing: true });
|
|
914
|
+
const registry = normalizeTaskRegistry(raw || {}, {
|
|
915
|
+
sessionId: paths.sessionId,
|
|
916
|
+
nowIso,
|
|
917
|
+
});
|
|
918
|
+
const filter = parseStatusFilters(status, statuses);
|
|
919
|
+
const normalizedLimit = normalizePositiveInteger(limit, DEFAULT_TASK_LIST_LIMIT);
|
|
920
|
+
const visible = registry.tasks
|
|
921
|
+
.filter((task) => (filter ? filter.has(normalizeTaskStatus(task.status, "")) : true))
|
|
922
|
+
.sort((left, right) => parseEpoch(right.updatedAt) - parseEpoch(left.updatedAt));
|
|
923
|
+
return {
|
|
924
|
+
sessionId: paths.sessionId,
|
|
925
|
+
tasksPath: paths.tasksPath,
|
|
926
|
+
totalCount: registry.tasks.length,
|
|
927
|
+
visibleCount: visible.length,
|
|
928
|
+
tasks: visible.slice(0, normalizedLimit),
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export function parseTaskDirectiveMessage(message = "") {
|
|
933
|
+
const normalizedMessage = normalizeString(message);
|
|
934
|
+
if (!normalizedMessage) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const assignMatch = /^assign\s*:\s*@([^\s]+)\s+(.+)$/i.exec(normalizedMessage);
|
|
939
|
+
if (assignMatch) {
|
|
940
|
+
const parsedTask = parsePriorityAndTask(assignMatch[2]);
|
|
941
|
+
const parsedTarget = parseAssignmentTargetToken(assignMatch[1]);
|
|
942
|
+
return {
|
|
943
|
+
action: "assign",
|
|
944
|
+
requestedToAgentId: parsedTarget.requestedToAgentId,
|
|
945
|
+
roleFilter: parsedTarget.roleFilter,
|
|
946
|
+
wildcard: parsedTarget.wildcard,
|
|
947
|
+
task: parsedTask.task,
|
|
948
|
+
priority: parsedTask.priority,
|
|
949
|
+
context: {
|
|
950
|
+
files: parseFilesFromTaskText(parsedTask.task),
|
|
951
|
+
},
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const acceptedMatch = /^accepted\s*:\s*(.+)$/i.exec(normalizedMessage);
|
|
956
|
+
if (acceptedMatch) {
|
|
957
|
+
const note = normalizeString(acceptedMatch[1]);
|
|
958
|
+
return {
|
|
959
|
+
action: "accepted",
|
|
960
|
+
taskId: extractTaskIdFromText(note),
|
|
961
|
+
note,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const doneMatch = /^done\s*:\s*(.+)$/i.exec(normalizedMessage);
|
|
966
|
+
if (doneMatch) {
|
|
967
|
+
const result = normalizeString(doneMatch[1]);
|
|
968
|
+
return {
|
|
969
|
+
action: "done",
|
|
970
|
+
taskId: extractTaskIdFromText(result),
|
|
971
|
+
result,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
export async function handleTaskDirective(
|
|
979
|
+
sessionId,
|
|
980
|
+
event = {},
|
|
981
|
+
{
|
|
982
|
+
targetPath = process.cwd(),
|
|
983
|
+
nowIso = new Date().toISOString(),
|
|
984
|
+
} = {}
|
|
985
|
+
) {
|
|
986
|
+
if (normalizeString(event.event) !== "session_message") {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
const message = normalizeString(event.payload?.message);
|
|
990
|
+
if (!message) {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
const parsed = parseTaskDirectiveMessage(message);
|
|
994
|
+
if (!parsed) {
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
const fromAgentId = normalizeString(event.agent?.id);
|
|
998
|
+
if (!fromAgentId) {
|
|
999
|
+
throw new Error("session_message agent id is required for task directives.");
|
|
1000
|
+
}
|
|
1001
|
+
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
1002
|
+
|
|
1003
|
+
if (parsed.action === "assign") {
|
|
1004
|
+
const assigned = await assignTask(sessionId, {
|
|
1005
|
+
fromAgentId,
|
|
1006
|
+
toAgentId: parsed.wildcard ? "*" : parsed.requestedToAgentId,
|
|
1007
|
+
roleFilter: parsed.roleFilter,
|
|
1008
|
+
task: parsed.task,
|
|
1009
|
+
priority: parsed.priority,
|
|
1010
|
+
context: parsed.context,
|
|
1011
|
+
targetPath,
|
|
1012
|
+
nowIso: normalizedNow,
|
|
1013
|
+
});
|
|
1014
|
+
return {
|
|
1015
|
+
action: "assign",
|
|
1016
|
+
parsed,
|
|
1017
|
+
assigned,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (parsed.action === "accepted") {
|
|
1022
|
+
const accepted = await acceptTask(sessionId, fromAgentId, parsed.taskId, {
|
|
1023
|
+
note: parsed.note,
|
|
1024
|
+
targetPath,
|
|
1025
|
+
nowIso: normalizedNow,
|
|
1026
|
+
});
|
|
1027
|
+
return {
|
|
1028
|
+
action: "accepted",
|
|
1029
|
+
parsed,
|
|
1030
|
+
accepted,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
if (parsed.action === "done") {
|
|
1035
|
+
const completed = await completeTask(sessionId, fromAgentId, parsed.taskId, {
|
|
1036
|
+
result: parsed.result,
|
|
1037
|
+
targetPath,
|
|
1038
|
+
nowIso: normalizedNow,
|
|
1039
|
+
});
|
|
1040
|
+
return {
|
|
1041
|
+
action: "done",
|
|
1042
|
+
parsed,
|
|
1043
|
+
completed,
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
export {
|
|
1051
|
+
DEFAULT_TASK_LEASE_TTL_MS,
|
|
1052
|
+
TASK_PRIORITIES,
|
|
1053
|
+
TASK_STATUSES,
|
|
1054
|
+
};
|