@owlmeans/viable-sdk 0.1.18-rc.21 → 0.1.18-rc.23

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.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/viable-sdk/SKILL.md +76 -21
  4. package/build/api/remote.d.ts +2 -3
  5. package/build/api/remote.d.ts.map +1 -1
  6. package/build/api/remote.js +13 -14
  7. package/build/api/remote.js.map +1 -1
  8. package/build/consts.d.ts +3 -13
  9. package/build/consts.d.ts.map +1 -1
  10. package/build/consts.js +3 -13
  11. package/build/consts.js.map +1 -1
  12. package/build/harness/templates.d.ts +2 -2
  13. package/build/harness/templates.d.ts.map +1 -1
  14. package/build/harness/templates.js +10 -10
  15. package/build/project/setup.d.ts +1 -1
  16. package/build/tools/catalogue.d.ts +2 -2
  17. package/build/tools/catalogue.d.ts.map +1 -1
  18. package/build/tools/catalogue.js +143 -142
  19. package/build/tools/catalogue.js.map +1 -1
  20. package/build/tools/index.d.ts +2 -1
  21. package/build/tools/index.d.ts.map +1 -1
  22. package/build/tools/index.js +2 -1
  23. package/build/tools/index.js.map +1 -1
  24. package/build/tools/mcp.d.ts +2 -2
  25. package/build/tools/mcp.d.ts.map +1 -1
  26. package/build/tools/mcp.js +15 -10
  27. package/build/tools/mcp.js.map +1 -1
  28. package/build/tools/platform.d.ts +27 -8
  29. package/build/tools/platform.d.ts.map +1 -1
  30. package/build/tools/platform.js +107 -42
  31. package/build/tools/platform.js.map +1 -1
  32. package/build/tools/refusal.d.ts +2 -2
  33. package/build/tools/refusal.d.ts.map +1 -1
  34. package/build/tools/refusal.js +22 -6
  35. package/build/tools/refusal.js.map +1 -1
  36. package/build/tools/settings.d.ts +45 -0
  37. package/build/tools/settings.d.ts.map +1 -0
  38. package/build/tools/settings.js +86 -0
  39. package/build/tools/settings.js.map +1 -0
  40. package/build/tools/status.d.ts +17 -0
  41. package/build/tools/status.d.ts.map +1 -0
  42. package/build/tools/status.js +104 -0
  43. package/build/tools/status.js.map +1 -0
  44. package/build/tools/stories.d.ts +19 -1
  45. package/build/tools/stories.d.ts.map +1 -1
  46. package/build/tools/stories.js +22 -1
  47. package/build/tools/stories.js.map +1 -1
  48. package/build/types.d.ts +34 -17
  49. package/build/types.d.ts.map +1 -1
  50. package/package.json +19 -19
  51. package/build/tools/jobs.d.ts +0 -12
  52. package/build/tools/jobs.d.ts.map +0 -1
  53. package/build/tools/jobs.js +0 -60
  54. package/build/tools/jobs.js.map +0 -1
@@ -1,17 +1,18 @@
1
1
  import { z } from 'zod';
2
2
  import { CommitTimeout, TransitionAction, WorkcardKind } from '@owlmeans/planning';
3
- import { ConnectJobKind, ConnectTarget, ConversionDecision, ConversionStatus, ConvertibilityVerdict, decisionFor, jobIdOf, MODEL_TIER_ROLES, OriginKind, STORY_BAND_MAX_USD, STORY_BAND_MIN_USD, VIABLE_STORY_TYPE, ViableStoryTransition } from '@owlmeans/viable-common';
4
- import { COMMIT_WAIT_MS, JOB_POLL_MAX_SEC, NEXT_QUESTION_WAIT_MS, NEXT_TASK_WAIT_MS, STORY_PAGE_SIZE } from '../consts.js';
3
+ import { ConnectTarget, ConversionDecision, ConversionStatus, ConvertibilityVerdict, MODEL_TIER_ROLES, OriginKind, STORY_BAND_MAX_USD, STORY_BAND_MIN_USD, VIABLE_STORY_TYPE, ViableStoryTransition } from '@owlmeans/viable-common';
4
+ import { COMMIT_WAIT_MS, NEXT_QUESTION_WAIT_MS, NEXT_TASK_WAIT_MS, STORY_PAGE_SIZE } from '../consts.js';
5
5
  import { describeHarness, installHarness } from '../harness/index.js';
6
6
  import { envStatus } from '../project/env.js';
7
7
  import { missingServices, readSetupReport, renderSetupGuide, setUserEnv } from '../project/setup.js';
8
8
  import { localStatus, runLocal, stopLocal } from '../run/index.js';
9
9
  import { parseTaskResult, renderTaskEnvelope } from '../task/envelope.js';
10
10
  import { parseAnswer, renderQuestionEnvelope } from '../task/inquiry.js';
