@brainervirus/workit-opencode 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.
@@ -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;
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({
@@ -30897,6 +30996,7 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30897
30996
  return failure("storage_error", "external action target could not be resolved");
30898
30997
  }
30899
30998
  };
30999
+ var prBabysitNext = (output) => `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.`;
30900
31000
  var executeConcreteExternalAction = async (request, root, marker, dateMs, step, workText, caller, approvedBeforeDigest, approvedBeforeExists) => {
30901
31001
  if (localAction(request.operation)) {
30902
31002
  if (!caller)
@@ -30940,7 +31040,8 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
30940
31040
  return result.error || result.ok === false ? unknown3(request.operation) : success(null, null, {
30941
31041
  ...result,
30942
31042
  babysit: request.payload.babysit ?? true,
30943
- babysitSkill: "workit-babysit"
31043
+ babysitSkill: "workit-babysit",
31044
+ ...request.payload.babysit ?? true ? { next: prBabysitNext(String(result.output ?? "")) } : {}
30944
31045
  });
30945
31046
  }
30946
31047
  case "changelog.apply": {
@@ -31038,11 +31139,12 @@ var decisionContent = (purpose, question, approvedContent) => ({
31038
31139
  ]
31039
31140
  });
31040
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();
31041
31143
  var purposeForQuestion = (question) => {
31042
31144
  const header = typeof question.header === "string" ? question.header.trim() : "";
31043
31145
  const options = decisionOptions(question.options);
31044
31146
  const decisionPurpose = header.match(/^Workit decision: (design|action|limitation|preference)$/)?.[1];
31045
- 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")
31046
31148
  return "decision";
31047
31149
  return;
31048
31150
  };
@@ -31051,6 +31153,10 @@ class NativeReceiptStore {
31051
31153
  #receipts = new Map;
31052
31154
  #observations = new WeakSet;
31053
31155
  #pending = new Map;
31156
+ #now;
31157
+ constructor(options = {}) {
31158
+ this.#now = options.now ?? Date.now;
31159
+ }
31054
31160
  recordRequest(requestID, sessionID, callID, questions) {
31055
31161
  if (typeof requestID !== "string" || !requestID)
31056
31162
  return;
@@ -31088,7 +31194,7 @@ class NativeReceiptStore {
31088
31194
  const options = decisionOptions(question.options);
31089
31195
  if (!options)
31090
31196
  return false;
31091
- const selected = options?.find((option) => option.label === answer);
31197
+ const selected = options.find((option) => normalizeReceiptLabel(option.label) === normalizeReceiptLabel(answer));
31092
31198
  if (!selected)
31093
31199
  return false;
31094
31200
  const decisionPurpose = typeof question.header === "string" ? question.header.match(/^Workit decision: (design|action|limitation|preference)$/)?.[1] : undefined;
@@ -31104,7 +31210,7 @@ class NativeReceiptStore {
31104
31210
  question: content.question,
31105
31211
  purpose,
31106
31212
  contentDigest: sha256(canonicalJson(content)),
31107
- recordedAt: Date.now()
31213
+ recordedAt: this.#now()
31108
31214
  };
31109
31215
  const queue = this.#receipts.get(sessionID) ?? [];
31110
31216
  queue.push(receipt);
@@ -31124,15 +31230,12 @@ class NativeReceiptStore {
31124
31230
  const receipt = queue[i];
31125
31231
  if (receipt.purpose !== purpose)
31126
31232
  continue;
31127
- 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)
31128
- return {
31129
- ok: false,
31130
- 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)"
31131
- };
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;
31132
31235
  queue.splice(i, 1);
31133
31236
  if (!queue.length)
31134
31237
  this.#receipts.delete(sessionID);
31135
- if (Date.now() - receipt.recordedAt > freshMs)
31238
+ if (this.#now() - receipt.recordedAt > freshMs)
31136
31239
  return { ok: false, error: "permission_denied: native question receipt is stale" };
31137
31240
  const observation = { receipt };
31138
31241
  this.#observations.add(observation);
@@ -31140,7 +31243,7 @@ class NativeReceiptStore {
31140
31243
  }
31141
31244
  return {
31142
31245
  ok: false,
31143
- 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"
31144
31247
  };
31145
31248
  }
31146
31249
  verify(observation, sessionID, purpose) {
@@ -31260,7 +31363,7 @@ var opencodeCapabilities = () => [
31260
31363
  var nativeAuthority = (receipts, actor, reconciliationTokens = new WeakSet) => ({
31261
31364
  verifyDecision: ({ observation, expected, caller }) => {
31262
31365
  const receipt = receipts.verify(observation, actor, "decision");
31263
- 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)
31264
31367
  return failure("permission_denied", "native decision receipt is not bound to this session");
31265
31368
  return success(null, null, {
31266
31369
  kind: "host_observed",
@@ -31415,13 +31518,15 @@ var createWorkitTools = ({
31415
31518
  let result;
31416
31519
  if (family === "decision" && args.action === "record") {
31417
31520
  const decision = parsed.data;
31418
- const observed = receipts.consume(context.sessionID, "decision", {
31521
+ const expectation = {
31419
31522
  selectedLabel: decision.response,
31420
- selectedDescription: decision.response === "approved" ? decision.binding.approvedContent : rejectedDescription,
31421
31523
  decisionPurpose: decision.purpose,
31422
31524
  contentDigest: sha256(canonicalJson(decisionContent(decision.purpose, decision.binding.presented, decision.binding.approvedContent))),
31423
31525
  question: decision.binding.presented
31424
- });
31526
+ };
31527
+ if (decision.response === "approved")
31528
+ expectation.selectedDescription = decision.binding.approvedContent;
31529
+ const observed = receipts.consume(context.sessionID, "decision", expectation);
31425
31530
  if (!observed.ok)
31426
31531
  return output2(failure("permission_denied", observed.error));
31427
31532
  result = core.observeDecision(parsed.data, observed.observation);
@@ -31683,7 +31788,6 @@ var plugin = async ({ client, directory }) => {
31683
31788
  const lifecycleBindings = new Map;
31684
31789
  const unresolvedTaskLaunches = new Set;
31685
31790
  const dispatches = new Map;
31686
- const bootstrapped = new Set;
31687
31791
  try {
31688
31792
  logger.info(EVENT.initialization, { host: "opencode", plugin_root: root });
31689
31793
  logger.info(EVENT.provenance, loadProvenance(logger, new URL("../package.json", import.meta.url)));
@@ -31711,16 +31815,20 @@ var plugin = async ({ client, directory }) => {
31711
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];
31712
31816
  };
31713
31817
  const prepareDispatch = (coordinator, callID) => {
31714
- dispatches.delete(coordinator);
31818
+ if (dispatches.has(coordinator))
31819
+ return "unsettled";
31715
31820
  const store = new TaskStore(directory);
31716
31821
  const workspace = store.readWorkspace();
31717
31822
  const listed = store.listTasks();
31718
31823
  if (!workspace.ok || !workspace.data || !listed.ok)
31719
- return;
31720
- 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 })));
31721
31829
  const next = oldestOfSingleTask(eligible);
31722
31830
  if (!next)
31723
- return;
31831
+ return "unbound";
31724
31832
  const generation = {
31725
31833
  coordinator,
31726
31834
  callID,
@@ -31743,7 +31851,7 @@ var plugin = async ({ client, directory }) => {
31743
31851
  observation: { stage: "prepare", sessionID: coordinator, callID }
31744
31852
  });
31745
31853
  if (!prepared.ok)
31746
- return;
31854
+ return "unbound";
31747
31855
  dispatches.set(coordinator, {
31748
31856
  generation,
31749
31857
  dispatch: prepared.data,
@@ -31751,6 +31859,7 @@ var plugin = async ({ client, directory }) => {
31751
31859
  taskId: next.task.id,
31752
31860
  workerId: next.entry.id
31753
31861
  });
31862
+ return "prepared";
31754
31863
  };
31755
31864
  const commitDispatchStart = (coordinator, childID) => {
31756
31865
  const pending = dispatches.get(coordinator);
@@ -31886,7 +31995,7 @@ var plugin = async ({ client, directory }) => {
31886
31995
  const listed = store.listTasks();
31887
31996
  if (!workspace.ok || !workspace.data || !listed.ok)
31888
31997
  return;
31889
- 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 })) : []);
31890
31999
  const next = oldestOfSingleTask(candidates);
31891
32000
  if (!next)
31892
32001
  return;
@@ -31949,8 +32058,15 @@ var plugin = async ({ client, directory }) => {
31949
32058
  unresolvedTaskLaunches.add(input.sessionID);
31950
32059
  }
31951
32060
  },
31952
- "tool.execute.before": async (input, _output) => {
32061
+ "tool.execute.before": async (input, output) => {
31953
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
+ }
31954
32070
  if (input.tool === "task") {
31955
32071
  const listed = new TaskStore(directory).listTasks();
31956
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)))
@@ -31960,7 +32076,11 @@ var plugin = async ({ client, directory }) => {
31960
32076
  const session = await sessionData2(client, input.sessionID);
31961
32077
  if (!client || !trustedSession(directory, input.sessionID, session) || session.parentID)
31962
32078
  throw new Error("delegation_lineage_denied: native task workers must be direct children of the coordinator");
31963
- 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");
31964
32084
  }
31965
32085
  },
31966
32086
  config: async (config) => {
@@ -32009,8 +32129,6 @@ var plugin = async ({ client, directory }) => {
32009
32129
  if (!first || !first.parts.length)
32010
32130
  return;
32011
32131
  const sessionID = first.info.sessionID;
32012
- if (bootstrapped.has(sessionID))
32013
- return;
32014
32132
  const anchor = first.parts[0];
32015
32133
  const session = await sessionData2(client, sessionID);
32016
32134
  if (!trustedSession(directory, sessionID, session))
@@ -32035,7 +32153,6 @@ var plugin = async ({ client, directory }) => {
32035
32153
  text: `<workit-worker-context>${workerContext}</workit-worker-context>`
32036
32154
  });
32037
32155
  }
32038
- bootstrapped.add(sessionID);
32039
32156
  }
32040
32157
  };
32041
32158
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-opencode",
3
- "version": "1.0.7",
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.7"
41
+ "@brainervirus/workit-core": "^1.0.9"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@opencode-ai/plugin": "1.18.30"