vibe-coding-master 0.4.26 → 0.4.28

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.
@@ -268,7 +268,6 @@ async function main() {
268
268
  const operations = [];
269
269
  await assertDirectory(projectRoot, "Project root");
270
270
  const manifest = await buildManifest(projectRoot);
271
- await installManifest({ projectRoot, manifest, dryRun, operations });
272
271
  for (const definition of MANAGED_FILES) {
273
272
  await installManagedFile({ projectRoot, definition, dryRun, operations });
274
273
  }
@@ -284,6 +283,13 @@ async function main() {
284
283
  }
285
284
  await removeLegacyFlatSkillFiles({ projectRoot, dryRun, operations });
286
285
  await removeLegacyCodexHarnessPaths({ projectRoot, dryRun, operations });
286
+ await installManifest({
287
+ projectRoot,
288
+ manifest,
289
+ dryRun,
290
+ operations,
291
+ forceWrite: hasRealHarnessChange(operations)
292
+ });
287
293
  printReport({ projectRoot, dryRun, operations });
288
294
  }
289
295
  function parseArgs(argv) {
@@ -449,13 +455,17 @@ function directoryCategory(directory) {
449
455
  }
450
456
  return "harness-tool-directory";
451
457
  }
452
- async function installManifest({ projectRoot, manifest, dryRun, operations }) {
458
+ async function installManifest({ projectRoot, manifest, dryRun, operations, forceWrite = false }) {
453
459
  const targetPath = path.join(projectRoot, MANIFEST_PATH);
454
460
  const currentManifest = await readOptionalJson(targetPath);
455
461
  if (currentManifest && manifestBodyEqual(currentManifest, manifest)) {
456
462
  operations.push(skip(MANIFEST_PATH, "unchanged"));
457
463
  return;
458
464
  }
465
+ if (!forceWrite && currentManifest && manifestBodyEqual(currentManifest, manifest, { ignoreHarnessVersion: true })) {
466
+ operations.push(skip(MANIFEST_PATH, "version-only change ignored"));
467
+ return;
468
+ }
459
469
  await writeIfChanged({
460
470
  targetPath,
461
471
  relativePath: MANIFEST_PATH,
@@ -731,15 +741,23 @@ async function pathExists(absolutePath) {
731
741
  throw error;
732
742
  });
733
743
  }
734
- function manifestBodyEqual(left, right) {
744
+ function manifestBodyEqual(left, right, options = {}) {
735
745
  const normalizedLeft = { ...left };
736
746
  const normalizedRight = { ...right };
737
747
  delete normalizedLeft.installedAt;
738
748
  delete normalizedLeft.updatedAt;
739
749
  delete normalizedRight.installedAt;
740
750
  delete normalizedRight.updatedAt;
751
+ if (options.ignoreHarnessVersion) {
752
+ delete normalizedLeft.harnessVersion;
753
+ delete normalizedRight.harnessVersion;
754
+ }
741
755
  return JSON.stringify(normalizedLeft) === JSON.stringify(normalizedRight);
742
756
  }
757
+ function hasRealHarnessChange(operations) {
758
+ return operations.some((operation) => operation.path !== MANIFEST_PATH &&
759
+ (operation.status === "done" || operation.status === "plan"));
760
+ }
743
761
  function resolveInside(root, relativePath) {
744
762
  if (path.isAbsolute(relativePath)) {
745
763
  fail(`Path must be relative: ${relativePath}`);
@@ -1284,11 +1284,7 @@ async function analyzeHarnessManifest(fs, repoRoot, vcmVersion) {
1284
1284
  };
1285
1285
  }
1286
1286
  if (installedVersion !== vcmVersion) {
1287
- return {
1288
- path: MANIFEST_PATH,
1289
- action: "update",
1290
- reason: `VCM fixed harness manifest version is ${installedVersion ?? "missing"}; current VCM version is ${vcmVersion}.`
1291
- };
1287
+ return undefined;
1292
1288
  }
1293
1289
  return undefined;
1294
1290
  }
@@ -1,4 +1,3 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import path from "node:path";
3
2
  import { VCM_ROLE_NAMES, isDispatchableRole } from "../../shared/constants.js";
4
3
  import { VcmError } from "../errors.js";
@@ -43,10 +42,10 @@ export function createSessionService(deps) {
43
42
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
44
43
  const model = normalizeClaudeModel(input.model ?? persisted?.model);
45
44
  const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort);
46
- const claudeSessionId = launchMode === "resume"
45
+ const resumeClaudeSessionId = launchMode === "resume"
47
46
  ? persisted?.claudeSessionId
48
- : randomUUID();
49
- if (!claudeSessionId) {
47
+ : undefined;
48
+ if (launchMode === "resume" && !resumeClaudeSessionId) {
50
49
  throw new VcmError({
51
50
  code: "CLAUDE_SESSION_MISSING",
52
51
  message: `${role} does not have a session id to resume.`,
@@ -54,11 +53,14 @@ export function createSessionService(deps) {
54
53
  hint: "Start the role once before using Resume."
55
54
  });
56
55
  }
56
+ const claudeSessionId = resumeClaudeSessionId ?? "";
57
57
  const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
58
58
  ? persisted.transcriptPath
59
- : claudeTranscriptPath(taskRepoRoot, claudeSessionId);
59
+ : resumeClaudeSessionId
60
+ ? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId)
61
+ : undefined;
60
62
  const startCommand = {
61
- ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
63
+ ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort),
62
64
  cwd: taskRepoRoot
63
65
  };
64
66
  const runtimeSession = await deps.runtime.createSession({
@@ -73,7 +75,7 @@ export function createSessionService(deps) {
73
75
  VCM_TASK_REPO_ROOT: taskRepoRoot,
74
76
  VCM_TASK_SLUG: taskSlug,
75
77
  VCM_ROLE: role,
76
- VCM_SESSION_ID: claudeSessionId
78
+ VCM_SESSION_ID: claudeSessionId || undefined
77
79
  },
78
80
  cols: input.cols,
79
81
  rows: input.rows
@@ -120,10 +122,10 @@ export function createSessionService(deps) {
120
122
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
121
123
  const model = normalizeClaudeModel(input.model ?? persisted?.model);
122
124
  const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
123
- const claudeSessionId = launchMode === "resume"
125
+ const resumeClaudeSessionId = launchMode === "resume"
124
126
  ? persisted?.claudeSessionId
125
- : randomUUID();
126
- if (!claudeSessionId) {
127
+ : undefined;
128
+ if (launchMode === "resume" && !resumeClaudeSessionId) {
127
129
  throw new VcmError({
128
130
  code: "TRANSLATOR_SESSION_MISSING",
129
131
  message: "Translator does not have a session id to resume.",
@@ -135,11 +137,14 @@ export function createSessionService(deps) {
135
137
  const launchCwd = launchMode === "resume"
136
138
  ? persisted?.cwd ?? taskContext.taskRepoRoot
137
139
  : taskContext.taskRepoRoot;
140
+ const claudeSessionId = resumeClaudeSessionId ?? "";
138
141
  const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
139
142
  ? persisted.transcriptPath
140
- : claudeTranscriptPath(launchCwd, claudeSessionId);
143
+ : resumeClaudeSessionId
144
+ ? claudeTranscriptPath(launchCwd, resumeClaudeSessionId)
145
+ : undefined;
141
146
  const startCommand = {
142
- ...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
147
+ ...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort),
143
148
  cwd: launchCwd
144
149
  };
145
150
  const runtimeSession = await deps.runtime.createSession({
@@ -154,7 +159,7 @@ export function createSessionService(deps) {
154
159
  VCM_TASK_REPO_ROOT: taskContext.taskRepoRoot,
155
160
  VCM_TASK_SLUG: taskContext.taskSlug,
156
161
  VCM_ROLE: TRANSLATOR_ROLE,
157
- VCM_SESSION_ID: claudeSessionId
162
+ VCM_SESSION_ID: claudeSessionId || undefined
158
163
  },
159
164
  cols: input.cols,
160
165
  rows: input.rows
@@ -197,10 +202,10 @@ export function createSessionService(deps) {
197
202
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
198
203
  const model = normalizeClaudeModel(input.model ?? persisted?.model);
199
204
  const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
200
- const claudeSessionId = launchMode === "resume"
205
+ const resumeClaudeSessionId = launchMode === "resume"
201
206
  ? persisted?.claudeSessionId
202
- : randomUUID();
203
- if (!claudeSessionId) {
207
+ : undefined;
208
+ if (launchMode === "resume" && !resumeClaudeSessionId) {
204
209
  throw new VcmError({
205
210
  code: "HARNESS_ENGINEER_SESSION_MISSING",
206
211
  message: "Harness Engineer does not have a session id to resume.",
@@ -212,11 +217,14 @@ export function createSessionService(deps) {
212
217
  const launchCwd = launchMode === "resume"
213
218
  ? persisted?.cwd ?? taskContext.taskRepoRoot
214
219
  : taskContext.taskRepoRoot;
220
+ const claudeSessionId = resumeClaudeSessionId ?? "";
215
221
  const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
216
222
  ? persisted.transcriptPath
217
- : claudeTranscriptPath(launchCwd, claudeSessionId);
223
+ : resumeClaudeSessionId
224
+ ? claudeTranscriptPath(launchCwd, resumeClaudeSessionId)
225
+ : undefined;
218
226
  const startCommand = {
219
- ...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
227
+ ...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort),
220
228
  cwd: launchCwd
221
229
  };
222
230
  const runtimeSession = await deps.runtime.createSession({
@@ -231,7 +239,7 @@ export function createSessionService(deps) {
231
239
  VCM_TASK_REPO_ROOT: taskContext.taskRepoRoot,
232
240
  VCM_TASK_SLUG: taskContext.taskSlug,
233
241
  VCM_ROLE: HARNESS_ENGINEER_ROLE,
234
- VCM_SESSION_ID: claudeSessionId
242
+ VCM_SESSION_ID: claudeSessionId || undefined
235
243
  },
236
244
  cols: input.cols,
237
245
  rows: input.rows
@@ -298,7 +306,9 @@ export function createSessionService(deps) {
298
306
  ...session,
299
307
  cwd: targetCwd,
300
308
  previousCwd: session.cwd,
301
- transcriptPath: claudeTranscriptPath(targetCwd, session.claudeSessionId),
309
+ transcriptPath: session.claudeSessionId
310
+ ? claudeTranscriptPath(targetCwd, session.claudeSessionId)
311
+ : session.transcriptPath,
302
312
  updatedAt: timestamp
303
313
  };
304
314
  deps.registry.upsert(normalizeProjectScopedRecordForPersistence(updated));
@@ -466,6 +476,7 @@ export function createSessionService(deps) {
466
476
  await deps.runtime.stop(existing.id);
467
477
  }
468
478
  deps.registry.remove(existing.id);
479
+ await clearPersistedTranslatorSession(deps.fs, repoRoot);
469
480
  return launchProjectTranslatorSession(repoRoot, input, "fresh");
470
481
  },
471
482
  async getProjectTranslatorSession(repoRoot) {
@@ -509,10 +520,11 @@ export function createSessionService(deps) {
509
520
  const timestamp = now();
510
521
  const isTurnEnd = isTurnEndHook(input.eventName);
511
522
  const isCompact = isCompactHook(input.eventName);
523
+ const sessionIdentity = nextHookSessionIdentity(current, input);
512
524
  const updated = {
513
525
  ...current,
514
- claudeSessionId: input.sessionId ?? current.claudeSessionId,
515
- transcriptPath: input.transcriptPath ?? current.transcriptPath,
526
+ claudeSessionId: sessionIdentity.claudeSessionId,
527
+ transcriptPath: sessionIdentity.transcriptPath,
516
528
  cwd: input.cwd ?? current.cwd,
517
529
  activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
518
530
  lastHookEventAt: timestamp,
@@ -590,6 +602,7 @@ export function createSessionService(deps) {
590
602
  await deps.runtime.stop(existing.id);
591
603
  }
592
604
  deps.registry.remove(existing.id);
605
+ await clearPersistedHarnessEngineerSession(deps.fs, repoRoot);
593
606
  return launchProjectHarnessEngineerSession(repoRoot, input, "fresh");
594
607
  },
595
608
  async getProjectHarnessEngineerSession(repoRoot) {
@@ -633,10 +646,11 @@ export function createSessionService(deps) {
633
646
  const timestamp = now();
634
647
  const isTurnEnd = isTurnEndHook(input.eventName);
635
648
  const isCompact = isCompactHook(input.eventName);
649
+ const sessionIdentity = nextHookSessionIdentity(current, input);
636
650
  const updated = {
637
651
  ...current,
638
- claudeSessionId: input.sessionId ?? current.claudeSessionId,
639
- transcriptPath: input.transcriptPath ?? current.transcriptPath,
652
+ claudeSessionId: sessionIdentity.claudeSessionId,
653
+ transcriptPath: sessionIdentity.transcriptPath,
640
654
  cwd: input.cwd ?? current.cwd,
641
655
  activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
642
656
  lastHookEventAt: timestamp,
@@ -725,6 +739,9 @@ export function createSessionService(deps) {
725
739
  await deps.runtime.stop(existing.id);
726
740
  }
727
741
  deps.registry.remove(existing.id);
742
+ const config = await deps.projectService.loadConfig(repoRoot);
743
+ const task = await deps.taskService.loadTask(repoRoot, taskSlug);
744
+ await clearPersistedRoleSessionRecord(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, taskSlug, role, now());
728
745
  return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
729
746
  },
730
747
  async getRoleSession(repoRoot, taskSlug, role) {
@@ -800,10 +817,11 @@ export function createSessionService(deps) {
800
817
  const timestamp = now();
801
818
  const isTurnEnd = isTurnEndHook(input.eventName);
802
819
  const isCompact = isCompactHook(input.eventName);
820
+ const sessionIdentity = nextHookSessionIdentity(current, input);
803
821
  const updated = {
804
822
  ...current,
805
- claudeSessionId: input.sessionId ?? current.claudeSessionId,
806
- transcriptPath: input.transcriptPath ?? current.transcriptPath,
823
+ claudeSessionId: sessionIdentity.claudeSessionId,
824
+ transcriptPath: sessionIdentity.transcriptPath,
807
825
  cwd: input.cwd ?? current.cwd,
808
826
  activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
809
827
  lastHookEventAt: timestamp,
@@ -890,6 +908,9 @@ function toRoleSessionRecordView(record, runtime) {
890
908
  };
891
909
  }
892
910
  function matchesRoleHookSession(record, input) {
911
+ if (!record.claudeSessionId && !record.transcriptPath) {
912
+ return input.eventName === "UserPromptSubmit";
913
+ }
893
914
  if (input.sessionId && record.claudeSessionId === input.sessionId) {
894
915
  return true;
895
916
  }
@@ -901,6 +922,20 @@ function matchesRoleHookSession(record, input) {
901
922
  }
902
923
  return false;
903
924
  }
925
+ function nextHookSessionIdentity(current, input) {
926
+ const canRecordFirstClaudeSessionId = Boolean(!current.claudeSessionId &&
927
+ input.eventName === "UserPromptSubmit" &&
928
+ input.sessionId);
929
+ const hasConfirmedClaudeSessionId = Boolean(current.claudeSessionId || canRecordFirstClaudeSessionId);
930
+ return {
931
+ claudeSessionId: canRecordFirstClaudeSessionId
932
+ ? input.sessionId ?? current.claudeSessionId
933
+ : current.claudeSessionId,
934
+ transcriptPath: hasConfirmedClaudeSessionId
935
+ ? input.transcriptPath ?? current.transcriptPath
936
+ : current.transcriptPath
937
+ };
938
+ }
904
939
  function isTurnEndHook(eventName) {
905
940
  return eventName === "Stop" || eventName === "StopFailure";
906
941
  }
@@ -1068,6 +1103,9 @@ function buildHarnessRefreshPrompt(role) {
1068
1103
  ].join("\n");
1069
1104
  }
1070
1105
  async function persistTaskSession(fs, repoRoot, stateRoot, session) {
1106
+ if (!hasConfirmedClaudeSessionId(session)) {
1107
+ return;
1108
+ }
1071
1109
  const sessionPath = getTaskSessionPath(repoRoot, stateRoot, session.taskSlug);
1072
1110
  const empty = createEmptyTaskSessionRecord(session.taskSlug, session.updatedAt);
1073
1111
  const current = await fs.pathExists(sessionPath)
@@ -1092,6 +1130,23 @@ async function persistTaskSession(fs, repoRoot, stateRoot, session) {
1092
1130
  }
1093
1131
  });
1094
1132
  }
1133
+ async function clearPersistedRoleSessionRecord(fs, repoRoot, stateRoot, taskSlug, role, updatedAt) {
1134
+ const sessionPath = getTaskSessionPath(repoRoot, stateRoot, taskSlug);
1135
+ const current = await fs.pathExists(sessionPath)
1136
+ ? await fs.readJson(sessionPath)
1137
+ : createEmptyTaskSessionRecord(taskSlug, updatedAt);
1138
+ await fs.writeJsonAtomic(sessionPath, {
1139
+ ...current,
1140
+ updatedAt,
1141
+ roles: {
1142
+ ...current.roles,
1143
+ [role]: {
1144
+ id: null,
1145
+ status: "not_started"
1146
+ }
1147
+ }
1148
+ });
1149
+ }
1095
1150
  async function persistRoleSessionRecord(fs, baseRepoRoot, taskRepoRoot, stateRoot, session) {
1096
1151
  if (session.role === TRANSLATOR_ROLE) {
1097
1152
  await persistTranslatorSession(fs, baseRepoRoot, session);
@@ -1104,6 +1159,9 @@ async function persistRoleSessionRecord(fs, baseRepoRoot, taskRepoRoot, stateRoo
1104
1159
  await persistTaskSession(fs, taskRepoRoot, stateRoot, session);
1105
1160
  }
1106
1161
  async function persistTranslatorSession(fs, repoRoot, session) {
1162
+ if (!hasConfirmedClaudeSessionId(session)) {
1163
+ return;
1164
+ }
1107
1165
  await fs.writeJsonAtomic(resolveRepoPath(repoRoot, TRANSLATOR_SESSION_PATH), {
1108
1166
  version: 1,
1109
1167
  role: session.role,
@@ -1114,7 +1172,13 @@ async function persistTranslatorSession(fs, repoRoot, session) {
1114
1172
  }
1115
1173
  });
1116
1174
  }
1175
+ async function clearPersistedTranslatorSession(fs, repoRoot) {
1176
+ await removePersistedProjectSessionFile(fs, repoRoot, TRANSLATOR_SESSION_PATH);
1177
+ }
1117
1178
  async function persistHarnessEngineerSession(fs, repoRoot, session) {
1179
+ if (!hasConfirmedClaudeSessionId(session)) {
1180
+ return;
1181
+ }
1118
1182
  await fs.writeJsonAtomic(resolveRepoPath(repoRoot, HARNESS_ENGINEER_SESSION_PATH), {
1119
1183
  version: 1,
1120
1184
  role: session.role,
@@ -1125,9 +1189,30 @@ async function persistHarnessEngineerSession(fs, repoRoot, session) {
1125
1189
  }
1126
1190
  });
1127
1191
  }
1192
+ async function clearPersistedHarnessEngineerSession(fs, repoRoot) {
1193
+ await removePersistedProjectSessionFile(fs, repoRoot, HARNESS_ENGINEER_SESSION_PATH);
1194
+ }
1195
+ async function removePersistedProjectSessionFile(fs, repoRoot, relativePath) {
1196
+ const sessionPath = resolveRepoPath(repoRoot, relativePath);
1197
+ if (!(await fs.pathExists(sessionPath))) {
1198
+ return;
1199
+ }
1200
+ if (!fs.removePath) {
1201
+ throw new VcmError({
1202
+ code: "SESSION_CLEAR_UNAVAILABLE",
1203
+ message: "VCM cannot clear the persisted Claude session file in this runtime.",
1204
+ statusCode: 500,
1205
+ hint: `Remove ${relativePath} manually before restarting the role.`
1206
+ });
1207
+ }
1208
+ await fs.removePath(sessionPath, { force: true });
1209
+ }
1128
1210
  function isProjectRoleSessionFile(value) {
1129
1211
  return "record" in value && typeof value.record === "object" && value.record !== null;
1130
1212
  }
1213
+ function hasConfirmedClaudeSessionId(session) {
1214
+ return session.claudeSessionId.trim().length > 0;
1215
+ }
1131
1216
  function createEmptyTaskSessionRecord(taskSlug, updatedAt) {
1132
1217
  return {
1133
1218
  version: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-coding-master",
3
- "version": "0.4.26",
3
+ "version": "0.4.28",
4
4
  "description": "Local GUI session cockpit for Claude Code role sessions.",
5
5
  "type": "module",
6
6
  "files": [