@oqtopus-team/pi-qdash 0.1.10 → 0.1.11-beta.20

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
@@ -16,7 +16,7 @@ It uses [`@oqtopus-team/qdash-client`](https://www.npmjs.com/package/@oqtopus-te
16
16
 
17
17
  ## Key Features
18
18
 
19
- - **Read-only investigation**: dedicated tools to inspect chips, qubits, couplings, calibration task results, task knowledge (`qdash_get_task_knowledge`), executions, issues, and Forum posts. Secrets are redacted from tool output.
19
+ - **Read-only investigation**: dedicated tools to inspect chips, qubits, couplings, calibration task results, task knowledge (`qdash_get_task_knowledge`), executions, and Forum posts. Secrets are redacted from tool output.
20
20
  - **Dashboards and insights**: compact dashboards, triage overviews, target-level incident reports, degradation reports, and wiring insights.
21
21
  - **Agent calibration workflow**: create agent sessions, submit and track agent actions, and commit/apply calibration candidates. Write operations are approval-gated.
22
22
  - **Calibration pipelines**: compose a declarative pipeline spec from QDash's step catalog (`qdash_get_pipeline_catalog`), validate it (`qdash_plan_pipeline`), and run it as one execution (`qdash_run_pipeline`, approval-gated).
@@ -44,12 +44,10 @@ export function qdashObjectLinks(client: QDashClient, object: Record<string, unk
44
44
  const executionId = firstString(object, ["execution_id", "executionId"]);
45
45
  const chipId = firstString(object, ["chip_id", "chipId"]);
46
46
  const postId = firstString(object, ["post_id", "forum_post_id", "id"]);
47
- const issueId = firstString(object, ["issue_id"]);
48
47
  const sessionId = firstString(object, ["session_id", "sessionId"]);
49
48
  if (taskId) links.task_result = qdashWebUrl(client, `/task-results/${encodeURIComponent(taskId)}`);
50
49
  if (executionId) links.execution = qdashWebUrl(client, executionPagePath(executionId, chipId));
51
50
  if (postId) links.forum_post = qdashWebUrl(client, forumPostPagePath(postId));
52
- if (issueId) links.issue = qdashWebUrl(client, `/issues/${encodeURIComponent(issueId)}`);
53
51
  if (sessionId) links.agent_session = qdashWebUrl(client, `/agent-sessions/${encodeURIComponent(sessionId)}`);
54
52
  return links;
55
53
  }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Evidence text for pi-qcaleval's `qcal_evaluate`: what a task result's
3
+ * measurement settings were, read from the run parameters QDash records.
4
+ *
5
+ * The evaluation model is meant to read the plot, so the context names the
6
+ * task, the target and the sweep (what was varied, over which range, in how
7
+ * many points), not the fitted results. The numbers come from the recorded
8
+ * parameters, so they are exact and need no hand-written summary.
9
+ */
10
+
11
+ /** One parameter as QDash records it on a task result. */
12
+ export interface RecordedParameter {
13
+ value: unknown;
14
+ unit: string;
15
+ value_type: string;
16
+ description: string;
17
+ }
18
+
19
+ /** A swept parameter: the axis a plot was taken along. */
20
+ export interface Sweep {
21
+ name: string;
22
+ unit: string;
23
+ description: string;
24
+ /** How the points are spaced. */
25
+ spacing: "linear" | "log" | "list";
26
+ /** First and last point, in the parameter's unit. */
27
+ start: number;
28
+ stop: number;
29
+ points: number;
30
+ /** Spacing between points for a linear sweep. */
31
+ step?: number;
32
+ }
33
+
34
+ /** The recorded parameters of a task result, as a name → record map. */
35
+ export function recordedParameters(value: unknown): Record<string, RecordedParameter> {
36
+ const output: Record<string, RecordedParameter> = {};
37
+ const input = value && typeof value === "object" ? (value as Record<string, unknown>) : {};
38
+ for (const [name, raw] of Object.entries(input)) {
39
+ if (raw && typeof raw === "object" && "value" in raw) {
40
+ const record = raw as Record<string, unknown>;
41
+ output[name] = {
42
+ value: record.value,
43
+ unit: typeof record.unit === "string" ? record.unit : "",
44
+ value_type: typeof record.value_type === "string" ? record.value_type : "",
45
+ description: typeof record.description === "string" ? record.description : "",
46
+ };
47
+ } else if (typeof raw === "number" || typeof raw === "string" || typeof raw === "boolean" || Array.isArray(raw)) {
48
+ output[name] = { value: raw, unit: "", value_type: Array.isArray(raw) ? "list" : typeof raw, description: "" };
49
+ }
50
+ }
51
+ return output;
52
+ }
53
+
54
+ function numbers(value: unknown): number[] | undefined {
55
+ if (!Array.isArray(value)) return undefined;
56
+ const list = value.map((item) => (typeof item === "number" ? item : typeof item === "string" ? Number(item) : Number.NaN));
57
+ return list.length > 0 && list.every((item) => Number.isFinite(item)) ? list : undefined;
58
+ }
59
+
60
+ /**
61
+ * The sweeps among the recorded parameters, in record order, read the way
62
+ * QDash's `get_value` expands them: `range` / `np.arange` as (start, stop,
63
+ * step), `np.linspace` as (start, stop, num), `np.logspace` as (log10 start,
64
+ * log10 stop, num), and `list` as the points themselves.
65
+ */
66
+ export function sweepsOf(parameters: Record<string, RecordedParameter>): Sweep[] {
67
+ const sweeps: Sweep[] = [];
68
+ for (const [name, parameter] of Object.entries(parameters)) {
69
+ const values = numbers(parameter.value);
70
+ if (!values) continue;
71
+ const base = { name, unit: parameter.unit, description: parameter.description };
72
+ const type = parameter.value_type;
73
+ if ((type === "range" || type === "np.arange") && values.length === 3) {
74
+ const [start, stop, step] = values as [number, number, number];
75
+ if (step === 0 || (stop - start) / step <= 0) continue;
76
+ const points = Math.ceil((stop - start) / step - 1e-9);
77
+ sweeps.push({ ...base, spacing: "linear", start, stop: start + (points - 1) * step, points, step });
78
+ } else if (type === "np.linspace" && values.length === 3) {
79
+ const [start, stop, num] = values as [number, number, number];
80
+ const points = Math.max(1, Math.round(num));
81
+ sweeps.push({ ...base, spacing: "linear", start, stop, points, ...(points > 1 ? { step: (stop - start) / (points - 1) } : {}) });
82
+ } else if (type === "np.logspace" && values.length === 3) {
83
+ const [start, stop, num] = values as [number, number, number];
84
+ sweeps.push({ ...base, spacing: "log", start: 10 ** start, stop: 10 ** stop, points: Math.max(1, Math.round(num)) });
85
+ } else if (type === "list" || type === "" || values.length !== 3) {
86
+ if (values.length < 2) continue;
87
+ sweeps.push({ ...base, spacing: "list", start: Math.min(...values), stop: Math.max(...values), points: values.length });
88
+ }
89
+ }
90
+ return sweeps;
91
+ }
92
+
93
+ /** A number for the model: up to four significant digits, no trailing zeros. */
94
+ export function formatNumber(value: number): string {
95
+ if (!Number.isFinite(value)) return String(value);
96
+ if (Number.isInteger(value) && Math.abs(value) < 1e15) return String(value);
97
+ const text = Math.abs(value) >= 1e6 || Math.abs(value) < 1e-4 ? value.toExponential(3) : value.toPrecision(4);
98
+ return text.includes("e") ? text.replace(/\.?0+e/, "e") : text.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
99
+ }
100
+
101
+ function withUnit(value: number, unit: string): string {
102
+ const text = formatNumber(value);
103
+ return unit && unit !== "a.u." ? `${text} ${unit}` : text;
104
+ }
105
+
106
+ /** One sweep as text, e.g. "time_range 0 to 400 ns in steps of 8 ns (51 points)". */
107
+ export function formatSweep(sweep: Sweep): string {
108
+ const range = `${sweep.name} ${formatNumber(sweep.start)} to ${withUnit(sweep.stop, sweep.unit)}`;
109
+ if (sweep.spacing === "log") return `${range}, ${sweep.points} points log-spaced`;
110
+ if (sweep.spacing === "list") return `${range}, ${sweep.points} points`;
111
+ return sweep.step === undefined ? `${range}, ${sweep.points} points` : `${range} in steps of ${withUnit(sweep.step, sweep.unit)} (${sweep.points} points)`;
112
+ }
113
+
114
+ /** Scalar settings as "name value unit" items, numbers and short strings only. */
115
+ export function formatSettings(parameters: Record<string, RecordedParameter>, skip: ReadonlySet<string> = new Set()): string[] {
116
+ const items: string[] = [];
117
+ for (const [name, parameter] of Object.entries(parameters)) {
118
+ if (skip.has(name)) continue;
119
+ const value = parameter.value;
120
+ if (typeof value === "number") items.push(`${name} ${withUnit(value, parameter.unit)}`);
121
+ else if (typeof value === "boolean") items.push(`${name} ${value}`);
122
+ else if (typeof value === "string" && value.length <= 40) items.push(`${name} ${value}`);
123
+ }
124
+ return items;
125
+ }
126
+
127
+ export interface QcalContext {
128
+ taskName?: string;
129
+ status?: string;
130
+ targetKind?: string;
131
+ targetId?: string;
132
+ chipId?: string;
133
+ runParameters: Record<string, RecordedParameter>;
134
+ /** Calibration inputs (previous values), shown only on request. */
135
+ inputParameters?: Record<string, RecordedParameter>;
136
+ /** Fitted results, shown only on request. */
137
+ metrics?: Record<string, RecordedParameter>;
138
+ message?: string;
139
+ }
140
+
141
+ /**
142
+ * The context for `qcal_evaluate`: the task and target, then the sweep and
143
+ * the other run settings. Inputs and fitted results are added only when the
144
+ * caller asks, since the model is meant to read the plot.
145
+ */
146
+ export function qcalContextText(context: QcalContext): string {
147
+ let head = `Task ${context.taskName ?? "unknown"}`;
148
+ if (context.targetId) {
149
+ head += ` on ${context.targetKind && context.targetKind !== "system" ? `${context.targetKind} ${context.targetId}` : context.targetId}`;
150
+ }
151
+ if (context.chipId) head += ` (chip ${context.chipId})`;
152
+ if (context.status) head += `, status ${context.status}`;
153
+ const lines = [`${head}.`];
154
+ const sweeps = sweepsOf(context.runParameters);
155
+ if (sweeps.length > 0) lines.push(`Sweep: ${sweeps.map(formatSweep).join("; ")}.`);
156
+ const settings = formatSettings(context.runParameters, new Set(sweeps.map((sweep) => sweep.name)));
157
+ if (settings.length > 0) lines.push(`Settings: ${settings.join("; ")}.`);
158
+ const inputs = context.inputParameters ? formatSettings(context.inputParameters) : [];
159
+ if (inputs.length > 0) lines.push(`Calibration inputs: ${inputs.join("; ")}.`);
160
+ const metrics = context.metrics ? formatSettings(context.metrics) : [];
161
+ if (metrics.length > 0) lines.push(`Fitted results: ${metrics.join("; ")}.`);
162
+ if (context.message?.trim()) lines.push(`Task message: ${context.message.trim()}`);
163
+ return lines.join("\n");
164
+ }
@@ -25,6 +25,7 @@ import { toFigureToolResult, toTextToolResult, toToolResult } from "./lib/result
25
25
  import { ansi, boxLinesToWidth, boxed, formatNumber, textComponent } from "./lib/render.js";
26
26
  import { plotSeriesLines, timeseriesPlotComponent, timeseriesPoints } from "./lib/timeseries-plot.js";
27
27
  import { installQDashWriteGate } from "./lib/write-gate.js";
28
+ import { qcalContextText, recordedParameters, sweepsOf, type RecordedParameter, type Sweep } from "./lib/qcal-evidence.js";
28
29
  import { registerCalibrationPipelineTools } from "./tools/calibration-pipeline.js";
29
30
  import { registerTaskKnowledgeTool } from "./tools/task-knowledge.js";
30
31
  import { registerTimeseriesComparisonTool } from "./tools/timeseries.js";
@@ -52,7 +53,6 @@ type QDashQueryParams = {
52
53
  | "task_results"
53
54
  | "task_result"
54
55
  | "task_note"
55
- | "task_result_issues"
56
56
  | "qubit_latest"
57
57
  | "qubit_history"
58
58
  | "coupling_latest"
@@ -65,8 +65,6 @@ type QDashQueryParams = {
65
65
  | "files_tree"
66
66
  | "file_content"
67
67
  | "git_status"
68
- | "issues"
69
- | "issue_knowledge"
70
68
  | "flows"
71
69
  | "flow"
72
70
  | "flow_templates"
@@ -108,7 +106,6 @@ type QDashQueryParams = {
108
106
  endAt?: string;
109
107
  startFrom?: string;
110
108
  startTo?: string;
111
- isClosed?: boolean;
112
109
  withinHours?: number;
113
110
  limit?: number;
114
111
  skip?: number;
@@ -294,7 +291,6 @@ const QueryAction = Type.Union([
294
291
  Type.Literal("task_results"),
295
292
  Type.Literal("task_result"),
296
293
  Type.Literal("task_note"),
297
- Type.Literal("task_result_issues"),
298
294
  Type.Literal("qubit_latest"),
299
295
  Type.Literal("qubit_history"),
300
296
  Type.Literal("coupling_latest"),
@@ -307,8 +303,6 @@ const QueryAction = Type.Union([
307
303
  Type.Literal("files_tree"),
308
304
  Type.Literal("file_content"),
309
305
  Type.Literal("git_status"),
310
- Type.Literal("issues"),
311
- Type.Literal("issue_knowledge"),
312
306
  Type.Literal("flows"),
313
307
  Type.Literal("flow"),
314
308
  Type.Literal("flow_templates"),
@@ -371,7 +365,6 @@ const querySchema = Type.Object({
371
365
  endAt: Type.Optional(Type.String()),
372
366
  startFrom: Type.Optional(Type.String()),
373
367
  startTo: Type.Optional(Type.String()),
374
- isClosed: Type.Optional(Type.Boolean()),
375
368
  withinHours: Type.Optional(Type.Number()),
376
369
  limit: Type.Optional(Type.Number()),
377
370
  skip: Type.Optional(Type.Number()),
@@ -413,6 +406,14 @@ type CalibrationEvidence = {
413
406
  task?: Record<string, unknown>;
414
407
  metrics?: Record<string, number | string | boolean | null>;
415
408
  parameters?: Record<string, number | string | boolean | null>;
409
+ /** The run parameters as recorded, with units and value types. */
410
+ runParameters?: Record<string, RecordedParameter>;
411
+ /** The calibration inputs as recorded (previous values), with units. */
412
+ inputParameters?: Record<string, RecordedParameter>;
413
+ /** The output parameters as recorded (fitted results), with units. */
414
+ metricRecords?: Record<string, RecordedParameter>;
415
+ /** The swept parameters among them: the axes the plot was taken along. */
416
+ sweeps?: Sweep[];
416
417
  resultJson?: unknown;
417
418
  figures?: Array<Record<string, unknown>>;
418
419
  logs?: string[];
@@ -439,23 +440,24 @@ function firstStringField(object: Record<string, unknown>, keys: string[]): stri
439
440
  return undefined;
440
441
  }
441
442
 
442
- function qcalEvidenceText(evidence: CalibrationEvidence): string {
443
- const metrics = Object.entries(evidence.metrics ?? {}).slice(0, 8).map(([key, value]) => ` - ${key}: ${String(value)}`);
444
- const figures = evidence.figures ?? [];
445
- return boxed("QDash QCal Evidence", [
446
- `id ${evidence.id ?? "n/a"}`,
447
- `source ${evidence.source}`,
448
- `target ${evidence.target?.id ?? evidence.target?.couplingId ?? evidence.target?.qids ?? "n/a"}`,
449
- `task ${evidence.task?.name ?? "n/a"} (${evidence.task?.status ?? "unknown"})`,
450
- "",
451
- "Metrics",
452
- ...(metrics.length ? metrics : [" - none"]),
453
- "",
454
- `Figures ${figures.length}`,
455
- ...figures.map((figure, index) => ` - #${index + 1} ${String(figure.role ?? "plot")} ${String(figure.path ?? figure.url ?? "")}`),
456
- "",
457
- "Pass this summary as the context of qcal_evaluate; the embedded figure is the measured result to judge.",
458
- ]).join("\n");
443
+ /**
444
+ * The evidence text, used as the context of `qcal_evaluate`: the task, the
445
+ * target, the sweep and the run settings. Fitted results and calibration
446
+ * inputs are left out unless asked for, since the evaluation model is meant
447
+ * to read them off the plot.
448
+ */
449
+ function qcalEvidenceText(evidence: CalibrationEvidence, include: { inputs?: boolean; metrics?: boolean } = {}): string {
450
+ return qcalContextText({
451
+ taskName: typeof evidence.task?.name === "string" ? evidence.task.name : undefined,
452
+ status: typeof evidence.task?.status === "string" ? evidence.task.status : undefined,
453
+ targetKind: typeof evidence.target?.kind === "string" ? evidence.target.kind : undefined,
454
+ targetId: typeof evidence.target?.id === "string" ? evidence.target.id : undefined,
455
+ chipId: typeof evidence.target?.chipId === "string" ? evidence.target.chipId : undefined,
456
+ runParameters: evidence.runParameters ?? {},
457
+ ...(include.inputs ? { inputParameters: recordedParameters(evidence.inputParameters) } : {}),
458
+ ...(include.metrics ? { metrics: recordedParameters(evidence.metricRecords) } : {}),
459
+ message: typeof evidence.task?.message === "string" ? evidence.task.message : undefined,
460
+ });
459
461
  }
460
462
 
461
463
  function cleanQuery(values: Record<string, unknown> = {}): Record<string, unknown> {
@@ -578,25 +580,25 @@ async function buildDashboard(params: { profile?: string; configPath?: string; u
578
580
  const limit = params.limit ?? 5;
579
581
  const client = await makeClient(params);
580
582
  const chipId = await defaultChipId(client, params.chipId);
581
- const [chips, openIssues, recentExecutions, failedTaskResults, provenanceStats] = await Promise.allSettled([
583
+ const [chips, openThreads, recentExecutions, failedTaskResults, provenanceStats] = await Promise.allSettled([
582
584
  client.listChips(),
583
- client.api.listIssues({ is_closed: false, limit }),
585
+ client.listForumPosts({ chipId, status: "open", limit }),
584
586
  client.api.listExecutions({ chip_id: chipId, limit }),
585
587
  client.api.listTaskResults({ chip_id: chipId, status: "failed", limit }),
586
588
  client.getProvenanceStats(),
587
589
  ]);
588
590
  const value = <T>(result: PromiseSettledResult<T>): T | { error: string } => result.status === "fulfilled" ? result.value : { error: result.reason instanceof Error ? result.reason.message : String(result.reason) };
589
- const issuesPayload = value(openIssues);
591
+ const threadsPayload = value(openThreads);
590
592
  const executionsPayload = value(recentExecutions);
591
593
  const failedTasksPayload = value(failedTaskResults);
592
594
  return {
593
595
  context: { ...currentContext, profile: params.profile ?? currentContext.profile, chipId },
594
596
  chips: value(chips),
595
- openIssues: {
596
- count: payloadTotal(issuesPayload),
597
- shown: arrayFromPayload(issuesPayload).length,
598
- items: compactItems(issuesPayload, ["issue_id", "id", "title", "task_id", "is_closed", "severity", "created_at"], limit),
599
- raw: issuesPayload,
597
+ openThreads: {
598
+ count: payloadTotal(threadsPayload),
599
+ shown: arrayFromPayload(threadsPayload).length,
600
+ items: compactItems(threadsPayload, ["number", "title", "category", "target_type", "target_id", "created_at"], limit),
601
+ raw: threadsPayload,
600
602
  },
601
603
  recentExecutions: {
602
604
  count: payloadTotal(executionsPayload),
@@ -624,14 +626,14 @@ function dashboardLines(dashboard: Awaited<ReturnType<typeof buildDashboard>>, c
624
626
  const success = (text: string) => color ? ansi("32", text) : text;
625
627
  const warning = (text: string) => color ? ansi("33", text) : text;
626
628
  const error = (text: string) => color ? ansi("31", text) : text;
627
- const issueCount = dashboard.openIssues.count > 0 ? warning(String(dashboard.openIssues.count)) : success(String(dashboard.openIssues.count));
629
+ const threadCount = dashboard.openThreads.count > 0 ? warning(String(dashboard.openThreads.count)) : success(String(dashboard.openThreads.count));
628
630
  const failedCount = dashboard.failedTaskResults.count > 0 ? error(`${dashboard.failedTaskResults.shown}/${dashboard.failedTaskResults.count}`) : success(`${dashboard.failedTaskResults.shown}/${dashboard.failedTaskResults.count}`);
629
631
  return boxed("QDash Harness", [
630
632
  `${muted("profile")} ${accent(profile)} ${muted("chip")} ${accent(dashboard.context.chipId)} ${muted("session")} ${dim(session)}`,
631
- `${muted("chips")} ${success(String(chips.length))} ${muted("open issues")} ${issueCount} ${muted("executions")} ${accent(`${dashboard.recentExecutions.shown}/${dashboard.recentExecutions.count}`)} ${muted("failed tasks")} ${failedCount}`,
633
+ `${muted("chips")} ${success(String(chips.length))} ${muted("open threads")} ${threadCount} ${muted("executions")} ${accent(`${dashboard.recentExecutions.shown}/${dashboard.recentExecutions.count}`)} ${muted("failed tasks")} ${failedCount}`,
632
634
  "",
633
- accent("Open issues"),
634
- ...(dashboard.openIssues.items.length > 0 ? dashboard.openIssues.items.map((item, index) => ` ${warning(formatItem(item, `issue-${index + 1}`))}`) : [` ${dim("none")}`]),
635
+ accent("Open Forum threads"),
636
+ ...(dashboard.openThreads.items.length > 0 ? dashboard.openThreads.items.map((item, index) => ` ${warning(formatItem(item, `thread-${index + 1}`))}`) : [` ${dim("none")}`]),
635
637
  "",
636
638
  accent("Recent executions"),
637
639
  ...(dashboard.recentExecutions.items.length > 0 ? dashboard.recentExecutions.items.map((item, index) => ` ${muted(formatItem(item, `exec-${index + 1}`))}`) : [` ${dim("none")}`]),
@@ -645,7 +647,7 @@ function styledDashboardLines(dashboard: Awaited<ReturnType<typeof buildDashboar
645
647
  const chips = arrayFromPayload(dashboard.chips);
646
648
  const profile = dashboard.context.profile ?? "env/default";
647
649
  const session = dashboard.context.agentSessionId ? shortId(dashboard.context.agentSessionId) : "none";
648
- const issueColor = dashboard.openIssues.count > 0 ? "warning" : "success";
650
+ const threadColor = dashboard.openThreads.count > 0 ? "warning" : "success";
649
651
  const failedColor = dashboard.failedTaskResults.count > 0 ? "error" : "success";
650
652
  return [
651
653
  theme.fg("accent", theme.bold("QDash Harness")),
@@ -653,14 +655,14 @@ function styledDashboardLines(dashboard: Awaited<ReturnType<typeof buildDashboar
653
655
  "",
654
656
  [
655
657
  `${theme.fg("muted", "chips")} ${theme.fg("success", String(chips.length))}`,
656
- `${theme.fg("muted", "open issues")} ${theme.fg(issueColor, String(dashboard.openIssues.count))}`,
658
+ `${theme.fg("muted", "open threads")} ${theme.fg(threadColor, String(dashboard.openThreads.count))}`,
657
659
  `${theme.fg("muted", "executions")} ${theme.fg("accent", `${dashboard.recentExecutions.shown}/${dashboard.recentExecutions.count}`)}`,
658
660
  `${theme.fg("muted", "failed tasks")} ${theme.fg(failedColor, `${dashboard.failedTaskResults.shown}/${dashboard.failedTaskResults.count}`)}`,
659
661
  ].join(" "),
660
662
  "",
661
- theme.fg("borderAccent", "Open issues"),
662
- ...(dashboard.openIssues.items.length > 0
663
- ? dashboard.openIssues.items.map((item, index) => ` ${theme.fg("warning", formatItem(item, `issue-${index + 1}`))}`)
663
+ theme.fg("borderAccent", "Open Forum threads"),
664
+ ...(dashboard.openThreads.items.length > 0
665
+ ? dashboard.openThreads.items.map((item, index) => ` ${theme.fg("warning", formatItem(item, `thread-${index + 1}`))}`)
664
666
  : [` ${theme.fg("dim", "none")}`]),
665
667
  "",
666
668
  theme.fg("borderAccent", "Recent executions"),
@@ -871,7 +873,6 @@ type TargetOperationsReport = {
871
873
  latestTarget: unknown;
872
874
  recentResults: Record<string, unknown>[];
873
875
  failures: Record<string, unknown>[];
874
- issues: Record<string, unknown>[];
875
876
  forumPosts: Record<string, unknown>[];
876
877
  recommendation: RecommendedNextAction | null;
877
878
  };
@@ -922,19 +923,12 @@ async function buildTargetOperationsReport(params: { profile?: string; configPat
922
923
  const end = new Date();
923
924
  const start = new Date(end.getTime() - (params.withinHours ?? 168) * 3600_000);
924
925
  const limit = Math.max(1, Math.min(100, params.limit ?? 10));
925
- const [latestTarget, results, issues, forumPosts] = await Promise.all([
926
+ const [latestTarget, results, forumPosts] = await Promise.all([
926
927
  kind === "qubit" ? client.getChipQubit(chipId, id) : client.getChipCoupling(chipId, id),
927
928
  client.listTaskResults({ chipId, qid, couplingId, startAt: start.toISOString(), endAt: end.toISOString(), limit }),
928
- client.api.listIssues({ is_closed: false, limit: 100 }),
929
929
  client.listForumPosts({ chipId, status: "open", targetType: kind, targetId: id, limit }),
930
930
  ]);
931
931
  const recentResults = arrayFromPayload(results).filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object");
932
- const taskIds = new Set(recentResults.map((item) => firstString(item, ["task_id", "taskId"])).filter((item): item is string => Boolean(item)));
933
- const targetIssues = arrayFromPayload(issues).filter((item): item is Record<string, unknown> => {
934
- if (!item || typeof item !== "object") return false;
935
- const issue = item as Record<string, unknown>;
936
- return (firstString(issue, ["task_id", "taskId"]) && taskIds.has(firstString(issue, ["task_id", "taskId"])!)) || targetFromText(firstString(issue, ["title", "summary"]) ?? "") === label;
937
- });
938
932
  const summary: RecentCalibrationSummary = { context: { profile: params.profile ?? currentContext.profile, chipId, webBaseUrl: qdashWebBaseUrl(client) }, items: recentResults, groups: [] };
939
933
  const groups = new Map<string, Record<string, unknown>[]>();
940
934
  groups.set(label, recentResults);
@@ -946,7 +940,6 @@ async function buildTargetOperationsReport(params: { profile?: string; configPat
946
940
  latestTarget,
947
941
  recentResults,
948
942
  failures: recentResults.filter((item) => firstString(item, ["status"])?.toLowerCase() === "failed"),
949
- issues: targetIssues,
950
943
  forumPosts: forumPostsFromPayload(forumPosts),
951
944
  recommendation,
952
945
  };
@@ -965,7 +958,7 @@ function targetOperationsReportLines(report: TargetOperationsReport, color = fal
965
958
  };
966
959
  return boxed(`QDash Target Report ${report.target.label}`, [
967
960
  `${muted("profile")} ${accent(report.context.profile ?? "env/default")} ${muted("chip")} ${accent(report.context.chipId)}`,
968
- `${muted("results")} ${report.recentResults.length} ${muted("failures")} ${report.failures.length ? error(String(report.failures.length)) : "0"} ${muted("issues")} ${report.issues.length ? warn(String(report.issues.length)) : "0"} ${muted("forum")} ${report.forumPosts.length}`,
961
+ `${muted("results")} ${report.recentResults.length} ${muted("failures")} ${report.failures.length ? error(String(report.failures.length)) : "0"} ${muted("forum")} ${report.forumPosts.length ? warn(String(report.forumPosts.length)) : "0"}`,
969
962
  "",
970
963
  accent("Recent results"),
971
964
  ...(report.recentResults.length ? report.recentResults.slice(0, 10).map(resultLine) : [` ${muted("none")}`]),
@@ -1097,7 +1090,6 @@ type CalibrationValidation = {
1097
1090
  status: "passed" | "failed" | "needs_review" | "unknown";
1098
1091
  gates: Array<{ name: string; passed: boolean; detail: string }>;
1099
1092
  figurePaths: string[];
1100
- issues: Record<string, unknown>[];
1101
1093
  comparison?: unknown;
1102
1094
  next: string;
1103
1095
  links: Record<string, string>;
@@ -1109,8 +1101,6 @@ async function buildCalibrationValidation(params: { profile?: string; configPath
1109
1101
  const task = await client.getTaskResult(params.taskId) as unknown as Record<string, unknown>;
1110
1102
  const status = firstString(task, ["status"])?.toLowerCase() ?? "unknown";
1111
1103
  const figurePaths = taskFigurePaths(task, 20);
1112
- const issuesPayload = await client.api.getTaskResultIssues(params.taskId).catch(() => []);
1113
- const issues = arrayFromPayload(issuesPayload).filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object");
1114
1104
  const afterExecutionId = firstString(task, ["execution_id", "executionId"]);
1115
1105
  const comparison = params.beforeExecutionId && afterExecutionId
1116
1106
  ? await client.compareExecutions(params.beforeExecutionId, afterExecutionId).catch(() => undefined)
@@ -1119,18 +1109,17 @@ async function buildCalibrationValidation(params: { profile?: string; configPath
1119
1109
  const changedParameters = comparisonRecord ? firstNumber(comparisonRecord, ["changed_count"]) ?? (Array.isArray(comparisonRecord.changed_parameters) ? (comparisonRecord.changed_parameters as unknown[]).length : undefined) : undefined;
1120
1110
  const gates = [
1121
1111
  { name: "task completed", passed: ["completed", "success", "succeeded"].includes(status), detail: status },
1122
- { name: "no task issues", passed: issues.length === 0, detail: issues.length ? `${issues.length} issue(s) require review` : "none" },
1123
1112
  { name: "figures available", passed: figurePaths.length > 0, detail: figurePaths.length ? `${figurePaths.length} figure(s)` : "no figures attached" },
1124
1113
  ...(params.beforeExecutionId ? [{ name: "execution comparison", passed: comparison !== undefined, detail: comparison === undefined ? "comparison unavailable" : `${changedParameters ?? 0} changed parameter(s)` }] : []),
1125
1114
  ];
1126
- const passed = gates[0].passed && gates[1].passed;
1115
+ const figuresGate = gates.find((gate) => gate.name === "figures available");
1127
1116
  const comparisonGate = gates.find((gate) => gate.name === "execution comparison");
1128
- const validationStatus: CalibrationValidation["status"] = !gates[0].passed ? "failed" : issues.length > 0 || !gates[2].passed || comparisonGate?.passed === false ? "needs_review" : "passed";
1117
+ const validationStatus: CalibrationValidation["status"] = !gates[0].passed ? "failed" : figuresGate?.passed === false || comparisonGate?.passed === false ? "needs_review" : "passed";
1129
1118
  const next = validationStatus === "passed"
1130
1119
  ? "Inspect candidates and ask for explicit confirmation before commit/apply. Preserve evidence in the target Forum thread when appropriate."
1131
1120
  : validationStatus === "failed"
1132
1121
  ? "Stop the recovery sequence; inspect the task message and figures before another operational action."
1133
- : "Review the listed issues/figures before treating this result as authoritative.";
1122
+ : "Review the figures and execution comparison before treating this result as authoritative.";
1134
1123
  return {
1135
1124
  context: { profile: params.profile ?? currentContext.profile, chipId: firstString(task, ["chip_id", "chipId"]) ?? currentContext.chipId, webBaseUrl: qdashWebBaseUrl(client) },
1136
1125
  taskId: params.taskId,
@@ -1138,7 +1127,6 @@ async function buildCalibrationValidation(params: { profile?: string; configPath
1138
1127
  status: validationStatus,
1139
1128
  gates,
1140
1129
  figurePaths,
1141
- issues,
1142
1130
  comparison,
1143
1131
  next,
1144
1132
  links: qdashObjectLinks(client, task),
@@ -1240,9 +1228,8 @@ async function buildDashboardInsights(params: { profile?: string; configPath?: s
1240
1228
  const limit = params.limit ?? 30;
1241
1229
  const end = new Date();
1242
1230
  const start = new Date(end.getTime() - (params.withinHours ?? 168) * 3600_000);
1243
- const [forums, issues, failed, metrics] = await Promise.allSettled([
1231
+ const [forums, failed, metrics] = await Promise.allSettled([
1244
1232
  client.listForumPosts({ chipId, status: "open", limit }),
1245
- client.api.listIssues({ is_closed: false, limit }),
1246
1233
  client.api.listTaskResults({ chip_id: chipId, status: "failed", start_from: start.toISOString(), start_to: end.toISOString(), limit }),
1247
1234
  client.getChipMetrics(chipId),
1248
1235
  ]);
@@ -1260,13 +1247,6 @@ async function buildDashboardInsights(params: { profile?: string; configPath?: s
1260
1247
  addEvidence(insightMap, target, severity, title, `forum: ${title}`, "Open forum context should be reviewed before calibration changes.", qdashObjectLinks(client, post));
1261
1248
  }
1262
1249
 
1263
- for (const issue of arrayFromPayload(value(issues)).filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object")) {
1264
- const title = firstString(issue, ["title", "summary"]) ?? "open issue";
1265
- const target = targetFromText(title) ?? firstString(issue, ["qid"]);
1266
- if (!target) continue;
1267
- addEvidence(insightMap, target.startsWith("q") || target.startsWith("c") ? target : `q${target}`, "warning", title, `open issue: ${title}`, "Resolve or account for the open issue before trusting metrics.", qdashObjectLinks(client, issue));
1268
- }
1269
-
1270
1250
  for (const task of arrayFromPayload(value(failed)).filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object")) {
1271
1251
  const target = calibrationTarget(task);
1272
1252
  const taskName = firstString(task, ["task_name", "name"]) ?? "task";
@@ -1465,7 +1445,6 @@ async function executeQuery(params: QDashQueryParams) {
1465
1445
  case "task_results": return rawGet(client, "/task-results", { chip_id: params.chipId, task_name: params.taskName, qid: params.qid, coupling_id: params.couplingId, execution_id: params.executionId, username: params.username, status: params.status, start_from: params.startFrom ?? params.startAt, start_to: params.startTo ?? params.endAt, message_contains: params.messageContains, limit: params.limit, skip: params.skip });
1466
1446
  case "task_result": return client.getTaskResult(requireValue(params.taskId, "taskId"));
1467
1447
  case "task_note": return client.api.getTaskNote(requireValue(params.taskId, "taskId"));
1468
- case "task_result_issues": return client.api.getTaskResultIssues(requireValue(params.taskId, "taskId"));
1469
1448
  case "qubit_latest": return client.api.getLatestQubitTaskResults({ chip_id: await defaultChipId(client, params.chipId), task: requireValue(params.task ?? params.taskName, "task") });
1470
1449
  case "qubit_history": return rawGet(client, `/task-results/qubits/${pathPart(requireValue(params.qid, "qid"))}/history`, { chip_id: await defaultChipId(client, params.chipId), task: requireValue(params.task ?? params.taskName, "task"), date: requireValue(params.date, "date") });
1471
1450
  case "coupling_latest": return client.api.getLatestCouplingTaskResults({ chip_id: await defaultChipId(client, params.chipId), task: requireValue(params.task ?? params.taskName, "task") });
@@ -1478,8 +1457,6 @@ async function executeQuery(params: QDashQueryParams) {
1478
1457
  case "files_tree": return client.getFilesTree();
1479
1458
  case "file_content": return client.getFileContent(requireValue(params.path, "path"));
1480
1459
  case "git_status": return client.getGitStatus();
1481
- case "issues": return client.api.listIssues({ task_id: params.taskId, is_closed: params.isClosed, limit: params.limit, skip: params.skip });
1482
- case "issue_knowledge": return client.api.listIssueKnowledge({ status: params.status, task_name: params.taskName, limit: params.limit, skip: params.skip });
1483
1460
  case "flows": return client.listFlows();
1484
1461
  case "flow": return client.getFlow(requireValue(params.flowName, "flowName"));
1485
1462
  case "flow_templates": return client.listFlowTemplates();
@@ -1532,7 +1509,7 @@ export default function qdashExtension(pi: ExtensionAPI) {
1532
1509
  description: "Run common read-only QDash queries via @oqtopus-team/qdash-client.",
1533
1510
  promptSnippet: "Query QDash chips, cryostats, cooldown wiring, metrics, tasks, files, flows, executions, projects, forum posts, and provenance",
1534
1511
  promptGuidelines: [
1535
- "Prefer dedicated QDash tools such as qdash_list_chips, qdash_get_chip_metrics, qdash_list_task_results, qdash_list_issues, qdash_list_flows, and qdash_list_executions when they match the task.",
1512
+ "Prefer dedicated QDash tools such as qdash_list_chips, qdash_get_chip_metrics, qdash_list_task_results, qdash_list_forum_posts, qdash_list_flows, and qdash_list_executions when they match the task.",
1536
1513
  "Use qdash_query for read-only QDash data access not covered by a dedicated tool, instead of curl or scraping the UI.",
1537
1514
  "Ask for confirmation before using operational or write APIs; qdash_query intentionally exposes read-only operations only.",
1538
1515
  "Never expose QDash tokens, passwords, or Cloudflare Access secrets.",
@@ -1808,15 +1785,6 @@ export default function qdashExtension(pi: ExtensionAPI) {
1808
1785
  parameters: Type.Object({ ...connectionParams, taskId: Type.String() }),
1809
1786
  });
1810
1787
 
1811
- registerQueryTool({
1812
- name: "qdash_list_issues",
1813
- label: "QDash List Issues",
1814
- description: "List QDash issues with optional task/closed filters.",
1815
- promptSnippet: "List QDash issues",
1816
- action: "issues",
1817
- parameters: Type.Object({ ...connectionParams, taskId: Type.Optional(Type.String()), isClosed: Type.Optional(Type.Boolean()), limit: Type.Optional(Type.Number()), skip: Type.Optional(Type.Number()) }),
1818
- });
1819
-
1820
1788
  registerQueryTool({
1821
1789
  name: "qdash_list_flows",
1822
1790
  label: "QDash List Flows",
@@ -2094,21 +2062,24 @@ export default function qdashExtension(pi: ExtensionAPI) {
2094
2062
  pi.registerTool({
2095
2063
  name: "qdash_execute_agent_action",
2096
2064
  label: "QDash Execute Agent Action",
2097
- description: "Link/execute a QDash agent action from a source execution. This is a write operation and requires confirmation.",
2098
- promptSnippet: "Execute or link a QDash agent action after explicit user confirmation",
2099
- promptGuidelines: ["Use qdash_execute_agent_action only after the user confirms the exact source execution and action."],
2065
+ description: "Run an authorized QDash agent action's task the way the Task Workbench runs it: inputs from the current calibration state, outputs staged for the candidate gate. This is a write operation and requires confirmation.",
2066
+ promptSnippet: "Run a QDash agent action's task after explicit user confirmation",
2067
+ promptGuidelines: [
2068
+ "Use qdash_execute_agent_action only after the user confirms the action, its task, target, and overrides.",
2069
+ "The task runs on the current calibration state; pass sourceExecutionId only to record which execution the action was decided from.",
2070
+ ],
2100
2071
  parameters: Type.Object({
2101
2072
  ...connectionParams,
2102
2073
  sessionId: Type.String(),
2103
2074
  actionId: Type.String(),
2104
- sourceExecutionId: Type.String(),
2075
+ sourceExecutionId: Type.Optional(Type.String({ description: "Execution the action was decided from, recorded for provenance. The run does not restore it." })),
2105
2076
  updateParams: Type.Optional(Type.Boolean()),
2106
2077
  reconfigure: Type.Optional(Type.Boolean()),
2107
2078
  confirmWrite: Type.Optional(Type.Boolean()),
2108
2079
  }),
2109
- async execute(_toolCallId, params: ConfirmableParams & { profile?: string; configPath?: string; useEnv?: boolean; sessionId: string; actionId: string; sourceExecutionId: string; updateParams?: boolean; reconfigure?: boolean }, _signal, _onUpdate, ctx) {
2080
+ async execute(_toolCallId, params: ConfirmableParams & { profile?: string; configPath?: string; useEnv?: boolean; sessionId: string; actionId: string; sourceExecutionId?: string; updateParams?: boolean; reconfigure?: boolean }, _signal, _onUpdate, ctx) {
2110
2081
  if (!params.confirmWrite) {
2111
- if (!ctx.hasUI || !(await ctx.ui.confirm("Execute QDash agent action?", `Execute action '${params.actionId}' from execution '${params.sourceExecutionId}'?`))) {
2082
+ if (!ctx.hasUI || !(await ctx.ui.confirm("Execute QDash agent action?", `Run action '${params.actionId}' on the current calibration state? Outputs stay staged until a candidate is committed.`))) {
2112
2083
  throw new Error("qdash_execute_agent_action requires explicit confirmation");
2113
2084
  }
2114
2085
  }
@@ -2613,7 +2584,7 @@ export default function qdashExtension(pi: ExtensionAPI) {
2613
2584
  pi.registerTool({
2614
2585
  name: "qdash_build_qcal_evidence",
2615
2586
  label: "QDash Build QCal Evidence",
2616
- description: "Summarize a QDash task result (target, parameters, metrics, figure analysis) as evidence for pi-qcaleval's qcal_evaluate, returning the task figure as image content.",
2587
+ description: "Summarize a QDash task result (target, sweep and run settings) as evidence for pi-qcaleval's qcal_evaluate, returning the task figure as image content. Fitted results are left out unless includeMetrics is set.",
2617
2588
  promptSnippet: "Summarize a QDash calibration task result as evidence for qcal_evaluate",
2618
2589
  promptGuidelines: [
2619
2590
  "Use qdash_build_qcal_evidence before qcal_evaluate when the user asks to evaluate a QDash calibration task with pi-qcaleval; pass the returned summary as the context of qcal_evaluate.",
@@ -2627,8 +2598,10 @@ export default function qdashExtension(pi: ExtensionAPI) {
2627
2598
  includeFigureAnalysis: Type.Optional(Type.Boolean({ description: "Analyze Plotly JSON figures and include summaries/warnings in notes. Defaults to true." })),
2628
2599
  maxFigures: Type.Optional(Type.Number({ description: "Maximum image figures to embed. Defaults to 1." })),
2629
2600
  includeRawTaskResult: Type.Optional(Type.Boolean({ description: "Include the full raw task result in resultJson. Defaults to false." })),
2601
+ includeInputs: Type.Optional(Type.Boolean({ description: "Add the calibration inputs (previous values) to the evidence text. Defaults to false: the evaluation model reads the plot." })),
2602
+ includeMetrics: Type.Optional(Type.Boolean({ description: "Add the fitted output parameters to the evidence text. Defaults to false: the evaluation model reads the plot." })),
2630
2603
  }),
2631
- async execute(_toolCallId, params: { profile?: string; configPath?: string; useEnv?: boolean; taskId: string; includeFigures?: boolean; includeFigureAnalysis?: boolean; maxFigures?: number; includeRawTaskResult?: boolean }) {
2604
+ async execute(_toolCallId, params: { profile?: string; configPath?: string; useEnv?: boolean; taskId: string; includeFigures?: boolean; includeFigureAnalysis?: boolean; maxFigures?: number; includeRawTaskResult?: boolean; includeInputs?: boolean; includeMetrics?: boolean }) {
2632
2605
  const client = await makeClient(params);
2633
2606
  const task = await client.getTaskResult(params.taskId) as unknown as Record<string, unknown>;
2634
2607
  const figures: Array<Record<string, unknown>> = [];
@@ -2673,6 +2646,7 @@ export default function qdashExtension(pi: ExtensionAPI) {
2673
2646
 
2674
2647
  const couplingId = firstStringField(task, ["coupling_id", "couplingId", "qid"]);
2675
2648
  const qid = firstStringField(task, ["qid"]);
2649
+ const runParameters = recordedParameters(task.run_parameters);
2676
2650
  const evidence: CalibrationEvidence = {
2677
2651
  id: params.taskId,
2678
2652
  source: "external",
@@ -2690,9 +2664,14 @@ export default function qdashExtension(pi: ExtensionAPI) {
2690
2664
  startedAt: firstStringField(task, ["start_at", "startAt"]),
2691
2665
  completedAt: firstStringField(task, ["end_at", "endAt"]),
2692
2666
  status: firstStringField(task, ["status"]),
2667
+ message: typeof task.message === "string" && task.message.trim() ? task.message.trim() : undefined,
2693
2668
  },
2694
2669
  metrics: parameterValueRecord(task.output_parameters),
2695
2670
  parameters: { ...parameterValueRecord(task.input_parameters), ...parameterValueRecord(task.run_parameters) },
2671
+ runParameters,
2672
+ sweeps: sweepsOf(runParameters),
2673
+ inputParameters: recordedParameters(task.input_parameters),
2674
+ metricRecords: recordedParameters(task.output_parameters),
2696
2675
  figures: figures.length ? figures : undefined,
2697
2676
  notes: [
2698
2677
  typeof task.message === "string" ? `Task message: ${task.message}` : undefined,
@@ -2708,7 +2687,11 @@ export default function qdashExtension(pi: ExtensionAPI) {
2708
2687
  },
2709
2688
  };
2710
2689
 
2711
- const result = toTextToolResult(qcalEvidenceText(evidence), { evidence }, { tool: "qdash_build_qcal_evidence", taskId: params.taskId, evidence });
2690
+ const result = toTextToolResult(
2691
+ qcalEvidenceText(evidence, { inputs: params.includeInputs === true, metrics: params.includeMetrics === true }),
2692
+ { evidence },
2693
+ { tool: "qdash_build_qcal_evidence", taskId: params.taskId, evidence },
2694
+ );
2712
2695
  return { ...result, content: [...result.content, ...images] };
2713
2696
  },
2714
2697
  });
@@ -2753,7 +2736,7 @@ export default function qdashExtension(pi: ExtensionAPI) {
2753
2736
  pi.registerTool({
2754
2737
  name: "qdash_validate_calibration",
2755
2738
  label: "QDash Validate Calibration",
2756
- description: "Validate a calibration task result with explicit gates for status, issues, and figures. Read-only; does not commit or apply candidates.",
2739
+ description: "Validate a calibration task result with explicit gates for status and figures. Read-only; does not commit or apply candidates.",
2757
2740
  promptSnippet: "Validate a QDash calibration result before continuing or committing parameters",
2758
2741
  promptGuidelines: [
2759
2742
  "Use qdash_validate_calibration after an operational task completes or fails.",
@@ -2807,7 +2790,7 @@ export default function qdashExtension(pi: ExtensionAPI) {
2807
2790
  pi.registerTool({
2808
2791
  name: "qdash_target_report",
2809
2792
  label: "QDash Target Report",
2810
- description: "Build a read-only operational report for one qubit or coupling, correlating recent results, failures, issues, forum context, and a safe next-action recommendation.",
2793
+ description: "Build a read-only operational report for one qubit or coupling, correlating recent results, failures, Forum context, and a safe next-action recommendation.",
2811
2794
  promptSnippet: "Inspect one QDash qubit or coupling as an operational incident report",
2812
2795
  promptGuidelines: [
2813
2796
  "Use qdash_target_report before calibration changes when a specific qubit or coupling is under investigation.",
@@ -2836,7 +2819,7 @@ export default function qdashExtension(pi: ExtensionAPI) {
2836
2819
  name: "qdash_dashboard",
2837
2820
  label: "QDash Dashboard",
2838
2821
  description: "Build a compact read-only QDash dashboard for the current profile/chip context.",
2839
- promptSnippet: "Show QDash harness dashboard: chips, open issues, recent executions, failed task results, and provenance stats",
2822
+ promptSnippet: "Show QDash harness dashboard: chips, open Forum threads, recent executions, failed task results, and provenance stats",
2840
2823
  promptGuidelines: [
2841
2824
  "Use qdash_dashboard when the user asks for QDash status, overview, triage, or a dashboard.",
2842
2825
  "Summarize dashboard data instead of dumping large raw payloads.",
@@ -2860,9 +2843,9 @@ export default function qdashExtension(pi: ExtensionAPI) {
2860
2843
  pi.registerTool({
2861
2844
  name: "qdash_dashboard_insights",
2862
2845
  label: "QDash Dashboard Insights",
2863
- description: "Correlate dashboard metrics, open forum posts, notes/issues, and failed tasks into target-level insights with links.",
2846
+ description: "Correlate dashboard metrics, open Forum posts, and failed tasks into target-level insights with links.",
2864
2847
  promptSnippet: "Generate QDash target insights from dashboard metrics, forum links, notes, and failed tasks",
2865
- promptGuidelines: ["Use qdash_dashboard_insights when the user asks for insights across dashboard metrics, target summaries, forum posts, notes, issues, or anomalous targets."],
2848
+ promptGuidelines: ["Use qdash_dashboard_insights when the user asks for insights across dashboard metrics, target summaries, Forum posts, or anomalous targets."],
2866
2849
  parameters: Type.Object({
2867
2850
  ...connectionParams,
2868
2851
  chipId: Type.Optional(Type.String()),
@@ -2907,8 +2890,8 @@ export default function qdashExtension(pi: ExtensionAPI) {
2907
2890
  pi.registerTool({
2908
2891
  name: "qdash_investigate",
2909
2892
  label: "QDash Investigate",
2910
- description: "Run a read-only natural-language-friendly QDash investigation combining recent calibration results, target history, failures, issues, forum context, and safe recommendations.",
2911
- promptSnippet: "Investigate a QDash target or recent calibration by correlating results, figures, issues, and Forum context",
2893
+ description: "Run a read-only natural-language-friendly QDash investigation combining recent calibration results, target history, failures, Forum context, and safe recommendations.",
2894
+ promptSnippet: "Investigate a QDash target or recent calibration by correlating results, figures, and Forum context",
2912
2895
  promptGuidelines: [
2913
2896
  "Use qdash_investigate when the user asks to investigate, compare, or understand a QDash target or recent calibration in natural language.",
2914
2897
  "This tool is read-only; do not execute tasks or change parameters based on its recommendation without explicit confirmation.",
@@ -2985,19 +2968,19 @@ export default function qdashExtension(pi: ExtensionAPI) {
2985
2968
  pi.registerTool({
2986
2969
  name: "qdash_triage_overview",
2987
2970
  label: "QDash Triage Overview",
2988
- description: "Create a read-only triage overview from open issues and failed task results for the current QDash context.",
2989
- promptSnippet: "Summarize QDash open issues and failed task results for triage",
2971
+ description: "Create a read-only triage overview from open Forum threads and failed task results for the current QDash context.",
2972
+ promptSnippet: "Summarize QDash open Forum threads and failed task results for triage",
2990
2973
  promptGuidelines: ["Use qdash_triage_overview when the user asks what to investigate next in QDash."],
2991
2974
  parameters: Type.Object({ ...connectionParams, ...chipScopedParams, limit: Type.Optional(Type.Number()), color: Type.Optional(Type.Boolean({ description: "Emit ANSI colors in text output for terminal display." })) }),
2992
2975
  async execute(_toolCallId, params: { profile?: string; configPath?: string; useEnv?: boolean; chipId?: string; limit?: number; color?: boolean }) {
2993
2976
  const dashboard = await buildDashboard(params);
2994
2977
  const triage = {
2995
2978
  context: dashboard.context,
2996
- openIssues: dashboard.openIssues,
2979
+ openThreads: dashboard.openThreads,
2997
2980
  failedTaskResults: dashboard.failedTaskResults,
2998
2981
  suggestedFocus: [
2999
2982
  dashboard.failedTaskResults.count > 0 ? "Review failed task results first." : undefined,
3000
- dashboard.openIssues.count > 0 ? "Review open issues and correlate with recent executions." : undefined,
2983
+ dashboard.openThreads.count > 0 ? "Review open Forum threads and correlate with recent executions." : undefined,
3001
2984
  ].filter(Boolean),
3002
2985
  };
3003
2986
  const accent = (text: string) => params.color ? ansi("1;36", text) : text;
@@ -3005,7 +2988,7 @@ export default function qdashExtension(pi: ExtensionAPI) {
3005
2988
  const success = (text: string) => params.color ? ansi("32", text) : text;
3006
2989
  const error = (text: string) => params.color ? ansi("31", text) : text;
3007
2990
  const lines = boxed("QDash Triage", [
3008
- `open issues ${triage.openIssues.count > 0 ? ansi("33", String(triage.openIssues.count)) : success(String(triage.openIssues.count))}`,
2991
+ `open threads ${triage.openThreads.count > 0 ? ansi("33", String(triage.openThreads.count)) : success(String(triage.openThreads.count))}`,
3009
2992
  `failed tasks ${triage.failedTaskResults.count > 0 ? error(`${triage.failedTaskResults.shown}/${triage.failedTaskResults.count}`) : success(`${triage.failedTaskResults.shown}/${triage.failedTaskResults.count}`)}`,
3010
2993
  "",
3011
2994
  accent("Suggested focus"),
@@ -3017,11 +3000,11 @@ export default function qdashExtension(pi: ExtensionAPI) {
3017
3000
  return toTextToolResult(lines.join("\n"), triage, { tool: "qdash_triage_overview" });
3018
3001
  },
3019
3002
  renderResult(result, _options, theme) {
3020
- const data = (result.details as { data?: { openIssues?: { count?: number }; failedTaskResults?: { count?: number }; suggestedFocus?: string[] } } | undefined)?.data;
3003
+ const data = (result.details as { data?: { openThreads?: { count?: number }; failedTaskResults?: { count?: number }; suggestedFocus?: string[] } } | undefined)?.data;
3021
3004
  if (!data) return new Text(theme.fg("dim", "QDash triage unavailable"), 0, 0);
3022
3005
  const lines = [
3023
3006
  theme.fg("accent", theme.bold("QDash Triage")),
3024
- `${theme.fg("muted", "open issues")} ${theme.fg((data.openIssues?.count ?? 0) > 0 ? "warning" : "success", String(data.openIssues?.count ?? 0))}`,
3007
+ `${theme.fg("muted", "open threads")} ${theme.fg((data.openThreads?.count ?? 0) > 0 ? "warning" : "success", String(data.openThreads?.count ?? 0))}`,
3025
3008
  `${theme.fg("muted", "failed tasks")} ${theme.fg((data.failedTaskResults?.count ?? 0) > 0 ? "error" : "success", String(data.failedTaskResults?.count ?? 0))}`,
3026
3009
  "",
3027
3010
  theme.fg("borderAccent", "Suggested focus"),
@@ -3293,7 +3276,7 @@ export default function qdashExtension(pi: ExtensionAPI) {
3293
3276
  const dashboard = await buildDashboard({ limit: Number.isFinite(limit) ? limit : undefined });
3294
3277
  refreshContextUi(ctx);
3295
3278
  ctx.ui.setWidget("qdash", (_tui, theme) => dashboardComponent(dashboard, theme));
3296
- ctx.ui.notify(`QDash dashboard updated: ${dashboard.openIssues.count} open issues, ${dashboard.failedTaskResults.count} failed tasks`, "info");
3279
+ ctx.ui.notify(`QDash dashboard updated: ${dashboard.openThreads.count} open Forum threads, ${dashboard.failedTaskResults.count} failed tasks`, "info");
3297
3280
  },
3298
3281
  });
3299
3282
 
@@ -250,10 +250,12 @@ export function registerCalibrationPipelineTools(pi: Pick<ExtensionAPI, "registe
250
250
  pi.registerTool({
251
251
  name: "qdash_run_pipeline",
252
252
  label: "QDash Run Pipeline",
253
- description: "Run a calibration pipeline spec as one QDash execution. Confirmation-gated write operation. Validate with qdash_plan_pipeline first; the spec runs exactly as validated.",
253
+ description: "Run a calibration pipeline spec as one QDash execution. Completed tasks write their calibrated parameters to QDash directly, without an agent candidate gate. Confirmation-gated write operation. Validate with qdash_plan_pipeline first; the spec runs exactly as validated.",
254
254
  promptSnippet: "Run a validated QDash calibration pipeline spec",
255
255
  promptGuidelines: [
256
256
  "Use qdash_run_pipeline only for a spec that qdash_plan_pipeline reported valid.",
257
+ "Tell the user before running that completed tasks update calibrated parameters in QDash directly.",
258
+ "Do not use qdash_run_pipeline to run a task an agent session action failed to run; that skips the session's candidate gate. Report the failure instead.",
257
259
  "After it starts, follow the execution with qdash_wait_execution and summarize failed targets.",
258
260
  ],
259
261
  parameters: Type.Object({
@@ -265,7 +267,7 @@ export function registerCalibrationPipelineTools(pi: Pick<ExtensionAPI, "registe
265
267
  }),
266
268
  async execute(_toolCallId, params: ConnectionParams & { chipId: string; spec: Record<string, unknown>; backendName?: string; confirmWrite?: boolean }, _signal, _onUpdate, ctx) {
267
269
  if (!params.confirmWrite) {
268
- if (!ctx.hasUI || !(await ctx.ui.confirm("Run QDash calibration pipeline?", `This starts a calibration execution on chip ${params.chipId}.`))) {
270
+ if (!ctx.hasUI || !(await ctx.ui.confirm("Run QDash calibration pipeline?", `This starts a calibration execution on chip ${params.chipId}. Completed tasks update calibrated parameters in QDash directly.`))) {
269
271
  throw new Error("qdash_run_pipeline requires explicit confirmation");
270
272
  }
271
273
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oqtopus-team/pi-qdash",
3
- "version": "0.1.10",
3
+ "version": "0.1.11-beta.20",
4
4
  "description": "Pi extension for querying QDash via @oqtopus-team/qdash-client",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -27,7 +27,7 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@oqtopus-team/qdash-client": "1.9.21-beta.171"
30
+ "@oqtopus-team/qdash-client": "1.9.21-beta.203"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@earendil-works/pi-coding-agent": "*",
@@ -22,7 +22,6 @@ Prefer the dedicated tool that matches the question. Use `qdash_query` only as a
22
22
  - `qdash_inspect_timeseries_csv`, `qdash_compare_timeseries` — validate and compare multiple QDash or CSV timeseries
23
23
  - `qdash_list_task_results`, `qdash_get_task_result` — find and inspect calibration results
24
24
  - `qdash_get_task_knowledge` — interpret a task's purpose, expected result, failure modes, and check questions
25
- - `qdash_list_issues` — inspect tracked issues
26
25
  - `qdash_list_flows`, `qdash_get_flow` — list or inspect calibration flows
27
26
  - `qdash_get_pipeline_catalog`, `qdash_plan_pipeline` — compose and check a calibration pipeline spec before running it
28
27
  - `qdash_list_executions`, `qdash_get_execution`, `qdash_wait_execution`, `qdash_compare_executions` — inspect, await, or compare executions
@@ -57,7 +56,7 @@ Workflow:
57
56
  1. `qdash_get_pipeline_catalog` — read the step types (with what each needs from an earlier step and its default task list), the one-qubit modes, and the task names by type. Use only names from this catalog.
58
57
  2. Turn the request into steps. Start from the catalog defaults and the flow templates (`qdash_query` with `flow_templates`, then `flow_template` for one template's task list): `OneQubitCheck` and `OneQubitFineTune` carry the standard one-qubit sequences, `TwoQubitCalibration` the two-qubit chain. Use `CustomOneQubit` / `CustomTwoQubit` only where the user wants a different task list, and keep the task order the templates use.
59
58
  3. `qdash_plan_pipeline` — validate. A result with problems is not an error: each problem carries a path into the spec (`steps[2].tasks[0]`, `targets.qids[1]`); fix it and plan again until it is valid.
60
- 4. Show the user the resolved steps and task counts from the plan, then `qdash_run_pipeline` with the same `chipId` and `spec`. The execution is confirmation-gated.
59
+ 4. Show the user the resolved steps and task counts from the plan, then `qdash_run_pipeline` with the same `chipId` and `spec`. The execution is confirmation-gated. Say that completed tasks write their calibrated parameters to QDash directly; there is no candidate gate, so inside an agent calibration session use the session's actions instead.
61
60
  5. `qdash_wait_execution` with the returned `execution_id`. It returns when the whole run has finished, not just its first step. Then summarize which targets passed and which failed, fetch the figures of failed tasks with `qdash_get_task_figures` so the user sees them, and read `qdash_get_task_knowledge` for any task whose result needs interpretation.
62
61
 
63
62
  Rules:
@@ -160,7 +159,7 @@ When the user wants to preserve an investigated observation in QDash forum/notes
160
159
  - Never print tokens, passwords, Cloudflare Access secrets, or full profile contents.
161
160
  - Treat write/operational endpoints (agent session/action creation, candidate commits, forum create/update, flow execute, git push/pull, re-execute, admin/auth APIs) as sensitive and ask the user before any write workflow.
162
161
  - Write-oriented QDash tools are approval-gated. In interactive pi, let the extension prompt for confirmation. In non-interactive runs, set `confirmWrite: true` only after explicit user confirmation.
163
- - Use `qdash_investigate` for natural-language requests to investigate, compare, or understand a target or recent calibration; it is read-only and combines recent results with target, Forum, issue, and recommendation context.
162
+ - Use `qdash_investigate` for natural-language requests to investigate, compare, or understand a target or recent calibration; it is read-only and combines recent results with target, Forum, and recommendation context.
164
163
  - Use `qdash_compare_calibration` when the user asks what changed between recent calibration experiments; treat differences as evidence, not automatic approval to commit/apply parameters.
165
164
  - Use `qdash_recent_calibration_figure` when the user asks to see recent experiment images without a task ID.
166
165
  - Use `qdash_analyze_figure_json` for read-only numeric summaries of Plotly JSON calibration figures; keep domain-specific interpretation in the matching skill.
@@ -47,7 +47,7 @@ This skill defines how pi should behave as a conservative, confirmation-gated ca
47
47
  - any in-cooldown wiring changes relevant to the target
48
48
  - failed task(s)
49
49
  - qid/coupling_id
50
- - recent executions and open forum/issue context
50
+ - recent executions and open Forum context
51
51
  2. Read evidence:
52
52
  - `qdash_list_task_results`
53
53
  - `qdash_get_task_result`
@@ -144,3 +144,7 @@ Stop and request human review when:
144
144
  - the same target fails after `CheckChevron` and `Configure`
145
145
  - figures suggest TLS, frequency collision, leakage, or multi-level behavior
146
146
  - the next action would require broad parameter sweeps or hardware-risky changes
147
+ - QDash refuses to run an agent action's task before it measures anything (a platform or
148
+ configuration error, not a calibration result). Report the error with the task result and
149
+ execution links; do not rerun the task through another path such as `qdash_run_pipeline`,
150
+ which writes parameters without the session's candidate gate