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.
- package/CHANGELOG.md +72 -1
- package/README.md +32 -25
- package/dist/cli.js +364 -39
- 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 +38 -31
- 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 +25 -29
- package/templates/main-constraint/AGENTS.md.tpl +27 -15
- package/templates/main-constraint/CLAUDE.md.tpl +24 -15
- 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 +2961 -372
- 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
|
|
@@ -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 =
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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" ? "
|
|
1090
|
+
const fallbackMode = projectWorkflowMode === "adaptive" ? "standard" : projectWorkflowMode;
|
|
929
1091
|
const concreteModes = relevantCandidates.map(
|
|
930
|
-
(candidate) => candidate.workflowMode === "adaptive" ? "
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|