@corners/cli 0.0.3 → 0.0.5

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
@@ -4,8 +4,9 @@ import { basename, dirname, 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
+ import { buildExpectedManagedGuidanceBlock, buildGuidanceFileContent, buildGuidanceRevision, extractManagedGuidanceBlock, GUIDANCE_SYNC_COMMAND, GUIDANCE_TARGETS, normalizeGuidanceTargets, } from "./guidance.js";
7
8
  import { createPromptApi } from "./prompts.js";
8
- import { CLIError, getPackageVersion, normalizeApiUrl, openUrlInBrowser, printJson, printLine, readTextFromStdin, sleep, toGraphQLAttachmentKind, toGraphQLWorkstreamUpdateType, toIsoString, } from "./support.js";
9
+ import { CLIError, getPackageVersion, normalizeApiUrl, openUrlInBrowser, printJson, printLine, readTextFromStdin, sleep, toGraphQLAttachmentKind, toGraphQLWorkstreamStatus, toGraphQLWorkstreamUpdateType, toIsoString, } from "./support.js";
9
10
  const WORKSTREAM_LOOKUP_QUERY = `
10
11
  query CliWorkstreamLookup($id: ID!) {
11
12
  workstream(id: $id) {
@@ -13,9 +14,11 @@ const WORKSTREAM_LOOKUP_QUERY = `
13
14
  accountId
14
15
  cornerId
15
16
  name
17
+ isOpen
16
18
  summary
17
19
  category
18
20
  status
21
+ statusDetails
19
22
  updatedAt
20
23
  topic {
21
24
  id
@@ -31,9 +34,11 @@ const WORKSTREAM_PULL_QUERY = `
31
34
  accountId
32
35
  cornerId
33
36
  name
37
+ isOpen
34
38
  summary
35
39
  category
36
40
  status
41
+ statusDetails
37
42
  createdAt
38
43
  updatedAt
39
44
  firstAttachmentActivityAt
@@ -283,6 +288,26 @@ const RECORD_WORKSTREAM_UPDATE_MUTATION = `
283
288
  }
284
289
  }
285
290
  `;
291
+ const UPDATE_WORKSTREAM_MUTATION = `
292
+ mutation CliUpdateWorkstream($id: ID!, $input: UpdateWorkstreamInput!) {
293
+ updateWorkstream(id: $id, input: $input) {
294
+ id
295
+ accountId
296
+ cornerId
297
+ name
298
+ isOpen
299
+ summary
300
+ category
301
+ status
302
+ statusDetails
303
+ updatedAt
304
+ topic {
305
+ id
306
+ name
307
+ }
308
+ }
309
+ }
310
+ `;
286
311
  const REVOKE_MCP_SESSION_MUTATION = `
