@distributionos/cli 0.1.18 → 0.1.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
@@ -22,8 +22,12 @@ distributionos setup --app <appId> --apply
22
22
  distributionos login --app <appId>
23
23
  distributionos verify --app <appId> --url <liveUrl> [--content-id <id>]
24
24
  distributionos report-implementation --app <appId> --artifact <artifactId> [--url <liveUrl>]
25
+ distributionos start-work --app <appId> [--task-type <type>] [--artifact <artifactId>] [--dedup-key <key>]
26
+ distributionos complete-work --app <appId> --work-session <id> --status <status> --summary <text> [--url <liveUrl>] [--file <path>] [--pr-url <url>]
25
27
  ```
26
28
 
29
+ `start-work` and `complete-work` are compact fallbacks for agents or terminals that cannot call the MCP `start_agent_work` / `complete_agent_work` tools directly. Use one work session per meaningful task, not per prompt.
30
+
27
31
  ## Safety
28
32
 
29
33
  - Uses DistributionOS MCP OAuth as the happy path.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@distributionos/cli",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "DistributionOS repo setup CLI for agent onboarding and first-party analytics.",
5
5
  "homepage": "https://distributionos.dev",
6
6
  "repository": {
package/src/api.js CHANGED
@@ -252,6 +252,65 @@ export async function reportImplementation(input) {
252
252
  return body?.data ?? body;
253
253
  }
254
254
 
255
+ export async function startAgentWork(input) {
256
+ const token = await resolveAuthToken({
257
+ env: input.env,
258
+ apiBase: input.apiBase ?? DEFAULT_API_BASE,
259
+ appId: input.appId,
260
+ fetchImpl: input.fetch,
261
+ });
262
+ if (!token) {
263
+ throw new Error('No stored OAuth token or env token was available.');
264
+ }
265
+
266
+ const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
267
+ const response = await input.fetch(`${apiBase}/api/v1/apps/${encodeURIComponent(input.appId)}/agent-work`, {
268
+ method: 'POST',
269
+ headers: {
270
+ Authorization: `Bearer ${token.value}`,
271
+ 'Content-Type': 'application/json',
272
+ Accept: 'application/json',
273
+ },
274
+ body: JSON.stringify(input.payload),
275
+ });
276
+ const body = await response.json().catch(() => null);
277
+ if (!response.ok || body?.success === false) {
278
+ throw new Error(body?.error || `Agent work start failed with HTTP ${response.status}`);
279
+ }
280
+ return body?.data ?? body;
281
+ }
282
+
283
+ export async function completeAgentWork(input) {
284
+ const token = await resolveAuthToken({
285
+ env: input.env,
286
+ apiBase: input.apiBase ?? DEFAULT_API_BASE,
287
+ appId: input.appId,
288
+ fetchImpl: input.fetch,
289
+ });
290
+ if (!token) {
291
+ throw new Error('No stored OAuth token or env token was available.');
292
+ }
293
+ if (!input.workSessionId) {
294
+ throw new Error('workSessionId is required.');
295
+ }
296
+
297
+ const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
298
+ const response = await input.fetch(`${apiBase}/api/v1/apps/${encodeURIComponent(input.appId)}/agent-work/${encodeURIComponent(input.workSessionId)}/complete`, {
299
+ method: 'POST',
300
+ headers: {
301
+ Authorization: `Bearer ${token.value}`,
302
+ 'Content-Type': 'application/json',
303
+ Accept: 'application/json',
304
+ },
305
+ body: JSON.stringify(input.payload),
306
+ });
307
+ const body = await response.json().catch(() => null);
308
+ if (!response.ok || body?.success === false) {
309
+ throw new Error(body?.error || `Agent work closeout failed with HTTP ${response.status}`);
310
+ }
311
+ return body?.data ?? body;
312
+ }
313
+
255
314
  async function fetchJson({ fetchImpl, apiBase, token, path }) {
256
315
  if (typeof fetchImpl !== 'function') {
257
316
  throw new Error('fetch is unavailable in this Node runtime');
package/src/bootstrap.js CHANGED
@@ -24,6 +24,10 @@ export function buildLocalBootstrapBlock(version = CURRENT_BOOTSTRAP_VERSION, ap
24
24
  '',
25
25
  'This repo is linked to DistributionOS.',
26
26
  '',
27
+ 'Session preflight: when starting a fresh agent chat in this repo, call check_distributionos_connection first, then fetch the current instructions below, then call list_agent_alerts. If DistributionOS returns setupUpdate or setupUpdateGuidance with a status other than current or not_installed, tell the user the repo has a setup update available, show the review command, and run the apply command only after the user approves.',
28
+ '',
29
+ 'Startup alerts: after get_agent_instructions, call list_agent_alerts for this app. If alerts are returned, summarize them to the user before unrelated work. For SEO incidents, call get_seo_incident for full evidence before editing code, report each fix attempt with report_seo_incident_attempt, and verify with verify_seo_incident_fix.',
30
+ '',
27
31
  'Hard preflight: before editing files, creating assets, generating images, creating HyperFrames launch/demo videos, or changing copy for SEO, blog, launch, content, visual asset, competitor, social, email, or growth work, fetch the current instructions from DistributionOS:',
28
32
  '',
29
33
  instructionCall,
package/src/cli.js CHANGED
@@ -1,13 +1,15 @@
1
- import { spawn } from 'node:child_process';
2
- import { existsSync, readdirSync, statSync } from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { CLI_LOCAL_VERSION, DEFAULT_API_BASE } from './constants.js';
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, readdirSync, statSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { CLI_LOCAL_VERSION, DEFAULT_API_BASE } from './constants.js';
6
6
  import {
7
7
  ensureAnalyticsSite,
8
+ completeAgentWork,
8
9
  reportSetupInstallation,
9
10
  reportImplementation,
10
11
  resolveAuthToken,
12
+ startAgentWork,
11
13
  verifyAnalyticsInstall,
12
14
  } from './api.js';
13
15
  import { loginWithOAuth } from './oauth.js';
@@ -132,6 +134,74 @@ export async function runCli(argv, runtime) {
132
134
  }
133
135
  }
134
136
 
137
+ if (args.command === 'start-work') {
138
+ if (!args.appId) {
139
+ stderr.write('Missing required --app <appId>.\n\n');
140
+ stderr.write(helpText());
141
+ return 1;
142
+ }
143
+
144
+ const authReady = await ensureCliAuth({ args, runtime, stderr });
145
+ if (!authReady) return 1;
146
+
147
+ try {
148
+ const result = await startAgentWork({
149
+ appId: args.appId,
150
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
151
+ env: runtime.env,
152
+ fetch: runtime.fetch,
153
+ payload: buildStartWorkPayload(args),
154
+ });
155
+ stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatStartWorkResult(result));
156
+ return 0;
157
+ } catch (error) {
158
+ stderr.write(`${error instanceof Error ? error.message : 'Agent work start failed'}\n`);
159
+ return 1;
160
+ }
161
+ }
162
+
163
+ if (args.command === 'complete-work') {
164
+ if (!args.appId) {
165
+ stderr.write('Missing required --app <appId>.\n\n');
166
+ stderr.write(helpText());
167
+ return 1;
168
+ }
169
+ if (!args.workSessionId) {
170
+ stderr.write('Missing required --work-session <workSessionId>.\n\n');
171
+ stderr.write(helpText());
172
+ return 1;
173
+ }
174
+ if (!args.status) {
175
+ stderr.write('Missing required --status <completed|shipped|reported|blocked|skipped|no_op|failed>.\n\n');
176
+ stderr.write(helpText());
177
+ return 1;
178
+ }
179
+ if (args.status !== 'no_op' && !args.summary) {
180
+ stderr.write('Missing required --summary <text> unless --status no_op is used.\n\n');
181
+ stderr.write(helpText());
182
+ return 1;
183
+ }
184
+
185
+ const authReady = await ensureCliAuth({ args, runtime, stderr });
186
+ if (!authReady) return 1;
187
+
188
+ try {
189
+ const result = await completeAgentWork({
190
+ appId: args.appId,
191
+ workSessionId: args.workSessionId,
192
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
193
+ env: runtime.env,
194
+ fetch: runtime.fetch,
195
+ payload: buildCompleteWorkPayload(args),
196
+ });
197
+ stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatCompleteWorkResult(result));
198
+ return 0;
199
+ } catch (error) {
200
+ stderr.write(`${error instanceof Error ? error.message : 'Agent work closeout failed'}\n`);
201
+ return 1;
202
+ }
203
+ }
204
+
135
205
  if (args.command !== 'setup') {
136
206
  stderr.write(`Unknown command: ${args.command}\n\n`);
137
207
  stderr.write(helpText());
@@ -366,8 +436,23 @@ export function parseArgs(rawArgs) {
366
436
  contentId: null,
367
437
  contractVersion: null,
368
438
  artifactId: null,
439
+ sourceArtifactId: null,
440
+ workSessionId: null,
441
+ taskType: null,
442
+ dedupKey: null,
369
443
  status: null,
370
444
  summary: null,
445
+ commitSha: null,
446
+ prUrl: null,
447
+ deploymentUrl: null,
448
+ deploymentId: null,
449
+ implementedType: null,
450
+ changedFiles: [],
451
+ changedRoutes: [],
452
+ liveUrls: [],
453
+ unknowns: [],
454
+ risks: [],
455
+ nextSteps: [],
371
456
  analyticsOptOutReason: null,
372
457
  help: false,
373
458
  version: false,
@@ -417,10 +502,41 @@ export function parseArgs(rawArgs) {
417
502
  else if (arg.startsWith('--contract-version=')) parsed.contractVersion = arg.slice('--contract-version='.length);
418
503
  else if (arg === '--artifact') parsed.artifactId = args[++index] ?? null;
419
504
  else if (arg.startsWith('--artifact=')) parsed.artifactId = arg.slice('--artifact='.length);
505
+ else if (arg === '--source-artifact') parsed.sourceArtifactId = args[++index] ?? null;
506
+ else if (arg.startsWith('--source-artifact=')) parsed.sourceArtifactId = arg.slice('--source-artifact='.length);
507
+ else if (arg === '--work-session' || arg === '--session') parsed.workSessionId = args[++index] ?? null;
508
+ else if (arg.startsWith('--work-session=')) parsed.workSessionId = arg.slice('--work-session='.length);
509
+ else if (arg.startsWith('--session=')) parsed.workSessionId = arg.slice('--session='.length);
510
+ else if (arg === '--task-type') parsed.taskType = args[++index] ?? null;
511
+ else if (arg.startsWith('--task-type=')) parsed.taskType = arg.slice('--task-type='.length);
512
+ else if (arg === '--dedup-key') parsed.dedupKey = args[++index] ?? null;
513
+ else if (arg.startsWith('--dedup-key=')) parsed.dedupKey = arg.slice('--dedup-key='.length);
420
514
  else if (arg === '--status') parsed.status = args[++index] ?? null;
421
515
  else if (arg.startsWith('--status=')) parsed.status = arg.slice('--status='.length);
422
516
  else if (arg === '--summary') parsed.summary = args[++index] ?? null;
423
517
  else if (arg.startsWith('--summary=')) parsed.summary = arg.slice('--summary='.length);
518
+ else if (arg === '--commit') parsed.commitSha = args[++index] ?? null;
519
+ else if (arg.startsWith('--commit=')) parsed.commitSha = arg.slice('--commit='.length);
520
+ else if (arg === '--pr-url') parsed.prUrl = args[++index] ?? null;
521
+ else if (arg.startsWith('--pr-url=')) parsed.prUrl = arg.slice('--pr-url='.length);
522
+ else if (arg === '--deployment-url') parsed.deploymentUrl = args[++index] ?? null;
523
+ else if (arg.startsWith('--deployment-url=')) parsed.deploymentUrl = arg.slice('--deployment-url='.length);
524
+ else if (arg === '--deployment-id') parsed.deploymentId = args[++index] ?? null;
525
+ else if (arg.startsWith('--deployment-id=')) parsed.deploymentId = arg.slice('--deployment-id='.length);
526
+ else if (arg === '--implemented-type') parsed.implementedType = args[++index] ?? null;
527
+ else if (arg.startsWith('--implemented-type=')) parsed.implementedType = arg.slice('--implemented-type='.length);
528
+ else if (arg === '--file') parsed.changedFiles.push(args[++index] ?? '');
529
+ else if (arg.startsWith('--file=')) parsed.changedFiles.push(arg.slice('--file='.length));
530
+ else if (arg === '--route') parsed.changedRoutes.push(args[++index] ?? '');
531
+ else if (arg.startsWith('--route=')) parsed.changedRoutes.push(arg.slice('--route='.length));
532
+ else if (arg === '--live-url') parsed.liveUrls.push(args[++index] ?? '');
533
+ else if (arg.startsWith('--live-url=')) parsed.liveUrls.push(arg.slice('--live-url='.length));
534
+ else if (arg === '--unknown') parsed.unknowns.push(args[++index] ?? '');
535
+ else if (arg.startsWith('--unknown=')) parsed.unknowns.push(arg.slice('--unknown='.length));
536
+ else if (arg === '--risk') parsed.risks.push(args[++index] ?? '');
537
+ else if (arg.startsWith('--risk=')) parsed.risks.push(arg.slice('--risk='.length));
538
+ else if (arg === '--next-step') parsed.nextSteps.push(args[++index] ?? '');
539
+ else if (arg.startsWith('--next-step=')) parsed.nextSteps.push(arg.slice('--next-step='.length));
424
540
  else if (arg === '--analytics-opt-out') parsed.analyticsOptOutReason = args[++index] ?? null;
425
541
  else if (arg.startsWith('--analytics-opt-out=')) parsed.analyticsOptOutReason = arg.slice('--analytics-opt-out='.length);
426
542
  else throw new Error(`Unknown argument: ${arg}`);
@@ -439,9 +555,11 @@ function helpText() {
439
555
  ' distributionos login --app <appId> [--api-base <url>]',
440
556
  ' distributionos verify --app <appId> --url <liveUrl> [--content-id <id>]',
441
557
  ' distributionos report-implementation --app <appId> --artifact <artifactId> [--url <liveUrl>] [--summary <text>]',
558
+ ' distributionos start-work --app <appId> [--task-type <type>] [--artifact <artifactId>] [--dedup-key <key>] [--summary <text>]',
559
+ ' distributionos complete-work --app <appId> --work-session <id> --status <status> --summary <text> [--url <liveUrl>] [--file <path>] [--pr-url <url>]',
442
560
  '',
443
561
  'Default mode reviews the setup plan without changing files. Run again with --apply after the plan looks right.',
444
- 'For Codex, --agent codex registers the DistributionOS MCP server, uses the Codex Desktop CLI when available, and runs MCP login before repo setup.',
562
+ 'For Codex, --agent codex registers the DistributionOS MCP server, uses the Codex Desktop CLI when available, and runs MCP login before repo setup.',
445
563
  'If the repo has existing uncommitted changes, commit/stash first or use --allow-dirty when you recognize them.',
446
564
  '',
447
565
  'Authentication:',
@@ -463,83 +581,83 @@ function parseAgent(value) {
463
581
  async function ensureRequestedAgentSetup({ args, runtime, stdout, stderr }) {
464
582
  if (args.skipAgentSetup || args.agent !== 'codex') {
465
583
  return { ok: true, requiresAgentRestart: false };
466
- }
467
-
468
- stdout.write('Codex MCP setup\n');
469
- const codexCommand = resolveCodexCommand(runtime);
470
- stdout.write(`- Using ${codexCommand.label}.\n`);
471
- const getResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'get', DISTRIBUTIONOS_MCP_NAME]);
472
- if (getResult.ok) {
473
- if (!getResult.stdout.includes(DISTRIBUTIONOS_MCP_URL)) {
474
- stderr.write([
475
- 'Codex already has a distributionos MCP server, but it does not point to the expected DistributionOS URL.',
476
- `Expected: ${DISTRIBUTIONOS_MCP_URL}`,
477
- `Codex CLI used: ${codexCommand.command}`,
478
- 'Remove the old distributionos MCP server from Codex, then rerun this setup command.',
479
- '',
480
- ].join('\n'));
481
- return { ok: false, requiresAgentRestart: false };
584
+ }
585
+
586
+ stdout.write('Codex MCP setup\n');
587
+ const codexCommand = resolveCodexCommand(runtime);
588
+ stdout.write(`- Using ${codexCommand.label}.\n`);
589
+ const getResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'get', DISTRIBUTIONOS_MCP_NAME]);
590
+ if (getResult.ok) {
591
+ if (!getResult.stdout.includes(DISTRIBUTIONOS_MCP_URL)) {
592
+ stderr.write([
593
+ 'Codex already has a distributionos MCP server, but it does not point to the expected DistributionOS URL.',
594
+ `Expected: ${DISTRIBUTIONOS_MCP_URL}`,
595
+ `Codex CLI used: ${codexCommand.command}`,
596
+ 'Remove the old distributionos MCP server from Codex, then rerun this setup command.',
597
+ '',
598
+ ].join('\n'));
599
+ return { ok: false, requiresAgentRestart: false };
482
600
  }
483
601
  stdout.write('- DistributionOS MCP server is already registered in Codex.\n');
484
- } else if (isCommandMissing(getResult.error)) {
485
- stderr.write([
486
- 'Codex CLI was not found, so DistributionOS cannot connect Codex automatically.',
487
- 'Open this repo in Codex Desktop or install the Codex CLI, then rerun setup.',
602
+ } else if (isCommandMissing(getResult.error)) {
603
+ stderr.write([
604
+ 'Codex CLI was not found, so DistributionOS cannot connect Codex automatically.',
605
+ 'Open this repo in Codex Desktop or install the Codex CLI, then rerun setup.',
488
606
  '',
489
607
  ].join('\n'));
490
608
  return { ok: false, requiresAgentRestart: false };
491
- } else {
492
- stdout.write('- Adding DistributionOS MCP server to Codex.\n');
493
- const addResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'add', DISTRIBUTIONOS_MCP_NAME, '--url', DISTRIBUTIONOS_MCP_URL], { stream: true });
494
- if (!addResult.ok) {
495
- stderr.write(formatCodexCommandFailure('Codex MCP add', addResult, codexCommand));
496
- return { ok: false, requiresAgentRestart: false };
497
- }
498
- }
499
-
500
- stdout.write('- Starting Codex MCP OAuth. Approve the browser sign-in if Codex asks.\n');
501
- const loginResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'login', DISTRIBUTIONOS_MCP_NAME], { stream: true });
502
- if (!loginResult.ok) {
503
- stderr.write(formatCodexCommandFailure('Codex MCP login', loginResult, codexCommand));
504
- return { ok: false, requiresAgentRestart: true };
505
- }
506
- const listResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'list']);
507
- if (!listResult.ok) {
508
- stderr.write(formatCodexCommandFailure('Codex MCP verification', listResult, codexCommand));
509
- return { ok: false, requiresAgentRestart: true };
510
- }
511
- if (!codexMcpListShowsDistributionOs(listResult.stdout)) {
512
- stderr.write([
513
- 'Codex MCP login finished, but this Codex runtime does not list DistributionOS yet.',
514
- `Codex CLI used: ${codexCommand.command}`,
515
- 'Fully quit and reopen Codex Desktop, then rerun the Codex setup message from the app repo.',
516
- '',
517
- ].join('\n'));
518
- return { ok: false, requiresAgentRestart: true };
519
- }
520
- if (codexMcpListShowsDistributionOsNotLoggedIn(listResult.stdout)) {
521
- stderr.write([
522
- 'Codex MCP login finished, but this Codex runtime still reports DistributionOS as Not logged in.',
523
- `Codex CLI used: ${codexCommand.command}`,
524
- 'Rerun the Codex setup message and approve the current browser sign-in while the command is waiting.',
525
- '',
526
- ].join('\n'));
527
- return { ok: false, requiresAgentRestart: true };
528
- }
529
- stdout.write('- Codex MCP OAuth step completed and verified.\n\n');
530
- return { ok: true, requiresAgentRestart: true };
531
- }
532
-
533
- function formatCodexCommandFailure(label, result, codexCommand) {
534
- const output = oneLine(result.stderr || result.stdout || result.error?.message || '');
535
- return [
536
- `${label} did not complete, so Codex cannot use DistributionOS tools yet.`,
537
- output ? `Last output: ${output}` : '',
538
- codexCommand?.command ? `Codex CLI used: ${codexCommand.command}` : '',
539
- 'Rerun the Codex setup message, approve the browser sign-in, and wait for the command to say the Codex MCP OAuth step completed and verified.',
540
- '',
541
- ].filter(Boolean).join('\n');
542
- }
609
+ } else {
610
+ stdout.write('- Adding DistributionOS MCP server to Codex.\n');
611
+ const addResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'add', DISTRIBUTIONOS_MCP_NAME, '--url', DISTRIBUTIONOS_MCP_URL], { stream: true });
612
+ if (!addResult.ok) {
613
+ stderr.write(formatCodexCommandFailure('Codex MCP add', addResult, codexCommand));
614
+ return { ok: false, requiresAgentRestart: false };
615
+ }
616
+ }
617
+
618
+ stdout.write('- Starting Codex MCP OAuth. Approve the browser sign-in if Codex asks.\n');
619
+ const loginResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'login', DISTRIBUTIONOS_MCP_NAME], { stream: true });
620
+ if (!loginResult.ok) {
621
+ stderr.write(formatCodexCommandFailure('Codex MCP login', loginResult, codexCommand));
622
+ return { ok: false, requiresAgentRestart: true };
623
+ }
624
+ const listResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'list']);
625
+ if (!listResult.ok) {
626
+ stderr.write(formatCodexCommandFailure('Codex MCP verification', listResult, codexCommand));
627
+ return { ok: false, requiresAgentRestart: true };
628
+ }
629
+ if (!codexMcpListShowsDistributionOs(listResult.stdout)) {
630
+ stderr.write([
631
+ 'Codex MCP login finished, but this Codex runtime does not list DistributionOS yet.',
632
+ `Codex CLI used: ${codexCommand.command}`,
633
+ 'Fully quit and reopen Codex Desktop, then rerun the Codex setup message from the app repo.',
634
+ '',
635
+ ].join('\n'));
636
+ return { ok: false, requiresAgentRestart: true };
637
+ }
638
+ if (codexMcpListShowsDistributionOsNotLoggedIn(listResult.stdout)) {
639
+ stderr.write([
640
+ 'Codex MCP login finished, but this Codex runtime still reports DistributionOS as Not logged in.',
641
+ `Codex CLI used: ${codexCommand.command}`,
642
+ 'Rerun the Codex setup message and approve the current browser sign-in while the command is waiting.',
643
+ '',
644
+ ].join('\n'));
645
+ return { ok: false, requiresAgentRestart: true };
646
+ }
647
+ stdout.write('- Codex MCP OAuth step completed and verified.\n\n');
648
+ return { ok: true, requiresAgentRestart: true };
649
+ }
650
+
651
+ function formatCodexCommandFailure(label, result, codexCommand) {
652
+ const output = oneLine(result.stderr || result.stdout || result.error?.message || '');
653
+ return [
654
+ `${label} did not complete, so Codex cannot use DistributionOS tools yet.`,
655
+ output ? `Last output: ${output}` : '',
656
+ codexCommand?.command ? `Codex CLI used: ${codexCommand.command}` : '',
657
+ 'Rerun the Codex setup message, approve the browser sign-in, and wait for the command to say the Codex MCP OAuth step completed and verified.',
658
+ '',
659
+ ].filter(Boolean).join('\n');
660
+ }
543
661
 
544
662
  function isCommandMissing(error) {
545
663
  const message = error?.message ?? '';
@@ -553,85 +671,85 @@ function isCommandMissing(error) {
553
671
  );
554
672
  }
555
673
 
556
- function resolveCodexCommand(runtime) {
557
- const env = runtime?.env ?? process.env;
558
- const configured = typeof env.CODEX_CLI_PATH === 'string' ? env.CODEX_CLI_PATH.trim() : '';
559
- if (configured) {
560
- return { command: configured, label: `Codex CLI at ${configured}` };
561
- }
562
-
563
- const desktopCommand = findCodexDesktopCommand(runtime);
564
- if (desktopCommand) {
565
- return { command: desktopCommand, label: `Codex Desktop CLI at ${desktopCommand}` };
566
- }
567
-
568
- return { command: 'codex', label: 'Codex CLI from PATH' };
569
- }
570
-
571
- function findCodexDesktopCommand(runtime) {
572
- if (Array.isArray(runtime?.codexCommandCandidates)) {
573
- return runtime.codexCommandCandidates.find((candidate) => typeof candidate === 'string' && candidate.trim()) ?? null;
574
- }
575
-
576
- const env = runtime?.env ?? process.env;
577
- const homeDir = typeof env.USERPROFILE === 'string' && env.USERPROFILE
578
- ? env.USERPROFILE
579
- : os.homedir();
580
- const localAppData = typeof env.LOCALAPPDATA === 'string' && env.LOCALAPPDATA
581
- ? env.LOCALAPPDATA
582
- : path.join(homeDir, 'AppData', 'Local');
583
- const codexBinRoot = path.join(localAppData, 'OpenAI', 'Codex', 'bin');
584
- const executableName = process.platform === 'win32' ? 'codex.exe' : 'codex';
585
- const candidates = [];
586
-
587
- const directCandidate = path.join(codexBinRoot, executableName);
588
- if (existsSync(directCandidate)) {
589
- candidates.push(directCandidate);
590
- }
591
-
592
- try {
593
- for (const entry of readdirSync(codexBinRoot, { withFileTypes: true })) {
594
- if (!entry.isDirectory()) continue;
595
- const candidate = path.join(codexBinRoot, entry.name, executableName);
596
- if (existsSync(candidate)) {
597
- candidates.push(candidate);
598
- }
599
- }
600
- } catch {
601
- return candidates[0] ?? null;
602
- }
603
-
604
- candidates.sort((left, right) => fileModifiedAt(right) - fileModifiedAt(left));
605
- return candidates[0] ?? null;
606
- }
607
-
608
- function fileModifiedAt(filePath) {
609
- try {
610
- return statSync(filePath).mtimeMs;
611
- } catch {
612
- return 0;
613
- }
614
- }
615
-
616
- function codexMcpListShowsDistributionOs(output) {
617
- return Boolean(findCodexMcpListDistributionOsLine(output));
618
- }
619
-
620
- function codexMcpListShowsDistributionOsNotLoggedIn(output) {
621
- const line = findCodexMcpListDistributionOsLine(output);
622
- return Boolean(line && /not\s+logged\s+in/i.test(line));
623
- }
624
-
625
- function findCodexMcpListDistributionOsLine(output) {
626
- return String(output)
627
- .split(/\r?\n/)
628
- .map((line) => line.trim())
629
- .find((line) => /\bdistributionos\b/i.test(line)) ?? null;
630
- }
631
-
632
- async function runCodexCommand(runtime, codexCommand, args, options = {}) {
633
- return runExternalCommand(runtime, codexCommand.command, args, options);
634
- }
674
+ function resolveCodexCommand(runtime) {
675
+ const env = runtime?.env ?? process.env;
676
+ const configured = typeof env.CODEX_CLI_PATH === 'string' ? env.CODEX_CLI_PATH.trim() : '';
677
+ if (configured) {
678
+ return { command: configured, label: `Codex CLI at ${configured}` };
679
+ }
680
+
681
+ const desktopCommand = findCodexDesktopCommand(runtime);
682
+ if (desktopCommand) {
683
+ return { command: desktopCommand, label: `Codex Desktop CLI at ${desktopCommand}` };
684
+ }
685
+
686
+ return { command: 'codex', label: 'Codex CLI from PATH' };
687
+ }
688
+
689
+ function findCodexDesktopCommand(runtime) {
690
+ if (Array.isArray(runtime?.codexCommandCandidates)) {
691
+ return runtime.codexCommandCandidates.find((candidate) => typeof candidate === 'string' && candidate.trim()) ?? null;
692
+ }
693
+
694
+ const env = runtime?.env ?? process.env;
695
+ const homeDir = typeof env.USERPROFILE === 'string' && env.USERPROFILE
696
+ ? env.USERPROFILE
697
+ : os.homedir();
698
+ const localAppData = typeof env.LOCALAPPDATA === 'string' && env.LOCALAPPDATA
699
+ ? env.LOCALAPPDATA
700
+ : path.join(homeDir, 'AppData', 'Local');
701
+ const codexBinRoot = path.join(localAppData, 'OpenAI', 'Codex', 'bin');
702
+ const executableName = process.platform === 'win32' ? 'codex.exe' : 'codex';
703
+ const candidates = [];
704
+
705
+ const directCandidate = path.join(codexBinRoot, executableName);
706
+ if (existsSync(directCandidate)) {
707
+ candidates.push(directCandidate);
708
+ }
709
+
710
+ try {
711
+ for (const entry of readdirSync(codexBinRoot, { withFileTypes: true })) {
712
+ if (!entry.isDirectory()) continue;
713
+ const candidate = path.join(codexBinRoot, entry.name, executableName);
714
+ if (existsSync(candidate)) {
715
+ candidates.push(candidate);
716
+ }
717
+ }
718
+ } catch {
719
+ return candidates[0] ?? null;
720
+ }
721
+
722
+ candidates.sort((left, right) => fileModifiedAt(right) - fileModifiedAt(left));
723
+ return candidates[0] ?? null;
724
+ }
725
+
726
+ function fileModifiedAt(filePath) {
727
+ try {
728
+ return statSync(filePath).mtimeMs;
729
+ } catch {
730
+ return 0;
731
+ }
732
+ }
733
+
734
+ function codexMcpListShowsDistributionOs(output) {
735
+ return Boolean(findCodexMcpListDistributionOsLine(output));
736
+ }
737
+
738
+ function codexMcpListShowsDistributionOsNotLoggedIn(output) {
739
+ const line = findCodexMcpListDistributionOsLine(output);
740
+ return Boolean(line && /not\s+logged\s+in/i.test(line));
741
+ }
742
+
743
+ function findCodexMcpListDistributionOsLine(output) {
744
+ return String(output)
745
+ .split(/\r?\n/)
746
+ .map((line) => line.trim())
747
+ .find((line) => /\bdistributionos\b/i.test(line)) ?? null;
748
+ }
749
+
750
+ async function runCodexCommand(runtime, codexCommand, args, options = {}) {
751
+ return runExternalCommand(runtime, codexCommand.command, args, options);
752
+ }
635
753
 
636
754
  async function runExternalCommand(runtime, command, args, options = {}) {
637
755
  if (typeof runtime.execFile === 'function') {
@@ -694,15 +812,15 @@ async function runExternalCommand(runtime, command, args, options = {}) {
694
812
  });
695
813
  }
696
814
 
697
- function resolveCommand(command, args, runtime) {
698
- const platform = runtime.platform ?? process.platform;
699
- if (platform === 'win32') {
700
- if (path.isAbsolute(command) && /\.(exe|com)$/i.test(command)) {
701
- return { command, args };
702
- }
703
- return {
704
- command: runtime.env?.ComSpec ?? process.env.ComSpec ?? 'cmd.exe',
705
- args: ['/d', '/s', '/c', command, ...args],
815
+ function resolveCommand(command, args, runtime) {
816
+ const platform = runtime.platform ?? process.platform;
817
+ if (platform === 'win32') {
818
+ if (path.isAbsolute(command) && /\.(exe|com)$/i.test(command)) {
819
+ return { command, args };
820
+ }
821
+ return {
822
+ command: runtime.env?.ComSpec ?? process.env.ComSpec ?? 'cmd.exe',
823
+ args: ['/d', '/s', '/c', command, ...args],
706
824
  };
707
825
  }
708
826
  return { command, args };
@@ -764,6 +882,54 @@ function buildImplementationReportPayload(args) {
764
882
  };
765
883
  }
766
884
 
885
+ function buildStartWorkPayload(args) {
886
+ return {
887
+ taskType: args.taskType ?? 'general_distribution',
888
+ source: 'cli',
889
+ sourceTool: 'distributionos-cli',
890
+ ...(args.summary ? { userRequest: args.summary } : {}),
891
+ ...(args.artifactId ? { sourceArtifactId: args.artifactId } : {}),
892
+ ...(args.sourceArtifactId ? { sourceArtifactId: args.sourceArtifactId } : {}),
893
+ ...(args.dedupKey ? { dedupKey: args.dedupKey } : {}),
894
+ metadata: {
895
+ startedBy: 'distributionos-cli',
896
+ },
897
+ };
898
+ }
899
+
900
+ function buildCompleteWorkPayload(args) {
901
+ const liveUrls = [...args.liveUrls];
902
+ if (args.url) liveUrls.unshift(args.url);
903
+ return {
904
+ workSessionId: args.workSessionId,
905
+ status: args.status,
906
+ ...(args.summary ? { summary: args.summary } : {}),
907
+ changedFiles: compactArray(args.changedFiles),
908
+ changedRoutes: compactArray(args.changedRoutes),
909
+ liveUrls: compactArray(liveUrls),
910
+ ...(args.commitSha ? { commitSha: args.commitSha } : {}),
911
+ ...(args.prUrl ? { prUrl: args.prUrl } : {}),
912
+ ...(args.deploymentUrl ? { deploymentUrl: args.deploymentUrl } : {}),
913
+ ...(args.deploymentId ? { deploymentId: args.deploymentId } : {}),
914
+ ...(args.sourceArtifactId ? { sourceArtifactId: args.sourceArtifactId } : {}),
915
+ ...(args.artifactId ? { artifactId: args.artifactId } : {}),
916
+ ...(args.implementedType ? { implementedType: args.implementedType } : {}),
917
+ ...(args.url ? { publishedUrl: args.url } : {}),
918
+ ...(args.contentId ? { analyticsIds: { dosContentId: args.contentId } } : {}),
919
+ ...(args.analyticsOptOutReason ? { analyticsOptOutReason: args.analyticsOptOutReason } : {}),
920
+ unknowns: compactArray(args.unknowns),
921
+ risks: compactArray(args.risks),
922
+ nextSteps: compactArray(args.nextSteps),
923
+ metadata: {
924
+ reportedBy: 'distributionos-cli',
925
+ },
926
+ };
927
+ }
928
+
929
+ function compactArray(values) {
930
+ return values.map((value) => String(value ?? '').trim()).filter(Boolean);
931
+ }
932
+
767
933
  function formatVerifyResult(result) {
768
934
  const lines = [
769
935
  'DistributionOS analytics verification',
@@ -785,6 +951,39 @@ function formatVerifyResult(result) {
785
951
  return `${lines.join('\n')}\n`;
786
952
  }
787
953
 
954
+ function formatStartWorkResult(result) {
955
+ const session = result.session ?? result;
956
+ const closeout = result.closeout ?? {};
957
+ const lines = [
958
+ 'DistributionOS agent work started',
959
+ `- Work session: ${session.id ?? closeout.workSessionId ?? 'created'}`,
960
+ `- Task type: ${session.taskType ?? closeout.taskType ?? 'general_distribution'}`,
961
+ ];
962
+ if (result.reused) {
963
+ lines.push('- Reused existing open session for this dedup key.');
964
+ }
965
+ if (closeout.instruction) {
966
+ lines.push('- Closeout: call complete-work or complete_agent_work once when this meaningful task is done.');
967
+ }
968
+ return `${lines.join('\n')}\n`;
969
+ }
970
+
971
+ function formatCompleteWorkResult(result) {
972
+ const session = result.session ?? result;
973
+ const lines = [
974
+ 'DistributionOS agent work closeout',
975
+ `- Status: ${session.status ?? result.status ?? 'closed'}`,
976
+ `- Work session: ${session.id ?? result.workSessionId ?? 'updated'}`,
977
+ ];
978
+ if (Array.isArray(result.routed) && result.routed.length > 0) {
979
+ lines.push(`- Routed: ${result.routed.join(', ')}`);
980
+ }
981
+ if (result.idempotent) {
982
+ lines.push('- Duplicate closeout ignored; the session was already closed.');
983
+ }
984
+ return `${lines.join('\n')}\n`;
985
+ }
986
+
788
987
  function formatReportResult(result) {
789
988
  const artifact = result.artifact ?? result;
790
989
  const lines = [
@@ -858,11 +1057,11 @@ function formatApplyResult(plan, changes, initialization, validation, inspection
858
1057
  lines.push('', 'Next step');
859
1058
  if (reviewUrl) {
860
1059
  lines.push(
861
- '- Open this DistributionOS review page now:',
862
- ` ${reviewUrl}`,
863
- '- Codex users: if you used the Codex setup option, the CLI already ran MCP add/login against the Codex runtime and verified it did not report DistributionOS as Not logged in.',
864
- '- Then fully quit and reopen Codex Desktop before checking the connection.',
865
- '- Claude Code users: if .mcp.json was created or updated, restart or reconnect Claude Code and approve the project MCP server if asked.',
1060
+ '- Open this DistributionOS review page now:',
1061
+ ` ${reviewUrl}`,
1062
+ '- Codex users: if you used the Codex setup option, the CLI already ran MCP add/login against the Codex runtime and verified it did not report DistributionOS as Not logged in.',
1063
+ '- Then fully quit and reopen Codex Desktop before checking the connection.',
1064
+ '- Claude Code users: if .mcp.json was created or updated, restart or reconnect Claude Code and approve the project MCP server if asked.',
866
1065
  '- Other MCP-aware agents: reconnect the agent from this repo so it can load the DistributionOS MCP server.',
867
1066
  '- On the review page, copy the connection check prompt into the fresh/reconnected agent chat.',
868
1067
  '- If your agent only sees DistributionOS authentication tools, copy the auth prompt from the review page and complete OAuth before continuing.',
package/src/constants.js CHANGED
@@ -6,9 +6,9 @@ export const CLI_PACKAGE_NAME = '@distributionos/cli';
6
6
  export const CLI_LOCAL_VERSION = typeof cliPackageJson.version === 'string' ? cliPackageJson.version : '0.0.0-local';
7
7
  export const DEFAULT_API_BASE = 'https://distributionos.dev';
8
8
  export const MCP_SERVER_URL = 'https://distributionos.dev/api/mcp';
9
- export const CURRENT_BOOTSTRAP_VERSION = '2026.06.27';
10
- export const CURRENT_ANALYTICS_CONTRACT_VERSION = '2026.06.20';
11
- export const CURRENT_SETUP_CONTRACT_VERSION = '2026.07.01';
9
+ export const CURRENT_BOOTSTRAP_VERSION = '2026.07.09';
10
+ export const CURRENT_ANALYTICS_CONTRACT_VERSION = '2026.06.20';
11
+ export const CURRENT_SETUP_CONTRACT_VERSION = '2026.07.09';
12
12
 
13
13
  export const TOKEN_ENV_NAMES = [
14
14
  'DISTRIBUTIONOS_API_KEY',
@@ -16,6 +16,7 @@ const ANALYTICS_BLOCK_RE =
16
16
 
17
17
  export async function buildSetupReport(plan, phase = 'plan', details = {}) {
18
18
  const detectedCapabilities = new Set([
19
+ 'agent.workSessionCloseout',
19
20
  'analytics.verificationReady',
20
21
  'setup.reporting',
21
22
  ]);
@@ -23,10 +24,13 @@ export async function buildSetupReport(plan, phase = 'plan', details = {}) {
23
24
  let blockedReason = null;
24
25
 
25
26
  const bootstrapVersion = detectBootstrapVersion(plan);
26
- if (bootstrapVersion && plan.bootstrap.action === 'none') {
27
- detectedCapabilities.add('agent.bootstrap');
28
- detectedCapabilities.add('agent.liveInstructions');
29
- }
27
+ if (bootstrapVersion && plan.bootstrap.action === 'none') {
28
+ detectedCapabilities.add('agent.bootstrap');
29
+ detectedCapabilities.add('agent.liveInstructions');
30
+ if (compareVersion(bootstrapVersion, '2026.07.09') >= 0) {
31
+ detectedCapabilities.add('agent.startupAlerts');
32
+ }
33
+ }
30
34
  managedFiles.push({
31
35
  path: plan.bootstrap.target,
32
36
  type: 'agent.bootstrap',
@@ -146,6 +150,12 @@ export function formatSetupReportSummary(setupReport) {
146
150
  }
147
151
 
148
152
  lines.push(`- Server status: ${localSetup.status}`);
153
+ if (localSetup.recommendedCliVersion) {
154
+ lines.push(`- Recommended CLI: ${localSetup.recommendedCliVersion}`);
155
+ }
156
+ if (localSetup.latestContractVersion) {
157
+ lines.push(`- Current setup contract: ${localSetup.latestContractVersion}`);
158
+ }
149
159
  lines.push(`- Missing capabilities: ${formatList(localSetup.missingCapabilities)}`);
150
160
  if (localSetup.status !== 'current') {
151
161
  lines.push(`- Review command: ${localSetup.updateCommand}`);
@@ -157,13 +167,29 @@ export function formatSetupReportSummary(setupReport) {
157
167
  return lines;
158
168
  }
159
169
 
160
- function detectBootstrapVersion(plan) {
161
- const current = (plan.repo.instructionFiles ?? []).find((file) => file.hasManagedDistributionOsBlock);
162
- return current?.managedDistributionOsVersion ??
163
- (plan.bootstrap.action === 'none' ? CURRENT_BOOTSTRAP_VERSION : null);
164
- }
165
-
166
- async function inspectAnalyticsInstall(plan) {
170
+ function detectBootstrapVersion(plan) {
171
+ const current = (plan.repo.instructionFiles ?? []).find((file) => file.hasManagedDistributionOsBlock);
172
+ return current?.managedDistributionOsVersion ??
173
+ (plan.bootstrap.action === 'none' ? CURRENT_BOOTSTRAP_VERSION : null);
174
+ }
175
+
176
+ function compareVersion(left, right) {
177
+ const a = String(left ?? '').split(/[.-]/).map(toComparableNumber);
178
+ const b = String(right ?? '').split(/[.-]/).map(toComparableNumber);
179
+ const length = Math.max(a.length, b.length);
180
+ for (let index = 0; index < length; index += 1) {
181
+ const diff = (a[index] ?? 0) - (b[index] ?? 0);
182
+ if (diff !== 0) return diff;
183
+ }
184
+ return 0;
185
+ }
186
+
187
+ function toComparableNumber(value) {
188
+ const parsed = Number.parseInt(value, 10);
189
+ return Number.isFinite(parsed) ? parsed : 0;
190
+ }
191
+
192
+ async function inspectAnalyticsInstall(plan) {
167
193
  const target = plan.analytics.layoutTarget;
168
194
  if (!target) {
169
195
  return {