@jaggerxtrm/specialists 3.6.1 → 3.6.3

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.
@@ -118,7 +118,7 @@ The specialists orchestration model uses three levels:
118
118
  | Term | Definition | Persisted? | Merge scope |
119
119
  |------|------------|:----------:|:-----------:|
120
120
  | **Job** | One specialist run (atomic execution unit) | Yes (SQLite + files) | — |
121
- | **Chain** | Worktree lineage: executor reviewer fix re-review (seeded by edit-capable specialist) | Yes (`worktree_owner_job_id`) | `sp merge <chain-root>` |
121
+ | **Chain** | Worktree lineage: all specialists sharing one workspace from first dispatch to merge (explorer executor → reviewer → fix) | Yes (`worktree_owner_job_id`) | `sp merge <chain-root>` |
122
122
  | **Epic** | Top merge-gated identity that owns chains across stages | Yes (`epic_runs` table) | `sp epic merge <epic>` |
123
123
  | **Wave** | Human shorthand for dispatch batches ("Wave 1", "Wave 2b") — **speech only, NOT persisted** | No | — |
124
124
 
@@ -503,6 +503,47 @@ Only dispatch a new fix executor when the original specialist is dead (crashed,
503
503
 
504
504
  ---
505
505
 
506
+ ## Chain Lifecycle — Members Are Alive Until Merge
507
+
508
+ A chain is not just a worktree — it is a **living group of specialists** sharing one workspace. All members of a chain are alive (running or waiting) until the chain is merged or abandoned. Treat chain members as a unit.
509
+
510
+ ### Rules
511
+
512
+ 1. **Never kill individual chain members prematurely.** A chain may include explorer, overthinker, executor, reviewer — all sharing one worktree via `--job`. Do not `sp stop` any member while the chain is active, unless the member has crashed or is context-exhausted (>80%).
513
+ 2. **The chain is alive until merge.** From first dispatch (even if it's a READ_ONLY explorer) through reviewer PASS and executor commit — the chain is one living unit. Members stay in `waiting` between turns.
514
+ 3. **Resume, don't re-dispatch.** When a chain member needs to act again (executor fixing reviewer findings, overthinker answering follow-ups), use `sp resume` on the existing member. Only dispatch a replacement if the original is dead.
515
+ 4. **Merge kills the chain.** When `sp merge` or `sp epic merge` publishes a chain's branch, all chain members become obsolete. *(Future: `sp merge` will auto-stop all chain members on successful merge — no manual cleanup needed.)*
516
+ 5. **Stop order matters (until auto-cleanup).** When manually stopping chain members after merge: stop dependents first (reviewer), then the chain owner (executor/explorer). This prevents race conditions with resume paths.
517
+
518
+ ### Chain member states
519
+
520
+ | Member state | Meaning | Action |
521
+ |-------------|---------|--------|
522
+ | `running` | Actively working | Wait or steer |
523
+ | `waiting` | Idle, retains full context | Resume when needed |
524
+ | `done` | Finished its turn, output appended | Leave alone — chain may still need it |
525
+ | `error` | Crashed or failed | May need replacement dispatch |
526
+
527
+ ### What "don't kill" means in practice
528
+
529
+ ```bash
530
+ # BAD — killing executor before review cycle completes
531
+ sp stop exec-job # ✗ kills resume path, risks uncommitted work
532
+
533
+ # BAD — killing overthinker before executor uses its output
534
+ sp stop overthinker-job # ✗ loses context if follow-up questions arise
535
+
536
+ # GOOD — chain completes naturally
537
+ sp resume exec-job "Reviewer PASS. Commit your changes."
538
+ # verify commit...
539
+ sp merge unitAI-impl # publishes branch
540
+ # THEN stop members (future: auto-stopped by merge)
541
+ sp stop rev-job
542
+ sp stop exec-job
543
+ ```
544
+
545
+ ---
546
+
506
547
  ## Merge Protocol — Epic Publication
507
548
 
508
549
  The orchestrator owns merge timing, but **no longer performs manual git merges**. Use `sp epic merge` or `sp merge` instead.
@@ -23,7 +23,7 @@
23
23
  "output_type": "analysis",
24
24
  "permission_required": "READ_ONLY",
25
25
  "max_retries": 0,
26
- "interactive": false
26
+ "interactive": true
27
27
  },
28
28
  "prompt": {
29
29
  "system": "You are a codebase explorer specialist with access to the GitNexus knowledge graph.\nYour job is to analyze codebases deeply and provide clear, structured answers about\narchitecture, patterns, and code organization.\n\n## Primary Approach — GitNexus (use when indexed)\n\nStart here for any codebase. GitNexus gives you call chains, execution flows,\nand symbol relationships that grep/find cannot provide:\n\n1. Read `gitnexus://repo/{name}/context`\n → Stats, staleness check. If stale, fall back to bash.\n2. `gitnexus_query({query: \"<what you want to understand>\"})`\n → Find execution flows and related symbols grouped by process.\n3. `gitnexus_context({name: \"<symbol>\"})`\n → 360-degree view: callers, callees, processes the symbol participates in.\n4. Read `gitnexus://repo/{name}/clusters`\n → Functional areas with cohesion scores (architectural map).\n5. Read `gitnexus://repo/{name}/process/{name}`\n → Step-by-step execution trace for a specific flow.\n\n## Fallback Approach — Bash/Grep\n\nUse when GitNexus is unavailable or index is stale:\n- `find`, `tree`, `grep -r` for structure discovery\n- Read key files: package.json, tsconfig.json, README.md, src/index.ts\n- Trace imports manually to understand layer dependencies\n\n## Output Format\n\nAlways provide:\n1. **Summary** (2-3 sentences)\n2. **Architecture overview** — layers, modules, key patterns\n3. **Execution flows** (GitNexus) or **Directory map** (fallback)\n4. **Key symbols** — entry points, central hubs, important interfaces\n5. **Answer** — direct response to the specific question\n\nSTRICT CONSTRAINTS:\n- You MUST NOT edit, write, or modify any files.\n- Read-only: bash (read-only commands), grep, find, ls, GitNexus tools only.\n- If you find something worth fixing, REPORT it — do not fix it.\nEFFICIENCY RULE: Stop using tools and write your final answer after at most 12 tool calls.\n",
package/dist/index.js CHANGED
@@ -21248,6 +21248,26 @@ class SqliteClient {
21248
21248
  return events;
21249
21249
  }, "readEvents");
21250
21250
  }
