easy-coding-harness 0.10.0-beta.9 → 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 +56 -1
  2. package/README.md +32 -25
  3. package/dist/cli.js +272 -37
  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 +35 -30
  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 +22 -29
  23. package/templates/main-constraint/AGENTS.md.tpl +19 -13
  24. package/templates/main-constraint/CLAUDE.md.tpl +19 -13
  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 +2793 -318
  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
@@ -486,6 +486,41 @@ async function readInstallManifest(cwd) {
486
486
  return null;
487
487
  }
488
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
+ }
489
524
  async function manifestFileMatches(filePath, sha256) {
490
525
  if (!await pathExists(filePath)) {
491
526
  return false;
@@ -724,7 +759,9 @@ import path7 from "path";
724
759
  var LEGACY_STAGE_MAP = {
725
760
  WAITING_CONFIRM: "ANALYSIS",
726
761
  MEMORY_SHORT: "MEMORY",
727
- MEMORY_LONG: "MEMORY"
762
+ MEMORY_LONG: "MEMORY",
763
+ REVIEW: "QUALITY",
764
+ VERIFICATION: "QUALITY"
728
765
  };
729
766
  function createProjectInitTask(params) {
730
767
  return {
@@ -813,7 +850,12 @@ function taskAgentFieldGroups(task) {
813
850
  fields: ["created_by", "last_agent", "workflow_mode_confirmed_by", "tdd_confirmed_by"]
814
851
  }
815
852
  ];
816
- for (const field of ["pending_transition", "workflow_mode_proposal", "verification_checkpoint"]) {
853
+ for (const field of [
854
+ "pending_transition",
855
+ "workflow_mode_proposal",
856
+ "quality_checkpoint",
857
+ "verification_checkpoint"
858
+ ]) {
817
859
  const value = task[field];
818
860
  if (!value || typeof value !== "object" || Array.isArray(value)) continue;
819
861
  const identityField = field === "pending_transition" ? "requested_by" : field === "workflow_mode_proposal" ? "proposed_by" : "recorded_by";
@@ -896,6 +938,26 @@ function migrateTaskWorkflowState(task) {
896
938
  task.status = LEGACY_STAGE_MAP[legacyStatus];
897
939
  changed = true;
898
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
+ }
899
961
  if (legacyStatus === "WAITING_CONFIRM" && !task.pending_transition) {
900
962
  const requestedAt = legacyRequestedAt && typeof legacyRequestedAt === "object" && !Array.isArray(legacyRequestedAt) ? String(
901
963
  legacyRequestedAt.entered_at ?? (/* @__PURE__ */ new Date()).toISOString()
@@ -904,7 +966,7 @@ function migrateTaskWorkflowState(task) {
904
966
  from: "ANALYSIS",
905
967
  to: "IMPLEMENT",
906
968
  requested_at: requestedAt,
907
- requested_by: String(task.last_agent ?? "upgrade-migration"),
969
+ requested_by: migratedAgentIdentity(task.last_agent) ?? "upgrade-migration",
908
970
  reason: "migrated-from-WAITING_CONFIRM"
909
971
  };
910
972
  changed = true;
@@ -921,9 +983,25 @@ function migrateTaskWorkflowState(task) {
921
983
  }
922
984
  const status2 = String(task.status ?? "");
923
985
  const taskType = String(task.type ?? "").toLowerCase();
924
- 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
+ }
925
1003
  if (isActive && taskType !== "project-init" && !["fast", "standard", "strict"].includes(String(task.workflow_mode ?? ""))) {
926
- task.workflow_mode = ["analysis", "doc", "report"].includes(taskType) ? "fast" : "strict";
1004
+ task.workflow_mode = "standard";
927
1005
  task.workflow_mode_confirmed_at = (/* @__PURE__ */ new Date()).toISOString();
928
1006
  task.workflow_mode_confirmed_by = "upgrade-migration";
929
1007
  task.workflow_mode_legacy = true;
@@ -995,8 +1073,7 @@ function sessionMigrationCandidate(session) {
995
1073
  currentTask: session.current_task,
996
1074
  agent: typeof session.agent === "string" ? session.agent : void 0,
997
1075
  lastAgent: typeof session.last_agent === "string" ? session.last_agent : void 0,
998
- workflowMode,
999
- legacyLite: session.workflow_mode_legacy_confirm_override === true
1076
+ workflowMode
1000
1077
  };
1001
1078
  }
1002
1079
  function relevantSessionMigrationCandidates(task, candidates) {
@@ -1010,25 +1087,15 @@ function migrationWorkflowMode(task, candidates, projectWorkflowMode) {
1010
1087
  const taskType = String(task.type ?? "").toLowerCase();
1011
1088
  if (["analysis", "doc", "report"].includes(taskType)) return "fast";
1012
1089
  const relevantCandidates = relevantSessionMigrationCandidates(task, candidates);
1013
- const fallbackMode = projectWorkflowMode === "adaptive" ? "strict" : projectWorkflowMode;
1090
+ const fallbackMode = projectWorkflowMode === "adaptive" ? "standard" : projectWorkflowMode;
1014
1091
  const concreteModes = relevantCandidates.map(
1015
- (candidate) => candidate.workflowMode === "adaptive" ? "strict" : candidate.workflowMode ?? fallbackMode
1092
+ (candidate) => candidate.workflowMode === "adaptive" ? "standard" : candidate.workflowMode ?? fallbackMode
1016
1093
  );
1017
1094
  if (concreteModes.length === 0) return fallbackMode;
1018
1095
  return concreteModes.reduce(
1019
1096
  (highest, mode) => WORKFLOW_MODE_RANK[mode] > WORKFLOW_MODE_RANK[highest] ? mode : highest
1020
1097
  );
1021
1098
  }
1022
- function preserveLegacyDirectEdge(task, candidates, projectLegacyLite) {
1023
- if (task.status !== "IMPLEMENT") return false;
1024
- const pending = task.pending_transition && typeof task.pending_transition === "object" && !Array.isArray(task.pending_transition) ? task.pending_transition : null;
1025
- if (pending?.from === "IMPLEMENT" && pending.to === "VERIFICATION") return true;
1026
- const relevantCandidates = relevantSessionMigrationCandidates(task, candidates);
1027
- if (relevantCandidates.length > 0) {
1028
- return relevantCandidates.every((candidate) => candidate.legacyLite);
1029
- }
1030
- return projectLegacyLite;
1031
- }
1032
1099
  async function taskFiles(cwd) {
1033
1100
  const tasksDir = path7.join(cwd, EASY_CODING_DIR, TASKS_DIR);
1034
1101
  if (!await pathExists(tasksDir)) return [];
@@ -1054,7 +1121,7 @@ async function hasLegacyWorkflowState(cwd) {
1054
1121
  if (!await pathExists(filePath)) continue;
1055
1122
  const task = await readJsonRecord(filePath);
1056
1123
  if (!task) continue;
1057
- if (hasLegacyTaskAgentIdentities(task) || 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(
1058
1125
  (entry) => entry && typeof entry === "object" && !Array.isArray(entry) && isLegacyStage(entry.stage)
1059
1126
  )) {
1060
1127
  return true;
@@ -1077,14 +1144,11 @@ async function migrateLegacyWorkflowState(cwd) {
1077
1144
  let sessionsUpdated = 0;
1078
1145
  const updatedTaskPaths = /* @__PURE__ */ new Set();
1079
1146
  let projectWorkflowMode = "adaptive";
1080
- let projectLegacyLite = false;
1081
1147
  const configPath2 = path7.join(cwd, EASY_CODING_DIR, "config.yaml");
1082
1148
  if (await pathExists(configPath2)) {
1083
1149
  try {
1084
1150
  const config2 = await readConfigYaml(configPath2);
1085
1151
  projectWorkflowMode = resolveLegacyBehavior(config2).workflowMode;
1086
- const behavior = config2.behavior ?? {};
1087
- projectLegacyLite = behavior.confirm_mode === "lite";
1088
1152
  } catch {
1089
1153
  }
1090
1154
  }
@@ -1108,6 +1172,15 @@ async function migrateLegacyWorkflowState(cwd) {
1108
1172
  changed = true;
1109
1173
  }
1110
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
+ }
1111
1184
  const candidate = sessionMigrationCandidate(session);
1112
1185
  if (candidate) sessionCandidates.push(candidate);
1113
1186
  if (changed) {
@@ -1138,15 +1211,7 @@ async function migrateLegacyWorkflowState(cwd) {
1138
1211
  task.workflow_mode_confirmed_at = (/* @__PURE__ */ new Date()).toISOString();
1139
1212
  changed = true;
1140
1213
  }
1141
- const keepDirectEdge = preserveLegacyDirectEdge(
1142
- task,
1143
- candidatesByTask.get(taskId) ?? [],
1144
- projectLegacyLite
1145
- );
1146
- if (keepDirectEdge && task.workflow_mode_legacy_direct_edge !== true) {
1147
- task.workflow_mode_legacy_direct_edge = true;
1148
- changed = true;
1149
- } else if (!keepDirectEdge && "workflow_mode_legacy_direct_edge" in task) {
1214
+ if ("workflow_mode_legacy_direct_edge" in task) {
1150
1215
  task.workflow_mode_legacy_direct_edge = void 0;
1151
1216
  changed = true;
1152
1217
  }
@@ -2210,6 +2275,16 @@ async function addAgent(opts) {
2210
2275
  throw new Error("No .easy-coding/config.yaml found. Run easy-coding init first.");
2211
2276
  }
2212
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
+ }
2213
2288
  const installedLabels = [];
2214
2289
  const refreshedLabels = [];
2215
2290
  for (const target of targets) {
@@ -3142,7 +3217,7 @@ async function config() {
3142
3217
  {
3143
3218
  value: "guard",
3144
3219
  label: "guard \u2014 confirm critical gates (default)",
3145
- hint: "ANALYSIS -> IMPLEMENT and VERIFICATION -> MEMORY"
3220
+ hint: "ANALYSIS -> IMPLEMENT and QUALITY -> MEMORY"
3146
3221
  },
3147
3222
  {
3148
3223
  value: "confirm",
@@ -3487,12 +3562,16 @@ import path23 from "path";
3487
3562
  import chalk6 from "chalk";
3488
3563
 
3489
3564
  // src/utils/session.ts
3490
- import { readdir as readdir7, unlink } from "fs/promises";
3565
+ import { readdir as readdir7, stat as stat3, unlink as unlink2 } from "fs/promises";
3491
3566
  import path22 from "path";
3492
- 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;
3493
3571
  function parseSessionFile(content) {
3494
3572
  try {
3495
- return JSON.parse(content);
3573
+ const parsed = JSON.parse(content);
3574
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
3496
3575
  } catch {
3497
3576
  return null;
3498
3577
  }
@@ -3527,6 +3606,138 @@ async function listSessionFiles(cwd) {
3527
3606
  }
3528
3607
  return entries.sort((left, right) => left.key.localeCompare(right.key));
3529
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
+ }
3530
3741
 
3531
3742
  // src/commands/status.ts
3532
3743
  async function status() {
@@ -3608,6 +3819,10 @@ async function status() {
3608
3819
  console.log(
3609
3820
  ` harness: ${session.harness_disabled ? "disabled for this session" : "enabled"}`
3610
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
+ }
3611
3826
  if (!session.current_task) {
3612
3827
  console.log(" current_task: none");
3613
3828
  continue;
@@ -3736,8 +3951,11 @@ async function upgrade(opts) {
3736
3951
  ...childTopologyRefreshes.map(({ target }) => `- ${target.label}: role: submodule-child`)
3737
3952
  ].filter(Boolean) : [],
3738
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.",
3739
3955
  "Will update project-init task to recommend ec-init re-run for version adaptation.",
3740
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.",
3741
3959
  "Will migrate legacy workflow/TDD task metadata; memory content, spec, and project knowledge files remain untouched."
3742
3960
  ].join("\n");
3743
3961
  if (opts.dryRun) {
@@ -3755,6 +3973,15 @@ async function upgrade(opts) {
3755
3973
  }
3756
3974
  }
3757
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
+ }
3758
3985
  const beta1ProjectTddRequested = Number(config2.version) === 4 && config2.behavior?.tdd_enabled === true;
3759
3986
  const projectId = await writeRuntimeScaffold(target.dir, config2.agents, {
3760
3987
  supermodule: target.supermodule
@@ -3763,6 +3990,14 @@ async function upgrade(opts) {
3763
3990
  supermodule: target.boundary,
3764
3991
  projectId
3765
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
+ }
3766
4001
  await writeInstallManifest(target.dir, {
3767
4002
  harnessVersion: VERSION,
3768
4003
  agents: config2.agents,