@openthink/team 0.0.3 → 0.0.4

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.
@@ -61,6 +61,8 @@ Write the comment in this shape:
61
61
  - <bullet 2>
62
62
  ```
63
63
 
64
+ If your appended system context flags the **AGT-107 haiku-downshift heuristic** as active (a `# Product agent: haiku-downshift heuristic active` block), use the header `### YYYY-MM-DD — Product agent (haiku-downshift)` instead of the standard form. The runner has already spawned you on Haiku 4.5; the suffix makes the heuristic visible in the ticket's audit trail.
65
+
64
66
  ## Phase 3 — Engineering agent (state: refined → spike phase)
65
67
 
66
68
  **Step 0 — Workspace is already prepared.** When `oteam assign` spawned you against a repo-bound ticket, it already cloned `/tmp/open-team-issues/<ticket-id-lowercased>/repo` (from the stamp server when `stamp.enforce: true` is set in `~/.open-team/config.json`; from GitHub otherwise, or when `--no-stamp` was passed) and set your cwd to it. Confirm with `pwd` and `git remote -v`; for stamp-governed repos you should see exactly one remote, `origin`, pointing at `ssh://git@<stamp-host>:<port>/srv/git/<basename>.git`.
package/dist/index.js CHANGED
@@ -250,6 +250,7 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
250
250
  // src/lib/models.ts
251
251
  var ROLE_PIPELINE_MODEL = "claude-opus-4-7";
252
252
  var NORMALISER_MODEL = "claude-sonnet-4-6";
253
+ var HAIKU_PRODUCT_MODEL = "claude-haiku-4-5";
253
254
  var PHASES = ["product", "spike", "implementation", "qa"];
