agent-relay-server 0.61.2 → 0.62.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/docs/openapi.json +479 -1
- package/package.json +2 -2
- package/public/assets/{MessageBubble-DVvlvi4B.js → MessageBubble-DRrxfjyT.js} +2 -2
- package/public/assets/{MessageBubble-DVvlvi4B.js.map → MessageBubble-DRrxfjyT.js.map} +1 -1
- package/public/assets/PlanGraphPanel-Dp2eYazz.js +2 -0
- package/public/assets/PlanGraphPanel-Dp2eYazz.js.map +1 -0
- package/public/assets/{activity-IYl3MEXC.js → activity--t0U2yzq.js} +2 -2
- package/public/assets/{activity-IYl3MEXC.js.map → activity--t0U2yzq.js.map} +1 -1
- package/public/assets/{agent-profiles-BfGTTbeY.js → agent-profiles-BMtJYzxn.js} +2 -2
- package/public/assets/{agent-profiles-BfGTTbeY.js.map → agent-profiles-BMtJYzxn.js.map} +1 -1
- package/public/assets/{agents-BquvREQr.js → agents-Ck9AePlc.js} +2 -2
- package/public/assets/{agents-BquvREQr.js.map → agents-Ck9AePlc.js.map} +1 -1
- package/public/assets/{analytics-De0cEZGx.js → analytics-CzfK387L.js} +3 -3
- package/public/assets/{analytics-De0cEZGx.js.map → analytics-CzfK387L.js.map} +1 -1
- package/public/assets/automation-Cgw8vtE8.js +2 -0
- package/public/assets/automation-Cgw8vtE8.js.map +1 -0
- package/public/assets/{badge-DhLyCPag.js → badge-CZhIdlTJ.js} +2 -2
- package/public/assets/{badge-DhLyCPag.js.map → badge-CZhIdlTJ.js.map} +1 -1
- package/public/assets/{branch-state-badge-DSyWKluD.js → branch-state-badge-DnEEPtOP.js} +2 -2
- package/public/assets/{branch-state-badge-DSyWKluD.js.map → branch-state-badge-DnEEPtOP.js.map} +1 -1
- package/public/assets/{button-K20wAZG1.js → button-2taQeNHW.js} +2 -2
- package/public/assets/{button-K20wAZG1.js.map → button-2taQeNHW.js.map} +1 -1
- package/public/assets/card-DzIsAI41.js +2 -0
- package/public/assets/{card-CcZ9hLI7.js.map → card-DzIsAI41.js.map} +1 -1
- package/public/assets/channels-Dc3L2MQs.js +2 -0
- package/public/assets/{channels-BG0Nib7y.js.map → channels-Dc3L2MQs.js.map} +1 -1
- package/public/assets/chat-Fc6iJ7sa.js +2 -0
- package/public/assets/{chat-Dx6qWNxb.js.map → chat-Fc6iJ7sa.js.map} +1 -1
- package/public/assets/{connectors-DWh7MwV_.js → connectors-Dc2ttGWs.js} +2 -2
- package/public/assets/{connectors-DWh7MwV_.js.map → connectors-Dc2ttGWs.js.map} +1 -1
- package/public/assets/copy-button-HEkwi6xX.js +2 -0
- package/public/assets/{copy-button-eF3cKp4Q.js.map → copy-button-HEkwi6xX.js.map} +1 -1
- package/public/assets/display-DzNvOu1j.js.map +1 -1
- package/public/assets/dist-1rXN5FN9.js +2 -0
- package/public/assets/{dist-YpHfnnIA.js.map → dist-1rXN5FN9.js.map} +1 -1
- package/public/assets/{dist-1dhfdhm9.js → dist-AbEQuFoK.js} +2 -2
- package/public/assets/{dist-1dhfdhm9.js.map → dist-AbEQuFoK.js.map} +1 -1
- package/public/assets/{dist-COQuunEv.js → dist-B7p1zkvf.js} +2 -2
- package/public/assets/{dist-COQuunEv.js.map → dist-B7p1zkvf.js.map} +1 -1
- package/public/assets/{dist-Ci5Y37nj.js → dist-CG10hM1-.js} +2 -2
- package/public/assets/{dist-Ci5Y37nj.js.map → dist-CG10hM1-.js.map} +1 -1
- package/public/assets/dist-Cfm5_zPB.js +2 -0
- package/public/assets/{dist-qd198ib9.js.map → dist-Cfm5_zPB.js.map} +1 -1
- package/public/assets/{dist-BrQsqBbc.js → dist-Cgw85UcE.js} +2 -2
- package/public/assets/{dist-BrQsqBbc.js.map → dist-Cgw85UcE.js.map} +1 -1
- package/public/assets/{dist-CoROafNg.js → dist-D3_TJKTC.js} +2 -2
- package/public/assets/{dist-CoROafNg.js.map → dist-D3_TJKTC.js.map} +1 -1
- package/public/assets/{dist-I-MFUv51.js → dist-DQkLq0hs.js} +2 -2
- package/public/assets/{dist-I-MFUv51.js.map → dist-DQkLq0hs.js.map} +1 -1
- package/public/assets/{dist-UQGKwqZH.js → dist-D_sl7u_f.js} +2 -2
- package/public/assets/{dist-UQGKwqZH.js.map → dist-D_sl7u_f.js.map} +1 -1
- package/public/assets/{dist-yGOq6sB3.js → dist-Ilb3r1im.js} +2 -2
- package/public/assets/{dist-yGOq6sB3.js.map → dist-Ilb3r1im.js.map} +1 -1
- package/public/assets/dist-Pf_3Gf38.js +2 -0
- package/public/assets/{dist-BLBF7qqr.js.map → dist-Pf_3Gf38.js.map} +1 -1
- package/public/assets/{es2015-CRxz9lOj.js → es2015-GSepNaIS.js} +2 -2
- package/public/assets/{es2015-CRxz9lOj.js.map → es2015-GSepNaIS.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DMm6jeqi.js → formatted-body-impl-Bg-gx7rG.js} +2 -2
- package/public/assets/{formatted-body-impl-DMm6jeqi.js.map → formatted-body-impl-Bg-gx7rG.js.map} +1 -1
- package/public/assets/index-BeDncjN4.js +22 -0
- package/public/assets/index-BeDncjN4.js.map +1 -0
- package/public/assets/index-DiR-fqy_.css +2 -0
- package/public/assets/{input-BHYNpXAG.js → input-BsNqlONJ.js} +2 -2
- package/public/assets/{input-BHYNpXAG.js.map → input-BsNqlONJ.js.map} +1 -1
- package/public/assets/{insights-C7wKt1_l.js → insights-c08aW-7n.js} +2 -2
- package/public/assets/{insights-C7wKt1_l.js.map → insights-c08aW-7n.js.map} +1 -1
- package/public/assets/{integrations-CG9qyHUf.js → integrations-BwpgcqnF.js} +2 -2
- package/public/assets/{integrations-CG9qyHUf.js.map → integrations-BwpgcqnF.js.map} +1 -1
- package/public/assets/{lib-Df4aph5u.js → lib-bXPu6xCL.js} +2 -2
- package/public/assets/{lib-Df4aph5u.js.map → lib-bXPu6xCL.js.map} +1 -1
- package/public/assets/lucide-react-DWYjU-AF.js +9 -0
- package/public/assets/{lucide-react-L2UxFDic.js.map → lucide-react-DWYjU-AF.js.map} +1 -1
- package/public/assets/{maintenance-CjpvnV_U.js → maintenance-CTKP0kWB.js} +2 -2
- package/public/assets/{maintenance-CjpvnV_U.js.map → maintenance-CTKP0kWB.js.map} +1 -1
- package/public/assets/{managed-agents-CdfuBKJA.js → managed-agents-BofCxo8d.js} +2 -2
- package/public/assets/{managed-agents-CdfuBKJA.js.map → managed-agents-BofCxo8d.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-Cf4xCmWt.js → markdown-preview-impl-CQ-6BIb8.js} +2 -2
- package/public/assets/{markdown-preview-impl-Cf4xCmWt.js.map → markdown-preview-impl-CQ-6BIb8.js.map} +1 -1
- package/public/assets/memory-DvfXjMoi.js +2 -0
- package/public/assets/{memory-xKWduhyr.js.map → memory-DvfXjMoi.js.map} +1 -1
- package/public/assets/{messages-BpZYOUtJ.js → messages-C9VsPzZ_.js} +2 -2
- package/public/assets/{messages-BpZYOUtJ.js.map → messages-C9VsPzZ_.js.map} +1 -1
- package/public/assets/orchestrators-MffVueeE.js +2 -0
- package/public/assets/{orchestrators-BrJvGIOc.js.map → orchestrators-MffVueeE.js.map} +1 -1
- package/public/assets/{overview-BUORwePi.js → overview-BSVajefM.js} +2 -2
- package/public/assets/{overview-BUORwePi.js.map → overview-BSVajefM.js.map} +1 -1
- package/public/assets/pairs-B5711-Cs.js +2 -0
- package/public/assets/{pairs-DR0GizDN.js.map → pairs-B5711-Cs.js.map} +1 -1
- package/public/assets/{react-dom-CrP_AtcL.js → react-dom-CPghDLpB.js} +2 -2
- package/public/assets/{react-dom-CrP_AtcL.js.map → react-dom-CPghDLpB.js.map} +1 -1
- package/public/assets/{security-DLwSqTc_.js → security-DkUwK9Vj.js} +2 -2
- package/public/assets/{security-DLwSqTc_.js.map → security-DkUwK9Vj.js.map} +1 -1
- package/public/assets/{select-pEru111r.js → select-DrM2RRiT.js} +2 -2
- package/public/assets/{select-pEru111r.js.map → select-DrM2RRiT.js.map} +1 -1
- package/public/assets/{settings-B-ueGvwV.js → settings-DdKCb-vy.js} +3 -3
- package/public/assets/{settings-B-ueGvwV.js.map → settings-DdKCb-vy.js.map} +1 -1
- package/public/assets/store-iTiheuTD.js +9 -0
- package/public/assets/store-iTiheuTD.js.map +1 -0
- package/public/assets/tasks-e1y1PFfO.js +2 -0
- package/public/assets/{tasks-C2rRQm6E.js.map → tasks-e1y1PFfO.js.map} +1 -1
- package/public/assets/teams-BUcxoGC_.js +2 -0
- package/public/assets/teams-BUcxoGC_.js.map +1 -0
- package/public/assets/{terminal-viewer-impl-BM_3qoeN.js → terminal-viewer-impl-CmZjQOB2.js} +3 -3
- package/public/assets/{terminal-viewer-impl-BM_3qoeN.js.map → terminal-viewer-impl-CmZjQOB2.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-DrRH_QzG.js → use-keyboard-viewport-NKw1O5gC.js} +2 -2
- package/public/assets/{use-keyboard-viewport-DrRH_QzG.js.map → use-keyboard-viewport-NKw1O5gC.js.map} +1 -1
- package/public/assets/{work-queue-CkIYDAZD.js → work-queue-CXBQx43n.js} +2 -2
- package/public/assets/{work-queue-CkIYDAZD.js.map → work-queue-CXBQx43n.js.map} +1 -1
- package/public/assets/{workspaces-CYm5bLg9.js → workspaces-D2EfZh9a.js} +3 -3
- package/public/assets/{workspaces-CYm5bLg9.js.map → workspaces-D2EfZh9a.js.map} +1 -1
- package/public/index.html +21 -21
- package/src/automations.ts +40 -7
- package/src/branch-landed.ts +5 -0
- package/src/bus.ts +6 -1
- package/src/cli/workspace.ts +9 -17
- package/src/db/activity.ts +191 -3
- package/src/db/agents.ts +14 -5
- package/src/db/integrations.ts +1 -0
- package/src/db/issue-associations.ts +128 -0
- package/src/db/issue-lifecycle.ts +187 -0
- package/src/db/issue-refs.ts +254 -0
- package/src/db/mappers.ts +2 -0
- package/src/db/migrations.ts +53 -0
- package/src/db/schema.ts +3 -2
- package/src/issue-tracker/adapter.ts +24 -0
- package/src/issue-tracker/github.ts +136 -0
- package/src/issue-tracker/index.ts +13 -0
- package/src/issue-tracker/registry.ts +26 -0
- package/src/maintenance.ts +22 -169
- package/src/mcp-issue-activity-tools.ts +114 -0
- package/src/mcp-issue-association-tools.ts +130 -0
- package/src/mcp-issue-lifecycle-tools.ts +55 -0
- package/src/mcp-issue-ref-tools.ts +174 -0
- package/src/mcp-team-tools.ts +1 -1
- package/src/mcp.ts +3 -3
- package/src/provider-config-store.ts +30 -8
- package/src/routes/activity.ts +6 -1
- package/src/routes/commands.ts +29 -0
- package/src/routes/index.ts +19 -0
- package/src/routes/issue-activity.ts +78 -0
- package/src/routes/issue-associations.ts +76 -0
- package/src/routes/issue-lifecycle.ts +34 -0
- package/src/routes/issue-refs.ts +137 -0
- package/src/routes/orchestrator-bootstrap.ts +2 -1
- package/src/routes/orchestrator.ts +42 -1
- package/src/routes/tokens.ts +1 -1
- package/src/runtime-tokens.ts +15 -11
- package/src/security.ts +51 -3
- package/src/services/issue-activity.ts +30 -0
- package/src/services/issue-associations.ts +72 -0
- package/src/services/issue-lifecycle.ts +308 -0
- package/src/services/issue-refs.ts +32 -0
- package/src/services/plan-graph.ts +40 -2
- package/src/services/team.ts +4 -0
- package/src/token-db.ts +27 -7
- package/src/workspace-actions.ts +8 -1
- package/src/workspace-auto-merge.ts +228 -0
- package/src/workspace-probe.ts +125 -0
- package/public/assets/PlanGraphPanel-Dby7wq9c.js +0 -2
- package/public/assets/PlanGraphPanel-Dby7wq9c.js.map +0 -1
- package/public/assets/automation-Ddbermbi.js +0 -2
- package/public/assets/automation-Ddbermbi.js.map +0 -1
- package/public/assets/card-CcZ9hLI7.js +0 -2
- package/public/assets/channels-BG0Nib7y.js +0 -2
- package/public/assets/chat-Dx6qWNxb.js +0 -2
- package/public/assets/copy-button-eF3cKp4Q.js +0 -2
- package/public/assets/dist-BLBF7qqr.js +0 -2
- package/public/assets/dist-YpHfnnIA.js +0 -2
- package/public/assets/dist-qd198ib9.js +0 -2
- package/public/assets/index-B3wOeAPO.js +0 -21
- package/public/assets/index-B3wOeAPO.js.map +0 -1
- package/public/assets/index-Dn_GDXbi.css +0 -2
- package/public/assets/lucide-react-L2UxFDic.js +0 -9
- package/public/assets/memory-xKWduhyr.js +0 -2
- package/public/assets/orchestrators-BrJvGIOc.js +0 -2
- package/public/assets/pairs-DR0GizDN.js +0 -2
- package/public/assets/store-B8xP6z6T.js +0 -9
- package/public/assets/store-B8xP6z6T.js.map +0 -1
- package/public/assets/tasks-C2rRQm6E.js +0 -2
- package/public/assets/teams-Bo0sdBTp.js +0 -2
- package/public/assets/teams-Bo0sdBTp.js.map +0 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { errMessage } from "agent-relay-sdk";
|
|
2
|
+
import { getAgent, listPlansWithBoundOwner } from "../db";
|
|
3
|
+
import { listIssuesForEntity, planNodeIssueEntityId } from "../db/issue-associations";
|
|
4
|
+
import {
|
|
5
|
+
getSuccessfulIssueLifecycleAction,
|
|
6
|
+
listSuccessfulCloseActionsForCommit,
|
|
7
|
+
upsertIssueLifecycleAction,
|
|
8
|
+
} from "../db/issue-lifecycle";
|
|
9
|
+
import { getIssueRef, resolveIssueRefByExternalKey, resolveIssueRefFromString, updateIssueRef } from "../db/issue-refs";
|
|
10
|
+
import { associateIssue } from "../db/issue-associations";
|
|
11
|
+
import { resolveIssueTrackerAdapter } from "../issue-tracker";
|
|
12
|
+
import type { IssueTrackerAdapter } from "../issue-tracker";
|
|
13
|
+
import { recordAndEmitIssueActivity } from "./issue-activity";
|
|
14
|
+
import type { IssueAssociationEntityType, IssueProvider, IssueRef, WorkspaceRecord } from "../types";
|
|
15
|
+
|
|
16
|
+
type LifecycleWorkspace = Pick<WorkspaceRecord, "id" | "repoRoot" | "branch" | "baseRef" | "ownerAgentId">;
|
|
17
|
+
type AdapterResolver = (ref: IssueRef) => IssueTrackerAdapter;
|
|
18
|
+
type LogFn = (message: string) => void;
|
|
19
|
+
|
|
20
|
+
export interface LandedIssueLifecycleInput {
|
|
21
|
+
workspace: LifecycleWorkspace;
|
|
22
|
+
mergedSha?: string;
|
|
23
|
+
subject?: string;
|
|
24
|
+
source?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ReopenIssueLifecycleInput {
|
|
28
|
+
issueRefId?: string;
|
|
29
|
+
ref?: string;
|
|
30
|
+
defaultRepo?: string;
|
|
31
|
+
provider?: IssueProvider;
|
|
32
|
+
repo?: string;
|
|
33
|
+
externalKey?: string;
|
|
34
|
+
url?: string;
|
|
35
|
+
title?: string;
|
|
36
|
+
comment?: string;
|
|
37
|
+
agentId?: string;
|
|
38
|
+
entityType?: IssueAssociationEntityType;
|
|
39
|
+
entityId?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface IssueLifecycleOptions {
|
|
43
|
+
adapterResolver?: AdapterResolver;
|
|
44
|
+
log?: LogFn;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type CloseIssueLifecycleResult =
|
|
48
|
+
| { issueRef: IssueRef; action: "closed"; source: "association" | "commit"; landKey: string }
|
|
49
|
+
| { issueRef: IssueRef; action: "skipped"; reason: "already-closed" | "already-processed"; landKey: string }
|
|
50
|
+
| { issueRef: IssueRef; action: "failed"; error: string; landKey: string };
|
|
51
|
+
|
|
52
|
+
export type ReopenIssueLifecycleResult =
|
|
53
|
+
| { issueRef: IssueRef; action: "reopened"; landKey: string }
|
|
54
|
+
| { issueRef: IssueRef; action: "skipped"; reason: "already-open"; landKey: string }
|
|
55
|
+
| { issueRef: IssueRef; action: "failed"; error: string; landKey: string };
|
|
56
|
+
|
|
57
|
+
let adapterResolverForTests: AdapterResolver | undefined;
|
|
58
|
+
|
|
59
|
+
export function __setIssueLifecycleAdapterResolverForTests(resolver: AdapterResolver | undefined): void {
|
|
60
|
+
adapterResolverForTests = resolver;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function adapterFor(ref: IssueRef, opts: IssueLifecycleOptions): IssueTrackerAdapter {
|
|
64
|
+
return (opts.adapterResolver ?? adapterResolverForTests ?? resolveIssueTrackerAdapter)(ref);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function logFailure(opts: IssueLifecycleOptions, message: string): void {
|
|
68
|
+
if (opts.log) opts.log(message);
|
|
69
|
+
else console.warn(message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function defaultIssueRepo(): string | undefined {
|
|
73
|
+
return process.env.AGENT_RELAY_DEFAULT_ISSUE_REPO || process.env.GITHUB_REPOSITORY || undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function cleanText(value: string | undefined): string | undefined {
|
|
77
|
+
const cleaned = value?.trim();
|
|
78
|
+
return cleaned || undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function landKey(input: LandedIssueLifecycleInput): string {
|
|
82
|
+
const base = input.workspace.baseRef ?? "main";
|
|
83
|
+
const sha = cleanText(input.mergedSha);
|
|
84
|
+
if (sha) return `land:${base}:${sha}`;
|
|
85
|
+
return `land:${base}:${input.workspace.id}:${input.workspace.branch ?? "unknown"}:${input.subject ?? ""}`.slice(0, 240);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function manualReopenKey(issueRef: IssueRef): string {
|
|
89
|
+
return `manual-reopen:${issueRef.id}:${Date.now()}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function issueRefMap(refs: IssueRef[]): IssueRef[] {
|
|
93
|
+
const out = new Map<string, IssueRef>();
|
|
94
|
+
for (const ref of refs) out.set(ref.id, ref);
|
|
95
|
+
return [...out.values()];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function associatedIssueRefs(workspace: LifecycleWorkspace): IssueRef[] {
|
|
99
|
+
const refs: IssueRef[] = [];
|
|
100
|
+
const owner = workspace.ownerAgentId ? getAgent(workspace.ownerAgentId) : null;
|
|
101
|
+
if (owner?.teamId) {
|
|
102
|
+
refs.push(...listIssuesForEntity("team", owner.teamId).map((item) => item.issueRef));
|
|
103
|
+
}
|
|
104
|
+
if (workspace.ownerAgentId) {
|
|
105
|
+
for (const plan of listPlansWithBoundOwner(workspace.ownerAgentId)) {
|
|
106
|
+
for (const node of plan.nodes) {
|
|
107
|
+
if (node.ownerRef?.agentId !== workspace.ownerAgentId) continue;
|
|
108
|
+
refs.push(...listIssuesForEntity("plan-node", planNodeIssueEntityId(plan.id, node.id)).map((item) => item.issueRef));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return issueRefMap(refs);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const CLOSING_KEYWORD_RE = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+([^\n\r]+)/gi;
|
|
116
|
+
const ISSUE_REF_AT_START_RE = /^(https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/issues\/[0-9]+(?:[/?#][^\s,)]*)?|[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+#[0-9]+|#[0-9]+)/;
|
|
117
|
+
const REF_SEPARATOR_RE = /^(?:\s*,\s*|\s+(?:and|&)\s+)/i;
|
|
118
|
+
|
|
119
|
+
export function parseClosingIssueRefs(text: string | undefined, fallbackDefaultRepo = defaultIssueRepo()): IssueRef[] {
|
|
120
|
+
if (!text) return [];
|
|
121
|
+
const refs: IssueRef[] = [];
|
|
122
|
+
for (const match of text.matchAll(CLOSING_KEYWORD_RE)) {
|
|
123
|
+
let rest = match[1] ?? "";
|
|
124
|
+
while (rest.trim()) {
|
|
125
|
+
rest = rest.trimStart();
|
|
126
|
+
const refMatch = rest.match(ISSUE_REF_AT_START_RE);
|
|
127
|
+
if (!refMatch) break;
|
|
128
|
+
try {
|
|
129
|
+
refs.push(resolveIssueRefFromString({ ref: refMatch[1]!, defaultRepo: fallbackDefaultRepo }));
|
|
130
|
+
} catch {
|
|
131
|
+
// Invalid or bare-without-default refs are ignored; the land itself must never fail here.
|
|
132
|
+
}
|
|
133
|
+
rest = rest.slice(refMatch[0].length);
|
|
134
|
+
const separator = rest.match(REF_SEPARATOR_RE);
|
|
135
|
+
if (!separator) break;
|
|
136
|
+
rest = rest.slice(separator[0].length);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return issueRefMap(refs);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function resolveIssueRefsForLand(input: LandedIssueLifecycleInput): { source: "association" | "commit"; refs: IssueRef[] } {
|
|
143
|
+
const associated = associatedIssueRefs(input.workspace);
|
|
144
|
+
if (associated.length > 0) return { source: "association", refs: associated };
|
|
145
|
+
return { source: "commit", refs: parseClosingIssueRefs(input.subject) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function closeAuditComment(input: LandedIssueLifecycleInput): string {
|
|
149
|
+
const commit = input.mergedSha ? input.mergedSha.slice(0, 12) : input.workspace.branch ?? input.workspace.id;
|
|
150
|
+
return `Closed by \`${commit}\` on merge to \`main\` - reopen if release surfaces more work.`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function closeIssueRefForLand(
|
|
154
|
+
issueRef: IssueRef,
|
|
155
|
+
source: "association" | "commit",
|
|
156
|
+
input: LandedIssueLifecycleInput,
|
|
157
|
+
opts: IssueLifecycleOptions,
|
|
158
|
+
): Promise<CloseIssueLifecycleResult> {
|
|
159
|
+
const key = landKey(input);
|
|
160
|
+
if (getSuccessfulIssueLifecycleAction(issueRef.id, "close", key)) return { issueRef, action: "skipped", reason: "already-processed", landKey: key };
|
|
161
|
+
const latest = getIssueRef(issueRef.id) ?? issueRef;
|
|
162
|
+
if (latest.status === "closed") return { issueRef: latest, action: "skipped", reason: "already-closed", landKey: key };
|
|
163
|
+
|
|
164
|
+
const comment = closeAuditComment(input);
|
|
165
|
+
try {
|
|
166
|
+
await adapterFor(latest, opts).close(latest, { comment, labels: ["fixed"] });
|
|
167
|
+
const closed = updateIssueRef(latest.id, { status: "closed", statusSyncedAt: Date.now() }) ?? latest;
|
|
168
|
+
upsertIssueLifecycleAction({
|
|
169
|
+
issueRefId: latest.id,
|
|
170
|
+
action: "close",
|
|
171
|
+
landKey: key,
|
|
172
|
+
workspaceId: input.workspace.id,
|
|
173
|
+
branch: input.workspace.branch,
|
|
174
|
+
baseRef: input.workspace.baseRef ?? "main",
|
|
175
|
+
commitSha: input.mergedSha,
|
|
176
|
+
commitSubject: input.subject,
|
|
177
|
+
agentId: input.workspace.ownerAgentId,
|
|
178
|
+
source,
|
|
179
|
+
comment,
|
|
180
|
+
status: "succeeded",
|
|
181
|
+
});
|
|
182
|
+
if (input.workspace.ownerAgentId) {
|
|
183
|
+
recordAndEmitIssueActivity({
|
|
184
|
+
agentId: input.workspace.ownerAgentId,
|
|
185
|
+
action: "done",
|
|
186
|
+
issueRefId: latest.id,
|
|
187
|
+
clientId: `issue-close:${latest.id}:${key}`,
|
|
188
|
+
title: "Issue closed on land",
|
|
189
|
+
body: comment,
|
|
190
|
+
metadata: { workspaceId: input.workspace.id, branch: input.workspace.branch, commitSha: input.mergedSha, source },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return { issueRef: closed, action: "closed", source, landKey: key };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const message = errMessage(error);
|
|
196
|
+
upsertIssueLifecycleAction({
|
|
197
|
+
issueRefId: latest.id,
|
|
198
|
+
action: "close",
|
|
199
|
+
landKey: key,
|
|
200
|
+
workspaceId: input.workspace.id,
|
|
201
|
+
branch: input.workspace.branch,
|
|
202
|
+
baseRef: input.workspace.baseRef ?? "main",
|
|
203
|
+
commitSha: input.mergedSha,
|
|
204
|
+
commitSubject: input.subject,
|
|
205
|
+
agentId: input.workspace.ownerAgentId,
|
|
206
|
+
source,
|
|
207
|
+
comment,
|
|
208
|
+
status: "failed",
|
|
209
|
+
error: message,
|
|
210
|
+
});
|
|
211
|
+
logFailure(opts, `issue lifecycle close failed for ${latest.id} on ${key}: ${message}`);
|
|
212
|
+
return { issueRef: latest, action: "failed", error: message, landKey: key };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function revertedCommitLookup(subject: string | undefined): { commitSha?: string; commitSubject?: string } {
|
|
217
|
+
const text = cleanText(subject);
|
|
218
|
+
if (!text) return {};
|
|
219
|
+
const sha = text.match(/\bThis reverts commit ([0-9a-f]{7,64})\b/i)?.[1];
|
|
220
|
+
if (sha) return { commitSha: sha };
|
|
221
|
+
const revertedSubject = text.match(/^Revert "(.+)"$/)?.[1];
|
|
222
|
+
return revertedSubject ? { commitSubject: revertedSubject } : {};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function reopenIssuesForRevert(input: LandedIssueLifecycleInput, opts: IssueLifecycleOptions): Promise<ReopenIssueLifecycleResult[]> {
|
|
226
|
+
const lookup = revertedCommitLookup(input.subject);
|
|
227
|
+
const closedActions = listSuccessfulCloseActionsForCommit(lookup);
|
|
228
|
+
const results: ReopenIssueLifecycleResult[] = [];
|
|
229
|
+
for (const action of closedActions) {
|
|
230
|
+
results.push(await reopenIssueLifecycle({
|
|
231
|
+
issueRefId: action.issueRefId,
|
|
232
|
+
comment: `Reopened by revert \`${input.mergedSha?.slice(0, 12) ?? input.workspace.branch ?? input.workspace.id}\` on merge to \`main\`.`,
|
|
233
|
+
agentId: input.workspace.ownerAgentId,
|
|
234
|
+
}, opts));
|
|
235
|
+
}
|
|
236
|
+
return results;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function handleLandedIssueLifecycle(input: LandedIssueLifecycleInput, opts: IssueLifecycleOptions = {}): Promise<{
|
|
240
|
+
source: "association" | "commit";
|
|
241
|
+
closed: CloseIssueLifecycleResult[];
|
|
242
|
+
reopened: ReopenIssueLifecycleResult[];
|
|
243
|
+
skipped: string[];
|
|
244
|
+
}> {
|
|
245
|
+
if ((input.workspace.baseRef ?? "main") !== "main") return { source: "commit", closed: [], reopened: [], skipped: ["non-default-branch"] };
|
|
246
|
+
const resolved = resolveIssueRefsForLand(input);
|
|
247
|
+
const closed: CloseIssueLifecycleResult[] = [];
|
|
248
|
+
for (const ref of resolved.refs) {
|
|
249
|
+
closed.push(await closeIssueRefForLand(ref, resolved.source, input, opts));
|
|
250
|
+
}
|
|
251
|
+
const reopened = await reopenIssuesForRevert(input, opts);
|
|
252
|
+
return { source: resolved.source, closed, reopened, skipped: [] };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function resolveReopenRef(input: ReopenIssueLifecycleInput): IssueRef {
|
|
256
|
+
if (input.issueRefId) {
|
|
257
|
+
const issueRef = getIssueRef(input.issueRefId);
|
|
258
|
+
if (!issueRef) throw new Error(`issue ref ${input.issueRefId} not found`);
|
|
259
|
+
return issueRef;
|
|
260
|
+
}
|
|
261
|
+
if (input.ref) return resolveIssueRefFromString({ ref: input.ref, defaultRepo: input.defaultRepo ?? input.repo, provider: input.provider, title: input.title });
|
|
262
|
+
if (input.provider && input.repo && input.externalKey) {
|
|
263
|
+
return resolveIssueRefByExternalKey({
|
|
264
|
+
provider: input.provider,
|
|
265
|
+
repo: input.repo,
|
|
266
|
+
externalKey: input.externalKey,
|
|
267
|
+
url: input.url,
|
|
268
|
+
title: input.title,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
throw new Error("issueRefId, ref, or provider/repo/externalKey required");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function reopenIssueLifecycle(input: ReopenIssueLifecycleInput, opts: IssueLifecycleOptions = {}): Promise<ReopenIssueLifecycleResult> {
|
|
275
|
+
const issueRef = resolveReopenRef(input);
|
|
276
|
+
const key = manualReopenKey(issueRef);
|
|
277
|
+
if (issueRef.status === "open") return { issueRef, action: "skipped", reason: "already-open", landKey: key };
|
|
278
|
+
const comment = input.comment ?? "Reopened by Agent Relay issue lifecycle.";
|
|
279
|
+
try {
|
|
280
|
+
await adapterFor(issueRef, opts).reopen(issueRef, { comment });
|
|
281
|
+
const opened = updateIssueRef(issueRef.id, { status: "open", statusSyncedAt: Date.now() }) ?? issueRef;
|
|
282
|
+
upsertIssueLifecycleAction({
|
|
283
|
+
issueRefId: issueRef.id,
|
|
284
|
+
action: "reopen",
|
|
285
|
+
landKey: key,
|
|
286
|
+
agentId: input.agentId,
|
|
287
|
+
comment,
|
|
288
|
+
status: "succeeded",
|
|
289
|
+
source: "explicit",
|
|
290
|
+
});
|
|
291
|
+
if (input.entityType && input.entityId) associateIssue(issueRef.id, input.entityType, input.entityId);
|
|
292
|
+
return { issueRef: opened, action: "reopened", landKey: key };
|
|
293
|
+
} catch (error) {
|
|
294
|
+
const message = errMessage(error);
|
|
295
|
+
upsertIssueLifecycleAction({
|
|
296
|
+
issueRefId: issueRef.id,
|
|
297
|
+
action: "reopen",
|
|
298
|
+
landKey: key,
|
|
299
|
+
agentId: input.agentId,
|
|
300
|
+
comment,
|
|
301
|
+
status: "failed",
|
|
302
|
+
error: message,
|
|
303
|
+
source: "explicit",
|
|
304
|
+
});
|
|
305
|
+
logFailure(opts, `issue lifecycle reopen failed for ${issueRef.id}: ${message}`);
|
|
306
|
+
return { issueRef, action: "failed", error: message, landKey: key };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deleteIssueRef,
|
|
3
|
+
getIssueRef,
|
|
4
|
+
listIssueRefs,
|
|
5
|
+
resolveIssueRefByExternalKey,
|
|
6
|
+
resolveIssueRefFromString,
|
|
7
|
+
updateIssueRef,
|
|
8
|
+
upsertIssueRef,
|
|
9
|
+
type ListIssueRefsOptions,
|
|
10
|
+
type ResolveIssueRefByExternalKeyInput,
|
|
11
|
+
type ResolveIssueRefFromStringInput,
|
|
12
|
+
type UpdateIssueRefInput,
|
|
13
|
+
type UpsertIssueRefInput,
|
|
14
|
+
} from "../db/issue-refs";
|
|
15
|
+
|
|
16
|
+
export { getIssueRef, listIssueRefs, type ListIssueRefsOptions };
|
|
17
|
+
|
|
18
|
+
export function upsertIssueRefMapping(input: UpsertIssueRefInput) {
|
|
19
|
+
return upsertIssueRef(input);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveIssueRefMapping(input: ResolveIssueRefByExternalKeyInput | ResolveIssueRefFromStringInput) {
|
|
23
|
+
return "ref" in input ? resolveIssueRefFromString(input) : resolveIssueRefByExternalKey(input);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function patchIssueRefMapping(id: string, input: UpdateIssueRefInput) {
|
|
27
|
+
return updateIssueRef(id, input);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function deleteIssueRefMapping(id: string) {
|
|
31
|
+
return deleteIssueRef(id);
|
|
32
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { isRecord } from "agent-relay-sdk";
|
|
2
2
|
import { createPlanRow, deletePlan, getAgent, getEntry, getPlan, getPlanByTeam, getPlanByThread, listPlansWithBoundOwner, planThreadExists, replacePlan, ValidationError } from "../db";
|
|
3
|
+
import { associateIssue, planNodeIssueEntityId, removeAssociation } from "../db/issue-associations";
|
|
4
|
+
import { resolveIssueRefFromString } from "../db/issue-refs";
|
|
3
5
|
import { emitRelayEvent } from "../events";
|
|
4
6
|
import { cleanString, cleanStringArray, optionalEnum } from "../validation";
|
|
5
7
|
import type { AuthContext } from "./auth-context";
|
|
@@ -72,7 +74,37 @@ function cleanOwnerRef(value: unknown): PlanOwnerRef | undefined {
|
|
|
72
74
|
|
|
73
75
|
function cleanRefs(value: unknown): PlanNode["refs"] | undefined {
|
|
74
76
|
if (value === undefined || value === null) return undefined;
|
|
75
|
-
|
|
77
|
+
const ref = cleanRecordRef(value, "refs");
|
|
78
|
+
if (!ref) return undefined;
|
|
79
|
+
const out: PlanNode["refs"] = {};
|
|
80
|
+
const issueId = cleanString(ref.issueId, "refs.issueId", { max: 1000 });
|
|
81
|
+
if (issueId) {
|
|
82
|
+
out.issueId = resolveIssueRefFromString({ ref: issueId, defaultRepo: defaultIssueRepo() }).id;
|
|
83
|
+
}
|
|
84
|
+
const unsupported = ["agentId", "taskId", "landingRef"].filter((key) => cleanString(ref[key], `refs.${key}`, { max: 240 }));
|
|
85
|
+
if (unsupported.length > 0) throw new ValidationError("agentId/taskId/landingRef refs are populated by live binding in slice c");
|
|
86
|
+
return Object.keys(out).length ? out : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function defaultIssueRepo(): string | undefined {
|
|
90
|
+
return process.env.AGENT_RELAY_DEFAULT_ISSUE_REPO || process.env.GITHUB_REPOSITORY || undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function syncPlanNodeIssueAssociation(planId: string, node: PlanNode): void {
|
|
94
|
+
if (!node.refs?.issueId) return;
|
|
95
|
+
associateIssue(node.refs.issueId, "plan-node", planNodeIssueEntityId(planId, node.id));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function removePlanNodeIssueAssociation(planId: string, node: PlanNode): void {
|
|
99
|
+
if (!node.refs?.issueId) return;
|
|
100
|
+
removeAssociation(node.refs.issueId, "plan-node", planNodeIssueEntityId(planId, node.id));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function syncChangedPlanNodeIssueAssociation(planId: string, before: PlanNode | undefined, after: PlanNode): void {
|
|
104
|
+
if (before?.refs?.issueId && before.refs.issueId !== after.refs?.issueId) {
|
|
105
|
+
removeAssociation(before.refs.issueId, "plan-node", planNodeIssueEntityId(planId, before.id));
|
|
106
|
+
}
|
|
107
|
+
if (after.refs?.issueId) syncPlanNodeIssueAssociation(planId, after);
|
|
76
108
|
}
|
|
77
109
|
|
|
78
110
|
function cleanBlackboardRefs(value: unknown, planTeamId?: string): string[] | undefined {
|
|
@@ -229,6 +261,7 @@ export function createPlan(input: PlanCreateInput, ctx: AuthContext): Plan {
|
|
|
229
261
|
edges,
|
|
230
262
|
createdBy: actor,
|
|
231
263
|
});
|
|
264
|
+
for (const node of plan.nodes) syncPlanNodeIssueAssociation(plan.id, node);
|
|
232
265
|
emitPlan("plan.created", plan, actor);
|
|
233
266
|
return plan;
|
|
234
267
|
}
|
|
@@ -238,6 +271,7 @@ export function addPlanNode(planId: string, nodeInput: PlanNodeInput, ctx: AuthC
|
|
|
238
271
|
const node = cleanNode(nodeInput, plan.teamId);
|
|
239
272
|
if (plan.nodes.some((item) => item.id === node.id)) throw new ValidationError(`node ${node.id} already exists`);
|
|
240
273
|
const updated = replacePlan({ ...plan, nodes: [...plan.nodes, node] }, actorId(ctx));
|
|
274
|
+
syncPlanNodeIssueAssociation(updated.id, node);
|
|
241
275
|
emitPlan("plan.changed", updated, actorId(ctx));
|
|
242
276
|
return updated;
|
|
243
277
|
}
|
|
@@ -260,6 +294,7 @@ export function updatePlanNode(planId: string, nodeId: string, patch: PlanNodePa
|
|
|
260
294
|
};
|
|
261
295
|
const next = applyBoundStatus(base);
|
|
262
296
|
const updated = replacePlan({ ...plan, nodes: plan.nodes.map((node) => node.id === nodeId ? next : node) }, actorId(ctx));
|
|
297
|
+
syncChangedPlanNodeIssueAssociation(updated.id, existing, next);
|
|
263
298
|
emitPlan("plan.changed", updated, actorId(ctx));
|
|
264
299
|
return updated;
|
|
265
300
|
}
|
|
@@ -280,12 +315,14 @@ export function addPlanEdge(planId: string, edgeInput: unknown, ctx: AuthContext
|
|
|
280
315
|
|
|
281
316
|
export function removePlanNode(planId: string, nodeId: string, ctx: AuthContext): Plan {
|
|
282
317
|
const plan = existingPlan(planId);
|
|
283
|
-
|
|
318
|
+
const removed = plan.nodes.find((node) => node.id === nodeId);
|
|
319
|
+
if (!removed) throw new ValidationError(`node ${nodeId} not found`);
|
|
284
320
|
const updated = replacePlan({
|
|
285
321
|
...plan,
|
|
286
322
|
nodes: plan.nodes.filter((node) => node.id !== nodeId),
|
|
287
323
|
edges: plan.edges.filter((edge) => edge.from !== nodeId && edge.to !== nodeId),
|
|
288
324
|
}, actorId(ctx));
|
|
325
|
+
removePlanNodeIssueAssociation(plan.id, removed);
|
|
289
326
|
emitPlan("plan.changed", updated, actorId(ctx));
|
|
290
327
|
return updated;
|
|
291
328
|
}
|
|
@@ -304,6 +341,7 @@ export function deletePlanGraph(planId: string, ctx: AuthContext): { id: string;
|
|
|
304
341
|
const plan = existingPlan(planId);
|
|
305
342
|
const actor = actorId(ctx);
|
|
306
343
|
deletePlan(plan.id, actor);
|
|
344
|
+
for (const node of plan.nodes) removePlanNodeIssueAssociation(plan.id, node);
|
|
307
345
|
emitPlan("plan.deleted", plan, actor);
|
|
308
346
|
return { id: plan.id, deleted: true };
|
|
309
347
|
}
|
package/src/services/team.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dissolveTeam, ensureTeam, getAgent, getPlanByTeam, getTeam, getTeamRoster, setTeamBrief } from "../db";
|
|
2
2
|
import { emitRelayEvent } from "../events";
|
|
3
|
+
import { listIssueRefsForEntity } from "./issue-associations";
|
|
3
4
|
import type { AuthContext } from "./auth-context";
|
|
4
5
|
import type { Team } from "../types";
|
|
5
6
|
|
|
@@ -12,6 +13,7 @@ export interface TeamPayload {
|
|
|
12
13
|
brief: string | null;
|
|
13
14
|
definitionOfDone: string | null;
|
|
14
15
|
plan: ReturnType<typeof getPlanByTeam>;
|
|
16
|
+
issues: ReturnType<typeof listIssueRefsForEntity>;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface TeamBriefInput {
|
|
@@ -41,6 +43,7 @@ function payloadForAgent(agentId: string, channel?: string): TeamPayload {
|
|
|
41
43
|
brief: team.brief ?? null,
|
|
42
44
|
definitionOfDone: team.definitionOfDone ?? null,
|
|
43
45
|
plan: getPlanByTeam(team.id, channel) ?? null,
|
|
46
|
+
issues: listIssueRefsForEntity("team", team.id),
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -85,5 +88,6 @@ export function dissolveCallerTeam(ctx: AuthContext, input: TeamDissolveServiceI
|
|
|
85
88
|
brief: team.brief ?? null,
|
|
86
89
|
definitionOfDone: team.definitionOfDone ?? null,
|
|
87
90
|
plan: getPlanByTeam(team.id) ?? null,
|
|
91
|
+
issues: listIssueRefsForEntity("team", team.id),
|
|
88
92
|
};
|
|
89
93
|
}
|
package/src/token-db.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { getDb } from "./db";
|
|
3
|
-
import { signComponentToken } from "./security";
|
|
3
|
+
import { signComponentToken, type TokenRevokeReason } from "./security";
|
|
4
4
|
import type { ComponentToken, CreateTokenProfileInput, TokenConstraints, TokenProfile, TokenRecord, UpdateTokenProfileInput } from "./types";
|
|
5
5
|
|
|
6
6
|
interface TokenRow {
|
|
@@ -13,6 +13,7 @@ interface TokenRow {
|
|
|
13
13
|
issued_at: number;
|
|
14
14
|
expires_at: number | null;
|
|
15
15
|
revoked_at: number | null;
|
|
16
|
+
revoked_reason: string | null;
|
|
16
17
|
created_by: string | null;
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -77,7 +78,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
77
78
|
name: "Provider Agent",
|
|
78
79
|
description: "Coding-agent runtime access for messages, commands, tasks, and scoped memory reads.",
|
|
79
80
|
role: "provider",
|
|
80
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "project:read", "project:write", "notifications:read", "notifications:write", "insights:write"],
|
|
81
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write"],
|
|
81
82
|
ttlSeconds: 24 * 60 * 60,
|
|
82
83
|
builtIn: true,
|
|
83
84
|
createdBy: "system",
|
|
@@ -87,7 +88,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
87
88
|
name: "Provider Child Agent",
|
|
88
89
|
description: "Delegated child-agent runtime access, constrained to its parent and spawn request.",
|
|
89
90
|
role: "provider",
|
|
90
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "notifications:read", "notifications:write", "insights:write"],
|
|
91
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write"],
|
|
91
92
|
constraints: { canDelegate: false },
|
|
92
93
|
ttlSeconds: 2 * 60 * 60,
|
|
93
94
|
builtIn: true,
|
|
@@ -98,7 +99,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
98
99
|
name: "Provider Interactive Agent",
|
|
99
100
|
description: "User-launched provider runtime access constrained to its own agent and cwd for long interactive sessions.",
|
|
100
101
|
role: "provider",
|
|
101
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "notifications:read", "notifications:write", "insights:write"],
|
|
102
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write"],
|
|
102
103
|
constraints: { terminalAttach: false, logsRead: false, canDelegate: false },
|
|
103
104
|
ttlSeconds: 30 * 24 * 60 * 60,
|
|
104
105
|
builtIn: true,
|
|
@@ -180,8 +181,26 @@ export function getToken(jti: string): TokenRecord | null {
|
|
|
180
181
|
return row ? rowToToken(row) : null;
|
|
181
182
|
}
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
// Revoke a token, optionally tagging WHY (#484). The reason gates self-heal:
|
|
185
|
+
// `verifyComponentTokenAllowExpired` lets the orchestrator re-mint a token revoked
|
|
186
|
+
// as "stale" (the restart-strand case) but never one revoked as a hard kill
|
|
187
|
+
// (admin/rotation/agent-removed/pruned). A NULL reason (legacy/unspecified) is a
|
|
188
|
+
// hard kill. Only flips an un-revoked token — already-revoked rows are left as-is
|
|
189
|
+
// (so the 404-on-double-revoke contract in routes/tokens.ts holds).
|
|
190
|
+
export function revokeToken(jti: string, reason?: TokenRevokeReason): boolean {
|
|
191
|
+
return getDb()
|
|
192
|
+
.query("UPDATE tokens SET revoked_at = ?, revoked_reason = ? WHERE jti = ? AND revoked_at IS NULL")
|
|
193
|
+
.run(Math.floor(Date.now() / 1000), reason ?? null, jti).changes > 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Downgrade a "stale"-revoked token to a hard "rotation" revocation so it can no
|
|
197
|
+
// longer be re-minted (#484). Called after a successful orchestrator re-mint: the
|
|
198
|
+
// fresh token now carries the identity, and the consumed stale token must not be
|
|
199
|
+
// re-mintable a second time (bounds replay of a captured stale token).
|
|
200
|
+
export function hardenStaleRevocation(jti: string): void {
|
|
201
|
+
getDb()
|
|
202
|
+
.query("UPDATE tokens SET revoked_reason = 'rotation' WHERE jti = ? AND revoked_reason = 'stale'")
|
|
203
|
+
.run(jti);
|
|
185
204
|
}
|
|
186
205
|
|
|
187
206
|
export function renewToken(input: {
|
|
@@ -201,7 +220,7 @@ export function renewToken(input: {
|
|
|
201
220
|
profileId: previous.profileId,
|
|
202
221
|
createdBy: input.createdBy ?? `renew:${previous.jti}`,
|
|
203
222
|
});
|
|
204
|
-
revokeToken(previous.jti);
|
|
223
|
+
revokeToken(previous.jti, "rotation");
|
|
205
224
|
return { ...renewed, previous };
|
|
206
225
|
}
|
|
207
226
|
|
|
@@ -342,6 +361,7 @@ function rowToToken(row: TokenRow): TokenRecord {
|
|
|
342
361
|
issuedAt: row.issued_at,
|
|
343
362
|
expiresAt: row.expires_at ?? undefined,
|
|
344
363
|
revokedAt: row.revoked_at ?? undefined,
|
|
364
|
+
revokedReason: row.revoked_reason ?? undefined,
|
|
345
365
|
createdBy: row.created_by ?? undefined,
|
|
346
366
|
};
|
|
347
367
|
}
|
package/src/workspace-actions.ts
CHANGED
|
@@ -24,7 +24,8 @@ import { isOwnerAlive, requestWorkspaceMerge } from "./workspace-merge";
|
|
|
24
24
|
// tuple as the HTTP route without a second import hop (one source: workspace-merge, #304).
|
|
25
25
|
export { LAND_STRATEGIES, DEFAULT_MERGE_STRATEGY } from "./workspace-merge";
|
|
26
26
|
import { claimMetadataPatch, workspaceActiveClaim } from "./workspace-claim";
|
|
27
|
-
import { TERMINAL_WORKSPACE_STATUSES } from "./workspace-phase";
|
|
27
|
+
import { READY_TO_LAND_STATUSES, TERMINAL_WORKSPACE_STATUSES } from "./workspace-phase";
|
|
28
|
+
import { scheduleRepoMerge } from "./workspace-auto-merge";
|
|
28
29
|
import type { Command, WorkspaceAutoMergePolicy, WorkspaceMergeStrategy, WorkspaceRecord, WorkspaceStatus } from "./types";
|
|
29
30
|
|
|
30
31
|
// Single source of truth for the action verb set. The route's `optionalEnum` and
|
|
@@ -239,6 +240,12 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
|
|
|
239
240
|
});
|
|
240
241
|
if (!updated) return { ok: false, httpStatus: 404, error: "workspace not found" };
|
|
241
242
|
|
|
243
|
+
// #237: a ready/request-review hand-off kicks the per-repo auto-merge inline right
|
|
244
|
+
// then, instead of waiting up to the backstop interval — this is what makes "mark
|
|
245
|
+
// ready → land" take seconds, not minutes (the #235 root-cause fix). Fire-and-forget
|
|
246
|
+
// + lease-serialized; a non-landable row is a harmless no-op (autoMergeForRepo re-checks).
|
|
247
|
+
if (READY_TO_LAND_STATUSES.has(nextStatus)) scheduleRepoMerge(updated.repoRoot);
|
|
248
|
+
|
|
242
249
|
let command: Command | undefined;
|
|
243
250
|
if (requiresCommand) {
|
|
244
251
|
// Only `cleanup` reaches here — `merge` returned early via the shared helper.
|