21251
+ readLatestToolEvent(jobId) {
21252
+ return withRetry(() => {
21253
+ const row = this.db.query(`
21254
+ SELECT seq, event_json FROM specialist_events
21255
+ WHERE job_id = ? AND type = 'tool'
21256
+ ORDER BY seq DESC, id DESC
21257
+ LIMIT 1;
21258
+ `).get(jobId);
21259
+ if (!row?.event_json)
21260
+ return null;
21261
+ try {
21262
+ const parsed = JSON.parse(row.event_json);
21263
+ if (parsed.type !== "tool")
21264
+ return null;
21265
+ return typeof parsed.seq === "number" ? parsed : { ...parsed, seq: row.seq };
21266
+ } catch {
21267
+ return null;
21268
+ }
21269
+ }, "readLatestToolEvent");
21270
+ }
21251
21271
  readResult(jobId) {
21252
21272
  return withRetry(() => {
21253
21273
  const row = this.db.query("SELECT output FROM specialist_results WHERE job_id = ? LIMIT 1").get(jobId);
@@ -22580,8 +22600,14 @@ class Supervisor {
22580
22600
  });
22581
22601
  if (timelineEvent) {
22582
22602
  appendTimelineEvent(timelineEvent);
22583
- if (eventType === "tool_execution_end" && toolCallId) {
22584
- activeToolCalls.delete(toolCallId);
22603
+ if (eventType === "tool_execution_end") {
22604
+ if (toolCallId) {
22605
+ activeToolCalls.delete(toolCallId);
22606
+ } else {
22607
+ latestUncorrelatedToolState = undefined;
22608
+ }
22609
+ const nextActiveTool = activeToolCalls.values().next().value?.tool;
22610
+ setStatus({ current_tool: nextActiveTool });
22585
22611
  }
22586
22612
  } else if (eventType === "text" && !textLogged) {
22587
22613
  textLogged = true;
@@ -22734,6 +22760,7 @@ class Supervisor {
22734
22760
  }
22735
22761
  }
22736
22762
  }
22763
+ setStatus({ current_tool: undefined });
22737
22764
  });
22738
22765
  latestOutput = result.output;
22739
22766
  mkdirSync3(this.jobDir(id), { recursive: true });
@@ -30582,6 +30609,49 @@ function readStatusesFromFiles(jobsDir) {
30582
30609
  }
