@corners/cli 0.0.5 → 0.0.7

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/cli.js CHANGED
@@ -1,12 +1,38 @@
1
- import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
1
+ import { readFile, realpath, writeFile } from "node:fs/promises";
2
2
  import { hostname } from "node:os";
3
- import { basename, dirname, join, relative, resolve, sep } from "node:path";
3
+ import { basename, join, relative, resolve, sep } from "node:path";
4
4
  import { parseArgs } from "node:util";
5
5
  import { CornersApiClient as DefaultCornersApiClient, } from "./client.js";
6
6
  import { ConfigStore, LOCAL_ROOT_CONFIG_RELATIVE_PATH, } from "./config.js";
7
7
  import { buildExpectedManagedGuidanceBlock, buildGuidanceFileContent, buildGuidanceRevision, extractManagedGuidanceBlock, GUIDANCE_SYNC_COMMAND, GUIDANCE_TARGETS, normalizeGuidanceTargets, } from "./guidance.js";
8
8
  import { createPromptApi } from "./prompts.js";
9
- import { CLIError, getPackageVersion, normalizeApiUrl, openUrlInBrowser, printJson, printLine, readTextFromStdin, sleep, toGraphQLAttachmentKind, toGraphQLWorkstreamStatus, toGraphQLWorkstreamUpdateType, toIsoString, } from "./support.js";
9
+ import { BUNDLED_SKILLS, buildInstalledSkillReferences, buildSkillPlannedEdits, getBundledSkillPath, parseSkillFrontmatter, resolveSkillsState, syncSkillsIfNeeded, withUpdatedSkillsConfig, } from "./skills.js";
10
+ import { buildPlannedEdit, CLIError, getPackageVersion, normalizeApiUrl, openUrlInBrowser, printJson, printLine, readOptionalUtf8, readTextFromStdin, sleep, toGraphQLAttachmentKind, toGraphQLWorkstreamStatus, toGraphQLWorkstreamUpdateType, toIsoString, writePlannedEdits, } from "./support.js";
11
+ const WORKSTREAM_LIST_QUERY = `
12
+ query CliWorkstreamList($first: Int!, $filter: WorkstreamsFilter) {
13
+ workstreams(first: $first, filter: $filter) {
14
+ edges {
15
+ node {
16
+ id
17
+ accountId
18
+ cornerId
19
+ name
20
+ isOpen
21
+ summary
22
+ category
23
+ status
24
+ statusDetails
25
+ updatedAt
26
+ topic {
27
+ id
28
+ name
29
+ }
30
+ }
31
+ }
32
+ totalCount
33
+ }
34
+ }
35
+ `;
10
36
  const WORKSTREAM_LOOKUP_QUERY = `
11
37
  query CliWorkstreamLookup($id: ID!) {
12
38
  workstream(id: $id) {
@@ -128,6 +154,24 @@ const WORKSTREAM_PULL_QUERY = `
128
154
  updatedAt
129
155
  }
130
156
  }
157
+ ... on WorkstreamTodoListAttachment {
158
+ todoList {
159
+ id
160
+ title
161
+ openItemCount
162
+ totalItemCount
163
+ items {
164
+ id
165
+ title
166
+ completed
167
+ itemIndex
168
+ assignee {
169
+ id
170
+ handle
171
+ }
172
+ }
173
+ }
174
+ }
131
175
  }
132
176
  }
133
177
  }
@@ -308,6 +352,71 @@ const UPDATE_WORKSTREAM_MUTATION = `
308
352
  }
309
353
  }
310
354
  `;
