@brainervirus/workit-opencode 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.
@@ -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
 
package/dist/plugin.js CHANGED
@@ -7628,7 +7628,7 @@ var workerReportSchema = object({
7628
7628
  }).strict();
7629
7629
  var workerSchema = object({
7630
7630
  assignment: assignmentSchema,
7631
- state: _enum(["assigned", "running", "cancelling", "stopped", "unknown"]),
7631
+ state: _enum(["assigned", "dispatching", "running", "cancelling", "stopped", "unknown"]),
7632
7632
  session: refSchema.nullable(),
7633
7633
  report: workerReportSchema.nullable()
7634
7634
  }).strict();
@@ -10399,6 +10399,95 @@ function evaluateClosure(requestedOutcome, view) {
10399
10399
  });
10400
10400
  }
10401
10401
 
10402
+ // packages/workit-core/src/core/methods.ts
10403
+ var METHODS = {
10404
+ "workit-challenge": { dimensions: ["challenge", "decisions"] },
10405
+ "workit-behavioral-tdd": { dimensions: ["testing"] },
10406
+ "workit-review": { dimensions: ["review"] },
10407
+ "workit-plan": { dimensions: ["artifacts", "continuity"] },
10408
+ "workit-implement": { dimensions: ["delegation"] },
10409
+ "workit-debug": { ruleIds: ["root-cause-investigation"] },
10410
+ "workit-handoff": { ruleIds: ["durable-handoff"] },
10411
+ "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
10412
+ };
10413
+ var methodMatches = (id, definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true && !(id === "workit-review" && requirement.ruleId === "self-review") || definition.ruleIds?.includes(requirement.ruleId) === true;
10414
+ var assuranceFor = (id, definition, capabilities) => {
10415
+ const keys = new Set([
10416
+ id,
10417
+ ...definition.dimensions ?? [],
10418
+ ...definition.ruleIds ?? []
10419
+ ]);
10420
+ const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
10421
+ if (id === "workit-review" && matches.length === 0)
10422
+ return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
10423
+ if (matches.some((capability) => capability.assurance === "unavailable"))
10424
+ return {
10425
+ assurance: "unavailable",
10426
+ reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
10427
+ };
10428
+ if (matches.some((capability) => capability.assurance === "enforced"))
10429
+ return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
10430
+ return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
10431
+ };
10432
+ function selectMethods(policy, capabilities) {
10433
+ return Object.entries(METHODS).flatMap(([id, definition]) => {
10434
+ const requirements = policy.requirements.filter((requirement) => methodMatches(id, definition, requirement));
10435
+ if (requirements.length === 0)
10436
+ return [];
10437
+ const capability = assuranceFor(id, definition, capabilities);
10438
+ const reasons = [
10439
+ ...new Set(requirements.map((requirement) => requirement.reason)),
10440
+ ...capability.reasons
10441
+ ];
10442
+ if (capability.assurance === "unavailable")
10443
+ reasons.unshift("required capability is unavailable");
10444
+ return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
10445
+ });
10446
+ }
10447
+ var invariantBootstrap = () => `
10448
+ Workit keeps one accountable lead and one shared task state. Inspect current task
10449
+ state before acting. An empty task.list means no session yet, not permission to
10450
+ skip Workit; for user-requested product, debug, or behavior work with no active
10451
+ or paused task, run task.start then policy.assess before other product mutations.
10452
+ Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
10453
+ to the current records; explicit values are still concurrency-checked, so never
10454
+ copy revisions between calls. Happy path: task.list, task.start {intent} with no
10455
+ revisions, policy.assess {assessment: {facts, signals, consequences,
10456
+ verification}} where facts are inferred or observed with file refs,
10457
+ writer.acquire {taskId} before product writes, evidence.record {evidence} where
10458
+ check and review kinds auto-bind the current tree and need no digests. Record
10459
+ self-review requirements with kind review and reviewContext matching your own
10460
+ host session; the lead may also record checks. Fresh-context review requires a
10461
+ session distinct from the task creator and every other evidence recorder. Record
10462
+ verification evidence last: later evidence or commits can reopen fixed findings
10463
+ for re-verification, so verify, resolve, then close with nothing in between.
10464
+ decision.record {binding with taskId/workspaceId from inspect plus presented and
10465
+ approvedContent} only through a native approval question asked receipt-shaped:
10466
+ header Workit decision: <purpose>, the same label repeated in the question
10467
+ text (some host UIs do not render headers), exactly two options approved and
10468
+ rejected; ask once, never re-ask to mint a receipt. task.close {outcome,
10469
+ summary, decisionIds}. A completed native subagent run stops its bound worker
10470
+ by itself; if a worker strands in cancelling with its run verifiably over,
10471
+ repeat worker.cancel {reason} to confirm the stop. Check and review evidence
10472
+ without a bound candidate goes stale; findings close only as fixed with passing
10473
+ verification, dismissed with supporting evidence, or deferred under an approved
10474
+ limitation. Use only the
10475
+ shared operations for task, policy, evidence, finding, decision, worker, writer,
10476
+ and state changes. Authority is bounded by the requested scope, current
10477
+ revision, caller/session provenance, and observed capabilities. Never claim host
10478
+ enforcement or evidence that the host cannot provide. Shell-executed writes are
10479
+ unattested agent-guided work even when a writer is held. Preserve unresolved
10480
+ requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
10481
+ always load on demand. Model-invoked: new instructions, interruptions, or
10482
+ forgotten items mid-task → load workit-steer before responding; about to open
10483
+ a PR or fresh from implementing → load workit-deslop; CI red → load
10484
+ workit-green-run; small-looking change before close or merge → load
10485
+ workit-blast-radius; ambiguous, consequential, or disputed proposal before
10486
+ recommending → load workit-challenge; spec or plan needs a flow diagram →
10487
+ load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
10488
+ act from memory of it.
10489
+ `.trim();
10490
+
10402
10491
  // packages/workit-core/src/core/task-context.ts
10403
10492
  function reconcileResume(view, observations = []) {
10404
10493
  if (observations.length > 0)
@@ -10408,7 +10497,7 @@ function reconcileResume(view, observations = []) {
10408
10497
  return candidate;
10409
10498
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
10410
10499
  const blockers = [...view.task.progress.blockers];
10411
- if (view.task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state)))
10500
+ if (view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
10412
10501
  blockers.push({
10413
10502
  reason: "worker state requires reconciliation",
10414
10503
  dependentAction: "resume",
@@ -10458,6 +10547,11 @@ function compactTaskContext(view) {
10458
10547
  digest: entry.data.digest,
10459
10548
  references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
10460
10549
  }));
10550
+ const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
10551
+ id: method.id,
10552
+ assurance: method.assurance,
10553
+ reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
10554
+ })) : [];
10461
10555
  const gaps = Array.from(new Set([
10462
10556
  ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
10463
10557
  ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
@@ -10467,15 +10561,18 @@ function compactTaskContext(view) {
10467
10561
  objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
10468
10562
  status: view.task.status,
10469
10563
  decisions,
10564
+ methods,
10470
10565
  gaps,
10471
10566
  nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
10472
10567
  };
10473
10568
  let encoded = JSON.stringify(context);
10474
- while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.gaps.length)) {
10569
+ while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
10475
10570
  if (context.gaps.length)
10476
10571
  context.gaps.pop();
10477
- else
10572
+ else if (context.decisions.length)
10478
10573
  context.decisions.pop();
10574
+ else
10575
+ context.methods.pop();
10479
10576
  encoded = JSON.stringify(context);
10480
10577
  }
10481
10578
  if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
@@ -10491,50 +10588,47 @@ var exportDigest = (bundle) => sha256({
10491
10588
  sourceWorkspaceId: bundle.sourceWorkspaceId,
10492
10589
  task: bundle.task
10493
10590
  });
10494
- // packages/workit-core/src/core/methods.ts
10495
- var invariantBootstrap = () => `
10496
- Workit keeps one accountable lead and one shared task state. Inspect current task
10497
- state before acting. An empty task.list means no session yet, not permission to
10498
- skip Workit; for user-requested product, debug, or behavior work with no active
10499
- or paused task, run task.start then policy.assess before other product mutations.
10500
- Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
10501
- to the current records; explicit values are still concurrency-checked, so never
10502
- copy revisions between calls. Happy path: task.list, task.start {intent} with no
10503
- revisions, policy.assess {assessment: {facts, signals, consequences,
10504
- verification}} where facts are inferred or observed with file refs,
10505
- writer.acquire {taskId} before product writes, evidence.record {evidence} where
10506
- check and review kinds auto-bind the current tree and need no digests. Record
10507
- self-review requirements with kind review and reviewContext matching your own
10508
- host session; the lead may also record checks. Fresh-context review requires a
10509
- session distinct from the task creator and every other evidence recorder. Record
10510
- verification evidence last: later evidence or commits can reopen fixed findings
10511
- for re-verification, so verify, resolve, then close with nothing in between.
10512
- decision.record {binding with taskId/workspaceId from inspect plus presented and
10513
- approvedContent} only through a native approval question asked receipt-shaped:
10514
- header Workit decision: <purpose>, the same label repeated in the question
10515
- text (some host UIs do not render headers), exactly two options approved and
10516
- rejected; ask once, never re-ask to mint a receipt. task.close {outcome,
10517
- summary, decisionIds}. A completed native subagent run stops its bound worker
10518
- by itself; if a worker strands in cancelling with its run verifiably over,
10519
- repeat worker.cancel {reason} to confirm the stop. Check and review evidence
10520
- without a bound candidate goes stale; findings close only as fixed with passing
10521
- verification, dismissed with supporting evidence, or deferred under an approved
10522
- limitation. Use only the
10523
- shared operations for task, policy, evidence, finding, decision, worker, writer,
10524
- and state changes. Authority is bounded by the requested scope, current
10525
- revision, caller/session provenance, and observed capabilities. Never claim host
10526
- enforcement or evidence that the host cannot provide. Shell-executed writes are
10527
- unattested agent-guided work even when a writer is held. Preserve unresolved
10528
- requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
10529
- always load on demand. Model-invoked: new instructions, interruptions, or
10530
- forgotten items mid-task → load workit-steer before responding; about to open
10531
- a PR or fresh from implementing → load workit-deslop; CI red → load
10532
- workit-green-run; small-looking change before close or merge → load
10533
- workit-blast-radius; ambiguous, consequential, or disputed proposal before
10534
- recommending → load workit-challenge; spec or plan needs a flow diagram →
10535
- load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
10536
- act from memory of it.
10537
- `.trim();
10591
+ // packages/workit-core/src/core/route-intent.ts
10592
+ var GUIDANCE = {
10593
+ "git.branch_setup": "use the Workit git.branch_setup action (workit_external_action in hosts, or `workit action git.branch_setup --payload '{}' --confirm --json` in the CLI)",
10594
+ "hosting.pull_request": "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"
10595
+ };
10596
+ var unbalancedQuotes = (segment) => {
10597
+ const text = segment.replace(/\\./gu, "");
10598
+ return (text.match(/"/gu)?.length ?? 0) % 2 !== 0 || (text.match(/'/gu)?.length ?? 0) % 2 !== 0;
10599
+ };
10600
+ var segmentTokens = (segment) => {
10601
+ if (segment.includes("`") || segment.includes("$("))
10602
+ return null;
10603
+ if (unbalancedQuotes(segment))
10604
+ return null;
10605
+ const parts = segment.trim().split(/\s+/u);
10606
+ while (parts.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/u.test(parts[0]))
10607
+ parts.shift();
10608
+ return parts.length > 0 ? parts : null;
10609
+ };
10610
+ var unquote = (token) => token.replace(/^['"]|['"]$/gu, "");
10611
+ function shellRouteIntent(command) {
10612
+ if (typeof command !== "string" || command.trim() === "")
10613
+ return null;
10614
+ for (const raw of command.split(/&&|\|\||;|\n/u)) {
10615
+ const parts = segmentTokens(raw);
10616
+ if (!parts)
10617
+ continue;
10618
+ const head = unquote(parts[0]);
10619
+ const second = unquote(parts[1] ?? "");
10620
+ const third = unquote(parts[2] ?? "");
10621
+ if (head === "git" && second === "switch" && (third === "-c" || third === "--create"))
10622
+ return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
10623
+ if (head === "git" && second === "checkout" && (third === "-b" || third === "-B"))
10624
+ return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
10625
+ if (head === "gh" && second === "pr" && third === "create")
10626
+ return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
10627
+ if (head === "glab" && second === "mr" && third === "create")
10628
+ return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
10629
+ }
10630
+ return null;
10631
+ }
10538
10632
  // packages/workit-core/src/core/external-action.ts
10539
10633
  var externalActionSchema = discriminatedUnion("operation", [
10540
10634
  object({
@@ -11313,10 +11407,10 @@ var applyWorkerLifecycle = (input) => {
11313
11407
  const notStarted = input.dispatch === true;
11314
11408
  if (notStarted && (input.state !== "stopped" || input.session !== null))
11315
11409
  return failure("invalid_input", "a never-dispatched worker stops with no session");
11316
- if (notStarted && (entry.data.state === "stopped" || entry.data.state === "unknown"))
11317
- return failure("invalid_transition", "worker is no longer awaiting dispatch");
11318
- if (notStarted && (entry.data.session !== null || entry.data.state === "running"))
11410
+ if (notStarted && entry.data.session !== null)
11319
11411
  return failure("invalid_transition", "worker already has an observed session");
11412
+ if (notStarted && !["dispatching", "cancelling"].includes(entry.data.state))
11413
+ return failure("invalid_transition", "worker launch was not claimed");
11320
11414
  if ((input.state === "running" || input.state === "cancelling" || input.state === "stopped" && !notStarted) && !input.session)
11321
11415
  return failure("invalid_input", "running worker observations require a session");
11322
11416
  if (entry.data.session && !sameValue(entry.data.session, input.session))
@@ -11703,7 +11797,7 @@ class WorkitCore {
11703
11797
  activeWorkerBlocker(task, workspace) {
11704
11798
  if (workspace.writer)
11705
11799
  return failure("recovery_required", "writer ownership must be released first");
11706
- if (task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state)))
11800
+ if (task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
11707
11801
  return failure("recovery_required", "worker state requires reconciliation");
11708
11802
  return null;
11709
11803
  }
@@ -12467,7 +12561,12 @@ class WorkitCore {
12467
12561
  return failure("invalid_transition", "worker is not awaiting dispatch");
12468
12562
  return success(mutation.revision, null, {
12469
12563
  ...current,
12470
- workers: current.workers.map((item) => item.id === input.workerId ? { ...item, recordedAt: mutation.now, provenance: attested.data } : item)
12564
+ workers: current.workers.map((item) => item.id === input.workerId ? {
12565
+ ...item,
12566
+ recordedAt: mutation.now,
12567
+ provenance: attested.data,
12568
+ data: { ...item.data, state: "dispatching" }
12569
+ } : item)
12471
12570
  });
12472
12571
  }
12473
12572
  });
@@ -12936,7 +13035,7 @@ class WorkitCore {
12936
13035
  const base = reconcileResume(effectiveView);
12937
13036
  if (!base.ok)
12938
13037
  return base;
12939
- const needsReconciliation = view.task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(observedStates.get(entry.id) ?? entry.data.state));
13038
+ const needsReconciliation = view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(observedStates.get(entry.id) ?? entry.data.state));
12940
13039
  const blockers = [...base.data.blockers];
12941
13040
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
12942
13041
  blockers.push({
@@ -31040,11 +31139,12 @@ var decisionContent = (purpose, question, approvedContent) => ({
31040
31139
  ]
31041
31140
  });
31042
31141
  var decisionOptions = (options) => Array.isArray(options) && options.length === 2 && options.every((option) => typeof option === "object" && option !== null && typeof option.label === "string" && typeof option.description === "string" && Object.keys(option).length === 2 && Object.keys(option).every((key) => key === "label" || key === "description")) ? options : null;
31142
+ var normalizeReceiptLabel = (label) => label.trim().replace(/(?:\s*[([]\s*[^()[\]]*\s*[)\]]\s*)+$/u, "").trim().toLowerCase();
31043
31143
  var purposeForQuestion = (question) => {
31044
31144
  const header = typeof question.header === "string" ? question.header.trim() : "";
31045
31145
  const options = decisionOptions(question.options);
31046
31146
  const decisionPurpose = header.match(/^Workit decision: (design|action|limitation|preference)$/)?.[1];
31047
- if (decisionPurpose && options !== null && options[0].label === "approved" && options[1].label === "rejected" && options[1].description === rejectedDescription)
31147
+ if (decisionPurpose && options !== null && normalizeReceiptLabel(options[0].label) === "approved" && normalizeReceiptLabel(options[1].label) === "rejected")
31048
31148
  return "decision";
31049
31149
  return;
31050
31150
  };
@@ -31053,6 +31153,10 @@ class NativeReceiptStore {
31053
31153
  #receipts = new Map;
31054
31154
  #observations = new WeakSet;
31055
31155
  #pending = new Map;
31156
+ #now;
31157
+ constructor(options = {}) {
31158
+ this.#now = options.now ?? Date.now;
31159
+ }
31056
31160
  recordRequest(requestID, sessionID, callID, questions) {
31057
31161
  if (typeof requestID !== "string" || !requestID)
31058
31162
  return;
@@ -31090,7 +31194,7 @@ class NativeReceiptStore {
31090
31194
  const options = decisionOptions(question.options);
31091
31195
  if (!options)
31092
31196
  return false;
31093
- const selected = options?.find((option) => option.label === answer);
31197
+ const selected = options.find((option) => normalizeReceiptLabel(option.label) === normalizeReceiptLabel(answer));
31094
31198
  if (!selected)
31095
31199
  return false;
31096
31200
  const decisionPurpose = typeof question.header === "string" ? question.header.match(/^Workit decision: (design|action|limitation|preference)$/)?.[1] : undefined;
@@ -31106,7 +31210,7 @@ class NativeReceiptStore {
31106
31210
  question: content.question,
31107
31211
  purpose,
31108
31212
  contentDigest: sha256(canonicalJson(content)),
31109
- recordedAt: Date.now()
31213
+ recordedAt: this.#now()
31110
31214
  };
31111
31215
  const queue = this.#receipts.get(sessionID) ?? [];
31112
31216
  queue.push(receipt);
@@ -31126,15 +31230,12 @@ class NativeReceiptStore {
31126
31230
  const receipt = queue[i];
31127
31231
  if (receipt.purpose !== purpose)
31128
31232
  continue;
31129
- if (expected.callID !== undefined && receipt.callID !== expected.callID || expected.selectedLabel !== undefined && receipt.selectedLabel !== expected.selectedLabel || expected.selectedDescription !== undefined && receipt.selectedDescription !== expected.selectedDescription || expected.decisionPurpose !== undefined && receipt.decisionPurpose !== expected.decisionPurpose || expected.contentDigest !== undefined && receipt.contentDigest !== expected.contentDigest || expected.question !== undefined && receipt.question !== expected.question)
31130
- return {
31131
- ok: false,
31132
- error: "permission_denied: native question receipt does not match the requested content " + "(binding questions must be asked receipt-shaped: header `Workit decision: <purpose>` " + "with the same label in the question text and exactly approved/rejected options)"
31133
- };
31233
+ if (expected.callID !== undefined && receipt.callID !== expected.callID || expected.selectedLabel !== undefined && normalizeReceiptLabel(receipt.selectedLabel) !== normalizeReceiptLabel(expected.selectedLabel) || expected.selectedDescription !== undefined && receipt.selectedDescription !== expected.selectedDescription || expected.decisionPurpose !== undefined && receipt.decisionPurpose !== expected.decisionPurpose || expected.contentDigest !== undefined && receipt.contentDigest !== expected.contentDigest || expected.question !== undefined && receipt.question !== expected.question)
31234
+ continue;
31134
31235
  queue.splice(i, 1);
31135
31236
  if (!queue.length)
31136
31237
  this.#receipts.delete(sessionID);
31137
- if (Date.now() - receipt.recordedAt > freshMs)
31238
+ if (this.#now() - receipt.recordedAt > freshMs)
31138
31239
  return { ok: false, error: "permission_denied: native question receipt is stale" };
31139
31240
  const observation = { receipt };
31140
31241
  this.#observations.add(observation);
@@ -31142,7 +31243,7 @@ class NativeReceiptStore {
31142
31243
  }
31143
31244
  return {
31144
31245
  ok: false,
31145
- error: "permission_denied: no native question receipt for this purpose " + "(ordinary multi-option questions mint no receipt; ask binding questions receipt-shaped: " + "header `Workit decision: <purpose>`, the same label in the question text, with exactly " + "approved/rejected options)"
31246
+ error: "permission_denied: no matching native question receipt for this purpose; if the user " + "already settled this choice in conversation, do not ask again — record the settled " + "choice in task progress and reassess so the requirement can retire. Otherwise ask " + "once, receipt-shaped: header `Workit decision: <purpose>`, the same label repeated in " + "the question text, with exactly approved/rejected options"
31146
31247
  };
31147
31248
  }
31148
31249
  verify(observation, sessionID, purpose) {
@@ -31262,7 +31363,7 @@ var opencodeCapabilities = () => [
31262
31363
  var nativeAuthority = (receipts, actor, reconciliationTokens = new WeakSet) => ({
31263
31364
  verifyDecision: ({ observation, expected, caller }) => {
31264
31365
  const receipt = receipts.verify(observation, actor, "decision");
31265
- if (!receipt || caller.host !== "opencode" || caller.actor !== actor || receipt.selectedLabel !== expected.response || receipt.selectedDescription !== (expected.response === "approved" ? expected.binding.approvedContent : rejectedDescription) || receipt.decisionPurpose !== expected.purpose || receipt.contentDigest !== sha256(canonicalJson(decisionContent(expected.purpose, expected.binding.presented, expected.binding.approvedContent))) || receipt.question !== expected.binding.presented)
31366
+ if (!receipt || caller.host !== "opencode" || caller.actor !== actor || normalizeReceiptLabel(receipt.selectedLabel) !== normalizeReceiptLabel(expected.response) || expected.response === "approved" && receipt.selectedDescription !== expected.binding.approvedContent || receipt.decisionPurpose !== expected.purpose || receipt.contentDigest !== sha256(canonicalJson(decisionContent(expected.purpose, expected.binding.presented, expected.binding.approvedContent))) || receipt.question !== expected.binding.presented)
31266
31367
  return failure("permission_denied", "native decision receipt is not bound to this session");
31267
31368
  return success(null, null, {
31268
31369
  kind: "host_observed",
@@ -31417,13 +31518,15 @@ var createWorkitTools = ({
31417
31518
  let result;
31418
31519
  if (family === "decision" && args.action === "record") {
31419
31520
  const decision = parsed.data;
31420
- const observed = receipts.consume(context.sessionID, "decision", {
31521
+ const expectation = {
31421
31522
  selectedLabel: decision.response,
31422
- selectedDescription: decision.response === "approved" ? decision.binding.approvedContent : rejectedDescription,
31423
31523
  decisionPurpose: decision.purpose,
31424
31524
  contentDigest: sha256(canonicalJson(decisionContent(decision.purpose, decision.binding.presented, decision.binding.approvedContent))),
31425
31525
  question: decision.binding.presented
31426
- });
31526
+ };
31527
+ if (decision.response === "approved")
31528
+ expectation.selectedDescription = decision.binding.approvedContent;
31529
+ const observed = receipts.consume(context.sessionID, "decision", expectation);
31427
31530
  if (!observed.ok)
31428
31531
  return output2(failure("permission_denied", observed.error));
31429
31532
  result = core.observeDecision(parsed.data, observed.observation);
@@ -31685,7 +31788,6 @@ var plugin = async ({ client, directory }) => {
31685
31788
  const lifecycleBindings = new Map;
31686
31789
  const unresolvedTaskLaunches = new Set;
31687
31790
  const dispatches = new Map;
31688
- const bootstrapped = new Set;
31689
31791
  try {
31690
31792
  logger.info(EVENT.initialization, { host: "opencode", plugin_root: root });
31691
31793
  logger.info(EVENT.provenance, loadProvenance(logger, new URL("../package.json", import.meta.url)));
@@ -31713,16 +31815,20 @@ var plugin = async ({ client, directory }) => {
31713
31815
  return [...items].sort((a, b) => a.entry.recordedAt < b.entry.recordedAt ? -1 : a.entry.recordedAt > b.entry.recordedAt ? 1 : a.entry.id < b.entry.id ? -1 : 1)[0];
31714
31816
  };
31715
31817
  const prepareDispatch = (coordinator, callID) => {
31716
- dispatches.delete(coordinator);
31818
+ if (dispatches.has(coordinator))
31819
+ return "unsettled";
31717
31820
  const store = new TaskStore(directory);
31718
31821
  const workspace = store.readWorkspace();
31719
31822
  const listed = store.listTasks();
31720
31823
  if (!workspace.ok || !workspace.data || !listed.ok)
31721
- return;
31722
- const eligible = listed.data.flatMap((task) => task.status === "active" && task.workspaceId === workspace.data?.id ? task.workers.filter((entry) => entry.data.state === "assigned" && entry.data.session === null && entry.provenance.session?.kind === "host" && entry.provenance.session.host === "opencode" && entry.provenance.session.handle === coordinator).map((entry) => ({ task, entry })) : []);
31824
+ return "unmanaged";
31825
+ const owned = listed.data.filter((task) => task.status === "active" && task.workspaceId === workspace.data?.id && task.intent.provenance.session?.kind === "host" && task.intent.provenance.session.host === "opencode" && task.intent.provenance.session.handle === coordinator);
31826
+ if (owned.length === 0)
31827
+ return "unmanaged";
31828
+ const eligible = owned.flatMap((task) => task.workers.filter((entry) => entry.data.state === "assigned" && entry.data.session === null && entry.provenance.session?.kind === "host" && entry.provenance.session.host === "opencode" && entry.provenance.session.handle === coordinator).map((entry) => ({ task, entry })));
31723
31829
  const next = oldestOfSingleTask(eligible);
31724
31830
  if (!next)
31725
- return;
31831
+ return "unbound";
31726
31832
  const generation = {
31727
31833
  coordinator,
31728
31834
  callID,
@@ -31745,7 +31851,7 @@ var plugin = async ({ client, directory }) => {
31745
31851
  observation: { stage: "prepare", sessionID: coordinator, callID }
31746
31852
  });
31747
31853
  if (!prepared.ok)
31748
- return;
31854
+ return "unbound";
31749
31855
  dispatches.set(coordinator, {
31750
31856
  generation,
31751
31857
  dispatch: prepared.data,
@@ -31753,6 +31859,7 @@ var plugin = async ({ client, directory }) => {
31753
31859
  taskId: next.task.id,
31754
31860
  workerId: next.entry.id
31755
31861
  });
31862
+ return "prepared";
31756
31863
  };
31757
31864
  const commitDispatchStart = (coordinator, childID) => {
31758
31865
  const pending = dispatches.get(coordinator);
@@ -31888,7 +31995,7 @@ var plugin = async ({ client, directory }) => {
31888
31995
  const listed = store.listTasks();
31889
31996
  if (!workspace.ok || !workspace.data || !listed.ok)
31890
31997
  return;
31891
- const candidates = listed.data.flatMap((task) => task.status === "active" && task.workspaceId === workspace.data?.id ? task.workers.filter((entry) => entry.data.state === "assigned" && entry.data.session === null && entry.provenance.session?.kind === "host" && entry.provenance.session.host === "opencode" && entry.provenance.session.handle === info.parentID).map((entry) => ({ task, entry })) : []);
31998
+ const candidates = listed.data.flatMap((task) => task.status === "active" && task.workspaceId === workspace.data?.id ? task.workers.filter((entry) => (entry.data.state === "assigned" || entry.data.state === "dispatching") && entry.data.session === null && entry.provenance.session?.kind === "host" && entry.provenance.session.host === "opencode" && entry.provenance.session.handle === info.parentID).map((entry) => ({ task, entry })) : []);
31892
31999
  const next = oldestOfSingleTask(candidates);
31893
32000
  if (!next)
31894
32001
  return;
@@ -31951,8 +32058,15 @@ var plugin = async ({ client, directory }) => {
31951
32058
  unresolvedTaskLaunches.add(input.sessionID);
31952
32059
  }
31953
32060
  },
31954
- "tool.execute.before": async (input, _output) => {
32061
+ "tool.execute.before": async (input, output) => {
31955
32062
  warnStaleSources();
32063
+ if (input.tool === "bash") {
32064
+ const command = output?.args?.command;
32065
+ const route = typeof command === "string" ? shellRouteIntent(command) : null;
32066
+ if (route)
32067
+ throw new Error(`recovery_required: direct branch or PR creation bypasses the Workit route; ${route.guidance}`);
32068
+ return;
32069
+ }
31956
32070
  if (input.tool === "task") {
31957
32071
  const listed = new TaskStore(directory).listTasks();
31958
32072
  if (listed.ok && listed.data.some((task) => task.workers.some((worker) => (worker.data.state === "cancelling" || worker.data.state === "unknown") && worker.provenance.session?.kind === "host" && worker.provenance.session.handle === input.sessionID)))
@@ -31962,7 +32076,11 @@ var plugin = async ({ client, directory }) => {
31962
32076
  const session = await sessionData2(client, input.sessionID);
31963
32077
  if (!client || !trustedSession(directory, input.sessionID, session) || session.parentID)
31964
32078
  throw new Error("delegation_lineage_denied: native task workers must be direct children of the coordinator");
31965
- prepareDispatch(input.sessionID, input.callID);
32079
+ const prepared = prepareDispatch(input.sessionID, input.callID);
32080
+ if (prepared === "unsettled")
32081
+ throw new Error("recovery_required: a previous native task launch is unsettled; wait for it to settle or reconcile its worker before launching again");
32082
+ if (prepared === "unbound")
32083
+ throw new Error("recovery_required: no assigned Workit worker is attributable to this coordinator; assign one with workit_worker before launching a native task");
31966
32084
  }
31967
32085
  },
31968
32086
  config: async (config) => {
@@ -32011,8 +32129,6 @@ var plugin = async ({ client, directory }) => {
32011
32129
  if (!first || !first.parts.length)
32012
32130
  return;
32013
32131
  const sessionID = first.info.sessionID;
32014
- if (bootstrapped.has(sessionID))
32015
- return;
32016
32132
  const anchor = first.parts[0];
32017
32133
  const session = await sessionData2(client, sessionID);
32018
32134
  if (!trustedSession(directory, sessionID, session))
@@ -32037,7 +32153,6 @@ var plugin = async ({ client, directory }) => {
32037
32153
  text: `<workit-worker-context>${workerContext}</workit-worker-context>`
32038
32154
  });
32039
32155
  }
32040
- bootstrapped.add(sessionID);
32041
32156
  }
32042
32157
  };
32043
32158
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-opencode",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
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.8"
41
+ "@brainervirus/workit-core": "^1.0.9"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@opencode-ai/plugin": "1.18.30"