cclaw-cli 6.7.0 → 6.9.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.
@@ -700,6 +700,43 @@ export async function writeFlowState(projectRoot, state, options = {}) {
700
700
  export async function writeFlowStateGuarded(projectRoot, state, options = {}) {
701
701
  await writeFlowState(projectRoot, state, options);
702
702
  }
703
+ /**
704
+ * v6.9.0 — backfill missing `completedStageMeta` rows for any stage that
705
+ * already lives in `completedStages` but has no audit timestamp. Uses the
706
+ * stage's artifact mtime when available, otherwise the current time. This
707
+ * runs as part of `flow-state-repair` so legacy v6.8 flow-state.json files
708
+ * get their meta carried forward without a destructive rewrite.
709
+ */
710
+ async function backfillCompletedStageMeta(projectRoot, state) {
711
+ const meta = { ...(state.completedStageMeta ?? {}) };
712
+ const backfilled = [];
713
+ for (const stage of state.completedStages) {
714
+ if (meta[stage] && typeof meta[stage].completedAt === "string" && meta[stage].completedAt.length > 0) {
715
+ continue;
716
+ }
717
+ let completedAt = new Date().toISOString();
718
+ try {
719
+ const { resolveArtifactPath } = await import("./artifact-paths.js");
720
+ const resolved = await resolveArtifactPath(stage, {
721
+ projectRoot,
722
+ track: state.track,
723
+ intent: "read"
724
+ });
725
+ const stat = await fs.stat(resolved.absPath);
726
+ completedAt = new Date(stat.mtimeMs).toISOString();
727
+ }
728
+ catch {
729
+ // artifact missing or unreadable — fall back to "now" so the meta row
730
+ // is at least consistently populated; operators can re-edit if needed.
731
+ }
732
+ meta[stage] = { completedAt };
733
+ backfilled.push(stage);
734
+ }
735
+ if (backfilled.length === 0) {
736
+ return { state, backfilled };
737
+ }
738
+ return { state: { ...state, completedStageMeta: meta }, backfilled };
739
+ }
703
740
  /**
704
741
  * Recompute the write-guard sidecar from the current on-disk flow-state
705
742
  * contents and append an audit entry to `.cclaw/.flow-state-repair.log`.
@@ -720,12 +757,26 @@ export async function repairFlowStateGuard(projectRoot, reason) {
720
757
  throw new Error(`flow-state-repair: ${FLOW_STATE_REL_PATH} does not exist; nothing to repair.`);
721
758
  }
722
759
  return withDirectoryLock(flowStateLockPath(projectRoot), async () => {
723
- const raw = await fs.readFile(statePath, "utf8");
760
+ let raw = await fs.readFile(statePath, "utf8");
724
761
  let runId = "unknown-run";
762
+ let backfilledStages = [];
725
763
  try {
726
764
  const parsed = JSON.parse(raw);
727
765
  const coerced = coerceFlowState(parsed).state;
728
766
  runId = coerced.activeRunId;
767
+ const { state: nextState, backfilled } = await backfillCompletedStageMeta(projectRoot, coerced);
768
+ backfilledStages = backfilled;
769
+ if (backfilled.length > 0) {
770
+ // Persist the migrated state inside the same lock window so the
771
+ // sha sidecar below covers the post-migration bytes, not the
772
+ // pre-migration ones.
773
+ await writeFlowState(projectRoot, nextState, {
774
+ allowReset: true,
775
+ skipLock: true,
776
+ writerSubsystem: "flow-state-repair-backfill"
777
+ });
778
+ raw = await fs.readFile(statePath, "utf8");
779
+ }
729
780
  }
730
781
  catch {
731
782
  // parsing failure falls back to "unknown-run"; repair intentionally
@@ -743,9 +794,17 @@ export async function repairFlowStateGuard(projectRoot, reason) {
743
794
  await writeFileSafe(guardPath, `${JSON.stringify(sidecar, null, 2)}\n`, { mode: 0o600 });
744
795
  const logPath = repairLogPath(projectRoot);
745
796
  await ensureDir(path.dirname(logPath));
746
- const logLine = `${sidecar.writtenAt} reason=${trimmed} runId=${sidecar.runId} sha256=${sidecar.sha256}\n`;
797
+ const backfillNote = backfilledStages.length > 0
798
+ ? ` backfilledCompletedStageMeta=${backfilledStages.join(",")}`
799
+ : "";
800
+ const logLine = `${sidecar.writtenAt} reason=${trimmed} runId=${sidecar.runId} sha256=${sidecar.sha256}${backfillNote}\n`;
747
801
  await fs.appendFile(logPath, logLine, "utf8");
748
- return { sidecar, repairLogPath: logPath, guardPath };
802
+ return {
803
+ sidecar,
804
+ repairLogPath: logPath,
805
+ guardPath,
806
+ completedStageMetaBackfilled: backfilledStages
807
+ };
749
808
  });
750
809
  }
751
810
  export function flowStateGuardSidecarPathFor(projectRoot) {