355
+ const WORKSTREAM_TASK_LIST_QUERY = `
356
+ query CliWorkstreamTasks($id: ID!, $attachmentsFirst: Int!) {
357
+ workstream(id: $id) {
358
+ id
359
+ name
360
+ attachments(first: $attachmentsFirst, kind: TODOLIST) {
361
+ edges {
362
+ node {
363
+ __typename
364
+ ... on WorkstreamTodoListAttachment {
365
+ todoList {
366
+ id
367
+ title
368
+ openItemCount
369
+ totalItemCount
370
+ items {
371
+ id
372
+ title
373
+ description
374
+ completed
375
+ completedAt
376
+ itemIndex
377
+ assignee {
378
+ id
379
+ handle
380
+ }
381
+ }
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }
387
+ }
388
+ }
389
+ `;
390
+ const CREATE_TODO_LIST_MUTATION = `
391
+ mutation CliCreateTodoList($input: CreateTodoListInput!) {
392
+ createTodoList(input: $input) {
393
+ id
394
+ title
395
+ openItemCount
396
+ totalItemCount
397
+ }
398
+ }
399
+ `;
400
+ const ADD_TODO_ITEM_MUTATION = `
401
+ mutation CliAddTodoItem($input: AddTodoItemInput!) {
402
+ addTodoItem(input: $input) {
403
+ id
404
+ title
405
+ completed
406
+ itemIndex
407
+ }
408
+ }
409
+ `;
410
+ const TOGGLE_TODO_ITEM_MUTATION = `
411
+ mutation CliToggleTodoItem($id: ID!, $completed: Boolean!) {
412
+ toggleTodoItem(id: $id, completed: $completed) {
413
+ id
414
+ title
415
+ completed
416
+ completedAt
417
+ }
418
+ }
419
+ `;
311
420
  const REVOKE_MCP_SESSION_MUTATION = `
312
421
  mutation CliRevokeMcpSession($id: ID!) {
313
422
  revokeMCPSession(id: $id)
@@ -379,6 +488,7 @@ function printMainHelp(runtime) {
379
488
  "Commands:",
380
489
  " auth login|logout|status",
381
490
  " guidance status|sync",
491
+ " skills list|status",
382
492
  " init",
383
493
  " corner use",
384
494
  " whoami",
@@ -403,6 +513,17 @@ function printInitHelp(runtime) {
403
513
  " corners init [--profile <name>] [--api-url <url>] [--json]",
404
514
  ].join("\n"));
405
515
  }
516
+ function printSkillsHelp(runtime) {
517
+ printLine(runtime.stdout, [
518
+ "corners skills",
519
+ "",
520
+ "Usage:",
521
+ " corners skills list [--json] List bundled skills",
522
+ " corners skills status [--json] Show installed skill state",
523
+ "",
524
+ "Skills are auto-synced on every CLI run once enabled via `corners init`.",
525
+ ].join("\n"));
526
+ }
406
527
  function printGuidanceHelp(runtime) {
407
528
  printLine(runtime.stdout, [
408
529
  "corners guidance",
@@ -443,18 +564,24 @@ function printWorkstreamHelp(runtime) {
443
564
  "corners workstream",
444
565
  "",
445
566
  "Usage:",
446
- " corners workstream list [--json]",
567
+ " corners workstream list [--all] [--json]",
447
568
  " corners workstream use <workstreamId> [--json]",
448
569
  " corners workstream current [--json]",
449
570
  " corners workstream create <name> [--summary <text>] [--corner <cornerNameOrId>] [--json]",
450
- " corners workstream pull <workstreamId> [--json]",
451
- " corners workstream update <workstreamId> [--status <status>] [--status-details <text> | --clear-status-details] [--json]",
452
- " corners workstream push <workstreamId> [--type <type>] [--message <text>] [--summary <text>] [--file <path>] [--title <title>] [--json]",
453
- " corners workstream question list <workstreamId> [--json]",
454
- " corners workstream question ask <workstreamId> [--question <text>] [--rationale <text>] [--suggested-answer <text>]... [--json]",
571
+ " corners workstream pull [-w <id>] [--json]",
572
+ " corners workstream update [-w <id>] [--status <status>] [--status-details <text> | --clear-status-details] [--json]",
573
+ " corners workstream push [-w <id>] [--type <type>] [--message <text>] [--summary <text>] [--file <path>] [--title <title>] [--json]",
574
+ " corners workstream question list [-w <id>] [--json]",
575
+ " corners workstream question ask [-w <id>] [--question <text>] [--rationale <text>] [--suggested-answer <text>]... [--json]",
455
576
  " corners workstream question answer <questionId> [--text <text>] [--json]",
456
- " corners workstream attach <workstreamId> --kind <kind> --entity-id <id> [--json]",
577
+ " corners workstream attach [-w <id>] --kind <kind> --entity-id <id> [--json]",
457
578
  " corners workstream reply-thread <threadId> [--text <text>] [--json]",
579
+ " corners workstream task list [-w <id>] [--json]",
580
+ " corners workstream task add [-w <id>] <title> [--list <todoListId>] [--json]",
581
+ " corners workstream task complete <taskItemId> [--undo] [--json]",
582
+ "",
583
+ "The -w/--workstream flag is optional when an active workstream is set.",
584
+ "Set the active workstream with: corners workstream use <workstreamId>",
458
585
  "",
459
586
  "Lifecycle statuses:",
460
587
  " scoping | in_progress | blocked | done",
@@ -672,17 +799,6 @@ async function withPrompts(runtime, run) {
672
799
  await prompts.close();
673
800
  }
674
801
  }
675
- async function readOptionalUtf8(path) {
676
- try {
677
- return await readFile(path, "utf8");
678
- }
679
- catch (error) {
680
- if (error.code === "ENOENT") {
681
- return null;
682
- }
683
- throw error;
684
- }
685
- }
686
802
  function ensureTrailingNewline(value) {
687
803
  return value.endsWith("\n") ? value : `${value}\n`;
688
804
  }
@@ -818,6 +934,7 @@ async function resolveRootContext(runtime, common, root, options) {
818
934
  printLine(runtime.stderr, warning);
819
935
  }
820
936
  }
937
+ await syncSkillsIfNeeded(root.rootDir, root.config, (dir, cfg) => runtime.config.writeLocalConfig(dir, cfg));
821
938
  return {
822
939
  root,
823
940
  guidance,
@@ -904,6 +1021,16 @@ async function resolveExplicitWorkstream(client, common, workstreamId) {
904
1021
  }
905
1022
  return data.workstream;
906
1023
  }
1024
+ async function resolveWorkstreamId(runtime, common, flagValue) {
1025
+ if (flagValue) {
1026
+ return flagValue;
1027
+ }
1028
+ const binding = await runtime.config.getBinding(runtime.cwd);
1029
+ if (binding) {
1030
+ return binding.workstreamId;
1031
+ }
1032
+ throw new CLIError("No --workstream flag and no active workstream. Use `corners workstream use <id>` first.", { json: common.json });
1033
+ }
907
1034
  function connectWorkstream(config, workstreamId) {
908
1035
  if (config.connectedWorkstreams.some((entry) => entry.id === workstreamId)) {
909
1036
  return config;
@@ -939,17 +1066,6 @@ function upsertCornerOverride(config, relativePath, corner) {
939
1066
  cornerOverrides: overrides,
940
1067
  };
941
1068
  }
942
- async function buildPlannedEdit(path, nextContent) {
943
- const existing = await readOptionalUtf8(path);
944
- if (existing === nextContent) {
945
- return null;
946
- }
947
- return {
948
- path,
949
- action: existing === null ? "create" : "update",
950
- content: nextContent,
951
- };
952
- }
953
1069
  function withUpdatedGuidanceConfig(config, managedTargets, syncedAt) {
954
1070
  return {
955
1071
  ...config,
@@ -977,12 +1093,6 @@ function ensureGitignoreEntry(existing) {
977
1093
  }
978
1094
  return `${trimmed}\n${LOCAL_ROOT_CONFIG_RELATIVE_PATH}\n`;
979
1095
  }
980
- async function writePlannedEdits(edits) {
981
- for (const edit of edits) {
982
- await mkdir(dirname(edit.path), { recursive: true });
983
- await writeFile(edit.path, edit.content, "utf8");
984
- }
985
- }
986
1096
  function formatWorkstreamLine(workstream) {
987
1097
  return [
988
1098
  workstream.id,
@@ -1304,6 +1414,7 @@ async function handleInit(args, runtime, inherited) {
1304
1414
  selectedTargets.push(target);
1305
1415
  }
1306
1416
  }
1417
+ const installSkills = await prompts.confirm("Install Corners skills? (sync, push, ask, plan)", { defaultValue: true });
1307
1418
  const selectedCornerId = await prompts.select("Choose the default corner for this local root.", joinedCorners.map((corner) => ({
1308
1419
  label: `${corner.name} (${corner.id})`,
1309
1420
  value: corner.id,
@@ -1313,7 +1424,7 @@ async function handleInit(args, runtime, inherited) {
1313
1424
  const defaultCorner = joinedCorners.find((corner) => corner.id === selectedCornerId) ??
1314
1425
  joinedCorners[0];
1315
1426
  const syncedAt = toIsoString();
1316
- const nextLocalConfig = withUpdatedGuidanceConfig({
1427
+ let nextLocalConfig = withUpdatedGuidanceConfig({
1317
1428
  version: 1,
1318
1429
  profile: profileName,
1319
1430
  workspace: profile.workspace,
@@ -1325,6 +1436,10 @@ async function handleInit(args, runtime, inherited) {
1325
1436
  cornerOverrides: existingConfig?.cornerOverrides ?? [],
1326
1437
  connectedWorkstreams: existingConfig?.connectedWorkstreams ?? [],
1327
1438
  }, selectedTargets.map((target) => target.key), syncedAt);
1439
+ if (installSkills) {
1440
+ const skillRefs = await buildInstalledSkillReferences(syncedAt);
1441
+ nextLocalConfig = withUpdatedSkillsConfig(nextLocalConfig, skillRefs, syncedAt);
1442
+ }
1328
1443
  const plannedEdits = [];
1329
1444
  const gitignorePath = join(rootDir, ".gitignore");
1330
1445
  const gitignoreContent = ensureGitignoreEntry(await readOptionalUtf8(gitignorePath));
@@ -1345,6 +1460,10 @@ async function handleInit(args, runtime, inherited) {
1345
1460
  plannedEdits.push(edit);
1346
1461
  }
1347
1462
  }
1463
+ if (installSkills) {
1464
+ const skillEdits = await buildSkillPlannedEdits(rootDir);
1465
+ plannedEdits.push(...skillEdits);
1466
+ }
1348
1467
  if (plannedEdits.length > 0 && !common.json) {
1349
1468
  printLine(runtime.stderr, [
1350
1469
  "Planned changes:",
@@ -1370,12 +1489,96 @@ async function handleInit(args, runtime, inherited) {
1370
1489
  }
1371
1490
  await writePlannedEdits(plannedEdits);
1372
1491
  }
1492
+ // --- Agent Communication Setup ---
1493
+ const setupAgentComms = await prompts.confirm("Set up agent communication? (auto-connect workstream + CLAUDE.md instructions)", { defaultValue: true });
1494
+ let connectedWorkstream = null;
1495
+ if (setupAgentComms) {
1496
+ // Auto-connect or create a workstream
1497
+ const workstreams = await client.graphql(`query CliInitWorkstreams($cornerId: ID!) {
1498
+ corner(id: $cornerId) { workstreams(first: 20) { edges { node { id name status } } } }
1499
+ }`, { cornerId: defaultCorner.id }).then((r) => {
1500
+ const corner = r.corner;
1501
+ return corner?.workstreams?.edges?.map((e) => e.node) ?? [];
1502
+ }).catch(() => []);
1503
+ if (workstreams.length > 0) {
1504
+ const wsChoice = await prompts.select("Connect a workstream for agent updates:", [
1505
+ ...workstreams.map((ws) => ({
1506
+ label: `${ws.name} (${ws.id})`,
1507
+ value: ws.id,
1508
+ })),
1509
+ { label: "Create new workstream", value: "__new__" },
1510
+ { label: "Skip", value: "__skip__" },
1511
+ ], { defaultValue: workstreams[0]?.id });
1512
+ if (wsChoice === "__new__") {
1513
+ const wsName = `agent/${basename(rootDir)}`;
1514
+ const created = await client.graphql(`mutation CliCreateWsInit($input: CreateWorkstreamInput!) { createWorkstream(input: $input) { id name } }`, { input: { cornerId: defaultCorner.id, name: wsName } });
1515
+ connectedWorkstream = created.createWorkstream;
1516
+ }
1517
+ else if (wsChoice !== "__skip__") {
1518
+ connectedWorkstream = workstreams.find((ws) => ws.id === wsChoice) ?? null;
1519
+ }
1520
+ }
1521
+ else {
1522
+ const wsName = `agent/${basename(rootDir)}`;
1523
+ const created = await client.graphql(`mutation CliCreateWsInit($input: CreateWorkstreamInput!) { createWorkstream(input: $input) { id name } }`, { input: { cornerId: defaultCorner.id, name: wsName } });
1524
+ connectedWorkstream = created.createWorkstream;
1525
+ }
1526
+ // Connect workstream to local root
1527
+ if (connectedWorkstream) {
1528
+ nextLocalConfig = {
1529
+ ...nextLocalConfig,
1530
+ connectedWorkstreams: [
1531
+ ...(nextLocalConfig.connectedWorkstreams ?? []),
1532
+ { id: connectedWorkstream.id, connectedAt: toIsoString() },
1533
+ ],
1534
+ };
1535
+ // Re-write the local config with the connected workstream
1536
+ const updatedConfigPath = runtime.config.getLocalConfigPath(rootDir);
1537
+ await writeFile(updatedConfigPath, `${JSON.stringify(nextLocalConfig, null, 2)}\n`, "utf8");
1538
+ }
1539
+ // Write CLAUDE.md agent instructions
1540
+ const claudeMdPath = join(rootDir, "CLAUDE.md");
1541
+ const existingClaudeMd = await readOptionalUtf8(claudeMdPath);
1542
+ const agentSection = `\n## Agent Communication\n\nWhen working on tasks, report progress to the team through Corners:\n- At the start: \`corners workstream push --type session_start --intent "description" --json\`\n- At natural checkpoints: \`corners workstream push --type checkpoint --message "what you did" --files "file1,file2" --json\`\n- When done: \`corners workstream push --type session_end --summary "what was accomplished" --json\`\n`;
1543
+ if (existingClaudeMd && !existingClaudeMd.includes("## Agent Communication")) {
1544
+ await writeFile(claudeMdPath, existingClaudeMd + agentSection, "utf8");
1545
+ if (!common.json) {
1546
+ printLine(runtime.stderr, " Added agent communication instructions to CLAUDE.md");
1547
+ }
1548
+ }
1549
+ else if (!existingClaudeMd) {
1550
+ await writeFile(claudeMdPath, `# ${basename(rootDir)}\n${agentSection}`, "utf8");
1551
+ if (!common.json) {
1552
+ printLine(runtime.stderr, " Created CLAUDE.md with agent communication instructions");
1553
+ }
1554
+ }
1555
+ // Send test checkpoint
1556
+ if (connectedWorkstream) {
1557
+ try {
1558
+ await client.graphql(RECORD_WORKSTREAM_UPDATE_MUTATION, {
1559
+ input: {
1560
+ workstreamId: connectedWorkstream.id,
1561
+ updateType: "CHECKPOINT",
1562
+ content: "Agent communication configured via corners init",
1563
+ agentType: "corners-cli",
1564
+ },
1565
+ });
1566
+ if (!common.json) {
1567
+ printLine(runtime.stderr, ` Test checkpoint sent to ${connectedWorkstream.name}`);
1568
+ }
1569
+ }
1570
+ catch {
1571
+ // Non-critical, don't fail init
1572
+ }
1573
+ }
1574
+ }
1373
1575
  const payload = {
1374
1576
  ok: true,
1375
1577
  root: rootDir,
1376
1578
  profile: profileName,
1377
1579
  workspace: profile.workspace,
1378
1580
  defaultCorner,
1581
+ connectedWorkstream,
1379
1582
  files: plannedEdits.map((edit) => ({
1380
1583
  action: edit.action,
1381
1584
  path: relative(rootDir, edit.path) || ".",
@@ -1385,11 +1588,108 @@ async function handleInit(args, runtime, inherited) {
1385
1588
  printJson(runtime.stdout, payload);
1386
1589
  }
1387
1590
  else {
1388
- printLine(runtime.stdout, `Initialized Corners CLI at ${rootDir} with default corner ${defaultCorner.name}. Run \`${GUIDANCE_SYNC_COMMAND}\` later to refresh managed guidance.`);
1591
+ const wsMsg = connectedWorkstream
1592
+ ? ` Connected workstream: ${connectedWorkstream.name}.`
1593
+ : "";
1594
+ printLine(runtime.stdout, `Initialized Corners CLI at ${rootDir} with default corner ${defaultCorner.name}.${wsMsg} Run \`${GUIDANCE_SYNC_COMMAND}\` later to refresh managed guidance.`);
1389
1595
  }
1390
1596
  return 0;
1391
1597
  });
1392
1598
  }
1599
+ async function handleSkills(args, runtime, inherited) {
1600
+ const subcommand = args[0];
1601
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
1602
+ printSkillsHelp(runtime);
1603
+ return 0;
1604
+ }
1605
+ switch (subcommand) {
1606
+ case "list": {
1607
+ const parsed = parseArgs({
1608
+ args: args.slice(1),
1609
+ allowPositionals: false,
1610
+ options: {
1611
+ json: { type: "boolean" },
1612
+ help: { type: "boolean", short: "h" },
1613
+ },
1614
+ });
1615
+ const common = mergeCommonOptions(inherited, {
1616
+ json: parsed.values.json,
1617
+ });
1618
+ if (parsed.values.help) {
1619
+ printSkillsHelp(runtime);
1620
+ return 0;
1621
+ }
1622
+ const skills = await Promise.all(BUNDLED_SKILLS.map(async (skill) => {
1623
+ const content = await readFile(getBundledSkillPath(skill), "utf8");
1624
+ const fm = parseSkillFrontmatter(content);
1625
+ return {
1626
+ name: skill.name,
1627
+ version: fm?.version ?? "unknown",
1628
+ description: fm?.description ?? "",
1629
+ };
1630
+ }));
1631
+ if (common.json) {
1632
+ printJson(runtime.stdout, { ok: true, skills });
1633
+ }
1634
+ else {
1635
+ printLine(runtime.stdout, [
1636
+ "Available skills:",
1637
+ ...skills.map((s) => ` ${s.name.padEnd(16)} ${s.version.padEnd(8)} ${s.description}`),
1638
+ ].join("\n"));
1639
+ }
1640
+ return 0;
1641
+ }
1642
+ case "status": {
1643
+ const parsed = parseArgs({
1644
+ args: args.slice(1),
1645
+ allowPositionals: false,
1646
+ options: {
1647
+ json: { type: "boolean" },
1648
+ help: { type: "boolean", short: "h" },
1649
+ },
1650
+ });
1651
+ const common = mergeCommonOptions(inherited, {
1652
+ json: parsed.values.json,
1653
+ });
1654
+ if (parsed.values.help) {
1655
+ printSkillsHelp(runtime);
1656
+ return 0;
1657
+ }
1658
+ const rootContext = await requireLocalRoot(runtime, common, {
1659
+ emitWarning: false,
1660
+ });
1661
+ const state = await resolveSkillsState(rootContext.root.rootDir, rootContext.root.config);
1662
+ const payload = {
1663
+ ok: true,
1664
+ root: rootContext.root.rootDir,
1665
+ skills: state,
1666
+ };
1667
+ if (common.json) {
1668
+ printJson(runtime.stdout, payload);
1669
+ }
1670
+ else {
1671
+ printLine(runtime.stdout, [
1672
+ `Root: ${payload.root}`,
1673
+ `Last synced: ${state.lastSyncedAt ?? "-"}`,
1674
+ "",
1675
+ "Skills:",
1676
+ ...state.skills.map((s) => {
1677
+ const stateLabel = s.state.padEnd(8);
1678
+ const version = s.state === "stale"
1679
+ ? `(installed: ${s.installedVersion}, available: ${s.bundledVersion})`
1680
+ : s.state === "missing"
1681
+ ? ""
1682
+ : `(${s.bundledVersion})`;
1683
+ return ` ${s.name.padEnd(16)} ${stateLabel} ${version}`;
1684
+ }),
1685
+ ].join("\n"));
1686
+ }
1687
+ return 0;
1688
+ }
1689
+ default:
1690
+ throw new CLIError(`Unknown skills command: ${subcommand}`);
1691
+ }
1692
+ }
1393
1693
  async function handleGuidance(args, runtime, inherited) {
1394
1694
  const subcommand = args[0];
1395
1695
  if (!subcommand || subcommand === "help" || subcommand === "--help") {
@@ -1587,6 +1887,7 @@ async function handleWorkstream(args, runtime, inherited) {
1587
1887
  help: { type: "boolean", short: "h" },
1588
1888
  profile: { type: "string" },
1589
1889
  "api-url": { type: "string" },
1890
+ all: { type: "boolean" },
1590
1891
  },
1591
1892
  });
