@yagni-app/code-staging 1.1.6-staging.1443.1 → 1.1.6-staging.1445.1

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.
@@ -362,6 +362,9 @@ function formatSummaryForChat(result, costText) {
362
362
  const UNTRACKED_NOTICE = {
363
363
  disabled: "Run tracking is off for this workspace, so this /go run won't appear on the Work page.",
364
364
  unauthorized: "Run tracking is unauthorized. Run `yagni login` to refresh your token so runs appear on the Work page.",
365
+ // A refusal always arrives with the server's own sentence; this is the line
366
+ // for the rare body that carries none.
367
+ refused: "YAGNI declined to record this run, so it won't appear on the Work page. The pipeline ran anyway.",
365
368
  error: "Couldn't reach YAGNI to record this run, so it won't appear on the Work page. The pipeline ran anyway.",
366
369
  };
367
370
  /** Toast severity for an outcome: clean is good, round_cap warns, abort/fail are errors. */
@@ -737,9 +740,11 @@ export function registerGoCommand(pi, deps = {}) {
737
740
  }
738
741
  return { runId: started.runId, workItemId: started.workItemId };
739
742
  }
740
- // Recording skipped (flag off) or failed (unauthorized / unreachable):
741
- // tell the user the run won't show on the Work page rather than swallowing it.
742
- notify(UNTRACKED_NOTICE[started.reason], "warning");
743
+ // Recording skipped (flag off) or failed (unauthorized / refused /
744
+ // unreachable): tell the user the run won't show on the Work page
745
+ // rather than swallowing it. A refusal carries the server's own
746
+ // sentence, which says what to do about it, so it wins over our copy.
747
+ notify(started.message ?? UNTRACKED_NOTICE[started.reason], "warning");
743
748
  return {};
744
749
  };
745
750
  let resumeFrom;
