@brainervirus/workit-opencode 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.
Files changed (2) hide show
  1. package/dist/plugin.js +156 -40
  2. package/package.json +2 -2
package/dist/plugin.js CHANGED
@@ -7591,10 +7591,11 @@ var decisionSchema = object({
7591
7591
  presented: text,
7592
7592
  approvedContent: text,
7593
7593
  displayed: text.optional(),
7594
- contentRefs: array(refSchema)
7594
+ contentRefs: array(refSchema),
7595
+ statedChoice: object({ ref: text, text }).strict().optional()
7595
7596
  }).strict(),
7596
7597
  digest,
7597
- response: _enum(["approved", "rejected"]),
7598
+ response: _enum(["approved", "rejected", "stated"]),
7598
7599
  requirementIds: array(digest),
7599
7600
  revoked: object({ at: utc, reason: text }).strict().nullable(),
7600
7601
  consumption: object({
@@ -10413,7 +10414,7 @@ function evaluateEvidence(task, candidate) {
10413
10414
  }
10414
10415
  var scopeCovers2 = scopeCovers;
10415
10416
  var applicableDecision2 = (task, workspace, requirement, checkoutRoot) => task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data).filter((decision) => decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.binding.taskId === task.id && decision.binding.workspaceId === workspace.id && decision.requirementIds.includes(requirement.id) && decision.binding.scope && scopeCovers2(decision.binding.scope, requirement.scope) && decision.digest === decisionDigest(decision) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, decision.binding).ok : decision.binding.contentRefs.every((reference) => reference.kind !== "file")));
10416
- var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && data.response === "approved" && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
10417
+ var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && (data.response === "approved" || data.response === "stated") && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
10417
10418
  var evidenceMatchesRequirement = (kind, evidenceKind, dimension) => {
10418
10419
  if (kind !== "passed")
10419
10420
  return false;
@@ -10489,7 +10490,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10489
10490
  status: "satisfied",
10490
10491
  evidenceIds: [],
10491
10492
  decisionIds: decisions.map((decision) => decision.id),
10492
- reason: "an applicable approved decision satisfies the requirement"
10493
+ reason: "an applicable approved or stated decision satisfies the requirement"
10493
10494
  };
10494
10495
  const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
10495
10496
  if (limitations.length)
@@ -10822,6 +10823,23 @@ function shellRouteIntent(command) {
10822
10823
  }
10823
10824
  return null;
10824
10825
  }
10826
+ function hasLiveWorkitTask(dir) {
10827
+ try {
10828
+ if (typeof dir !== "string" || dir === "")
10829
+ return false;
10830
+ const listed = new TaskStore(dir).listTasks();
10831
+ if (!listed.ok)
10832
+ return false;
10833
+ return listed.data.some((task) => task.status === "active" || task.status === "paused");
10834
+ } catch {
10835
+ return false;
10836
+ }
10837
+ }
10838
+ function shouldDenyShellRoute(dir, command) {
10839
+ if (!hasLiveWorkitTask(dir))
10840
+ return null;
10841
+ return shellRouteIntent(command);
10842
+ }
10825
10843
  // packages/workit-core/src/core/external-action.ts
10826
10844
  import { execFileSync } from "node:child_process";