287
312
  mutation CliRevokeMcpSession($id: ID!) {
288
313
  revokeMCPSession(id: $id)
@@ -318,9 +343,11 @@ const CREATE_WORKSTREAM_MUTATION = `
318
343
  id
319
344
  cornerId
320
345
  name
346
+ isOpen
321
347
  summary
322
348
  category
323
349
  status
350
+ statusDetails
324
351
  updatedAt
325
352
  topic {
326
353
  id
@@ -351,10 +378,11 @@ function printMainHelp(runtime) {
351
378
  "",
352
379
  "Commands:",
353
380
  " auth login|logout|status",
381
+ " guidance status|sync",
354
382
  " init",
355
383
  " corner use",
356
384
  " whoami",
357
- " workstream (ws) list|use|current|create|pull|push|question|attach|reply-thread",
385
+ " workstream (ws) list|use|current|create|pull|update|push|question|attach|reply-thread",
358
386
  " help",
359
387
  " version",
360
388
  "",
@@ -375,6 +403,15 @@ function printInitHelp(runtime) {
375
403
  " corners init [--profile <name>] [--api-url <url>] [--json]",
376
404
  ].join("\n"));
377
405
  }
406
+ function printGuidanceHelp(runtime) {
407
+ printLine(runtime.stdout, [
408
+ "corners guidance",
409
+ "",
410
+ "Usage:",
411
+ " corners guidance status [--json]",
412
+ " corners guidance sync [--json]",
413
+ ].join("\n"));
414
+ }
378
415
  function printAuthHelp(runtime) {
379
416
  printLine(runtime.stdout, [
380
417
  "corners auth",
@@ -411,6 +448,7 @@ function printWorkstreamHelp(runtime) {
411
448
  " corners workstream current [--json]",
412
449
  " corners workstream create <name> [--summary <text>] [--corner <cornerNameOrId>] [--json]",
413
450
  " corners workstream pull <workstreamId> [--json]",
451
+ " corners workstream update <workstreamId> [--status <status>] [--status-details <text> | --clear-status-details] [--json]",
414
452
  " corners workstream push <workstreamId> [--type <type>] [--message <text>] [--summary <text>] [--file <path>] [--title <title>] [--json]",
415
453
  " corners workstream question list <workstreamId> [--json]",
416
454
  " corners workstream question ask <workstreamId> [--question <text>] [--rationale <text>] [--suggested-answer <text>]... [--json]",
@@ -418,6 +456,9 @@ function printWorkstreamHelp(runtime) {
418
456
  " corners workstream attach <workstreamId> --kind <kind> --entity-id <id> [--json]",
419
457
  " corners workstream reply-thread <threadId> [--text <text>] [--json]",
420
458
  "",
459
+ "Lifecycle statuses:",
460
+ " scoping | in_progress | blocked | done",
461
+ "",
421
462
  "Update types:",
422
463
  " status | blocker | learning | outcome",
423
464
  ].join("\n"));
@@ -693,14 +734,104 @@ function resolveCornerForCwd(root, cwd) {
693
734
  corner: resolveCornerForRelativePath(root.config, relativePath),
694
735
  };
695
736
  }
696
- async function requireLocalRoot(runtime, common) {
737
+ async function resolveGuidanceState(root) {
738
+ const expectedBlock = buildExpectedManagedGuidanceBlock();
739
+ const fileEntries = await Promise.all(GUIDANCE_TARGETS.map(async (target) => ({
740
+ target,
741
+ content: await readOptionalUtf8(join(root.rootDir, target.relativePath)),
742
+ })));
743
+ const discoveredManagedTargets = GUIDANCE_TARGETS.map((target) => target.key).filter((key) => {
744
+ const entry = fileEntries.find((candidate) => candidate.target.key === key);
745
+ return extractManagedGuidanceBlock(entry?.content ?? null) !== null;
746
+ });
747
+ const configuredManagedTargets = root.config.guidance?.managedTargets;
748
+ const managedTargets = configuredManagedTargets !== undefined
749
+ ? normalizeGuidanceTargets(configuredManagedTargets)
750
+ : discoveredManagedTargets;
751
+ const files = fileEntries.map(({ target, content }) => {
752
+ if (content === null) {
753
+ return {
754
+ target: target.key,
755
+ path: target.relativePath,
756
+ state: "missing",
757
+ };
758
+ }
759
+ if (!managedTargets.includes(target.key)) {
760
+ return {
761
+ target: target.key,
762
+ path: target.relativePath,
763
+ state: "unmanaged",
764
+ };
765
+ }
766
+ return {
767
+ target: target.key,
768
+ path: target.relativePath,
769
+ state: extractManagedGuidanceBlock(content) === expectedBlock
770
+ ? "current"
771
+ : "stale",
772
+ };
773
+ });
774
+ const staleFiles = files
775
+ .filter((file) => file.state === "stale")
776
+ .map((file) => file.path);
777
+ return {
778
+ stale: staleFiles.length > 0,
779
+ lastSyncedAt: root.config.guidance?.syncedAt ?? null,
780
+ managedTargets,
781
+ staleFiles,
782
+ files,
783
+ };
784
+ }
785
+ function buildGuidanceWarning(guidance) {
786
+ if (!guidance.stale) {
787
+ return null;
788
+ }
789
+ const syncedSuffix = guidance.lastSyncedAt
790
+ ? ` Last synced: ${guidance.lastSyncedAt}.`
791
+ : "";
792
+ return `Warning: managed Corners guidance is out of date in ${guidance.staleFiles.length} file(s).${syncedSuffix} Run \`${GUIDANCE_SYNC_COMMAND}\`.`;
793
+ }
794
+ function buildGuidanceJsonMetadata(guidance) {
795
+ return {
796
+ stale: guidance.stale,
797
+ lastSyncedAt: guidance.lastSyncedAt,
798
+ staleFiles: guidance.staleFiles,
799
+ syncCommand: GUIDANCE_SYNC_COMMAND,
800
+ };
801
+ }
802
+ function withGuidanceJsonMetadata(payload, guidance) {
803
+ if (!guidance) {
804
+ return payload;
805
+ }
806
+ return {
807
+ ...payload,
808
+ _corners: {
809
+ guidance: buildGuidanceJsonMetadata(guidance),
810
+ },
811
+ };
812
+ }
813
+ async function resolveRootContext(runtime, common, root, options) {
814
+ const guidance = await resolveGuidanceState(root);
815
+ if ((options?.emitWarning ?? true) && !common.json) {
816
+ const warning = buildGuidanceWarning(guidance);
817
+ if (warning) {
818
+ printLine(runtime.stderr, warning);
819
+ }
820
+ }
821
+ return {
822
+ root,
823
+ guidance,
824
+ };
825
+ }
826
+ async function requireLocalRoot(runtime, common, options) {
697
827
  const root = await runtime.config.findLocalRoot(runtime.cwd);
698
828
  if (!root) {
699
829
  throw new CLIError("No local Corners CLI root found. Run `corners init` from your project root first.", { json: common.json });
700
830
  }
701
- return root;
831
+ return resolveRootContext(runtime, common, root, options);
702
832
  }