11
- import { renderJob } from './jobs.js';
12
- import { PLATFORM_CATALOGUE, renderPlatform } from './platform.js';
11
+ import { GENERATED_SUMMARY, PLATFORM_CATALOGUE, renderPlatform } from './platform.js';
13
12
  import { refusalMessage, refusalPhrase } from './refusal.js';
14
- import { renderStories, resolveStory, STORY_ORDER, storyQuery } from './stories.js';
13
+ import { renderProjectSettings, settingsPatch, settingsReach } from './settings.js';
14
+ import { conversionNext, renderPipelineStatus, renderProjectStatus, renderStoryStatus, } from './status.js';
15
+ import { isLandingStory, renderStories, resolveStory, STORY_ORDER, storyQuery } from './stories.js';
15
16
  import { anyHost, cloudTarget, localTarget, performsModelTasks, sessionCapable, ToolHostKind, withExecutor } from './types.js';
16
17
  const ok = (text, structured) => ({ text, structured });
17
18
  const fail = (text) => ({ text, isError: true });
@@ -72,7 +73,16 @@ const projectOf = (args, deps) => {
72
73
  }
73
74
  return project;
74
75
  };
75
- const jobResult = (job) => ok(renderJob(job), { job: job });
76
+ const projectResult = (status) => ok(renderProjectStatus(status), { project: status });
77
+ /** A story's domain status, with what its card says that the status route does not carry. */
78
+ const storyResult = (status, card) => {
79
+ const landing = isLandingStory(card);
80
+ return ok(renderStoryStatus(status, { landing }), { story: status, ...(landing ? { landing } : {}) });
81
+ };
82
+ /** The project settings, led by what a save just did where one did. */
83
+ const settingsResult = (projectId, settings, lead) => ok((lead != null ? `${lead}\n\n` : '') + renderProjectSettings(projectId, settings), { projectId, settings: settings });
84
+ const pipelineResult = (status) => ok(renderPipelineStatus(status), { pipeline: status });
85
+ const conversionResult = (status) => ok(renderConversion(status), { conversion: status });
76
86
  /**
77
87
  * Attach a connector, where this host can hold one.
78
88
  *
@@ -123,36 +133,35 @@ const waitForStoryCleanup = async (deps, projectId) => {
123
133
  /** The project a tool acts on when it can also work without one. */
124
134
  const projectOrNull = (args, deps) => typeof args.projectId === 'string' ? args.projectId : deps.attached();
125
135
  /**
126
- * The question a PARKED run is waiting on, when the connector's own queue holds none.
136
+ * The question a parked domain operation is waiting on, when this session holds none.
127
137
  *
128
138
  * A question is delivered as an operation and answered against it, and that is the fast path. But
129
139
  * an operation expires: a run that asked while nobody was attached, or while this connector was
130
140
  * being restarted, is left `Waiting` with a question no queue here has ever seen. Without this the
131
- * only way back to it is the web application, and a parent polling the job would wait out the
132
- * whole timeout on a question it could have carried in seconds.
141
+ * only way back to it is the web application, and a parent watching only the run would wait out the
142
+ * whole interaction on a question it could have carried in seconds.
133
143
  *
134
144
  * Best-effort on both reads. A project with no conversion answers an error, and failing the call
135
145
  * that asked for a question because the lookup for it failed would be worse than answering "none".
136
146
  */
137
- const parkedQuestion = async (deps, projectId, jobId) => {
147
+ const parkedQuestion = async (deps, projectId) => {
138
148
  try {
139
- const status = await deps.api.convert.status(projectId);
149
+ const status = await deps.api.project.status(projectId);
140
150
  if (status.pendingInquiry != null)
141
151
  return status.pendingInquiry;
142
152
  }
143
153
  catch (e) {
144
- deps.log(`no conversion question for ${projectId}: ${e.message}`);
154
+ deps.log(`no project question for ${projectId}: ${e.message}`);
145
155
  }
146
- if (jobId == null)
147
- return null;
148
156
  try {
149
- const job = await deps.api.project.job(projectId, jobId);
150
- return job.inquiry ?? null;
157
+ const status = await deps.api.convert.status(projectId);
158
+ if (status.pendingInquiry != null)
159
+ return status.pendingInquiry;
151
160
  }
152
161
  catch (e) {
153
- deps.log(`no question on job ${jobId}: ${e.message}`);
154
- return null;
162
+ deps.log(`no conversion question for ${projectId}: ${e.message}`);
155
163
  }
164
+ return null;
156
165
  };
157
166
  const questionResult = (inquiry, deps, note) => ok((note != null ? `${note}\n\n` : '') + renderQuestionEnvelope(inquiry, { harness: deps.host.harness }), { questionId: inquiry.id });
158
167
  /** A cost estimate, always with the sentence that says what it is not. */
@@ -237,7 +246,7 @@ const renderCheck = (check, conversion) => {
237
246
  ? 'nothing — this origin cannot be converted'
238
247
  : conversion != null
239
248
  ? conversionNext(conversion)
240
- : 'convert_project to start, then poll the job with wait_for'}`);
249
+ : 'convert_project to start, then read conversion_status'}`);
241
250
  return lines.join('\n');