10827
10845
  var externalActionSchema = discriminatedUnion("operation", [
@@ -11068,12 +11086,62 @@ var planCommitBinding = (store, host, actor, operation) => {
11068
11086
  };
11069
11087
  var approvedExternalAction = (store, host, actor, operation) => {
11070
11088
  const selected = externalActionState(store, host, actor, operation);
11071
- if (!selected.ok)
11089
+ if (selected.ok) {
11090
+ if (selected.data.entry.data.consumption !== null)
11091
+ return failure(selected.data.entry.data.consumption.state === "uncertain" ? "external_outcome_unknown" : "permission_denied", selected.data.entry.data.consumption.state === "uncertain" ? "previous external action outcome is unknown" : "external action was already settled");
11072
11092
  return selected;
11073
- if (selected.data.entry.data.consumption !== null)
11074
- return failure(selected.data.entry.data.consumption.state === "uncertain" ? "external_outcome_unknown" : "permission_denied", selected.data.entry.data.consumption.state === "uncertain" ? "previous external action outcome is unknown" : "external action was already settled");
11093
+ }
11094
+ const intent = approvedBranchSetupIntent(store, host, actor, operation);
11095
+ if (intent.ok) {
11096
+ if (intent.data.entry.data.consumption !== null)
11097
+ return failure(intent.data.entry.data.consumption.state === "uncertain" ? "external_outcome_unknown" : "permission_denied", intent.data.entry.data.consumption.state === "uncertain" ? "previous external action outcome is unknown" : "external action was already settled");
11098
+ return intent;
11099
+ }
11075
11100
  return selected;
11076
11101
  };
11102
+ var branchSetupIntent = (descriptor) => {
11103
+ try {
11104
+ const value = JSON.parse(descriptor);
11105
+ if (value.operation !== "git.branch_setup" || !value.payload)
11106
+ return null;
11107
+ const { target_branch, resolved } = value.payload;
11108
+ if (typeof target_branch !== "string" || !target_branch || !resolved)
11109
+ return null;
11110
+ if (typeof resolved.base_branch !== "string" || typeof resolved.target_exists !== "boolean")
11111
+ return null;
11112
+ return {
11113
+ target: target_branch,
11114
+ base: resolved.base_branch,
11115
+ remoteBase: typeof resolved.remote_base === "string" ? resolved.remote_base : null,
11116
+ targetExists: resolved.target_exists
11117
+ };
11118
+ } catch {
11119
+ return null;
11120
+ }
11121
+ };
11122
+ var approvedBranchSetupIntent = (store, host, actor, operation) => {
11123
+ const current = branchSetupIntent(operation);
11124
+ if (!current)
11125
+ return failure("permission_denied", "no approved action is bound to this session");
11126
+ const listed = store.listTasks();
11127
+ const workspace = store.readWorkspace();
11128
+ if (!listed.ok || !workspace.ok)
11129
+ return failure("storage_error", "external action state is unavailable");
11130
+ if (!workspace.data)
11131
+ return failure("not_found", "workspace not found");
11132
+ const candidates = listed.data.flatMap((task) => {
11133
+ if (task.status !== "active" || task.workspaceId !== workspace.data.id || task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
11134
+ return [];
11135
+ return task.decisions.filter((entry) => entry.data.purpose === "action" && entry.data.response === "approved" && entry.data.revoked === null).map((entry) => ({ task, entry }));
11136
+ });
11137
+ const matches = candidates.filter(({ entry }) => {
11138
+ const intent = branchSetupIntent(entry.data.binding.approvedContent);
11139
+ return intent !== null && intent.target === current.target && intent.base === current.base && intent.remoteBase === current.remoteBase && intent.targetExists === current.targetExists;
11140
+ });
11141
+ if (matches.length !== 1)
11142
+ return failure("permission_denied", matches.length === 0 ? "no approved action is bound to this session" : "multiple approved actions match this session");
11143
+ return success(workspace.data.revision, null, { ...matches[0], workspace: workspace.data });
11144
+ };
11077
11145
  var priorResolvedDrift = (store, host, actor, operation, payload, currentResolved) => {
11078
11146
  const prior = priorExternalAction(store, host, actor, operation, payload);
11079
11147
  if (!prior.ok)
@@ -11087,6 +11155,24 @@ var priorResolvedDrift = (store, host, actor, operation, payload, currentResolve
11087
11155
  }
11088
11156
  if (baseline === undefined || baseline === null)
11089
11157
  return success(null, null, null);
11158
+ if (operation === "git.branch_setup") {
11159
+ const before = branchSetupIntent(prior.data.entry.data.binding.approvedContent);
11160
+ let after = null;
11161
+ try {
11162
+ after = branchSetupIntent(JSON.stringify({
11163
+ operation,
11164
+ payload: { ...payload, resolved: currentResolved }
11165
+ }));
11166
+ } catch {
11167
+ after = null;
11168
+ }
11169
+ if (before && after) {
11170
+ const moved = before.target !== after.target ? "target_branch" : before.base !== after.base ? "base_branch" : before.remoteBase !== after.remoteBase ? "remote_base" : before.targetExists !== after.targetExists ? "target_exists" : null;
11171
+ if (moved === null)
11172
+ return success(null, null, null);
11173
+ return failure("invalid_input", `repository state changed since approval (${moved} moved); show the current state and approve the action again`, { outcome: "not_started", operation });
11174
+ }
11175
+ }
11090
11176
  if (canonicalJson(baseline) === canonicalJson(currentResolved))
11091
11177
  return success(null, null, null);
11092
11178
  return failure("invalid_input", "repository state changed since approval; show the current state and approve the action again", { outcome: "not_started", operation });
@@ -12361,7 +12447,12 @@ class WorkitCore {
12361
12447
  const input = parsed.data;
12362
12448
  if (nativeRequired && input.action !== "record")
12363
12449
  return failure("permission_denied", "native observation is only valid for recording decisions");
12364
- if (nativeRequired && nativeObservation === undefined)
12450
+ const stated = input.action === "record" && input.response === "stated";
12451
+ if (stated && input.purpose === "action")
12452
+ return failure("permission_denied", "stated choices cannot authorize mutating actions");
12453
+ if (stated && !input.binding?.statedChoice)
12454
+ return failure("invalid_input", "stated choices require binding.statedChoice");
12455
+ if (nativeRequired && nativeObservation === undefined && !stated)
12365
12456
  return failure("permission_denied", "native decision observation is required");
12366
12457
  const task = this.store.readTask(input.taskId);
12367
12458
  if (!task.ok)
@@ -12394,7 +12485,7 @@ class WorkitCore {
12394
12485
  consumption: null
12395
12486
  };
12396
12487
  const data = { ...base, digest: decisionDigest(base) };
12397
- const native = nativeRequired ? verifyNativeDecision(this.context.nativeAuthority, {
12488
+ const native = nativeRequired && !stated ? verifyNativeDecision(this.context.nativeAuthority, {
12398
12489
  observation: nativeObservation,
12399
12490
  expected: {
12400
12491
  taskId: task.data.id,
@@ -30820,13 +30911,20 @@ var actionProposalQuestion = (request, descriptorPayload) => {
30820
30911
  approvedText: "Reapply the recorded stash."
30821
30912
  };
30822
30913
  const target = String(payload.target_branch ?? "");
30914
+ const stashes = payload.stash === "yes" && resolved.dirty === true;
30823
30915
  if (resolved.target_exists === true)
30824
- return {
30916
+ return stashes ? {
30917
+ presented: `Workit decision: action — Stash the dirty working tree and switch to branch \`${target}\`?`,
30918
+ approvedText: `Stash and switch to \`${target}\`.`
30919
+ } : {
30825
30920
  presented: `Workit decision: action — Switch to branch \`${target}\`?`,
30826
30921
  approvedText: `Switch to \`${target}\`.`
30827
30922
  };
30828
30923
  const base = String(resolved.base_branch ?? "the base branch");
30829
- return {
30924
+ return stashes ? {
30925
+ presented: `Workit decision: action — Stash the dirty working tree and create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
30926
+ approvedText: `Stash and create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
30927
+ } : {
30830
30928
  presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
30831
30929
  approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
30832
30930
  };
@@ -31545,7 +31643,6 @@ var sessionParent = (session) => {
31545
31643
  const parentID = session.parentID;
31546
31644
  return typeof parentID === "string" && parentID.length > 0 ? parentID : null;
31547
31645
  };
31548
- var freshMs = 5 * 60 * 1000;
31549
31646
  var rejectedDescription = "Reject this decision";
31550
31647
  var isSelfAuthorizingActionContent = (content) => {
31551
31648
  try {
@@ -31660,8 +31757,6 @@ class NativeReceiptStore {
31660
31757
  queue.splice(i, 1);
31661
31758
  if (!queue.length)
31662
31759
  this.#receipts.delete(sessionID);
31663
- if (this.#now() - receipt.recordedAt > freshMs)
31664
- return { ok: false, error: "permission_denied: native question receipt is stale" };
31665
31760
  const observation = { receipt };
31666
31761
  this.#observations.add(observation);
31667
31762
  return { ok: true, receipt, observation };
@@ -31682,8 +31777,6 @@ class NativeReceiptStore {
31682
31777
  reasons.push(`purpose was "${closest.decisionPurpose}"`);
31683
31778
  if (expected.contentDigest !== undefined && closest.contentDigest !== expected.contentDigest)
31684
31779
  reasons.push("question or approved content differed from the asked receipt");
31685
- if (this.#now() - closest.recordedAt > freshMs)
31686
- reasons.push("receipt is stale (older than 5 minutes)");
31687
31780
  if (reasons.length)
31688
31781
  return `${base} (closest ${purpose} receipt: ${reasons.join("; ")})`;
31689
31782
  }
@@ -31962,8 +32055,7 @@ var workerIdFor = (store, actor, current, directChildren) => {
31962
32055
  var createWorkitTools = ({
31963
32056
  client,
31964
32057
  receipts = new NativeReceiptStore,
31965
- directChildren = new Map,
31966
- now = Date.now
32058
+ directChildren = new Map
31967
32059
  } = {}) => {
31968
32060
  const actionProposals = new Map;
31969
32061
  const make = (family) => tool({
@@ -31999,6 +32091,10 @@ var createWorkitTools = ({
31999
32091
  let result;
32000
32092
  if (family === "decision" && args.action === "record") {
32001
32093
  const decision = parsed.data;
32094
+ if (decision.response === "stated") {
32095
+ result = core.observeDecision(parsed.data, undefined);
32096
+ return output2(result);
32097
+ }
32002
32098
  const budgetIssue = workitBindingQuestionIssue([
32003
32099
  decisionContent(decision.purpose, decision.binding.presented, decision.binding.approvedContent)
32004
32100
  ]);
@@ -32019,22 +32115,30 @@ var createWorkitTools = ({
32019
32115
  if (decision.purpose === "action" && decision.response === "approved") {
32020
32116
  const queue = actionProposals.get(context.sessionID) ?? [];
32021
32117
  const textMatches = queue.filter((pending) => pending.presented === decision.binding.presented && pending.approvedText === decision.binding.approvedContent);
32022
- const matches = textMatches.filter((pending) => now() - pending.createdAt <= 5 * 60 * 1000);
32023
- if (matches.length > 1)
32118
+ if (textMatches.length > 1 && new Set(textMatches.map((pending) => pending.descriptor)).size > 1)
32024
32119
  return output2(failure("invalid_input", "multiple action proposals match this approval; resolve the action again"));
32025
- const pending = matches[0];
32120
+ let bound = false;
32121
+ const pending = textMatches[0];
32026
32122
  if (pending) {
32027
- actionProposals.set(context.sessionID, queue.filter((candidate) => candidate !== pending));
32028
- recordInput = {
32029
- ...recordInput,
32030
- binding: {
32031
- ...recordInput.binding,
32032
- approvedContent: pending.descriptor,
32033
- displayed: pending.approvedText
32034
- }
32035
- };
32036
- } else if (!isSelfAuthorizingActionContent(decision.binding.approvedContent)) {
32037
- return output2(failure("invalid_input", textMatches.length > 0 ? "action proposal expired; resolve the action again for a fresh proposal" : "no matching action proposal; resolve the action through the action tool first"));
32123
+ const fresh = resolveExternalActionRequest(context.directory, pending.request);
32124
+ const freshDescriptor = fresh.ok ? externalActionDescriptor(pending.request.operation, fresh.data.descriptorPayload) : null;
32125
+ if (freshDescriptor === pending.descriptor) {
32126
+ actionProposals.set(context.sessionID, queue.filter((candidate) => candidate !== pending));
32127
+ recordInput = {
32128
+ ...recordInput,
32129
+ binding: {
32130
+ ...recordInput.binding,
32131
+ approvedContent: pending.descriptor,
32132
+ displayed: pending.approvedText
32133
+ }
32134
+ };
32135
+ bound = true;
32136
+ } else if (fresh.ok) {
32137
+ actionProposals.set(context.sessionID, queue.filter((candidate) => candidate !== pending));
32138
+ }
32139
+ }
32140
+ if (!bound && !isSelfAuthorizingActionContent(decision.binding.approvedContent)) {
32141
+ return output2(failure("invalid_input", textMatches.length > 0 ? "repository state changed since the proposal; resolve the action again for a fresh proposal" : "no matching action proposal; resolve the action through the action tool first"));
32038
32142
  }
32039
32143
  }
32040
32144
  result = core.observeDecision(recordInput, observed.observation);
@@ -32073,9 +32177,17 @@ var createWorkitTools = ({
32073
32177
  if (earlySession !== null && sessionParent(earlySession) !== undefined)
32074
32178
  return output2(failure("permission_denied", "child sessions cannot run external actions"));
32075
32179
  }
32076
- const resolved = resolveExternalActionRequest(context.directory, parsed.data);
32180
+ let resolved = resolveExternalActionRequest(context.directory, parsed.data);
32077
32181
  if (!resolved.ok)
32078
32182
  return output2(resolved);
32183
+ if (resolved.ok && resolved.data.request.operation === "git.branch_setup" && resolved.data.descriptorPayload.resolved?.dirty === true && resolved.data.descriptorPayload.stash !== "yes") {
32184
+ const upgraded = resolveExternalActionRequest(context.directory, {
32185
+ ...parsed.data,
32186
+ payload: { ...parsed.data.payload, stash: "yes" }
32187
+ });
32188
+ if (upgraded.ok)
32189
+ resolved = upgraded;
32190
+ }
32079
32191
  if (resolved.data.request.operation === "context.read")
32080
32192
  return output2(await executeResolvedExternalAction(resolved.data, context.directory));
32081
32193
  if (!client)
@@ -32174,15 +32286,19 @@ var createWorkitTools = ({
32174
32286
  if (selected.error.startsWith("no approved action")) {
32175
32287
  const proposal = actionProposalQuestion(resolved.data.request, resolved.data.descriptorPayload);
32176
32288
  const pending = actionProposals.get(context.sessionID) ?? [];
32177
- pending.push({
32289
+ const existing = pending.find((candidate) => candidate.descriptor === descriptor);
32290
+ const open = existing ?? {
32178
32291
  descriptor,
32179
32292
  presented: proposal.presented,
32180
32293
  approvedText: proposal.approvedText,
32181
- createdAt: now()
32182
- });
32183
- if (pending.length > 8)
32184
- pending.shift();
32185
- actionProposals.set(context.sessionID, pending);
32294
+ request: resolved.data.request
32295
+ };
32296
+ if (!existing) {
32297
+ pending.push(open);
32298
+ if (pending.length > 8)
32299
+ pending.shift();
32300
+ actionProposals.set(context.sessionID, pending);
32301
+ }
32186
32302
  return output2(failure("needs_input", proposal.presented, {
32187
32303
  outcome: "not_started",
32188
32304
  operation: resolved.data.request.operation,
@@ -32616,7 +32732,7 @@ var plugin = async ({ client, directory }) => {
32616
32732
  warnStaleSources();
32617
32733
  if (input.tool === "bash") {
32618
32734
  const command = output?.args?.command;
32619
- const route = typeof command === "string" ? shellRouteIntent(command) : null;
32735
+ const route = typeof command === "string" ? shouldDenyShellRoute(directory, command) : null;
32620
32736
  if (route)
32621
32737
  throw new Error(`recovery_required: direct branch or PR creation bypasses the Workit route; ${route.guidance}`);
32622
32738
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-opencode",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "private": false,
5
5
  "description": "Workit OpenCode plugin — native tools, adaptive policy, and fourteen method skills",
6
6
  "keywords": [
@@ -38,7 +38,7 @@
38
38
  "typecheck": "tsc --noEmit"
39
39
  },
40
40
  "dependencies": {
41
- "@brainervirus/workit-core": "^1.0.10"
41
+ "@brainervirus/workit-core": "^1.0.11"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@opencode-ai/plugin": "1.18.30"