@brainervirus/workit-core 1.0.10 → 1.0.11

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.10",
3
+ "version": "1.0.11",
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": [
@@ -286,16 +286,29 @@ export const actionProposalQuestion = (
286
286
  approvedText: "Reapply the recorded stash.",
287
287
  };
288
288
  const target = String(payload.target_branch ?? "");
289
+ // A dirty tree binds stash: the approval carries the stash through,
290
+ // so the question names it instead of failing later at preflight.
291
+ const stashes = payload.stash === "yes" && resolved.dirty === true;
289
292
  if (resolved.target_exists === true)
290
- return {
291
- presented: `Workit decision: action — Switch to branch \`${target}\`?`,
292
- approvedText: `Switch to \`${target}\`.`,
293
- };
293
+ return stashes
294
+ ? {
295
+ presented: `Workit decision: action — Stash the dirty working tree and switch to branch \`${target}\`?`,
296
+ approvedText: `Stash and switch to \`${target}\`.`,
297
+ }
298
+ : {
299
+ presented: `Workit decision: action — Switch to branch \`${target}\`?`,
300
+ approvedText: `Switch to \`${target}\`.`,
301
+ };
294
302
  const base = String(resolved.base_branch ?? "the base branch");
295
- return {
296
- presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
297
- approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`,
298
- };
303
+ return stashes
304
+ ? {
305
+ presented: `Workit decision: action — Stash the dirty working tree and create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
306
+ approvedText: `Stash and create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`,
307
+ }
308
+ : {
309
+ presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
310
+ approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`,
311
+ };
299
312
  }
300
313
  case "git.commit": {
301
314
  const paths = Array.isArray(resolved.paths) ? (resolved.paths as string[]) : [];
@@ -472,24 +472,131 @@ export const approvedExternalAction = (
472
472
  operation: string,
473
473
  ) => {
474
474
  const selected = externalActionState(store, host, actor, operation);
475
- if (!selected.ok) return selected;
476
- if (selected.data.entry.data.consumption !== null)
475
+ if (selected.ok) {
476
+ if (selected.data.entry.data.consumption !== null)
477
+ return failure(
478
+ selected.data.entry.data.consumption.state === "uncertain"
479
+ ? "external_outcome_unknown"
480
+ : "permission_denied",
481
+ selected.data.entry.data.consumption.state === "uncertain"
482
+ ? "previous external action outcome is unknown"
483
+ : "external action was already settled",
484
+ );
485
+ return selected;
486
+ }
487
+ // Intent carry for branch setup: an approval binds (target, base) intent,
488
+ // so unrelated HEAD or dirt moves between approval and execution re-resolve
489
+ // instead of demanding re-approval. Remote, existence, and target drift
490
+ // still fail closed through priorResolvedDrift.
491
+ const intent = approvedBranchSetupIntent(store, host, actor, operation);
492
+ if (intent.ok) {
493
+ if (intent.data.entry.data.consumption !== null)
494
+ return failure(
495
+ intent.data.entry.data.consumption.state === "uncertain"
496
+ ? "external_outcome_unknown"
497
+ : "permission_denied",
498
+ intent.data.entry.data.consumption.state === "uncertain"
499
+ ? "previous external action outcome is unknown"
500
+ : "external action was already settled",
501
+ );
502
+ return intent;
503
+ }
504
+ return selected;
505
+ };
506
+
507
+ /** Intent identity for a branch-setup descriptor: target and base bind the
508
+ * effect; head and dirt are execution-time state that re-resolves. */
509
+ export const branchSetupIntent = (
510
+ descriptor: string,
511
+ ): { target: string; base: string; remoteBase: string | null; targetExists: boolean } | null => {
512
+ try {
513
+ const value = JSON.parse(descriptor) as {
514
+ operation?: unknown;
515
+ payload?: {
516
+ target_branch?: unknown;
517
+ resolved?: {
518
+ base_branch?: unknown;
519
+ remote_base?: unknown;
520
+ target_exists?: unknown;
521
+ };
522
+ };
523
+ };
524
+ if (value.operation !== "git.branch_setup" || !value.payload) return null;
525
+ const { target_branch, resolved } = value.payload;
526
+ if (typeof target_branch !== "string" || !target_branch || !resolved) return null;
527
+ if (typeof resolved.base_branch !== "string" || typeof resolved.target_exists !== "boolean")
528
+ return null;
529
+ return {
530
+ target: target_branch,
531
+ base: resolved.base_branch,
532
+ remoteBase: typeof resolved.remote_base === "string" ? resolved.remote_base : null,
533
+ targetExists: resolved.target_exists,
534
+ };
535
+ } catch {
536
+ return null;
537
+ }
538
+ };
539
+
540
+ /** Find the single unconsumed branch-setup approval with the same intent as
541
+ * the current descriptor, regardless of head or dirt moves since approval. */
542
+ export const approvedBranchSetupIntent = (
543
+ store: TaskStore,
544
+ host: string,
545
+ actor: string,
546
+ operation: string,
547
+ ) => {
548
+ const current = branchSetupIntent(operation);
549
+ if (!current) return failure("permission_denied", "no approved action is bound to this session");
550
+ const listed = store.listTasks();
551
+ const workspace = store.readWorkspace();
552
+ if (!listed.ok || !workspace.ok)
553
+ return failure("storage_error", "external action state is unavailable");
554
+ if (!workspace.data) return failure("not_found", "workspace not found");
555
+ const candidates = listed.data.flatMap((task) => {
556
+ if (
557
+ task.status !== "active" ||
558
+ task.workspaceId !== workspace.data!.id ||
559
+ task.intent.provenance.session?.kind !== "host" ||
560
+ task.intent.provenance.session.host !== host ||
561
+ task.intent.provenance.session.handle !== actor
562
+ )
563
+ return [];
564
+ return task.decisions
565
+ .filter(
566
+ (entry) =>
567
+ entry.data.purpose === "action" &&
568
+ entry.data.response === "approved" &&
569
+ entry.data.revoked === null,
570
+ )
571
+ .map((entry) => ({ task, entry }));
572
+ });
573
+ const matches = candidates.filter(({ entry }) => {
574
+ const intent = branchSetupIntent(entry.data.binding.approvedContent);
575
+ return (
576
+ intent !== null &&
577
+ intent.target === current.target &&
578
+ intent.base === current.base &&
579
+ intent.remoteBase === current.remoteBase &&
580
+ intent.targetExists === current.targetExists
581
+ );
582
+ });
583
+ if (matches.length !== 1)
477
584
  return failure(
478
- selected.data.entry.data.consumption.state === "uncertain"
479
- ? "external_outcome_unknown"
480
- : "permission_denied",
481
- selected.data.entry.data.consumption.state === "uncertain"
482
- ? "previous external action outcome is unknown"
483
- : "external action was already settled",
585
+ "permission_denied",
586
+ matches.length === 0
587
+ ? "no approved action is bound to this session"
588
+ : "multiple approved actions match this session",
484
589
  );
485
- return selected;
590
+ return success(workspace.data.revision, null, { ...matches[0], workspace: workspace.data });
486
591
  };
487
592
 
488
593
  /**
489
594
  * Re-resolve guard: when a prior approval exists for the same requested
490
- * operation/payload, the freshly resolved baseline (source state and remote
491
- * base) must still match the approved descriptor. Drift returns not_started so
492
- * the agent re-presents the current state instead of executing a stale effect.
595
+ * operation/payload, the intent baseline must still match. For branch setup
596
+ * only (target, base, remote base, existence) bind the effect — head and
597
+ * dirt moves re-resolve instead of demanding re-approval. Drift names the
598
+ * moved element and returns not_started so the agent re-presents the
599
+ * current state instead of executing a stale effect.
493
600
  */
494
601
  export const priorResolvedDrift = (
495
602
  store: TaskStore,
@@ -511,6 +618,38 @@ export const priorResolvedDrift = (
511
618
  return success(null, null, null);
512
619
  }
513
620
  if (baseline === undefined || baseline === null) return success(null, null, null);
621
+ if (operation === "git.branch_setup") {
622
+ const before = branchSetupIntent(prior.data.entry.data.binding.approvedContent);
623
+ let after: ReturnType<typeof branchSetupIntent> = null;
624
+ try {
625
+ after = branchSetupIntent(
626
+ JSON.stringify({
627
+ operation,
628
+ payload: { ...(payload as Record<string, unknown>), resolved: currentResolved },
629
+ }),
630
+ );
631
+ } catch {
632
+ after = null;
633
+ }
634
+ if (before && after) {
635
+ const moved =
636
+ before.target !== after.target
637
+ ? "target_branch"
638
+ : before.base !== after.base
639
+ ? "base_branch"
640
+ : before.remoteBase !== after.remoteBase
641
+ ? "remote_base"
642
+ : before.targetExists !== after.targetExists
643
+ ? "target_exists"
644
+ : null;
645
+ if (moved === null) return success(null, null, null);
646
+ return failure(
647
+ "invalid_input",
648
+ `repository state changed since approval (${moved} moved); show the current state and approve the action again`,
649
+ { outcome: "not_started", operation },
650
+ );
651
+ }
652
+ }
514
653
  if (canonicalJson(baseline) === canonicalJson(currentResolved)) return success(null, null, null);
515
654
  return failure(
516
655
  "invalid_input",
@@ -1,3 +1,5 @@
1
+ import { TaskStore } from "./task-store";
2
+
1
3
  export type ShellRouteIntent = {
2
4
  route: "git.branch_setup" | "hosting.pull_request";
3
5
  guidance: string;
@@ -49,3 +51,29 @@ export function shellRouteIntent(command: string): ShellRouteIntent | null {
49
51
  }
50
52
  return null;
51
53
  }
54
+
55
+ /**
56
+ * Session-scoped enforcement: the denial protects live Workit coordination,
57
+ * so it fires only where an active or paused task exists. Any filesystem or
58
+ * storage failure means "no live task" — never deny on uncertainty.
59
+ */
60
+ export function hasLiveWorkitTask(dir: string): boolean {
61
+ try {
62
+ if (typeof dir !== "string" || dir === "") return false;
63
+ const listed = new TaskStore(dir).listTasks();
64
+ if (!listed.ok) return false;
65
+ return listed.data.some((task) => task.status === "active" || task.status === "paused");
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Single core gate for the shell route denial. Adapters pass their
73
+ * session/project directory and delete local deny duplication; outside
74
+ * attributed sessions the command is silently allowed.
75
+ */
76
+ export function shouldDenyShellRoute(dir: string, command: string): ShellRouteIntent | null {
77
+ if (!hasLiveWorkitTask(dir)) return null;
78
+ return shellRouteIntent(command);
79
+ }
@@ -455,10 +455,11 @@ export const decisionSchema = z
455
455
  approvedContent: text,
456
456
  displayed: text.optional(),
457
457
  contentRefs: z.array(refSchema),
458
+ statedChoice: z.object({ ref: text, text: text }).strict().optional(),
458
459
  })
459
460
  .strict(),
460
461
  digest,
461
- response: z.enum(["approved", "rejected"]),
462
+ response: z.enum(["approved", "rejected", "stated"]),
462
463
  requirementIds: z.array(digest),
463
464
  revoked: z.object({ at: utc, reason: text }).strict().nullable(),
464
465
  consumption: z
@@ -1189,7 +1189,15 @@ export class WorkitCore {
1189
1189
  "permission_denied",
1190
1190
  "native observation is only valid for recording decisions",
1191
1191
  );
1192
- if (nativeRequired && nativeObservation === undefined)
1192
+ // Stated choices carry user-settled decisions without a native receipt.
1193
+ // They never authorize mutating actions; native verification is skipped
1194
+ // and provenance stays agent-reported.
1195
+ const stated = input.action === "record" && input.response === "stated";
1196
+ if (stated && input.purpose === "action")
1197
+ return failure("permission_denied", "stated choices cannot authorize mutating actions");
1198
+ if (stated && !input.binding?.statedChoice)
1199
+ return failure("invalid_input", "stated choices require binding.statedChoice");
1200
+ if (nativeRequired && nativeObservation === undefined && !stated)
1193
1201
  return failure("permission_denied", "native decision observation is required");
1194
1202
  const task = this.store.readTask(input.taskId);
1195
1203
  if (!task.ok) return task as Result<never>;
@@ -1220,26 +1228,27 @@ export class WorkitCore {
1220
1228
  consumption: null,
1221
1229
  } satisfies Omit<Decision, "digest">;
1222
1230
  const data: Decision = { ...base, digest: decisionDigest(base) };
1223
- const native = nativeRequired
1224
- ? verifyNativeDecision(
1225
- this.context.nativeAuthority,
1226
- {
1227
- observation: nativeObservation,
1228
- expected: {
1229
- taskId: task.data.id,
1230
- workspaceId: workspace.data.id,
1231
- purpose: input.purpose,
1232
- response: input.response,
1233
- binding: input.binding,
1234
- bindingBytes: canonicalJson(input.binding),
1235
- digest: data.digest,
1236
- requirementIds: [...input.requirementIds],
1231
+ const native =
1232
+ nativeRequired && !stated
1233
+ ? verifyNativeDecision(
1234
+ this.context.nativeAuthority,
1235
+ {
1236
+ observation: nativeObservation,
1237
+ expected: {
1238
+ taskId: task.data.id,
1239
+ workspaceId: workspace.data.id,
1240
+ purpose: input.purpose,
1241
+ response: input.response,
1242
+ binding: input.binding,
1243
+ bindingBytes: canonicalJson(input.binding),
1244
+ digest: data.digest,
1245
+ requirementIds: [...input.requirementIds],
1246
+ },
1247
+ caller: this.context.caller,
1237
1248
  },
1238
- caller: this.context.caller,
1239
- },
1240
- { owner: this.authorityOwner, store: this.store, root: this.store.root },
1241
- )
1242
- : null;
1249
+ { owner: this.authorityOwner, store: this.store, root: this.store.root },
1250
+ )
1251
+ : null;
1243
1252
  if (native && !native.ok) return native as Result<never>;
1244
1253
  const nativeProvenance = native?.ok ? retireNativeAuthority(native.data) : null;
1245
1254
  if (native?.ok && !nativeProvenance)
@@ -413,7 +413,7 @@ const applicableRequirementDecision = (
413
413
  ({ data, provenance }) =>
414
414
  provenance.kind !== "imported" &&
415
415
  data.purpose !== "limitation" &&
416
- data.response === "approved" &&
416
+ (data.response === "approved" || data.response === "stated") &&
417
417
  data.revoked === null &&
418
418
  data.binding.taskId === task.id &&
419
419
  data.binding.workspaceId === workspace.id &&
@@ -553,7 +553,7 @@ export function evaluateRequirements(
553
553
  status: "satisfied" as const,
554
554
  evidenceIds: [],
555
555
  decisionIds: decisions.map((decision) => decision.id),
556
- reason: "an applicable approved decision satisfies the requirement",
556
+ reason: "an applicable approved or stated decision satisfies the requirement",
557
557
  };
558
558
  const limitations = requirement.acceptanceAllowed
559
559
  ? applicableDecision(task, workspace, requirement, checkoutRoot)
package/src/core.ts CHANGED
@@ -68,7 +68,7 @@ export type {
68
68
  } from "./core/task-context";
69
69
  export { METHODS, invariantBootstrap, selectMethods } from "./core/methods";
70
70
  export type { MethodId, SelectedMethod } from "./core/methods";
71
- export { shellRouteIntent } from "./core/route-intent";
71
+ export { hasLiveWorkitTask, shellRouteIntent, shouldDenyShellRoute } from "./core/route-intent";
72
72
  export type { ShellRouteIntent } from "./core/route-intent";
73
73
  export { applicableDecision, reserveAction, settleAction, reconcileAction } from "./core/authority";
74
74
  export {
@@ -86,8 +86,10 @@ export type {
86
86
  ExternalActionOperation,
87
87
  } from "./core/external-action";
88
88
  export {
89
+ approvedBranchSetupIntent,
89
90
  approvedExternalAction,
90
91
  approvedPlanCommit,
92
+ branchSetupIntent,
91
93
  planCommitDescriptor,
92
94
  planCommitBinding,
93
95
  commitMessageFromDescriptor,