@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "1.0.8",
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": [
@@ -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
+ }
@@ -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,