@vladimir-ks/aigile 0.2.3 → 0.2.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.
@@ -407,16 +407,26 @@ var init_config = __esm({
407
407
  // src/db/connection.ts
408
408
  var connection_exports = {};
409
409
  __export(connection_exports, {
410
+ assignFilesToChunk: () => assignFilesToChunk,
410
411
  closeDatabase: () => closeDatabase,
412
+ createChunk: () => createChunk,
413
+ flagFileQualityIssue: () => flagFileQualityIssue,
411
414
  generateId: () => generateId,
415
+ getChunk: () => getChunk,
416
+ getCoverageStats: () => getCoverageStats,
412
417
  getDatabase: () => getDatabase,
418
+ getFilesWithQualityIssues: () => getFilesWithQualityIssues,
413
419
  getNextKey: () => getNextKey,
420
+ getSessionChunks: () => getSessionChunks,
421
+ getSessionFiles: () => getSessionFiles,
422
+ getUntaggedFiles: () => getUntaggedFiles,
414
423
  initDatabase: () => initDatabase,
415
424
  queryAll: () => queryAll,
416
425
  queryOne: () => queryOne,
417
426
  run: () => run,
418
427
  runMigrations: () => runMigrations,
419
- saveDatabase: () => saveDatabase
428
+ saveDatabase: () => saveDatabase,
429
+ tagFileReviewed: () => tagFileReviewed
420
430
  });
421
431
  import initSqlJs from "sql.js";
422
432
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
@@ -748,6 +758,7 @@ function initializeSchema(database) {
748
758
  CREATE TABLE IF NOT EXISTS sessions (
749
759
  id TEXT PRIMARY KEY,
750
760
  project_id TEXT REFERENCES projects(id),
761
+ name TEXT,
751
762
  started_at TEXT DEFAULT (datetime('now')),
752
763
  ended_at TEXT,
753
764
  summary TEXT,
@@ -789,6 +800,39 @@ function initializeSchema(database) {
789
800
  updated_at TEXT DEFAULT (datetime('now'))
790
801
  )
791
802
  `);
803
+ database.run(`
804
+ CREATE TABLE IF NOT EXISTS chunks (
805
+ id TEXT PRIMARY KEY,
806
+ session_id TEXT REFERENCES sessions(id),
807
+ name TEXT NOT NULL,
808
+ patterns TEXT,
809
+ assigned_files TEXT,
810
+ review_mode TEXT DEFAULT 'standard',
811
+ created_at TEXT DEFAULT (datetime('now')),
812
+ updated_at TEXT DEFAULT (datetime('now'))
813
+ )
814
+ `);
815
+ database.run(`
816
+ CREATE TABLE IF NOT EXISTS session_files (
817
+ id TEXT PRIMARY KEY,
818
+ session_id TEXT NOT NULL REFERENCES sessions(id),
819
+ document_id TEXT NOT NULL REFERENCES documents(id),
820
+ chunk_id TEXT REFERENCES chunks(id),
821
+ agent_id TEXT,
822
+ report_path TEXT,
823
+ reviewed_at TEXT NOT NULL DEFAULT (datetime('now')),
824
+ review_type TEXT DEFAULT 'assigned',
825
+ is_foundational INTEGER DEFAULT 0,
826
+ quality_issues TEXT,
827
+ created_at TEXT DEFAULT (datetime('now')),
828
+ UNIQUE(session_id, document_id)
829
+ )
830
+ `);
831
+ database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_session ON session_files(session_id)`);
832
+ database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_document ON session_files(document_id)`);
833
+ database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_chunk ON session_files(chunk_id)`);
834
+ database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_report ON session_files(report_path)`);
835
+ database.run(`CREATE INDEX IF NOT EXISTS idx_chunks_session ON chunks(session_id)`);
792
836
  }
793
837
  function generateId() {
794
838
  return randomUUID();
@@ -812,6 +856,214 @@ function getNextKey(projectKey) {
812
856
  }
813
857
  return `${projectKey}-${nextValue}`;
814
858
  }
859
+ function createChunk(sessionId, chunkId, name, patterns, assignedFiles, reviewMode = "standard") {
860
+ run(
861
+ `INSERT INTO chunks (id, session_id, name, patterns, assigned_files, review_mode)
862
+ VALUES (?, ?, ?, ?, ?, ?)`,
863
+ [
864
+ chunkId,
865
+ sessionId,
866
+ name,
867
+ patterns ? JSON.stringify(patterns) : null,
868
+ assignedFiles ? JSON.stringify(assignedFiles) : null,
869
+ reviewMode
870
+ ]
871
+ );
872
+ }
873
+ function getChunk(chunkId) {
874
+ return queryOne("SELECT * FROM chunks WHERE id = ?", [chunkId]);
875
+ }
876
+ function getSessionChunks(sessionId) {
877
+ return queryAll(
878
+ "SELECT * FROM chunks WHERE session_id = ? ORDER BY created_at",
879
+ [sessionId]
880
+ );
881
+ }
882
+ function assignFilesToChunk(chunkId, files) {
883
+ const chunk = getChunk(chunkId);
884
+ if (!chunk) {
885
+ throw new Error(`Chunk "${chunkId}" not found`);
886
+ }
887
+ const existing = chunk.assigned_files ? JSON.parse(chunk.assigned_files) : [];
888
+ const merged = [.../* @__PURE__ */ new Set([...existing, ...files])];
889
+ run(
890
+ `UPDATE chunks SET assigned_files = ?, updated_at = datetime('now') WHERE id = ?`,
891
+ [JSON.stringify(merged), chunkId]
892
+ );
893
+ }
894
+ function tagFileReviewed(sessionId, documentId, options = {}) {
895
+ const existing = queryOne(
896
+ "SELECT id FROM session_files WHERE session_id = ? AND document_id = ?",
897
+ [sessionId, documentId]
898
+ );
899
+ if (existing) {
900
+ run(
901
+ `UPDATE session_files SET
902
+ chunk_id = COALESCE(?, chunk_id),
903
+ agent_id = COALESCE(?, agent_id),
904
+ report_path = COALESCE(?, report_path),
905
+ review_type = ?,
906
+ is_foundational = CASE WHEN ? = 1 THEN 1 ELSE is_foundational END,
907
+ reviewed_at = datetime('now')
908
+ WHERE id = ?`,
909
+ [
910
+ options.chunkId ?? null,
911
+ options.agentId ?? null,
912
+ options.reportPath ?? null,
913
+ options.reviewType ?? "assigned",
914
+ options.isFoundational ? 1 : 0,
915
+ existing.id
916
+ ]
917
+ );
918
+ return existing.id;
919
+ }
920
+ const id = generateId();
921
+ run(
922
+ `INSERT INTO session_files (id, session_id, document_id, chunk_id, agent_id, report_path, review_type, is_foundational)
923
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
924
+ [
925
+ id,
926
+ sessionId,
927
+ documentId,
928
+ options.chunkId ?? null,
929
+ options.agentId ?? null,
930
+ options.reportPath ?? null,
931
+ options.reviewType ?? "assigned",
932
+ options.isFoundational ? 1 : 0
933
+ ]
934
+ );
935
+ return id;
936
+ }
937
+ function flagFileQualityIssue(sessionFileId, issues) {
938
+ const existing = queryOne(
939
+ "SELECT quality_issues FROM session_files WHERE id = ?",
940
+ [sessionFileId]
941
+ );
942
+ const current = existing?.quality_issues ? JSON.parse(existing.quality_issues) : [];
943
+ const merged = [.../* @__PURE__ */ new Set([...current, ...issues])];
944
+ run(
945
+ `UPDATE session_files SET quality_issues = ? WHERE id = ?`,
946
+ [JSON.stringify(merged), sessionFileId]
947
+ );
948
+ }
949
+ function getSessionFiles(sessionId, options = {}) {
950
+ let sql = "SELECT * FROM session_files WHERE session_id = ?";
951
+ const params = [sessionId];
952
+ if (options.chunkId) {
953
+ sql += " AND chunk_id = ?";
954
+ params.push(options.chunkId);
955
+ }
956
+ if (options.reviewType) {
957
+ sql += " AND review_type = ?";
958
+ params.push(options.reviewType);
959
+ }
960
+ if (options.foundationalOnly) {
961
+ sql += " AND is_foundational = 1";
962
+ }
963
+ return queryAll(sql, params);
964
+ }
965
+ function getFilesWithQualityIssues(sessionId) {
966
+ return queryAll(
967
+ `SELECT * FROM session_files
968
+ WHERE session_id = ? AND quality_issues IS NOT NULL AND quality_issues != '[]'`,
969
+ [sessionId]
970
+ );
971
+ }
972
+ function getUntaggedFiles(projectId, sessionId, options = {}) {
973
+ if (options.assignedOnly) {
974
+ let assignedFiles = [];
975
+ if (options.chunkId) {
976
+ const chunk = getChunk(options.chunkId);
977
+ if (chunk?.assigned_files) {
978
+ try {
979
+ assignedFiles = JSON.parse(chunk.assigned_files);
980
+ } catch {
981
+ }
982
+ }
983
+ } else {
984
+ const chunks = getSessionChunks(sessionId);
985
+ for (const chunk of chunks) {
986
+ if (chunk.assigned_files) {
987
+ try {
988
+ const files = JSON.parse(chunk.assigned_files);
989
+ assignedFiles.push(...files);
990
+ } catch {
991
+ }
992
+ }
993
+ }
994
+ assignedFiles = [...new Set(assignedFiles)];
995
+ }
996
+ if (assignedFiles.length === 0) {
997
+ return [];
998
+ }
999
+ return queryAll(
1000
+ `SELECT d.path, d.id as document_id
1001
+ FROM documents d
1002
+ WHERE d.project_id = ?
1003
+ AND d.path IN (${assignedFiles.map(() => "?").join(",")})
1004
+ AND d.id NOT IN (
1005
+ SELECT sf.document_id FROM session_files sf WHERE sf.session_id = ?
1006
+ )`,
1007
+ [projectId, ...assignedFiles, sessionId]
1008
+ );
1009
+ }
1010
+ return queryAll(
1011
+ `SELECT d.path, d.id as document_id
1012
+ FROM documents d
1013
+ WHERE d.project_id = ?
1014
+ AND d.id NOT IN (
1015
+ SELECT sf.document_id FROM session_files sf WHERE sf.session_id = ?
1016
+ )`,
1017
+ [projectId, sessionId]
1018
+ );
1019
+ }
1020
+ function getCoverageStats(sessionId, chunkId) {
1021
+ const baseWhere = chunkId ? "WHERE session_id = ? AND chunk_id = ?" : "WHERE session_id = ?";
1022
+ const params = chunkId ? [sessionId, chunkId] : [sessionId];
1023
+ const assigned = queryOne(
1024
+ `SELECT COUNT(*) as count FROM session_files ${baseWhere} AND review_type = 'assigned'`,
1025
+ params
1026
+ );
1027
+ const explored = queryOne(
1028
+ `SELECT COUNT(*) as count FROM session_files ${baseWhere} AND review_type = 'explored'`,
1029
+ params
1030
+ );
1031
+ const foundational = queryOne(
1032
+ `SELECT COUNT(*) as count FROM session_files ${baseWhere} AND is_foundational = 1`,
1033
+ params
1034
+ );
1035
+ const skipped = queryOne(
1036
+ `SELECT COUNT(*) as count FROM session_files ${baseWhere} AND review_type = 'skipped'`,
1037
+ params
1038
+ );
1039
+ let assignedTotal = 0;
1040
+ if (chunkId) {
1041
+ const chunk = getChunk(chunkId);
1042
+ if (chunk?.assigned_files) {
1043
+ try {
1044
+ assignedTotal = JSON.parse(chunk.assigned_files).length;
1045
+ } catch {
1046
+ assignedTotal = 0;
1047
+ }
1048
+ }
1049
+ } else {
1050
+ const chunks = getSessionChunks(sessionId);
1051
+ for (const chunk of chunks) {
1052
+ if (chunk.assigned_files) {
1053
+ try {
1054
+ assignedTotal += JSON.parse(chunk.assigned_files).length;
1055
+ } catch {
1056
+ }
1057
+ }
1058
+ }
1059
+ }
1060
+ return {
1061
+ assigned: { total: assignedTotal, reviewed: assigned?.count ?? 0 },
1062
+ explored: explored?.count ?? 0,
1063
+ foundational: foundational?.count ?? 0,
1064
+ skipped: skipped?.count ?? 0
1065
+ };
1066
+ }
815
1067
  function runMigrations() {
816
1068
  const database = getDatabase();
817
1069
  const columns = queryAll(
@@ -856,6 +1108,7 @@ function runMigrations() {
856
1108
  CREATE TABLE IF NOT EXISTS sessions (
857
1109
  id TEXT PRIMARY KEY,
858
1110
  project_id TEXT REFERENCES projects(id),
1111
+ name TEXT,
859
1112
  started_at TEXT DEFAULT (datetime('now')),
860
1113
  ended_at TEXT,
861
1114
  summary TEXT,
@@ -864,6 +1117,10 @@ function runMigrations() {
864
1117
  status TEXT DEFAULT 'active'
865
1118
  )
866
1119
  `);
1120
+ try {
1121
+ database.run(`ALTER TABLE sessions ADD COLUMN name TEXT`);
1122
+ } catch {
1123
+ }
867
1124
  database.run(`
868
1125
  CREATE TABLE IF NOT EXISTS activity_log (
869
1126
  id TEXT PRIMARY KEY,
@@ -877,6 +1134,42 @@ function runMigrations() {
877
1134
  timestamp TEXT DEFAULT (datetime('now'))
878
1135
  )
879
1136
  `);
1137
+ database.run(`
1138
+ CREATE TABLE IF NOT EXISTS chunks (
1139
+ id TEXT PRIMARY KEY,
1140
+ session_id TEXT REFERENCES sessions(id),
1141
+ name TEXT NOT NULL,
1142
+ patterns TEXT,
1143
+ assigned_files TEXT,
1144
+ review_mode TEXT DEFAULT 'standard',
1145
+ created_at TEXT DEFAULT (datetime('now')),
1146
+ updated_at TEXT DEFAULT (datetime('now'))
1147
+ )
1148
+ `);
1149
+ database.run(`
1150
+ CREATE TABLE IF NOT EXISTS session_files (
1151
+ id TEXT PRIMARY KEY,
1152
+ session_id TEXT NOT NULL REFERENCES sessions(id),
1153
+ document_id TEXT NOT NULL REFERENCES documents(id),
1154
+ chunk_id TEXT REFERENCES chunks(id),
1155
+ agent_id TEXT,
1156
+ report_path TEXT,
1157
+ reviewed_at TEXT NOT NULL DEFAULT (datetime('now')),
1158
+ review_type TEXT DEFAULT 'assigned',
1159
+ is_foundational INTEGER DEFAULT 0,
1160
+ quality_issues TEXT,
1161
+ created_at TEXT DEFAULT (datetime('now')),
1162
+ UNIQUE(session_id, document_id)
1163
+ )
1164
+ `);
1165
+ try {
1166
+ database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_session ON session_files(session_id)`);
1167
+ database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_document ON session_files(document_id)`);
1168
+ database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_chunk ON session_files(chunk_id)`);
1169
+ database.run(`CREATE INDEX IF NOT EXISTS idx_session_files_report ON session_files(report_path)`);
1170
+ database.run(`CREATE INDEX IF NOT EXISTS idx_chunks_session ON chunks(session_id)`);
1171
+ } catch {
1172
+ }
880
1173
  saveDatabase();
881
1174
  }
882
1175
  var db, dbPath;
@@ -890,10 +1183,10 @@ var init_connection = __esm({
890
1183
  });
891
1184
 
892
1185
  // src/bin/aigile.ts
893
- import { Command as Command22 } from "commander";
1186
+ import { Command as Command23 } from "commander";
894
1187
 
895
1188
  // src/index.ts
896
- var VERSION = true ? "0.2.3" : "0.0.0-dev";
1189
+ var VERSION = true ? "0.2.5" : "0.0.0-dev";
897
1190
 
898
1191
  // src/bin/aigile.ts
899
1192
  init_connection();
@@ -997,7 +1290,7 @@ function error(message, opts = {}) {
997
1290
  console.error(`${prefix} ${message}`);
998
1291
  }
999
1292
  }