1592
1893
  const common = mergeCommonOptions(inherited, {
@@ -1598,44 +1899,41 @@ async function handleWorkstream(args, runtime, inherited) {
1598
1899
  printWorkstreamHelp(runtime);
1599
1900
  return 0;
1600
1901
  }
1601
- const rootContext = await requireLocalRoot(runtime, common);
1602
- const { root, guidance, client } = await requirePinnedRootProfile(runtime, common, rootContext);
1603
- const hydrated = await Promise.all(root.config.connectedWorkstreams.map(async (entry) => {
1604
- try {
1605
- return {
1606
- entry,
1607
- workstream: await resolveExplicitWorkstream(client, common, entry.id),
1608
- };
1609
- }
1610
- catch {
1611
- return {
1612
- entry,
1613
- workstream: null,
1614
- };
1615
- }
1616
- }));
1617
- const workstreams = hydrated
1618
- .filter((entry) => entry.workstream !== null)
1619
- .map((entry) => entry.workstream);
1620
- const missingWorkstreamIds = hydrated
1621
- .filter((entry) => entry.workstream === null)
1622
- .map((entry) => entry.entry.id);
1902
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1903
+ const filter = {};
1904
+ if (!parsed.values.all) {
1905
+ filter.isOpen = true;
1906
+ }
1907
+ const data = await client.graphql(WORKSTREAM_LIST_QUERY, { first: 100, filter });
1908
+ const allWorkstreams = data.workstreams.edges.map((edge) => edge.node);
1909
+ const totalCount = data.workstreams.totalCount;
1910
+ // Try to find local root for connected workstream annotation
1911
+ const root = await runtime.config.findLocalRoot(runtime.cwd);
1912
+ const connectedIds = new Set(root?.config.connectedWorkstreams.map((entry) => entry.id) ?? []);
1623
1913
  if (common.json) {
1624
1914
  printJson(runtime.stdout, withGuidanceJsonMetadata({
1625
- root: root.rootDir,
1626
- workstreams,
1627
- missingWorkstreamIds,
1915
+ workstreams: allWorkstreams.map((ws) => ({
1916
+ ...ws,
1917
+ connected: connectedIds.has(ws.id),
1918
+ })),
1919
+ totalCount,
1628
1920
  }, guidance));
1629
1921
  }
1630
1922
  else {
1631
- if (missingWorkstreamIds.length > 0) {
1632
- printLine(runtime.stderr, missingWorkstreamIds
1633
- .map((workstreamId) => `Warning: connected workstream ${workstreamId} is missing or inaccessible.`)
1923
+ if (allWorkstreams.length === 0) {
1924
+ printLine(runtime.stdout, "No workstreams found.");
1925
+ }
1926
+ else {
1927
+ printLine(runtime.stdout, allWorkstreams
1928
+ .map((ws) => {
1929
+ const prefix = connectedIds.has(ws.id) ? "* " : " ";
1930
+ return `${prefix}${formatWorkstreamLine(ws)}`;
1931
+ })
1634
1932
  .join("\n"));
1933
+ if (totalCount > 100) {
1934
+ printLine(runtime.stderr, `Showing 100 of ${totalCount} workstreams.`);
1935
+ }
1635
1936
  }
1636
- printLine(runtime.stdout, workstreams.length > 0
1637
- ? workstreams.map(formatWorkstreamLine).join("\n")
1638
- : "No workstreams are connected to this local environment.");
1639
1937
  }
1640
1938
  return 0;
1641
1939
  }
@@ -1672,15 +1970,23 @@ async function handleWorkstream(args, runtime, inherited) {
1672
1970
  if (nextConfig !== root.config) {
1673
1971
  await runtime.config.writeLocalConfig(root.rootDir, nextConfig);
1674
1972
  }
1973
+ await runtime.config.setBinding(runtime.cwd, {
1974
+ profile: root.config.profile,
1975
+ workspace: root.config.workspace ?? "",
1976
+ workstreamId: workstream.id,
1977
+ cornerId: workstream.cornerId,
1978
+ boundAt: toIsoString(),
1979
+ });
1675
1980
  if (common.json) {
1676
1981
  printJson(runtime.stdout, withGuidanceJsonMetadata({
1677
1982
  ok: true,
1678
1983
  root: root.rootDir,
1679
1984
  workstream,
1985
+ bound: true,
1680
1986
  }, guidance));
1681
1987
  }
1682
1988
  else {
1683
- printLine(runtime.stdout, `Connected ${workstream.id} (${workstream.name}) to ${root.rootDir}.`);
1989
+ printLine(runtime.stdout, `Connected ${workstream.id} (${workstream.name}) to ${root.rootDir} and set as active workstream.`);
1684
1990
  }
1685
1991
  return 0;
1686
1992
  }
@@ -1703,12 +2009,14 @@ async function handleWorkstream(args, runtime, inherited) {
1703
2009
  const { root, guidance } = await requireLocalRoot(runtime, common);
1704
2010
  const cwdPath = await realpath(runtime.cwd);
1705
2011
  const resolvedCorner = resolveCornerForCwd(root, cwdPath);
2012
+ const binding = await runtime.config.getBinding(runtime.cwd);
1706
2013
  const payload = {
1707
2014
  cwd: runtime.cwd,
1708
2015
  root: root.rootDir,
1709
2016
  profile: root.config.profile,
1710
2017
  workspace: root.config.workspace,
1711
2018
  resolvedCorner,
2019
+ activeWorkstreamId: binding?.workstreamId ?? null,
1712
2020
  connectedWorkstreamIds: root.config.connectedWorkstreams.map((entry) => entry.id),
1713
2021
  };
1714
2022
  if (common.json) {
@@ -1721,6 +2029,7 @@ async function handleWorkstream(args, runtime, inherited) {
1721
2029
  `Profile: ${payload.profile}`,
1722
2030
  `Workspace: ${payload.workspace ?? "-"}`,
1723
2031
  `Resolved corner: ${payload.resolvedCorner.corner.name} (${payload.resolvedCorner.corner.id})`,
2032
+ `Active workstream: ${payload.activeWorkstreamId ?? "none"}`,
1724
2033
  `Connected workstreams: ${payload.connectedWorkstreamIds.length}`,
1725
2034
  ].join("\n"));
1726
2035
  }
@@ -1785,12 +2094,13 @@ async function handleWorkstream(args, runtime, inherited) {
1785
2094
  case "pull": {
1786
2095
  const parsed = parseArgs({
1787
2096
  args: args.slice(1),
1788
- allowPositionals: true,
2097
+ allowPositionals: false,
1789
2098
  options: {
1790
2099
  json: { type: "boolean" },
1791
2100
  help: { type: "boolean", short: "h" },
1792
2101
  profile: { type: "string" },
1793
2102
  "api-url": { type: "string" },
2103
+ workstream: { type: "string", short: "w" },
1794
2104
  },
1795
2105
  });
1796
2106
  const common = mergeCommonOptions(inherited, {
@@ -1802,12 +2112,7 @@ async function handleWorkstream(args, runtime, inherited) {
1802
2112
  printWorkstreamHelp(runtime);
1803
2113
  return 0;
1804
2114
  }
1805
- const workstreamId = parsed.positionals[0];
1806
- if (!workstreamId) {
1807
- throw new CLIError("Usage: corners workstream pull <workstreamId>", {
1808
- json: common.json,
1809
- });
1810
- }
2115
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
1811
2116
  const { guidance, client } = await requireCommandProfile(runtime, common);
1812
2117
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1813
2118
  const data = await client.graphql(WORKSTREAM_PULL_QUERY, {
@@ -1820,7 +2125,7 @@ async function handleWorkstream(args, runtime, inherited) {
1820
2125
  }
1821
2126
  else {
1822
2127
  const snapshot = data.workstream;
1823
- printLine(runtime.stdout, [
2128
+ const lines = [
1824
2129
  `${snapshot.name} (${snapshot.id})`,
1825
2130
  snapshot.summary ? `Summary: ${snapshot.summary}` : "Summary: -",
1826
2131
  `State: ${snapshot.isOpen === false ? "archived" : "open"}`,
@@ -1830,19 +2135,41 @@ async function handleWorkstream(args, runtime, inherited) {
1830
2135
  `Answered questions: ${snapshot.answeredQuestions?.length ?? 0}`,
1831
2136
  `Attachments: ${snapshot.attachments?.totalCount ?? 0}`,
1832
2137
  `Feed items returned: ${snapshot.feed?.totalCount ?? 0}`,
1833
- ].join("\n"));
2138
+ ];
2139
+ // Append tasks from TodoList attachments
2140
+ const todoLists = (snapshot.attachments?.edges ?? []).flatMap((e) => e.node.__typename === "WorkstreamTodoListAttachment" &&
2141
+ e.node.todoList
2142
+ ? [e.node.todoList]
2143
+ : []);
2144
+ if (todoLists.length > 0) {
2145
+ lines.push("");
2146
+ lines.push("Tasks:");
2147
+ for (const list of todoLists) {
2148
+ const completed = list.totalItemCount - list.openItemCount;
2149
+ lines.push(` ${list.title} (${completed}/${list.totalItemCount} complete)`);
2150
+ for (const item of list.items) {
2151
+ const check = item.completed ? "[x]" : "[ ]";
2152
+ const assignee = item.assignee
2153
+ ? ` (@${item.assignee.handle})`
2154
+ : "";
2155
+ lines.push(` ${check} ${item.title}${assignee}`);
2156
+ }
2157
+ }
2158
+ }
2159
+ printLine(runtime.stdout, lines.join("\n"));
1834
2160
  }
1835
2161
  return 0;
1836
2162
  }
1837
2163
  case "update": {
1838
2164
  const parsed = parseArgs({
1839
2165
  args: args.slice(1),
1840
- allowPositionals: true,
2166
+ allowPositionals: false,
1841
2167
  options: {
1842
2168
  json: { type: "boolean" },
1843
2169
  help: { type: "boolean", short: "h" },
1844
2170
  profile: { type: "string" },
1845
2171
  "api-url": { type: "string" },
2172
+ workstream: { type: "string", short: "w" },
1846
2173
  status: { type: "string" },
1847
2174
  "status-details": { type: "string" },
1848
2175
  "clear-status-details": { type: "boolean" },
@@ -1857,10 +2184,6 @@ async function handleWorkstream(args, runtime, inherited) {
1857
2184
  printWorkstreamHelp(runtime);
1858
2185
  return 0;
1859
2186
  }
1860
- const workstreamId = parsed.positionals[0];
1861
- if (!workstreamId) {
1862
- throw new CLIError("Usage: corners workstream update <workstreamId> [--status <status>] [--status-details <text> | --clear-status-details]", { json: common.json });
1863
- }
1864
2187
  const rawStatusDetails = parsed.values["status-details"];
1865
2188
  const clearStatusDetails = parsed.values["clear-status-details"] === true;
1866
2189
  if (rawStatusDetails !== undefined && clearStatusDetails) {
@@ -1871,6 +2194,7 @@ async function handleWorkstream(args, runtime, inherited) {
1871
2194
  !clearStatusDetails) {
1872
2195
  throw new CLIError("Provide --status, --status-details, or --clear-status-details.", { json: common.json });
1873
2196
  }
2197
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
1874
2198
  const { guidance, client } = await requireCommandProfile(runtime, common);
1875
2199
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1876
2200
  const result = await client.graphql(UPDATE_WORKSTREAM_MUTATION, {
@@ -1912,11 +2236,16 @@ async function handleWorkstream(args, runtime, inherited) {
1912
2236
  help: { type: "boolean", short: "h" },
1913
2237
  profile: { type: "string" },
1914
2238
  "api-url": { type: "string" },
2239
+ workstream: { type: "string", short: "w" },
1915
2240
  type: { type: "string" },
1916
2241
  message: { type: "string" },
1917
2242
  summary: { type: "string" },
1918
2243
  file: { type: "string" },
1919
2244
  title: { type: "string" },
2245
+ "agent-type": { type: "string" },
2246
+ files: { type: "string" },
2247
+ scope: { type: "string" },
2248
+ intent: { type: "string" },
1920
2249
  },
1921
2250
  });
1922
2251
  const common = mergeCommonOptions(inherited, {
@@ -1928,15 +2257,10 @@ async function handleWorkstream(args, runtime, inherited) {
1928
2257
  printWorkstreamHelp(runtime);
1929
2258
  return 0;
1930
2259
  }
1931
- const workstreamId = parsed.positionals[0];
1932
- if (!workstreamId) {
1933
- throw new CLIError("Usage: corners workstream push <workstreamId>", {
1934
- json: common.json,
1935
- });
1936
- }
2260
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
1937
2261
  const message = parsed.values.message ??
1938
- (parsed.positionals.length > 1
1939
- ? parsed.positionals.slice(1).join(" ")
2262
+ (parsed.positionals.length > 0
2263
+ ? parsed.positionals.join(" ")
1940
2264
  : await readTextFromStdin(runtime.stdin));
1941
2265
  if (!message) {
1942
2266
  throw new CLIError("Workstream updates need a message. Use --message or pipe text on stdin.", { json: common.json });
@@ -1956,18 +2280,38 @@ async function handleWorkstream(args, runtime, inherited) {
1956
2280
  });
1957
2281
  createdDocument = documentResult.createDocument;
1958
2282
  }
2283
+ // Parse agent-specific fields
2284
+ const agentType = parsed.values["agent-type"] ??
2285
+ (process.env.CLAUDE_CODE ? "claude-code" : null) ??
2286
+ (process.env.CODEX_SESSION ? "codex" : null) ??
2287
+ null;
2288
+ const filesTouched = parsed.values.files
2289
+ ? parsed.values.files.split(",").map((f) => f.trim())
2290
+ : null;
2291
+ const scope = parsed.values.scope ?? null;
2292
+ // For session_start, --intent is an alias for --message
2293
+ const updateType = parsed.values.type ?? "status";
2294
+ const effectiveContent = updateType === "session_start" && parsed.values.intent
2295
+ ? parsed.values.intent
2296
+ : message;
1959
2297
  const updateResult = await client.graphql(RECORD_WORKSTREAM_UPDATE_MUTATION, {
1960
2298
  input: {
1961
2299
  workstreamId: workstream.id,
1962
- updateType: toGraphQLWorkstreamUpdateType(parsed.values.type ?? "status"),
1963
- content: message,
2300
+ updateType: toGraphQLWorkstreamUpdateType(updateType),
2301
+ content: effectiveContent,
1964
2302
  summary: parsed.values.summary,
1965
2303
  documentId: createdDocument?.id ?? null,
2304
+ ...(agentType ? { agentType } : {}),
2305
+ ...(filesTouched ? { filesTouched } : {}),
2306
+ ...(scope ? { scope } : {}),
1966
2307
  },
1967
2308
  });
1968
2309
  const payload = {
1969
2310
  ok: true,
2311
+ success: true,
1970
2312
  workstreamId: workstream.id,
2313
+ type: updateType,
2314
+ messageId: updateResult.recordWorkstreamUpdate.id,
1971
2315
  update: updateResult.recordWorkstreamUpdate,
1972
2316
  document: createdDocument,
1973
2317
  };
@@ -1989,12 +2333,13 @@ async function handleWorkstream(args, runtime, inherited) {
1989
2333
  case "list": {
1990
2334
  const parsed = parseArgs({
1991
2335
  args: args.slice(2),
1992
- allowPositionals: true,
2336
+ allowPositionals: false,
1993
2337
  options: {
1994
2338
  json: { type: "boolean" },
1995
2339
  help: { type: "boolean", short: "h" },
1996
2340
  profile: { type: "string" },
1997
2341
  "api-url": { type: "string" },
2342
+ workstream: { type: "string", short: "w" },
1998
2343
  },
1999
2344
  });
2000
2345
  const common = mergeCommonOptions(inherited, {
@@ -2006,10 +2351,7 @@ async function handleWorkstream(args, runtime, inherited) {
2006
2351
  printWorkstreamHelp(runtime);
2007
2352
  return 0;
2008
2353
  }
2009
- const workstreamId = parsed.positionals[0];
2010
- if (!workstreamId) {
2011
- throw new CLIError("Usage: corners workstream question list <workstreamId>", { json: common.json });
2012
- }
2354
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
2013
2355
  const { guidance, client } = await requireCommandProfile(runtime, common);
2014
2356
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
2015
2357
  const data = await client.graphql(WORKSTREAM_QUESTION_LIST_QUERY, {
@@ -2040,6 +2382,7 @@ async function handleWorkstream(args, runtime, inherited) {
2040
2382
  help: { type: "boolean", short: "h" },
2041
2383
  profile: { type: "string" },
2042
2384
  "api-url": { type: "string" },
2385
+ workstream: { type: "string", short: "w" },
2043
2386
  question: { type: "string" },
2044
2387
  rationale: { type: "string" },
2045
2388
  "suggested-answer": { type: "string", multiple: true },
@@ -2054,13 +2397,10 @@ async function handleWorkstream(args, runtime, inherited) {
2054
2397
  printWorkstreamHelp(runtime);
2055
2398
  return 0;
2056
2399
  }
2057
- const workstreamId = parsed.positionals[0];
2058
- if (!workstreamId) {
2059
- throw new CLIError("Usage: corners workstream question ask <workstreamId> [--question <text>]", { json: common.json });
2060
- }
2400
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
2061
2401
  const question = parsed.values.question ??
2062
- (parsed.positionals.length > 1
2063
- ? parsed.positionals.slice(1).join(" ")
2402
+ (parsed.positionals.length > 0
2403
+ ? parsed.positionals.join(" ")
2064
2404
  : await readTextFromStdin(runtime.stdin));
2065
2405
  if (!question) {
2066
2406
  throw new CLIError("Question text is required. Use --question or pipe text on stdin.", { json: common.json });
@@ -2137,12 +2477,13 @@ async function handleWorkstream(args, runtime, inherited) {
2137
2477
  case "attach": {
2138
2478
  const parsed = parseArgs({
2139
2479
  args: args.slice(1),
2140
- allowPositionals: true,
2480
+ allowPositionals: false,
2141
2481
  options: {
2142
2482
  json: { type: "boolean" },
2143
2483
  help: { type: "boolean", short: "h" },
2144
2484
  profile: { type: "string" },
2145
2485
  "api-url": { type: "string" },
2486
+ workstream: { type: "string", short: "w" },
2146
2487
  kind: { type: "string" },
2147
2488
  "entity-id": { type: "string" },
2148
2489
  },
@@ -2156,12 +2497,12 @@ async function handleWorkstream(args, runtime, inherited) {
2156
2497
  printWorkstreamHelp(runtime);
2157
2498
  return 0;
2158
2499
  }
2159
- const workstreamId = parsed.positionals[0];
2160
2500
  const kind = parsed.values.kind;
2161
2501
  const entityId = parsed.values["entity-id"];
2162
- if (!workstreamId || !kind || !entityId) {
2163
- throw new CLIError("Usage: corners workstream attach <workstreamId> --kind <kind> --entity-id <id>", { json: common.json });
2502
+ if (!kind || !entityId) {
2503
+ throw new CLIError("Usage: corners workstream attach [--workstream <id>] --kind <kind> --entity-id <id>", { json: common.json });
2164
2504
  }
2505
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
2165
2506
  const { guidance, client } = await requireCommandProfile(runtime, common);
2166
2507
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
2167
2508
  const result = await client.graphql(ADD_WORKSTREAM_ATTACHMENT_MUTATION, {
@@ -2221,10 +2562,241 @@ async function handleWorkstream(args, runtime, inherited) {
2221
2562
  }
2222
2563
  return 0;
2223
2564
  }
2565
+ case "task": {
2566
+ const nested = args[1];
2567
+ if (!nested || nested === "help" || nested === "--help") {
2568
+ printWorkstreamHelp(runtime);
2569
+ return 0;
2570
+ }
2571
+ switch (nested) {
2572
+ case "list": {
2573
+ const parsed = parseArgs({
2574
+ args: args.slice(2),
2575
+ allowPositionals: false,
2576
+ options: {
2577
+ json: { type: "boolean" },
2578
+ help: { type: "boolean", short: "h" },
2579
+ profile: { type: "string" },
2580
+ "api-url": { type: "string" },
2581
+ workstream: { type: "string", short: "w" },
2582
+ },
2583
+ });
2584
+ const common = mergeCommonOptions(inherited, {
2585
+ json: parsed.values.json,
2586
+ profile: parsed.values.profile,
2587
+ apiUrl: parsed.values["api-url"],
2588
+ });
2589
+ if (parsed.values.help) {
2590
+ printWorkstreamHelp(runtime);
2591
+ return 0;
2592
+ }
2593
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
2594
+ const { guidance, client } = await requireCommandProfile(runtime, common);
2595
+ const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
2596
+ const data = await client.graphql(WORKSTREAM_TASK_LIST_QUERY, {
2597
+ id: workstream.id,
2598
+ attachmentsFirst: 50,
2599
+ });
2600
+ if (!data.workstream) {
2601
+ throw new CLIError("Workstream not found", { json: common.json });
2602
+ }
2603
+ const todoLists = data.workstream.attachments.edges.flatMap((e) => e.node.todoList ? [e.node.todoList] : []);
2604
+ if (common.json) {
2605
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
2606
+ workstreamId: workstream.id,
2607
+ workstreamName: workstream.name,
2608
+ todoLists,
2609
+ }, guidance));
2610
+ }
2611
+ else {
2612
+ if (todoLists.length === 0) {
2613
+ printLine(runtime.stdout, "No task lists attached to this workstream.");
2614
+ }
2615
+ else {
2616
+ const lines = [
2617
+ `${workstream.name} (${workstream.id}) - Tasks`,
2618
+ "",
2619
+ ];
2620
+ for (const list of todoLists) {
2621
+ const completed = list.totalItemCount - list.openItemCount;
2622
+ lines.push(`${list.title} (${completed}/${list.totalItemCount} complete)`);
2623
+ for (const item of list.items) {
2624
+ const check = item.completed ? "[x]" : "[ ]";
2625
+ const assignee = item.assignee
2626
+ ? ` (@${item.assignee.handle})`
2627
+ : "";
2628
+ lines.push(` ${check} ${item.title}${assignee}`);
2629
+ }
2630
+ }
2631
+ printLine(runtime.stdout, lines.join("\n"));
2632
+ }
2633
+ }
2634
+ return 0;
2635
+ }
2636
+ case "add": {
2637
+ const parsed = parseArgs({
2638
+ args: args.slice(2),
2639
+ allowPositionals: true,
2640
+ options: {
2641
+ json: { type: "boolean" },
2642
+ help: { type: "boolean", short: "h" },
2643
+ profile: { type: "string" },
2644
+ "api-url": { type: "string" },
2645
+ workstream: { type: "string", short: "w" },
2646
+ title: { type: "string" },
2647
+ list: { type: "string" },
2648
+ description: { type: "string" },
2649
+ },
2650
+ });
2651
+ const common = mergeCommonOptions(inherited, {
2652
+ json: parsed.values.json,
2653
+ profile: parsed.values.profile,
2654
+ apiUrl: parsed.values["api-url"],
2655
+ });
2656
+ if (parsed.values.help) {
2657
+ printWorkstreamHelp(runtime);
2658
+ return 0;
2659
+ }
2660
+ const workstreamId = await resolveWorkstreamId(runtime, common, parsed.values.workstream);
2661
+ const taskTitle = parsed.values.title ??
2662
+ (parsed.positionals.length > 0
2663
+ ? parsed.positionals.join(" ")
2664
+ : await readTextFromStdin(runtime.stdin));
2665
+ if (!taskTitle) {
2666
+ throw new CLIError("Task title is required. Use --title or provide as positional argument.", { json: common.json });
2667
+ }
2668
+ const { guidance, client } = await requireCommandProfile(runtime, common);
2669
+ const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
2670
+ let todoListId = parsed.values.list;
2671
+ if (!todoListId) {
2672
+ // Query existing TODOLIST attachments
2673
+ const data = await client.graphql(WORKSTREAM_TASK_LIST_QUERY, {
2674
+ id: workstream.id,
2675
+ attachmentsFirst: 50,
2676
+ });
2677
+ const lists = (data.workstream?.attachments.edges ?? []).flatMap((e) => (e.node.todoList ? [e.node.todoList] : []));
2678
+ if (lists.length === 1) {
2679
+ todoListId = lists[0].id;
2680
+ }
2681
+ else if (lists.length === 0) {
2682
+ // Auto-create a todo list with workstreamId for server-side attachment
2683
+ const createResult = await client.graphql(CREATE_TODO_LIST_MUTATION, {
2684
+ input: {
2685
+ title: `${workstream.name} Tasks`,
2686
+ workstreamId: workstream.id,
2687
+ },
2688
+ });
2689
+ todoListId = createResult.createTodoList.id;
2690
+ }
2691
+ else {
2692
+ const listSummary = lists
2693
+ .map((l) => ` ${l.id} (${l.title})`)
2694
+ .join("\n");
2695
+ throw new CLIError(`Multiple task lists found. Use --list <todoListId> to specify:\n${listSummary}`, { json: common.json });
2696
+ }
2697
+ }
2698
+ const result = await client.graphql(ADD_TODO_ITEM_MUTATION, {
2699
+ input: {
2700
+ todoListId,
2701
+ title: taskTitle,
2702
+ ...(parsed.values.description
2703
+ ? { description: parsed.values.description }
2704
+ : {}),
2705
+ },
2706
+ });
2707
+ if (common.json) {
2708
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
2709
+ ok: true,
2710
+ workstreamId: workstream.id,
2711
+ todoListId,
2712
+ item: result.addTodoItem,
2713
+ }, guidance));
2714
+ }
2715
+ else {
2716
+ printLine(runtime.stdout, `Added task "${result.addTodoItem.title}" to ${workstream.id}.`);
2717
+ }
2718
+ return 0;
2719
+ }
2720
+ case "complete": {
2721
+ const parsed = parseArgs({
2722
+ args: args.slice(2),
2723
+ allowPositionals: true,
2724
+ options: {
2725
+ json: { type: "boolean" },
2726
+ help: { type: "boolean", short: "h" },
2727
+ profile: { type: "string" },
2728
+ "api-url": { type: "string" },
2729
+ undo: { type: "boolean" },
2730
+ },
2731
+ });
2732
+ const common = mergeCommonOptions(inherited, {
2733
+ json: parsed.values.json,
2734
+ profile: parsed.values.profile,
2735
+ apiUrl: parsed.values["api-url"],
2736
+ });
2737
+ if (parsed.values.help) {
2738
+ printWorkstreamHelp(runtime);
2739
+ return 0;
2740
+ }
2741
+ const taskItemId = parsed.positionals[0];
2742
+ if (!taskItemId) {
2743
+ throw new CLIError("Usage: corners workstream task complete <taskItemId> [--undo]", { json: common.json });
2744
+ }
2745
+ const completed = parsed.values.undo !== true;
2746
+ const { guidance, client } = await requireCommandProfile(runtime, common);
2747
+ const result = await client.graphql(TOGGLE_TODO_ITEM_MUTATION, {
2748
+ id: taskItemId,
2749
+ completed,
2750
+ });
2751
+ if (common.json) {
2752
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
2753
+ ok: true,
2754
+ item: result.toggleTodoItem,
2755
+ }, guidance));
2756
+ }
2757
+ else {
2758
+ const verb = completed ? "Completed" : "Reopened";
2759
+ printLine(runtime.stdout, `${verb} task "${result.toggleTodoItem.title}" (${result.toggleTodoItem.id}).`);
2760
+ }
2761
+ return 0;
2762
+ }
2763
+ default:
2764
+ throw new CLIError(`Unknown workstream task command: ${nested}`);
2765
+ }
2766
+ }
2224
2767
  default:
2225
2768
  throw new CLIError(`Unknown workstream command: ${subcommand}`);
2226
2769
  }
2227
2770
  }
2771
+ async function handleMcp(args, runtime, inherited) {
2772
+ const subcommand = args[0];
2773
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
2774
+ printLine(runtime.stdout, "Usage: corners mcp serve [--profile <name>] [--api-url <url>]");
2775
+ printLine(runtime.stdout, "");
2776
+ printLine(runtime.stdout, "Run an MCP server over stdio exposing agent communication tools.");
2777
+ printLine(runtime.stdout, "Configure in Claude Code: corners mcp serve");
2778
+ return 0;
2779
+ }
2780
+ if (subcommand !== "serve") {
2781
+ throw new CLIError(`Unknown mcp command: ${subcommand}. Use "corners mcp serve".`);
2782
+ }
2783
+ const parsed = parseArgs({
2784
+ args: args.slice(1),
2785
+ allowPositionals: false,
2786
+ options: {
2787
+ profile: { type: "string" },
2788
+ "api-url": { type: "string" },
2789
+ },
2790
+ });
2791
+ const common = mergeCommonOptions(inherited, {
2792
+ profile: parsed.values.profile,
2793
+ apiUrl: parsed.values["api-url"],
2794
+ });
2795
+ const { client } = await requireCommandProfile(runtime, common);
2796
+ const { runMcpServer } = await import("./mcp-serve.js");
2797
+ await runMcpServer(client);
2798
+ return 0;
2799
+ }
2228
2800
  export async function runCli(argv, runtime = createRuntime()) {
2229
2801
  const normalizedArgv = argv[0] === "--" ? argv.slice(1) : argv;
2230
2802
  const parsed = parseLeadingCommonOptions(normalizedArgv);
@@ -2249,6 +2821,9 @@ export async function runCli(argv, runtime = createRuntime()) {
2249
2821
  else if (parsed.rest[1] === "guidance") {
2250
2822
  printGuidanceHelp(runtime);
2251
2823
  }
2824
+ else if (parsed.rest[1] === "skills") {
2825
+ printSkillsHelp(runtime);
2826
+ }
2252
2827
  else if (parsed.rest[1] === "init") {
2253
2828
  printInitHelp(runtime);
2254
2829
  }
@@ -2272,6 +2847,8 @@ export async function runCli(argv, runtime = createRuntime()) {
2272
2847
  return handleInit(parsed.rest.slice(1), runtime, parsed.common);
2273
2848
  case "guidance":
2274
2849
  return handleGuidance(parsed.rest.slice(1), runtime, parsed.common);
2850
+ case "skills":
2851
+ return handleSkills(parsed.rest.slice(1), runtime, parsed.common);
2275
2852
  case "auth":
2276
2853
  return handleAuth(parsed.rest.slice(1), runtime, parsed.common);
2277
2854
  case "corner":
@@ -2281,6 +2858,8 @@ export async function runCli(argv, runtime = createRuntime()) {
2281
2858
  case "workstream":
2282
2859
  case "ws":
2283
2860
  return handleWorkstream(parsed.rest.slice(1), runtime, parsed.common);
2861
+ case "mcp":
2862
+ return handleMcp(parsed.rest.slice(1), runtime, parsed.common);
2284
2863
  default:
2285
2864
  throw new CLIError(`Unknown command: ${command}`, {
2286
2865
  json: parsed.common.json,