@brainervirus/workit-core 1.0.7 → 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 +1 -1
- package/skills/workit-babysit/SKILL.md +9 -4
- package/src/core/external-action-effects.ts +6 -0
- package/src/core/route-intent.ts +51 -0
- package/src/core/task-context.ts +15 -3
- package/src/core/task-contract.ts +1 -1
- package/src/core/task-engine.ts +13 -6
- package/src/core.ts +2 -0
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: workit-babysit
|
|
3
|
-
description: Use when a PR needs driving to merge-ready, CI is red, or the user says babysit
|
|
3
|
+
description: Use after creating a PR (auto-starts, drive default), when a PR needs driving to merge-ready, CI is red, or the user says babysit
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Babysit a PR to merge-ready
|
|
7
7
|
|
|
8
|
-
Babysit starts automatically on PR creation unless declined (
|
|
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
|
|
|
@@ -17,8 +19,11 @@ selects requirements; do not wait for a rule that can only exist after assess.
|
|
|
17
19
|
|
|
18
20
|
## Method
|
|
19
21
|
|
|
20
|
-
1. Declare mode: drive (fix + merge
|
|
22
|
+
1. Declare mode in the same turn as the PR URL: drive (fix + merge, the
|
|
23
|
+
default), watch (report only), or threads-only.
|
|
21
24
|
2. Work the merge frontier in order: conflicts → review threads → CI.
|
|
25
|
+
Record a frontier brief (frontier state, next action) in task progress
|
|
26
|
+
per pass so the steps stay visible.
|
|
22
27
|
3. Classify CI before retry: flake (rerun once) vs stale base (verify with
|
|
23
28
|
`git merge-base --is-ancestor` before updating) vs real failure (fix).
|
|
24
29
|
4. Triage bot findings skeptically: reproduce or quote code before acting;
|
|
@@ -937,6 +937,9 @@ export const resolveExternalActionRequest = (
|
|
|
937
937
|
}
|
|
938
938
|
};
|
|
939
939
|
|
|
940
|
+
export const prBabysitNext = (output: string): string =>
|
|
941
|
+
`Babysit ${output} to merge-ready (drive mode default). Follow skill workit-babysit: declare mode in this turn, work conflicts, review threads, then CI, and record a frontier brief in task progress per pass. Opt out with babysit:false.`;
|
|
942
|
+
|
|
940
943
|
export const executeConcreteExternalAction = async (
|
|
941
944
|
request: ExternalActionRequest,
|
|
942
945
|
root: string,
|
|
@@ -1015,6 +1018,9 @@ export const executeConcreteExternalAction = async (
|
|
|
1015
1018
|
...result,
|
|
1016
1019
|
babysit: request.payload.babysit ?? true,
|
|
1017
1020
|
babysitSkill: "workit-babysit",
|
|
1021
|
+
...((request.payload.babysit ?? true)
|
|
1022
|
+
? { next: prBabysitNext(String((result as { output?: unknown }).output ?? "")) }
|
|
1023
|
+
: {}),
|
|
1018
1024
|
});
|
|
1019
1025
|
}
|
|
1020
1026
|
case "changelog.apply": {
|
|
@@ -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,
|