242
251
  };
243
252
  const renderConversion = (view) => {
@@ -264,47 +273,20 @@ const renderConversion = (view) => {
264
273
  + ` $${STORY_BAND_MIN_USD}–$${STORY_BAND_MAX_USD} each.`);
265
274
  }
266
275
  // Phrased, exactly like a thrown one: the platform writes a failed stage's cause here with
267
- // `describeFailure`, which for a refusal is the marker verbatim — so a conversion stopped by a
268
- // declined relocation answered this tool with `viable-agent-common:conversion:relocate-declined`
269
- // while `wait_for` on the very same run read out the sentence. One refusal, two readings.
276
+ // `describeFailure`, which for a refusal is the marker verbatim. Phrase the stored value the
277
+ // same way as a thrown refusal so every conversion status has one user-facing reading.
270
278
  if (view.lastError != null && view.lastError !== '') {
271
279
  lines.push(`error: ${refusalPhrase(view.lastError)}`);
272
280
  }
273
281
  lines.push(`next: ${conversionNext(view)}`);
274
282
  return lines.join('\n');
275
283
  };
276
- /**
277
- * What to call after reading a conversion.
278
- *
279
- * Derived from the STATUS first and the stage second, because the two say different things: a
280
- * stage says how far the conversion has got, and only the status says whether the platform is
281
- * working, waiting for a person, or waiting for a decision that is the user's.
282
- */
283
- const conversionNext = (view) => {
284
- switch (view.status) {
285
- case ConversionStatus.Running:
286
- return 'wait_for on the job, or conversion_status again';
287
- case ConversionStatus.Waiting:
288
- return 'next_question — this conversion needs a decision from the person you work for';
289
- case ConversionStatus.Awaiting:
290
- return `proceed_conversion { "decision": "${decisionFor(view.stage)}" } to go on, or`
291
- + ` { "decision": "${ConversionDecision.Leave}" } to keep what it has produced`;
292
- case ConversionStatus.Failed:
293
- return `proceed_conversion { "decision": "${ConversionDecision.Retry}" } once the cause is addressed`;
294
- case ConversionStatus.Done:
295
- return 'nothing — this conversion is finished. purge_origin deletes the original sources.';
296
- case ConversionStatus.Cancelled:
297
- return 'nothing — this conversion was cancelled';
298
- default:
299
- return 'convert_project to start it';
300
- }
301
- };
302
284
  /**
303
285
  * Everything a parent agent can ask the platform to do.
304
286
  *
305
287
  * Two rules shape the whole list. Every tool answers inside the strictest host's per-tool ceiling,
306
- * so anything that takes minutes returns a JOB at once and is polled — a coding agent that blocks
307
- * for twenty minutes is reported to its user as a hung server. And every tool that cannot work in
288
+ * so anything that takes minutes returns its current domain status and continues server-side. And
289
+ * every tool that cannot work in
308
290
  * a given mode is HIDDEN rather than failing, so the catalogue a parent reads is exactly the set
309
291
  * of things that work for it.
310
292
  */
@@ -313,9 +295,9 @@ export const catalogue = [
313
295
  name: 'describe_platform',
314
296
  title: 'What this platform can build, and what you can drive from here',
315
297
  description: 'Everything the platform runs — building an application from a description, implementing'
316
- + ' user stories, converting an application you already have — and which of it this session'
317
- + ' can start. Read it before deciding how to approach a request. Needs no project and'
318
- + ' makes no network call.',
298
+ + ' user stories, converting an application you already have — what every application it'
299
+ + ' generates carries, and which of it this session can start. Read it before deciding how to'
300
+ + ' approach a request. Needs no project and makes no network call.',
319
301
  input: {},
320
302
  availability: anyHost,
321
303
  run: async (_args, deps) => ok(renderPlatform(PLATFORM_CATALOGUE, deps.host)),
@@ -324,7 +306,8 @@ export const catalogue = [
324
306
  name: 'describe_capabilities',
325
307
  title: 'Report what your models can do',
326
308
  description: 'Tell the platform which of your models it may use, so it can size each call. Call this once,'
327
- + ' before creating or developing anything. Returns the mapping it will use.',
309
+ + ' before creating or developing anything. Returns the mapping it will use, and what every'
310
+ + ' generated application carries.',
328
311
  input: {
329
312
  strong: z.string().optional().describe('Your most capable model, e.g. the one you plan with'),
330
313
  standard: z.string().optional().describe('Your everyday model'),
@@ -351,7 +334,8 @@ export const catalogue = [
351
334
  return ok('No model was recorded: this tool takes `strong`, `standard` and `cheap` as TOP-LEVEL'
352
335
  + ' string arguments, not nested under another key. The platform will size every call'
353
336
  + ' with its own defaults until you send them.\n\n'
354
- + roleSummary(), { tiers, roles: rolesByTier() });
337
+ + roleSummary()
338
+ + `\n\n${GENERATED_SUMMARY}`, { tiers, roles: rolesByTier() });
355
339
  }
356
340
  const byTier = rolesByTier();
357
341
  return ok('Recorded. The platform asks for three power classes:\n'
@@ -364,13 +348,14 @@ export const catalogue = [
364
348
  // a delegated session performs all of them, an ordinary one performs a conversion's. Both
365
349
  // collect them the same way, so both are told to call next_task.
366
350
  + (performsModelTasks(deps.host)
367
- ? '\n\nThis session runs the platform\'s model calls on YOUR side. Whenever a job reports'
368
- + ' "blocked on: model-task", call next_task.'
351
+ ? '\n\nThis session runs the platform\'s model calls on YOUR side. Whenever a domain status'
352
+ + ' reports waiting for a model task, call next_task.'
369
353
  : sessionCapable(deps.host)
370
354
  ? '\n\nThe platform performs its own model calls for stories and free flight. A'
371
- + ' CONVERSION\'s calls are yours by default. Whenever a job reports "blocked on:'
372
- + ' model-task", call next_task.'
373
- : ''), { tiers, subagents, effortControl, roles: byTier });
355
+ + ' CONVERSION\'s calls are yours by default. Whenever a domain status reports'
356
+ + ' waiting for a model task, call next_task.'
357
+ : '')
358
+ + `\n\n${GENERATED_SUMMARY}`, { tiers, subagents, effortControl, roles: byTier });
374
359
  },