703
- async function requirePinnedRootProfile(runtime, common, root) {
833
+ async function requirePinnedRootProfile(runtime, common, rootContext) {
834
+ const { root, guidance } = rootContext;
704
835
  if (common.profile && common.profile !== root.config.profile) {
705
836
  throw new CLIError(`This initialized root is pinned to profile ${root.config.profile}.`, { json: common.json });
706
837
  }
@@ -724,6 +855,7 @@ async function requirePinnedRootProfile(runtime, common, root) {
724
855
  };
725
856
  return {
726
857
  root,
858
+ guidance,
727
859
  profileName: selected.name,
728
860
  profile,
729
861
  client: runtime.createClient({
@@ -735,11 +867,13 @@ async function requirePinnedRootProfile(runtime, common, root) {
735
867
  async function requireCommandProfile(runtime, common) {
736
868
  const root = await runtime.config.findLocalRoot(runtime.cwd);
737
869
  if (root) {
738
- return requirePinnedRootProfile(runtime, common, root);
870
+ const rootContext = await resolveRootContext(runtime, common, root);
871
+ return requirePinnedRootProfile(runtime, common, rootContext);
739
872
  }
740
873
  const selected = await requireStoredProfile(runtime, common);
741
874
  return {
742
875
  root: null,
876
+ guidance: null,
743
877
  ...selected,
744
878
  };
745
879
  }
@@ -805,55 +939,6 @@ function upsertCornerOverride(config, relativePath, corner) {
805
939
  cornerOverrides: overrides,
806
940
  };
807
941
  }
808
- const GUIDANCE_SECTION_START = "<!-- corners-cli:start -->";
809
- const GUIDANCE_SECTION_END = "<!-- corners-cli:end -->";
810
- function buildGuidanceSection() {
811
- return [
812
- "## Corners CLI",
813
- "",
814
- "Corners CLI usage is local to each user. These shared instructions describe how AI should use the CLI, not which corners or workstreams a user has selected locally.",
815
- "",
816
- "- Only assume Corners CLI is initialized when `.corners/config.json` exists locally in the current repo root or an ancestor folder.",
817
- "- The current folder selects the default corner through local path rules managed by `corners corner use`.",
818
- "- Create new workstreams with `corners workstream create`.",
819
- "- Connect existing workstreams to the local environment with `corners workstream use <workstreamId>`.",
820
- "- Pass explicit workstream IDs to `corners workstream pull`, `push`, `question`, and `attach` commands.",
821
- "- `corners workstream list` shows the workstreams connected for the current local environment.",
822
- "",
823
- "If `.corners/config.json` is absent, do not assume the repo is initialized for the current user.",
824
- ].join("\n");
825
- }
826
- function upsertManagedSection(existing, section) {
827
- const block = `${GUIDANCE_SECTION_START}\n${section}\n${GUIDANCE_SECTION_END}`;
828
- const startIndex = existing.indexOf(GUIDANCE_SECTION_START);
829
- const endIndex = existing.indexOf(GUIDANCE_SECTION_END);
830
- if (startIndex >= 0 && endIndex > startIndex) {
831
- return ensureTrailingNewline(`${existing.slice(0, startIndex).trimEnd()}\n\n${block}\n${existing
832
- .slice(endIndex + GUIDANCE_SECTION_END.length)
833
- .trimStart()}`.trim());
834
- }
835
- const trimmed = existing.trim();
836
- if (!trimmed) {
837
- return ensureTrailingNewline(block);
838
- }
839
- return ensureTrailingNewline(`${trimmed}\n\n${block}`);
840
- }
841
- function buildGuidanceFileContent(target, existing) {
842
- const section = buildGuidanceSection();
843
- if (target === "cursor") {
844
- const base = existing?.trim()
845
- ? existing
846
- : [
847
- "---",
848
- "description: Corners CLI guidance",
849
- "alwaysApply: true",
850
- "---",
851
- "",
852
- ].join("\n");
853
- return upsertManagedSection(base, section);
854
- }
855
- return upsertManagedSection(existing ?? "", section);
856
- }
857
942
  async function buildPlannedEdit(path, nextContent) {
858
943
  const existing = await readOptionalUtf8(path);
859
944
  if (existing === nextContent) {
@@ -865,6 +950,16 @@ async function buildPlannedEdit(path, nextContent) {
865
950
  content: nextContent,
866
951
  };
867
952
  }
953
+ function withUpdatedGuidanceConfig(config, managedTargets, syncedAt) {
954
+ return {
955
+ ...config,
956
+ guidance: {
957
+ managedTargets: normalizeGuidanceTargets(managedTargets),
958
+ syncedAt,
959
+ revision: buildGuidanceRevision(),
960
+ },
961
+ };
962
+ }
868
963
  function ensureGitignoreEntry(existing) {
869
964
  const content = existing ?? "";
870
965
  const lines = content
@@ -889,7 +984,16 @@ async function writePlannedEdits(edits) {
889
984
  }
890
985
  }
891
986
  function formatWorkstreamLine(workstream) {
892
- return `${workstream.id} ${workstream.name} ${workstream.status.toLowerCase()}${workstream.summary ? ` ${workstream.summary}` : ""}`;
987
+ return [
988
+ workstream.id,
989
+ workstream.name,
990
+ workstream.isOpen ? "open" : "archived",
991
+ workstream.status.toLowerCase(),
992
+ workstream.statusDetails ?? null,
993
+ workstream.summary ? workstream.summary : null,
994
+ ]
995
+ .filter((part) => Boolean(part))
996
+ .join(" ");
893
997
  }
894
998
  async function handleAuth(args, runtime, inherited) {
895
999
  const subcommand = args[0];
@@ -1191,30 +1295,8 @@ async function handleInit(args, runtime, inherited) {
1191
1295
  const rootDir = existingRoot?.rootDir ?? currentRootPath;
1192
1296
  const existingConfig = existingRoot?.config ?? null;
1193
1297
  return withPrompts(runtime, async (prompts) => {
1194
- const guidanceTargets = [
1195
- {
1196
- key: "agents",
1197
- label: "Manage AGENTS.md guidance",
1198
- relativePath: "AGENTS.md",
1199
- },
1200
- {
1201
- key: "claude",
1202
- label: "Manage CLAUDE.md guidance",
1203
- relativePath: "CLAUDE.md",
1204
- },
1205
- {
1206
- key: "cursor",
1207
- label: "Manage Cursor rule guidance",
1208
- relativePath: ".cursor/rules/corners-cli.mdc",
1209
- },
1210
- {
1211
- key: "copilot",
1212
- label: "Manage Copilot instructions",
1213
- relativePath: ".github/copilot-instructions.md",
1214
- },
1215
- ];
1216
1298
  const selectedTargets = [];
1217
- for (const target of guidanceTargets) {
1299
+ for (const target of GUIDANCE_TARGETS) {
1218
1300
  const include = await prompts.confirm(target.label, {
1219
1301
  defaultValue: true,
1220
1302
  });
@@ -1230,7 +1312,8 @@ async function handleInit(args, runtime, inherited) {
1230
1312
  });
1231
1313
  const defaultCorner = joinedCorners.find((corner) => corner.id === selectedCornerId) ??
1232
1314
  joinedCorners[0];
1233
- const nextLocalConfig = {
1315
+ const syncedAt = toIsoString();
1316
+ const nextLocalConfig = withUpdatedGuidanceConfig({
1234
1317
  version: 1,
1235
1318
  profile: profileName,
1236
1319
  workspace: profile.workspace,
@@ -1241,7 +1324,7 @@ async function handleInit(args, runtime, inherited) {
1241
1324
  },
1242
1325
  cornerOverrides: existingConfig?.cornerOverrides ?? [],
1243
1326
  connectedWorkstreams: existingConfig?.connectedWorkstreams ?? [],
1244
- };
1327
+ }, selectedTargets.map((target) => target.key), syncedAt);
1245
1328
  const plannedEdits = [];
1246
1329
  const gitignorePath = join(rootDir, ".gitignore");
1247
1330
  const gitignoreContent = ensureGitignoreEntry(await readOptionalUtf8(gitignorePath));
@@ -1302,11 +1385,132 @@ async function handleInit(args, runtime, inherited) {
1302
1385
  printJson(runtime.stdout, payload);
1303
1386
  }
1304
1387
  else {
1305
- printLine(runtime.stdout, `Initialized Corners CLI at ${rootDir} with default corner ${defaultCorner.name}.`);
1388
+ printLine(runtime.stdout, `Initialized Corners CLI at ${rootDir} with default corner ${defaultCorner.name}. Run \`${GUIDANCE_SYNC_COMMAND}\` later to refresh managed guidance.`);
1306
1389
  }
1307
1390
  return 0;
1308
1391
  });
1309
1392
  }
1393
+ async function handleGuidance(args, runtime, inherited) {
1394
+ const subcommand = args[0];
1395
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
1396
+ printGuidanceHelp(runtime);
1397
+ return 0;
1398
+ }
1399
+ switch (subcommand) {
1400
+ case "status": {
1401
+ const parsed = parseArgs({
1402
+ args: args.slice(1),
1403
+ allowPositionals: false,
1404
+ options: {
1405
+ json: { type: "boolean" },
1406
+ help: { type: "boolean", short: "h" },
1407
+ },
1408
+ });
1409
+ const common = mergeCommonOptions(inherited, {
1410
+ json: parsed.values.json,
1411
+ });
1412
+ if (parsed.values.help) {
1413
+ printGuidanceHelp(runtime);
1414
+ return 0;
1415
+ }
1416
+ const rootContext = await requireLocalRoot(runtime, common, {
1417
+ emitWarning: false,
1418
+ });
1419
+ const payload = {
1420
+ ok: true,
1421
+ root: rootContext.root.rootDir,
1422
+ guidance: rootContext.guidance,
1423
+ };
1424
+ if (common.json) {
1425
+ printJson(runtime.stdout, payload);
1426
+ }
1427
+ else {
1428
+ printLine(runtime.stdout, [
1429
+ `Root: ${payload.root}`,
1430
+ `Stale: ${payload.guidance.stale ? "yes" : "no"}`,
1431
+ `Last synced: ${payload.guidance.lastSyncedAt ?? "-"}`,
1432
+ `Managed targets: ${payload.guidance.managedTargets.join(", ") || "-"}`,
1433
+ ...payload.guidance.files.map((file) => `${file.path}: ${file.state}`),
1434
+ ].join("\n"));
1435
+ }
1436
+ return 0;
1437
+ }
1438
+ case "sync": {
1439
+ const parsed = parseArgs({
1440
+ args: args.slice(1),
1441
+ allowPositionals: false,
1442
+ options: {
1443
+ json: { type: "boolean" },
1444
+ help: { type: "boolean", short: "h" },
1445
+ },
1446
+ });
1447
+ const common = mergeCommonOptions(inherited, {
1448
+ json: parsed.values.json,
1449
+ });
1450
+ if (parsed.values.help) {
1451
+ printGuidanceHelp(runtime);
1452
+ return 0;
1453
+ }
1454
+ const rootContext = await requireLocalRoot(runtime, common, {
1455
+ emitWarning: false,
1456
+ });
1457
+ const managedTargets = rootContext.root.config.guidance?.managedTargets !== undefined
1458
+ ? normalizeGuidanceTargets(rootContext.root.config.guidance.managedTargets)
1459
+ : rootContext.guidance.managedTargets;
1460
+ const edits = [];
1461
+ const actions = new Map();
1462
+ for (const target of GUIDANCE_TARGETS) {
1463
+ if (!managedTargets.includes(target.key)) {
1464
+ actions.set(target.key, "skipped");
1465
+ continue;
1466
+ }
1467
+ const targetPath = join(rootContext.root.rootDir, target.relativePath);
1468
+ const nextContent = buildGuidanceFileContent(target.key, await readOptionalUtf8(targetPath));
1469
+ const edit = await buildPlannedEdit(targetPath, nextContent);
1470
+ if (edit) {
1471
+ edits.push(edit);
1472
+ actions.set(target.key, edit.action);
1473
+ }
1474
+ else {
1475
+ actions.set(target.key, "unchanged");
1476
+ }
1477
+ }
1478
+ if (edits.length > 0) {
1479
+ await writePlannedEdits(edits);
1480
+ }
1481
+ const syncedAt = toIsoString();
1482
+ const nextConfig = withUpdatedGuidanceConfig(rootContext.root.config, managedTargets, syncedAt);
1483
+ await runtime.config.writeLocalConfig(rootContext.root.rootDir, nextConfig);
1484
+ const nextRoot = {
1485
+ ...rootContext.root,
1486
+ config: nextConfig,
1487
+ };
1488
+ const nextGuidance = await resolveGuidanceState(nextRoot);
1489
+ const guidance = {
1490
+ ...nextGuidance,
1491
+ files: nextGuidance.files.map((file) => ({
1492
+ ...file,
1493
+ action: actions.get(file.target) ?? "skipped",
1494
+ })),
1495
+ };
1496
+ const payload = {
1497
+ ok: true,
1498
+ root: nextRoot.rootDir,
1499
+ guidance,
1500
+ };
1501
+ if (common.json) {
1502
+ printJson(runtime.stdout, payload);
1503
+ }
1504
+ else {
1505
+ const updatedCount = guidance.files.filter((file) => file.action === "create" || file.action === "update").length;
1506
+ printLine(runtime.stdout, `Synchronized Corners guidance at ${payload.root}. Updated ${updatedCount} file(s).`);
1507
+ }
1508
+ return 0;
1509
+ }
1510
+ default:
1511
+ throw new CLIError(`Unknown guidance command: ${subcommand}`);
1512
+ }
1513
+ }
1310
1514
  async function handleCorner(args, runtime, inherited) {
1311
1515
  const subcommand = args[0];
1312
1516
  if (!subcommand || subcommand === "help" || subcommand === "--help") {
@@ -1339,8 +1543,8 @@ async function handleCorner(args, runtime, inherited) {
1339
1543
  if (!cornerNameOrId) {
1340
1544
  throw new CLIError("Usage: corners corner use <cornerNameOrId> [--path <path>]", { json: common.json });
1341
1545
  }
1342
- const root = await requireLocalRoot(runtime, common);
1343
- const { client } = await requirePinnedRootProfile(runtime, common, root);
1546
+ const rootContext = await requireLocalRoot(runtime, common);
1547
+ const { root, guidance, client } = await requirePinnedRootProfile(runtime, common, rootContext);
1344
1548
  const corner = await resolveMemberCorner(client, common, cornerNameOrId);
1345
1549
  const cwdPath = await realpath(runtime.cwd);
1346
1550
  const targetAbsolutePath = resolvePathWithinRoot(root.rootDir, cwdPath, parsed.values.path);
@@ -1351,12 +1555,12 @@ async function handleCorner(args, runtime, inherited) {
1351
1555
  });
1352
1556
  await runtime.config.writeLocalConfig(root.rootDir, nextConfig);
1353
1557
  if (common.json) {
1354
- printJson(runtime.stdout, {
1558
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
1355
1559
  ok: true,
1356
1560
  root: root.rootDir,
1357
1561
  path: relativePath,
1358
1562
  corner,
1359
- });
1563
+ }, guidance));
1360
1564
  }
1361
1565
  else {
1362
1566
  printLine(runtime.stdout, `Set default corner for ${relativePath} to ${corner.name}.`);
@@ -1394,8 +1598,8 @@ async function handleWorkstream(args, runtime, inherited) {
1394
1598
  printWorkstreamHelp(runtime);
1395
1599
  return 0;
1396
1600
  }
1397
- const root = await requireLocalRoot(runtime, common);
1398
- const { client } = await requirePinnedRootProfile(runtime, common, root);
1601
+ const rootContext = await requireLocalRoot(runtime, common);
1602
+ const { root, guidance, client } = await requirePinnedRootProfile(runtime, common, rootContext);
1399
1603
  const hydrated = await Promise.all(root.config.connectedWorkstreams.map(async (entry) => {
1400
1604
  try {
1401
1605
  return {
@@ -1417,11 +1621,11 @@ async function handleWorkstream(args, runtime, inherited) {
1417
1621
  .filter((entry) => entry.workstream === null)
1418
1622
  .map((entry) => entry.entry.id);
1419
1623
  if (common.json) {
1420
- printJson(runtime.stdout, {
1624
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
1421
1625
  root: root.rootDir,
1422
1626
  workstreams,
1423
1627
  missingWorkstreamIds,
1424
- });
1628
+ }, guidance));
1425
1629
  }
1426
1630
  else {
1427
1631
  if (missingWorkstreamIds.length > 0) {
@@ -1461,19 +1665,19 @@ async function handleWorkstream(args, runtime, inherited) {
1461
1665
  json: common.json,
1462
1666
  });
1463
1667
  }
1464
- const root = await requireLocalRoot(runtime, common);
1465
- const { client } = await requirePinnedRootProfile(runtime, common, root);
1668
+ const rootContext = await requireLocalRoot(runtime, common);
1669
+ const { root, guidance, client } = await requirePinnedRootProfile(runtime, common, rootContext);
1466
1670
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1467
1671
  const nextConfig = connectWorkstream(root.config, workstream.id);
1468
1672
  if (nextConfig !== root.config) {
1469
1673
  await runtime.config.writeLocalConfig(root.rootDir, nextConfig);
1470
1674
  }
1471
1675
  if (common.json) {
1472
- printJson(runtime.stdout, {
1676
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
1473
1677
  ok: true,
1474
1678
  root: root.rootDir,
1475
1679
  workstream,
1476
- });
1680
+ }, guidance));
1477
1681
  }
1478
1682
  else {
1479
1683
  printLine(runtime.stdout, `Connected ${workstream.id} (${workstream.name}) to ${root.rootDir}.`);
@@ -1496,7 +1700,7 @@ async function handleWorkstream(args, runtime, inherited) {
1496
1700
  printWorkstreamHelp(runtime);
1497
1701
  return 0;
1498
1702
  }
1499
- const root = await requireLocalRoot(runtime, common);
1703
+ const { root, guidance } = await requireLocalRoot(runtime, common);
1500
1704
  const cwdPath = await realpath(runtime.cwd);
1501
1705
  const resolvedCorner = resolveCornerForCwd(root, cwdPath);
1502
1706
  const payload = {
@@ -1508,7 +1712,7 @@ async function handleWorkstream(args, runtime, inherited) {
1508
1712
  connectedWorkstreamIds: root.config.connectedWorkstreams.map((entry) => entry.id),
1509
1713
  };
1510
1714
  if (common.json) {
1511
- printJson(runtime.stdout, payload);
1715
+ printJson(runtime.stdout, withGuidanceJsonMetadata(payload, guidance));
1512
1716
  }
1513
1717
  else {
1514
1718
  printLine(runtime.stdout, [
@@ -1548,8 +1752,8 @@ async function handleWorkstream(args, runtime, inherited) {
1548
1752
  if (!name) {
1549
1753
  throw new CLIError("Usage: corners workstream create <name> [--summary <text>] [--corner <cornerNameOrId>]", { json: common.json });
1550
1754
  }
1551
- const root = await requireLocalRoot(runtime, common);
1552
- const { client } = await requirePinnedRootProfile(runtime, common, root);
1755
+ const rootContext = await requireLocalRoot(runtime, common);
1756
+ const { root, guidance, client } = await requirePinnedRootProfile(runtime, common, rootContext);
1553
1757
  const cwdPath = await realpath(runtime.cwd);
1554
1758
  const corner = parsed.values.corner
1555
1759
  ? await resolveMemberCorner(client, common, parsed.values.corner)
@@ -1566,12 +1770,12 @@ async function handleWorkstream(args, runtime, inherited) {
1566
1770
  await runtime.config.writeLocalConfig(root.rootDir, nextConfig);
1567
1771
  }
1568
1772
  if (common.json) {
1569
- printJson(runtime.stdout, {
1773
+ printJson(runtime.stdout, withGuidanceJsonMetadata({
1570
1774
  ok: true,
1571
1775
  root: root.rootDir,
1572
1776
  corner,
1573
1777
  workstream: result.createWorkstream,
1574
- });
1778
+ }, guidance));
1575
1779
  }
1576
1780
  else {
1577
1781
  printLine(runtime.stdout, `Created ${result.createWorkstream.id} (${result.createWorkstream.name}) in ${corner.name}.`);
@@ -1604,7 +1808,7 @@ async function handleWorkstream(args, runtime, inherited) {
1604
1808
  json: common.json,
1605
1809
  });
1606
1810
  }
1607
- const { client } = await requireCommandProfile(runtime, common);
1811
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1608
1812
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1609
1813
  const data = await client.graphql(WORKSTREAM_PULL_QUERY, {
1610
1814
  id: workstream.id,
@@ -1612,13 +1816,16 @@ async function handleWorkstream(args, runtime, inherited) {
1612
1816
  feedFirst: 20,
1613
1817
  });
1614
1818
  if (common.json) {
1615
- printJson(runtime.stdout, data.workstream);
1819
+ printJson(runtime.stdout, withGuidanceJsonMetadata(data.workstream, guidance));
1616
1820
  }
1617
1821
  else {
1618
1822
  const snapshot = data.workstream;
1619
1823
  printLine(runtime.stdout, [
1620
1824
  `${snapshot.name} (${snapshot.id})`,
1621
1825
  snapshot.summary ? `Summary: ${snapshot.summary}` : "Summary: -",
1826
+ `State: ${snapshot.isOpen === false ? "archived" : "open"}`,
1827
+ `Lifecycle status: ${(snapshot.status ?? "SCOPING").toLowerCase()}`,
1828
+ `Status details: ${snapshot.statusDetails ?? "-"}`,
1622
1829
  `Open questions: ${snapshot.openQuestions?.length ?? 0}`,
1623
1830
  `Answered questions: ${snapshot.answeredQuestions?.length ?? 0}`,
1624
1831
  `Attachments: ${snapshot.attachments?.totalCount ?? 0}`,
@@ -1627,6 +1834,75 @@ async function handleWorkstream(args, runtime, inherited) {
1627
1834
  }
1628
1835
  return 0;
1629
1836
  }
1837
+ case "update": {
1838
+ const parsed = parseArgs({
1839
+ args: args.slice(1),
1840
+ allowPositionals: true,
1841
+ options: {
1842
+ json: { type: "boolean" },
1843
+ help: { type: "boolean", short: "h" },
1844
+ profile: { type: "string" },
1845
+ "api-url": { type: "string" },
1846
+ status: { type: "string" },
1847
+ "status-details": { type: "string" },
1848
+ "clear-status-details": { type: "boolean" },
1849
+ },
1850
+ });
1851
+ const common = mergeCommonOptions(inherited, {
1852
+ json: parsed.values.json,
1853
+ profile: parsed.values.profile,
1854
+ apiUrl: parsed.values["api-url"],
1855
+ });
1856
+ if (parsed.values.help) {
1857
+ printWorkstreamHelp(runtime);
1858
+ return 0;
1859
+ }
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
+ const rawStatusDetails = parsed.values["status-details"];
1865
+ const clearStatusDetails = parsed.values["clear-status-details"] === true;
1866
+ if (rawStatusDetails !== undefined && clearStatusDetails) {
1867
+ throw new CLIError("Choose either --status-details or --clear-status-details, not both.", { json: common.json });
1868
+ }
1869
+ if (parsed.values.status === undefined &&
1870
+ rawStatusDetails === undefined &&
1871
+ !clearStatusDetails) {
1872
+ throw new CLIError("Provide --status, --status-details, or --clear-status-details.", { json: common.json });
1873
+ }
1874
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1875
+ const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1876
+ const result = await client.graphql(UPDATE_WORKSTREAM_MUTATION, {
1877
+ id: workstream.id,
1878
+ input: {
1879
+ status: parsed.values.status === undefined
1880
+ ? undefined
1881
+ : toGraphQLWorkstreamStatus(parsed.values.status),
1882
+ statusDetails: clearStatusDetails
1883
+ ? null
1884
+ : rawStatusDetails === undefined
1885
+ ? undefined
1886
+ : rawStatusDetails,
1887
+ },
1888
+ });
1889
+ const payload = {
1890
+ ok: true,
1891
+ workstream: result.updateWorkstream,
1892
+ };
1893
+ if (common.json) {
1894
+ printJson(runtime.stdout, withGuidanceJsonMetadata(payload, guidance));
1895
+ }
1896
+ else {
1897
+ printLine(runtime.stdout, [
1898
+ `Updated ${result.updateWorkstream.id}.`,
1899
+ `State: ${result.updateWorkstream.isOpen ? "open" : "archived"}`,
1900
+ `Lifecycle status: ${result.updateWorkstream.status.toLowerCase()}`,
1901
+ `Status details: ${result.updateWorkstream.statusDetails ?? "-"}`,
1902
+ ].join("\n"));
1903
+ }
1904
+ return 0;
1905
+ }
1630
1906
  case "push": {
1631
1907
  const parsed = parseArgs({
1632
1908
  args: args.slice(1),
@@ -1665,7 +1941,7 @@ async function handleWorkstream(args, runtime, inherited) {
1665
1941
  if (!message) {
1666
1942
  throw new CLIError("Workstream updates need a message. Use --message or pipe text on stdin.", { json: common.json });
1667
1943
  }
1668
- const { client } = await requireCommandProfile(runtime, common);
1944
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1669
1945
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1670
1946
  let createdDocument = null;
1671
1947
  if (parsed.values.file) {
@@ -1696,7 +1972,7 @@ async function handleWorkstream(args, runtime, inherited) {
1696
1972
  document: createdDocument,
1697
1973
  };
1698
1974
  if (common.json) {
1699
- printJson(runtime.stdout, payload);
1975
+ printJson(runtime.stdout, withGuidanceJsonMetadata(payload, guidance));
1700
1976
  }
1701
1977
  else {
1702
1978
  printLine(runtime.stdout, `Recorded ${String(parsed.values.type ?? "status")} update on ${workstream.id}.`);
@@ -1734,7 +2010,7 @@ async function handleWorkstream(args, runtime, inherited) {
1734
2010
  if (!workstreamId) {
1735
2011
  throw new CLIError("Usage: corners workstream question list <workstreamId>", { json: common.json });
1736
2012
  }
1737
- const { client } = await requireCommandProfile(runtime, common);
2013
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1738
2014
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1739
2015
  const data = await client.graphql(WORKSTREAM_QUESTION_LIST_QUERY, {
1740
2016
  id: workstream.id,
@@ -1743,7 +2019,7 @@ async function handleWorkstream(args, runtime, inherited) {
1743
2019
  throw new CLIError("Workstream not found", { json: common.json });
1744
2020
  }
1745
2021
  if (common.json) {
1746
- printJson(runtime.stdout, data.workstream);
2022
+ printJson(runtime.stdout, withGuidanceJsonMetadata(data.workstream, guidance));
1747
2023
  }
1748
2024
  else {
1749
2025
  printLine(runtime.stdout, [
@@ -1789,7 +2065,7 @@ async function handleWorkstream(args, runtime, inherited) {
1789
2065
  if (!question) {
1790
2066
  throw new CLIError("Question text is required. Use --question or pipe text on stdin.", { json: common.json });
1791
2067
  }
1792
- const { client } = await requireCommandProfile(runtime, common);
2068
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1793
2069
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1794
2070
  const result = await client.graphql(CREATE_WORKSTREAM_QUESTION_MUTATION, {
1795
2071
  input: {
@@ -1800,7 +2076,7 @@ async function handleWorkstream(args, runtime, inherited) {
1800
2076
  },
1801
2077
  });
1802
2078
  if (common.json) {
1803
- printJson(runtime.stdout, result.createWorkstreamQuestion);
2079
+ printJson(runtime.stdout, withGuidanceJsonMetadata(result.createWorkstreamQuestion, guidance));
1804
2080
  }
1805
2081
  else {
1806
2082
  printLine(runtime.stdout, `Created question on ${workstream.id}.`);
@@ -1841,13 +2117,13 @@ async function handleWorkstream(args, runtime, inherited) {
1841
2117
  json: common.json,
1842
2118
  });
1843
2119
  }
1844
- const { client } = await requireCommandProfile(runtime, common);
2120
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1845
2121
  const result = await client.graphql(ANSWER_WORKSTREAM_QUESTION_MUTATION, {
1846
2122
  questionId,
1847
2123
  answerText: answer,
1848
2124
  });
1849
2125
  if (common.json) {
1850
- printJson(runtime.stdout, result.answerWorkstreamQuestion);
2126
+ printJson(runtime.stdout, withGuidanceJsonMetadata(result.answerWorkstreamQuestion, guidance));
1851
2127
  }
1852
2128
  else {
1853
2129
  printLine(runtime.stdout, `Answered ${questionId}.`);
@@ -1886,7 +2162,7 @@ async function handleWorkstream(args, runtime, inherited) {
1886
2162
  if (!workstreamId || !kind || !entityId) {
1887
2163
  throw new CLIError("Usage: corners workstream attach <workstreamId> --kind <kind> --entity-id <id>", { json: common.json });
1888
2164
  }
1889
- const { client } = await requireCommandProfile(runtime, common);
2165
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1890
2166
  const workstream = await resolveExplicitWorkstream(client, common, workstreamId);
1891
2167
  const result = await client.graphql(ADD_WORKSTREAM_ATTACHMENT_MUTATION, {
1892
2168
  workstreamId: workstream.id,
@@ -1894,7 +2170,7 @@ async function handleWorkstream(args, runtime, inherited) {
1894
2170
  entityId,
1895
2171
  });
1896
2172
  if (common.json) {
1897
- printJson(runtime.stdout, result.addWorkstreamAttachment);
2173
+ printJson(runtime.stdout, withGuidanceJsonMetadata(result.addWorkstreamAttachment, guidance));
1898
2174
  }
1899
2175
  else {
1900
2176
  printLine(runtime.stdout, `Attached ${kind}:${entityId} to ${workstream.id}.`);
@@ -1930,7 +2206,7 @@ async function handleWorkstream(args, runtime, inherited) {
1930
2206
  if (!threadId || !text) {
1931
2207
  throw new CLIError("Usage: corners workstream reply-thread <threadId> [--text <text>]", { json: common.json });
1932
2208
  }
1933
- const { client } = await requireCommandProfile(runtime, common);
2209
+ const { guidance, client } = await requireCommandProfile(runtime, common);
1934
2210
  const result = await client.graphql(REPLY_ARTIFACT_THREAD_MUTATION, {
1935
2211
  id: threadId,
1936
2212
  input: {
@@ -1938,7 +2214,7 @@ async function handleWorkstream(args, runtime, inherited) {
1938
2214
  },
1939
2215
  });
1940
2216
  if (common.json) {
1941
- printJson(runtime.stdout, result.replyArtifactThread);
2217
+ printJson(runtime.stdout, withGuidanceJsonMetadata(result.replyArtifactThread, guidance));
1942
2218
  }
1943
2219
  else {
1944
2220
  printLine(runtime.stdout, `Replied to ${threadId}.`);
@@ -1970,6 +2246,9 @@ export async function runCli(argv, runtime = createRuntime()) {
1970
2246
  if (parsed.rest[1] === "auth") {
1971
2247
  printAuthHelp(runtime);
1972
2248
  }
2249
+ else if (parsed.rest[1] === "guidance") {
2250
+ printGuidanceHelp(runtime);
2251
+ }
1973
2252
  else if (parsed.rest[1] === "init") {
1974
2253
  printInitHelp(runtime);
1975
2254
  }
@@ -1991,6 +2270,8 @@ export async function runCli(argv, runtime = createRuntime()) {
1991
2270
  return 0;
1992
2271
  case "init":
1993
2272
  return handleInit(parsed.rest.slice(1), runtime, parsed.common);
2273
+ case "guidance":
2274
+ return handleGuidance(parsed.rest.slice(1), runtime, parsed.common);
1994
2275
  case "auth":
1995
2276
  return handleAuth(parsed.rest.slice(1), runtime, parsed.common);
1996
2277
  case "corner":