opencode-forgekeeper 2.2.1 → 2.3.0

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/README.md CHANGED
@@ -5,7 +5,7 @@ It prefixes the session title with their references, shows the PR/MR status in t
5
5
  footer, and tells the agent about new review feedback. It supports GitLab and GitHub. The status
6
6
  and review notifications are optional; see [Keep only session titles](#keep-only-session-titles).
7
7
 
8
- Requires OpenCode 2. It replaces
8
+ Requires OpenCode 2.2 or later. It replaces
9
9
  [`opencode-forge-session-title`](https://www.npmjs.com/package/opencode-forge-session-title); see
10
10
  [Migrating from opencode-forge-session-title](#migrating-from-opencode-forge-session-title).
11
11
 
@@ -46,7 +46,9 @@ what's missing, such as `glab is not installed`.
46
46
  ## Session targets
47
47
 
48
48
  The server registers the `set_session_target` tool and adds instructions to the agent's
49
- context. When you establish or change the primary issue, PR, or MR, the agent sets its full
49
+ context. The current target is stored in the `forgekeeper-session-target` session instruction
50
+ entry. Updates take effect at the next step without changing the system prompt.
51
+ When you establish or change the primary issue, PR, or MR, the agent sets its full
50
52
  URL as the session target. Background references, comparisons, and dependencies keep the
51
53
  current target. After the agent creates a PR/MR for the current task, it sets the new PR/MR
52
54
  as the target, passing the current issue as `issue_url`.
package/dist/index.js CHANGED
@@ -2423,6 +2423,7 @@ var shorten = (text, length) => text.length > length ? `${text.slice(0, length -
2423
2423
 
2424
2424
  // packages/forgekeeper/src/server.ts
2425
2425
  var SKIP_BRANCHES = new Set(["master", "main", "HEAD", "develop"]);
2426
+ var TARGET_KEY = "forgekeeper-session-target";
2426
2427
  var GUIDANCE = [
2427
2428
  "When the user establishes or changes the primary issue, PR, or MR for this session, call set_session_target with its full URL before starting that work.",
2428
2429
  "When the user asks you to work on several issues, PRs, or MRs together, call set_session_target with all of their full URLs in targets. If the user describes them instead of linking them, find them first, then set them before starting that work. When they are all PRs/MRs for one established issue, pass it as issue_url.",
@@ -2430,7 +2431,8 @@ var GUIDANCE = [
2430
2431
  'When the user drops some of several targets from the task, call set_session_target with operation "remove" and their URLs.',
2431
2432
  "Background references, comparisons, and dependencies do not change the target. Follow-ups without a new primary target keep the current target.",
2432
2433
  "Include issue_url only when its relationship to the target PRs/MRs is established. Never carry over the checked-out branch\u2019s issue to another target.",
2433
- 'When the user explicitly returns to work on the checked-out branch, call set_session_target with target "branch".'
2434
+ 'When the user explicitly returns to work on the checked-out branch, call set_session_target with target "branch".',
2435
+ `The current session target is in the <context> entry "${TARGET_KEY}". The plugin updates it when it changes, so trust the latest value.`
2434
2436
  ];
2435
2437
  function describeTargets(targets) {
2436
2438
  if (targets.length === 0)
@@ -2459,6 +2461,7 @@ async function drain(tasks, limit) {
2459
2461
  var json = (value) => JSON.parse(JSON.stringify(value));
2460
2462
  async function setupServer(ctx, lookups, status2) {
2461
2463
  const pending = new Map;
2464
+ const instructing = new Map;
2462
2465
  const controller = new AbortController;
2463
2466
  let closing = false;
2464
2467
  const known = new Map;
@@ -2560,6 +2563,7 @@ async function setupServer(ctx, lookups, status2) {
2560
2563
  const tools = await ctx.tool.transform((editor) => {
2561
2564
  editor.add({
2562
2565
  name: "set_session_target",
2566
+ options: { codemode: false },
2563
2567
  description: 'Set the issues, PRs, or MRs this session works on, for its title and PR/MR status. Use target for one full URL, or "branch" to return to branch-based naming. Use targets for several full URLs. The default operation, "replace", sets exactly the given targets; "add" keeps the current targets, and "remove" drops the given ones. Call it after creating a PR/MR for the current task. Without issue_url, the related issue is inferred from the PR/MR source branch name. This only changes local session metadata.',
2564
2568
  input: {
2565
2569
  type: "object",
@@ -2603,6 +2607,7 @@ async function setupServer(ctx, lookups, status2) {
2603
2607
  ...state.prefix ? { prefix: state.prefix } : {}
2604
2608
  });
2605
2609
  watcher?.targetsChanged(sessionID, result.targets);
2610
+ await instruct(sessionID).catch(() => {});
2606
2611
  await rpc.events.emit("targetChanged", { sessionID, ...targetOutput(result.targets) });
2607
2612
  await update(sessionID);
2608
2613
  });
@@ -2610,21 +2615,53 @@ async function setupServer(ctx, lookups, status2) {
2610
2615
  }
2611
2616
  });
2612
2617
  });
2618
+ async function writeInstructions(sessionID) {
2619
+ if (closing)
2620
+ return [];
2621
+ const state = await stateFor(sessionID);
2622
+ const value = `Current session target: ${describeTargets(state.targets)}.`;
2623
+ const entries = await ctx.session.instructions.entry.list({ sessionID });
2624
+ if (entries.some((entry) => entry.key === TARGET_KEY && entry.value === value))
2625
+ return [];
2626
+ if (closing)
2627
+ return [];
2628
+ await ctx.session.instructions.entry.put({ sessionID, key: TARGET_KEY, value });
2629
+ return [value];
2630
+ }
2631
+ function instruct(sessionID) {
2632
+ const next = (instructing.get(sessionID) ?? Promise.resolve()).then(() => writeInstructions(sessionID));
2633
+ const settled = next.catch(() => {});
2634
+ instructing.set(sessionID, settled);
2635
+ settled.then(() => {
2636
+ if (instructing.get(sessionID) === settled)
2637
+ instructing.delete(sessionID);
2638
+ });
2639
+ return next;
2640
+ }
2613
2641
  await ctx.session.hook("context", async (event) => {
2614
2642
  try {
2615
2643
  const session = await ctx.session.get({ sessionID: event.sessionID });
2616
2644
  if (session.parentID || session.location.directory !== ctx.location.directory)
2617
2645
  return;
2618
- const state = await stateFor(event.sessionID);
2619
- event.system.push({
2620
- type: "text",
2621
- text: [...GUIDANCE, `Current session target: ${describeTargets(state.targets)}.`].join(`
2622
- `)
2623
- });
2646
+ event.system.push({ type: "text", text: GUIDANCE.join(`
2647
+ `) });
2648
+ const written = await instruct(event.sessionID);
2649
+ if (written.length)
2650
+ event.system.push({ type: "text", text: written.join(`
2651
+ `) });
2624
2652
  } catch {}
2625
2653
  });
2626
2654
  (async () => {
2627
- for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
2655
+ for await (const event of ctx.event.subscribe({
2656
+ types: [
2657
+ "session.renamed",
2658
+ "session.moved",
2659
+ "session.execution.succeeded",
2660
+ "session.execution.failed",
2661
+ "vcs.branch.updated"
2662
+ ],
2663
+ signal: controller.signal
2664
+ })) {
2628
2665
  const { type } = event;
2629
2666
  const sessionID = event.data?.sessionID;
2630
2667
  if ((type === "session.renamed" || type === "session.execution.succeeded") && sessionID) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-forgekeeper",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "OpenCode plugin that tracks each session's GitHub and GitLab issues and PRs/MRs: title prefixes, a PR/MR status footer, and review notifications",
5
5
  "author": {
6
6
  "name": "Pedro Pombeiro",
@@ -41,9 +41,9 @@
41
41
  ],
42
42
  "license": "MIT",
43
43
  "peerDependencies": {
44
- "@opencode/plugin": "^2.0.26",
45
- "@opentui/core": ">=0.5.8",
46
- "@opentui/solid": ">=0.5.8",
44
+ "@opencode/plugin": "^2.2.0",
45
+ "@opentui/core": ">=0.7.0",
46
+ "@opentui/solid": ">=0.7.0",
47
47
  "solid-js": ">=1.9.0"
48
48
  }
49
49
  }