375
360
  },
376
361
  {
@@ -438,36 +423,17 @@ export const catalogue = [
438
423
  return ok(lines.join('\n'));
439
424
  },
440
425
  },
441
- {
442
- name: 'wait_for',
443
- title: 'Wait for a job',
444
- description: `Poll one job for up to ${JOB_POLL_MAX_SEC} seconds. Returns as soon as it finishes or`
445
- + ' becomes blocked; call again while it is still running.',
446
- input: {
447
- jobId: z.string(),
448
- projectId: z.string().optional(),
449
- maxWaitSec: z.number().int().min(0).max(JOB_POLL_MAX_SEC).optional(),
450
- },
451
- availability: anyHost,
452
- run: async (args, deps) => {
453
- const project = projectOf(args, deps);
454
- const wait = Math.min(args.maxWaitSec ?? JOB_POLL_MAX_SEC, JOB_POLL_MAX_SEC);
455
- const job = await deps.api.project.job(project, args.jobId, wait);
456
- return jobResult(job);
457
- },
458
- },
459
426
  {
460
427
  name: 'create_project',
461
428
  title: 'Start a project from a description',
462
429
  description: 'Describe the application in a sentence or two. The platform writes a specification, names it'
463
- + ' and drafts a vision. Returns a job — poll it with wait_for, then read the draft and call'
464
- + ' confirm_project.',
430
+ + ' and drafts a vision. Returns the project status; read the draft and call confirm_project.',
465
431
  input: { prompt: z.string().min(1) },
466
432
  availability: anyHost,
467
433
  run: async (args, deps) => {
468
- const job = await deps.api.project.create(args.prompt, deps.host.target === ConnectTarget.Local ? ConnectTarget.Local : ConnectTarget.Cloud);
469
- deps.attach(job.projectId);
470
- return jobResult(job);
434
+ const status = await deps.api.project.create(args.prompt, deps.host.target === ConnectTarget.Local ? ConnectTarget.Local : ConnectTarget.Cloud);
435
+ deps.attach(status.project.id);
436
+ return projectResult(status);
471
437
  },
472
438
  },
