easy-coding-harness 0.10.0-beta.8 → 1.0.0-beta.0

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +72 -1
  2. package/README.md +32 -25
  3. package/dist/cli.js +364 -39
  4. package/dist/cli.js.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/claude/agents/ec-implementer.md +7 -8
  7. package/templates/claude/agents/ec-reviewer.md +14 -2
  8. package/templates/claude/agents/ec-verifier.md +11 -2
  9. package/templates/codex/agents/ec-implementer.toml +7 -8
  10. package/templates/codex/agents/ec-reviewer.toml +14 -2
  11. package/templates/codex/agents/ec-verifier.toml +11 -2
  12. package/templates/common/bundled-skills/ec-init/SKILL.md +1 -1
  13. package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +15 -11
  14. package/templates/common/skills/ec-analysis/SKILL.md +9 -8
  15. package/templates/common/skills/ec-config/SKILL.md +2 -2
  16. package/templates/common/skills/ec-implementing/SKILL.md +38 -31
  17. package/templates/common/skills/ec-lite/SKILL.md +74 -0
  18. package/templates/common/skills/ec-no-harness/SKILL.md +3 -0
  19. package/templates/common/skills/ec-quality/SKILL.md +153 -0
  20. package/templates/common/skills/ec-task-management/SKILL.md +9 -5
  21. package/templates/common/skills/ec-tdd-init/SKILL.md +5 -4
  22. package/templates/common/skills/ec-workflow/SKILL.md +25 -29
  23. package/templates/main-constraint/AGENTS.md.tpl +27 -15
  24. package/templates/main-constraint/CLAUDE.md.tpl +24 -15
  25. package/templates/qoder/agents/ec-implementer.md +7 -8
  26. package/templates/qoder/agents/ec-reviewer.md +14 -2
  27. package/templates/qoder/agents/ec-verifier.md +11 -2
  28. package/templates/runtime/templates/dev-spec-skeleton.md +2 -2
  29. package/templates/shared-hooks/easy_coding_state.py +2961 -372
  30. package/templates/claude/agents/ec-fixer.md +0 -37
  31. package/templates/codex/agents/ec-fixer.toml +0 -26
  32. package/templates/common/skills/ec-reviewing/SKILL.md +0 -109
  33. package/templates/common/skills/ec-verification/SKILL.md +0 -177
  34. package/templates/qoder/agents/ec-fixer.md +0 -37
