@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.
- package/assets/skills/workit-babysit/SKILL.md +9 -4
- package/dist/plugin.js +198 -81
- package/package.json +2 -2
|
@@ -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 (
|
|
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
|
|
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/
|
|
10495
|
-
var
|
|
10496
|
-
Workit
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
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 &&
|
|
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 ? {
|
|
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"
|
|
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
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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 " + "
|
|
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 ||
|
|
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
|
|
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.
|
|
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
|
|
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,
|
|
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.
|
|
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.
|
|
41
|
+
"@brainervirus/workit-core": "^1.0.9"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@opencode-ai/plugin": "1.18.30"
|