254
255
  var DEFAULT_MODELS = {
255
256
  product: "claude-sonnet-4-6",
@@ -281,6 +282,55 @@ function resolveRoleModel(state, models) {
281
282
  if (pinned && pinned.length > 0) return pinned;
282
283
  return ROLE_PIPELINE_MODEL;
283
284
  }
285
+ function acceptanceCriteriaIsPopulated(body) {
286
+ const lines = body.split("\n");
287
+ let inSection = false;
288
+ let inHtmlComment = false;
289
+ const numberedBullet = /^\s*\d+\.\s+\S/;
290
+ for (const line of lines) {
291
+ if (!inSection) {
292
+ if (/^##\s+Acceptance Criteria\s*$/.test(line)) {
293
+ inSection = true;
294
+ }
295
+ continue;
296
+ }
297
+ if (/^##\s+/.test(line)) return false;
298
+ let scan = line;
299
+ while (scan.length > 0) {
300
+ if (inHtmlComment) {
301
+ const close = scan.indexOf("-->");
302
+ if (close === -1) {
303
+ scan = "";
304
+ break;
305
+ }
306
+ scan = scan.slice(close + 3);
307
+ inHtmlComment = false;
308
+ } else {
309
+ const open = scan.indexOf("<!--");
310
+ if (open === -1) break;
311
+ const before = scan.slice(0, open);
312
+ if (numberedBullet.test(before)) return true;
313
+ const rest = scan.slice(open + 4);
314
+ const close = rest.indexOf("-->");
315
+ if (close === -1) {
316
+ inHtmlComment = true;
317
+ scan = "";
318
+ break;
319
+ }
320
+ scan = rest.slice(close + 3);
321
+ }
322
+ }
323
+ if (inHtmlComment) continue;
324
+ if (numberedBullet.test(scan)) return true;
325
+ }
326
+ return false;
327
+ }
328
+ function resolveModelForTicket(args) {
329
+ if (args.state === "triage" && args.sourceType === "manual" && args.productDownshift && acceptanceCriteriaIsPopulated(args.body)) {
330
+ return HAIKU_PRODUCT_MODEL;
331
+ }
332
+ return resolveRoleModel(args.state, args.models);
333
+ }
284
334
 
285
335
  // src/lib/normalise.ts
286
336
  var SYSTEM_PROMPT = `You normalise unstructured work-item payloads (GitHub issues, Linear tickets, etc.) into well-formed product-vault tickets.
@@ -458,6 +508,7 @@ import {
458
508
  } from "fs";
459
509
  import { homedir } from "os";
460
510
  import { basename, isAbsolute, resolve, join as join2 } from "path";
511
+ var DEFAULT_PRODUCT_DOWNSHIFT = true;
461
512
  function configDir() {
462
513
  return join2(homedir(), ".open-team");
463
514
  }
@@ -484,8 +535,12 @@ function writeConfig(config) {
484
535
  default: config.default,
485
536
  stamp: config.stamp
486
537
  };
487
- if (Object.keys(config.models).length > 0) {
488
- onDisk.models = config.models;
538
+ const onDiskModels = { ...config.models };
539
+ if (config.productDownshift !== DEFAULT_PRODUCT_DOWNSHIFT) {
540
+ onDiskModels.productDownshift = config.productDownshift;
541
+ }
542
+ if (Object.keys(onDiskModels).length > 0) {
543
+ onDisk.models = onDiskModels;
489
544
  }
490
545
  if (!config.telemetry.enabled) {
491
546
  onDisk.telemetry = config.telemetry;
@@ -499,6 +554,7 @@ function emptyConfig() {
499
554
  default: null,
500
555
  stamp: null,
501
556
  models: {},
557
+ productDownshift: DEFAULT_PRODUCT_DOWNSHIFT,
502
558
  telemetry: { enabled: true }
503
559
  };
504
560
  }
@@ -600,9 +656,15 @@ function normalise(parsed) {
600
656
  default: def,
601
657
  stamp: normaliseStamp(obj.stamp),
602
658
  models: normaliseModels(obj.models),
659
+ productDownshift: normaliseProductDownshift(obj.models),
603
660
  telemetry: normaliseTelemetry(obj.telemetry)
604
661
  };
605
662
  }
663
+ function normaliseProductDownshift(value) {
664
+ if (!value || typeof value !== "object") return DEFAULT_PRODUCT_DOWNSHIFT;
665
+ const v = value;
666
+ return v.productDownshift !== false;
667
+ }
606
668
  function normaliseTelemetry(value) {
607
669
  if (!value || typeof value !== "object") return { enabled: true };
608
670
  const v = value;
@@ -712,6 +774,15 @@ function seedDefaultModelsIfEmpty() {
712
774
  writeConfig(config);
713
775
  return { action: "seeded", models: config.models };
714
776
  }
777
+ function getProductDownshift() {
778
+ return readConfig().productDownshift;
779
+ }
780
+ function setProductDownshift(enabled) {
781
+ const config = readConfig();
782
+ config.productDownshift = enabled;
783
+ writeConfig(config);
784
+ return config.productDownshift;
785
+ }
715
786
  function getTelemetryEnabled() {
716
787
  return readConfig().telemetry.enabled;
717
788
  }
@@ -1299,6 +1370,28 @@ enforce: ${s.enforce ? "on" : "off"}
1299
1370
  const lines = PHASES.map((p) => `${p.padEnd(15)} ${m[p] ?? "(unset)"}`);
1300
1371
  process.stdout.write(lines.join("\n") + "\n");
1301
1372
  });
1373
+ models.command("product-downshift <on|off|show>").description(
1374
+ "Toggle the AGT-107 Haiku-downshift heuristic for well-formed manual tickets (default: on)"
1375
+ ).action((flag) => {
1376
+ const lower = flag.toLowerCase();
1377
+ if (lower === "show") {
1378
+ process.stdout.write(`${getProductDownshift() ? "on" : "off"}
1379
+ `);
1380
+ return;
1381
+ }
1382
+ if (lower !== "on" && lower !== "off") {
1383
+ process.stderr.write(
1384
+ `oteam config models product-downshift: expected on|off|show, got "${flag}"
1385
+ `
1386
+ );
1387
+ process.exit(2);
1388
+ }
1389
+ const next = setProductDownshift(lower === "on");
1390
+ process.stdout.write(
1391
+ `\u2705 models.productDownshift = ${next ? "on" : "off"}
1392
+ `
1393
+ );
1394
+ });
1302
1395
  const telemetry = new Command("telemetry").description(
1303
1396
  "Manage per-phase telemetry recording (default: on)"
1304
1397
  );
@@ -2411,7 +2504,7 @@ function buildTicketCommand() {
2411
2504
  // src/role-pipeline/runner.ts
2412
2505
  import { spawnSync as spawnSync4 } from "child_process";
2413
2506
  import { randomUUID } from "crypto";
2414
- import { writeFileSync as writeFileSync7 } from "fs";
2507
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
2415
2508
  import { tmpdir } from "os";
2416
2509
  import { resolve as resolve4, basename as basename5, dirname as dirname2, join as join14 } from "path";
2417
2510
 
@@ -2757,7 +2850,16 @@ async function assignTicket(opts) {
2757
2850
  }
2758
2851
  }
2759
2852
  const projectContext = loadProjectContext(resolvedVault.path, ticket.project);
2760
- const model = resolveRoleModel(ticket.state, config.models);
2853
+ const ticketBody = readTicketBody(ticketPath);
2854
+ const model = resolveModelForTicket({
2855
+ state: ticket.state,
2856
+ sourceType: ticket.source.type,
2857
+ body: ticketBody,
2858
+ productDownshift: config.productDownshift,
2859
+ models: config.models
2860
+ });
2861
+ const haikuDownshift = model === HAIKU_PRODUCT_MODEL && ticket.state === "triage";
2862
+ const systemPrompt = composeSystemPrompt(ticket.id, projectContext, haikuDownshift);
2761
2863
  const phase = phaseForState(ticket.state);
2762
2864
  const telemetry = phase !== null && getTelemetryEnabled() ? {
2763
2865
  ticketId: ticket.id,
@@ -2771,7 +2873,7 @@ async function assignTicket(opts) {
2771
2873
  claudePath,
2772
2874
  ticketPath,
2773
2875
  resolvedVault.path,
2774
- projectContext,
2876
+ systemPrompt,
2775
2877
  workspace,
2776
2878
  model,
2777
2879
  telemetry
@@ -2790,7 +2892,7 @@ async function assignTicket(opts) {
2790
2892
  claudePath,
2791
2893
  ticketPath,
2792
2894
  resolvedVault.path,
2793
- projectContext,
2895
+ systemPrompt,
2794
2896
  workspace,
2795
2897
  model,
2796
2898
  telemetry
@@ -2808,7 +2910,7 @@ async function assignTicket(opts) {
2808
2910
  const escapedTicket = shellEscape(ticketPath);
2809
2911
  const slashPrompt = `/assign-ticket ${escapedTicket}`;
2810
2912
  const escapedPrompt = shellEscape(slashPrompt);
2811
- const projectFlag = projectContext ? ` --append-system-prompt "$(cat '${shellEscape(projectContext.tmpFile)}')"` : "";
2913
+ const projectFlag = systemPrompt ? ` --append-system-prompt "$(cat '${shellEscape(systemPrompt.tmpFile)}')"` : "";
2812
2914
  const sessionFlag = telemetry ? ` --session-id '${shellEscape(telemetry.sessionId)}'` : "";
2813
2915
  const claudeCmd = `'${escapedClaude}' --dangerously-skip-permissions --model ${shellEscape(model)}${sessionFlag}${projectFlag} '${escapedPrompt}'`;
2814
2916
  const telemetryTail = telemetry ? buildTelemetryTail({
@@ -2845,7 +2947,7 @@ function buildTelemetryTail(input) {
2845
2947
  ].join(" ");
2846
2948
  return `; EC=$?; ${oteam} telemetry record ${args} >/dev/null 2>&1 || true; exit "$EC"`;
2847
2949
  }
2848
- function runInline(claudePath, ticketPath, vaultPath, projectContext, workspace, model, telemetry) {
2950
+ function runInline(claudePath, ticketPath, vaultPath, systemPrompt, workspace, model, telemetry) {
2849
2951
  const args = [
2850
2952
  "--dangerously-skip-permissions",
2851
2953
  "--model",
@@ -2854,8 +2956,8 @@ function runInline(claudePath, ticketPath, vaultPath, projectContext, workspace,
2854
2956
  if (telemetry) {
2855
2957
  args.push("--session-id", telemetry.sessionId);
2856
2958
  }
2857
- if (projectContext) {
2858
- args.push("--append-system-prompt", projectContext.content);
2959
+ if (systemPrompt) {
2960
+ args.push("--append-system-prompt", systemPrompt.content);
2859
2961
  }
2860
2962
  args.push(`/assign-ticket ${ticketPath}`);
2861
2963
  const cwd = workspace?.path ?? process.cwd();
@@ -2901,12 +3003,39 @@ function loadProjectContext(vaultPath, projectId) {
2901
3003
  );
2902
3004
  return null;
2903
3005
  }
2904
- const content = formatProjectContextForPrompt(project);
2905
- const safeId = projectId.replace(/[^a-zA-Z0-9._-]/g, "_");
2906
- const tmpFile = join14(tmpdir(), `oteam-project-${safeId}.md`);
3006
+ return formatProjectContextForPrompt(project);
3007
+ }
3008
+ function composeSystemPrompt(ticketId, projectContext, haikuDownshift) {
3009
+ const parts = [];
3010
+ if (projectContext) parts.push(projectContext);
3011
+ if (haikuDownshift) parts.push(haikuDownshiftPromptHint());
3012
+ if (parts.length === 0) return null;
3013
+ const content = parts.join("\n\n");
3014
+ const safeId = ticketId.replace(/[^a-zA-Z0-9._-]/g, "_");
3015
+ const tmpFile = join14(tmpdir(), `oteam-prompt-${safeId}.md`);
2907
3016
  writeFileSync7(tmpFile, content, "utf8");
2908
3017
  return { tmpFile, content };
2909
3018
  }
3019
+ function haikuDownshiftPromptHint() {
3020
+ return [
3021
+ "# Product agent: haiku-downshift heuristic active",
3022
+ "",
3023
+ "AGT-107: this ticket is a well-formed manual ticket (source.type=manual + populated `## Acceptance Criteria`), so the runner spawned you on Haiku 4.5 instead of the configured Product model. The heuristic exists to handle structural-cleanup cases cheaply; full synthesis still belongs on the configured Product model.",
3024
+ "",
3025
+ "When you advance the ticket, write the comment header as:",
3026
+ "",
3027
+ " ### YYYY-MM-DD \u2014 Product agent (haiku-downshift)",
3028
+ "",
3029
+ "instead of the standard `### YYYY-MM-DD \u2014 Product agent`. That makes the heuristic visible in the ticket's audit trail."
3030
+ ].join("\n");
3031
+ }
3032
+ function readTicketBody(path) {
3033
+ try {
3034
+ return readFileSync9(path, "utf8");
3035
+ } catch {
3036
+ return "";
3037
+ }
3038
+ }
2910
3039
  function findToolOnPath(name) {
2911
3040
  const r = spawnSync4("/usr/bin/env", ["which", name], { encoding: "utf8" });
2912
3041
  if (r.status !== 0) return null;