30583
30610
  return statuses.sort((a, b) => b.started_at_ms - a.started_at_ms);
30584
30611
  }
30612
+ function readLastToolEventFromFile(jobsDir, jobId) {
30613
+ const eventsPath = join20(jobsDir, jobId, "events.jsonl");
30614
+ if (!existsSync18(eventsPath))
30615
+ return;
30616
+ try {
30617
+ const lines = readFileSync14(eventsPath, "utf-8").split(`
30618
+ `);
30619
+ for (let index = lines.length - 1;index >= 0; index -= 1) {
30620
+ const line = lines[index]?.trim();
30621
+ if (!line)
30622
+ continue;
30623
+ const parsed = parseTimelineEvent(line);
30624
+ if (!parsed || parsed.type !== "tool")
30625
+ continue;
30626
+ return parsed;
30627
+ }
30628
+ } catch {
30629
+ return;
30630
+ }
30631
+ return;
30632
+ }
30633
+ function resolveDerivedCurrentTool(status, jobsDir, sqliteClient) {
30634
+ let lastToolEvent;
30635
+ try {
30636
+ lastToolEvent = sqliteClient?.readLatestToolEvent(status.id) ?? undefined;
30637
+ } catch {
30638
+ lastToolEvent = undefined;
30639
+ }
30640
+ if (!lastToolEvent) {
30641
+ lastToolEvent = readLastToolEventFromFile(jobsDir, status.id);
30642
+ }
30643
+ if (!lastToolEvent)
30644
+ return status.current_tool;
30645
+ if (lastToolEvent.phase === "start")
30646
+ return lastToolEvent.tool;
30647
+ return;
30648
+ }
30649
+ function enrichStatusesWithDerivedCurrentTool(statuses, jobsDir, sqliteClient) {
30650
+ return statuses.map((status) => ({
30651
+ ...status,
30652
+ current_tool: resolveDerivedCurrentTool(status, jobsDir, sqliteClient)
30653
+ }));
30654
+ }
30585
30655
  function loadStatuses() {
30586
30656
  const sqliteClient = createObservabilitySqliteClient();
30587
30657
  const jobsDir = resolveJobsDir();
@@ -30589,10 +30659,7 @@ function loadStatuses() {
30589
30659
  try {
30590
30660
  const sqliteStatuses = sqliteClient?.listStatuses() ?? [];
30591
30661
  if (sqliteStatuses.length === 0) {
30592
- return fileStatuses;
30593
- }
30594
- if (fileStatuses.length === 0) {
30595
- return sqliteStatuses.sort((a, b) => b.started_at_ms - a.started_at_ms);
30662
+ return enrichStatusesWithDerivedCurrentTool(fileStatuses, jobsDir, sqliteClient).sort((a, b) => b.started_at_ms - a.started_at_ms);
30596
30663
  }
30597
30664
  const merged = new Map;
30598
30665
  for (const status of fileStatuses)
@@ -30603,9 +30670,9 @@ function loadStatuses() {
30603
30670
  merged.set(status.id, status);
30604
30671
  }
30605
30672
  }
30606
- return [...merged.values()].sort((a, b) => b.started_at_ms - a.started_at_ms);
30673
+ return enrichStatusesWithDerivedCurrentTool([...merged.values()], jobsDir, sqliteClient).sort((a, b) => b.started_at_ms - a.started_at_ms);
30607
30674
  } catch {
30608
- return fileStatuses;
30675
+ return enrichStatusesWithDerivedCurrentTool(fileStatuses, jobsDir, sqliteClient).sort((a, b) => b.started_at_ms - a.started_at_ms);
30609
30676
  } finally {
30610
30677
  sqliteClient?.close();
30611
30678
  }
@@ -31284,6 +31351,7 @@ var init_ps = __esm(() => {
31284
31351
  init_supervisor();
31285
31352
  init_job_root();
31286
31353
  init_observability_sqlite();
31354
+ init_timeline_events();
31287
31355
  init_node_resolve();
31288
31356
  init_epic_readiness();
31289
31357
  ACTIVE_STATES = ["starting", "running", "waiting"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaggerxtrm/specialists",
3
- "version": "3.6.1",
3
+ "version": "3.6.3",
4
4
  "description": "OmniSpecialist — 7-tool MCP orchestration layer powered by the Specialist System. Discover and execute .specialist.yaml files across project/user/system scopes via pi.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",