@taiga-ai/mcp-server 0.25.0-dev.476 → 0.25.0-dev.477

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/dist/index.js CHANGED
@@ -981,6 +981,19 @@ const INITIATIVE_STATES = [
981
981
  "done",
982
982
  "canceled"
983
983
  ];
984
+ /**
985
+ * The states the line works, and the only ones that carry its flag. One copy
986
+ * for every reader that means "on the line": the flag the API clears when an
987
+ * initiative leaves it, the remediation settle on the same departure, and the
988
+ * MCP's account of where an initiative stands (PRO-2840). Everything else
989
+ * (backlog, todo, done, canceled) is off it. The lifecycle derivation does not
990
+ * read it: it derives the stage from the run alone (PRO-2828).
991
+ */
992
+ const LINE_STATES = [
993
+ "queue",
994
+ "plan",
995
+ "build"
996
+ ];
984
997
  /** Finished work: settled by a person or a merge, or archived. The one
985
998
  * predicate every "can this still move / plan / build" guard reads. */
986
999
  function isSettledOrArchived(item) {
@@ -4273,7 +4286,7 @@ const ITEM_OUTCOME_FIELDS = [
4273
4286
  * needsHumanCause is only the line's flag; a plan that failed outside the line
4274
4287
  * leaves it null, and the lifecycle is what says why and what moves it on.
4275
4288
  */
4276
- const LIFECYCLE_MEANING = "situation says in a sentence where it stands and what moves it on. lifecycle.cta names the next call: start_plan taiga_queue_initiative, retry_plan taiga_plan_again (planFailureCause says why), start_build taiga_approve_plan, resume_build taiga_resume_initiative, retry_build a fresh run (web only), review_pr and view_pr the pull request. needsHumanCause is set only when the line stopped on it.";
4289
+ const LIFECYCLE_MEANING = "situation says in a sentence where it stands and what moves it on. lifecycle.cta names the next call: start_plan taiga_queue_initiative, retry_plan taiga_plan_again (planFailureCause says why), start_build taiga_approve_plan, resume_build taiga_resume_initiative, retry_build a fresh run (web only; off the line, taiga_queue_initiative instead, as situation says), review_pr and view_pr the pull request. needsHumanCause is set only when the line stopped on it.";
4277
4290
  /** The move each call to action is, in the tools' words. */
4278
4291
  const CTA_MOVE = {
4279
4292
  start_plan: "taiga_queue_initiative puts it on the line to be planned and built (paid work: ask first)",
@@ -4284,6 +4297,10 @@ const CTA_MOVE = {
4284
4297
  review_pr: "a person reviews and merges the pull request",
4285
4298
  view_pr: "the line merges the pull request once its checks pass"
4286
4299
  };
4300
+ /** The move for an initiative off the line whose last run did not land: there is no line to take it off (PRO-2840). */
4301
+ const OFF_THE_LINE_MOVE = "taiga_queue_initiative puts it back on the line (paid work: ask first)";
4302
+ /** What happens to a queued initiative whose last run did not land: the line plans it again (PRO-2840). */
4303
+ const REQUEUED_MOVE = "the line plans it again when its turn comes";
4287
4304
  /** How a run stands, as the end of "its latest run ...", by disposition. */
4288
4305
  const RUN_STANDS = {
4289
4306
  in_flight: "is running",
@@ -4317,6 +4334,11 @@ function situationOf(item) {
4317
4334
  const parts = [];
4318
4335
  let stage = STAGE[life.stage] ?? life.stage;
4319
4336
  if (life.stage === "planning" && life.turn === "you") stage = `Planning stopped${typeof life.planFailureCause === "string" ? ` (${life.planFailureCause})` : ""}`;
4337
+ const column = typeof item["state"] === "string" ? item["state"] : null;
4338
+ const offTheLine = column !== null && !LINE_STATES.includes(column);
4339
+ const stalled = life.stage === "building" && life.turn !== "agent";
4340
+ const requeued = stalled && column === "queue";
4341
+ if (stalled) stage = requeued ? "Queued" : offTheLine ? "Off the line" : column === null ? "Its build stopped" : "On the line, but its build stopped";
4320
4342
  const build = life.latestBuild;
4321
4343
  if (build && typeof build.status === "string" && life.stage !== "done" && life.stage !== "canceled") {
4322
4344
  const pr = typeof build.prNumber === "number" ? ` (PR #${build.prNumber})` : "";
@@ -4336,8 +4358,9 @@ function situationOf(item) {
4336
4358
  }
4337
4359
  const turn = life.turn === "you" ? "A person's turn" : life.turn === "agent" ? "The agent's turn" : null;
4338
4360
  const handedBack = handbackSaid(build?.reviewEscalation);
4339
- const move = life.cta === "review_pr" && handedBack ? `a person should ${handbackNextStep(build.reviewEscalation)}` : typeof life.cta === "string" ? CTA_MOVE[life.cta] : void 0;
4340
- if (life.cta === "view_pr") parts.push(`Nothing to do: ${move}.`);
4361
+ const move = life.cta === "review_pr" && handedBack ? `a person should ${handbackNextStep(build.reviewEscalation)}` : stalled && offTheLine ? OFF_THE_LINE_MOVE : typeof life.cta === "string" ? CTA_MOVE[life.cta] : void 0;
4362
+ if (requeued) parts.push(`Nothing to do: ${REQUEUED_MOVE}.`);
4363
+ else if (life.cta === "view_pr") parts.push(`Nothing to do: ${move}.`);
4341
4364
  else if (turn && move) parts.push(`${turn}: ${move}.`);
4342
4365
  else if (turn) parts.push(`${turn}.`);
4343
4366
  else if (move) parts.push(`Next: ${move}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.25.0-dev.476",
3
+ "version": "0.25.0-dev.477",
4
4
  "description": "MCP server for Taiga -- gives AI assistants access to documents and products",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -7978,12 +7978,67 @@ describe('what the third agent review found (PRO-2800)', () => {
7978
7978
  );
7979
7979
 
7980
7980
  const said =
7981
- "Being built; its latest run had its pull request closed without merging (PR #28). A person's turn: a new " +
7981
+ "On the line, but its build stopped; its latest run had its pull request closed without merging (PR #28). A person's turn: a new " +
7982
7982
  'run starts over from the web; taiga_move_initiative takes it off the line.';
7983
7983
  expect(got.situation).toBe(said);
7984
7984
  expect(listed.data[0].situation).toBe(said);
7985
7985
  });
7986
7986
 
7987
+ it.each([
7988
+ ['todo', 'Off the line', 'taiga_queue_initiative puts it back on the line (paid work: ask first)'],
7989
+ ['build', 'On the line, but its build stopped', 'a new run starts over from the web'],
7990
+ ['queue', 'Queued', 'Nothing to do: the line plans it again when its turn comes.'],
7991
+ ])('says where a %s initiative with a dead run is, from its column (PRO-2840)', async (state, where, move) => {
7992
+ const item = {
7993
+ id: ITEM_ID,
7994
+ title: 'Add login',
7995
+ state,
7996
+ lifecycle: {
7997
+ turn: 'you',
7998
+ stage: 'building',
7999
+ cta: 'retry_build',
8000
+ planFailureCause: null,
8001
+ latestBuild: { status: 'closed', prNumber: 28 },
8002
+ },
8003
+ };
8004
+ const { client } = await bootMcpPair({
8005
+ capabilities: ['backlog:read'],
8006
+ routes: { [`GET /v1/backlog/${ITEM_ID}`]: { data: item } },
8007
+ });
8008
+
8009
+ const got = parseToolResult(
8010
+ await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
8011
+ );
8012
+
8013
+ expect(got.situation).toMatch(new RegExp(`^${where}; its latest run had its pull request closed`));
8014
+ expect(got.situation).toContain(move);
8015
+ expect(got.situation).not.toMatch(/Being built/);
8016
+ });
8017
+
8018
+ it('says Being built only while the agent is building it (PRO-2840)', async () => {
8019
+ const item = {
8020
+ id: ITEM_ID,
8021
+ title: 'Add login',
8022
+ lifecycle: {
8023
+ turn: 'agent',
8024
+ stage: 'building',
8025
+ cta: null,
8026
+ planFailureCause: null,
8027
+ latestBuild: { status: 'implementing', prNumber: null },
8028
+ },
8029
+ };
8030
+ const { client } = await bootMcpPair({
8031
+ capabilities: ['backlog:read'],
8032
+ routes: { [`GET /v1/backlog/${ITEM_ID}`]: { data: item } },
8033
+ });
8034
+
8035
+ const got = parseToolResult(
8036
+ await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
8037
+ );
8038
+
8039
+ expect(got.situation).toMatch(/^Being built; its latest run is running\. The agent's turn\./);
8040
+ });
8041
+
7987
8042
  it("keeps the run's own reason beside the halt that stopped the line", async () => {
7988
8043
  const item = {
7989
8044
  id: ITEM_ID,
@@ -8002,7 +8057,9 @@ describe('what the third agent review found (PRO-2800)', () => {
8002
8057
  );
8003
8058
 
8004
8059
  expect(got.situation).toContain('The pull request was closed by alice.');
8005
- expect(got.situation).toMatch(/^Being built\. The line stopped on it: .+\. The pull request was closed by alice\./);
8060
+ expect(got.situation).toMatch(
8061
+ /^Its build stopped\. The line stopped on it: .+\. The pull request was closed by alice\./
8062
+ );
8006
8063
  });
8007
8064
 
8008
8065
  it("leaves the keys of a document's sections as their writers named them", async () => {
@@ -6,6 +6,7 @@ import {
6
6
  type BacklogItem,
7
7
  BUILD_RUN_STATUSES,
8
8
  INITIATIVE_STATES,
9
+ LINE_STATES,
9
10
  PLAN_REJECTION_COMMENT_MAX_CHARS,
10
11
  PR_CLOSING_STATES,
11
12
  USER_DROP_STATES,
@@ -68,8 +69,9 @@ const ITEM_OUTCOME_FIELDS = [
68
69
  const LIFECYCLE_MEANING =
69
70
  'situation says in a sentence where it stands and what moves it on. lifecycle.cta names the next call: ' +
70
71
  'start_plan taiga_queue_initiative, retry_plan taiga_plan_again (planFailureCause says why), start_build ' +
71
- 'taiga_approve_plan, resume_build taiga_resume_initiative, retry_build a fresh run (web only), review_pr and ' +
72
- 'view_pr the pull request. needsHumanCause is set only when the line stopped on it.';
72
+ 'taiga_approve_plan, resume_build taiga_resume_initiative, retry_build a fresh run (web only; off the line, ' +
73
+ 'taiga_queue_initiative instead, as situation says), review_pr and view_pr the pull request. needsHumanCause ' +
74
+ 'is set only when the line stopped on it.';
73
75
 
74
76
  /** The move each call to action is, in the tools' words. */
75
77
  const CTA_MOVE: Record<string, string> = {
@@ -83,6 +85,12 @@ const CTA_MOVE: Record<string, string> = {
83
85
  view_pr: 'the line merges the pull request once its checks pass',
84
86
  };
85
87
 
88
+ /** The move for an initiative off the line whose last run did not land: there is no line to take it off (PRO-2840). */
89
+ const OFF_THE_LINE_MOVE = 'taiga_queue_initiative puts it back on the line (paid work: ask first)';
90
+
91
+ /** What happens to a queued initiative whose last run did not land: the line plans it again (PRO-2840). */
92
+ const REQUEUED_MOVE = 'the line plans it again when its turn comes';
93
+
86
94
  /** How a run stands, as the end of "its latest run ...", by disposition. */
87
95
  const RUN_STANDS: Record<string, string> = {
88
96
  in_flight: 'is running',
@@ -123,6 +131,26 @@ export function situationOf(item: Record<string, unknown>): string | null {
123
131
  if (life.stage === 'planning' && life.turn === 'you') {
124
132
  stage = `Planning stopped${typeof life.planFailureCause === 'string' ? ` (${life.planFailureCause})` : ''}`;
125
133
  }
134
+ // Being built only while the agent is at it (PRO-2840). With its run failed,
135
+ // stopped or its pull request closed, nothing is building: the run's outcome
136
+ // follows and the turn says whose move it is. The lifecycle derives this
137
+ // stage from the run alone, so an initiative already moved back to the
138
+ // backlog or todo is said to be off the line, which only its column knows.
139
+ // Queued, the line plans it again when its turn comes (activation replaces a dead run), so it is nobody's
140
+ // move. Without a column (an old row) it is said neutrally.
141
+ const column = typeof item['state'] === 'string' ? item['state'] : null;
142
+ const offTheLine = column !== null && !(LINE_STATES as readonly string[]).includes(column);
143
+ const stalled = life.stage === 'building' && life.turn !== 'agent';
144
+ const requeued = stalled && column === 'queue';
145
+ if (stalled) {
146
+ stage = requeued
147
+ ? 'Queued'
148
+ : offTheLine
149
+ ? 'Off the line'
150
+ : column === null
151
+ ? 'Its build stopped'
152
+ : 'On the line, but its build stopped';
153
+ }
126
154
  const build = life.latestBuild as { status?: unknown; prNumber?: unknown; prMerged?: unknown } | null | undefined;
127
155
  if (build && typeof build.status === 'string' && life.stage !== 'done' && life.stage !== 'canceled') {
128
156
  const pr = typeof build.prNumber === 'number' ? ` (PR #${build.prNumber})` : '';
@@ -151,10 +179,13 @@ export function situationOf(item: Record<string, unknown>): string | null {
151
179
  const move =
152
180
  life.cta === 'review_pr' && handedBack
153
181
  ? `a person should ${handbackNextStep((build as { reviewEscalation?: unknown }).reviewEscalation)}`
154
- : typeof life.cta === 'string'
155
- ? CTA_MOVE[life.cta]
156
- : undefined;
157
- if (life.cta === 'view_pr') parts.push(`Nothing to do: ${move}.`);
182
+ : stalled && offTheLine
183
+ ? OFF_THE_LINE_MOVE
184
+ : typeof life.cta === 'string'
185
+ ? CTA_MOVE[life.cta]
186
+ : undefined;
187
+ if (requeued) parts.push(`Nothing to do: ${REQUEUED_MOVE}.`);
188
+ else if (life.cta === 'view_pr') parts.push(`Nothing to do: ${move}.`);
158
189
  else if (turn && move) parts.push(`${turn}: ${move}.`);
159
190
  else if (turn) parts.push(`${turn}.`);
160
191
  else if (move) parts.push(`Next: ${move}.`);