@outcomeeng/spx 0.1.6 → 0.1.8

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,7 +1,7 @@
1
1
  import {
2
2
  LEAF_KIND,
3
3
  parseWorkItemName
4
- } from "./chunk-5L7CHFBC.js";
4
+ } from "./chunk-BQLLI7GS.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command } from "commander";
@@ -17,19 +17,19 @@ async function initCommand(options = {}) {
17
17
  ["plugin", "marketplace", "list"],
18
18
  { cwd }
19
19
  );
20
- const exists = listOutput.includes("spx-claude");
20
+ const exists = listOutput.includes("outcomeeng");
21
21
  if (!exists) {
22
22
  await execa(
23
23
  "claude",
24
- ["plugin", "marketplace", "add", "simonheimlicher/spx-claude"],
24
+ ["plugin", "marketplace", "add", "outcomeeng/claude"],
25
25
  { cwd }
26
26
  );
27
- return "\u2713 spx-claude marketplace installed successfully\n\nRun 'claude plugin marketplace list' to view all marketplaces.";
27
+ return "\u2713 outcomeeng marketplace installed successfully\n\nRun 'claude plugin marketplace list' to view all marketplaces.";
28
28
  } else {
29
- await execa("claude", ["plugin", "marketplace", "update", "spx-claude"], {
29
+ await execa("claude", ["plugin", "marketplace", "update", "outcomeeng"], {
30
30
  cwd
31
31
  });
32
- return "\u2713 spx-claude marketplace updated successfully\n\nThe marketplace is now up to date.";
32
+ return "\u2713 outcomeeng marketplace updated successfully\n\nThe marketplace is now up to date.";
33
33
  }
34
34
  } catch (error) {
35
35
  if (error instanceof Error) {
@@ -643,7 +643,7 @@ Searched for: **/.claude/settings.local.json`;
643
643
 
644
644
  // src/domains/claude/index.ts
645
645
  function registerClaudeCommands(claudeCmd) {
646
- claudeCmd.command("init").description("Initialize or update spx-claude marketplace plugin").action(async () => {
646
+ claudeCmd.command("init").description("Initialize or update outcomeeng marketplace plugin").action(async () => {
647
647
  try {
648
648
  const output = await initCommand({ cwd: process.cwd() });
649
649
  console.log(output);
@@ -704,7 +704,154 @@ var claudeDomain = {
704
704
 
705
705
  // src/commands/session/archive.ts
706
706
  import { mkdir, rename, stat } from "fs/promises";
707
- import { dirname, join as join2 } from "path";
707
+ import { dirname as dirname2, join as join2 } from "path";
708
+
709
+ // src/git/root.ts
710
+ import { execa as execa2 } from "execa";
711
+ import { dirname, isAbsolute, join, resolve } from "path";
712
+
713
+ // src/config/defaults.ts
714
+ var DEFAULT_CONFIG = {
715
+ specs: {
716
+ root: "specs",
717
+ work: {
718
+ dir: "work",
719
+ statusDirs: {
720
+ doing: "doing",
721
+ backlog: "backlog",
722
+ done: "archive"
723
+ }
724
+ },
725
+ decisions: "decisions",
726
+ templates: "templates"
727
+ },
728
+ sessions: {
729
+ dir: ".spx/sessions",
730
+ statusDirs: {
731
+ todo: "todo",
732
+ doing: "doing",
733
+ archive: "archive"
734
+ }
735
+ }
736
+ };
737
+
738
+ // src/git/root.ts
739
+ var defaultDeps = {
740
+ execa: async (command, args, options) => {
741
+ const result = await execa2(command, args, options);
742
+ return {
743
+ exitCode: result.exitCode ?? 0,
744
+ stdout: typeof result.stdout === "string" ? result.stdout : String(result.stdout),
745
+ stderr: typeof result.stderr === "string" ? result.stderr : String(result.stderr)
746
+ };
747
+ }
748
+ };
749
+ var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository. Sessions will be created relative to current directory.";
750
+ function extractStdout(stdout) {
751
+ if (!stdout) return "";
752
+ const str = typeof stdout === "string" ? stdout : String(stdout);
753
+ return str.trim().replace(/\/+$/, "");
754
+ }
755
+ async function detectMainRepoRoot(cwd = process.cwd(), deps = defaultDeps) {
756
+ try {
757
+ const toplevelResult = await deps.execa(
758
+ "git",
759
+ ["rev-parse", "--show-toplevel"],
760
+ { cwd, reject: false }
761
+ );
762
+ if (toplevelResult.exitCode !== 0 || !toplevelResult.stdout) {
763
+ return {
764
+ root: cwd,
765
+ isGitRepo: false,
766
+ warning: NOT_GIT_REPO_WARNING
767
+ };
768
+ }
769
+ const toplevel = extractStdout(toplevelResult.stdout);
770
+ const commonDirResult = await deps.execa(
771
+ "git",
772
+ ["rev-parse", "--git-common-dir"],
773
+ { cwd, reject: false }
774
+ );
775
+ if (commonDirResult.exitCode !== 0 || !commonDirResult.stdout) {
776
+ return {
777
+ root: toplevel,
778
+ isGitRepo: true
779
+ };
780
+ }
781
+ const commonDir = extractStdout(commonDirResult.stdout);
782
+ const absoluteCommonDir = isAbsolute(commonDir) ? commonDir : resolve(toplevel, commonDir);
783
+ const mainRepoRoot = dirname(absoluteCommonDir);
784
+ return {
785
+ root: mainRepoRoot,
786
+ isGitRepo: true
787
+ };
788
+ } catch {
789
+ return {
790
+ root: cwd,
791
+ isGitRepo: false,
792
+ warning: NOT_GIT_REPO_WARNING
793
+ };
794
+ }
795
+ }
796
+ async function resolveSessionConfig(options = {}) {
797
+ const { sessionsDir, cwd, deps } = options;
798
+ const { statusDirs: statusDirs2 } = DEFAULT_CONFIG.sessions;
799
+ if (sessionsDir) {
800
+ return {
801
+ config: {
802
+ todoDir: join(sessionsDir, statusDirs2.todo),
803
+ doingDir: join(sessionsDir, statusDirs2.doing),
804
+ archiveDir: join(sessionsDir, statusDirs2.archive)
805
+ }
806
+ };
807
+ }
808
+ const gitResult = await detectMainRepoRoot(cwd, deps);
809
+ const baseDir = join(gitResult.root, DEFAULT_CONFIG.sessions.dir);
810
+ return {
811
+ config: {
812
+ todoDir: join(baseDir, statusDirs2.todo),
813
+ doingDir: join(baseDir, statusDirs2.doing),
814
+ archiveDir: join(baseDir, statusDirs2.archive)
815
+ },
816
+ warning: gitResult.warning
817
+ };
818
+ }
819
+
820
+ // src/session/batch.ts
821
+ var BatchError = class extends Error {
822
+ results;
823
+ constructor(results) {
824
+ const failures = results.filter((r) => !r.ok);
825
+ const successes = results.filter((r) => r.ok);
826
+ super(
827
+ `${failures.length} of ${results.length} operations failed. ${successes.length} succeeded.`
828
+ );
829
+ this.name = "BatchError";
830
+ this.results = results;
831
+ }
832
+ };
833
+ async function processBatch(ids, handler) {
834
+ const results = [];
835
+ for (const id of ids) {
836
+ try {
837
+ const output2 = await handler(id);
838
+ results.push({ id, ok: true, message: output2 });
839
+ } catch (error) {
840
+ const message = error instanceof Error ? error.message : String(error);
841
+ results.push({ id, ok: false, message });
842
+ }
843
+ }
844
+ const output = results.map((r) => r.ok ? r.message : `Error (${r.id}): ${r.message}`).join("\n\n");
845
+ const hasFailures = results.some((r) => !r.ok);
846
+ if (hasFailures) {
847
+ const err = new BatchError(results);
848
+ err.message = `${err.message}
849
+
850
+ ${output}`;
851
+ throw err;
852
+ }
853
+ return output;
854
+ }
708
855
 
709
856
  // src/session/errors.ts
710
857
  var SessionError = class extends Error {
@@ -753,33 +900,76 @@ var NoSessionsAvailableError = class extends SessionError {
753
900
  }
754
901
  };
755
902
 
756
- // src/session/show.ts
757
- import { join } from "path";
758
-
759
- // src/config/defaults.ts
760
- var DEFAULT_CONFIG = {
761
- specs: {
762
- root: "specs",
763
- work: {
764
- dir: "work",
765
- statusDirs: {
766
- doing: "doing",
767
- backlog: "backlog",
768
- done: "archive"
769
- }
770
- },
771
- decisions: "decisions",
772
- templates: "templates"
773
- },
774
- sessions: {
775
- dir: ".spx/sessions",
776
- statusDirs: {
777
- todo: "todo",
778
- doing: "doing",
779
- archive: "archive"
780
- }
903
+ // src/commands/session/archive.ts
904
+ var SessionAlreadyArchivedError = class extends Error {
905
+ /** The session ID that is already archived */
906
+ sessionId;
907
+ constructor(sessionId) {
908
+ super(`Session already archived: ${sessionId}.`);
909
+ this.name = "SessionAlreadyArchivedError";
910
+ this.sessionId = sessionId;
781
911
  }
782
912
  };
913
+ async function resolveArchivePaths(sessionId, config) {
914
+ const filename = `${sessionId}.md`;
915
+ const todoPath = join2(config.todoDir, filename);
916
+ const doingPath = join2(config.doingDir, filename);
917
+ const archivePath = join2(config.archiveDir, filename);
918
+ try {
919
+ const archiveStats = await stat(archivePath);
920
+ if (archiveStats.isFile()) {
921
+ throw new SessionAlreadyArchivedError(sessionId);
922
+ }
923
+ } catch (error) {
924
+ if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
925
+ throw error;
926
+ }
927
+ if (error instanceof SessionAlreadyArchivedError) {
928
+ throw error;
929
+ }
930
+ }
931
+ try {
932
+ const todoStats = await stat(todoPath);
933
+ if (todoStats.isFile()) {
934
+ return { source: todoPath, target: archivePath };
935
+ }
936
+ } catch {
937
+ }
938
+ try {
939
+ const doingStats = await stat(doingPath);
940
+ if (doingStats.isFile()) {
941
+ return { source: doingPath, target: archivePath };
942
+ }
943
+ } catch {
944
+ }
945
+ throw new SessionNotFoundError(sessionId);
946
+ }
947
+ async function archiveSingle(sessionId, config) {
948
+ const { source, target } = await resolveArchivePaths(sessionId, config);
949
+ await mkdir(dirname2(target), { recursive: true });
950
+ await rename(source, target);
951
+ return `Archived session: ${sessionId}
952
+ Archive location: ${target}`;
953
+ }
954
+ async function archiveCommand(options) {
955
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
956
+ return processBatch(options.sessionIds, (id) => archiveSingle(id, config));
957
+ }
958
+
959
+ // src/commands/session/delete.ts
960
+ import { stat as stat2, unlink } from "fs/promises";
961
+
962
+ // src/session/delete.ts
963
+ function resolveDeletePath(sessionId, existingPaths) {
964
+ const matchingPath = existingPaths.find((path8) => path8.includes(sessionId));
965
+ if (!matchingPath) {
966
+ throw new SessionNotFoundError(sessionId);
967
+ }
968
+ return matchingPath;
969
+ }
970
+
971
+ // src/session/show.ts
972
+ import { join as join3 } from "path";
783
973
 
784
974
  // src/session/list.ts
785
975
  import { parse as parseYaml } from "yaml";
@@ -819,6 +1009,8 @@ function parseSessionId(id) {
819
1009
  }
820
1010
 
821
1011
  // src/session/types.ts
1012
+ var SESSION_STATUSES = ["todo", "doing", "archive"];
1013
+ var DEFAULT_LIST_STATUSES = ["doing", "todo"];
822
1014
  var PRIORITY_ORDER = {
823
1015
  high: 0,
824
1016
  medium: 1,
@@ -905,11 +1097,11 @@ function sortSessions(sessions) {
905
1097
  // src/session/show.ts
906
1098
  var { dir: sessionsBaseDir, statusDirs } = DEFAULT_CONFIG.sessions;
907
1099
  var DEFAULT_SESSION_CONFIG = {
908
- todoDir: join(sessionsBaseDir, statusDirs.todo),
909
- doingDir: join(sessionsBaseDir, statusDirs.doing),
910
- archiveDir: join(sessionsBaseDir, statusDirs.archive)
1100
+ todoDir: join3(sessionsBaseDir, statusDirs.todo),
1101
+ doingDir: join3(sessionsBaseDir, statusDirs.doing),
1102
+ archiveDir: join3(sessionsBaseDir, statusDirs.archive)
911
1103
  };
912
- var SEARCH_ORDER = ["todo", "doing", "archive"];
1104
+ var SEARCH_ORDER = [...SESSION_STATUSES];
913
1105
  function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
914
1106
  const filename = `${id}.md`;
915
1107
  return [
@@ -941,76 +1133,6 @@ function formatShowOutput(content, options) {
941
1133
  return header + separator + content;
942
1134
  }
943
1135
 
944
- // src/commands/session/archive.ts
945
- var SessionAlreadyArchivedError = class extends Error {
946
- /** The session ID that is already archived */
947
- sessionId;
948
- constructor(sessionId) {
949
- super(`Session already archived: ${sessionId}.`);
950
- this.name = "SessionAlreadyArchivedError";
951
- this.sessionId = sessionId;
952
- }
953
- };
954
- async function resolveArchivePaths(sessionId, config) {
955
- const filename = `${sessionId}.md`;
956
- const todoPath = join2(config.todoDir, filename);
957
- const doingPath = join2(config.doingDir, filename);
958
- const archivePath = join2(config.archiveDir, filename);
959
- try {
960
- const archiveStats = await stat(archivePath);
961
- if (archiveStats.isFile()) {
962
- throw new SessionAlreadyArchivedError(sessionId);
963
- }
964
- } catch (error) {
965
- if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
966
- throw error;
967
- }
968
- if (error instanceof SessionAlreadyArchivedError) {
969
- throw error;
970
- }
971
- }
972
- try {
973
- const todoStats = await stat(todoPath);
974
- if (todoStats.isFile()) {
975
- return { source: todoPath, target: archivePath };
976
- }
977
- } catch {
978
- }
979
- try {
980
- const doingStats = await stat(doingPath);
981
- if (doingStats.isFile()) {
982
- return { source: doingPath, target: archivePath };
983
- }
984
- } catch {
985
- }
986
- throw new SessionNotFoundError(sessionId);
987
- }
988
- async function archiveCommand(options) {
989
- const config = options.sessionsDir ? {
990
- todoDir: join2(options.sessionsDir, "todo"),
991
- doingDir: join2(options.sessionsDir, "doing"),
992
- archiveDir: join2(options.sessionsDir, "archive")
993
- } : DEFAULT_SESSION_CONFIG;
994
- const { source, target } = await resolveArchivePaths(options.sessionId, config);
995
- await mkdir(dirname(target), { recursive: true });
996
- await rename(source, target);
997
- return `Archived session: ${options.sessionId}
998
- Archive location: ${target}`;
999
- }
1000
-
1001
- // src/commands/session/delete.ts
1002
- import { stat as stat2, unlink } from "fs/promises";
1003
- import { join as join3 } from "path";
1004
-
1005
- // src/session/delete.ts
1006
- function resolveDeletePath(sessionId, existingPaths) {
1007
- const matchingPath = existingPaths.find((path8) => path8.includes(sessionId));
1008
- if (!matchingPath) {
1009
- throw new SessionNotFoundError(sessionId);
1010
- }
1011
- return matchingPath;
1012
- }
1013
-
1014
1136
  // src/commands/session/delete.ts
1015
1137
  async function findExistingPaths(paths) {
1016
1138
  const existing = [];
@@ -1025,62 +1147,21 @@ async function findExistingPaths(paths) {
1025
1147
  }
1026
1148
  return existing;
1027
1149
  }
1028
- async function deleteCommand(options) {
1029
- const config = options.sessionsDir ? {
1030
- todoDir: join3(options.sessionsDir, "todo"),
1031
- doingDir: join3(options.sessionsDir, "doing"),
1032
- archiveDir: join3(options.sessionsDir, "archive")
1033
- } : DEFAULT_SESSION_CONFIG;
1034
- const paths = resolveSessionPaths(options.sessionId, config);
1150
+ async function deleteSingle(sessionId, config) {
1151
+ const paths = resolveSessionPaths(sessionId, config);
1035
1152
  const existingPaths = await findExistingPaths(paths);
1036
- const pathToDelete = resolveDeletePath(options.sessionId, existingPaths);
1153
+ const pathToDelete = resolveDeletePath(sessionId, existingPaths);
1037
1154
  await unlink(pathToDelete);
1038
- return `Deleted session: ${options.sessionId}`;
1155
+ return `Deleted session: ${sessionId}`;
1156
+ }
1157
+ async function deleteCommand(options) {
1158
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1159
+ return processBatch(options.sessionIds, (id) => deleteSingle(id, config));
1039
1160
  }
1040
1161
 
1041
1162
  // src/commands/session/handoff.ts
1042
1163
  import { mkdir as mkdir2, writeFile } from "fs/promises";
1043
- import { join as join5, resolve } from "path";
1044
-
1045
- // src/git/root.ts
1046
- import { execa as execa2 } from "execa";
1047
- import { join as join4 } from "path";
1048
- var defaultDeps = {
1049
- execa: (command, args, options) => execa2(command, args, options)
1050
- };
1051
- var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository. Sessions will be created relative to current directory.";
1052
- async function detectGitRoot(cwd = process.cwd(), deps = defaultDeps) {
1053
- try {
1054
- const result = await deps.execa(
1055
- "git",
1056
- ["rev-parse", "--show-toplevel"],
1057
- { cwd, reject: false }
1058
- );
1059
- if (result.exitCode === 0 && result.stdout) {
1060
- const stdout = typeof result.stdout === "string" ? result.stdout : result.stdout.toString();
1061
- const gitRoot = stdout.trim().replace(/\/+$/, "");
1062
- return {
1063
- root: gitRoot,
1064
- isGitRepo: true
1065
- };
1066
- }
1067
- return {
1068
- root: cwd,
1069
- isGitRepo: false,
1070
- warning: NOT_GIT_REPO_WARNING
1071
- };
1072
- } catch {
1073
- return {
1074
- root: cwd,
1075
- isGitRepo: false,
1076
- warning: NOT_GIT_REPO_WARNING
1077
- };
1078
- }
1079
- }
1080
- function buildSessionPathFromRoot(gitRoot, sessionId, config) {
1081
- const filename = `${sessionId}.md`;
1082
- return join4(gitRoot, config.todoDir, filename);
1083
- }
1164
+ import { join as join4, resolve as resolve2 } from "path";
1084
1165
 
1085
1166
  // src/session/create.ts
1086
1167
  var MIN_CONTENT_LENGTH = 1;
@@ -1101,7 +1182,6 @@ function validateSessionContent(content) {
1101
1182
  }
1102
1183
 
1103
1184
  // src/commands/session/handoff.ts
1104
- var { statusDirs: statusDirs2 } = DEFAULT_CONFIG.sessions;
1105
1185
  var FRONT_MATTER_START = /^---\r?\n/;
1106
1186
  function hasFrontmatter(content) {
1107
1187
  return FRONT_MATTER_START.test(content);
@@ -1126,20 +1206,7 @@ priority: medium
1126
1206
  ${content}`;
1127
1207
  }
1128
1208
  async function handoffCommand(options) {
1129
- const config = options.sessionsDir ? {
1130
- todoDir: join5(options.sessionsDir, statusDirs2.todo),
1131
- doingDir: join5(options.sessionsDir, statusDirs2.doing),
1132
- archiveDir: join5(options.sessionsDir, statusDirs2.archive)
1133
- } : DEFAULT_SESSION_CONFIG;
1134
- let baseDir;
1135
- let warningMessage;
1136
- if (options.sessionsDir) {
1137
- baseDir = options.sessionsDir;
1138
- } else {
1139
- const gitResult = await detectGitRoot();
1140
- baseDir = gitResult.root;
1141
- warningMessage = gitResult.warning;
1142
- }
1209
+ const { config, warning } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1143
1210
  const sessionId = generateSessionId();
1144
1211
  const fullContent = buildSessionContent(options.content);
1145
1212
  const validation = validateSessionContent(fullContent);
@@ -1147,23 +1214,21 @@ async function handoffCommand(options) {
1147
1214
  throw new SessionInvalidContentError(validation.error ?? "Unknown validation error");
1148
1215
  }
1149
1216
  const filename = `${sessionId}.md`;
1150
- const sessionPath = options.sessionsDir ? join5(config.todoDir, filename) : buildSessionPathFromRoot(baseDir, sessionId, config);
1151
- const absolutePath = resolve(sessionPath);
1152
- const todoDir = options.sessionsDir ? config.todoDir : join5(baseDir, config.todoDir);
1153
- await mkdir2(todoDir, { recursive: true });
1217
+ const sessionPath = join4(config.todoDir, filename);
1218
+ const absolutePath = resolve2(sessionPath);
1219
+ await mkdir2(config.todoDir, { recursive: true });
1154
1220
  await writeFile(sessionPath, fullContent, "utf-8");
1155
- let output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
1156
- <SESSION_FILE>${absolutePath}</SESSION_FILE>`;
1157
- if (warningMessage) {
1158
- process.stderr.write(`${warningMessage}
1221
+ if (warning) {
1222
+ process.stderr.write(`${warning}
1159
1223
  `);
1160
1224
  }
1161
- return output;
1225
+ return `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
1226
+ <SESSION_FILE>${absolutePath}</SESSION_FILE>`;
1162
1227
  }
1163
1228
 
1164
1229
  // src/commands/session/list.ts
1165
1230
  import { readdir, readFile } from "fs/promises";
1166
- import { join as join6 } from "path";
1231
+ import { join as join5 } from "path";
1167
1232
  async function loadSessionsFromDir(dir, status) {
1168
1233
  try {
1169
1234
  const files = await readdir(dir);
@@ -1171,7 +1236,7 @@ async function loadSessionsFromDir(dir, status) {
1171
1236
  for (const file of files) {
1172
1237
  if (!file.endsWith(".md")) continue;
1173
1238
  const id = file.replace(".md", "");
1174
- const filePath = join6(dir, file);
1239
+ const filePath = join5(dir, file);
1175
1240
  const content = await readFile(filePath, "utf-8");
1176
1241
  const metadata = parseSessionMetadata(content);
1177
1242
  sessions.push({
@@ -1189,7 +1254,12 @@ async function loadSessionsFromDir(dir, status) {
1189
1254
  throw error;
1190
1255
  }
1191
1256
  }
1192
- function formatTextOutput(sessions, _status) {
1257
+ var STATUS_DIR_KEY = {
1258
+ todo: "todoDir",
1259
+ doing: "doingDir",
1260
+ archive: "archiveDir"
1261
+ };
1262
+ function formatTextOutput(sessions) {
1193
1263
  if (sessions.length === 0) {
1194
1264
  return ` (no sessions)`;
1195
1265
  }
@@ -1199,47 +1269,38 @@ function formatTextOutput(sessions, _status) {
1199
1269
  return ` ${s.id}${priority}${tags}`;
1200
1270
  }).join("\n");
1201
1271
  }
1272
+ function validateStatus(input) {
1273
+ if (SESSION_STATUSES.includes(input)) {
1274
+ return input;
1275
+ }
1276
+ throw new Error(
1277
+ `Invalid status: "${input}". Valid values: ${SESSION_STATUSES.join(", ")}`
1278
+ );
1279
+ }
1202
1280
  async function listCommand(options) {
1203
- const config = options.sessionsDir ? {
1204
- todoDir: join6(options.sessionsDir, "todo"),
1205
- doingDir: join6(options.sessionsDir, "doing"),
1206
- archiveDir: join6(options.sessionsDir, "archive")
1207
- } : DEFAULT_SESSION_CONFIG;
1208
- const statuses = options.status ? [options.status] : ["todo", "doing", "archive"];
1209
- const allSessions = {
1210
- todo: [],
1211
- doing: [],
1212
- archive: []
1213
- };
1281
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1282
+ const statuses = options.status !== void 0 ? [validateStatus(options.status)] : DEFAULT_LIST_STATUSES;
1283
+ const sessionsByStatus = {};
1214
1284
  for (const status of statuses) {
1215
- const dir = status === "todo" ? config.todoDir : status === "doing" ? config.doingDir : config.archiveDir;
1216
- const sessions = await loadSessionsFromDir(dir, status);
1217
- allSessions[status] = sortSessions(sessions);
1285
+ const dirKey = STATUS_DIR_KEY[status];
1286
+ const sessions = await loadSessionsFromDir(config[dirKey], status);
1287
+ sessionsByStatus[status] = sortSessions(sessions);
1218
1288
  }
1219
1289
  if (options.format === "json") {
1220
- return JSON.stringify(allSessions, null, 2);
1290
+ return JSON.stringify(sessionsByStatus, null, 2);
1221
1291
  }
1222
1292
  const lines = [];
1223
- if (statuses.includes("doing")) {
1224
- lines.push("DOING:");
1225
- lines.push(formatTextOutput(allSessions.doing, "doing"));
1226
- lines.push("");
1227
- }
1228
- if (statuses.includes("todo")) {
1229
- lines.push("TODO:");
1230
- lines.push(formatTextOutput(allSessions.todo, "todo"));
1293
+ for (const status of statuses) {
1294
+ lines.push(`${status.toUpperCase()}:`);
1295
+ lines.push(formatTextOutput(sessionsByStatus[status] ?? []));
1231
1296
  lines.push("");
1232
1297
  }
1233
- if (statuses.includes("archive")) {
1234
- lines.push("ARCHIVE:");
1235
- lines.push(formatTextOutput(allSessions.archive, "archive"));
1236
- }
1237
1298
  return lines.join("\n").trim();
1238
1299
  }
1239
1300
 
1240
1301
  // src/commands/session/pickup.ts
1241
1302
  import { mkdir as mkdir3, readdir as readdir2, readFile as readFile2, rename as rename2 } from "fs/promises";
1242
- import { join as join7 } from "path";
1303
+ import { join as join6 } from "path";
1243
1304
 
1244
1305
  // src/session/pickup.ts
1245
1306
  function buildClaimPaths(sessionId, config) {
@@ -1275,6 +1336,8 @@ function selectBestSession(sessions) {
1275
1336
  }
1276
1337
 
1277
1338
  // src/commands/session/pickup.ts
1339
+ var PICKUP_SOURCE_STATUS = SESSION_STATUSES[0];
1340
+ var PICKUP_TARGET_STATUS = SESSION_STATUSES[1];
1278
1341
  async function loadTodoSessions(config) {
1279
1342
  try {
1280
1343
  const files = await readdir2(config.todoDir);
@@ -1282,12 +1345,12 @@ async function loadTodoSessions(config) {
1282
1345
  for (const file of files) {
1283
1346
  if (!file.endsWith(".md")) continue;
1284
1347
  const id = file.replace(".md", "");
1285
- const filePath = join7(config.todoDir, file);
1348
+ const filePath = join6(config.todoDir, file);
1286
1349
  const content = await readFile2(filePath, "utf-8");
1287
1350
  const metadata = parseSessionMetadata(content);
1288
1351
  sessions.push({
1289
1352
  id,
1290
- status: "todo",
1353
+ status: PICKUP_SOURCE_STATUS,
1291
1354
  path: filePath,
1292
1355
  metadata
1293
1356
  });
@@ -1301,11 +1364,7 @@ async function loadTodoSessions(config) {
1301
1364
  }
1302
1365
  }
1303
1366
  async function pickupCommand(options) {
1304
- const config = options.sessionsDir ? {
1305
- todoDir: join7(options.sessionsDir, "todo"),
1306
- doingDir: join7(options.sessionsDir, "doing"),
1307
- archiveDir: join7(options.sessionsDir, "archive")
1308
- } : DEFAULT_SESSION_CONFIG;
1367
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1309
1368
  let sessionId;
1310
1369
  if (options.auto) {
1311
1370
  const sessions = await loadTodoSessions(config);
@@ -1327,7 +1386,7 @@ async function pickupCommand(options) {
1327
1386
  throw classifyClaimError(error, sessionId);
1328
1387
  }
1329
1388
  const content = await readFile2(paths.target, "utf-8");
1330
- const output = formatShowOutput(content, { status: "doing" });
1389
+ const output = formatShowOutput(content, { status: PICKUP_TARGET_STATUS });
1331
1390
  return `Claimed session <PICKUP_ID>${sessionId}</PICKUP_ID>
1332
1391
 
1333
1392
  ${output}`;
@@ -1335,7 +1394,8 @@ ${output}`;
1335
1394
 
1336
1395
  // src/commands/session/prune.ts
1337
1396
  import { readdir as readdir3, readFile as readFile3, unlink as unlink2 } from "fs/promises";
1338
- import { join as join8 } from "path";
1397
+ import { join as join7 } from "path";
1398
+ var PRUNE_STATUS = SESSION_STATUSES[2];
1339
1399
  var DEFAULT_KEEP_COUNT = 5;
1340
1400
  var PruneValidationError = class extends Error {
1341
1401
  constructor(message) {
@@ -1359,12 +1419,12 @@ async function loadArchiveSessions(config) {
1359
1419
  for (const file of files) {
1360
1420
  if (!file.endsWith(".md")) continue;
1361
1421
  const id = file.replace(".md", "");
1362
- const filePath = join8(config.archiveDir, file);
1422
+ const filePath = join7(config.archiveDir, file);
1363
1423
  const content = await readFile3(filePath, "utf-8");
1364
1424
  const metadata = parseSessionMetadata(content);
1365
1425
  sessions.push({
1366
1426
  id,
1367
- status: "archive",
1427
+ status: PRUNE_STATUS,
1368
1428
  path: filePath,
1369
1429
  metadata
1370
1430
  });
@@ -1388,11 +1448,7 @@ async function pruneCommand(options) {
1388
1448
  validatePruneOptions(options);
1389
1449
  const keep = options.keep ?? DEFAULT_KEEP_COUNT;
1390
1450
  const dryRun = options.dryRun ?? false;
1391
- const config = options.sessionsDir ? {
1392
- todoDir: join8(options.sessionsDir, "todo"),
1393
- doingDir: join8(options.sessionsDir, "doing"),
1394
- archiveDir: join8(options.sessionsDir, "archive")
1395
- } : DEFAULT_SESSION_CONFIG;
1451
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1396
1452
  const sessions = await loadArchiveSessions(config);
1397
1453
  const toPrune = selectSessionsToPrune(sessions, keep);
1398
1454
  if (toPrune.length === 0) {
@@ -1421,7 +1477,6 @@ async function pruneCommand(options) {
1421
1477
 
1422
1478
  // src/commands/session/release.ts
1423
1479
  import { readdir as readdir4, rename as rename3 } from "fs/promises";
1424
- import { join as join9 } from "path";
1425
1480
 
1426
1481
  // src/session/release.ts
1427
1482
  function buildReleasePaths(sessionId, config) {
@@ -1458,11 +1513,7 @@ async function loadDoingSessions(config) {
1458
1513
  }
1459
1514
  }
1460
1515
  async function releaseCommand(options) {
1461
- const config = options.sessionsDir ? {
1462
- todoDir: join9(options.sessionsDir, "todo"),
1463
- doingDir: join9(options.sessionsDir, "doing"),
1464
- archiveDir: join9(options.sessionsDir, "archive")
1465
- } : DEFAULT_SESSION_CONFIG;
1516
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1466
1517
  let sessionId;
1467
1518
  if (options.sessionId) {
1468
1519
  sessionId = options.sessionId;
@@ -1489,7 +1540,6 @@ Session returned to todo directory.`;
1489
1540
 
1490
1541
  // src/commands/session/show.ts
1491
1542
  import { readFile as readFile4, stat as stat3 } from "fs/promises";
1492
- import { join as join10 } from "path";
1493
1543
  async function findExistingPath(paths, _config) {
1494
1544
  for (let i = 0; i < paths.length; i++) {
1495
1545
  const filePath = paths[i];
@@ -1503,20 +1553,19 @@ async function findExistingPath(paths, _config) {
1503
1553
  }
1504
1554
  return null;
1505
1555
  }
1506
- async function showCommand(options) {
1507
- const config = options.sessionsDir ? {
1508
- todoDir: join10(options.sessionsDir, "todo"),
1509
- doingDir: join10(options.sessionsDir, "doing"),
1510
- archiveDir: join10(options.sessionsDir, "archive")
1511
- } : DEFAULT_SESSION_CONFIG;
1512
- const paths = resolveSessionPaths(options.sessionId, config);
1556
+ async function showSingle(sessionId, config) {
1557
+ const paths = resolveSessionPaths(sessionId, config);
1513
1558
  const found = await findExistingPath(paths, config);
1514
1559
  if (!found) {
1515
- throw new SessionNotFoundError(options.sessionId);
1560
+ throw new SessionNotFoundError(sessionId);
1516
1561
  }
1517
1562
  const content = await readFile4(found.path, "utf-8");
1518
1563
  return formatShowOutput(content, { status: found.status });
1519
1564
  }
1565
+ async function showCommand(options) {
1566
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1567
+ return processBatch(options.sessionIds, (id) => showSingle(id, config));
1568
+ }
1520
1569
 
1521
1570
  // src/domains/session/help.ts
1522
1571
  var SESSION_FORMAT_HELP = `
@@ -1535,7 +1584,8 @@ Workflow:
1535
1584
  1. handoff - Create session (todo)
1536
1585
  2. pickup - Claim session (todo -> doing)
1537
1586
  3. release - Return session (doing -> todo)
1538
- 4. delete - Remove session
1587
+ 4. archive - Move session to archive
1588
+ 5. delete - Remove session permanently
1539
1589
  `;
1540
1590
  var HANDOFF_FRONTMATTER_HELP = `
1541
1591
  Usage:
@@ -1580,17 +1630,17 @@ async function readStdin() {
1580
1630
  if (process.stdin.isTTY) {
1581
1631
  return void 0;
1582
1632
  }
1583
- return new Promise((resolve2) => {
1633
+ return new Promise((resolve3) => {
1584
1634
  let data = "";
1585
1635
  process.stdin.setEncoding("utf-8");
1586
1636
  process.stdin.on("data", (chunk) => {
1587
1637
  data += chunk;
1588
1638
  });
1589
1639
  process.stdin.on("end", () => {
1590
- resolve2(data.trim() || void 0);
1640
+ resolve3(data.trim() || void 0);
1591
1641
  });
1592
1642
  process.stdin.on("error", () => {
1593
- resolve2(void 0);
1643
+ resolve3(void 0);
1594
1644
  });
1595
1645
  });
1596
1646
  }
@@ -1599,7 +1649,7 @@ function handleError(error) {
1599
1649
  process.exit(1);
1600
1650
  }
1601
1651
  function registerSessionCommands(sessionCmd) {
1602
- sessionCmd.command("list").description("List all sessions").option("--status <status>", "Filter by status (todo|doing|archive)").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
1652
+ sessionCmd.command("list").description("List active sessions (doing + todo by default)").option("--status <status>", "Filter by status (todo|doing|archive); defaults to doing + todo").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
1603
1653
  try {
1604
1654
  const output = await listCommand({
1605
1655
  status: options.status,
@@ -1611,10 +1661,22 @@ function registerSessionCommands(sessionCmd) {
1611
1661
  handleError(error);
1612
1662
  }
1613
1663
  });
1614
- sessionCmd.command("show <id>").description("Show session content").option("--sessions-dir <path>", "Custom sessions directory").action(async (id, options) => {
1664
+ sessionCmd.command("todo").description("List todo sessions").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
1665
+ try {
1666
+ const output = await listCommand({
1667
+ status: SESSION_STATUSES[0],
1668
+ format: options.json ? "json" : "text",
1669
+ sessionsDir: options.sessionsDir
1670
+ });
1671
+ console.log(output);
1672
+ } catch (error) {
1673
+ handleError(error);
1674
+ }
1675
+ });
1676
+ sessionCmd.command("show <id...>").description("Show session content").option("--sessions-dir <path>", "Custom sessions directory").action(async (ids, options) => {
1615
1677
  try {
1616
1678
  const output = await showCommand({
1617
- sessionId: id,
1679
+ sessionIds: ids,
1618
1680
  sessionsDir: options.sessionsDir
1619
1681
  });
1620
1682
  console.log(output);
@@ -1661,10 +1723,10 @@ function registerSessionCommands(sessionCmd) {
1661
1723
  handleError(error);
1662
1724
  }
1663
1725
  });
1664
- sessionCmd.command("delete <id>").description("Delete a session").option("--sessions-dir <path>", "Custom sessions directory").action(async (id, options) => {
1726
+ sessionCmd.command("delete <id...>").description("Delete one or more sessions").option("--sessions-dir <path>", "Custom sessions directory").action(async (ids, options) => {
1665
1727
  try {
1666
1728
  const output = await deleteCommand({
1667
- sessionId: id,
1729
+ sessionIds: ids,
1668
1730
  sessionsDir: options.sessionsDir
1669
1731
  });
1670
1732
  console.log(output);
@@ -1689,10 +1751,10 @@ function registerSessionCommands(sessionCmd) {
1689
1751
  handleError(error);
1690
1752
  }
1691
1753
  });
1692
- sessionCmd.command("archive <id>").description("Move a session to the archive directory").option("--sessions-dir <path>", "Custom sessions directory").action(async (id, options) => {
1754
+ sessionCmd.command("archive <id...>").description("Move one or more sessions to the archive directory").option("--sessions-dir <path>", "Custom sessions directory").action(async (ids, options) => {
1693
1755
  try {
1694
1756
  const output = await archiveCommand({
1695
- sessionId: id,
1757
+ sessionIds: ids,
1696
1758
  sessionsDir: options.sessionsDir
1697
1759
  });
1698
1760
  console.log(output);
@@ -3182,7 +3244,7 @@ var ParseErrorCode;
3182
3244
 
3183
3245
  // src/validation/config/scope.ts
3184
3246
  import { existsSync, readdirSync, readFileSync } from "fs";
3185
- import { join as join11 } from "path";
3247
+ import { join as join8 } from "path";
3186
3248
  var TSCONFIG_FILES = {
3187
3249
  full: "tsconfig.json",
3188
3250
  production: "tsconfig.production.json"
@@ -3229,7 +3291,7 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
3229
3291
  if (hasDirectTsFiles) return true;
3230
3292
  const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
3231
3293
  for (const subdir of subdirs.slice(0, 5)) {
3232
- if (hasTypeScriptFilesRecursive(join11(dirPath, subdir.name), maxDepth - 1, deps)) {
3294
+ if (hasTypeScriptFilesRecursive(join8(dirPath, subdir.name), maxDepth - 1, deps)) {
3233
3295
  return true;
3234
3296
  }
3235
3297
  }
@@ -3572,7 +3634,7 @@ function buildEslintArgs(context) {
3572
3634
  }
3573
3635
  async function validateESLint(context, runner = defaultEslintProcessRunner) {
3574
3636
  const { scope, validatedFiles, mode } = context;
3575
- return new Promise((resolve2) => {
3637
+ return new Promise((resolve3) => {
3576
3638
  if (!validatedFiles || validatedFiles.length === 0) {
3577
3639
  if (scope === VALIDATION_SCOPES.PRODUCTION) {
3578
3640
  process.env.ESLINT_PRODUCTION_ONLY = "1";
@@ -3591,13 +3653,13 @@ async function validateESLint(context, runner = defaultEslintProcessRunner) {
3591
3653
  });
3592
3654
  eslintProcess.on("close", (code) => {
3593
3655
  if (code === 0) {
3594
- resolve2({ success: true });
3656
+ resolve3({ success: true });
3595
3657
  } else {
3596
- resolve2({ success: false, error: `ESLint exited with code ${code}` });
3658
+ resolve3({ success: false, error: `ESLint exited with code ${code}` });
3597
3659
  }
3598
3660
  });
3599
3661
  eslintProcess.on("error", (error) => {
3600
- resolve2({ success: false, error: error.message });
3662
+ resolve3({ success: false, error: error.message });
3601
3663
  });
3602
3664
  });
3603
3665
  }
@@ -3644,7 +3706,7 @@ async function validateKnip(typescriptScope, runner = defaultKnipProcessRunner)
3644
3706
  if (analyzeDirectories.length === 0) {
3645
3707
  return { success: true };
3646
3708
  }
3647
- return new Promise((resolve2) => {
3709
+ return new Promise((resolve3) => {
3648
3710
  const knipProcess = runner.spawn("npx", ["knip"], {
3649
3711
  cwd: process.cwd(),
3650
3712
  stdio: "pipe"
@@ -3659,17 +3721,17 @@ async function validateKnip(typescriptScope, runner = defaultKnipProcessRunner)
3659
3721
  });
3660
3722
  knipProcess.on("close", (code) => {
3661
3723
  if (code === 0) {
3662
- resolve2({ success: true });
3724
+ resolve3({ success: true });
3663
3725
  } else {
3664
3726
  const errorOutput = knipOutput || knipError || "Unused code detected";
3665
- resolve2({
3727
+ resolve3({
3666
3728
  success: false,
3667
3729
  error: errorOutput
3668
3730
  });
3669
3731
  }
3670
3732
  });
3671
3733
  knipProcess.on("error", (error) => {
3672
- resolve2({ success: false, error: error.message });
3734
+ resolve3({ success: false, error: error.message });
3673
3735
  });
3674
3736
  });
3675
3737
  } catch (error) {
@@ -3761,7 +3823,7 @@ import { spawn as spawn3 } from "child_process";
3761
3823
  import { existsSync as existsSync2, mkdirSync, rmSync, writeFileSync } from "fs";
3762
3824
  import { mkdtemp } from "fs/promises";
3763
3825
  import { tmpdir } from "os";
3764
- import { isAbsolute, join as join12 } from "path";
3826
+ import { isAbsolute as isAbsolute2, join as join9 } from "path";
3765
3827
  var defaultTypeScriptProcessRunner = { spawn: spawn3 };
3766
3828
  var defaultTypeScriptDeps = {
3767
3829
  mkdtemp,
@@ -3775,19 +3837,19 @@ function buildTypeScriptArgs(context) {
3775
3837
  return scope === VALIDATION_SCOPES.FULL ? ["tsc", "--noEmit"] : ["tsc", "--project", configFile];
3776
3838
  }
3777
3839
  async function createFileSpecificTsconfig(scope, files, deps = defaultTypeScriptDeps) {
3778
- const tempDir = await deps.mkdtemp(join12(tmpdir(), "validate-ts-"));
3779
- const configPath = join12(tempDir, "tsconfig.json");
3840
+ const tempDir = await deps.mkdtemp(join9(tmpdir(), "validate-ts-"));
3841
+ const configPath = join9(tempDir, "tsconfig.json");
3780
3842
  const baseConfigFile = TSCONFIG_FILES[scope];
3781
3843
  const projectRoot = process.cwd();
3782
- const absoluteFiles = files.map((file) => isAbsolute(file) ? file : join12(projectRoot, file));
3844
+ const absoluteFiles = files.map((file) => isAbsolute2(file) ? file : join9(projectRoot, file));
3783
3845
  const tempConfig = {
3784
- extends: join12(projectRoot, baseConfigFile),
3846
+ extends: join9(projectRoot, baseConfigFile),
3785
3847
  files: absoluteFiles,
3786
3848
  include: [],
3787
3849
  exclude: [],
3788
3850
  compilerOptions: {
3789
3851
  noEmit: true,
3790
- typeRoots: [join12(projectRoot, "node_modules", "@types")],
3852
+ typeRoots: [join9(projectRoot, "node_modules", "@types")],
3791
3853
  types: ["node"]
3792
3854
  }
3793
3855
  };
@@ -3807,7 +3869,7 @@ async function validateTypeScript(scope, typescriptScope, files, runner = defaul
3807
3869
  if (files && files.length > 0) {
3808
3870
  const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, deps);
3809
3871
  try {
3810
- return await new Promise((resolve2) => {
3872
+ return await new Promise((resolve3) => {
3811
3873
  const tscProcess = runner.spawn("npx", ["tsc", "--project", configPath], {
3812
3874
  cwd: process.cwd(),
3813
3875
  stdio: "inherit"
@@ -3815,14 +3877,14 @@ async function validateTypeScript(scope, typescriptScope, files, runner = defaul
3815
3877
  tscProcess.on("close", (code) => {
3816
3878
  cleanup();
3817
3879
  if (code === 0) {
3818
- resolve2({ success: true, skipped: false });
3880
+ resolve3({ success: true, skipped: false });
3819
3881
  } else {
3820
- resolve2({ success: false, error: `TypeScript exited with code ${code}` });
3882
+ resolve3({ success: false, error: `TypeScript exited with code ${code}` });
3821
3883
  }
3822
3884
  });
3823
3885
  tscProcess.on("error", (error) => {
3824
3886
  cleanup();
3825
- resolve2({ success: false, error: error.message });
3887
+ resolve3({ success: false, error: error.message });
3826
3888
  });
3827
3889
  });
3828
3890
  } catch (error) {
@@ -3834,20 +3896,20 @@ async function validateTypeScript(scope, typescriptScope, files, runner = defaul
3834
3896
  tool = "npx";
3835
3897
  tscArgs = buildTypeScriptArgs({ scope, configFile });
3836
3898
  }
3837
- return new Promise((resolve2) => {
3899
+ return new Promise((resolve3) => {
3838
3900
  const tscProcess = runner.spawn(tool, tscArgs, {
3839
3901
  cwd: process.cwd(),
3840
3902
  stdio: "inherit"
3841
3903
  });
3842
3904
  tscProcess.on("close", (code) => {
3843
3905
  if (code === 0) {
3844
- resolve2({ success: true, skipped: false });
3906
+ resolve3({ success: true, skipped: false });
3845
3907
  } else {
3846
- resolve2({ success: false, error: `TypeScript exited with code ${code}` });
3908
+ resolve3({ success: false, error: `TypeScript exited with code ${code}` });
3847
3909
  }
3848
3910
  });
3849
3911
  tscProcess.on("error", (error) => {
3850
- resolve2({ success: false, error: error.message });
3912
+ resolve3({ success: false, error: error.message });
3851
3913
  });
3852
3914
  });
3853
3915
  }