@brainervirus/workit-core 1.0.8 → 1.0.9
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
CHANGED
|
@@ -7,7 +7,9 @@ description: Use after creating a PR (auto-starts, drive default), when a PR nee
|
|
|
7
7
|
|
|
8
8
|
Babysit starts automatically on PR creation unless declined (`babysit:false`).
|
|
9
9
|
One babysitter per PR; never mutate PR topology (no rebase strategy changes,
|
|
10
|
-
no force-push).
|
|
10
|
+
no force-push). When a PR URL is observed from a route Workit did not enforce
|
|
11
|
+
(for example a raw `gh pr create` the host allowed), load this skill and drive
|
|
12
|
+
that PR anyway; do not claim the Workit route was enforced.
|
|
11
13
|
|
|
12
14
|
## Before method work
|
|
13
15
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type ShellRouteIntent = {
|
|
2
|
+
route: "git.branch_setup" | "hosting.pull_request";
|
|
3
|
+
guidance: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const GUIDANCE: Record<ShellRouteIntent["route"], string> = {
|
|
7
|
+
"git.branch_setup":
|
|
8
|
+
"use the Workit git.branch_setup action (workit_external_action in hosts, or `workit action git.branch_setup --payload '{}' --confirm --json` in the CLI)",
|
|
9
|
+
"hosting.pull_request":
|
|
10
|
+
"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",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const unbalancedQuotes = (segment: string): boolean => {
|
|
14
|
+
const text = segment.replace(/\\./gu, "");
|
|
15
|
+
return (text.match(/"/gu)?.length ?? 0) % 2 !== 0 || (text.match(/'/gu)?.length ?? 0) % 2 !== 0;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const segmentTokens = (segment: string): string[] | null => {
|
|
19
|
+
if (segment.includes("`") || segment.includes("$(")) return null;
|
|
20
|
+
if (unbalancedQuotes(segment)) return null;
|
|
21
|
+
const parts = segment.trim().split(/\s+/u);
|
|
22
|
+
while (parts.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/u.test(parts[0])) parts.shift();
|
|
23
|
+
return parts.length > 0 ? parts : null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const unquote = (token: string): string => token.replace(/^['"]|['"]$/gu, "");
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Narrow recognizer for direct branch and PR creation commands. It returns null
|
|
30
|
+
* for everything it cannot classify unambiguously, so hosts deny only the exact
|
|
31
|
+
* routes that must go through the shared action reservation.
|
|
32
|
+
*/
|
|
33
|
+
export function shellRouteIntent(command: string): ShellRouteIntent | null {
|
|
34
|
+
if (typeof command !== "string" || command.trim() === "") return null;
|
|
35
|
+
for (const raw of command.split(/&&|\|\||;|\n/u)) {
|
|
36
|
+
const parts = segmentTokens(raw);
|
|
37
|
+
if (!parts) continue;
|
|
38
|
+
const head = unquote(parts[0]);
|
|
39
|
+
const second = unquote(parts[1] ?? "");
|
|
40
|
+
const third = unquote(parts[2] ?? "");
|
|
41
|
+
if (head === "git" && second === "switch" && (third === "-c" || third === "--create"))
|
|
42
|
+
return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
|
|
43
|
+
if (head === "git" && second === "checkout" && (third === "-b" || third === "-B"))
|
|
44
|
+
return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
|
|
45
|
+
if (head === "gh" && second === "pr" && third === "create")
|
|
46
|
+
return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
|
|
47
|
+
if (head === "glab" && second === "mr" && third === "create")
|
|
48
|
+
return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
package/src/core/task-context.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type Utc,
|
|
14
14
|
} from "./task-contract";
|
|
15
15
|
import { captureCandidate, evaluateEvidence } from "./task-evaluation";
|
|
16
|
+
import { selectMethods, type SelectedMethod } from "./methods";
|
|
16
17
|
import type { NativeWorkerObservation } from "./workers";
|
|
17
18
|
|
|
18
19
|
export type ResumeObservation = {
|
|
@@ -43,7 +44,7 @@ export function reconcileResume(
|
|
|
43
44
|
const blockers = [...view.task.progress.blockers];
|
|
44
45
|
if (
|
|
45
46
|
view.task.workers.some((entry) =>
|
|
46
|
-
["running", "cancelling", "unknown"].includes(entry.data.state),
|
|
47
|
+
["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state),
|
|
47
48
|
)
|
|
48
49
|
)
|
|
49
50
|
blockers.push({
|
|
@@ -71,10 +72,12 @@ type CompactDecision = {
|
|
|
71
72
|
digest: string;
|
|
72
73
|
references: CompactReference[];
|
|
73
74
|
};
|
|
75
|
+
type CompactMethod = Pick<SelectedMethod, "id" | "assurance"> & { reason: string };
|
|
74
76
|
export type CompactTaskContext = {
|
|
75
77
|
objective: string;
|
|
76
78
|
status: TaskView["task"]["status"];
|
|
77
79
|
decisions: CompactDecision[];
|
|
80
|
+
methods: CompactMethod[];
|
|
78
81
|
gaps: string[];
|
|
79
82
|
nextAction: string | null;
|
|
80
83
|
};
|
|
@@ -127,6 +130,13 @@ export function compactTaskContext(view: TaskView): string {
|
|
|
127
130
|
digest: entry.data.digest,
|
|
128
131
|
references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference),
|
|
129
132
|
}));
|
|
133
|
+
const methods: CompactMethod[] = view.task.policy
|
|
134
|
+
? selectMethods(view.task.policy, view.capabilities).map((method) => ({
|
|
135
|
+
id: method.id,
|
|
136
|
+
assurance: method.assurance,
|
|
137
|
+
reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? "",
|
|
138
|
+
}))
|
|
139
|
+
: [];
|
|
130
140
|
const gaps = Array.from(
|
|
131
141
|
new Set([
|
|
132
142
|
...view.evidence
|
|
@@ -145,16 +155,18 @@ export function compactTaskContext(view: TaskView): string {
|
|
|
145
155
|
objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
|
|
146
156
|
status: view.task.status,
|
|
147
157
|
decisions,
|
|
158
|
+
methods,
|
|
148
159
|
gaps,
|
|
149
160
|
nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES),
|
|
150
161
|
};
|
|
151
162
|
let encoded = JSON.stringify(context);
|
|
152
163
|
while (
|
|
153
164
|
compactBytes(encoded) > COMPACT_MAX_BYTES &&
|
|
154
|
-
(context.decisions.length || context.gaps.length)
|
|
165
|
+
(context.decisions.length || context.methods.length || context.gaps.length)
|
|
155
166
|
) {
|
|
156
167
|
if (context.gaps.length) context.gaps.pop();
|
|
157
|
-
else context.decisions.pop();
|
|
168
|
+
else if (context.decisions.length) context.decisions.pop();
|
|
169
|
+
else context.methods.pop();
|
|
158
170
|
encoded = JSON.stringify(context);
|
|
159
171
|
}
|
|
160
172
|
if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
|
|
@@ -510,7 +510,7 @@ export type WorkerReport = z.infer<typeof workerReportSchema>;
|
|
|
510
510
|
export const workerSchema = z
|
|
511
511
|
.object({
|
|
512
512
|
assignment: assignmentSchema,
|
|
513
|
-
state: z.enum(["assigned", "running", "cancelling", "stopped", "unknown"]),
|
|
513
|
+
state: z.enum(["assigned", "dispatching", "running", "cancelling", "stopped", "unknown"]),
|
|
514
514
|
session: refSchema.nullable(),
|
|
515
515
|
report: workerReportSchema.nullable(),
|
|
516
516
|
})
|
package/src/core/task-engine.ts
CHANGED
|
@@ -343,10 +343,10 @@ const applyWorkerLifecycle = (input: ObserveWorkerLifecycleInput): Result<Entry<
|
|
|
343
343
|
const notStarted = input.dispatch === true;
|
|
344
344
|
if (notStarted && (input.state !== "stopped" || input.session !== null))
|
|
345
345
|
return failure("invalid_input", "a never-dispatched worker stops with no session");
|
|
346
|
-
if (notStarted &&
|
|
347
|
-
return failure("invalid_transition", "worker is no longer awaiting dispatch");
|
|
348
|
-
if (notStarted && (entry.data.session !== null || entry.data.state === "running"))
|
|
346
|
+
if (notStarted && entry.data.session !== null)
|
|
349
347
|
return failure("invalid_transition", "worker already has an observed session");
|
|
348
|
+
if (notStarted && !["dispatching", "cancelling"].includes(entry.data.state))
|
|
349
|
+
return failure("invalid_transition", "worker launch was not claimed");
|
|
350
350
|
if (
|
|
351
351
|
(input.state === "running" ||
|
|
352
352
|
input.state === "cancelling" ||
|
|
@@ -824,7 +824,9 @@ export class WorkitCore {
|
|
|
824
824
|
if (workspace.writer)
|
|
825
825
|
return failure("recovery_required", "writer ownership must be released first");
|
|
826
826
|
if (
|
|
827
|
-
task.workers.some((entry) =>
|
|
827
|
+
task.workers.some((entry) =>
|
|
828
|
+
["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state),
|
|
829
|
+
)
|
|
828
830
|
)
|
|
829
831
|
return failure("recovery_required", "worker state requires reconciliation");
|
|
830
832
|
return null;
|
|
@@ -1776,7 +1778,12 @@ export class WorkitCore {
|
|
|
1776
1778
|
...current,
|
|
1777
1779
|
workers: current.workers.map((item) =>
|
|
1778
1780
|
item.id === input.workerId
|
|
1779
|
-
? {
|
|
1781
|
+
? {
|
|
1782
|
+
...item,
|
|
1783
|
+
recordedAt: mutation.now,
|
|
1784
|
+
provenance: attested.data,
|
|
1785
|
+
data: { ...item.data, state: "dispatching" as const },
|
|
1786
|
+
}
|
|
1780
1787
|
: item,
|
|
1781
1788
|
),
|
|
1782
1789
|
});
|
|
@@ -2337,7 +2344,7 @@ export class WorkitCore {
|
|
|
2337
2344
|
const base = reconcileResumeContext(effectiveView);
|
|
2338
2345
|
if (!base.ok) return base;
|
|
2339
2346
|
const needsReconciliation = view.task.workers.some((entry) =>
|
|
2340
|
-
["running", "cancelling", "unknown"].includes(
|
|
2347
|
+
["dispatching", "running", "cancelling", "unknown"].includes(
|
|
2341
2348
|
observedStates.get(entry.id) ?? entry.data.state,
|
|
2342
2349
|
),
|
|
2343
2350
|
);
|
package/src/core.ts
CHANGED
|
@@ -66,6 +66,8 @@ export type {
|
|
|
66
66
|
} from "./core/task-context";
|
|
67
67
|
export { METHODS, invariantBootstrap, selectMethods } from "./core/methods";
|
|
68
68
|
export type { MethodId, SelectedMethod } from "./core/methods";
|
|
69
|
+
export { shellRouteIntent } from "./core/route-intent";
|
|
70
|
+
export type { ShellRouteIntent } from "./core/route-intent";
|
|
69
71
|
export { applicableDecision, reserveAction, settleAction, reconcileAction } from "./core/authority";
|
|
70
72
|
export {
|
|
71
73
|
createAuthorizedExternalActionRunner,
|