@brainervirus/workit-core 1.0.12 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/authority.ts +31 -0
- package/src/core/auto-approval.ts +326 -0
- package/src/core/branch.ts +44 -0
- package/src/core/external-action-effects.ts +93 -1
- package/src/core/external-action.ts +12 -1
- package/src/core/pr-create.ts +50 -0
- package/src/core/route-intent.ts +7 -1
- package/src/core/task-contract.ts +14 -0
- package/src/core/task-engine.ts +31 -3
- package/src/core/workspaces.ts +7 -1
- package/src/core.ts +17 -0
package/package.json
CHANGED
package/src/core/authority.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { operationAutoClass, standingApprovalLive } from "./auto-approval";
|
|
4
5
|
import {
|
|
5
6
|
canonicalJson,
|
|
6
7
|
decisionDigest,
|
|
@@ -579,6 +580,36 @@ const validateAction = (
|
|
|
579
580
|
!provenanceMatches(entry.provenance, authority.provenance)
|
|
580
581
|
)
|
|
581
582
|
return failure("permission_denied", "action approval lacks native receipt assurance");
|
|
583
|
+
// Standing auto-approval receipts carry no live question. They authorize
|
|
584
|
+
// only while the referenced workspace rule still covers the operation —
|
|
585
|
+
// verified here, at reserve time, against current config. Question-backed
|
|
586
|
+
// approvals (host-kind receipts) skip this; anything else was already
|
|
587
|
+
// denied above.
|
|
588
|
+
const receipts = entry.provenance.receipts as unknown[];
|
|
589
|
+
const standing = receipts.filter(
|
|
590
|
+
(receipt) =>
|
|
591
|
+
typeof receipt === "object" &&
|
|
592
|
+
receipt !== null &&
|
|
593
|
+
(receipt as { kind?: unknown }).kind === "standing",
|
|
594
|
+
);
|
|
595
|
+
const questionBacked = receipts.some(
|
|
596
|
+
(receipt) =>
|
|
597
|
+
typeof receipt === "object" &&
|
|
598
|
+
receipt !== null &&
|
|
599
|
+
(receipt as { kind?: unknown }).kind === "host",
|
|
600
|
+
);
|
|
601
|
+
if (!questionBacked && standing.length > 0) {
|
|
602
|
+
let operation: unknown;
|
|
603
|
+
try {
|
|
604
|
+
operation = (JSON.parse(decision.binding.approvedContent) as { operation?: unknown })
|
|
605
|
+
.operation;
|
|
606
|
+
} catch {
|
|
607
|
+
operation = null;
|
|
608
|
+
}
|
|
609
|
+
const cls = operationAutoClass(operation);
|
|
610
|
+
if (!cls || !standing.some((receipt) => standingApprovalLive(store.root, receipt, cls)))
|
|
611
|
+
return failure("permission_denied", "standing auto-approval is not live for this operation");
|
|
612
|
+
}
|
|
582
613
|
if (decision.revoked) return failure("permission_denied", "decision is revoked");
|
|
583
614
|
if (decision.digest !== decisionDigest(decision))
|
|
584
615
|
return failure("permission_denied", "decision binding is invalid");
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolveWorkspaceFrom, type WorkspaceConfig } from "./workspaces";
|
|
4
|
+
import { configDir } from "./config";
|
|
5
|
+
import { verifyPushIdentity } from "./branch";
|
|
6
|
+
import {
|
|
7
|
+
failure,
|
|
8
|
+
success,
|
|
9
|
+
type Caller,
|
|
10
|
+
type Id,
|
|
11
|
+
type Provenance,
|
|
12
|
+
type Result,
|
|
13
|
+
type Revision,
|
|
14
|
+
type TaskRecord,
|
|
15
|
+
} from "./task-contract";
|
|
16
|
+
import type { TaskStore } from "./task-store";
|
|
17
|
+
import type { WorkitCore } from "./task-engine";
|
|
18
|
+
import type { Decision } from "./task-contract";
|
|
19
|
+
|
|
20
|
+
/** Action classes auto-approval can cover. Unknown classes never match. */
|
|
21
|
+
export const AUTO_CLASSES = ["branch", "commit", "push", "pr", "merge"] as const;
|
|
22
|
+
export type AutoClass = (typeof AUTO_CLASSES)[number];
|
|
23
|
+
|
|
24
|
+
export type AutoApproval =
|
|
25
|
+
| { status: "off" }
|
|
26
|
+
| {
|
|
27
|
+
status: "on";
|
|
28
|
+
workspace: string;
|
|
29
|
+
classes: AutoClass[];
|
|
30
|
+
account: string | null;
|
|
31
|
+
configDigest: string | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type StandingReceipt = {
|
|
35
|
+
kind: "standing";
|
|
36
|
+
workspace: string;
|
|
37
|
+
class: string;
|
|
38
|
+
configDigest: string | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const OPERATION_CLASS: Record<string, AutoClass> = {
|
|
42
|
+
"git.branch_setup": "branch",
|
|
43
|
+
"git.commit": "commit",
|
|
44
|
+
"git.push": "push",
|
|
45
|
+
"hosting.pull_request": "pr",
|
|
46
|
+
"hosting.merge": "merge",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Map an external-action operation to its auto-approval class, if any. */
|
|
50
|
+
export const operationAutoClass = (operation: unknown): AutoClass | null =>
|
|
51
|
+
typeof operation === "string" ? (OPERATION_CLASS[operation] ?? null) : null;
|
|
52
|
+
|
|
53
|
+
const digestOf = (configDirPath: string): string | null => {
|
|
54
|
+
try {
|
|
55
|
+
const raw = readFileSync(`${configDirPath}/workspaces.json`, "utf8");
|
|
56
|
+
return createHash("sha256").update(raw).digest("hex");
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const normalizeClasses = (value: unknown): AutoClass[] => {
|
|
63
|
+
if (value === true) return [...AUTO_CLASSES];
|
|
64
|
+
if (!Array.isArray(value)) return [];
|
|
65
|
+
const seen = new Set<AutoClass>();
|
|
66
|
+
for (const item of value) {
|
|
67
|
+
if (typeof item === "string" && (AUTO_CLASSES as readonly string[]).includes(item))
|
|
68
|
+
seen.add(item as AutoClass);
|
|
69
|
+
}
|
|
70
|
+
return [...seen];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Resolve the auto-approval rule for a checkout root (live read, never cached). */
|
|
74
|
+
export const resolveAutoApproval = (root: string, dir: string = configDir()): AutoApproval => {
|
|
75
|
+
let match: WorkspaceConfig | null;
|
|
76
|
+
try {
|
|
77
|
+
match = resolveWorkspaceFrom(root, dir);
|
|
78
|
+
} catch {
|
|
79
|
+
return { status: "off" };
|
|
80
|
+
}
|
|
81
|
+
if (!match) return { status: "off" };
|
|
82
|
+
const classes = normalizeClasses(match.autoApprove);
|
|
83
|
+
if (classes.length === 0) return { status: "off" };
|
|
84
|
+
return {
|
|
85
|
+
status: "on",
|
|
86
|
+
workspace: match.name,
|
|
87
|
+
classes,
|
|
88
|
+
account: typeof match.vcs?.account === "string" && match.vcs.account ? match.vcs.account : null,
|
|
89
|
+
configDigest: digestOf(dir),
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** True when the workspace rule covers the class right now. */
|
|
94
|
+
export const autoApproves = (root: string, cls: unknown, dir?: string): boolean => {
|
|
95
|
+
if (typeof cls !== "string" || !(AUTO_CLASSES as readonly string[]).includes(cls)) return false;
|
|
96
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
97
|
+
return resolved.status === "on" && resolved.classes.includes(cls as AutoClass);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const isStandingReceipt = (value: unknown): value is StandingReceipt =>
|
|
101
|
+
typeof value === "object" && value !== null && (value as { kind?: unknown }).kind === "standing";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A standing receipt authorizes only while the referenced workspace rule
|
|
105
|
+
* still covers the class. Removing the flag (or narrowing the list) fails
|
|
106
|
+
* closed on the very next reserve — no revocation step needed.
|
|
107
|
+
*/
|
|
108
|
+
export const standingApprovalLive = (
|
|
109
|
+
root: string,
|
|
110
|
+
receipt: unknown,
|
|
111
|
+
cls: unknown,
|
|
112
|
+
dir?: string,
|
|
113
|
+
): boolean => {
|
|
114
|
+
if (!isStandingReceipt(receipt)) return false;
|
|
115
|
+
if (typeof cls !== "string" || !(AUTO_CLASSES as readonly string[]).includes(cls)) return false;
|
|
116
|
+
if (receipt.class !== cls) return false;
|
|
117
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
118
|
+
if (resolved.status !== "on") return false;
|
|
119
|
+
if (resolved.workspace !== receipt.workspace) return false;
|
|
120
|
+
return resolved.classes.includes(cls as AutoClass);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Build the receipt an auto-recorded decision carries (current digest). */
|
|
124
|
+
export const standingReceiptFor = (
|
|
125
|
+
root: string,
|
|
126
|
+
workspace: string,
|
|
127
|
+
cls: AutoClass,
|
|
128
|
+
dir?: string,
|
|
129
|
+
): StandingReceipt => {
|
|
130
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
131
|
+
return {
|
|
132
|
+
kind: "standing",
|
|
133
|
+
workspace,
|
|
134
|
+
class: cls,
|
|
135
|
+
configDigest:
|
|
136
|
+
resolved.status === "on" && resolved.workspace === workspace ? resolved.configDigest : null,
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Verify a standing approval record request: active task, lead session
|
|
142
|
+
* match (helpers never auto-approve), operation class equals the claimed
|
|
143
|
+
* class, rule live right now, push identity enforced for push.
|
|
144
|
+
* Returns the host-observed provenance the entry carries on success.
|
|
145
|
+
*/
|
|
146
|
+
export const verifyStandingApproval = (
|
|
147
|
+
root: string,
|
|
148
|
+
task: TaskRecord,
|
|
149
|
+
caller: Caller,
|
|
150
|
+
binding: {
|
|
151
|
+
standing?: { workspace?: unknown; class?: unknown };
|
|
152
|
+
approvedContent?: unknown;
|
|
153
|
+
},
|
|
154
|
+
): Result<Provenance> => {
|
|
155
|
+
if (task.status !== "active")
|
|
156
|
+
return failure("invalid_transition", "only active tasks can authorize actions");
|
|
157
|
+
const session = task.intent?.provenance?.session;
|
|
158
|
+
if (
|
|
159
|
+
!session ||
|
|
160
|
+
session.kind !== "host" ||
|
|
161
|
+
session.host !== caller.host ||
|
|
162
|
+
session.handle !== caller.actor
|
|
163
|
+
)
|
|
164
|
+
return failure("permission_denied", "standing approval requires the lead session");
|
|
165
|
+
const standing = binding.standing;
|
|
166
|
+
if (!standing || typeof standing.workspace !== "string" || typeof standing.class !== "string")
|
|
167
|
+
return failure("invalid_input", "standing approval needs a workspace and class");
|
|
168
|
+
let operation: unknown;
|
|
169
|
+
try {
|
|
170
|
+
operation =
|
|
171
|
+
typeof binding.approvedContent === "string"
|
|
172
|
+
? (JSON.parse(binding.approvedContent) as { operation?: unknown }).operation
|
|
173
|
+
: null;
|
|
174
|
+
} catch {
|
|
175
|
+
operation = null;
|
|
176
|
+
}
|
|
177
|
+
const cls = operationAutoClass(operation);
|
|
178
|
+
if (!cls || cls !== standing.class)
|
|
179
|
+
return failure("permission_denied", "standing approval does not cover this operation");
|
|
180
|
+
const resolved = resolveAutoApproval(root);
|
|
181
|
+
if (
|
|
182
|
+
resolved.status !== "on" ||
|
|
183
|
+
resolved.workspace !== standing.workspace ||
|
|
184
|
+
!resolved.classes.includes(cls)
|
|
185
|
+
)
|
|
186
|
+
return failure("permission_denied", "standing auto-approval is not live for this operation");
|
|
187
|
+
if (cls === "push") {
|
|
188
|
+
const workspace = resolveWorkspaceFrom(root, configDir());
|
|
189
|
+
const provider = workspace?.vcs?.provider;
|
|
190
|
+
const account = resolved.account;
|
|
191
|
+
if (!provider || !account)
|
|
192
|
+
return failure(
|
|
193
|
+
"permission_denied",
|
|
194
|
+
"auto-push requires a workspace provider and area account",
|
|
195
|
+
);
|
|
196
|
+
const identity = verifyPushIdentity(root, provider, account);
|
|
197
|
+
if (!identity.ok) return failure("permission_denied", identity.error);
|
|
198
|
+
}
|
|
199
|
+
return success(null, null, {
|
|
200
|
+
kind: "host_observed",
|
|
201
|
+
host: caller.host,
|
|
202
|
+
session: { kind: "host", host: caller.host, handle: caller.actor },
|
|
203
|
+
workerId: null,
|
|
204
|
+
receipts: [standingReceiptFor(root, standing.workspace, cls)],
|
|
205
|
+
} as Provenance);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export type StandingAutoBinding = {
|
|
209
|
+
taskId: Id;
|
|
210
|
+
decisionId: Id;
|
|
211
|
+
binding: Decision["binding"];
|
|
212
|
+
expectedRevision: Revision;
|
|
213
|
+
expectedWorkspaceRevision: Revision;
|
|
214
|
+
step?: string;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
type StandingMatch = {
|
|
218
|
+
task: TaskRecord;
|
|
219
|
+
workspaceId: Id;
|
|
220
|
+
workspaceRevision: Revision;
|
|
221
|
+
cls: AutoClass;
|
|
222
|
+
workspaceName: string;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Dry-run half of standingAutoBinding: same matching, no recording. Tool
|
|
227
|
+
* gates use this to decide between executing and asking; the runner records.
|
|
228
|
+
*/
|
|
229
|
+
export const standingAutoApplies = (
|
|
230
|
+
store: TaskStore,
|
|
231
|
+
host: string,
|
|
232
|
+
actor: string,
|
|
233
|
+
operation: string,
|
|
234
|
+
): StandingMatch | null => {
|
|
235
|
+
let descriptor: { operation?: unknown };
|
|
236
|
+
try {
|
|
237
|
+
descriptor = JSON.parse(operation) as { operation?: unknown };
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const cls = operationAutoClass(descriptor.operation);
|
|
242
|
+
if (!cls || !autoApproves(store.root, cls)) return null;
|
|
243
|
+
const listed = store.listTasks();
|
|
244
|
+
const workspace = store.readWorkspace();
|
|
245
|
+
if (!listed.ok || !workspace.ok || !workspace.data) return null;
|
|
246
|
+
const matches = listed.data.filter(
|
|
247
|
+
(task) =>
|
|
248
|
+
task.status === "active" &&
|
|
249
|
+
task.workspaceId === workspace.data!.id &&
|
|
250
|
+
task.intent?.provenance?.session?.kind === "host" &&
|
|
251
|
+
(task.intent.provenance.session as { host?: unknown; handle?: unknown }).host === host &&
|
|
252
|
+
(task.intent.provenance.session as { host?: unknown; handle?: unknown }).handle === actor,
|
|
253
|
+
);
|
|
254
|
+
if (matches.length !== 1) return null;
|
|
255
|
+
const resolved = resolveAutoApproval(store.root);
|
|
256
|
+
if (resolved.status !== "on") return null;
|
|
257
|
+
return {
|
|
258
|
+
task: matches[0],
|
|
259
|
+
workspaceId: workspace.data.id,
|
|
260
|
+
workspaceRevision: workspace.data.revision,
|
|
261
|
+
cls,
|
|
262
|
+
workspaceName: resolved.workspace,
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Shared adapter path: resolve one gated operation under the workspace
|
|
268
|
+
* standing rule without asking. Returns the exact binding the runner needs
|
|
269
|
+
* for reserve/settle, or null when anything is uncovered, ambiguous, or
|
|
270
|
+
* unresolvable — callers fall through to the question proposal.
|
|
271
|
+
*
|
|
272
|
+
* Single active task per session (ambiguity asks), plan-before-mutation
|
|
273
|
+
* (no task asks), guardrails enforced at record time, revocation instant.
|
|
274
|
+
*/
|
|
275
|
+
export const standingAutoBinding = (
|
|
276
|
+
core: WorkitCore,
|
|
277
|
+
store: TaskStore,
|
|
278
|
+
host: string,
|
|
279
|
+
actor: string,
|
|
280
|
+
operation: string,
|
|
281
|
+
): StandingAutoBinding | null => {
|
|
282
|
+
let descriptor: { operation?: unknown };
|
|
283
|
+
try {
|
|
284
|
+
descriptor = JSON.parse(operation) as { operation?: unknown };
|
|
285
|
+
} catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const cls = operationAutoClass(descriptor.operation);
|
|
289
|
+
if (!cls || !autoApproves(store.root, cls)) return null;
|
|
290
|
+
const applies = standingAutoApplies(store, host, actor, operation);
|
|
291
|
+
if (!applies) return null;
|
|
292
|
+
const { task } = applies;
|
|
293
|
+
const resolved = resolveAutoApproval(store.root);
|
|
294
|
+
if (resolved.status !== "on") return null;
|
|
295
|
+
const presented =
|
|
296
|
+
`Workit decision: action — auto-approved ${String(descriptor.operation)} ` +
|
|
297
|
+
`under workspace rule "${resolved.workspace}" (no question asked).`;
|
|
298
|
+
const recorded = core.observeStandingDecision({
|
|
299
|
+
schemaVersion: 1,
|
|
300
|
+
action: "record",
|
|
301
|
+
taskId: task.id,
|
|
302
|
+
expectedRevision: task.revision,
|
|
303
|
+
purpose: "action",
|
|
304
|
+
binding: {
|
|
305
|
+
taskId: task.id,
|
|
306
|
+
workspaceId: applies.workspaceId,
|
|
307
|
+
scope: task.intent.data.scope,
|
|
308
|
+
presented,
|
|
309
|
+
approvedContent: operation,
|
|
310
|
+
contentRefs: [],
|
|
311
|
+
standing: { workspace: resolved.workspace, class: cls },
|
|
312
|
+
},
|
|
313
|
+
response: "approved",
|
|
314
|
+
requirementIds: [],
|
|
315
|
+
});
|
|
316
|
+
if (!recorded.ok) return null;
|
|
317
|
+
const fresh = store.readTask(task.id);
|
|
318
|
+
if (!fresh.ok) return null;
|
|
319
|
+
return {
|
|
320
|
+
taskId: task.id,
|
|
321
|
+
decisionId: recorded.data.id,
|
|
322
|
+
binding: recorded.data.data.binding,
|
|
323
|
+
expectedRevision: fresh.data.revision,
|
|
324
|
+
expectedWorkspaceRevision: applies.workspaceRevision,
|
|
325
|
+
};
|
|
326
|
+
};
|
package/src/core/branch.ts
CHANGED
|
@@ -18,6 +18,50 @@ const policy = (root: string) => resolveBranchPolicyFor(root);
|
|
|
18
18
|
const allowedBranch = (root: string, name: string) =>
|
|
19
19
|
policy(root).allowed.some((r) => r.test(name));
|
|
20
20
|
const isProtected = (root: string, name: string) => policy(root).protected.has(name.toLowerCase());
|
|
21
|
+
|
|
22
|
+
/** Public guardrail: protected refs fail closed anywhere, never ask. */
|
|
23
|
+
export const isProtectedTarget = (root: string, name: string): boolean => {
|
|
24
|
+
try {
|
|
25
|
+
return isProtected(root, name);
|
|
26
|
+
} catch {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Push identity check as code: the credential actually used must belong to
|
|
33
|
+
* the workspace area account. Anything unresolvable fails closed — an
|
|
34
|
+
* unreachable host never reads as a match.
|
|
35
|
+
*/
|
|
36
|
+
export const verifyPushIdentity = (
|
|
37
|
+
root: string,
|
|
38
|
+
provider: string,
|
|
39
|
+
account: string,
|
|
40
|
+
): { ok: true } | { ok: false; error: string } => {
|
|
41
|
+
if (!account || typeof account !== "string")
|
|
42
|
+
return { ok: false, error: "push identity requires a configured area account" };
|
|
43
|
+
const probe =
|
|
44
|
+
provider === "gitlab"
|
|
45
|
+
? (["api", "user", "--jq", ".username"] as const)
|
|
46
|
+
: (["api", "user", "--jq", ".login"] as const);
|
|
47
|
+
const bin = provider === "gitlab" ? "glab" : "gh";
|
|
48
|
+
let login: string;
|
|
49
|
+
try {
|
|
50
|
+
login = execFileSync(bin, [...probe], {
|
|
51
|
+
cwd: root,
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
54
|
+
}).trim();
|
|
55
|
+
} catch {
|
|
56
|
+
return { ok: false, error: `${bin} identity could not be resolved` };
|
|
57
|
+
}
|
|
58
|
+
if (!login || login.toLowerCase() !== account.toLowerCase())
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: `push identity ${login || "(unknown)"} does not match area account ${account}`,
|
|
62
|
+
};
|
|
63
|
+
return { ok: true };
|
|
64
|
+
};
|
|
21
65
|
// RL-01: malformed vcs.json blocks branch resolution with an exact-path error.
|
|
22
66
|
const baseBranch = (cwd: string): { base: string } | { error: string } => {
|
|
23
67
|
const resolved = vcsConfig("resolve", cwd);
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
resolveBranchPolicyFor,
|
|
5
5
|
resolveCommitPolicyFor,
|
|
6
6
|
} from "./branch";
|
|
7
|
-
import { hostingCliAvailable, prCreate } from "./pr-create";
|
|
7
|
+
import { hostingCliAvailable, mergePr, prCreate } from "./pr-create";
|
|
8
8
|
import {
|
|
9
9
|
changelogContext,
|
|
10
10
|
docsRefreshContext,
|
|
@@ -356,6 +356,11 @@ export const actionProposalQuestion = (
|
|
|
356
356
|
presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
|
|
357
357
|
approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`,
|
|
358
358
|
};
|
|
359
|
+
case "hosting.merge":
|
|
360
|
+
return {
|
|
361
|
+
presented: `Workit decision: action — Merge \`${String(resolved.source_branch ?? "")}\` into \`${payloadTarget}\` (squash, delete branch)?`,
|
|
362
|
+
approvedText: `Merge ${String(resolved.source_branch ?? "")} → ${payloadTarget}.`,
|
|
363
|
+
};
|
|
359
364
|
case "changelog.apply":
|
|
360
365
|
return {
|
|
361
366
|
presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
|
|
@@ -404,6 +409,7 @@ export const approvedResolvedExternalAction = (content: string): Result<Resolved
|
|
|
404
409
|
const value = JSON.parse(content) as { operation?: unknown; payload?: unknown };
|
|
405
410
|
if (
|
|
406
411
|
value.operation !== "hosting.pull_request" &&
|
|
412
|
+
value.operation !== "hosting.merge" &&
|
|
407
413
|
value.operation !== "changelog.apply" &&
|
|
408
414
|
value.operation !== "youtrack.update" &&
|
|
409
415
|
value.operation !== "youtrack.time" &&
|
|
@@ -428,6 +434,15 @@ export const approvedResolvedExternalAction = (content: string): Result<Resolved
|
|
|
428
434
|
typeof payload.target_branch !== "string")
|
|
429
435
|
)
|
|
430
436
|
return failure("invalid_input", "approved hosting target is incomplete");
|
|
437
|
+
if (
|
|
438
|
+
value.operation === "hosting.merge" &&
|
|
439
|
+
(typeof target.marker !== "string" ||
|
|
440
|
+
typeof target.source_branch !== "string" ||
|
|
441
|
+
typeof target.source_commit !== "string" ||
|
|
442
|
+
typeof target.remote !== "string" ||
|
|
443
|
+
typeof payload.target_branch !== "string")
|
|
444
|
+
)
|
|
445
|
+
return failure("invalid_input", "approved hosting target is incomplete");
|
|
431
446
|
if (
|
|
432
447
|
value.operation === "changelog.apply" &&
|
|
433
448
|
(typeof target.target !== "string" ||
|
|
@@ -910,6 +925,11 @@ export const resolveExternalActionRequest = (
|
|
|
910
925
|
}
|
|
911
926
|
case "git.push": {
|
|
912
927
|
const branch = request.payload.branch ?? gitValue(root, ["branch", "--show-current"]);
|
|
928
|
+
if (branch && isProtectedBranch(root, branch))
|
|
929
|
+
return failure("invalid_input", `cannot push protected branch ${branch}`, {
|
|
930
|
+
outcome: "not_started",
|
|
931
|
+
fields: [{ path: "branch", reason: "protected branch" }],
|
|
932
|
+
});
|
|
913
933
|
const commit = branch ? gitValue(root, ["rev-parse", branch]) : null;
|
|
914
934
|
const remote = pushRemote(root);
|
|
915
935
|
const identity = remote ? credentialFreeRemote(remote) : null;
|
|
@@ -1026,6 +1046,15 @@ export const resolveExternalActionRequest = (
|
|
|
1026
1046
|
const target_branch =
|
|
1027
1047
|
request.payload.target_branch ?? resolveBranchPolicyFor(root).defaultTargetBranch;
|
|
1028
1048
|
const source_branch = gitValue(root, ["branch", "--show-current"]);
|
|
1049
|
+
if (source_branch && isProtectedBranch(root, source_branch))
|
|
1050
|
+
return failure(
|
|
1051
|
+
"invalid_input",
|
|
1052
|
+
`cannot open a PR from protected branch ${source_branch}`,
|
|
1053
|
+
{
|
|
1054
|
+
outcome: "not_started",
|
|
1055
|
+
fields: [{ path: "source_branch", reason: "protected branch" }],
|
|
1056
|
+
},
|
|
1057
|
+
);
|
|
1029
1058
|
const source_commit = gitValue(root, ["rev-parse", "HEAD"]);
|
|
1030
1059
|
const remote = pushRemote(root);
|
|
1031
1060
|
const identity = remote ? credentialFreeRemote(remote) : null;
|
|
@@ -1067,6 +1096,56 @@ export const resolveExternalActionRequest = (
|
|
|
1067
1096
|
},
|
|
1068
1097
|
});
|
|
1069
1098
|
}
|
|
1099
|
+
case "hosting.merge": {
|
|
1100
|
+
const target_branch =
|
|
1101
|
+
request.payload.target_branch ?? resolveBranchPolicyFor(root).defaultTargetBranch;
|
|
1102
|
+
const source_branch =
|
|
1103
|
+
request.payload.source_branch ?? gitValue(root, ["branch", "--show-current"]);
|
|
1104
|
+
if (source_branch && isProtectedBranch(root, source_branch))
|
|
1105
|
+
return failure("invalid_input", `cannot merge protected branch ${source_branch}`, {
|
|
1106
|
+
outcome: "not_started",
|
|
1107
|
+
fields: [{ path: "source_branch", reason: "protected branch" }],
|
|
1108
|
+
});
|
|
1109
|
+
const source_commit = gitValue(root, ["rev-parse", "HEAD"]);
|
|
1110
|
+
const remote = pushRemote(root);
|
|
1111
|
+
const identity = remote ? credentialFreeRemote(remote) : null;
|
|
1112
|
+
if (
|
|
1113
|
+
!source_branch ||
|
|
1114
|
+
!source_commit ||
|
|
1115
|
+
!identity ||
|
|
1116
|
+
(remoteProvider(identity) !== null &&
|
|
1117
|
+
remoteProvider(identity) !== String(vcsConfig("load", root).provider).toLowerCase())
|
|
1118
|
+
)
|
|
1119
|
+
return failure(
|
|
1120
|
+
"capability_unavailable",
|
|
1121
|
+
"hosting target is not bound to the configured remote provider",
|
|
1122
|
+
{ capability: "hosting.merge" },
|
|
1123
|
+
);
|
|
1124
|
+
const vcs = vcsConfig("load", root);
|
|
1125
|
+
if (!vcs.ok || !vcs.tokenReady)
|
|
1126
|
+
return failure("capability_unavailable", "hosting credentials are unavailable", {
|
|
1127
|
+
capability: "hosting.merge",
|
|
1128
|
+
outcome: "not_started",
|
|
1129
|
+
});
|
|
1130
|
+
if (!hostingCliAvailable(String(vcs.provider)))
|
|
1131
|
+
return failure("capability_unavailable", "hosting CLI is unavailable", {
|
|
1132
|
+
capability: "hosting.merge",
|
|
1133
|
+
outcome: "not_started",
|
|
1134
|
+
});
|
|
1135
|
+
const normalized = {
|
|
1136
|
+
operation: request.operation,
|
|
1137
|
+
payload: { ...request.payload, target_branch, source_branch },
|
|
1138
|
+
} as ExternalActionRequest;
|
|
1139
|
+
const marker = `<!-- workit-action:${sha256({ operation: request.operation, payload: normalized.payload, source_commit, remote: identity })} -->`;
|
|
1140
|
+
return success(null, null, {
|
|
1141
|
+
request: normalized,
|
|
1142
|
+
marker,
|
|
1143
|
+
descriptorPayload: {
|
|
1144
|
+
...normalized.payload,
|
|
1145
|
+
resolved: { source_branch, source_commit, remote: identity, target_branch, marker },
|
|
1146
|
+
},
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1070
1149
|
case "changelog.apply": {
|
|
1071
1150
|
const preview = changelogApplyPreview({ ...request.payload, workspace_root: root });
|
|
1072
1151
|
if (preview.error) return failure("invalid_input", preview.error);
|
|
@@ -1289,6 +1368,19 @@ export const executeConcreteExternalAction = async (
|
|
|
1289
1368
|
: {}),
|
|
1290
1369
|
});
|
|
1291
1370
|
}
|
|
1371
|
+
case "hosting.merge": {
|
|
1372
|
+
const target = String(request.payload.target_branch ?? "");
|
|
1373
|
+
const source = String(request.payload.source_branch ?? "");
|
|
1374
|
+
if (!target || !source)
|
|
1375
|
+
return failure("invalid_input", "hosting.merge requires target and source branches", {
|
|
1376
|
+
outcome: "not_started",
|
|
1377
|
+
fields: [{ path: "target_branch", reason: "required" }],
|
|
1378
|
+
});
|
|
1379
|
+
const result = mergePr(root, { target, source });
|
|
1380
|
+
return result.error || result.ok === false
|
|
1381
|
+
? unknown(request.operation)
|
|
1382
|
+
: success(null, null, result);
|
|
1383
|
+
}
|
|
1292
1384
|
case "changelog.apply": {
|
|
1293
1385
|
const preview = changelogApplyPreview({ ...request.payload, workspace_root: root });
|
|
1294
1386
|
if (
|
|
@@ -86,6 +86,17 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
86
86
|
.strict(),
|
|
87
87
|
})
|
|
88
88
|
.strict(),
|
|
89
|
+
z
|
|
90
|
+
.object({
|
|
91
|
+
operation: z.literal("hosting.merge"),
|
|
92
|
+
payload: z
|
|
93
|
+
.object({
|
|
94
|
+
target_branch: z.string().min(1).optional(),
|
|
95
|
+
source_branch: z.string().min(1).optional(),
|
|
96
|
+
})
|
|
97
|
+
.strict(),
|
|
98
|
+
})
|
|
99
|
+
.strict(),
|
|
89
100
|
z
|
|
90
101
|
.object({
|
|
91
102
|
operation: z.literal("youtrack.update"),
|
|
@@ -216,7 +227,7 @@ export const externalActionDescriptor = (operation: string, payload: unknown): s
|
|
|
216
227
|
|
|
217
228
|
/** Compact host-facing help for the fixed optional-action surface. */
|
|
218
229
|
export const externalActionHelp =
|
|
219
|
-
"Fixed actions: git.branch_setup {action?,sdd_dir?,target_branch(required unless reapply_stash; the working branch to create or switch to),stash?}; git.commit {message}; git.push {branch?}; hosting.pull_request {title,body?,draft?,target_branch?,babysit?}; youtrack.update {issueId,markdown,minutes?}; youtrack.time {issueId,minutes,text?,dateMs?}; youtrack.meeting {issueId,minutes,text}; changelog.apply {entries?,path?,normalize_only?}; context.read {kind,range?,issueId?,issueUrl?,issueRef?,mode?,specPath?,planPath?}. context.read is read-only and needs no approval; all other operations require native approval (CLI uses a TTY; caller-unattested MCP cannot mutate).";
|
|
230
|
+
"Fixed actions: git.branch_setup {action?,sdd_dir?,target_branch(required unless reapply_stash; the working branch to create or switch to),stash?}; git.commit {message}; git.push {branch?}; hosting.pull_request {title,body?,draft?,target_branch?,babysit?}; hosting.merge {target_branch?,source_branch?}; youtrack.update {issueId,markdown,minutes?}; youtrack.time {issueId,minutes,text?,dateMs?}; youtrack.meeting {issueId,minutes,text}; changelog.apply {entries?,path?,normalize_only?}; context.read {kind,range?,issueId?,issueUrl?,issueRef?,mode?,specPath?,planPath?}. context.read is read-only and needs no approval; all other operations require native approval (CLI uses a TTY; caller-unattested MCP cannot mutate).";
|
|
220
231
|
|
|
221
232
|
export const externalActionRef = (
|
|
222
233
|
host: "opencode" | "pi" | "workit_cli",
|
package/src/core/pr-create.ts
CHANGED
|
@@ -373,4 +373,54 @@ export function prCreate(env: NodeJS.ProcessEnv, cwd: string): Record<string, an
|
|
|
373
373
|
};
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
/** Merge the open PR/MR for a source branch (squash + delete per pr settings). */
|
|
377
|
+
export function mergePr(
|
|
378
|
+
cwd: string,
|
|
379
|
+
opts: { target: string; source: string },
|
|
380
|
+
): Record<string, any> {
|
|
381
|
+
const root = repoRoot(cwd);
|
|
382
|
+
const cfg = vcsConfig("load", root);
|
|
383
|
+
if (!cfg.ok) return { error: cfg.error ?? "vcs config missing" };
|
|
384
|
+
if (!cfg.tokenReady)
|
|
385
|
+
return { error: "VCS token not ready — run /wk-init and edit token file locally" };
|
|
386
|
+
const provider = cfg.provider as string;
|
|
387
|
+
if (provider !== "gitlab" && provider !== "github")
|
|
388
|
+
return { error: `unsupported provider: ${provider}` };
|
|
389
|
+
if (!hostingCliAvailable(provider))
|
|
390
|
+
return { error: `workflow CLI missing (required for ${provider})` };
|
|
391
|
+
const pr = (cfg.pr ?? {}) as Record<string, any>;
|
|
392
|
+
const token = fs.readFileSync(cfg.tokenPath as string, "utf8").trim();
|
|
393
|
+
const squash = pr.squashOnMerge !== false;
|
|
394
|
+
const removeBranch = pr.removeSourceBranch !== false;
|
|
395
|
+
let cmd: string[];
|
|
396
|
+
let cmdEnv: NodeJS.ProcessEnv;
|
|
397
|
+
if (provider === "gitlab") {
|
|
398
|
+
cmd = ["glab", "mr", "merge", opts.source];
|
|
399
|
+
cmd.push(squash ? "--squash" : "--squash=false");
|
|
400
|
+
if (removeBranch) cmd.push("--remove-source-branch");
|
|
401
|
+
cmd.push("--yes");
|
|
402
|
+
cmdEnv = { ...process.env, PATH: process.env.PATH ?? "", GITLAB_TOKEN: token };
|
|
403
|
+
} else {
|
|
404
|
+
cmd = ["gh", "pr", "merge", opts.source];
|
|
405
|
+
cmd.push(squash ? "--squash" : "--merge");
|
|
406
|
+
if (removeBranch) cmd.push("--delete-branch");
|
|
407
|
+
cmd.push("--yes");
|
|
408
|
+
cmdEnv = { ...process.env, PATH: process.env.PATH ?? "", GH_TOKEN: token };
|
|
409
|
+
}
|
|
410
|
+
const result = spawnSync(cmd[0], cmd.slice(1), { cwd: root, encoding: "utf8", env: cmdEnv });
|
|
411
|
+
if (result.status !== 0)
|
|
412
|
+
return {
|
|
413
|
+
error: "merge failed",
|
|
414
|
+
provider,
|
|
415
|
+
stderr: (result.stderr ?? result.stdout ?? "").trim().slice(0, 800),
|
|
416
|
+
};
|
|
417
|
+
return {
|
|
418
|
+
ok: true,
|
|
419
|
+
provider,
|
|
420
|
+
targetBranch: opts.target,
|
|
421
|
+
sourceBranch: opts.source,
|
|
422
|
+
output: (result.stdout ?? "").trim(),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
376
426
|
export { buildBody, parseGhRepo, parseGhIssue };
|
package/src/core/route-intent.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TaskStore } from "./task-store";
|
|
2
2
|
|
|
3
3
|
export type ShellRouteIntent = {
|
|
4
|
-
route: "git.branch_setup" | "hosting.pull_request";
|
|
4
|
+
route: "git.branch_setup" | "hosting.pull_request" | "hosting.merge";
|
|
5
5
|
guidance: string;
|
|
6
6
|
};
|
|
7
7
|
|
|
@@ -10,6 +10,8 @@ const GUIDANCE: Record<ShellRouteIntent["route"], string> = {
|
|
|
10
10
|
"use the Workit git.branch_setup action (workit_external_action in hosts, or `workit action git.branch_setup --payload '{}' --confirm --json` in the CLI)",
|
|
11
11
|
"hosting.pull_request":
|
|
12
12
|
"use the Workit hosting.pull_request action (workit_external_action in hosts, or `workit action hosting.pull_request --payload '{...}' --confirm --json` in the CLI), then drive the created PR through workit-babysit",
|
|
13
|
+
"hosting.merge":
|
|
14
|
+
"use the Workit hosting.merge action (workit_external_action in hosts, or `workit action hosting.merge --payload '{...}' --confirm --json` in the CLI)",
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
const unbalancedQuotes = (segment: string): boolean => {
|
|
@@ -48,6 +50,10 @@ export function shellRouteIntent(command: string): ShellRouteIntent | null {
|
|
|
48
50
|
return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
|
|
49
51
|
if (head === "glab" && second === "mr" && third === "create")
|
|
50
52
|
return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
|
|
53
|
+
if (head === "gh" && second === "pr" && third === "merge")
|
|
54
|
+
return { route: "hosting.merge", guidance: GUIDANCE["hosting.merge"] };
|
|
55
|
+
if (head === "glab" && second === "mr" && third === "merge")
|
|
56
|
+
return { route: "hosting.merge", guidance: GUIDANCE["hosting.merge"] };
|
|
51
57
|
}
|
|
52
58
|
return null;
|
|
53
59
|
}
|
|
@@ -144,6 +144,19 @@ export const refSchema = z.discriminatedUnion("kind", [
|
|
|
144
144
|
.strict(),
|
|
145
145
|
z.object({ kind: z.literal("host"), host: hostSchema, handle: nonEmpty }).strict(),
|
|
146
146
|
z.object({ kind: z.literal("external"), url: text.url() }).strict(),
|
|
147
|
+
// Standing auto-approval receipt: authorizes without a live question while
|
|
148
|
+
// the referenced workspace rule covers the operation class. Verified live
|
|
149
|
+
// at reserve time against current config; dropped on export/import (a rule
|
|
150
|
+
// from another machine must never authorize here). Scalar-only shape keeps
|
|
151
|
+
// advertised operation schemas within provider depth limits.
|
|
152
|
+
z
|
|
153
|
+
.object({
|
|
154
|
+
kind: z.literal("standing"),
|
|
155
|
+
workspace: nonEmpty,
|
|
156
|
+
class: nonEmpty,
|
|
157
|
+
configDigest: nullableDigest,
|
|
158
|
+
})
|
|
159
|
+
.strict(),
|
|
147
160
|
]);
|
|
148
161
|
export type Ref = z.infer<typeof refSchema>;
|
|
149
162
|
|
|
@@ -563,6 +576,7 @@ export const decisionSchema = z
|
|
|
563
576
|
displayed: text.optional(),
|
|
564
577
|
contentRefs: z.array(refSchema),
|
|
565
578
|
statedChoice: z.object({ ref: text, text: text }).strict().optional(),
|
|
579
|
+
standing: z.object({ workspace: nonEmpty, class: nonEmpty }).strict().optional(),
|
|
566
580
|
})
|
|
567
581
|
.strict(),
|
|
568
582
|
digest,
|
package/src/core/task-engine.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
type ReconcileActionInput,
|
|
57
57
|
} from "./authority";
|
|
58
58
|
import { diffPolicy, resolvePolicy } from "./policy-resolver";
|
|
59
|
+
import { verifyStandingApproval } from "./auto-approval";
|
|
59
60
|
import { TaskStore, type MetadataLock, type ProcessEvidence } from "./task-store";
|
|
60
61
|
import {
|
|
61
62
|
exportDigest,
|
|
@@ -1087,10 +1088,21 @@ export class WorkitCore {
|
|
|
1087
1088
|
return this.recordDecision(request, observation, true);
|
|
1088
1089
|
}
|
|
1089
1090
|
|
|
1091
|
+
/**
|
|
1092
|
+
* Record a standing auto-approval: no live question, authorized by the
|
|
1093
|
+
* workspace rule named in binding.standing while it covers the operation.
|
|
1094
|
+
* Lead-only, task-scoped, verified live here — revocation is config
|
|
1095
|
+
* removal, which fails the next reserve closed.
|
|
1096
|
+
*/
|
|
1097
|
+
observeStandingDecision(request: unknown): Result<Entry<Decision>> {
|
|
1098
|
+
return this.recordDecision(request, undefined, true, true);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1090
1101
|
private recordDecision(
|
|
1091
1102
|
request: unknown,
|
|
1092
1103
|
nativeObservation?: unknown,
|
|
1093
1104
|
nativeRequired = false,
|
|
1105
|
+
standing = false,
|
|
1094
1106
|
): Result<Entry<Decision>> {
|
|
1095
1107
|
const root = this.contextRootError();
|
|
1096
1108
|
if (!root.ok) return root as Result<never>;
|
|
@@ -1110,8 +1122,16 @@ export class WorkitCore {
|
|
|
1110
1122
|
return failure("permission_denied", "stated choices cannot authorize mutating actions");
|
|
1111
1123
|
if (stated && !input.binding?.statedChoice)
|
|
1112
1124
|
return failure("invalid_input", "stated choices require binding.statedChoice");
|
|
1113
|
-
if (nativeRequired && nativeObservation === undefined && !stated)
|
|
1125
|
+
if (nativeRequired && nativeObservation === undefined && !stated && !standing)
|
|
1114
1126
|
return failure("permission_denied", "native decision observation is required");
|
|
1127
|
+
if (
|
|
1128
|
+
standing &&
|
|
1129
|
+
(!input.binding?.standing || input.purpose !== "action" || input.response !== "approved")
|
|
1130
|
+
)
|
|
1131
|
+
return failure(
|
|
1132
|
+
"invalid_input",
|
|
1133
|
+
"standing approvals need an approved action binding with a standing rule",
|
|
1134
|
+
);
|
|
1115
1135
|
const task = this.store.readTask(input.taskId);
|
|
1116
1136
|
if (!task.ok) return task as Result<never>;
|
|
1117
1137
|
if ((this.context.workerId ?? null) !== null)
|
|
@@ -1141,8 +1161,13 @@ export class WorkitCore {
|
|
|
1141
1161
|
consumption: null,
|
|
1142
1162
|
} satisfies Omit<Decision, "digest">;
|
|
1143
1163
|
const data: Decision = { ...base, digest: decisionDigest(base) };
|
|
1164
|
+
const standingApproval =
|
|
1165
|
+
standing && !stated
|
|
1166
|
+
? verifyStandingApproval(this.store.root, task.data, this.context.caller, input.binding)
|
|
1167
|
+
: null;
|
|
1168
|
+
if (standingApproval && !standingApproval.ok) return standingApproval as Result<never>;
|
|
1144
1169
|
const native =
|
|
1145
|
-
nativeRequired && !stated
|
|
1170
|
+
nativeRequired && !stated && !standingApproval
|
|
1146
1171
|
? verifyNativeDecision(
|
|
1147
1172
|
this.context.nativeAuthority,
|
|
1148
1173
|
{
|
|
@@ -1189,7 +1214,10 @@ export class WorkitCore {
|
|
|
1189
1214
|
const entry: Entry<Decision> = {
|
|
1190
1215
|
id: newId(),
|
|
1191
1216
|
recordedAt: mutation.now,
|
|
1192
|
-
provenance:
|
|
1217
|
+
provenance:
|
|
1218
|
+
standingApproval?.ok === true
|
|
1219
|
+
? standingApproval.data
|
|
1220
|
+
: (nativeProvenance ?? provenance(this.context, "agent_reported")),
|
|
1193
1221
|
data,
|
|
1194
1222
|
};
|
|
1195
1223
|
return success(mutation.revision, null, {
|
package/src/core/workspaces.ts
CHANGED
|
@@ -19,7 +19,13 @@ export type WorkspaceBranchPolicy = {
|
|
|
19
19
|
export type WorkspaceConfig = {
|
|
20
20
|
name: string;
|
|
21
21
|
glob: string;
|
|
22
|
-
vcs?: {
|
|
22
|
+
vcs?: {
|
|
23
|
+
provider: VcsProvider;
|
|
24
|
+
defaultTargetBranch?: string;
|
|
25
|
+
tokenFile?: string;
|
|
26
|
+
account?: string;
|
|
27
|
+
};
|
|
28
|
+
autoApprove?: boolean | string[];
|
|
23
29
|
youtrack?: { baseUrl?: string; link_issues?: boolean };
|
|
24
30
|
issues?: { provider?: "github"; link_on_pr?: boolean };
|
|
25
31
|
branchPolicy?: WorkspaceBranchPolicy;
|
package/src/core.ts
CHANGED
|
@@ -73,6 +73,23 @@ export type { MethodId, SelectedMethod } from "./core/methods";
|
|
|
73
73
|
export { hasLiveWorkitTask, shellRouteIntent, shouldDenyShellRoute } from "./core/route-intent";
|
|
74
74
|
export { classifyBranchDirt } from "./core/branch";
|
|
75
75
|
export type { BranchDirt } from "./core/branch";
|
|
76
|
+
export {
|
|
77
|
+
AUTO_CLASSES,
|
|
78
|
+
autoApproves,
|
|
79
|
+
operationAutoClass,
|
|
80
|
+
resolveAutoApproval,
|
|
81
|
+
standingApprovalLive,
|
|
82
|
+
standingAutoApplies,
|
|
83
|
+
standingAutoBinding,
|
|
84
|
+
standingReceiptFor,
|
|
85
|
+
verifyStandingApproval,
|
|
86
|
+
} from "./core/auto-approval";
|
|
87
|
+
export type {
|
|
88
|
+
AutoApproval,
|
|
89
|
+
AutoClass,
|
|
90
|
+
StandingAutoBinding,
|
|
91
|
+
StandingReceipt,
|
|
92
|
+
} from "./core/auto-approval";
|
|
76
93
|
export { bundleHashOfFile, isEphemeralCachePath, sha256Hex } from "./core/runtime-identity";
|
|
77
94
|
export type { ShellRouteIntent } from "./core/route-intent";
|
|
78
95
|
export { applicableDecision, reserveAction, settleAction, reconcileAction } from "./core/authority";
|