@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "private": false,
5
5
  "description": "Workit shared core — task, policy, evidence, review, decision, worker, and writer state for agentic coding workflows",
6
6
  "keywords": [
@@ -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 (`--no-babysit`).
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), watch (report only), or threads-only.
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
+ }
@@ -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
  })
@@ -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 && (entry.data.state === "stopped" || entry.data.state === "unknown"))
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) => ["running", "cancelling", "unknown"].includes(entry.data.state))
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
- ? { ...item, recordedAt: mutation.now, provenance: attested.data }
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,