package/dist/cli.js CHANGED
@@ -292,7 +292,7 @@ async function ensureHookBytecodeIgnored(cwd) {
292
292
 
293
293
  // src/utils/install-manifest.ts
294
294
  import { createHash } from "crypto";
295
- import { readFile as readFile3 } from "fs/promises";
295
+ import { readFile as readFile3, rmdir, unlink } from "fs/promises";
296
296
  import path4 from "path";
297
297
 
298
298
  // src/types/platform.ts
@@ -311,6 +311,7 @@ var PLATFORM_META = {
311
311
  stateInjectEvent: ["SessionStart", "UserPromptSubmit"],
312
312
  hasSubagentContext: true,
313
313
  templateContext: {
314
+ workflow_agent_id: "claude-code",
314
315
  sub_agent_dispatch: "Agent tool",
315
316
  platform_spawn_instruction: 'Use the Agent tool with run_in_background when useful; use isolation: "worktree" for parallel file edits.',
316
317
  skill_trigger: "/",
@@ -334,6 +335,7 @@ var PLATFORM_META = {
334
335
  stateInjectEvent: ["SessionStart", "UserPromptSubmit"],
335
336
  hasSubagentContext: false,
336
337
  templateContext: {
338
+ workflow_agent_id: "codex",
337
339
  sub_agent_dispatch: "Codex sub-agent dispatch",
338
340
  platform_spawn_instruction: "Use Codex sub-agent delegation where available; pass the full task card in the prompt.",
339
341
  skill_trigger: "$",
@@ -358,6 +360,7 @@ var PLATFORM_META = {
358
360
  hasSubagentContext: true,
359
361
  cnVariant: ".qodercn",
360
362
  templateContext: {
363
+ workflow_agent_id: "qoder",
361
364
  sub_agent_dispatch: "Agent tool",
362
365
  platform_spawn_instruction: "Use the Agent tool with worktree isolation for parallel file edits.",
363
366
  skill_trigger: "/",
@@ -483,6 +486,41 @@ async function readInstallManifest(cwd) {
483
486
  return null;
484
487
  }
485
488
  }
489
+ async function pruneRetiredManagedFiles(cwd, previous, artifacts) {
490
+ if (!previous) {
491
+ return { removed: [], preserved: [] };
492
+ }
493
+ const installedPaths = new Set(
494
+ artifacts.filter(
495
+ (artifact) => artifact.type === "file"
496
+ ).map((artifact) => toProjectPath(cwd, artifact.filePath))
497
+ );
498
+ const removed = [];
499
+ const preserved = [];
500
+ for (const file of previous.files) {
501
+ if (installedPaths.has(file.path)) {
502
+ continue;
503
+ }
504
+ const filePath = manifestPath(cwd, file.path);
505
+ if (!await pathExists(filePath)) {
506
+ continue;
507
+ }
508
+ if (!await manifestFileMatches(filePath, file.sha256)) {
509
+ preserved.push(file.path);
510
+ continue;
511
+ }
512
+ await unlink(filePath);
513
+ removed.push(file.path);
514
+ try {
515
+ await rmdir(path4.dirname(filePath));
516
+ } catch (error) {
517
+ if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? "")) {
518
+ throw error;
519
+ }
520
+ }
521
+ }
522
+ return { removed, preserved };
523
+ }
486
524
  async function manifestFileMatches(filePath, sha256) {
487
525
  if (!await pathExists(filePath)) {
488
526
  return false;
@@ -721,7 +759,9 @@ import path7 from "path";
721
759
  var LEGACY_STAGE_MAP = {
722
760
  WAITING_CONFIRM: "ANALYSIS",
723
761
  MEMORY_SHORT: "MEMORY",
724
- MEMORY_LONG: "MEMORY"
762
+ MEMORY_LONG: "MEMORY",
763
+ REVIEW: "QUALITY",
764
+ VERIFICATION: "QUALITY"
725
765
  };
726
766
  function createProjectInitTask(params) {
727
767
  return {
@@ -776,6 +816,92 @@ function isLegacyStage(value) {
776
816
  function migrateStage(value) {
777
817
  return isLegacyStage(value) ? LEGACY_STAGE_MAP[value] : value;
778
818
  }
819
+ var LEGACY_DISPLAY_AGENT_IDENTITIES = {
820
+ "claude with easy coding": "claude-code",
821
+ "claude-code with easy coding": "claude-code",
822
+ "claude code with easy coding": "claude-code",
823
+ "codex with easy coding": "codex",
824
+ "qoder with easy coding": "qoder"
825
+ };
826
+ function migratedAgentIdentity(value) {
827
+ if (typeof value !== "string") return void 0;
828
+ const normalized = value.trim().toLowerCase();
829
+ if (normalized === "claude-code" || normalized === "codex" || normalized === "qoder") {
830
+ return normalized;
831
+ }
832
+ if (/^\/?root(?:\/[a-z0-9._-]+)*$/.test(normalized)) return "codex";
833
+ return LEGACY_DISPLAY_AGENT_IDENTITIES[normalized];
834
+ }
835
+ function migrateAgentFields(record, fields) {
836
+ let changed = false;
837
+ for (const field of fields) {
838
+ const migrated = migratedAgentIdentity(record[field]);
839
+ if (migrated && migrated !== record[field]) {
840
+ record[field] = migrated;
841
+ changed = true;
842
+ }
843
+ }
844
+ return changed;
845
+ }
846
+ function taskAgentFieldGroups(task) {
847
+ const groups = [
848
+ {
849
+ record: task,
850
+ fields: ["created_by", "last_agent", "workflow_mode_confirmed_by", "tdd_confirmed_by"]
851
+ }
852
+ ];
853
+ for (const field of [
854
+ "pending_transition",
855
+ "workflow_mode_proposal",
856
+ "quality_checkpoint",
857
+ "verification_checkpoint"
858
+ ]) {
859
+ const value = task[field];
860
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
861
+ const identityField = field === "pending_transition" ? "requested_by" : field === "workflow_mode_proposal" ? "proposed_by" : "recorded_by";
862
+ groups.push({ record: value, fields: [identityField] });
863
+ }
864
+ if (Array.isArray(task.workflow_mode_escalations)) {
865
+ for (const escalation of task.workflow_mode_escalations) {
866
+ if (!escalation || typeof escalation !== "object" || Array.isArray(escalation)) continue;
867
+ groups.push({ record: escalation, fields: ["raised_by"] });
868
+ }
869
+ }
870
+ const memoryProgress = task.memory_progress;
871
+ if (memoryProgress && typeof memoryProgress === "object" && !Array.isArray(memoryProgress)) {
872
+ const assessment = memoryProgress.architecture_assessment;
873
+ if (assessment && typeof assessment === "object" && !Array.isArray(assessment)) {
874
+ groups.push({ record: assessment, fields: ["recorded_by"] });
875
+ }
876
+ }
877
+ if (Array.isArray(task.spec_dependency_evidence)) {
878
+ for (const evidence of task.spec_dependency_evidence) {
879
+ if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) continue;
880
+ groups.push({ record: evidence, fields: ["satisfied_by"] });
881
+ }
882
+ }
883
+ if (Array.isArray(task.stage_history)) {
884
+ for (const entry of task.stage_history) {
885
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
886
+ groups.push({ record: entry, fields: ["agent"] });
887
+ }
888
+ }
889
+ return groups;
890
+ }
891
+ function migrateTaskAgentIdentities(task) {
892
+ return taskAgentFieldGroups(task).reduce(
893
+ (changed, group) => migrateAgentFields(group.record, group.fields) || changed,
894
+ false
895
+ );
896
+ }
897
+ function hasLegacyTaskAgentIdentities(task) {
898
+ return taskAgentFieldGroups(task).some(
899
+ ({ record, fields }) => fields.some((field) => {
900
+ const migrated = migratedAgentIdentity(record[field]);
901
+ return migrated !== void 0 && migrated !== record[field];
902
+ })
903
+ );
904
+ }
779
905
  function migrateStageHistory(task) {
780
906
  if (!Array.isArray(task.stage_history)) return false;
781
907
  let changed = false;
@@ -806,11 +932,32 @@ function migrateTaskWorkflowState(task) {
806
932
  const legacyRequestedAt = Array.isArray(task.stage_history) ? [...task.stage_history].reverse().find(
807
933
  (entry) => entry && typeof entry === "object" && !Array.isArray(entry) && entry.stage === "WAITING_CONFIRM"
808
934
  ) : null;
809
- let changed = migrateStageHistory(task);
935
+ let changed = migrateTaskAgentIdentities(task);
936
+ changed = migrateStageHistory(task) || changed;
810
937
  if (legacyStatus) {
811
938
  task.status = LEGACY_STAGE_MAP[legacyStatus];
812
939
  changed = true;
813
940
  }
941
+ const pending = task.pending_transition;
942
+ if (pending && typeof pending === "object" && !Array.isArray(pending)) {
943
+ const record = pending;
944
+ const from = migrateStage(record.from);
945
+ const to = migrateStage(record.to);
946
+ if (from === to) {
947
+ task.pending_transition = void 0;
948
+ changed = true;
949
+ } else if (from !== record.from || to !== record.to) {
950
+ task.pending_transition = { ...record, from, to };
951
+ changed = true;
952
+ }
953
+ }
954
+ if (!task.quality_checkpoint && task.verification_checkpoint && typeof task.verification_checkpoint === "object" && !Array.isArray(task.verification_checkpoint)) {
955
+ task.quality_checkpoint = task.verification_checkpoint;
956
+ }
957
+ if ("verification_checkpoint" in task) {
958
+ task.verification_checkpoint = void 0;
959
+ changed = true;
960
+ }
814
961
  if (legacyStatus === "WAITING_CONFIRM" && !task.pending_transition) {
815
962
  const requestedAt = legacyRequestedAt && typeof legacyRequestedAt === "object" && !Array.isArray(legacyRequestedAt) ? String(
816
963
  legacyRequestedAt.entered_at ?? (/* @__PURE__ */ new Date()).toISOString()
@@ -819,7 +966,7 @@ function migrateTaskWorkflowState(task) {
819
966
  from: "ANALYSIS",
820
967
  to: "IMPLEMENT",
821
968
  requested_at: requestedAt,
822
- requested_by: String(task.last_agent ?? "upgrade-migration"),
969
+ requested_by: migratedAgentIdentity(task.last_agent) ?? "upgrade-migration",
823
970
  reason: "migrated-from-WAITING_CONFIRM"
824
971
  };
825
972
  changed = true;
@@ -836,9 +983,25 @@ function migrateTaskWorkflowState(task) {
836
983
  }
837
984
  const status2 = String(task.status ?? "");
838
985
  const taskType = String(task.type ?? "").toLowerCase();
839
- const isActive = !["", "PENDING", "COMPLETE", "CLOSED"].includes(status2);
986
+ let isActive = !["", "PENDING", "COMPLETE", "CLOSED"].includes(status2);
987
+ if (isActive && ["analysis", "doc", "report"].includes(taskType)) {
988
+ task.status = "CLOSED";
989
+ task.closed_reason = "legacy-read-only-task-retired";
990
+ task.pending_transition = void 0;
991
+ task.stage_history = [
992
+ ...Array.isArray(task.stage_history) ? task.stage_history : [],
993
+ {
994
+ stage: "CLOSED",
995
+ agent: "upgrade-migration",
996
+ entered_at: (/* @__PURE__ */ new Date()).toISOString()
997
+ }
998
+ ];
999
+ task.last_agent = "upgrade-migration";
1000
+ isActive = false;
1001
+ changed = true;
1002
+ }
840
1003
  if (isActive && taskType !== "project-init" && !["fast", "standard", "strict"].includes(String(task.workflow_mode ?? ""))) {
841
- task.workflow_mode = ["analysis", "doc", "report"].includes(taskType) ? "fast" : "strict";
1004
+ task.workflow_mode = "standard";
842
1005
  task.workflow_mode_confirmed_at = (/* @__PURE__ */ new Date()).toISOString();
843
1006
  task.workflow_mode_confirmed_by = "upgrade-migration";
844
1007
  task.workflow_mode_legacy = true;
@@ -858,7 +1021,7 @@ function migrateTaskWorkflowState(task) {
858
1021
  return changed;
859
1022
  }
860
1023
  function migrateSessionBehavior(session) {
861
- let changed = false;
1024
+ let changed = migrateAgentFields(session, ["agent", "last_agent"]);
862
1025
  const legacyMode = session.confirm_mode;
863
1026
  const legacyLite = legacyMode === "lite";
864
1027
  if (!["approve", "guard", "confirm", "auto"].includes(String(session.approval_mode ?? ""))) {
@@ -910,8 +1073,7 @@ function sessionMigrationCandidate(session) {
910
1073
  currentTask: session.current_task,
911
1074
  agent: typeof session.agent === "string" ? session.agent : void 0,
912
1075
  lastAgent: typeof session.last_agent === "string" ? session.last_agent : void 0,
913
- workflowMode,
914
- legacyLite: session.workflow_mode_legacy_confirm_override === true
1076
+ workflowMode
915
1077
  };
916
1078
  }
917
1079
  function relevantSessionMigrationCandidates(task, candidates) {
@@ -925,25 +1087,15 @@ function migrationWorkflowMode(task, candidates, projectWorkflowMode) {
925
1087
  const taskType = String(task.type ?? "").toLowerCase();
926
1088
  if (["analysis", "doc", "report"].includes(taskType)) return "fast";
927
1089
  const relevantCandidates = relevantSessionMigrationCandidates(task, candidates);
928
- const fallbackMode = projectWorkflowMode === "adaptive" ? "strict" : projectWorkflowMode;
1090
+ const fallbackMode = projectWorkflowMode === "adaptive" ? "standard" : projectWorkflowMode;
929
1091
  const concreteModes = relevantCandidates.map(
930
- (candidate) => candidate.workflowMode === "adaptive" ? "strict" : candidate.workflowMode ?? fallbackMode
1092
+ (candidate) => candidate.workflowMode === "adaptive" ? "standard" : candidate.workflowMode ?? fallbackMode
931
1093
  );
932
1094
  if (concreteModes.length === 0) return fallbackMode;
933
1095
  return concreteModes.reduce(
934
1096
  (highest, mode) => WORKFLOW_MODE_RANK[mode] > WORKFLOW_MODE_RANK[highest] ? mode : highest
935
1097
  );
936
1098
  }
937
- function preserveLegacyDirectEdge(task, candidates, projectLegacyLite) {
938
- if (task.status !== "IMPLEMENT") return false;
939
- const pending = task.pending_transition && typeof task.pending_transition === "object" && !Array.isArray(task.pending_transition) ? task.pending_transition : null;
940
- if (pending?.from === "IMPLEMENT" && pending.to === "VERIFICATION") return true;
941
- const relevantCandidates = relevantSessionMigrationCandidates(task, candidates);
942
- if (relevantCandidates.length > 0) {
943
- return relevantCandidates.every((candidate) => candidate.legacyLite);
944
- }
945
- return projectLegacyLite;
946
- }
947
1099
  async function taskFiles(cwd) {
948
1100
  const tasksDir = path7.join(cwd, EASY_CODING_DIR, TASKS_DIR);
949
1101
  if (!await pathExists(tasksDir)) return [];
@@ -969,7 +1121,7 @@ async function hasLegacyWorkflowState(cwd) {
969
1121
  if (!await pathExists(filePath)) continue;
970
1122
  const task = await readJsonRecord(filePath);
971
1123
  if (!task) continue;
972
- if (isLegacyStage(task.status) || !["PENDING", "COMPLETE", "CLOSED"].includes(String(task.status ?? "")) && String(task.type ?? "") !== "project-init" && !["fast", "standard", "strict"].includes(String(task.workflow_mode ?? "")) || Array.isArray(task.stage_history) && task.stage_history.some(
1124
+ if (hasLegacyTaskAgentIdentities(task) || isLegacyStage(task.status) || "verification_checkpoint" in task || !["PENDING", "COMPLETE", "CLOSED"].includes(String(task.status ?? "")) && ["analysis", "doc", "report"].includes(String(task.type ?? "").toLowerCase()) || !["PENDING", "COMPLETE", "CLOSED"].includes(String(task.status ?? "")) && String(task.type ?? "") !== "project-init" && !["fast", "standard", "strict"].includes(String(task.workflow_mode ?? "")) || Array.isArray(task.stage_history) && task.stage_history.some(
973
1125
  (entry) => entry && typeof entry === "object" && !Array.isArray(entry) && isLegacyStage(entry.stage)
974
1126
  )) {
975
1127
  return true;
@@ -978,7 +1130,12 @@ async function hasLegacyWorkflowState(cwd) {
978
1130
  for (const filePath of await sessionFiles(cwd)) {
979
1131
  const session = await readJsonRecord(filePath);
980
1132
  if (!session) continue;
981
- if (isLegacyStage(session.last_seen_stage) || "confirm_mode" in session) return true;
1133
+ if (["agent", "last_agent"].some((field) => {
1134
+ const migrated = migratedAgentIdentity(session[field]);
1135
+ return migrated !== void 0 && migrated !== session[field];
1136
+ }) || isLegacyStage(session.last_seen_stage) || "confirm_mode" in session) {
1137
+ return true;
1138
+ }
982
1139
  }
983
1140
  return false;
984
1141
  }
@@ -987,14 +1144,11 @@ async function migrateLegacyWorkflowState(cwd) {
987
1144
  let sessionsUpdated = 0;
988
1145
  const updatedTaskPaths = /* @__PURE__ */ new Set();
989
1146
  let projectWorkflowMode = "adaptive";
990
- let projectLegacyLite = false;
991
1147
  const configPath2 = path7.join(cwd, EASY_CODING_DIR, "config.yaml");
992
1148
  if (await pathExists(configPath2)) {
993
1149
  try {
994
1150
  const config2 = await readConfigYaml(configPath2);
995
1151
  projectWorkflowMode = resolveLegacyBehavior(config2).workflowMode;
996
- const behavior = config2.behavior ?? {};
997
- projectLegacyLite = behavior.confirm_mode === "lite";
998
1152
  } catch {
999
1153
  }
1000
1154
  }
@@ -1018,6 +1172,15 @@ async function migrateLegacyWorkflowState(cwd) {
1018
1172
  changed = true;
1019
1173
  }
1020
1174
  changed = migrateSessionBehavior(session) || changed;
1175
+ if (typeof session.current_task === "string") {
1176
+ const task = await readJsonRecord(getTaskJsonPath(cwd, session.current_task));
1177
+ if (task && ["COMPLETE", "CLOSED"].includes(String(task.status ?? ""))) {
1178
+ session.current_task = null;
1179
+ session.last_seen_task = null;
1180
+ session.last_seen_stage = "idle";
1181
+ changed = true;
1182
+ }
1183
+ }
1021
1184
  const candidate = sessionMigrationCandidate(session);
1022
1185
  if (candidate) sessionCandidates.push(candidate);
1023
1186
  if (changed) {
@@ -1048,15 +1211,7 @@ async function migrateLegacyWorkflowState(cwd) {
1048
1211
  task.workflow_mode_confirmed_at = (/* @__PURE__ */ new Date()).toISOString();
1049
1212
  changed = true;
1050
1213
  }
1051
- const keepDirectEdge = preserveLegacyDirectEdge(
1052
- task,
1053
- candidatesByTask.get(taskId) ?? [],
1054
- projectLegacyLite
1055
- );
1056
- if (keepDirectEdge && task.workflow_mode_legacy_direct_edge !== true) {
1057
- task.workflow_mode_legacy_direct_edge = true;
1058
- changed = true;
1059
- } else if (!keepDirectEdge && "workflow_mode_legacy_direct_edge" in task) {
1214
+ if ("workflow_mode_legacy_direct_edge" in task) {
1060
1215
  task.workflow_mode_legacy_direct_edge = void 0;
1061
1216
  changed = true;
1062
1217
  }
@@ -2120,6 +2275,16 @@ async function addAgent(opts) {
2120
2275
  throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
2121
2276
  }
2122
2277
  const platforms = await resolvePlatforms(opts, ["claude-code"]);
2278
+ for (const target of targets) {
2279
+ if (!await pathExists(target.configPath)) continue;
2280
+ const config2 = await readConfigYaml(target.configPath);
2281
+ const installedVersion = String(config2.harness_version ?? "");
2282
+ if (installedVersion !== VERSION) {
2283
+ throw new Error(
2284
+ `${target.label}: installed harness ${installedVersion || "unknown"} does not match CLI ${VERSION}. Run easy-coding upgrade before add-agent.`
2285
+ );
2286
+ }
2287
+ }
2123
2288
  const installedLabels = [];
2124
2289
  const refreshedLabels = [];
2125
2290
  for (const target of targets) {
@@ -3052,7 +3217,7 @@ async function config() {
3052
3217
  {
3053
3218
  value: "guard",
3054
3219
  label: "guard \u2014 confirm critical gates (default)",
3055
- hint: "ANALYSIS -> IMPLEMENT and VERIFICATION -> MEMORY"
3220
+ hint: "ANALYSIS -> IMPLEMENT and QUALITY -> MEMORY"
3056
3221
  },
3057
3222
  {
3058
3223
  value: "confirm",
@@ -3397,12 +3562,16 @@ import path23 from "path";
3397
3562
  import chalk6 from "chalk";
3398
3563
 
3399
3564
  // src/utils/session.ts
3400
- import { readdir as readdir7, unlink } from "fs/promises";
3565
+ import { readdir as readdir7, stat as stat3, unlink as unlink2 } from "fs/promises";
3401
3566
  import path22 from "path";
3402
- var STALE_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1e3;
3567
+ var DAY_MS = 24 * 60 * 60 * 1e3;
3568
+ var IDLE_SESSION_RETENTION_MS = 7 * DAY_MS;
3569
+ var ATTACHED_SESSION_RETENTION_MS = 30 * DAY_MS;
3570
+ var MAX_SESSION_FILES = 100;
3403
3571
  function parseSessionFile(content) {
3404
3572
  try {
3405
- return JSON.parse(content);
3573
+ const parsed = JSON.parse(content);
3574
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
3406
3575
  } catch {
3407
3576
  return null;
3408
3577
  }
@@ -3437,6 +3606,138 @@ async function listSessionFiles(cwd) {
3437
3606
  }
3438
3607
  return entries.sort((left, right) => left.key.localeCompare(right.key));
3439
3608
  }
3609
+ async function cleanSessionRuntime(cwd, options = {}) {
3610
+ const now = Date.now();
3611
+ const idleRetentionMs = options.idleRetentionMs ?? IDLE_SESSION_RETENTION_MS;
3612
+ const attachedRetentionMs = options.attachedRetentionMs ?? ATTACHED_SESSION_RETENTION_MS;
3613
+ const maxSessions = options.maxSessions ?? MAX_SESSION_FILES;
3614
+ const reserveSlots = options.reserveSlots ?? 0;
3615
+ const candidates = await listSessionCleanupCandidates(cwd);
3616
+ const removed = /* @__PURE__ */ new Set();
3617
+ for (const candidate of candidates) {
3618
+ const retentionMs = candidate.attached ? attachedRetentionMs : idleRetentionMs;
3619
+ if (now - candidate.activityTime <= retentionMs) {
3620
+ continue;
3621
+ }
3622
+ if (await unlinkIfUnchanged(candidate)) {
3623
+ removed.add(candidate.filePath);
3624
+ }
3625
+ }
3626
+ const allowedExistingSessions = Math.max(0, maxSessions - reserveSlots);
3627
+ const remaining = candidates.filter((candidate) => !removed.has(candidate.filePath)).sort((left, right) => left.activityTime - right.activityTime);
3628
+ for (const candidate of remaining.slice(
3629
+ 0,
3630
+ Math.max(0, remaining.length - allowedExistingSessions)
3631
+ )) {
3632
+ if (await unlinkIfUnchanged(candidate)) {
3633
+ removed.add(candidate.filePath);
3634
+ }
3635
+ }
3636
+ return {
3637
+ sessionsRemoved: removed.size,
3638
+ acceptanceSnapshotsRemoved: await cleanOrphanAcceptanceSnapshots(cwd)
3639
+ };
3640
+ }
3641
+ async function listSessionCleanupCandidates(cwd) {
3642
+ const dir = getSessionDir(cwd);
3643
+ if (!await pathExists(dir)) {
3644
+ return [];
3645
+ }
3646
+ const candidates = [];
3647
+ for (const entry of await readdir7(dir, { withFileTypes: true })) {
3648
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
3649
+ continue;
3650
+ }
3651
+ const filePath = path22.join(dir, entry.name);
3652
+ try {
3653
+ const [content, fileStat] = await Promise.all([readTextFile(filePath), stat3(filePath)]);
3654
+ const session = parseSessionFile(content);
3655
+ const activityValue = session?.last_active_at ?? session?.created_at;
3656
+ const parsedActivity = typeof activityValue === "string" ? new Date(activityValue).getTime() : Number.NaN;
3657
+ candidates.push({
3658
+ filePath,
3659
+ content,
3660
+ activityTime: Number.isNaN(parsedActivity) ? fileStat.mtimeMs : parsedActivity,
3661
+ attached: Boolean(session?.current_task)
3662
+ });
3663
+ } catch (error) {
3664
+ if (!isFileNotFound(error)) {
3665
+ throw error;
3666
+ }
3667
+ }
3668
+ }
3669
+ return candidates;
3670
+ }
3671
+ async function unlinkIfUnchanged(candidate) {
3672
+ try {
3673
+ if (await readTextFile(candidate.filePath) !== candidate.content) {
3674
+ return false;
3675
+ }
3676
+ await unlink2(candidate.filePath);
3677
+ return true;
3678
+ } catch (error) {
3679
+ if (isFileNotFound(error)) {
3680
+ return false;
3681
+ }
3682
+ throw error;
3683
+ }
3684
+ }
3685
+ async function cleanOrphanAcceptanceSnapshots(cwd) {
3686
+ const acceptanceDir = path22.join(getSessionDir(cwd), "acceptance");
3687
+ if (!await pathExists(acceptanceDir)) {
3688
+ return 0;
3689
+ }
3690
+ let removed = 0;
3691
+ for (const entry of await readdir7(acceptanceDir, { withFileTypes: true })) {
3692
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
3693
+ continue;
3694
+ }
3695
+ const snapshotPath = path22.join(acceptanceDir, entry.name);
3696
+ if (!await isOrphanAcceptanceSnapshot(cwd, snapshotPath, entry.name.slice(0, -5))) {
3697
+ continue;
3698
+ }
3699
+ try {
3700
+ await unlink2(snapshotPath);
3701
+ removed++;
3702
+ } catch (error) {
3703
+ if (!isFileNotFound(error)) {
3704
+ throw error;
3705
+ }
3706
+ }
3707
+ }
3708
+ return removed;
3709
+ }
3710
+ async function isOrphanAcceptanceSnapshot(cwd, snapshotPath, taskId) {
3711
+ let taskContent;
3712
+ try {
3713
+ taskContent = await readTextFile(
3714
+ path22.join(cwd, EASY_CODING_DIR, TASKS_DIR, taskId, "task.json")
3715
+ );
3716
+ } catch (error) {
3717
+ if (isFileNotFound(error)) {
3718
+ return true;
3719
+ }
3720
+ throw error;
3721
+ }
3722
+ let parsedTask;
3723
+ try {
3724
+ parsedTask = JSON.parse(taskContent);
3725
+ } catch {
3726
+ return false;
3727
+ }
3728
+ if (typeof parsedTask !== "object" || parsedTask === null || Array.isArray(parsedTask)) {
3729
+ return false;
3730
+ }
3731
+ const task = parsedTask;
3732
+ if (task.status === "COMPLETE" || task.status === "CLOSED") {
3733
+ return true;
3734
+ }
3735
+ const checkpoint = task.quality_checkpoint ?? task.verification_checkpoint;
3736
+ return typeof checkpoint?.snapshot_file !== "string" || path22.resolve(cwd, checkpoint.snapshot_file) !== path22.resolve(snapshotPath);
3737
+ }
3738
+ function isFileNotFound(error) {
3739
+ return error.code === "ENOENT";
3740
+ }
3440
3741
 
3441
3742
  // src/commands/status.ts
3442
3743
  async function status() {
@@ -3518,6 +3819,10 @@ async function status() {
3518
3819
  console.log(
3519
3820
  ` harness: ${session.harness_disabled ? "disabled for this session" : "enabled"}`
3520
3821
  );
3822
+ console.log(` lite_mode: ${session.lite_mode === true ? "enabled" : "disabled"}`);
3823
+ if (session.lite_proposal) {
3824
+ console.log(` lite_proposal: ${session.lite_proposal.digest}`);
3825
+ }
3521
3826
  if (!session.current_task) {
3522
3827
  console.log(" current_task: none");
3523
3828
  continue;
@@ -3646,8 +3951,11 @@ async function upgrade(opts) {
3646
3951
  ...childTopologyRefreshes.map(({ target }) => `- ${target.label}: role: submodule-child`)
3647
3952
  ].filter(Boolean) : [],
3648
3953
  "Will overwrite managed skills, hooks, agents, templates, and generated main-constraint regions.",
3954
+ "Will remove retired files that still match the previous install manifest and preserve locally modified copies.",
3649
3955
  "Will update project-init task to recommend ec-init re-run for version adaptation.",
3650
3956
  "Will migrate behavior config to schema 5 and disable unready project/session TDD settings.",
3957
+ "Will prune expired session bindings and orphan acceptance snapshots in each upgraded target while preserving tasks, memory, spec, and project knowledge.",
3958
+ "Will migrate active REVIEW/VERIFICATION tasks to QUALITY, retire active read-only task types as CLOSED, and preserve their artifacts and history.",
3651
3959
  "Will migrate legacy workflow/TDD task metadata; memory content, spec, and project knowledge files remain untouched."
3652
3960
  ].join("\n");
3653
3961
  if (opts.dryRun) {
@@ -3665,6 +3973,15 @@ async function upgrade(opts) {
3665
3973
  }
3666
3974
  }
3667
3975
  for (const { target, config: config2 } of pending) {
3976
+ const previousManifest = await readInstallManifest(target.dir);
3977
+ const sessionCleanup = await cleanSessionRuntime(target.dir);
3978
+ if (sessionCleanup.sessionsRemoved > 0 || sessionCleanup.acceptanceSnapshotsRemoved > 0) {
3979
+ console.log(
3980
+ chalk8.yellow(
3981
+ `${target.label}: session GC removed ${sessionCleanup.sessionsRemoved} session file(s) and ${sessionCleanup.acceptanceSnapshotsRemoved} orphan acceptance snapshot(s).`
3982
+ )
3983
+ );
3984
+ }
3668
3985
  const beta1ProjectTddRequested = Number(config2.version) === 4 && config2.behavior?.tdd_enabled === true;
3669
3986
  const projectId = await writeRuntimeScaffold(target.dir, config2.agents, {
3670
3987
  supermodule: target.supermodule
@@ -3673,6 +3990,14 @@ async function upgrade(opts) {
3673
3990
  supermodule: target.boundary,
3674
3991
  projectId
3675
3992
  });
3993
+ const retiredFiles = await pruneRetiredManagedFiles(target.dir, previousManifest, artifacts);
3994
+ if (retiredFiles.removed.length > 0 || retiredFiles.preserved.length > 0) {
3995
+ console.log(
3996
+ chalk8.yellow(
3997
+ `${target.label}: removed ${retiredFiles.removed.length} retired managed file(s) and preserved ${retiredFiles.preserved.length} locally modified file(s).`
3998
+ )
3999
+ );
4000
+ }
3676
4001
  await writeInstallManifest(target.dir, {
3677
4002
  harnessVersion: VERSION,
3678
4003
  agents: config2.agents,