1000
- function warning(message, opts = {}) {
1293
+ function warning2(message, opts = {}) {
1001
1294
  if (opts.json) {
1002
1295
  console.log(JSON.stringify({ warning: message }));
1003
1296
  } else {
@@ -2106,7 +2399,7 @@ projectCommand.command("list").alias("ls").description("List all registered proj
2106
2399
  console.log(" \u2713 = valid path, \u2717 = missing/invalid path");
2107
2400
  if (invalidCount > 0) {
2108
2401
  blank();
2109
- warning(`${invalidCount} project(s) have invalid paths. Run "aigile project cleanup" to remove.`, opts);
2402
+ warning2(`${invalidCount} project(s) have invalid paths. Run "aigile project cleanup" to remove.`, opts);
2110
2403
  }
2111
2404
  });
2112
2405
  projectCommand.command("show").argument("[key]", "Project key (uses default if not specified)").description("Show project details").action((key) => {
@@ -2515,7 +2808,7 @@ epicCommand.command("delete").alias("rm").argument("<key>", "Epic key").option("
2515
2808
  (SELECT id FROM user_stories WHERE epic_id = ?)`,
2516
2809
  [epic.id]
2517
2810
  );
2518
- warning(
2811
+ warning2(
2519
2812
  `Deleting ${childCount.count} child story(s) and ${taskCount?.count || 0} task(s)`,
2520
2813
  opts
2521
2814
  );
@@ -2525,7 +2818,7 @@ epicCommand.command("delete").alias("rm").argument("<key>", "Epic key").option("
2525
2818
  );
2526
2819
  run("DELETE FROM user_stories WHERE epic_id = ?", [epic.id]);
2527
2820
  } else {
2528
- warning(`Orphaning ${childCount.count} child story(s)`, opts);
2821
+ warning2(`Orphaning ${childCount.count} child story(s)`, opts);
2529
2822
  run("UPDATE user_stories SET epic_id = NULL WHERE epic_id = ?", [epic.id]);
2530
2823
  }
2531
2824
  }
@@ -2724,10 +3017,10 @@ storyCommand.command("delete").alias("rm").argument("<key>", "Story key").option
2724
3017
  process.exit(1);
2725
3018
  }
2726
3019
  if (options.cascade) {
2727
- warning(`Deleting ${childCount.count} child task(s)`, opts);
3020
+ warning2(`Deleting ${childCount.count} child task(s)`, opts);
2728
3021
  run("DELETE FROM tasks WHERE story_id = ?", [story.id]);
2729
3022
  } else {
2730
- warning(`Orphaning ${childCount.count} child task(s)`, opts);
3023
+ warning2(`Orphaning ${childCount.count} child task(s)`, opts);
2731
3024
  run("UPDATE tasks SET story_id = NULL WHERE story_id = ?", [story.id]);
2732
3025
  }
2733
3026
  }
@@ -4739,8 +5032,8 @@ syncCommand.command("comments").option("-t, --type <type>", "Filter by type (use
4739
5032
  params.push(options.type);
4740
5033
  }
4741
5034
  query += " ORDER BY d.path, dc.line_number";
4742
- const { queryAll: queryAll2 } = (init_connection(), __toCommonJS(connection_exports));
4743
- const comments = queryAll2(query, params);
5035
+ const { queryAll: queryAll3 } = (init_connection(), __toCommonJS(connection_exports));
5036
+ const comments = queryAll3(query, params);
4744
5037
  data(
4745
5038
  comments,
4746
5039
  [
@@ -4760,7 +5053,7 @@ init_config();
4760
5053
 
4761
5054
  // src/services/session-service.ts
4762
5055
  init_connection();
4763
- function startSession(projectId) {
5056
+ function startSession(projectId, name) {
4764
5057
  const existing = queryOne(
4765
5058
  "SELECT id FROM sessions WHERE project_id = ? AND status = ?",
4766
5059
  [projectId, "active"]
@@ -4773,14 +5066,16 @@ function startSession(projectId) {
4773
5066
  }
4774
5067
  const sessionId = generateId();
4775
5068
  const now = (/* @__PURE__ */ new Date()).toISOString();
5069
+ const sessionName = name || null;
4776
5070
  run(
4777
- `INSERT INTO sessions (id, project_id, started_at, status, entities_modified, files_modified)
4778
- VALUES (?, ?, ?, 'active', 0, 0)`,
4779
- [sessionId, projectId, now]
5071
+ `INSERT INTO sessions (id, project_id, name, started_at, status, entities_modified, files_modified)
5072
+ VALUES (?, ?, ?, ?, 'active', 0, 0)`,
5073
+ [sessionId, projectId, sessionName, now]
4780
5074
  );
4781
5075
  return {
4782
5076
  id: sessionId,
4783
5077
  projectId,
5078
+ name: sessionName,
4784
5079
  startedAt: now,
4785
5080
  endedAt: null,
4786
5081
  summary: null,
@@ -4832,6 +5127,86 @@ function getActiveSession(projectId) {
4832
5127
  status: session.status
4833
5128
  };
4834
5129
  }
5130
+ function getSessionByName(projectId, name) {
5131
+ const session = queryOne(
5132
+ "SELECT * FROM sessions WHERE project_id = ? AND name = ?",
5133
+ [projectId, name]
5134
+ );
5135
+ if (!session) {
5136
+ return null;
5137
+ }
5138
+ return {
5139
+ id: session.id,
5140
+ projectId: session.project_id,
5141
+ name: session.name,
5142
+ startedAt: session.started_at,
5143
+ endedAt: session.ended_at,
5144
+ summary: session.summary,
5145
+ entitiesModified: session.entities_modified,
5146
+ filesModified: session.files_modified,
5147
+ status: session.status
5148
+ };
5149
+ }
5150
+ function resumeSession(projectId, sessionId) {
5151
+ const session = queryOne(
5152
+ "SELECT * FROM sessions WHERE id = ? AND project_id = ?",
5153
+ [sessionId, projectId]
5154
+ );
5155
+ if (!session) {
5156
+ return null;
5157
+ }
5158
+ if (session.status === "completed") {
5159
+ return null;
5160
+ }
5161
+ const existing = queryOne(
5162
+ "SELECT id FROM sessions WHERE project_id = ? AND status = ? AND id != ?",
5163
+ [projectId, "active", sessionId]
5164
+ );
5165
+ if (existing) {
5166
+ run(
5167
+ `UPDATE sessions SET status = 'abandoned', ended_at = datetime('now') WHERE id = ?`,
5168
+ [existing.id]
5169
+ );
5170
+ }
5171
+ run(
5172
+ `UPDATE sessions SET status = 'active', ended_at = NULL WHERE id = ?`,
5173
+ [sessionId]
5174
+ );
5175
+ return {
5176
+ id: session.id,
5177
+ projectId: session.project_id,
5178
+ name: session.name,
5179
+ startedAt: session.started_at,
5180
+ endedAt: null,
5181
+ summary: session.summary,
5182
+ entitiesModified: session.entities_modified,
5183
+ filesModified: session.files_modified,
5184
+ status: "active"
5185
+ };
5186
+ }
5187
+ function getSessionResumeInfo(sessionId) {
5188
+ const chunks = getSessionChunks(sessionId);
5189
+ let totalAssigned = 0;
5190
+ for (const chunk of chunks) {
5191
+ if (chunk.assigned_files) {
5192
+ try {
5193
+ totalAssigned += JSON.parse(chunk.assigned_files).length;
5194
+ } catch {
5195
+ }
5196
+ }
5197
+ }
5198
+ const reviewed = queryOne(
5199
+ `SELECT COUNT(*) as count FROM session_files WHERE session_id = ? AND review_type = 'assigned'`,
5200
+ [sessionId]
5201
+ );
5202
+ return {
5203
+ chunks,
5204
+ coverage: {
5205
+ total: totalAssigned,
5206
+ reviewed: reviewed?.count ?? 0
5207
+ }
5208
+ };
5209
+ }
4835
5210
  function incrementSessionEntities(projectId, count = 1) {
4836
5211
  run(
4837
5212
  `UPDATE sessions SET entities_modified = entities_modified + ? WHERE project_id = ? AND status = 'active'`,
@@ -5016,7 +5391,7 @@ function getActivitySummary(projectId, since) {
5016
5391
 
5017
5392
  // src/commands/session.ts
5018
5393
  var sessionCommand = new Command11("session").description("Manage AI work sessions");
5019
- sessionCommand.command("start").description("Start a new AI work session").action(() => {
5394
+ sessionCommand.command("start").argument("[name]", "Optional session name (e.g., init-241215-1030)").description("Start a new AI work session").action((name) => {
5020
5395
  const opts = getOutputOptions(sessionCommand);
5021
5396
  const projectRoot = findProjectRoot();
5022
5397
  if (!projectRoot) {
@@ -5033,17 +5408,21 @@ sessionCommand.command("start").description("Start a new AI work session").actio
5033
5408
  error(`Project "${config.project.key}" not found in database.`, opts);
5034
5409
  process.exit(1);
5035
5410
  }
5036
- const session = startSession(project.id);
5411
+ const session = startSession(project.id, name);
5037
5412
  if (opts.json) {
5038
5413
  console.log(JSON.stringify({
5039
5414
  success: true,
5040
5415
  data: {
5041
5416
  sessionId: session.id,
5417
+ name: session.name,
5042
5418
  startedAt: session.startedAt
5043
5419
  }
5044
5420
  }));
5045
5421
  } else {
5046
5422
  success(`Session started: ${session.id.slice(0, 8)}...`, opts);
5423
+ if (session.name) {
5424
+ info(`Name: ${session.name}`, opts);
5425
+ }
5047
5426
  info(`Started at: ${session.startedAt}`, opts);
5048
5427
  }
5049
5428
  });
@@ -5318,6 +5697,109 @@ sessionCommand.command("activity").option("-t, --type <type>", "Filter by entity
5318
5697
  opts
5319
5698
  );
5320
5699
  });
5700
+ sessionCommand.command("find").argument("<name>", "Session name to search for").description("Find a session by name").action((name) => {
5701
+ const opts = getOutputOptions(sessionCommand);
5702
+ const projectRoot = findProjectRoot();
5703
+ if (!projectRoot) {
5704
+ error('Not in an AIGILE project. Run "aigile init" first.', opts);
5705
+ process.exit(1);
5706
+ }
5707
+ const config = loadProjectConfig(projectRoot);
5708
+ if (!config) {
5709
+ error("Could not load project config.", opts);
5710
+ process.exit(1);
5711
+ }
5712
+ const project = queryOne("SELECT id FROM projects WHERE key = ?", [config.project.key]);
5713
+ if (!project) {
5714
+ error(`Project "${config.project.key}" not found in database.`, opts);
5715
+ process.exit(1);
5716
+ }
5717
+ const session = getSessionByName(project.id, name);
5718
+ if (!session) {
5719
+ error(`Session "${name}" not found.`, opts);
5720
+ process.exit(1);
5721
+ }
5722
+ if (opts.json) {
5723
+ console.log(JSON.stringify({
5724
+ success: true,
5725
+ data: {
5726
+ id: session.id,
5727
+ name: session.name,
5728
+ status: session.status,
5729
+ startedAt: session.startedAt,
5730
+ endedAt: session.endedAt
5731
+ }
5732
+ }));
5733
+ } else {
5734
+ success(`Found session: ${session.id.slice(0, 8)}...`, opts);
5735
+ console.log(` Name: ${session.name}`);
5736
+ console.log(` Status: ${session.status}`);
5737
+ console.log(` Started: ${session.startedAt}`);
5738
+ if (session.endedAt) {
5739
+ console.log(` Ended: ${session.endedAt}`);
5740
+ }
5741
+ }
5742
+ });
5743
+ sessionCommand.command("resume").argument("<name-or-id>", "Session name or ID to resume").description("Resume an incomplete session").action((nameOrId) => {
5744
+ const opts = getOutputOptions(sessionCommand);
5745
+ const projectRoot = findProjectRoot();
5746
+ if (!projectRoot) {
5747
+ error('Not in an AIGILE project. Run "aigile init" first.', opts);
5748
+ process.exit(1);
5749
+ }
5750
+ const config = loadProjectConfig(projectRoot);
5751
+ if (!config) {
5752
+ error("Could not load project config.", opts);
5753
+ process.exit(1);
5754
+ }
5755
+ const project = queryOne("SELECT id FROM projects WHERE key = ?", [config.project.key]);
5756
+ if (!project) {
5757
+ error(`Project "${config.project.key}" not found in database.`, opts);
5758
+ process.exit(1);
5759
+ }
5760
+ let session = getSessionByName(project.id, nameOrId);
5761
+ if (!session) {
5762
+ session = getSession(nameOrId);
5763
+ }
5764
+ if (!session) {
5765
+ error(`Session "${nameOrId}" not found.`, opts);
5766
+ process.exit(1);
5767
+ }
5768
+ if (session.status === "completed") {
5769
+ error(`Session "${nameOrId}" is already completed. Cannot resume.`, opts);
5770
+ process.exit(1);
5771
+ }
5772
+ const resumed = resumeSession(project.id, session.id);
5773
+ if (!resumed) {
5774
+ error(`Failed to resume session "${nameOrId}".`, opts);
5775
+ process.exit(1);
5776
+ }
5777
+ const resumeInfo = getSessionResumeInfo(session.id);
5778
+ if (opts.json) {
5779
+ console.log(JSON.stringify({
5780
+ success: true,
5781
+ data: {
5782
+ id: resumed.id,
5783
+ name: resumed.name,
5784
+ status: resumed.status,
5785
+ chunks: resumeInfo.chunks.length,
5786
+ coverage: resumeInfo.coverage
5787
+ }
5788
+ }));
5789
+ } else {
5790
+ success(`Session resumed: ${resumed.id.slice(0, 8)}...`, opts);
5791
+ if (resumed.name) {
5792
+ console.log(` Name: ${resumed.name}`);
5793
+ }
5794
+ console.log(` Status: ${resumed.status}`);
5795
+ console.log(` Chunks: ${resumeInfo.chunks.length}`);
5796
+ console.log(` Coverage: ${resumeInfo.coverage.reviewed}/${resumeInfo.coverage.total} assigned files reviewed`);
5797
+ if (resumeInfo.coverage.total > 0) {
5798
+ const pct = Math.round(resumeInfo.coverage.reviewed / resumeInfo.coverage.total * 100);
5799
+ console.log(` Progress: ${pct}%`);
5800
+ }
5801
+ }
5802
+ });
5321
5803
  function calculateDuration2(start, end) {
5322
5804
  const startDate = new Date(start);
5323
5805
  const endDate = end ? new Date(end) : /* @__PURE__ */ new Date();
@@ -9024,7 +9506,7 @@ daemonCommand.command("install").description("Install daemon to start automatica
9024
9506
  info("Daemon will watch ALL registered projects", opts);
9025
9507
  info('Run "aigile daemon start" to start the watcher', opts);
9026
9508
  } catch (err) {
9027
- warning("Service file created but could not enable. You may need to run:", opts);
9509
+ warning2("Service file created but could not enable. You may need to run:", opts);
9028
9510
  console.log(` systemctl --user daemon-reload`);
9029
9511
  console.log(` systemctl --user enable ${DAEMON_NAME}`);
9030
9512
  }
@@ -9516,9 +9998,11 @@ function formatBytes(bytes) {
9516
9998
 
9517
9999
  // src/commands/file.ts
9518
10000
  init_connection();
10001
+ init_connection();
9519
10002
  import { Command as Command21 } from "commander";
9520
10003
  import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
9521
10004
  import { join as join12, relative as relative4 } from "path";
10005
+ import { glob } from "glob";
9522
10006
  init_config();
9523
10007
  var fileCommand = new Command21("file").description("Shadow mode file analysis and management for brownfield projects");
9524
10008
  function getProjectContext(opts) {
@@ -9908,10 +10392,721 @@ fileCommand.command("show <path>").description("Show detailed file analysis").ac
9908
10392
  );
9909
10393
  }
9910
10394
  });
10395
+ fileCommand.command("tag").argument("<path>", "File path to tag").option("--chunk <id>", "Chunk ID this file belongs to").option("--report <path>", "Report this file contributes to").option("--type <type>", "Review type: assigned|explored|skipped", "assigned").option("--foundational", "Mark as foundational file").option("--agent <id>", "Agent ID that reviewed this file").description("Tag a file as reviewed in the current session").action((filePath, options) => {
10396
+ const opts = getOutputOptions(fileCommand);
10397
+ const ctx = getProjectContext(opts);
10398
+ if (!ctx) {
10399
+ process.exit(1);
10400
+ }
10401
+ const session = getActiveSession(ctx.projectId);
10402
+ if (!session) {
10403
+ error('No active session. Start one with "aigile session start".', opts);
10404
+ process.exit(1);
10405
+ }
10406
+ const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
10407
+ const doc = queryOne(
10408
+ "SELECT id FROM documents WHERE project_id = ? AND path = ?",
10409
+ [ctx.projectId, normalizedPath]
10410
+ );
10411
+ if (!doc) {
10412
+ const tracked = trackShadowFile(ctx.projectId, ctx.projectRoot, normalizedPath);
10413
+ if (!tracked) {
10414
+ error(`File not tracked: ${normalizedPath}. Run "aigile sync scan" first.`, opts);
10415
+ process.exit(1);
10416
+ }
10417
+ const newDoc = queryOne(
10418
+ "SELECT id FROM documents WHERE project_id = ? AND path = ?",
10419
+ [ctx.projectId, normalizedPath]
10420
+ );
10421
+ if (!newDoc) {
10422
+ error(`Could not track file: ${normalizedPath}`, opts);
10423
+ process.exit(1);
10424
+ }
10425
+ doc.id = newDoc.id;
10426
+ }
10427
+ const validTypes = ["assigned", "explored", "skipped"];
10428
+ if (!validTypes.includes(options.type)) {
10429
+ error(`Invalid review type "${options.type}". Must be: ${validTypes.join(", ")}`, opts);
10430
+ process.exit(1);
10431
+ }
10432
+ const sessionFileId = tagFileReviewed(session.id, doc.id, {
10433
+ chunkId: options.chunk,
10434
+ agentId: options.agent,
10435
+ reportPath: options.report,
10436
+ reviewType: options.type,
10437
+ isFoundational: options.foundational ?? false
10438
+ });
10439
+ if (opts.json) {
10440
+ console.log(JSON.stringify({
10441
+ success: true,
10442
+ data: {
10443
+ session_file_id: sessionFileId,
10444
+ path: normalizedPath,
10445
+ session_id: session.id,
10446
+ chunk_id: options.chunk ?? null,
10447
+ review_type: options.type,
10448
+ is_foundational: options.foundational ?? false
10449
+ }
10450
+ }));
10451
+ } else {
10452
+ success(`Tagged: ${normalizedPath}`, opts);
10453
+ if (options.chunk) {
10454
+ info(` Chunk: ${options.chunk}`, opts);
10455
+ }
10456
+ info(` Type: ${options.type}`, opts);
10457
+ }
10458
+ });
10459
+ fileCommand.command("tag-batch").option("--chunk <id>", "Chunk ID for all files").option("--glob <pattern>", "Glob pattern for files").option("--type <type>", "Review type: assigned|explored|skipped", "assigned").option("--foundational", "Mark all as foundational").option("--agent <id>", "Agent ID").description("Tag multiple files as reviewed (from glob pattern or stdin)").action(async (options) => {
10460
+ const opts = getOutputOptions(fileCommand);
10461
+ const ctx = getProjectContext(opts);
10462
+ if (!ctx) {
10463
+ process.exit(1);
10464
+ }
10465
+ const session = getActiveSession(ctx.projectId);
10466
+ if (!session) {
10467
+ error('No active session. Start one with "aigile session start".', opts);
10468
+ process.exit(1);
10469
+ }
10470
+ let filesToTag = [];
10471
+ if (options.glob) {
10472
+ const matches = await glob(options.glob, { cwd: ctx.projectRoot, nodir: true });
10473
+ filesToTag = matches.map((f) => relative4(ctx.projectRoot, join12(ctx.projectRoot, f)));
10474
+ } else {
10475
+ error("Please provide --glob pattern. Stdin not supported yet.", opts);
10476
+ process.exit(1);
10477
+ }
10478
+ if (filesToTag.length === 0) {
10479
+ error("No files matched the pattern.", opts);
10480
+ process.exit(1);
10481
+ }
10482
+ let tagged = 0;
10483
+ let skipped = 0;
10484
+ for (const filePath of filesToTag) {
10485
+ const doc = queryOne(
10486
+ "SELECT id FROM documents WHERE project_id = ? AND path = ?",
10487
+ [ctx.projectId, filePath]
10488
+ );
10489
+ if (!doc) {
10490
+ skipped++;
10491
+ continue;
10492
+ }
10493
+ tagFileReviewed(session.id, doc.id, {
10494
+ chunkId: options.chunk,
10495
+ agentId: options.agent,
10496
+ reviewType: options.type,
10497
+ isFoundational: options.foundational ?? false
10498
+ });
10499
+ tagged++;
10500
+ }
10501
+ if (opts.json) {
10502
+ console.log(JSON.stringify({
10503
+ success: true,
10504
+ data: { tagged, skipped, total: filesToTag.length }
10505
+ }));
10506
+ } else {
10507
+ success(`Tagged ${tagged} files (${skipped} skipped - not tracked)`, opts);
10508
+ }
10509
+ });
10510
+ fileCommand.command("untag").argument("<path>", "File path to untag").option("--session <id>", "Session ID (default: current)").description("Remove review tag from a file").action((filePath, options) => {
10511
+ const opts = getOutputOptions(fileCommand);
10512
+ const ctx = getProjectContext(opts);
10513
+ if (!ctx) {
10514
+ process.exit(1);
10515
+ }
10516
+ let sessionId = options.session;
10517
+ if (!sessionId) {
10518
+ const session = getActiveSession(ctx.projectId);
10519
+ if (!session) {
10520
+ error("No active session. Specify --session or start one.", opts);
10521
+ process.exit(1);
10522
+ }
10523
+ sessionId = session.id;
10524
+ }
10525
+ const doc = queryOne(
10526
+ "SELECT id FROM documents WHERE project_id = ? AND path = ?",
10527
+ [ctx.projectId, filePath]
10528
+ );
10529
+ if (!doc) {
10530
+ error(`File "${filePath}" not found in project.`, opts);
10531
+ process.exit(1);
10532
+ }
10533
+ const existing = queryOne(
10534
+ "SELECT id FROM session_files WHERE session_id = ? AND document_id = ?",
10535
+ [sessionId, doc.id]
10536
+ );
10537
+ if (!existing) {
10538
+ warning(`File "${filePath}" is not tagged in this session.`, opts);
10539
+ return;
10540
+ }
10541
+ queryOne("DELETE FROM session_files WHERE id = ?", [existing.id]);
10542
+ if (opts.json) {
10543
+ console.log(JSON.stringify({
10544
+ success: true,
10545
+ data: { path: filePath, untagged: true }
10546
+ }));
10547
+ } else {
10548
+ success(`Untagged: ${filePath}`, opts);
10549
+ }
10550
+ });
10551
+ fileCommand.command("clear-tags").option("--session <id>", "Session ID (default: current)").option("--chunk <id>", "Only clear tags for specific chunk").option("--confirm", "Skip confirmation prompt").description("Remove all file tags from a session (for re-review)").action((options) => {
10552
+ const opts = getOutputOptions(fileCommand);
10553
+ const ctx = getProjectContext(opts);
10554
+ if (!ctx) {
10555
+ process.exit(1);
10556
+ }
10557
+ let sessionId = options.session;
10558
+ if (!sessionId) {
10559
+ const session = getActiveSession(ctx.projectId);
10560
+ if (!session) {
10561
+ error("No active session. Specify --session or start one.", opts);
10562
+ process.exit(1);
10563
+ }
10564
+ sessionId = session.id;
10565
+ }
10566
+ let countQuery = "SELECT COUNT(*) as count FROM session_files WHERE session_id = ?";
10567
+ const params = [sessionId];
10568
+ if (options.chunk) {
10569
+ countQuery += " AND chunk_id = ?";
10570
+ params.push(options.chunk);
10571
+ }
10572
+ const result = queryOne(countQuery, params);
10573
+ const count = result?.count ?? 0;
10574
+ if (count === 0) {
10575
+ warning("No tags to clear.", opts);
10576
+ return;
10577
+ }
10578
+ if (!options.confirm && !opts.json) {
10579
+ console.log(`This will remove ${count} tag(s).`);
10580
+ console.log("Use --confirm to proceed.");
10581
+ return;
10582
+ }
10583
+ let deleteQuery = "DELETE FROM session_files WHERE session_id = ?";
10584
+ const deleteParams = [sessionId];
10585
+ if (options.chunk) {
10586
+ deleteQuery += " AND chunk_id = ?";
10587
+ deleteParams.push(options.chunk);
10588
+ }
10589
+ queryOne(deleteQuery, deleteParams);
10590
+ if (opts.json) {
10591
+ console.log(JSON.stringify({
10592
+ success: true,
10593
+ data: { session_id: sessionId, cleared: count }
10594
+ }));
10595
+ } else {
10596
+ success(`Cleared ${count} tag(s)`, opts);
10597
+ }
10598
+ });
10599
+ fileCommand.command("untagged").option("--session <id>", "Session ID (default: current)").option("--chunk <id>", "Filter by chunk").option("--assigned-only", "Only show untagged assigned files").description("List files not yet tagged/reviewed in session").action((options) => {
10600
+ const opts = getOutputOptions(fileCommand);
10601
+ const ctx = getProjectContext(opts);
10602
+ if (!ctx) {
10603
+ process.exit(1);
10604
+ }
10605
+ let sessionId = options.session;
10606
+ if (!sessionId) {
10607
+ const session = getActiveSession(ctx.projectId);
10608
+ if (!session) {
10609
+ error("No active session. Specify --session or start one.", opts);
10610
+ process.exit(1);
10611
+ }
10612
+ sessionId = session.id;
10613
+ }
10614
+ const untagged = getUntaggedFiles(ctx.projectId, sessionId, {
10615
+ chunkId: options.chunk,
10616
+ assignedOnly: options.assignedOnly
10617
+ });
10618
+ if (opts.json) {
10619
+ console.log(JSON.stringify({
10620
+ success: true,
10621
+ data: {
10622
+ session_id: sessionId,
10623
+ chunk_id: options.chunk ?? null,
10624
+ count: untagged.length,
10625
+ files: untagged.map((f) => f.path)
10626
+ }
10627
+ }));
10628
+ } else {
10629
+ if (untagged.length === 0) {
10630
+ success("All files have been tagged!", opts);
10631
+ return;
10632
+ }
10633
+ console.log(`Untagged files (${untagged.length}):`);
10634
+ for (const file of untagged) {
10635
+ console.log(` ${file.path}`);
10636
+ }
10637
+ }
10638
+ });
10639
+ fileCommand.command("coverage").option("--session <id>", "Session ID (default: current)").option("--by-chunk", "Group statistics by chunk").description("Show file review coverage statistics").action((options) => {
10640
+ const opts = getOutputOptions(fileCommand);
10641
+ const ctx = getProjectContext(opts);
10642
+ if (!ctx) {
10643
+ process.exit(1);
10644
+ }
10645
+ let sessionId = options.session;
10646
+ if (!sessionId) {
10647
+ const session = getActiveSession(ctx.projectId);
10648
+ if (!session) {
10649
+ error("No active session. Specify --session or start one.", opts);
10650
+ process.exit(1);
10651
+ }
10652
+ sessionId = session.id;
10653
+ }
10654
+ if (options.byChunk) {
10655
+ const chunks = getSessionChunks(sessionId);
10656
+ if (chunks.length === 0) {
10657
+ info("No chunks defined in this session.", opts);
10658
+ return;
10659
+ }
10660
+ const chunkStats = chunks.map((chunk) => {
10661
+ const stats = getCoverageStats(sessionId, chunk.id);
10662
+ const pct = stats.assigned.total > 0 ? Math.round(stats.assigned.reviewed / stats.assigned.total * 100) : 100;
10663
+ return {
10664
+ id: chunk.id,
10665
+ name: chunk.name,
10666
+ assigned: `${stats.assigned.reviewed}/${stats.assigned.total} (${pct}%)`,
10667
+ explored: stats.explored,
10668
+ foundational: stats.foundational,
10669
+ skipped: stats.skipped
10670
+ };
10671
+ });
10672
+ if (opts.json) {
10673
+ console.log(JSON.stringify({ success: true, data: chunkStats }));
10674
+ } else {
10675
+ data(
10676
+ chunkStats,
10677
+ [
10678
+ { header: "ID", key: "id", width: 15 },
10679
+ { header: "Name", key: "name", width: 20 },
10680
+ { header: "Assigned", key: "assigned", width: 15 },
10681
+ { header: "Explored", key: "explored", width: 10 },
10682
+ { header: "Found.", key: "foundational", width: 8 },
10683
+ { header: "Skipped", key: "skipped", width: 8 }
10684
+ ],
10685
+ opts
10686
+ );
10687
+ }
10688
+ } else {
10689
+ const stats = getCoverageStats(sessionId);
10690
+ const totalTagged = getSessionFiles(sessionId).length;
10691
+ const untagged = getUntaggedFiles(ctx.projectId, sessionId);
10692
+ const total = totalTagged + untagged.length;
10693
+ const pct = total > 0 ? Math.round(totalTagged / total * 100) : 100;
10694
+ if (opts.json) {
10695
+ console.log(JSON.stringify({
10696
+ success: true,
10697
+ data: {
10698
+ session_id: sessionId,
10699
+ total_files: total,
10700
+ tagged: totalTagged,
10701
+ untagged: untagged.length,
10702
+ coverage_percent: pct,
10703
+ by_type: {
10704
+ assigned: stats.assigned.reviewed,
10705
+ explored: stats.explored,
10706
+ foundational: stats.foundational,
10707
+ skipped: stats.skipped
10708
+ }
10709
+ }
10710
+ }));
10711
+ } else {
10712
+ console.log(`
10713
+ Coverage for session ${sessionId.slice(0, 8)}...`);
10714
+ console.log(` Total files: ${total}`);
10715
+ console.log(` Tagged: ${totalTagged} (${pct}%)`);
10716
+ console.log(` Untagged: ${untagged.length}`);
10717
+ console.log(`
10718
+ By type:`);
10719
+ console.log(` Assigned: ${stats.assigned.reviewed}`);
10720
+ console.log(` Explored: ${stats.explored}`);
10721
+ console.log(` Foundational: ${stats.foundational}`);
10722
+ console.log(` Skipped: ${stats.skipped}`);
10723
+ }
10724
+ }
10725
+ });
10726
+ fileCommand.command("flag").argument("<path>", "File path to flag").option("--duplicate <path>", "Similar/duplicate file").option("--unclear <lines>", 'Unclear code at lines (e.g., "45-60")').option("--note <text>", "Description of the issue").description("Flag a file with quality issues").action((filePath, options) => {
10727
+ const opts = getOutputOptions(fileCommand);
10728
+ const ctx = getProjectContext(opts);
10729
+ if (!ctx) {
10730
+ process.exit(1);
10731
+ }
10732
+ const session = getActiveSession(ctx.projectId);
10733
+ if (!session) {
10734
+ error('No active session. Start one with "aigile session start".', opts);
10735
+ process.exit(1);
10736
+ }
10737
+ const normalizedPath = filePath.startsWith("/") ? relative4(ctx.projectRoot, filePath) : filePath;
10738
+ const sessionFile = queryOne(
10739
+ `SELECT sf.id FROM session_files sf
10740
+ JOIN documents d ON sf.document_id = d.id
10741
+ WHERE sf.session_id = ? AND d.path = ?`,
10742
+ [session.id, normalizedPath]
10743
+ );
10744
+ if (!sessionFile) {
10745
+ error(`File not tagged in this session: ${normalizedPath}. Tag it first with "aigile file tag".`, opts);
10746
+ process.exit(1);
10747
+ }
10748
+ const issues = [];
10749
+ if (options.duplicate) {
10750
+ issues.push(`duplicate:${options.duplicate}`);
10751
+ }
10752
+ if (options.unclear) {
10753
+ issues.push(`unclear:${options.unclear}`);
10754
+ }
10755
+ if (options.note) {
10756
+ issues.push(`note:${options.note}`);
10757
+ }
10758
+ if (issues.length === 0) {
10759
+ error("No issues specified. Use --duplicate, --unclear, or --note.", opts);
10760
+ process.exit(1);
10761
+ }
10762
+ flagFileQualityIssue(sessionFile.id, issues);
10763
+ if (opts.json) {
10764
+ console.log(JSON.stringify({
10765
+ success: true,
10766
+ data: {
10767
+ path: normalizedPath,
10768
+ issues
10769
+ }
10770
+ }));
10771
+ } else {
10772
+ success(`Flagged: ${normalizedPath}`, opts);
10773
+ for (const issue of issues) {
10774
+ info(` ${issue}`, opts);
10775
+ }
10776
+ }
10777
+ });
10778
+ fileCommand.command("duplicates").option("--session <id>", "Session ID (default: current)").description("List files flagged as duplicates").action((options) => {
10779
+ const opts = getOutputOptions(fileCommand);
10780
+ const ctx = getProjectContext(opts);
10781
+ if (!ctx) {
10782
+ process.exit(1);
10783
+ }
10784
+ let sessionId = options.session;
10785
+ if (!sessionId) {
10786
+ const session = getActiveSession(ctx.projectId);
10787
+ if (!session) {
10788
+ error("No active session. Specify --session or start one.", opts);
10789
+ process.exit(1);
10790
+ }
10791
+ sessionId = session.id;
10792
+ }
10793
+ const filesWithIssues = getFilesWithQualityIssues(sessionId);
10794
+ const duplicates = [];
10795
+ for (const sf of filesWithIssues) {
10796
+ if (!sf.quality_issues) continue;
10797
+ const issues = JSON.parse(sf.quality_issues);
10798
+ const doc = queryOne(
10799
+ "SELECT path FROM documents WHERE id = ?",
10800
+ [sf.document_id]
10801
+ );
10802
+ if (!doc) continue;
10803
+ for (const issue of issues) {
10804
+ if (issue.startsWith("duplicate:")) {
10805
+ const duplicateOf = issue.replace("duplicate:", "");
10806
+ const noteIssue = issues.find((i) => i.startsWith("note:"));
10807
+ duplicates.push({
10808
+ path: doc.path,
10809
+ duplicate_of: duplicateOf,
10810
+ note: noteIssue ? noteIssue.replace("note:", "") : void 0
10811
+ });
10812
+ }
10813
+ }
10814
+ }
10815
+ if (opts.json) {
10816
+ console.log(JSON.stringify({
10817
+ success: true,
10818
+ data: { duplicates }
10819
+ }));
10820
+ } else {
10821
+ if (duplicates.length === 0) {
10822
+ info("No duplicates flagged.", opts);
10823
+ return;
10824
+ }
10825
+ console.log(`Flagged duplicates (${duplicates.length}):`);
10826
+ for (const dup of duplicates) {
10827
+ console.log(` ${dup.path} <-> ${dup.duplicate_of}`);
10828
+ if (dup.note) {
10829
+ console.log(` Note: ${dup.note}`);
10830
+ }
10831
+ }
10832
+ }
10833
+ });
10834
+ fileCommand.command("issues").option("--session <id>", "Session ID (default: current)").description("List all files with quality issues").action((options) => {
10835
+ const opts = getOutputOptions(fileCommand);
10836
+ const ctx = getProjectContext(opts);
10837
+ if (!ctx) {
10838
+ process.exit(1);
10839
+ }
10840
+ let sessionId = options.session;
10841
+ if (!sessionId) {
10842
+ const session = getActiveSession(ctx.projectId);
10843
+ if (!session) {
10844
+ error("No active session. Specify --session or start one.", opts);
10845
+ process.exit(1);
10846
+ }
10847
+ sessionId = session.id;
10848
+ }
10849
+ const filesWithIssues = getFilesWithQualityIssues(sessionId);
10850
+ if (filesWithIssues.length === 0) {
10851
+ info("No quality issues flagged.", opts);
10852
+ return;
10853
+ }
10854
+ const issueList = [];
10855
+ for (const sf of filesWithIssues) {
10856
+ const doc = queryOne(
10857
+ "SELECT path FROM documents WHERE id = ?",
10858
+ [sf.document_id]
10859
+ );
10860
+ if (!doc) continue;
10861
+ issueList.push({
10862
+ path: doc.path,
10863
+ issues: sf.quality_issues ? JSON.parse(sf.quality_issues) : []
10864
+ });
10865
+ }
10866
+ if (opts.json) {
10867
+ console.log(JSON.stringify({
10868
+ success: true,
10869
+ data: { files_with_issues: issueList }
10870
+ }));
10871
+ } else {
10872
+ console.log(`Files with quality issues (${issueList.length}):`);
10873
+ for (const file of issueList) {
10874
+ console.log(`
10875
+ ${file.path}:`);
10876
+ for (const issue of file.issues) {
10877
+ console.log(` - ${issue}`);
10878
+ }
10879
+ }
10880
+ }
10881
+ });
10882
+
10883
+ // src/commands/chunk.ts
10884
+ init_connection();
10885
+ init_connection();
10886
+ import { Command as Command22 } from "commander";
10887
+ import { glob as glob2 } from "glob";
10888
+ import { relative as relative5, resolve as resolve2 } from "path";
10889
+ init_config();
10890
+ function safeParseArray(json) {
10891
+ if (!json) return [];
10892
+ try {
10893
+ return JSON.parse(json);
10894
+ } catch {
10895
+ return [];
10896
+ }
10897
+ }
10898
+ var chunkCommand = new Command22("chunk").description("Manage file review chunks for verified coverage");
10899
+ chunkCommand.command("create").argument("<id>", "Chunk ID (e.g., chunk-001)").option("-n, --name <name>", "Human-readable name").option("-p, --pattern <patterns...>", "Glob patterns for files").option("-a, --assign <files...>", "Explicit file assignments").option("-m, --mode <mode>", "Review mode: quick|standard|audit", "standard").description("Create a new chunk with file assignments").action(async (id, options) => {
10900
+ const opts = getOutputOptions(chunkCommand);
10901
+ const projectRoot = findProjectRoot();
10902
+ if (!projectRoot) {
10903
+ error('Not in an AIGILE project. Run "aigile init" first.', opts);
10904
+ process.exit(1);
10905
+ }
10906
+ const config = loadProjectConfig(projectRoot);
10907
+ if (!config) {
10908
+ error("Could not load project config.", opts);
10909
+ process.exit(1);
10910
+ }
10911
+ const project = queryOne("SELECT id FROM projects WHERE key = ?", [config.project.key]);
10912
+ if (!project) {
10913
+ error(`Project "${config.project.key}" not found in database.`, opts);
10914
+ process.exit(1);
10915
+ }
10916
+ const session = getActiveSession(project.id);
10917
+ if (!session) {
10918
+ error('No active session. Start one with "aigile session start".', opts);
10919
+ process.exit(1);
10920
+ }
10921
+ const existing = getChunk(id);
10922
+ if (existing) {
10923
+ error(`Chunk "${id}" already exists.`, opts);
10924
+ process.exit(1);
10925
+ }
10926
+ const validModes = ["quick", "standard", "audit"];
10927
+ if (!validModes.includes(options.mode)) {
10928
+ error(`Invalid mode "${options.mode}". Must be: ${validModes.join(", ")}`, opts);
10929
+ process.exit(1);
10930
+ }
10931
+ let assignedFiles = [];
10932
+ if (options.pattern) {
10933
+ for (const pattern of options.pattern) {
10934
+ const matches = await glob2(pattern, { cwd: projectRoot, nodir: true });
10935
+ assignedFiles.push(...matches.map((f) => relative5(projectRoot, resolve2(projectRoot, f))));
10936
+ }
10937
+ }
10938
+ if (options.assign) {
10939
+ assignedFiles.push(...options.assign);
10940
+ }
10941
+ assignedFiles = [...new Set(assignedFiles)];
10942
+ const name = options.name ?? id;
10943
+ createChunk(
10944
+ session.id,
10945
+ id,
10946
+ name,
10947
+ options.pattern ?? null,
10948
+ assignedFiles.length > 0 ? assignedFiles : null,
10949
+ options.mode
10950
+ );
10951
+ if (opts.json) {
10952
+ console.log(JSON.stringify({
10953
+ success: true,
10954
+ data: {
10955
+ id,
10956
+ name,
10957
+ patterns: options.pattern ?? [],
10958
+ assigned_files: assignedFiles,
10959
+ review_mode: options.mode,
10960
+ session_id: session.id
10961
+ }
10962
+ }));
10963
+ } else {
10964
+ success(`Created chunk "${id}" (${name})`, opts);
10965
+ if (assignedFiles.length > 0) {
10966
+ console.log(` Assigned files: ${assignedFiles.length}`);
10967
+ }
10968
+ if (options.pattern) {
10969
+ console.log(` Patterns: ${options.pattern.join(", ")}`);
10970
+ }
10971
+ console.log(` Review mode: ${options.mode}`);
10972
+ }
10973
+ });
10974
+ chunkCommand.command("files").argument("<id>", "Chunk ID").option("--json", "Output as JSON").description("List files assigned to a chunk").action((id) => {
10975
+ const opts = getOutputOptions(chunkCommand);
10976
+ const chunk = getChunk(id);
10977
+ if (!chunk) {
10978
+ error(`Chunk "${id}" not found.`, opts);
10979
+ process.exit(1);
10980
+ }
10981
+ const assignedFiles = safeParseArray(chunk.assigned_files);
10982
+ if (opts.json) {
10983
+ console.log(JSON.stringify({
10984
+ success: true,
10985
+ data: {
10986
+ chunk_id: id,
10987
+ name: chunk.name,
10988
+ patterns: safeParseArray(chunk.patterns),
10989
+ files: assignedFiles,
10990
+ review_mode: chunk.review_mode
10991
+ }
10992
+ }));
10993
+ } else {
10994
+ console.log(`Chunk: ${chunk.name} (${id})`);
10995
+ console.log(`Review mode: ${chunk.review_mode}`);
10996
+ console.log(`
10997
+ Assigned files (${assignedFiles.length}):`);
10998
+ for (const file of assignedFiles) {
10999
+ console.log(` ${file}`);
11000
+ }
11001
+ }
11002
+ });
11003
+ chunkCommand.command("assign").argument("<id>", "Chunk ID").argument("<files...>", "Files to assign").description("Assign additional files to a chunk").action((id, files) => {
11004
+ const opts = getOutputOptions(chunkCommand);
11005
+ const chunk = getChunk(id);
11006
+ if (!chunk) {
11007
+ error(`Chunk "${id}" not found.`, opts);
11008
+ process.exit(1);
11009
+ }
11010
+ try {
11011
+ assignFilesToChunk(id, files);
11012
+ if (opts.json) {
11013
+ const updated = getChunk(id);
11014
+ console.log(JSON.stringify({
11015
+ success: true,
11016
+ data: {
11017
+ chunk_id: id,
11018
+ added: files.length,
11019
+ total: safeParseArray(updated.assigned_files).length
11020
+ }
11021
+ }));
11022
+ } else {
11023
+ success(`Assigned ${files.length} file(s) to chunk "${id}"`, opts);
11024
+ }
11025
+ } catch (err) {
11026
+ error(err instanceof Error ? err.message : "Failed to assign files", opts);
11027
+ process.exit(1);
11028
+ }
11029
+ });
11030
+ chunkCommand.command("list").alias("ls").description("List all chunks in current session").action(() => {
11031
+ const opts = getOutputOptions(chunkCommand);
11032
+ const projectRoot = findProjectRoot();
11033
+ if (!projectRoot) {
11034
+ error('Not in an AIGILE project. Run "aigile init" first.', opts);
11035
+ process.exit(1);
11036
+ }
11037
+ const config = loadProjectConfig(projectRoot);
11038
+ if (!config) {
11039
+ error("Could not load project config.", opts);
11040
+ process.exit(1);
11041
+ }
11042
+ const project = queryOne("SELECT id FROM projects WHERE key = ?", [config.project.key]);
11043
+ if (!project) {
11044
+ error(`Project "${config.project.key}" not found in database.`, opts);
11045
+ process.exit(1);
11046
+ }
11047
+ const session = getActiveSession(project.id);
11048
+ if (!session) {
11049
+ warning2("No active session.", opts);
11050
+ return;
11051
+ }
11052
+ const chunks = getSessionChunks(session.id);
11053
+ if (chunks.length === 0) {
11054
+ warning2("No chunks defined in current session.", opts);
11055
+ return;
11056
+ }
11057
+ data(
11058
+ chunks.map((c) => ({
11059
+ id: c.id,
11060
+ name: c.name,
11061
+ files: safeParseArray(c.assigned_files).length,
11062
+ mode: c.review_mode,
11063
+ created: c.created_at.split("T")[0]
11064
+ })),
11065
+ [
11066
+ { header: "ID", key: "id", width: 15 },
11067
+ { header: "Name", key: "name", width: 25 },
11068
+ { header: "Files", key: "files", width: 8 },
11069
+ { header: "Mode", key: "mode", width: 10 },
11070
+ { header: "Created", key: "created", width: 12 }
11071
+ ],
11072
+ opts
11073
+ );
11074
+ });
11075
+ chunkCommand.command("show").argument("<id>", "Chunk ID").description("Show chunk details").action((id) => {
11076
+ const opts = getOutputOptions(chunkCommand);
11077
+ const chunk = getChunk(id);
11078
+ if (!chunk) {
11079
+ error(`Chunk "${id}" not found.`, opts);
11080
+ process.exit(1);
11081
+ }
11082
+ const assignedFiles = safeParseArray(chunk.assigned_files);
11083
+ const patterns = safeParseArray(chunk.patterns);
11084
+ details(
11085
+ {
11086
+ id: chunk.id,
11087
+ name: chunk.name,
11088
+ review_mode: chunk.review_mode,
11089
+ patterns: patterns.length > 0 ? patterns.join(", ") : "-",
11090
+ assigned_files: assignedFiles.length,
11091
+ session_id: chunk.session_id.slice(0, 8) + "...",
11092
+ created_at: chunk.created_at
11093
+ },
11094
+ [
11095
+ { label: "ID", key: "id" },
11096
+ { label: "Name", key: "name" },
11097
+ { label: "Review Mode", key: "review_mode" },
11098
+ { label: "Patterns", key: "patterns" },
11099
+ { label: "Assigned Files", key: "assigned_files" },
11100
+ { label: "Session", key: "session_id" },
11101
+ { label: "Created", key: "created_at" }
11102
+ ],
11103
+ opts
11104
+ );
11105
+ });
9911
11106
 
9912
11107
  // src/bin/aigile.ts
9913
11108
  async function main() {
9914
- const program = new Command22();
11109
+ const program = new Command23();
9915
11110
  program.name("aigile").description("JIRA-compatible Agile project management CLI for AI-assisted development").version(VERSION, "-v, --version", "Display version number").option("--json", "Output in JSON format for machine parsing").option("--no-color", "Disable colored output");
9916
11111
  program.hook("preAction", async () => {
9917
11112
  await initDatabase();
@@ -9944,6 +11139,7 @@ async function main() {
9944
11139
  program.addCommand(docCommand);
9945
11140
  program.addCommand(daemonCommand);
9946
11141
  program.addCommand(fileCommand);
11142
+ program.addCommand(chunkCommand);
9947
11143
  await program.parseAsync(process.argv);
9948
11144
  }
9949
11145
  main().catch((err) => {