brainclaw 0.29.2 → 1.5.3
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 +193 -170
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +673 -24
- package/dist/commands/accept.js +3 -0
- package/dist/commands/add-step.js +11 -26
- package/dist/commands/agent-board.js +70 -3
- package/dist/commands/audit.js +19 -0
- package/dist/commands/check-policy.js +54 -0
- package/dist/commands/check-security-mcp.js +145 -0
- package/dist/commands/check-security.js +106 -0
- package/dist/commands/claim-resource.js +1 -0
- package/dist/commands/codev.js +672 -0
- package/dist/commands/compact.js +74 -0
- package/dist/commands/complete-step.js +16 -26
- package/dist/commands/constraint.js +8 -20
- package/dist/commands/decision.js +9 -20
- package/dist/commands/delete-plan.js +10 -12
- package/dist/commands/delete-step.js +16 -0
- package/dist/commands/dispatch.js +163 -0
- package/dist/commands/doctor.js +1122 -49
- package/dist/commands/enable-agent.js +1 -0
- package/dist/commands/export.js +280 -22
- package/dist/commands/handoff.js +33 -0
- package/dist/commands/harvest.js +189 -0
- package/dist/commands/hooks.js +82 -25
- package/dist/commands/inbox.js +169 -0
- package/dist/commands/init.js +38 -31
- package/dist/commands/install-hooks.js +71 -44
- package/dist/commands/link.js +89 -0
- package/dist/commands/list-claims.js +48 -3
- package/dist/commands/list-plans.js +129 -25
- package/dist/commands/loops-handlers.js +409 -0
- package/dist/commands/mcp-read-handlers.js +1628 -0
- package/dist/commands/mcp-schemas.generated.js +74 -0
- package/dist/commands/mcp.js +4221 -1501
- package/dist/commands/plan-resource.js +64 -0
- package/dist/commands/plan.js +12 -26
- package/dist/commands/prune.js +37 -2
- package/dist/commands/reflect.js +20 -7
- package/dist/commands/release-claim.js +11 -6
- package/dist/commands/release-notes.js +170 -0
- package/dist/commands/repair.js +210 -0
- package/dist/commands/run-profile.js +57 -0
- package/dist/commands/sequence.js +113 -0
- package/dist/commands/session-end.js +423 -14
- package/dist/commands/session-start.js +214 -41
- package/dist/commands/setup-security.js +103 -0
- package/dist/commands/setup.js +42 -4
- package/dist/commands/stale.js +109 -0
- package/dist/commands/switch.js +100 -2
- package/dist/commands/trap.js +14 -31
- package/dist/commands/update-handoff.js +63 -4
- package/dist/commands/update-plan.js +21 -28
- package/dist/commands/update-step.js +37 -0
- package/dist/commands/upgrade.js +313 -6
- package/dist/commands/usage.js +102 -0
- package/dist/commands/version.js +20 -0
- package/dist/commands/who.js +33 -5
- package/dist/commands/worktree.js +105 -0
- package/dist/core/actions.js +315 -0
- package/dist/core/agent-capability.js +610 -17
- package/dist/core/agent-context.js +7 -1
- package/dist/core/agent-files.js +1169 -85
- package/dist/core/agent-integrations.js +160 -5
- package/dist/core/agent-inventory.js +2 -0
- package/dist/core/agent-profiles.js +93 -0
- package/dist/core/agent-registry.js +162 -30
- package/dist/core/agentrun-reconciler.js +345 -0
- package/dist/core/agentruns.js +424 -0
- package/dist/core/ai-agent-detection.js +31 -10
- package/dist/core/archival.js +77 -0
- package/dist/core/assignment-sweeper.js +82 -0
- package/dist/core/assignments.js +367 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/brainclaw-version.js +94 -2
- package/dist/core/candidates.js +93 -2
- package/dist/core/claims.js +419 -0
- package/dist/core/codev-metrics.js +77 -0
- package/dist/core/codev-personas.js +31 -0
- package/dist/core/codev-plan-gen.js +35 -0
- package/dist/core/codev-prompts.js +74 -0
- package/dist/core/codev-responses.js +62 -0
- package/dist/core/codev-rounds.js +218 -0
- package/dist/core/config.js +4 -0
- package/dist/core/context.js +381 -34
- package/dist/core/coordination.js +201 -6
- package/dist/core/cross-project.js +230 -16
- package/dist/core/default-profiles/doctor.yaml +11 -0
- package/dist/core/default-profiles/janitor.yaml +11 -0
- package/dist/core/default-profiles/onboarder.yaml +11 -0
- package/dist/core/default-profiles/reviewer.yaml +13 -0
- package/dist/core/dispatcher.js +1189 -0
- package/dist/core/duplicates.js +2 -2
- package/dist/core/entity-operations.js +450 -0
- package/dist/core/entity-registry.js +344 -0
- package/dist/core/events.js +106 -2
- package/dist/core/execution-adapters.js +154 -0
- package/dist/core/execution-context.js +63 -0
- package/dist/core/execution-profile.js +270 -0
- package/dist/core/execution.js +255 -0
- package/dist/core/facade-schema.js +81 -0
- package/dist/core/federation-cloud.js +99 -0
- package/dist/core/federation-message.js +52 -0
- package/dist/core/federation-transport.js +65 -0
- package/dist/core/gc-semantic.js +482 -0
- package/dist/core/governance.js +247 -0
- package/dist/core/guards.js +19 -0
- package/dist/core/ideation.js +72 -0
- package/dist/core/identity.js +110 -25
- package/dist/core/ids.js +6 -0
- package/dist/core/input-validation.js +2 -2
- package/dist/core/instruction-templates.js +344 -136
- package/dist/core/io.js +90 -11
- package/dist/core/lock.js +6 -2
- package/dist/core/loops/brief-assembly.js +213 -0
- package/dist/core/loops/facade-schema.js +148 -0
- package/dist/core/loops/index.js +7 -0
- package/dist/core/loops/iteration-engine.js +139 -0
- package/dist/core/loops/lock.js +385 -0
- package/dist/core/loops/store.js +201 -0
- package/dist/core/loops/types.js +403 -0
- package/dist/core/loops/verbs.js +534 -0
- package/dist/core/markdown.js +15 -3
- package/dist/core/memory-compactor.js +432 -0
- package/dist/core/memory-git.js +152 -8
- package/dist/core/messaging.js +278 -0
- package/dist/core/migration.js +32 -1
- package/dist/core/mutation-pipeline.js +4 -2
- package/dist/core/operations/memory-mutation.js +129 -0
- package/dist/core/operations/memory-write.js +78 -0
- package/dist/core/operations/plan.js +190 -0
- package/dist/core/policy.js +169 -0
- package/dist/core/reputation.js +9 -3
- package/dist/core/schema.js +491 -6
- package/dist/core/search.js +21 -2
- package/dist/core/security-cache.js +71 -0
- package/dist/core/security-guard.js +152 -0
- package/dist/core/security-scoring.js +86 -0
- package/dist/core/sequence.js +130 -0
- package/dist/core/socket-client.js +113 -0
- package/dist/core/staleness.js +246 -0
- package/dist/core/state.js +98 -22
- package/dist/core/store-resolution.js +43 -11
- package/dist/core/toml-writer.js +76 -0
- package/dist/core/upgrades/backup.js +232 -0
- package/dist/core/upgrades/health-check.js +169 -0
- package/dist/core/upgrades/patches/candidate-archive.js +145 -0
- package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
- package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
- package/dist/core/upgrades/schema-version.js +97 -0
- package/dist/core/worktree.js +606 -0
- package/dist/facts.js +114 -0
- package/dist/facts.json +111 -0
- package/docs/architecture/project-refs.md +5 -1
- package/docs/cli.md +690 -43
- package/docs/concepts/ideation-loop.md +317 -0
- package/docs/concepts/loop-engine.md +456 -0
- package/docs/concepts/mcp-governance.md +268 -0
- package/docs/concepts/memory-staleness.md +122 -0
- package/docs/concepts/multi-agent-workflows.md +166 -0
- package/docs/concepts/plans-and-claims.md +31 -6
- package/docs/concepts/project-md-convention.md +35 -0
- package/docs/concepts/troubleshooting.md +220 -0
- package/docs/concepts/upgrade-cli.md +202 -0
- package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
- package/docs/context-format-changelog.md +2 -2
- package/docs/context-format.md +2 -2
- package/docs/index.md +68 -0
- package/docs/integrations/agents.md +15 -16
- package/docs/integrations/cline.md +88 -0
- package/docs/integrations/codex.md +75 -23
- package/docs/integrations/continue.md +60 -0
- package/docs/integrations/copilot.md +67 -9
- package/docs/integrations/kilocode.md +72 -0
- package/docs/integrations/mcp.md +304 -21
- package/docs/integrations/mistral-vibe.md +122 -0
- package/docs/integrations/opencode.md +84 -0
- package/docs/integrations/overview.md +23 -8
- package/docs/integrations/roo.md +74 -0
- package/docs/integrations/windsurf.md +83 -0
- package/docs/mcp-schema-changelog.md +191 -1
- package/docs/playbooks/integration/index.md +121 -0
- package/docs/playbooks/productivity/index.md +102 -0
- package/docs/playbooks/team/index.md +122 -0
- package/docs/product/agent-first-model.md +184 -0
- package/docs/product/entity-model-audit.md +462 -0
- package/docs/quickstart-existing-project.md +135 -0
- package/docs/quickstart.md +124 -37
- package/docs/release-maintenance.md +79 -0
- package/docs/review.md +2 -0
- package/docs/server-operations.md +118 -0
- package/package.json +20 -12
- package/dist/commands/claude-desktop-extension.js +0 -18
- package/dist/commands/diff.js +0 -99
- package/dist/core/claude-desktop-extension.js +0 -224
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
import { memoryExists } from '../core/io.js';
|
|
2
|
-
import { listClaims } from '../core/claims.js';
|
|
2
|
+
import { listClaims, expireStaleActiveClaims, isClaimExpired, assessClaimLiveness } from '../core/claims.js';
|
|
3
|
+
import { resolveStoreChain } from '../core/store-resolution.js';
|
|
3
4
|
export function runListClaims(options = {}) {
|
|
4
5
|
if (!memoryExists(options.cwd)) {
|
|
5
6
|
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
6
7
|
process.exit(1);
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
+
const effectiveCwd = options.cwd ?? process.cwd();
|
|
10
|
+
// Auto-expire claims whose TTL has passed before listing (local store only — writes are always local)
|
|
11
|
+
expireStaleActiveClaims(options.cwd);
|
|
12
|
+
let claims;
|
|
13
|
+
if (options.localOnly) {
|
|
14
|
+
claims = listClaims(options.cwd);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const chain = resolveStoreChain(effectiveCwd);
|
|
18
|
+
const seenIds = new Set();
|
|
19
|
+
claims = [];
|
|
20
|
+
for (const store of chain) {
|
|
21
|
+
try {
|
|
22
|
+
// Expire stale claims in each parent store too
|
|
23
|
+
expireStaleActiveClaims(store.cwd);
|
|
24
|
+
for (const claim of listClaims(store.cwd)) {
|
|
25
|
+
if (!seenIds.has(claim.id)) {
|
|
26
|
+
seenIds.add(claim.id);
|
|
27
|
+
claims.push(claim);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { /* skip unreadable stores */ }
|
|
32
|
+
}
|
|
33
|
+
// Fallback when no chain found
|
|
34
|
+
if (claims.length === 0 && chain.length === 0) {
|
|
35
|
+
claims = listClaims(options.cwd);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
9
38
|
if (!options.all) {
|
|
10
39
|
claims = claims.filter(c => c.status === 'active');
|
|
11
40
|
}
|
|
@@ -30,6 +59,7 @@ export function runListClaims(options = {}) {
|
|
|
30
59
|
console.log(`${claims.length} ${label}:`);
|
|
31
60
|
console.log('');
|
|
32
61
|
for (const c of claims) {
|
|
62
|
+
const expired = isClaimExpired(c) ? ' [EXPIRED]' : '';
|
|
33
63
|
const status = c.status !== 'active' ? ` (${c.status})` : '';
|
|
34
64
|
const extras = [];
|
|
35
65
|
if (c.session_id)
|
|
@@ -38,8 +68,23 @@ export function runListClaims(options = {}) {
|
|
|
38
68
|
extras.push(`plan ${c.plan_id}`);
|
|
39
69
|
if (c.project)
|
|
40
70
|
extras.push(`project ${c.project}`);
|
|
71
|
+
if (c.expires_at && !isClaimExpired(c))
|
|
72
|
+
extras.push(`expires ${c.expires_at.slice(0, 16).replace('T', ' ')}`);
|
|
41
73
|
const suffix = extras.length ? ` [${extras.join(', ')}]` : '';
|
|
42
|
-
|
|
74
|
+
// Liveness tag for active claims (omit for released/done)
|
|
75
|
+
let livenessTag = '';
|
|
76
|
+
if (c.status === 'active') {
|
|
77
|
+
const liveness = assessClaimLiveness(c, { cwd: options.cwd });
|
|
78
|
+
if (liveness.status === 'live')
|
|
79
|
+
livenessTag = ' [LIVE]';
|
|
80
|
+
else if (liveness.status === 'orphaned')
|
|
81
|
+
livenessTag = ' [ORPHANED]';
|
|
82
|
+
else if (liveness.status === 'never-adopted')
|
|
83
|
+
livenessTag = ' [NEVER-ADOPTED]';
|
|
84
|
+
else if (liveness.status === 'stale')
|
|
85
|
+
livenessTag = ' [STALE]';
|
|
86
|
+
}
|
|
87
|
+
console.log(` [${c.id}] ${c.agent} → ${c.scope}: ${c.description}${suffix}${livenessTag}${status}${expired}`);
|
|
43
88
|
}
|
|
44
89
|
}
|
|
45
90
|
//# sourceMappingURL=list-claims.js.map
|
|
@@ -1,48 +1,152 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { loadState } from '../core/state.js';
|
|
2
3
|
import { memoryExists } from '../core/io.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
let plans = loadState(options.cwd).plan_items;
|
|
4
|
+
import { resolveStoreChain } from '../core/store-resolution.js';
|
|
5
|
+
import { scanNestedBrainclawProjects } from '../core/workspace-projects.js';
|
|
6
|
+
function filterPlans(plans, options) {
|
|
7
|
+
let filtered = plans;
|
|
9
8
|
if (!options.all) {
|
|
10
|
-
|
|
9
|
+
filtered = filtered.filter((plan) => plan.status !== 'done' && plan.status !== 'dropped');
|
|
11
10
|
}
|
|
12
11
|
if (options.status) {
|
|
13
|
-
|
|
12
|
+
filtered = filtered.filter((plan) => plan.status === options.status);
|
|
14
13
|
}
|
|
15
14
|
if (options.type) {
|
|
16
|
-
|
|
15
|
+
filtered = filtered.filter((plan) => plan.type === options.type);
|
|
17
16
|
}
|
|
18
17
|
if (options.assignee) {
|
|
19
18
|
const target = options.assignee.toLowerCase();
|
|
20
|
-
|
|
19
|
+
filtered = filtered.filter((plan) => plan.assignee?.toLowerCase() === target);
|
|
21
20
|
}
|
|
22
21
|
if (options.project) {
|
|
23
22
|
const project = options.project.toLowerCase();
|
|
24
|
-
|
|
23
|
+
filtered = filtered.filter((plan) => plan.project?.toLowerCase() === project);
|
|
24
|
+
}
|
|
25
|
+
return filtered;
|
|
26
|
+
}
|
|
27
|
+
function formatPlan(plan) {
|
|
28
|
+
const meta = [plan.type ?? 'feat', plan.status, plan.priority];
|
|
29
|
+
if (plan.assignee)
|
|
30
|
+
meta.push(`assignee ${plan.assignee}`);
|
|
31
|
+
if (plan.project)
|
|
32
|
+
meta.push(`project ${plan.project}`);
|
|
33
|
+
if (plan.depends_on.length > 0)
|
|
34
|
+
meta.push(`depends_on ${plan.depends_on.join(',')}`);
|
|
35
|
+
const tags = plan.tags.length ? ` [${plan.tags.join(', ')}]` : '';
|
|
36
|
+
return ` [${plan.id}] ${plan.text} (${meta.join(' · ')})${tags}`;
|
|
37
|
+
}
|
|
38
|
+
export function scanDescendantPlans(cwd, options) {
|
|
39
|
+
const resolvedCwd = path.resolve(cwd ?? process.cwd());
|
|
40
|
+
const descendants = scanNestedBrainclawProjects(resolvedCwd);
|
|
41
|
+
const groups = [];
|
|
42
|
+
// In recursive mode, --project filters by descendant project name/path, not plan.project field
|
|
43
|
+
const projectFilter = options.project?.toLowerCase();
|
|
44
|
+
const filterOpts = { ...options, project: undefined };
|
|
45
|
+
for (const project of descendants) {
|
|
46
|
+
const relativePath = path.relative(resolvedCwd, project.path);
|
|
47
|
+
const projectName = project.project_name?.toLowerCase();
|
|
48
|
+
const relLower = relativePath.toLowerCase();
|
|
49
|
+
const baseLower = path.basename(project.path).toLowerCase();
|
|
50
|
+
if (projectFilter && projectName !== projectFilter && relLower !== projectFilter && baseLower !== projectFilter) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const plans = filterPlans(loadState(project.path).plan_items, filterOpts);
|
|
55
|
+
if (plans.length > 0) {
|
|
56
|
+
groups.push({
|
|
57
|
+
path: project.path,
|
|
58
|
+
relative_path: relativePath,
|
|
59
|
+
project_name: project.project_name,
|
|
60
|
+
plans,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch { /* skip unreadable project */ }
|
|
65
|
+
}
|
|
66
|
+
return groups;
|
|
67
|
+
}
|
|
68
|
+
export function runListPlans(options = {}) {
|
|
69
|
+
if (!memoryExists(options.cwd)) {
|
|
70
|
+
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
const effectiveCwd = options.cwd ?? process.cwd();
|
|
74
|
+
let rawPlans;
|
|
75
|
+
if (options.localOnly) {
|
|
76
|
+
rawPlans = loadState(options.cwd).plan_items;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const chain = resolveStoreChain(effectiveCwd);
|
|
80
|
+
const seenIds = new Set();
|
|
81
|
+
rawPlans = [];
|
|
82
|
+
for (const store of chain) {
|
|
83
|
+
try {
|
|
84
|
+
for (const plan of loadState(store.cwd).plan_items) {
|
|
85
|
+
if (!seenIds.has(plan.id)) {
|
|
86
|
+
seenIds.add(plan.id);
|
|
87
|
+
rawPlans.push(plan);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch { /* skip unreadable stores */ }
|
|
92
|
+
}
|
|
93
|
+
// Fallback when no chain found (should not happen if memoryExists passed)
|
|
94
|
+
if (rawPlans.length === 0 && chain.length === 0) {
|
|
95
|
+
rawPlans = loadState(options.cwd).plan_items;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const localPlans = filterPlans(rawPlans, options);
|
|
99
|
+
if (options.recursive) {
|
|
100
|
+
const cwd = options.cwd ?? process.cwd();
|
|
101
|
+
const descendantGroups = scanDescendantPlans(cwd, options);
|
|
102
|
+
const totalDescendantPlans = descendantGroups.reduce((sum, g) => sum + g.plans.length, 0);
|
|
103
|
+
if (options.json) {
|
|
104
|
+
console.log(JSON.stringify({
|
|
105
|
+
local: localPlans,
|
|
106
|
+
descendants: descendantGroups,
|
|
107
|
+
total: localPlans.length + totalDescendantPlans,
|
|
108
|
+
}, null, 2));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Local plans
|
|
112
|
+
console.log(`── local (${localPlans.length} plans) ──`);
|
|
113
|
+
if (localPlans.length === 0) {
|
|
114
|
+
console.log(' (none)');
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
for (const plan of localPlans)
|
|
118
|
+
console.log(formatPlan(plan));
|
|
119
|
+
}
|
|
120
|
+
// Descendant plans
|
|
121
|
+
for (const group of descendantGroups) {
|
|
122
|
+
const label = group.project_name ?? group.relative_path;
|
|
123
|
+
console.log(`\n── ${label} (${group.plans.length} plans) ──`);
|
|
124
|
+
for (const plan of group.plans)
|
|
125
|
+
console.log(formatPlan(plan));
|
|
126
|
+
}
|
|
127
|
+
if (localPlans.length === 0 && totalDescendantPlans === 0) {
|
|
128
|
+
console.log('\nNo plan items found locally or in descendants.');
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
25
131
|
}
|
|
26
132
|
if (options.json) {
|
|
27
|
-
console.log(JSON.stringify(
|
|
133
|
+
console.log(JSON.stringify(localPlans, null, 2));
|
|
28
134
|
return;
|
|
29
135
|
}
|
|
30
|
-
if (
|
|
136
|
+
if (localPlans.length === 0) {
|
|
137
|
+
// Check descendants for signal
|
|
138
|
+
const cwd = options.cwd ?? process.cwd();
|
|
139
|
+
const descendantGroups = scanDescendantPlans(cwd, options);
|
|
140
|
+
const totalDescendantPlans = descendantGroups.reduce((sum, g) => sum + g.plans.length, 0);
|
|
31
141
|
console.log('No plan items found.');
|
|
142
|
+
if (totalDescendantPlans > 0) {
|
|
143
|
+
console.log(`ℹ ${totalDescendantPlans} plan(s) found in ${descendantGroups.length} descendant project(s) (use --recursive to see all)`);
|
|
144
|
+
}
|
|
32
145
|
return;
|
|
33
146
|
}
|
|
34
|
-
console.log(`${
|
|
147
|
+
console.log(`${localPlans.length} plan item(s):`);
|
|
35
148
|
console.log('');
|
|
36
|
-
for (const plan of
|
|
37
|
-
|
|
38
|
-
if (plan.assignee)
|
|
39
|
-
meta.push(`assignee ${plan.assignee}`);
|
|
40
|
-
if (plan.project)
|
|
41
|
-
meta.push(`project ${plan.project}`);
|
|
42
|
-
if (plan.depends_on.length > 0)
|
|
43
|
-
meta.push(`depends_on ${plan.depends_on.join(',')}`);
|
|
44
|
-
const tags = plan.tags.length ? ` [${plan.tags.join(', ')}]` : '';
|
|
45
|
-
console.log(` [${plan.id}] ${plan.text} (${meta.join(' · ')})${tags}`);
|
|
46
|
-
}
|
|
149
|
+
for (const plan of localPlans)
|
|
150
|
+
console.log(formatPlan(plan));
|
|
47
151
|
}
|
|
48
152
|
//# sourceMappingURL=list-plans.js.map
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { ZodError } from 'zod';
|
|
2
|
+
import { listAgentRuns } from '../core/agentruns.js';
|
|
3
|
+
import { reconcileAgentRun } from '../core/agentrun-reconciler.js';
|
|
4
|
+
import { add_artifact, advance, closeLoop, complete_turn, getLoop, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, listLoopEvents, listLoops, LockLostError, LockTimeoutError, openLoop, pause, resume, turn, VersionConflictError, withLoopLock, } from '../core/loops/index.js';
|
|
5
|
+
import { BclawLoopRequestSchema, BCLAW_LOOP_INTENTS, } from '../core/loops/facade-schema.js';
|
|
6
|
+
function resolveActor(req, defaultActor) {
|
|
7
|
+
const agentId = req.agentId?.trim() || defaultActor;
|
|
8
|
+
const actor = req.agent?.trim() || req.agentId?.trim() || defaultActor;
|
|
9
|
+
return { actor, agentId };
|
|
10
|
+
}
|
|
11
|
+
function successResponse(intent, result, artifacts, side_effects, warnings, durationMs, summary) {
|
|
12
|
+
return {
|
|
13
|
+
response: {
|
|
14
|
+
status: 'ok',
|
|
15
|
+
intent: `bclaw_loop.${intent}`,
|
|
16
|
+
result,
|
|
17
|
+
artifacts,
|
|
18
|
+
side_effects,
|
|
19
|
+
warnings,
|
|
20
|
+
duration_ms: durationMs,
|
|
21
|
+
},
|
|
22
|
+
summary,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function errorResponse(intent, code, message, durationMs, result = null) {
|
|
26
|
+
return {
|
|
27
|
+
response: {
|
|
28
|
+
status: 'error',
|
|
29
|
+
intent: `bclaw_loop.${intent}`,
|
|
30
|
+
result,
|
|
31
|
+
artifacts: [],
|
|
32
|
+
side_effects: [],
|
|
33
|
+
warnings: [],
|
|
34
|
+
error: `${code}: ${message}`,
|
|
35
|
+
duration_ms: durationMs,
|
|
36
|
+
},
|
|
37
|
+
summary: `✘ bclaw_loop[${intent}] ${code}: ${message}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function inferIntent(args) {
|
|
41
|
+
if (!args || typeof args !== 'object')
|
|
42
|
+
return 'unknown';
|
|
43
|
+
const candidate = args.intent;
|
|
44
|
+
if (typeof candidate !== 'string')
|
|
45
|
+
return 'unknown';
|
|
46
|
+
return BCLAW_LOOP_INTENTS.includes(candidate)
|
|
47
|
+
? candidate
|
|
48
|
+
: 'unknown';
|
|
49
|
+
}
|
|
50
|
+
function loopArtifactEntry(id) {
|
|
51
|
+
return { type: 'loop', id };
|
|
52
|
+
}
|
|
53
|
+
function loopEventArtifactEntry(id) {
|
|
54
|
+
return { type: 'loop_event', id };
|
|
55
|
+
}
|
|
56
|
+
function sideEffectCreate(entity, id) {
|
|
57
|
+
return { action: 'create', entity, id };
|
|
58
|
+
}
|
|
59
|
+
function sideEffectUpdate(entity, id) {
|
|
60
|
+
return { action: 'update', entity, id };
|
|
61
|
+
}
|
|
62
|
+
function snapshotLoopEvents(loopId, cwd) {
|
|
63
|
+
return new Set(listLoopEvents(loopId, cwd).map((event) => event.event_id));
|
|
64
|
+
}
|
|
65
|
+
function findNewLoopEvents(loopId, before, cwd) {
|
|
66
|
+
const events = listLoopEvents(loopId, cwd);
|
|
67
|
+
if (!before)
|
|
68
|
+
return events;
|
|
69
|
+
return events.filter((event) => !before.has(event.event_id));
|
|
70
|
+
}
|
|
71
|
+
function loopEventArtifacts(events) {
|
|
72
|
+
return events.map((event) => loopEventArtifactEntry(event.event_id));
|
|
73
|
+
}
|
|
74
|
+
function loopEventSideEffects(events) {
|
|
75
|
+
return events.map((event) => sideEffectCreate('loop_event', event.event_id));
|
|
76
|
+
}
|
|
77
|
+
function validateSemanticRequest(req) {
|
|
78
|
+
if (req.intent === 'turn' && !req.slot_id && !req.role) {
|
|
79
|
+
return 'turn requires slot_id or role';
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function requestPayload(req) {
|
|
84
|
+
const { agent, agentId, client_request_id, ...rest } = req;
|
|
85
|
+
return rest;
|
|
86
|
+
}
|
|
87
|
+
function currentLoopVersion(loopId, cwd) {
|
|
88
|
+
return getLoop(loopId, cwd)?.version ?? 0;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Slot-bound intents must not let a cached idempotent response leak to a
|
|
92
|
+
* different caller. `complete_turn` is the obvious case: it carries auth
|
|
93
|
+
* semantics (caller_agent_id must match the slot owner or loop.created_by).
|
|
94
|
+
* Without caller-match enforcement on the idempotency cache, a second caller
|
|
95
|
+
* who learned the client_request_id could replay it and receive the cached
|
|
96
|
+
* success payload — bypassing slot-bound auth from an information-disclosure
|
|
97
|
+
* perspective even though the state change already happened. Intents listed
|
|
98
|
+
* here get `requireCallerMatch: true` on their withLoopLock invocation.
|
|
99
|
+
*/
|
|
100
|
+
const SLOT_BOUND_INTENTS = new Set(['complete_turn']);
|
|
101
|
+
/**
|
|
102
|
+
* Fence-check discipline for mutations.
|
|
103
|
+
*
|
|
104
|
+
* The `work` callback calls `fenceCheck()` at entry before invoking the verb.
|
|
105
|
+
* This closes the "lock acquired, then reaped mid-wait, then foreign writer
|
|
106
|
+
* took over" window — the verb will not proceed if the lock's mutation_id
|
|
107
|
+
* changed between `acquireLock` and `work` dispatch. It does NOT cover mid-verb
|
|
108
|
+
* fs operations: the verbs themselves (`openLoop`, `advance`, …) perform their
|
|
109
|
+
* atomic-rename + JSONL append without consulting the fence. That gap is
|
|
110
|
+
* intentional for the MVP because the verbs are synchronous and complete in
|
|
111
|
+
* single-digit milliseconds, much shorter than any reasonable hard_deadline
|
|
112
|
+
* (default 30_000 ms). If a future slice adds async dispatch inside a
|
|
113
|
+
* mutation, thread `fenceCheck` down into the verb and call it before each
|
|
114
|
+
* committing write.
|
|
115
|
+
*/
|
|
116
|
+
function withLockedLoopMutation(req, agentId, cwd, work) {
|
|
117
|
+
return withLoopLock({
|
|
118
|
+
cwd,
|
|
119
|
+
intent: req.intent,
|
|
120
|
+
agentId,
|
|
121
|
+
scope: { kind: 'loop', loopId: req.loop_id },
|
|
122
|
+
expectedVersion: req.expected_version,
|
|
123
|
+
clientRequestId: req.client_request_id,
|
|
124
|
+
requestPayload: requestPayload(req),
|
|
125
|
+
currentVersion: () => currentLoopVersion(req.loop_id, cwd),
|
|
126
|
+
requireCallerMatch: SLOT_BOUND_INTENTS.has(req.intent),
|
|
127
|
+
work: ({ fenceCheck }) => {
|
|
128
|
+
// Best-effort fence at entry; see SLOT_BOUND_INTENTS / fence-check
|
|
129
|
+
// discipline comment above for why mid-verb re-checks are deferred.
|
|
130
|
+
fenceCheck();
|
|
131
|
+
return work();
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* `NextExpectedHint` — self-describing hint to the caller about the most
|
|
137
|
+
* natural next intent. Kept conservative for the MVP: we look at the loop's
|
|
138
|
+
* status + slot states and pick the smallest correct action.
|
|
139
|
+
*/
|
|
140
|
+
function computeNextExpected(loop) {
|
|
141
|
+
if (loop.status === 'completed' || loop.status === 'cancelled' || loop.status === 'blocked') {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (loop.status === 'paused') {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const currentPhaseSlots = loop.slots.filter((s) => (s.phase ?? loop.current_phase) === loop.current_phase);
|
|
148
|
+
const openSlots = currentPhaseSlots.filter((s) => s.status === 'open');
|
|
149
|
+
if (openSlots.length > 0) {
|
|
150
|
+
const first = openSlots[0];
|
|
151
|
+
return {
|
|
152
|
+
action: 'turn',
|
|
153
|
+
intent: 'bclaw_loop.turn',
|
|
154
|
+
phase: loop.current_phase,
|
|
155
|
+
slot_id: first.slot_id,
|
|
156
|
+
role: first.role,
|
|
157
|
+
blocking_on: openSlots.map((s) => s.slot_id),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const assignedOrWorking = currentPhaseSlots.filter((s) => s.status === 'assigned' || s.status === 'working');
|
|
161
|
+
if (assignedOrWorking.length > 0) {
|
|
162
|
+
return {
|
|
163
|
+
action: 'complete_turn',
|
|
164
|
+
intent: 'bclaw_loop.complete_turn',
|
|
165
|
+
phase: loop.current_phase,
|
|
166
|
+
slot_id: assignedOrWorking[0].slot_id,
|
|
167
|
+
role: assignedOrWorking[0].role,
|
|
168
|
+
blocking_on: assignedOrWorking.map((s) => s.slot_id),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const phaseNames = loop.phases.map((p) => p.name);
|
|
172
|
+
const currentIndex = phaseNames.indexOf(loop.current_phase);
|
|
173
|
+
if (currentIndex >= 0 && currentIndex + 1 < phaseNames.length) {
|
|
174
|
+
return {
|
|
175
|
+
action: 'advance',
|
|
176
|
+
intent: 'bclaw_loop.advance',
|
|
177
|
+
from_phase: loop.current_phase,
|
|
178
|
+
to_phase: phaseNames[currentIndex + 1],
|
|
179
|
+
blocking_on: [],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
action: 'close',
|
|
184
|
+
intent: 'bclaw_loop.close',
|
|
185
|
+
reason: 'terminal_phase_reached',
|
|
186
|
+
blocking_on: [],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function summarizeLoop(loop, autoClosed) {
|
|
190
|
+
const suffix = autoClosed ? ' (auto-closed)' : '';
|
|
191
|
+
return `✔ loop ${loop.id} [${loop.kind}] phase=${loop.current_phase} status=${loop.status}${suffix}`;
|
|
192
|
+
}
|
|
193
|
+
export function handleBclawLoop(options) {
|
|
194
|
+
const startMs = Date.now();
|
|
195
|
+
const defaultActor = options.defaultActor ?? 'bclaw_loop';
|
|
196
|
+
const inferredIntent = inferIntent(options.args);
|
|
197
|
+
const parseResult = BclawLoopRequestSchema.safeParse(options.args);
|
|
198
|
+
if (!parseResult.success) {
|
|
199
|
+
return errorResponse(inferredIntent, 'validation_error', parseResult.error.message, Date.now() - startMs);
|
|
200
|
+
}
|
|
201
|
+
const req = parseResult.data;
|
|
202
|
+
const semanticError = validateSemanticRequest(req);
|
|
203
|
+
if (semanticError) {
|
|
204
|
+
return errorResponse(req.intent, 'validation_error', semanticError, Date.now() - startMs);
|
|
205
|
+
}
|
|
206
|
+
const { actor, agentId } = resolveActor(req, defaultActor);
|
|
207
|
+
try {
|
|
208
|
+
switch (req.intent) {
|
|
209
|
+
case 'open': {
|
|
210
|
+
if (!req.allow_orphan) {
|
|
211
|
+
return errorResponse('open', 'validation_error', "Direct bclaw_loop(intent='open') creates a loop with no dispatch — no claim, no inbox message, no agent will pick up the work. Use bclaw_coordinate(intent='review', open_loop=true) for review loops (recommended), or pass allow_orphan: true to acknowledge that you will handle turn() + dispatch manually (advanced/test use only). See CLAUDE.md anti-pattern note and pln#461.", Date.now() - startMs);
|
|
212
|
+
}
|
|
213
|
+
const runOpen = () => {
|
|
214
|
+
const loop = openLoop({
|
|
215
|
+
kind: req.kind,
|
|
216
|
+
title: req.title,
|
|
217
|
+
goal: req.goal,
|
|
218
|
+
phases: req.phases,
|
|
219
|
+
slots: req.slots,
|
|
220
|
+
linked: req.linked,
|
|
221
|
+
stop_condition: req.stop_condition,
|
|
222
|
+
mode: req.mode,
|
|
223
|
+
created_by: agentId,
|
|
224
|
+
}, options.cwd);
|
|
225
|
+
const newEvents = findNewLoopEvents(loop.id, undefined, options.cwd);
|
|
226
|
+
return successResponse('open', { loop, next_expected: computeNextExpected(loop) }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectCreate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, `✔ opened ${loop.id} [${loop.kind}] phase=${loop.current_phase}`);
|
|
227
|
+
};
|
|
228
|
+
if (!req.client_request_id) {
|
|
229
|
+
return runOpen();
|
|
230
|
+
}
|
|
231
|
+
return withLoopLock({
|
|
232
|
+
cwd: options.cwd,
|
|
233
|
+
intent: req.intent,
|
|
234
|
+
agentId,
|
|
235
|
+
scope: { kind: 'open_idempotency', clientRequestId: req.client_request_id },
|
|
236
|
+
clientRequestId: req.client_request_id,
|
|
237
|
+
requestPayload: requestPayload(req),
|
|
238
|
+
loopIdForIdempotency: undefined,
|
|
239
|
+
work: () => runOpen(),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
case 'get': {
|
|
243
|
+
const loop = getLoop(req.loop_id, options.cwd);
|
|
244
|
+
if (!loop) {
|
|
245
|
+
return errorResponse('get', 'not_found', `unknown loop_id ${req.loop_id}`, Date.now() - startMs);
|
|
246
|
+
}
|
|
247
|
+
// pln#496 Phase 2: reconcile each slot's assigned run before
|
|
248
|
+
// returning the loop. Catches the silent-completion case where a
|
|
249
|
+
// dispatched reviewer committed work / released their claim but
|
|
250
|
+
// never emitted run_completed — the loop appeared stuck on
|
|
251
|
+
// 'assigned' slots forever in May 2026 (lop_3b2068e25166e183 +
|
|
252
|
+
// lop_ea5852302acb8cbb). Targeted to slots with assignment_id so
|
|
253
|
+
// we never run a broad scan here.
|
|
254
|
+
try {
|
|
255
|
+
for (const slot of loop.slots ?? []) {
|
|
256
|
+
const slotStatus = slot.status;
|
|
257
|
+
const assignmentId = slot.assignment_id;
|
|
258
|
+
if (!assignmentId)
|
|
259
|
+
continue;
|
|
260
|
+
if (slotStatus === 'done' || slotStatus === 'failed' || slotStatus === 'cancelled')
|
|
261
|
+
continue;
|
|
262
|
+
for (const run of listAgentRuns(options.cwd, { assignment_id: assignmentId })) {
|
|
263
|
+
reconcileAgentRun(run.id, options.cwd);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch { /* defensive: never block loop reads on reconcile errors */ }
|
|
268
|
+
// Re-load the loop after reconciliation so any slot transitions
|
|
269
|
+
// triggered by the reconciler (e.g. via assignment lifecycle hooks)
|
|
270
|
+
// are reflected in the response. Cheap (fs read).
|
|
271
|
+
const reconciledLoop = getLoop(req.loop_id, options.cwd) ?? loop;
|
|
272
|
+
const events = req.include_events ? listLoopEvents(req.loop_id, options.cwd) : undefined;
|
|
273
|
+
return successResponse('get', { loop: reconciledLoop, events, next_expected: computeNextExpected(reconciledLoop) }, [loopArtifactEntry(reconciledLoop.id)], [], [], Date.now() - startMs, summarizeLoop(reconciledLoop));
|
|
274
|
+
}
|
|
275
|
+
case 'list': {
|
|
276
|
+
const loops = listLoops({ kind: req.kind, status: req.status }, options.cwd);
|
|
277
|
+
const sliced = typeof req.offset === 'number' || typeof req.limit === 'number'
|
|
278
|
+
? loops.slice(req.offset ?? 0, (req.offset ?? 0) + (req.limit ?? loops.length))
|
|
279
|
+
: loops;
|
|
280
|
+
return successResponse('list', { loops: sliced, total: loops.length }, sliced.map((l) => loopArtifactEntry(l.id)), [], [], Date.now() - startMs, `✔ list ${sliced.length}/${loops.length} loops`);
|
|
281
|
+
}
|
|
282
|
+
case 'turn': {
|
|
283
|
+
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
284
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
285
|
+
const loop = turn({
|
|
286
|
+
id: req.loop_id,
|
|
287
|
+
slot_id: req.slot_id,
|
|
288
|
+
role: req.role,
|
|
289
|
+
input: req.input,
|
|
290
|
+
dispatch: req.dispatch,
|
|
291
|
+
assignment_id: req.assignment_id,
|
|
292
|
+
actor,
|
|
293
|
+
}, options.cwd);
|
|
294
|
+
const newEvents = findNewLoopEvents(loop.id, beforeEvents, options.cwd);
|
|
295
|
+
return successResponse('turn', { loop, next_expected: computeNextExpected(loop) }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(loop));
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
case 'complete_turn': {
|
|
299
|
+
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
300
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
301
|
+
const loop = complete_turn({
|
|
302
|
+
id: req.loop_id,
|
|
303
|
+
slot_id: req.slot_id,
|
|
304
|
+
outcome: req.outcome,
|
|
305
|
+
failure_reason: req.failure_reason,
|
|
306
|
+
artifact: req.artifact
|
|
307
|
+
? {
|
|
308
|
+
phase: req.artifact.phase,
|
|
309
|
+
type: req.artifact.type,
|
|
310
|
+
body: req.artifact.body,
|
|
311
|
+
ref: req.artifact.ref,
|
|
312
|
+
}
|
|
313
|
+
: undefined,
|
|
314
|
+
actor,
|
|
315
|
+
caller_agent_id: req.agentId,
|
|
316
|
+
}, options.cwd);
|
|
317
|
+
const newEvents = findNewLoopEvents(loop.id, beforeEvents, options.cwd);
|
|
318
|
+
return successResponse('complete_turn', { loop, next_expected: computeNextExpected(loop) }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(loop));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
case 'advance': {
|
|
322
|
+
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
323
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
324
|
+
const result = advance({
|
|
325
|
+
id: req.loop_id,
|
|
326
|
+
to_phase: req.to_phase,
|
|
327
|
+
reason: req.reason,
|
|
328
|
+
force: req.force,
|
|
329
|
+
actor,
|
|
330
|
+
}, options.cwd);
|
|
331
|
+
const newEvents = findNewLoopEvents(result.loop.id, beforeEvents, options.cwd);
|
|
332
|
+
return successResponse('advance', { loop: result.loop, auto_closed: result.auto_closed, next_expected: computeNextExpected(result.loop) }, [loopArtifactEntry(result.loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', result.loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(result.loop, result.auto_closed));
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
case 'add_artifact': {
|
|
336
|
+
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
337
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
338
|
+
const loop = add_artifact({
|
|
339
|
+
id: req.loop_id,
|
|
340
|
+
artifact: {
|
|
341
|
+
phase: req.artifact.phase,
|
|
342
|
+
type: req.artifact.type,
|
|
343
|
+
body: req.artifact.body,
|
|
344
|
+
produced_by: req.artifact.produced_by,
|
|
345
|
+
ref: req.artifact.ref,
|
|
346
|
+
},
|
|
347
|
+
actor,
|
|
348
|
+
}, options.cwd);
|
|
349
|
+
const newEvents = findNewLoopEvents(loop.id, beforeEvents, options.cwd);
|
|
350
|
+
return successResponse('add_artifact', { loop, next_expected: computeNextExpected(loop) }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(loop));
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
case 'pause': {
|
|
354
|
+
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
355
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
356
|
+
const loop = pause({ id: req.loop_id, reason: req.reason, actor }, options.cwd);
|
|
357
|
+
const newEvents = findNewLoopEvents(loop.id, beforeEvents, options.cwd);
|
|
358
|
+
return successResponse('pause', { loop, next_expected: null }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(loop));
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
case 'resume': {
|
|
362
|
+
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
363
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
364
|
+
const loop = resume({ id: req.loop_id, actor }, options.cwd);
|
|
365
|
+
const newEvents = findNewLoopEvents(loop.id, beforeEvents, options.cwd);
|
|
366
|
+
return successResponse('resume', { loop, next_expected: computeNextExpected(loop) }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(loop));
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
case 'close': {
|
|
370
|
+
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
371
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
372
|
+
const loop = closeLoop({ id: req.loop_id, final_status: req.status, reason: req.reason, actor }, options.cwd);
|
|
373
|
+
const newEvents = findNewLoopEvents(loop.id, beforeEvents, options.cwd);
|
|
374
|
+
return successResponse('close', { loop, next_expected: null }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(loop));
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
if (err instanceof ZodError) {
|
|
381
|
+
return errorResponse(req.intent, 'validation_error', err.message, Date.now() - startMs);
|
|
382
|
+
}
|
|
383
|
+
if (err instanceof VersionConflictError) {
|
|
384
|
+
return errorResponse(req.intent, 'version_conflict', `expected=${err.expected} actual=${err.actual}`, Date.now() - startMs, { actual_version: err.actual });
|
|
385
|
+
}
|
|
386
|
+
if (err instanceof IdempotencyKeyReusedError) {
|
|
387
|
+
return errorResponse(req.intent, 'idempotency_key_reused_with_different_body', `stored_hash=${err.storedHash} submitted_hash=${err.submittedHash}`, Date.now() - startMs, { stored_hash: err.storedHash, submitted_hash: err.submittedHash });
|
|
388
|
+
}
|
|
389
|
+
if (err instanceof IdempotencyOwnerMismatchError) {
|
|
390
|
+
return errorResponse(req.intent, 'idempotency_owner_mismatch', `stored_owner=${err.storedOwner ?? 'unknown'} submitted_owner=${err.submittedOwner}`, Date.now() - startMs, { stored_owner: err.storedOwner, submitted_owner: err.submittedOwner });
|
|
391
|
+
}
|
|
392
|
+
if (err instanceof LockTimeoutError) {
|
|
393
|
+
return errorResponse(req.intent, 'lock_timeout', err.message, Date.now() - startMs);
|
|
394
|
+
}
|
|
395
|
+
if (err instanceof LockLostError) {
|
|
396
|
+
return errorResponse(req.intent, 'lock_lost', err.message, Date.now() - startMs);
|
|
397
|
+
}
|
|
398
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
399
|
+
if (message.includes('unauthorized_slot_write')) {
|
|
400
|
+
return errorResponse(req.intent, 'unauthorized_slot_write', message, Date.now() - startMs);
|
|
401
|
+
}
|
|
402
|
+
if (message.startsWith('unknown loop_id')) {
|
|
403
|
+
return errorResponse(req.intent, 'not_found', message, Date.now() - startMs);
|
|
404
|
+
}
|
|
405
|
+
return errorResponse(req.intent, 'verb_error', message, Date.now() - startMs);
|
|
406
|
+
}
|
|
407
|
+
return errorResponse('unknown', 'unreachable', 'unexpected fallthrough', Date.now() - startMs);
|
|
408
|
+
}
|
|
409
|
+
//# sourceMappingURL=loops-handlers.js.map
|