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.
- package/CHANGELOG.md +56 -1
- package/README.md +32 -25
- package/dist/cli.js +272 -37
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/claude/agents/ec-implementer.md +7 -8
- package/templates/claude/agents/ec-reviewer.md +14 -2
- package/templates/claude/agents/ec-verifier.md +11 -2
- package/templates/codex/agents/ec-implementer.toml +7 -8
- package/templates/codex/agents/ec-reviewer.toml +14 -2
- package/templates/codex/agents/ec-verifier.toml +11 -2
- package/templates/common/bundled-skills/ec-init/SKILL.md +1 -1
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +15 -11
- package/templates/common/skills/ec-analysis/SKILL.md +9 -8
- package/templates/common/skills/ec-config/SKILL.md +2 -2
- package/templates/common/skills/ec-implementing/SKILL.md +35 -30
- package/templates/common/skills/ec-lite/SKILL.md +74 -0
- package/templates/common/skills/ec-no-harness/SKILL.md +3 -0
- package/templates/common/skills/ec-quality/SKILL.md +153 -0
- package/templates/common/skills/ec-task-management/SKILL.md +9 -5
- package/templates/common/skills/ec-tdd-init/SKILL.md +5 -4
- package/templates/common/skills/ec-workflow/SKILL.md +22 -29
- package/templates/main-constraint/AGENTS.md.tpl +19 -13
- package/templates/main-constraint/CLAUDE.md.tpl +19 -13
- package/templates/qoder/agents/ec-implementer.md +7 -8
- package/templates/qoder/agents/ec-reviewer.md +14 -2
- package/templates/qoder/agents/ec-verifier.md +11 -2
- package/templates/runtime/templates/dev-spec-skeleton.md +2 -2
- package/templates/shared-hooks/easy_coding_state.py +2793 -318
- package/templates/claude/agents/ec-fixer.md +0 -37
- package/templates/codex/agents/ec-fixer.toml +0 -26
- package/templates/common/skills/ec-reviewing/SKILL.md +0 -109
- package/templates/common/skills/ec-verification/SKILL.md +0 -177
- 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 [
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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" ? "
|
|
1090
|
+
const fallbackMode = projectWorkflowMode === "adaptive" ? "standard" : projectWorkflowMode;
|
|
1014
1091
|
const concreteModes = relevantCandidates.map(
|
|
1015
|
-
(candidate) => candidate.workflowMode === "adaptive" ? "
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|