473
439
  {
@@ -475,7 +441,7 @@ export const catalogue = [
475
441
  title: 'Accept the specification and build the application',
476
442
  description: 'Confirm the drafted project, optionally editing what the analysis produced. This is what'
477
443
  + ' starts the build: the template lands, dependencies install, the whole application is'
478
- + ' drawn. Returns a job — it takes several minutes.',
444
+ + ' drawn. Returns the project status while the server-side run continues.',
479
445
  input: {
480
446
  projectId: z.string().optional(),
481
447
  name: z.string().optional(),
@@ -492,14 +458,14 @@ export const catalogue = [
492
458
  // whose connector has not filed a session yet is not queued for later — a local target
493
459
  // answers it with `ConnectSessionGone` and the step fails.
494
460
  await ensureSession(deps, project);
495
- const job = await deps.api.project.confirm(project, {
461
+ const status = await deps.api.project.confirm(project, {
496
462
  ...(typeof args.name === 'string' ? { name: args.name } : {}),
497
463
  ...(typeof args.description === 'string' ? { description: args.description } : {}),
498
464
  ...(typeof args.specification === 'string' ? { specification: args.specification } : {}),
499
465
  ...(typeof args.vision === 'string' ? { vision: args.vision } : {}),
500
466
  ...(typeof args.designSystem === 'string' ? { designSystem: args.designSystem } : {}),
501
467
  });
502
- return jobResult(job);
468
+ return projectResult(status);
503
469
  },
504
470
  },
505
471
  {
@@ -598,14 +564,63 @@ export const catalogue = [
598
564
  run: async (args, deps) => {
599
565
  const project = projectOf(args, deps);
600
566
  await ensureSession(deps, project);
601
- const job = await deps.api.project.reinit(project);
602
- return jobResult(job);
567
+ return projectResult(await deps.api.project.reinit(project));
568
+ },
569
+ },
570
+ {
571
+ name: 'project_settings',
572
+ title: 'The project\'s settings',
573
+ description: 'The settings a person edits on the project\'s control panel: the copyright line, the'
574
+ + ' organization name, the Terms and Privacy links, and the Google tag. A relative link such'
575
+ + ' as /terms or /privacy is normal — it is the page the platform generated inside the'
576
+ + ' application, and the generated pages name the organization and copyright set here.',
577
+ input: { projectId: z.string().optional() },
578
+ availability: anyHost,
579
+ run: async (args, deps) => {
580
+ const project = projectOf(args, deps);
581
+ return settingsResult(project, await deps.api.projectBranding(project));
603
582
  },
604
583
  },
584
+ {
585
+ name: 'update_project_settings',
586
+ title: 'Change the project\'s settings',
587
+ description: 'Change any of the copyright line, the organization name, the Terms and Privacy links and the'
588
+ + ' Google tag; whatever you leave out keeps its value. Copyright and organization are never'
589
+ + ' empty. A legal link is an https:// address or a path on the application itself — /terms'
590
+ + ' and /privacy are the generated pages. A Google tag is a GTM-, G-, GT-, AW- or DC- id, or'
591
+ + ' an empty string to remove it; it loads on the preview and in production behind the cookie'
592
+ + ' consent. The preview is rebuilt with the change; production takes it at the next Publish.',
593
+ input: {
594
+ projectId: z.string().optional(),
595
+ copyright: z.string().optional().describe('The copyright line, e.g. "© 2026 Acme Ltd"'),
596
+ organizationName: z.string().optional().describe('Who runs the application'),
597
+ termsUrl: z.string().optional().describe('https://… or a path such as /terms'),
598
+ privacyUrl: z.string().optional().describe('https://… or a path such as /privacy'),
599
+ googleTag: z.string().optional()
600
+ .describe('GTM-XXXXXXX, G-XXXXXXXXXX, GT-…, AW-… or DC-…; an empty string removes it'),
601
+ },
602
+ availability: anyHost,
603
+ run: async (args, deps) => await answering(deps, 'update_project_settings', async () => {
604
+ const project = projectOf(args, deps);
605
+ const patch = settingsPatch(args);
606
+ const changed = Object.keys(patch);
607
+ if (changed.length < 1) {
608
+ return fail('Nothing to change. Give at least one of copyright, organizationName, termsUrl, privacyUrl'
609
+ + ' or googleTag — project_settings shows the current values.');
610
+ }
611
+ // The save ends in a configuration push, and for a local project that push is an operation
612
+ // THIS connector answers — so it is attached before the platform is asked, exactly as a story
613
+ // mutation is. A host that holds no session skips it and the platform delivers the push itself.
614
+ await ensureSession(deps, project);
615
+ const saved = await deps.api.saveProjectBranding(project, patch);
616
+ return settingsResult(project, saved, `Saved ${changed.join(', ')}. ${settingsReach(deps.host.target)}`);
617
+ }),
618
+ },
605
619
  {
606
620
  name: 'list_stories',
607
621
  title: 'The user stories of a project',
608
- description: 'One page of the project\'s stories, with their status.',
622
+ description: 'One page of the project\'s stories, with their status, the primary story, and the landing'
623
+ + ' gate story a guest starts on the guest home.',
609
624
  input: {
610
625
  projectId: z.string().optional(),
611
626
  page: z.number().int().min(0).optional(),
@@ -656,7 +671,7 @@ export const catalogue = [
656
671
  const project = projectOf(args, deps);
657
672
  // Formatting reads the target's file-backed entities before it persists the story. A local
658
673
  // project therefore needs its connector back after an MCP restart even though this tool does
659
- // not itself return a long-running job.
674
+ // not itself start a long-running operation.
660
675
  await ensureSession(deps, project);
661
676
  // The narrative goes as written and with no area: rewriting it and deciding the area is the
662
677
  // platform's, done on the way in for a person's story — a connector that guessed an area
@@ -724,7 +739,7 @@ export const catalogue = [
724
739
  name: 'develop_story',
725
740
  title: 'Implement a user story',
726
741
  description: 'Ask the platform to design and build one story: its screens, its data, its endpoints, its'
727
- + ' navigation. This is the main event and takes many minutes. Returns a job.',
742
+ + ' navigation. This is the main event and takes many minutes. Returns the story status.',
728
743
  input: { storyId: z.string(), projectId: z.string().optional() },
729
744
  availability: anyHost,
730
745
  run: async (args, deps) => {
@@ -745,40 +760,38 @@ export const catalogue = [
745
760
  catch (e) {
746
761
  if (!(e instanceof CommitTimeout))
747
762
  throw e;
748
- // The move is durable and only its commit is late, so the job row is still the answer —
763
+ // The move is durable and only its commit is late, so the story status is still the answer —
749
764
  // waiting out the host's ceiling here would report a slow platform as a broken server.
750
765
  deps.log(`develop_story: the start of ${card.code ?? card.id} has not committed yet;`
751
- + ' answering from the job');
766
+ + ' answering from story status');
752
767
  }
753
- return jobResult(await deps.api.project.job(project, jobIdOf(ConnectJobKind.StoryDevelop, project, card.id)));
768
+ return storyResult(await deps.api.story.status(project, card.id), card);
754
769
  },
755
770
  },
756
771
  {
757
772
  name: 'story_status',
758
773
  title: 'What happened to a story',
759
- description: 'One story: its status, and the warning explaining a failure.',
774
+ description: 'One story, its development run, any warning explaining a failure, and whether it is the'
775
+ + ' project\'s landing gate story — the one a guest starts on the guest home.',
760
776
  input: { storyId: z.string(), projectId: z.string().optional() },
761
777
  availability: anyHost,
762
778
  run: async (args, deps) => {
763
- const card = await resolveStory(deps, projectOf(args, deps), args.storyId);
764
- // A warning is cleared by writing it empty, so an empty one is no warning at all.
765
- const warning = card.fields?.warning;
766
- return ok(`${card.code ?? card.id} · ${card.status}`
767
- + (warning != null && warning !== '' ? `\nwarning: ${String(warning)}` : ''), card);
779
+ const project = projectOf(args, deps);
780
+ const card = await resolveStory(deps, project, args.storyId);
781
+ return storyResult(await deps.api.story.status(project, card.id), card);
768
782
  },
769
783
  },
770
784
  {
771
785
  name: 'modify_project',
772
786
  title: 'Ask the agent for an open-ended change',
773
787
  description: 'Describe a change in words and let the platform\'s own coding agent make it. For anything'
774
- + ' that is not a user story: a rename, a fix, a styling change. Returns a job.',
788
+ + ' that is not a user story: a rename, a fix, a styling change. Returns project status.',
775
789
  input: { prompt: z.string().min(1), projectId: z.string().optional() },
776
790
  availability: anyHost,
777
791
  run: async (args, deps) => {
778
792
  const project = projectOf(args, deps);
779
793
  await ensureSession(deps, project);
780
- const job = await deps.api.project.modify(project, args.prompt);
781
- return jobResult(job);
794
+ return projectResult(await deps.api.project.modify(project, args.prompt));
782
795
  },
783
796
  },
784
797
  {
@@ -789,10 +802,7 @@ export const catalogue = [
789
802
  availability: anyHost,
790
803
  run: async (args, deps) => {
791
804
  const state = await deps.api.pipeline.state(projectOf(args, deps), args.runId);
792
- return ok(`${String(state.pipeline)} · ${String(state.status)}`
793
- + (state.step != null ? ` · stopped at ${String(state.step)}` : '')
794
- // A run stops on the same refusals a call is thrown, and the row keeps the cause as text.
795
- + (state.error != null ? `\nerror: ${refusalPhrase(String(state.error))}` : ''), state);
805
+ return pipelineResult(state);
796
806
  },
797
807
  },
798
808
  {
@@ -809,10 +819,10 @@ export const catalogue = [
809
819
  run: async (args, deps) => {
810
820
  const project = projectOf(args, deps);
811
821
  await ensureSession(deps, project);
812
- const job = await deps.api.pipeline.resume(project, args.runId, {
822
+ const status = await deps.api.pipeline.resume(project, args.runId, {
813
823
  ...(typeof args.from === 'string' ? { from: args.from } : {}),
814
824
  });
815
- return jobResult(job);
825
+ return pipelineResult(status);
816
826
  },
817
827
  },
818
828
  {
@@ -820,8 +830,8 @@ export const catalogue = [
820
830
  title: 'Get the next model call to perform',
821
831
  description: 'The platform hands you model calls to perform: a conversion\'s by default, and everything'
822
832
  + ' else when this session runs in the delegated mode. Returns one task with instructions for'
823
- + ' running it in a clean subagent, or says there is nothing yet. Call it whenever a job'
824
- + ' reports "blocked on: model-task", and keep calling until it says none.',
833
+ + ' running it in a clean subagent, or says there is nothing yet. Call it whenever a domain'
834
+ + ' status says it is waiting for a model task, and keep calling until it says none.',
825
835
  input: {
826
836
  maxWaitSec: z.number().int().min(0).max(45).optional(),
827
837
  taskId: z.string().optional()
@@ -837,7 +847,7 @@ export const catalogue = [
837
847
  // Asking for one by id re-reads it rather than taking a new one. A task is handed out once
838
848
  // and the platform waits on it for up to 45 minutes, so a parent that lost the envelope —
839
849
  // a compacted conversation, a subagent that died before answering — otherwise has no way
840
- // back to it and the job blocks until the deadline for no reason.
850
+ // back to it and the run waits until the deadline for no reason.
841
851
  if (typeof args.taskId === 'string') {
842
852
  const known = session.taskById(args.taskId);
843
853
  if (known == null) {
@@ -856,8 +866,8 @@ export const catalogue = [
856
866
  + ` ${outstanding.map(one => one.id).join(', ')}. Submit the answer with`
857
867
  + ' submit_task_result, or call next_task with that taskId to read it again.');
858
868
  }
859
- return ok('No task right now. If a job is still running, call wait_for; if it reported'
860
- + ' "blocked on: model-task", call next_task again.');
869
+ return ok('No task right now. Read the matching project, story, conversion, or pipeline status;'
870
+ + ' call next_task again only while it reports waiting for a model task.');
861
871
  }
862
872
  return ok(renderTaskEnvelope(task, { harness: deps.host.harness }), { taskId: task.id });
863
873
  },
@@ -889,20 +899,18 @@ export const catalogue = [
889
899
  const next = session.pendingTasks();
890
900
  return ok(next > 0
891
901
  ? `Accepted. ${next} more task(s) waiting — call next_task.`
892
- : 'Accepted. Call wait_for on the job, or next_task if it is still blocked.');
902
+ : 'Accepted. Read the matching project, story, conversion, or pipeline status.');
893
903
  },
894
904
  },
895
905
  {
896
906
  name: 'next_question',
897
907
  title: 'Get the question the platform is asking',
898
908
  description: 'The platform sometimes needs a decision only the person you are working for can make.'
899
- + ' Returns one question to put to them, or says there is none. Call it whenever a job'
900
- + ' reports "blocked on: question".',
909
+ + ' Returns one question to put to them, or says there is none. Domain status responses also'
910
+ + ' include parked questions directly.',
901
911
  input: {
902
912
  maxWaitSec: z.number().int().min(0).max(45).optional(),
903
913
  questionId: z.string().optional().describe('Re-read a question you were already given.'),
904
- jobId: z.string().optional()
905
- .describe('The job that reported "blocked on: question", if you have its id.'),
906
914
  projectId: z.string().optional(),
907
915
  },
908
916
  // Not `performsModelTasks`: a question has nothing to do with who performs the model calls,
@@ -912,12 +920,11 @@ export const catalogue = [
912
920
  run: async (args, deps) => {
913
921
  const session = await deps.session();
914
922
  const project = projectOrNull(args, deps);
915
- const jobId = typeof args.jobId === 'string' ? args.jobId : undefined;
916
923
  // Asking for one by id re-reads it rather than taking a new one — the same reason
917
924
  // `next_task` does: a parent that lost the envelope has no other way back to it.
918
925
  if (typeof args.questionId === 'string') {
919
926
  const known = session.questionById(args.questionId)
920
- ?? (project != null ? await parkedQuestion(deps, project, jobId) : null);
927
+ ?? (project != null ? await parkedQuestion(deps, project) : null);
921
928
  if (known == null || known.id !== args.questionId) {
922
929
  return fail(`No question ${args.questionId} is waiting. Call next_question with no id.`);
923
930
  }
@@ -935,12 +942,12 @@ export const catalogue = [
935
942
  }
936
943
  // Nothing queued here, which does not mean nothing is being asked: a run that parked while
937
944
  // this connector was away is waiting on a question whose operation has long expired.
938
- const parked = project != null ? await parkedQuestion(deps, project, jobId) : null;
945
+ const parked = project != null ? await parkedQuestion(deps, project) : null;
939
946
  if (parked != null) {
940
947
  return questionResult(parked, deps, 'This run parked on a question before this session attached. It is still open:');
941
948
  }
942
- return ok('No question right now. If a job is still running, call wait_for; if it reported'
943
- + ' "blocked on: question", call next_question again.');
949
+ return ok('No question right now. Read the matching domain status and answer any pendingInquiry it'
950
+ + ' returns; call next_question again only while it reports waiting for a person.');
944
951
  },
945
952
  },
946
953
  {
@@ -954,11 +961,6 @@ export const catalogue = [
954
961
  answer: z.union([z.string(), z.array(z.string())]).optional(),
955
962
  text: z.string().optional().describe('What they said, where the question takes words.'),
956
963
  declined: z.boolean().optional().describe('Nobody could decide this.'),
957
- // Taken here for the same reason `next_question` takes it, and it has to be the SAME reach:
958
- // a question this tool cannot find is refused after a person has already answered it, and
959
- // the parent is sent back to `next_question`, which would offer it again through the job.
960
- jobId: z.string().optional()
961
- .describe('The job that reported "blocked on: question", if you have its id.'),
962
964
  projectId: z.string().optional(),
963
965
  },
964
966
  availability: sessionCapable,
@@ -966,11 +968,10 @@ export const catalogue = [
966
968
  const session = await deps.session();
967
969
  const questionId = args.questionId;
968
970
  const project = projectOrNull(args, deps);
969
- const jobId = typeof args.jobId === 'string' ? args.jobId : undefined;
970
971
  // The question this session is holding, if it still is. That is what decides HOW the answer
971
972
  // travels: an operation the platform is waiting on, or the question's own id.
972
973
  const held = session.questionById(questionId);
973
- const inquiry = held ?? (project != null ? await parkedQuestion(deps, project, jobId) : null);
974
+ const inquiry = held ?? (project != null ? await parkedQuestion(deps, project) : null);
974
975
  if (inquiry == null || inquiry.id !== questionId) {
975
976
  return fail(`No question ${questionId} is waiting. Call next_question for the current one.`);
976
977
  }
@@ -989,7 +990,7 @@ export const catalogue = [
989
990
  }
990
991
  await deps.api.inquiry.answer(project, questionId, answer);
991
992
  }
992
- return ok('Recorded. Call wait_for on the job.');
993
+ return ok('Recorded. Read the matching project, story, conversion, or pipeline status.');
993
994
  },
994
995
  },
995
996
  {
@@ -1018,7 +1019,7 @@ export const catalogue = [
1018
1019
  title: 'Convert an application you already have',
1019
1020
  description: 'Bring an existing application onto the platform: it reads the code, restores the'
1020
1021
  + ' specification and the user stories nobody wrote down, and rebuilds it on the platform\'s'
1021
- + ' stack. The original is kept beside it. Returns a job, and stops at your decision after'
1022
+ + ' stack. The original is kept beside it. Returns conversion status and stops at your decision after'
1022
1023
  + ' each stage.',
1023
1024
  input: {
1024
1025
  projectId: z.string().optional().describe('Convert into a project that already exists.'),
@@ -1033,7 +1034,7 @@ export const catalogue = [
1033
1034
  const named = typeof args.projectId === 'string' ? args.projectId : deps.attached();
1034
1035
  if (named != null) {
1035
1036
  await ensureSession(deps, named);
1036
- return jobResult(await deps.api.convert.start(named));
1037
+ return conversionResult(await deps.api.convert.start(named));
1037
1038
  }
1038
1039
  const repoUrl = typeof args.repoUrl === 'string' && args.repoUrl.trim() !== ''
1039
1040
  ? args.repoUrl.trim()
@@ -1042,7 +1043,7 @@ export const catalogue = [
1042
1043
  return fail('Nothing to convert. Name a project, attach one, run the connector in the directory you'
1043
1044
  + ' want converted, or give a repoUrl.');
1044
1045
  }
1045
- const job = await deps.api.convert.create({
1046
+ const status = await deps.api.convert.create({
1046
1047
  ...(typeof args.name === 'string' ? { name: args.name } : {}),
1047
1048
  ...(typeof args.about === 'string' ? { about: args.about } : {}),
1048
1049
  // A NAMED repository outranks the directory this connector runs in. A stdio connector
@@ -1062,9 +1063,9 @@ export const catalogue = [
1062
1063
  },
1063
1064
  }),
1064
1065
  });
1065
- deps.attach(job.projectId);
1066
- await ensureSession(deps, job.projectId);
1067
- return jobResult(await deps.api.convert.start(job.projectId));
1066
+ deps.attach(status.projectId);
1067
+ await ensureSession(deps, status.projectId);
1068
+ return conversionResult(await deps.api.convert.start(status.projectId));
1068
1069
  }),
1069
1070
  },
1070
1071
  {
@@ -1072,7 +1073,7 @@ export const catalogue = [
1072
1073
  title: 'Decide what a conversion does next',
1073
1074
  description: 'A conversion stops after each stage and waits for a decision: analyze what was read,'
1074
1075
  + ' extract the user stories, implement them, leave it as it stands, retry a stage that'
1075
- + ' failed, or cancel. Returns a job for anything that runs.',
1076
+ + ' failed, or cancel. Returns the conversion status.',
1076
1077
  input: {
1077
1078
  decision: z.enum(Object.values(ConversionDecision)),
1078
1079
  projectId: z.string().optional(),
@@ -1082,8 +1083,8 @@ export const catalogue = [
1082
1083
  run: async (args, deps) => await answering(deps, 'proceed_conversion', async () => {
1083
1084
  const project = projectOf(args, deps);
1084
1085
  await ensureSession(deps, project);
1085
- const job = await deps.api.convert.proceed(project, args.decision, typeof args.note === 'string' ? args.note : undefined);
1086
- return jobResult(job);
1086
+ const status = await deps.api.convert.proceed(project, args.decision, typeof args.note === 'string' ? args.note : undefined);
1087
+ return conversionResult(status);
1087
1088
  }),
1088
1089
  },
1089
1090
  {
@@ -1117,7 +1118,7 @@ export const catalogue = [
1117
1118
  }
1118
1119
  const project = projectOf(args, deps);
1119
1120
  await ensureSession(deps, project);
1120
- return jobResult(await deps.api.convert.purge(project));
1121
+ return conversionResult(await deps.api.convert.purge(project));
1121
1122
  }),
1122
1123
  },
1123
1124
  {