@@ -67,21 +67,27 @@ export interface RunStartResult extends RunSessionHandle {
67
67
  * workspace (the write endpoints 404 to hide themselves).
68
68
  * - `unauthorized` — 401/403: the device token is missing/expired or lacks the
69
69
  * `yagni_code:write` scope (re-run `yagni login`).
70
+ * - `refused` — 400: the server declined for a reason a person can act on
71
+ * and said so in one sentence (this repository belongs to
72
+ * no Team). The sentence rides `message` and is what the
73
+ * caller shows.
70
74
  * - `error` — anything else: an over-budget 402, a 5xx, a 2xx with an
71
75
  * unrecognized body, or an unreachable backend / thrown
72
76
  * network error.
73
77
  */
74
- export type RunTrackingFailure = "disabled" | "unauthorized" | "error";
78
+ export type RunTrackingFailure = "disabled" | "unauthorized" | "refused" | "error";
75
79
  /**
76
80
  * The outcome of opening a run. `tracked: true` carries the handle + gate
77
81
  * verdict; `tracked: false` carries the reason so `/go` can warn that this run
78
- * won't appear on the Work page. `start` never returns null and never throws.
82
+ * won't appear on the Work page, plus the server's own sentence when it sent
83
+ * one. `start` never returns null and never throws.
79
84
  */
80
85
  export type RunStartOutcome = ({
81
86
  tracked: true;
82
87
  } & RunStartResult) | {
83
88
  tracked: false;
84
89
  reason: RunTrackingFailure;
90
+ message?: string;
85
91
  };
86
92
  /** Arguments to open a run. */
87
93
  export interface StartArgs {
@@ -25,27 +25,45 @@ function failureReason(status) {
25
25
  return "disabled";
26
26
  if (status === 401 || status === 403)
27
27
  return "unauthorized";
28
+ if (status === 400)
29
+ return "refused";
28
30
  return "error";
29
31
  }
30
32
  /** Cap a debug body excerpt so a giant HTML/JSON error never floods the channel. */
31
33
  const MAX_DEBUG_BODY = 200;
34
+ /** Cap the sentence a refusal is allowed to print, so a stray body stays one line. */
35
+ const MAX_REFUSAL_MESSAGE = 240;
32
36
  /**
33
- * A token-free, capped `: <body>` suffix for a failed-response debug line. Reads
34
- * the body only on the `!res.ok` branch (where `post` never reads it otherwise,
35
- * so there is no double-consume with the ok-path `res.json()`), and swallows any
36
- * read error so observability never breaks the fail-soft contract.
37
+ * The server's one-sentence refusal, if the body is the shape our own routes
38
+ * send (`{ error }`). Anything else, including HTML from a proxy, reads as no
39
+ * sentence at all, so the caller falls back to its own copy. Newlines are
40
+ * folded so a notice is never a stack.
37
41
  */
38
- async function bodyTail(res) {
42
+ function refusalMessage(body) {
39
43
  try {
40
- const t = (await res.text()).trim();
41
- if (!t)
42
- return "";
43
- return `: ${t.length > MAX_DEBUG_BODY ? `${t.slice(0, MAX_DEBUG_BODY)} ...` : t}`;
44
+ const parsed = JSON.parse(body);
45
+ const text = typeof parsed.error === "string" ? parsed.error.trim() : "";
46
+ if (!text)
47
+ return undefined;
48
+ const oneLine = text.replace(/\s+/g, " ");
49
+ return oneLine.length > MAX_REFUSAL_MESSAGE
50
+ ? `${oneLine.slice(0, MAX_REFUSAL_MESSAGE)}...`
51
+ : oneLine;
44
52
  }
45
53
  catch {
46
- return "";
54
+ return undefined;
47
55
  }
48
56
  }
57
+ /**
58
+ * A token-free, capped `: <body>` suffix for a failed-response debug line, from
59
+ * a body already read once by the `!res.ok` branch.
60
+ */
61
+ function bodyTail(body) {
62
+ const t = body.trim();
63
+ if (!t)
64
+ return "";
65
+ return `: ${t.length > MAX_DEBUG_BODY ? `${t.slice(0, MAX_DEBUG_BODY)} ...` : t}`;
66
+ }
49
67
  export function makeRunSession(opts) {
50
68
  const f = opts.fetchImpl ?? fetch;
51
69
  let handle = null;
@@ -94,9 +112,21 @@ export function makeRunSession(opts) {
94
112
  ...(opts.random ? { random: opts.random } : {}),
95
113
  });
96
114
  if (!res.ok) {
115
+ // Read the body ONCE on the failure branch (the ok path reads
116
+ // `res.json()` instead, so there is no double-consume): the debug line
117
+ // and the refusal sentence both come from this one read, and a body
118
+ // that cannot be read costs neither.
119
+ let body = "";
120
+ try {
121
+ body = await res.text();
122
+ }
123
+ catch {
124
+ /* a body we cannot read is a body we do not quote */
125
+ }
97
126
  if (debugOn)
98
- debug(`[yagni-code] run-record ${path} -> HTTP ${res.status}${await bodyTail(res)}`);
99
- return { ok: false, status: res.status };
127
+ debug(`[yagni-code] run-record ${path} -> HTTP ${res.status}${bodyTail(body)}`);
128
+ const message = refusalMessage(body);
129
+ return { ok: false, status: res.status, ...(message ? { failureText: message } : {}) };
100
130
  }
101
131
  return { ok: true, data: (await res.json()) };
102
132
  }
@@ -119,8 +149,13 @@ export function makeRunSession(opts) {
119
149
  // deduped by the server (startCodeRun.findRunByIdempotencyKey) instead of
120
150
  // opening a second run + a second free-text work item.
121
151
  const r = await post("runs", { ...b, idempotencyKey: randomUUID() }, { retry: true });
122
- if (!r.ok)
123
- return { tracked: false, reason: failureReason(r.status) };
152
+ if (!r.ok) {
153
+ return {
154
+ tracked: false,
155
+ reason: failureReason(r.status),
156
+ ...(r.failureText ? { message: r.failureText } : {}),
157
+ };
158
+ }
124
159
  const data = r.data;
125
160
  if (typeof data.runId === "string" && typeof data.workItemId === "string") {
126
161
  handle = { runId: data.runId, workItemId: data.workItemId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.1.6-staging.1443.1",
3
+ "version": "1.1.6-staging.1445.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -58,5 +58,5 @@
58
58
  "turndown": "^7.2.4",
59
59
  "typebox": "^1.3.15"
60
60
  },
61
- "yagniSourceSha": "8e549e888319699160837efefdfa8f1d835a84b5"
61
+ "yagniSourceSha": "df1d49933fa6a71d3b18754a183b5e400a45053a"
62
62
  }