@flowmind/cli 0.3.8 → 0.3.9

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/dist/index.js CHANGED
@@ -293,9 +293,9 @@ var isHttpEvidenceUrl = (value) => {
293
293
  };
294
294
 
295
295
  // src/agent-session.ts
296
- import { createHash as createHash3 } from "node:crypto";
297
- import { appendFile, mkdir as mkdir3, readdir as readdir4, readFile as readFile4, rm, stat as stat2 } from "node:fs/promises";
298
- import { join as join6 } from "node:path";
296
+ import { createHash as createHash4 } from "node:crypto";
297
+ import { appendFile, mkdir as mkdir5, readdir as readdir7, readFile as readFile6, rename as rename4, rm as rm3, rmdir, stat as stat3, utimes, writeFile as writeFile5 } from "node:fs/promises";
298
+ import { join as join8 } from "node:path";
299
299
 
300
300
  // src/awake-clock.ts
301
301
  import { execFileSync } from "node:child_process";
@@ -513,6 +513,33 @@ function bindAccount(apiKey, account, since, url = API_URL) {
513
513
  if (url === API_URL) Object.assign(snapshot, updated);
514
514
  }
515
515
  var accountBinding = () => checkedBinding(snapshot, API_URL);
516
+ var legacySessionsDir = () => join3(dirname(config.path), "agent-sessions");
517
+ function soleAccountProfile() {
518
+ const current = config.store;
519
+ const periods = current.signedOutPeriods;
520
+ return checkedBinding(current, API_URL) !== null && (periods === void 0 || Object.keys(periods).length === 0);
521
+ }
522
+ function extendSignInStart(since) {
523
+ const current = config.store;
524
+ const binding = checkedBinding(current, API_URL);
525
+ const pinned = accountBinding();
526
+ if (!binding || !pinned || binding.credential !== pinned.credential || binding.account !== pinned.account) {
527
+ throw new Error("The sign-in changed while Flowmind adopted older local records. Run the command again.");
528
+ }
529
+ if (!soleAccountProfile() || !validPoint(since) || Date.parse(since.at) >= Date.parse(binding.since.at)) return;
530
+ const updated = { ...current, binding: { ...binding, since } };
531
+ config.store = updated;
532
+ Object.assign(snapshot, updated);
533
+ }
534
+ function refreshSignInStart() {
535
+ const pinned = accountBinding();
536
+ if (!pinned) return;
537
+ const stored = checkedBinding(config.store, API_URL);
538
+ if (!stored || stored.account !== pinned.account || stored.credential !== pinned.credential || (stored.origin ?? API_URL) !== (pinned.origin ?? API_URL)) {
539
+ throw new Error("The sign-in changed while this command ran. Run it again.");
540
+ }
541
+ if (Date.parse(stored.since.at) < Date.parse(pinned.since.at)) snapshot.binding = { ...pinned, since: stored.since };
542
+ }
516
543
  function ownershipPeriods() {
517
544
  const binding = accountBinding();
518
545
  if (!binding) throw new Error("Sign in to measure agent time.");
@@ -612,9 +639,12 @@ async function writeIgnore(start, reason) {
612
639
  }
613
640
 
614
641
  // src/manual-save.ts
615
- import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
616
- import { mkdir as mkdir2, readdir as readdir3, readFile as readFile3, rename, unlink as unlink2, writeFile as writeFile3 } from "node:fs/promises";
617
- import { join as join5 } from "node:path";
642
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
643
+ import { mkdir as mkdir4, readdir as readdir6, readFile as readFile5, rename as rename3, unlink as unlink2, writeFile as writeFile4 } from "node:fs/promises";
644
+ import { join as join7 } from "node:path";
645
+
646
+ // src/account.ts
647
+ import { readdir as readdir5 } from "node:fs/promises";
618
648
 
619
649
  // ../../packages/contracts/src/generated/revision.ts
620
650
  var contractRevision = "3620e18f995dfc34dee05d8a132148178ce03b4a0223aafb99778e00e5585ef4";
@@ -944,292 +974,694 @@ async function apiRequest(endpoint, args, options = {}) {
944
974
  }
945
975
  }
946
976
 
947
- // src/account.ts
948
- var bindingStartNow = () => ({ at: (/* @__PURE__ */ new Date()).toISOString(), up: awakeMs() });
949
- async function withConfigWriters(change) {
950
- let lock = await configWriters();
951
- for (let waited = 0; !lock && waited < 15e3; waited += 250) {
952
- await new Promise((resolve3) => setTimeout(resolve3, 250));
953
- lock = await configWriters();
954
- }
955
- if (!lock) throw new Error("Another Flowmind process is changing the sign-in. Try again when it finishes.");
956
- try {
957
- return await change();
958
- } finally {
959
- await lock.release();
960
- }
961
- }
962
- async function ensureAccount() {
963
- if (accountBinding()) return;
964
- const key = requireApiKey();
965
- await withConfigWriters(async () => {
966
- const before = config.store;
967
- if (before.binding !== void 0 || before.apiKey !== key || (before.apiUrl ?? "https://api.flowmind.pro") !== API_URL) {
968
- throw new Error("The sign-in changed while Flowmind checked it. Run the command again.");
969
- }
970
- let account;
971
- try {
972
- account = (await apiRequest(getV1Me, {}, { signal: AbortSignal.timeout(1e4) })).userId;
973
- } catch (error) {
974
- if (error instanceof BridgeError && error.context.kind === "api" && error.context.status === 401) {
975
- throw new Error("Flowmind refused the saved sign-in. Run: flowmind auth login");
976
- }
977
- throw new Error(
978
- "Could not verify the signed-in account: " + (error instanceof Error ? error.message : String(error))
979
- );
980
- }
981
- const current = config.store;
982
- if (current.apiKey !== key || (current.apiUrl ?? "https://api.flowmind.pro") !== API_URL) {
983
- throw new Error("The sign-in changed while Flowmind checked it. Run the command again.");
984
- }
985
- bindAccount(key, account, bindingStartNow());
986
- });
977
+ // src/legacy-proofs.ts
978
+ import { mkdir as mkdir3, readdir as readdir4, readFile as readFile4, rename as rename2, rm as rm2, stat as stat2 } from "node:fs/promises";
979
+ import { join as join6 } from "node:path";
980
+
981
+ // src/pending-stages.ts
982
+ import { spawn } from "node:child_process";
983
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
984
+ import { mkdir as mkdir2, readdir as readdir3, readFile as readFile3, rename, rm, writeFile as writeFile3 } from "node:fs/promises";
985
+ import { basename, join as join5 } from "node:path";
986
+
987
+ // src/stored-stage-input.ts
988
+ var SOURCE_KINDS = [
989
+ "calendar",
990
+ "claude_code",
991
+ "cli",
992
+ "codex",
993
+ "github",
994
+ "linear",
995
+ "manual",
996
+ "session_log",
997
+ "other"
998
+ ];
999
+ var evidenceKinds = ["calendar_event", "commit", "issue", "linear_issue", "pr", "session_log", "other"];
1000
+ var shape = (v6, keys2) => v6 !== null && typeof v6 === "object" && !Array.isArray(v6) && Object.keys(v6).every((key) => keys2.includes(key));
1001
+ var text = (v6, max) => typeof v6 === "string" && v6.trim().length > 0 && [...v6].length <= max;
1002
+ var optionalText = (v6, max) => v6 === void 0 || v6 === null || text(v6, max);
1003
+ var member = (v6, kinds) => typeof v6 === "string" && kinds.includes(v6);
1004
+ var isoDate = (v6) => typeof v6 === "string" && Number.isFinite(Date.parse(v6)) && new Date(v6).toISOString() === v6;
1005
+ var keys = [
1006
+ "targetKind",
1007
+ "projectId",
1008
+ "startedAt",
1009
+ "durationMinutes",
1010
+ "description",
1011
+ "note",
1012
+ "details",
1013
+ "artifactUrl",
1014
+ "source",
1015
+ "evidence"
1016
+ ];
1017
+ function isStoredStageInput(v6) {
1018
+ if (!shape(v6, keys) || v6.targetKind !== "project" || typeof v6.projectId !== "string" || !isProjectId(v6.projectId) || !isoDate(v6.startedAt) || typeof v6.durationMinutes !== "number" || !Number.isSafeInteger(v6.durationMinutes) || v6.durationMinutes < 1 || !text(v6.description, 280) || !text(v6.note, AGENT_OUTCOME_MAX_LENGTH) || /[\r\n]/.test(v6.note) || v6.artifactUrl !== void 0 && (!text(v6.artifactUrl, 2048) || !isHttpEvidenceUrl(v6.artifactUrl)))
1019
+ return false;
1020
+ const details = v6.details === void 0 ? [] : v6.details;
1021
+ if (!Array.isArray(details) || !details.every((d5) => text(d5, AGENT_DETAIL_MAX_LENGTH) && !/[\r\n]/.test(d5)) || agentProofIssues({ outcome: v6.note, details }).length)
1022
+ return false;
1023
+ const source = v6.source;
1024
+ return shape(source, ["kind", "externalId", "label"]) && member(source.kind, SOURCE_KINDS) && optionalText(source.externalId, 200) && optionalText(source.label, 160) && Array.isArray(v6.evidence) && v6.evidence.length <= 20 && v6.evidence.every(
1025
+ (e4) => shape(e4, ["kind", "label", "externalId", "url"]) && member(e4.kind, evidenceKinds) && text(e4.label, 160) && optionalText(e4.externalId, 200) && (e4.url === null || e4.url === void 0 || text(e4.url, 500) && isHttpEvidenceUrl(e4.url))
1026
+ );
987
1027
  }
988
1028
 
989
- // src/manual-save.ts
990
- var RECEIPT_MS = 24 * 60 * 6e4;
991
- var savesDir = () => join5(accountDirectory(), "manual-saves");
992
- var fileOf = (id) => join5(savesDir(), id + ".json");
993
- async function withWriters(work) {
994
- await mkdir2(savesDir(), { recursive: true, mode: 448 });
995
- const lock = await tryLock(join5(savesDir(), "writers"));
996
- if (!lock) throw new Error("Another Flowmind process is saving a log. Try again when it finishes.");
997
- try {
998
- return await work();
999
- } finally {
1000
- await lock.release();
1001
- }
1029
+ // src/pending-stages.ts
1030
+ var STAGE_KEY_IN_USE = "This stage key is already in use";
1031
+ var MAX_BACKOFF_MINUTES = 60;
1032
+ var outboxDir = () => join5(accountDirectory(), "pending-stages");
1033
+ var takeStageLock = () => tryLock(join5(accountDirectory(), "stage-writers"));
1034
+ var fileOf = (id) => join5(outboxDir(), id + ".json");
1035
+ var nextAttempt = (attempts, now) => new Date(now.getTime() + Math.min(MAX_BACKOFF_MINUTES, 2 ** Math.max(0, attempts - 1)) * 6e4).toISOString();
1036
+ var errorText = (error) => error instanceof Error ? error.message : String(error);
1037
+ var classifyStageFailure = (error) => {
1038
+ if (!(error instanceof BridgeError)) return { kind: "unexpected", reason: errorText(error) };
1039
+ const { kind, status } = error.context;
1040
+ if (kind === "transport") return { kind: "retry", reason: "The Flowmind API could not be reached in time" };
1041
+ if (kind === "contract")
1042
+ return {
1043
+ kind: "retry",
1044
+ reason: "Flowmind's answer could not be read" + (status === null ? "" : " (HTTP " + status + ")")
1045
+ };
1046
+ const message = error instanceof ApiRejectionError ? error.rejectionMessage : "The Flowmind API answered HTTP " + status;
1047
+ if (status === 401 || status === 403) return { kind: "login", reason: message };
1048
+ if (status === 409 && message === STAGE_KEY_IN_USE) return { kind: "conflict", reason: message };
1049
+ if (status !== null && status >= 400 && status < 500 && status !== 408 && status !== 429)
1050
+ return { kind: "reject", reason: message };
1051
+ return { kind: "retry", reason: message };
1052
+ };
1053
+ var stageIdOf = (idempotencyKey, input2) => createHash2("sha256").update(idempotencyKey + "\n" + JSON.stringify(input2)).digest("hex").slice(0, 16);
1054
+ var cutDigestOf = (id, sessionId, cut) => createHash2("sha256").update(JSON.stringify([id, sessionId, cut === null ? null : [cut.at, cut.up]])).digest("hex");
1055
+ function readStage(value, fileName, legacy = false) {
1056
+ const stage = value;
1057
+ if (typeof stage !== "object" || stage === null) return null;
1058
+ const input2 = stage.input;
1059
+ const { id, sessionId, cut } = stage;
1060
+ const valid = typeof id === "string" && /^[a-f0-9]{16}$/.test(id) && fileName === id + ".json" && typeof stage.idempotencyKey === "string" && stage.idempotencyKey.trim() === stage.idempotencyKey && /^[\x20-\x7e]+$/.test(stage.idempotencyKey) && typeof stage.apiUrl === "string" && isStoredStageInput(input2) && (legacy && stage.credentialFingerprint === void 0 || typeof stage.credentialFingerprint === "string" && /^[a-f0-9]{64}$/.test(stage.credentialFingerprint)) && stageIdOf(stage.idempotencyKey, input2) === id && isoDate(stage.savedAt) && isoDate(stage.nextAttemptAt) && (stage.lastAttemptAt === null || isoDate(stage.lastAttemptAt)) && (sessionId === null || typeof sessionId === "string" && sessionId.length > 0) && (cut === null || typeof cut === "object" && isoDate(cut.at) && Date.parse(cut.at) <= Date.parse(stage.savedAt) && (cut.up === null || Number.isFinite(cut.up))) && sessionId === null === (cut === null) && (stage.state === "waiting" || stage.state === "rejected" || stage.state === "conflict") && Number.isSafeInteger(stage.attempts) && stage.attempts >= 0 && (typeof stage.lastError === "string" || stage.state === "waiting" && stage.lastError === null);
1061
+ if (!valid) return null;
1062
+ const cutDigest = cutDigestOf(id, sessionId, cut);
1063
+ if (stage.cutDigest !== cutDigest && !(legacy && stage.cutDigest === void 0)) return null;
1064
+ return { ...stage, credentialFingerprint: stage.credentialFingerprint ?? null, cutDigest };
1002
1065
  }
1003
- function readSaved(text2, id) {
1004
- const saved = JSON.parse(text2);
1005
- if (!saved || saved.id !== id || !Number.isFinite(saved.savedAt) || typeof saved.key !== "string" || typeof saved.body !== "object" || saved.body === null) {
1006
- throw new Error("Unreadable saved log " + id + " in " + savesDir());
1066
+ var writePendingStage = async (stage) => {
1067
+ await mkdir2(outboxDir(), { recursive: true });
1068
+ const temporary = fileOf(stage.id) + "." + process.pid + "." + randomUUID2() + ".tmp";
1069
+ await writeFile3(temporary, JSON.stringify(stage, null, 2) + "\n", { encoding: "utf8", mode: 384 });
1070
+ await rename(temporary, fileOf(stage.id));
1071
+ };
1072
+ async function saveStageBeforeSending(input2) {
1073
+ const at2 = input2.now.toISOString();
1074
+ const id = stageIdOf(input2.idempotencyKey, input2.stage);
1075
+ const pending = {
1076
+ id,
1077
+ savedAt: at2,
1078
+ apiUrl: API_URL,
1079
+ idempotencyKey: input2.idempotencyKey,
1080
+ credentialFingerprint: accountBinding().credential,
1081
+ input: input2.stage,
1082
+ sessionId: input2.sessionId,
1083
+ cut: input2.cut,
1084
+ cutDigest: cutDigestOf(id, input2.sessionId, input2.cut),
1085
+ state: "waiting",
1086
+ attempts: 0,
1087
+ lastAttemptAt: null,
1088
+ nextAttemptAt: at2,
1089
+ lastError: null
1090
+ };
1091
+ if (!readStage(pending, pending.id + ".json"))
1092
+ throw new Error("The stage request does not match the Flowmind proof contract. Check its fields before retrying.");
1093
+ const { stages, unreadable } = await listPendingStages();
1094
+ if (unreadable.includes(fileOf(pending.id)) || stages.some((stage) => stage.id === pending.id)) {
1095
+ throw new Error("This proof (" + pending.id + ") is already saved. See: flowmind agent pending");
1007
1096
  }
1008
- return saved;
1097
+ await writePendingStage(pending);
1098
+ return pending;
1009
1099
  }
1010
- async function manualSave(identity, prepare, send) {
1011
- await ensureAccount();
1012
- const id = createHash2("sha256").update(JSON.stringify(identity)).digest("hex").slice(0, 16);
1013
- return withWriters(async () => {
1014
- let saved;
1015
- try {
1016
- saved = readSaved(await readFile3(fileOf(id), "utf8"), id);
1017
- } catch (error) {
1018
- if (error.code !== "ENOENT") throw error;
1019
- const body = await prepare();
1020
- if (body === null) return null;
1021
- saved = { id, body, key: randomUUID2(), savedAt: Date.now() };
1022
- const temp = fileOf(id) + "." + randomUUID2() + ".tmp";
1023
- await writeFile3(temp, JSON.stringify(saved), { mode: 384, flag: "wx" });
1024
- await rename(temp, fileOf(id));
1025
- }
1026
- if (Date.now() - saved.savedAt >= RECEIPT_MS) {
1027
- throw new Error(
1028
- `This log (${id}) was not confirmed within 24 hours. Check Activity, then run flowmind pending-logs done ${id} if it is there, or flowmind pending-logs discard ${id} to log it again.`
1029
- );
1030
- }
1031
- let result;
1032
- try {
1033
- result = await send(saved.body, saved.key);
1034
- } catch (error) {
1035
- const status = error instanceof BridgeError && error.context.kind === "api" ? error.context.status : null;
1036
- if (status !== null && status >= 400 && status < 500 && ![401, 403, 408, 409, 429].includes(status))
1037
- await unlink2(fileOf(id));
1038
- else console.error(`This log (${id}) was not confirmed. Run the same command to retry the saved request.`);
1039
- throw error;
1040
- }
1041
- await unlink2(fileOf(id));
1042
- return result;
1043
- });
1100
+ async function recordStageFailure(stage, failure, now) {
1101
+ const attempts = stage.attempts + 1;
1102
+ const next = {
1103
+ ...stage,
1104
+ state: failure.kind === "conflict" || stage.state === "conflict" ? "conflict" : failure.kind === "reject" ? "rejected" : "waiting",
1105
+ attempts,
1106
+ lastAttemptAt: now.toISOString(),
1107
+ nextAttemptAt: nextAttempt(attempts, now),
1108
+ lastError: failure.kind === "conflict" || stage.state === "conflict" ? conflictAdvice(stage.id) : failure.reason
1109
+ };
1110
+ await writePendingStage(next);
1111
+ return next;
1044
1112
  }
1045
- async function listSavedLogs() {
1046
- await ensureAccount();
1113
+ async function forgetStage(id) {
1114
+ await rm(fileOf(id), { force: true });
1115
+ }
1116
+ async function listPendingStages() {
1047
1117
  let names;
1048
1118
  try {
1049
- names = await readdir3(savesDir());
1119
+ names = await readdir3(outboxDir());
1050
1120
  } catch (error) {
1051
- if (error.code === "ENOENT") return [];
1121
+ if (error.code === "ENOENT") return { stages: [], unreadable: [] };
1052
1122
  throw error;
1053
1123
  }
1054
- const logs = [];
1055
- for (const name of names.filter((entry) => /^[a-f0-9]{16}\.json$/.test(entry))) {
1056
- const id = name.slice(0, 16);
1057
- const saved = readSaved(await readFile3(fileOf(id), "utf8"), id);
1058
- const field = (key, test) => test(saved.body[key]) ? saved.body[key] : null;
1059
- const text2 = (value) => typeof value === "string";
1060
- logs.push({
1061
- id,
1062
- savedAt: new Date(saved.savedAt).toISOString(),
1063
- expired: Date.now() - saved.savedAt >= RECEIPT_MS,
1064
- projectId: field("projectId", text2),
1065
- durationMinutes: field("durationMinutes", Number.isSafeInteger),
1066
- startedAt: field("startedAt", text2),
1067
- date: field("date", text2)
1068
- });
1069
- }
1070
- return logs.sort((a5, b4) => Date.parse(a5.savedAt) - Date.parse(b4.savedAt));
1071
- }
1072
- async function resolveSavedLog(id, resolution) {
1073
- await ensureAccount();
1074
- if (!/^[a-f0-9]{16}$/.test(id)) throw new Error("No saved log " + id + ". See: flowmind pending-logs");
1075
- await withWriters(async () => {
1076
- let saved;
1124
+ const stages = [];
1125
+ const unreadable = [];
1126
+ for (const name of names.filter((entry) => entry.endsWith(".json"))) {
1127
+ const path = join5(outboxDir(), name);
1128
+ let text2;
1077
1129
  try {
1078
- saved = readSaved(await readFile3(fileOf(id), "utf8"), id);
1130
+ text2 = await readFile3(path, "utf8");
1079
1131
  } catch (error) {
1080
- if (error.code === "ENOENT")
1081
- throw new Error("No saved log " + id + ". See: flowmind pending-logs");
1082
- throw error;
1132
+ if (error.code === "ENOENT") continue;
1133
+ unreadable.push(path);
1134
+ continue;
1083
1135
  }
1084
- if (resolution === "discard") return unlink2(fileOf(id));
1085
- await mkdir2(join5(savesDir(), "resolved"), { recursive: true, mode: 448 });
1086
- await writeFile3(
1087
- join5(savesDir(), "resolved", id + "-" + saved.savedAt + ".json"),
1088
- JSON.stringify({ ...saved, resolution: "checked-in-activity", resolvedAt: (/* @__PURE__ */ new Date()).toISOString() }),
1089
- { mode: 384, flag: "wx" }
1090
- );
1091
- await unlink2(fileOf(id));
1092
- });
1093
- }
1094
-
1095
- // ../../packages/contracts/src/generated/validators/shared/OUWE7ZSG.js
1096
- var U = c((r2) => {
1097
- "use strict";
1098
- Object.defineProperty(r2, "__esModule", { value: true });
1099
- r2.formatNames = r2.fastFormats = r2.fullFormats = void 0;
1100
- function n7(a5, d5) {
1101
- return { validate: a5, compare: d5 };
1102
- }
1103
- r2.fullFormats = { date: n7(b4, m6), time: n7(c5(true), $2), "date-time": n7(v6(true), N2), "iso-time": n7(c5(), A2), "iso-date-time": n7(v6(), g), duration: /^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$/, uri: R2, "uri-reference": /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i, "uri-template": /^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i, url: /^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu, email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, hostname: /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i, ipv4: /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/, ipv6: /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i, regex: P4, uuid: /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i, "json-pointer": /^(?:\/(?:[^~/]|~0|~1)*)*$/, "json-pointer-uri-fragment": /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i, "relative-json-pointer": /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/, byte: w3, int32: { type: "number", validate: Z }, int64: { type: "number", validate: H2 }, float: { type: "number", validate: D2 }, double: { type: "number", validate: D2 }, password: true, binary: true };
1104
- r2.fastFormats = { ...r2.fullFormats, date: n7(/^\d\d\d\d-[0-1]\d-[0-3]\d$/, m6), time: n7(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, $2), "date-time": n7(/^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, N2), "iso-time": n7(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, A2), "iso-date-time": n7(/^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, g), uri: /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/i, "uri-reference": /^(?:(?:[a-z][a-z0-9+\-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i, email: /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i };
1105
- r2.formatNames = Object.keys(r2.fullFormats);
1106
- function y3(a5) {
1107
- return a5 % 4 === 0 && (a5 % 100 !== 0 || a5 % 400 === 0);
1108
- }
1109
- var O2 = /^(\d\d\d\d)-(\d\d)-(\d\d)$/, h10 = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1110
- function b4(a5) {
1111
- let d5 = O2.exec(a5);
1112
- if (!d5) return false;
1113
- let t8 = +d5[1], e4 = +d5[2], f3 = +d5[3];
1114
- return e4 >= 1 && e4 <= 12 && f3 >= 1 && f3 <= (e4 === 2 && y3(t8) ? 29 : h10[e4]);
1115
- }
1116
- function m6(a5, d5) {
1117
- if (a5 && d5) return a5 > d5 ? 1 : a5 < d5 ? -1 : 0;
1118
- }
1119
- var s7 = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
1120
- function c5(a5) {
1121
- return function(t8) {
1122
- let e4 = s7.exec(t8);
1123
- if (!e4) return false;
1124
- let f3 = +e4[1], u4 = +e4[2], i5 = +e4[3], M = e4[4], l4 = e4[5] === "-" ? -1 : 1, _ = +(e4[6] || 0), p6 = +(e4[7] || 0);
1125
- if (_ > 23 || p6 > 59 || a5 && !M) return false;
1126
- if (f3 <= 23 && u4 <= 59 && i5 < 60) return true;
1127
- let o11 = u4 - p6 * l4, T2 = f3 - _ * l4 - (o11 < 0 ? 1 : 0);
1128
- return (T2 === 23 || T2 === -1) && (o11 === 59 || o11 === -1) && i5 < 61;
1129
- };
1130
- }
1131
- function $2(a5, d5) {
1132
- if (!(a5 && d5)) return;
1133
- let t8 = (/* @__PURE__ */ new Date("2020-01-01T" + a5)).valueOf(), e4 = (/* @__PURE__ */ new Date("2020-01-01T" + d5)).valueOf();
1134
- if (t8 && e4) return t8 - e4;
1135
- }
1136
- function A2(a5, d5) {
1137
- if (!(a5 && d5)) return;
1138
- let t8 = s7.exec(a5), e4 = s7.exec(d5);
1139
- if (t8 && e4) return a5 = t8[1] + t8[2] + t8[3], d5 = e4[1] + e4[2] + e4[3], a5 > d5 ? 1 : a5 < d5 ? -1 : 0;
1140
- }
1141
- var z = /t|\s/i;
1142
- function v6(a5) {
1143
- let d5 = c5(a5);
1144
- return function(e4) {
1145
- let f3 = e4.split(z);
1146
- return f3.length === 2 && b4(f3[0]) && d5(f3[1]);
1147
- };
1148
- }
1149
- function N2(a5, d5) {
1150
- if (!(a5 && d5)) return;
1151
- let t8 = new Date(a5).valueOf(), e4 = new Date(d5).valueOf();
1152
- if (t8 && e4) return t8 - e4;
1153
- }
1154
- function g(a5, d5) {
1155
- if (!(a5 && d5)) return;
1156
- let [t8, e4] = a5.split(z), [f3, u4] = d5.split(z), i5 = m6(t8, f3);
1157
- if (i5 !== void 0) return i5 || $2(e4, u4);
1158
- }
1159
- var E = /\/|:/, F2 = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
1160
- function R2(a5) {
1161
- return E.test(a5) && F2.test(a5);
1162
- }
1163
- var I2 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
1164
- function w3(a5) {
1165
- return I2.lastIndex = 0, I2.test(a5);
1166
- }
1167
- var S3 = -(2 ** 31), j2 = 2 ** 31 - 1;
1168
- function Z(a5) {
1169
- return Number.isInteger(a5) && a5 <= j2 && a5 >= S3;
1170
- }
1171
- function H2(a5) {
1172
- return Number.isInteger(a5);
1173
- }
1174
- function D2() {
1175
- return true;
1176
- }
1177
- var Y = /[^\\]\\Z/;
1178
- function P4(a5) {
1179
- if (Y.test(a5)) return false;
1136
+ let value;
1180
1137
  try {
1181
- return new RegExp(a5), true;
1138
+ value = JSON.parse(text2);
1182
1139
  } catch {
1183
- return false;
1140
+ value = null;
1184
1141
  }
1142
+ const stage = readStage(value, name);
1143
+ if (stage && stage.apiUrl === API_URL) stages.push(stage);
1144
+ else unreadable.push(path);
1185
1145
  }
1186
- });
1187
-
1188
- // ../../packages/contracts/src/generated/validators/shared/J43X2ZUC.js
1189
- var t3 = U().fullFormats.date;
1190
-
1191
- // ../../packages/contracts/src/generated/validators/shared/HNC2DQA2.js
1192
- function s(e4, { instancePath: a5 = "", parentData: n7, parentDataProperty: u4, rootData: l4 = e4 } = {}) {
1193
- let m6 = null, r2 = 0;
1194
- if (r2 === 0) if (e4 && typeof e4 == "object" && !Array.isArray(e4)) {
1195
- let i5;
1196
- if (e4.text === void 0 && (i5 = "text") || e4.sourceDay === void 0 && (i5 = "sourceDay")) return s.errors = [{ instancePath: a5, schemaPath: "#/required", keyword: "required", params: { missingProperty: i5 }, message: "must have required property '" + i5 + "'" }], false;
1197
- {
1198
- let y3 = r2;
1199
- for (let t8 in e4) if (!(t8 === "sourceDay" || t8 === "text")) {
1200
- return s.errors = [{ instancePath: a5, schemaPath: "#/additionalProperties", keyword: "additionalProperties", params: { additionalProperty: t8 }, message: "must NOT have additional properties" }], false;
1201
- break;
1146
+ return { stages: stages.sort((a5, b4) => Date.parse(a5.savedAt) - Date.parse(b4.savedAt)), unreadable };
1147
+ }
1148
+ async function restoreSavedStageMarks(measuring = false) {
1149
+ const { stages, unreadable } = await listPendingStages();
1150
+ if (measuring && unreadable.length) {
1151
+ throw new Error(
1152
+ "Cannot measure new work: unreadable reservation " + unreadable[0] + ". Check Activity. Restore the original file and run flowmind agent retry, or discard the reservation with flowmind agent discard " + basename(unreadable[0], ".json") + ". An unowned original needs flowmind agent adopt <id>."
1153
+ );
1154
+ }
1155
+ for (const stage of stages) {
1156
+ if (stage.sessionId === null || stage.cut === null) continue;
1157
+ const mark = { at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id };
1158
+ if (await restoreStageMark(stage.sessionId, mark) === "accepted") {
1159
+ await forgetStage(stage.id);
1160
+ continue;
1161
+ }
1162
+ if ((stage.state === "rejected" || stage.state === "conflict") && stage.lastError !== null) {
1163
+ await settleStageMark(stage.sessionId, mark, { state: stage.state, reason: stage.lastError });
1164
+ }
1165
+ }
1166
+ }
1167
+ var isDue = (stage, now) => stage.state === "waiting" && stage.apiUrl === API_URL && Date.parse(stage.nextAttemptAt) <= now.getTime();
1168
+ async function sendPendingStages(options) {
1169
+ const clock = options.now ?? (() => /* @__PURE__ */ new Date());
1170
+ const count = async () => {
1171
+ const { stages, unreadable } = await listPendingStages();
1172
+ return { waiting: stages.filter((stage) => stage.state === "waiting").length, unreadable };
1173
+ };
1174
+ const result = {
1175
+ busy: false,
1176
+ sent: [],
1177
+ rejected: [],
1178
+ conflicts: [],
1179
+ stoppedBy: null,
1180
+ waiting: 0,
1181
+ unreadable: []
1182
+ };
1183
+ const lock = await takeStageLock();
1184
+ if (!lock) return { ...result, busy: true, ...await count() };
1185
+ try {
1186
+ await restoreSavedStageMarks();
1187
+ const candidates = (await listPendingStages()).stages.filter((stage) => {
1188
+ const now = clock();
1189
+ if (stage.state === "conflict") {
1190
+ if (options.manual && stage.credentialFingerprint === accountBinding().credential) return true;
1191
+ if (options.manual) result.conflicts.push({ id: stage.id, reason: conflictAdvice(stage.id) });
1192
+ return false;
1202
1193
  }
1203
- if (y3 === r2) {
1204
- if (e4.sourceDay !== void 0) {
1205
- let t8 = e4.sourceDay, p6 = r2;
1206
- if (r2 === p6 && r2 === p6) if (typeof t8 == "string") {
1207
- if (!t3.validate(t8)) return s.errors = [{ instancePath: a5 + "/sourceDay", schemaPath: "#/properties/sourceDay/format", keyword: "format", params: { format: "date" }, message: 'must match format "date"' }], false;
1208
- } else return s.errors = [{ instancePath: a5 + "/sourceDay", schemaPath: "#/properties/sourceDay/type", keyword: "type", params: { type: "string" }, message: "must be string" }], false;
1209
- var o11 = p6 === r2;
1210
- } else var o11 = true;
1211
- if (o11) if (e4.text !== void 0) {
1212
- let t8 = r2;
1213
- if (typeof e4.text != "string") return s.errors = [{ instancePath: a5 + "/text", schemaPath: "#/properties/text/type", keyword: "type", params: { type: "string" }, message: "must be string" }], false;
1214
- var o11 = t8 === r2;
1215
- } else var o11 = true;
1194
+ return options.due ? isDue(stage, now) : stage.state === "waiting" && stage.apiUrl === API_URL;
1195
+ });
1196
+ for (const stage of candidates) {
1197
+ let proposal;
1198
+ try {
1199
+ proposal = await createAgentLogProposal(stage.input, stage.idempotencyKey);
1200
+ } catch (error) {
1201
+ const failure = classifyStageFailure(error);
1202
+ const saved = await recordStageFailure(stage, failure, clock());
1203
+ if (saved.state === "conflict") {
1204
+ if (stage.sessionId && stage.cut)
1205
+ await settleStageMark(
1206
+ stage.sessionId,
1207
+ { at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id },
1208
+ { state: "conflict", reason: conflictAdvice(stage.id) }
1209
+ );
1210
+ result.conflicts.push({ id: stage.id, reason: conflictAdvice(stage.id) });
1211
+ continue;
1212
+ }
1213
+ if (failure.kind === "reject") {
1214
+ if (stage.sessionId && stage.cut)
1215
+ await settleStageMark(
1216
+ stage.sessionId,
1217
+ { at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id },
1218
+ { state: "rejected", reason: failure.reason }
1219
+ );
1220
+ result.rejected.push({ id: stage.id, reason: failure.reason });
1221
+ continue;
1222
+ }
1223
+ result.stoppedBy = failure.reason;
1224
+ break;
1216
1225
  }
1226
+ if (stage.sessionId && stage.cut)
1227
+ await settleStageMark(
1228
+ stage.sessionId,
1229
+ { at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id },
1230
+ { state: "accepted", proposalId: proposal.id }
1231
+ );
1232
+ await forgetStage(stage.id);
1233
+ result.sent.push({ id: stage.id, proposal });
1217
1234
  }
1218
- } else return s.errors = [{ instancePath: a5, schemaPath: "#/type", keyword: "type", params: { type: "object" }, message: "must be object" }], false;
1219
- return s.errors = m6, r2 === 0;
1235
+ } finally {
1236
+ await lock.release();
1237
+ }
1238
+ return { ...result, ...await count() };
1239
+ }
1240
+ var conflictAdvice = (id) => "Stage key conflict (" + id + "). Delivery is unknown. Check Activity, then retry with the original credential or run flowmind agent discard " + id + ".";
1241
+ async function discardReservation(id) {
1242
+ if (!id || basename(id) !== id || id === "." || id === "..") return false;
1243
+ const { stages, unreadable } = await listPendingStages();
1244
+ const stage = stages.find((item) => item.id === id);
1245
+ if (!stage && !unreadable.includes(fileOf(id))) return false;
1246
+ if (stage && stage.state !== "conflict")
1247
+ throw new Error("This account already has a saved proof " + id + ". See: flowmind agent pending");
1248
+ if (stage?.sessionId && stage.cut)
1249
+ await settleStageMark(
1250
+ stage.sessionId,
1251
+ { at: stage.savedAt, cut: stage.cut, pendingStageId: id },
1252
+ { state: "discarded", reason: "Discarded after checking Activity." }
1253
+ );
1254
+ await forgetStage(id);
1255
+ return true;
1256
+ }
1257
+ async function startDueRetry(now) {
1258
+ const { stages } = await listPendingStages();
1259
+ if (!stages.some((stage) => isDue(stage, now))) return false;
1260
+ const script = process.argv[1];
1261
+ if (!script) throw new Error("Cannot find the Flowmind CLI script to send saved stages");
1262
+ const child = spawn(process.execPath, [script, "agent", "retry", "--due"], {
1263
+ detached: true,
1264
+ stdio: "ignore",
1265
+ windowsHide: true
1266
+ });
1267
+ child.on("error", (error) => {
1268
+ console.error("Flowmind could not start sending saved stages: " + error.message);
1269
+ process.exitCode = 1;
1270
+ });
1271
+ child.unref();
1272
+ return true;
1220
1273
  }
1221
1274
 
1222
- // ../../packages/contracts/src/generated/validators/shared/VSRNQHIL.js
1223
- var e = U().fullFormats["date-time"];
1224
-
1225
- // ../../packages/contracts/src/generated/validators/shared/CA7CC2WJ.js
1226
- var a2 = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;
1227
-
1228
- // ../../packages/contracts/src/generated/validators/getV1Projects200.js
1229
- var D = { description: "A listed project with its time totals. totalSeconds counts every non-voided entry, including approved agent work; agentSeconds is the agent share of that total.", properties: { agentSeconds: { minimum: 0, type: "integer" }, archivedAt: { format: "date-time", nullable: true, type: "string" }, category: { type: "string" }, color: { type: "string" }, completedAt: { format: "date-time", nullable: true, type: "string" }, createdAt: { format: "date-time", type: "string" }, description: { nullable: true, type: "string" }, details: { description: "Private details for lastOutcome, or an empty array when no outcome exists.", items: { type: "string" }, type: "array" }, estimatedHours: { nullable: true, type: "integer" }, icon: { type: "string" }, id: { format: "uuid", type: "string" }, lastActiveDay: { format: "date", nullable: true, type: "string" }, lastOutcome: { nullable: true, type: "string" }, launchDescription: { nullable: true, type: "string" }, launchUrl: { nullable: true, type: "string" }, launchedAt: { format: "date-time", nullable: true, type: "string" }, name: { type: "string" }, nextStep: { anyOf: [{ $ref: "urn:flowmind:schema:ProjectNextStep" }, { enum: [null], nullable: true, type: "object" }] }, totalSeconds: { minimum: 0, type: "integer" }, updatedAt: { format: "date-time", type: "string" } }, required: ["id", "name", "description", "color", "icon", "category", "estimatedHours", "launchedAt", "completedAt", "launchDescription", "launchUrl", "createdAt", "updatedAt", "archivedAt", "nextStep", "lastOutcome", "details", "lastActiveDay", "totalSeconds", "agentSeconds"], type: "object" };
1230
- function n(t8, { instancePath: i5 = "", parentData: w3, parentDataProperty: k3, rootData: d5 = t8 } = {}) {
1231
- let o11 = null, r2 = 0;
1232
- if (r2 === 0) if (t8 && typeof t8 == "object" && !Array.isArray(t8)) {
1275
+ // src/legacy-proofs.ts
1276
+ async function legacyStages() {
1277
+ let names;
1278
+ try {
1279
+ names = await readdir4(legacyOutboxDir());
1280
+ } catch (error) {
1281
+ if (error.code === "ENOENT") return { stages: [], unreadable: [] };
1282
+ throw error;
1283
+ }
1284
+ const stages = [];
1285
+ const unreadable = [];
1286
+ for (const name of names.filter((entry) => entry.endsWith(".json"))) {
1287
+ const path = join6(legacyOutboxDir(), name);
1288
+ let value = null;
1289
+ try {
1290
+ value = JSON.parse(await readFile4(path, "utf8"));
1291
+ } catch (error) {
1292
+ if (error.code === "ENOENT") continue;
1293
+ if (!(error instanceof SyntaxError)) throw error;
1294
+ }
1295
+ const stage = readStage(value, name, true);
1296
+ if (stage) stages.push(stage);
1297
+ else unreadable.push(path);
1298
+ }
1299
+ return { stages, unreadable };
1300
+ }
1301
+ async function moveIntoAccount(stage) {
1302
+ await mkdir3(outboxDir(), { recursive: true, mode: 448 });
1303
+ const target = join6(outboxDir(), stage.id + ".json");
1304
+ const present = await stat2(target).then(
1305
+ () => true,
1306
+ (error) => {
1307
+ if (error.code === "ENOENT") return false;
1308
+ throw error;
1309
+ }
1310
+ );
1311
+ if (present) return false;
1312
+ await rename2(join6(legacyOutboxDir(), stage.id + ".json"), target);
1313
+ await writePendingStage({ ...stage, credentialFingerprint: accountBinding().credential });
1314
+ return true;
1315
+ }
1316
+ async function adoptLegacyProofs() {
1317
+ for (const stage of (await legacyStages()).stages) if (stage.apiUrl === API_URL) await moveIntoAccount(stage);
1318
+ }
1319
+ async function listUnownedProofs() {
1320
+ const { stages, unreadable } = await legacyStages();
1321
+ const unowned = stages.map((stage) => ({
1322
+ id: stage.id,
1323
+ projectId: stage.input.projectId,
1324
+ durationMinutes: stage.input.durationMinutes,
1325
+ savedAt: stage.savedAt,
1326
+ apiUrl: stage.apiUrl
1327
+ })).sort((a5, b4) => Date.parse(a5.savedAt) - Date.parse(b4.savedAt));
1328
+ return { unowned, unreadable };
1329
+ }
1330
+ async function findUnowned(id) {
1331
+ if (!/^[a-f0-9]{16}$/.test(id)) throw new Error("No unowned saved proof " + id + ". See: flowmind agent pending");
1332
+ const stage = (await legacyStages()).stages.find((candidate) => candidate.id === id);
1333
+ if (!stage) throw new Error("No unowned saved proof " + id + ". See: flowmind agent pending");
1334
+ return stage;
1335
+ }
1336
+ async function adoptUnownedProof(id) {
1337
+ const stage = await findUnowned(id);
1338
+ if (stage.apiUrl !== API_URL)
1339
+ throw new Error("Saved proof " + id + " belongs to " + stage.apiUrl + ", not " + API_URL + ".");
1340
+ if (!await moveIntoAccount(stage))
1341
+ throw new Error("This account already has a reservation for " + id + ". Check Activity before adopting it.");
1342
+ }
1343
+ async function discardUnownedProof(id) {
1344
+ await findUnowned(id);
1345
+ await rm2(join6(legacyOutboxDir(), id + ".json"));
1346
+ }
1347
+
1348
+ // src/account.ts
1349
+ var bindingStartNow = () => ({ at: (/* @__PURE__ */ new Date()).toISOString(), up: awakeMs() });
1350
+ async function withConfigWriters(change) {
1351
+ let lock = await configWriters();
1352
+ for (let waited = 0; !lock && waited < 15e3; waited += 250) {
1353
+ await new Promise((resolve3) => setTimeout(resolve3, 250));
1354
+ lock = await configWriters();
1355
+ }
1356
+ if (!lock) throw new Error("Another Flowmind process is changing the sign-in. Try again when it finishes.");
1357
+ try {
1358
+ return await change();
1359
+ } finally {
1360
+ await lock.release();
1361
+ }
1362
+ }
1363
+ async function ensureAccount() {
1364
+ if (!accountBinding()) await learnAccount();
1365
+ if (!await legacyStateWaits()) return;
1366
+ const lock = await takeStageLock();
1367
+ if (!lock) return;
1368
+ try {
1369
+ await adoptLegacyStateHeld();
1370
+ } finally {
1371
+ await lock.release();
1372
+ }
1373
+ }
1374
+ async function learnAccount() {
1375
+ const key = requireApiKey();
1376
+ await withConfigWriters(async () => {
1377
+ const before = config.store;
1378
+ if (before.binding !== void 0 || before.apiKey !== key || (before.apiUrl ?? "https://api.flowmind.pro") !== API_URL) {
1379
+ throw new Error("The sign-in changed while Flowmind checked it. Run the command again.");
1380
+ }
1381
+ let account;
1382
+ try {
1383
+ account = (await apiRequest(getV1Me, {}, { signal: AbortSignal.timeout(1e4) })).userId;
1384
+ } catch (error) {
1385
+ if (error instanceof BridgeError && error.context.kind === "api" && error.context.status === 401) {
1386
+ throw new Error("Flowmind refused the saved sign-in. Run: flowmind auth login");
1387
+ }
1388
+ throw new Error(
1389
+ "Could not verify the signed-in account: " + (error instanceof Error ? error.message : String(error))
1390
+ );
1391
+ }
1392
+ const current = config.store;
1393
+ if (current.apiKey !== key || (current.apiUrl ?? "https://api.flowmind.pro") !== API_URL) {
1394
+ throw new Error("The sign-in changed while Flowmind checked it. Run the command again.");
1395
+ }
1396
+ bindAccount(key, account, bindingStartNow());
1397
+ });
1398
+ }
1399
+ async function holdsFiles(directory, suffix) {
1400
+ try {
1401
+ return (await readdir5(directory)).some((name) => name.endsWith(suffix));
1402
+ } catch (error) {
1403
+ if (error.code === "ENOENT") return false;
1404
+ throw error;
1405
+ }
1406
+ }
1407
+ async function legacyStateWaits() {
1408
+ return soleAccountProfile() && (await legacyJournalsWaiting(legacySessionsDir()) || await holdsFiles(legacyOutboxDir(), ".json"));
1409
+ }
1410
+ async function adoptLegacyStateHeld() {
1411
+ if (await legacyStateWaits()) {
1412
+ await adoptLegacyJournals(
1413
+ legacySessionsDir(),
1414
+ (earliest) => withConfigWriters(() => extendSignInStart({ at: new Date(earliest).toISOString(), up: null }))
1415
+ );
1416
+ await adoptLegacyProofs();
1417
+ }
1418
+ refreshSignInStart();
1419
+ }
1420
+
1421
+ // src/manual-save.ts
1422
+ var RECEIPT_MS = 24 * 60 * 6e4;
1423
+ var savesDir = () => join7(accountDirectory(), "manual-saves");
1424
+ var fileOf2 = (id) => join7(savesDir(), id + ".json");
1425
+ async function withWriters(work) {
1426
+ await mkdir4(savesDir(), { recursive: true, mode: 448 });
1427
+ const lock = await tryLock(join7(savesDir(), "writers"));
1428
+ if (!lock) throw new Error("Another Flowmind process is saving a log. Try again when it finishes.");
1429
+ try {
1430
+ return await work();
1431
+ } finally {
1432
+ await lock.release();
1433
+ }
1434
+ }
1435
+ function readSaved(text2, id) {
1436
+ const saved = JSON.parse(text2);
1437
+ if (!saved || saved.id !== id || !Number.isFinite(saved.savedAt) || typeof saved.key !== "string" || typeof saved.body !== "object" || saved.body === null) {
1438
+ throw new Error("Unreadable saved log " + id + " in " + savesDir());
1439
+ }
1440
+ return saved;
1441
+ }
1442
+ async function manualSave(identity, prepare, send) {
1443
+ await ensureAccount();
1444
+ const id = createHash3("sha256").update(JSON.stringify(identity)).digest("hex").slice(0, 16);
1445
+ return withWriters(async () => {
1446
+ let saved;
1447
+ try {
1448
+ saved = readSaved(await readFile5(fileOf2(id), "utf8"), id);
1449
+ } catch (error) {
1450
+ if (error.code !== "ENOENT") throw error;
1451
+ const body = await prepare();
1452
+ if (body === null) return null;
1453
+ saved = { id, body, key: randomUUID3(), savedAt: Date.now() };
1454
+ const temp = fileOf2(id) + "." + randomUUID3() + ".tmp";
1455
+ await writeFile4(temp, JSON.stringify(saved), { mode: 384, flag: "wx" });
1456
+ await rename3(temp, fileOf2(id));
1457
+ }
1458
+ if (Date.now() - saved.savedAt >= RECEIPT_MS) {
1459
+ throw new Error(
1460
+ `This log (${id}) was not confirmed within 24 hours. Check Activity, then run flowmind pending-logs done ${id} if it is there, or flowmind pending-logs discard ${id} to log it again.`
1461
+ );
1462
+ }
1463
+ let result;
1464
+ try {
1465
+ result = await send(saved.body, saved.key);
1466
+ } catch (error) {
1467
+ const status = error instanceof BridgeError && error.context.kind === "api" ? error.context.status : null;
1468
+ if (status !== null && status >= 400 && status < 500 && ![401, 403, 408, 409, 429].includes(status))
1469
+ await unlink2(fileOf2(id));
1470
+ else console.error(`This log (${id}) was not confirmed. Run the same command to retry the saved request.`);
1471
+ throw error;
1472
+ }
1473
+ await unlink2(fileOf2(id));
1474
+ return result;
1475
+ });
1476
+ }
1477
+ async function listSavedLogs() {
1478
+ await ensureAccount();
1479
+ let names;
1480
+ try {
1481
+ names = await readdir6(savesDir());
1482
+ } catch (error) {
1483
+ if (error.code === "ENOENT") return [];
1484
+ throw error;
1485
+ }
1486
+ const logs = [];
1487
+ for (const name of names.filter((entry) => /^[a-f0-9]{16}\.json$/.test(entry))) {
1488
+ const id = name.slice(0, 16);
1489
+ const saved = readSaved(await readFile5(fileOf2(id), "utf8"), id);
1490
+ const field = (key, test) => test(saved.body[key]) ? saved.body[key] : null;
1491
+ const text2 = (value) => typeof value === "string";
1492
+ logs.push({
1493
+ id,
1494
+ savedAt: new Date(saved.savedAt).toISOString(),
1495
+ expired: Date.now() - saved.savedAt >= RECEIPT_MS,
1496
+ projectId: field("projectId", text2),
1497
+ durationMinutes: field("durationMinutes", Number.isSafeInteger),
1498
+ startedAt: field("startedAt", text2),
1499
+ date: field("date", text2)
1500
+ });
1501
+ }
1502
+ return logs.sort((a5, b4) => Date.parse(a5.savedAt) - Date.parse(b4.savedAt));
1503
+ }
1504
+ async function resolveSavedLog(id, resolution) {
1505
+ await ensureAccount();
1506
+ if (!/^[a-f0-9]{16}$/.test(id)) throw new Error("No saved log " + id + ". See: flowmind pending-logs");
1507
+ await withWriters(async () => {
1508
+ let saved;
1509
+ try {
1510
+ saved = readSaved(await readFile5(fileOf2(id), "utf8"), id);
1511
+ } catch (error) {
1512
+ if (error.code === "ENOENT")
1513
+ throw new Error("No saved log " + id + ". See: flowmind pending-logs");
1514
+ throw error;
1515
+ }
1516
+ if (resolution === "discard") return unlink2(fileOf2(id));
1517
+ await mkdir4(join7(savesDir(), "resolved"), { recursive: true, mode: 448 });
1518
+ await writeFile4(
1519
+ join7(savesDir(), "resolved", id + "-" + saved.savedAt + ".json"),
1520
+ JSON.stringify({ ...saved, resolution: "checked-in-activity", resolvedAt: (/* @__PURE__ */ new Date()).toISOString() }),
1521
+ { mode: 384, flag: "wx" }
1522
+ );
1523
+ await unlink2(fileOf2(id));
1524
+ });
1525
+ }
1526
+
1527
+ // ../../packages/contracts/src/generated/validators/shared/OUWE7ZSG.js
1528
+ var U = c((r2) => {
1529
+ "use strict";
1530
+ Object.defineProperty(r2, "__esModule", { value: true });
1531
+ r2.formatNames = r2.fastFormats = r2.fullFormats = void 0;
1532
+ function n7(a5, d5) {
1533
+ return { validate: a5, compare: d5 };
1534
+ }
1535
+ r2.fullFormats = { date: n7(b4, m6), time: n7(c5(true), $2), "date-time": n7(v6(true), N2), "iso-time": n7(c5(), A2), "iso-date-time": n7(v6(), g), duration: /^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$/, uri: R2, "uri-reference": /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i, "uri-template": /^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i, url: /^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu, email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, hostname: /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i, ipv4: /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/, ipv6: /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i, regex: P4, uuid: /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i, "json-pointer": /^(?:\/(?:[^~/]|~0|~1)*)*$/, "json-pointer-uri-fragment": /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i, "relative-json-pointer": /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/, byte: w3, int32: { type: "number", validate: Z }, int64: { type: "number", validate: H2 }, float: { type: "number", validate: D2 }, double: { type: "number", validate: D2 }, password: true, binary: true };
1536
+ r2.fastFormats = { ...r2.fullFormats, date: n7(/^\d\d\d\d-[0-1]\d-[0-3]\d$/, m6), time: n7(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, $2), "date-time": n7(/^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, N2), "iso-time": n7(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, A2), "iso-date-time": n7(/^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, g), uri: /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/i, "uri-reference": /^(?:(?:[a-z][a-z0-9+\-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i, email: /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i };
1537
+ r2.formatNames = Object.keys(r2.fullFormats);
1538
+ function y3(a5) {
1539
+ return a5 % 4 === 0 && (a5 % 100 !== 0 || a5 % 400 === 0);
1540
+ }
1541
+ var O2 = /^(\d\d\d\d)-(\d\d)-(\d\d)$/, h10 = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1542
+ function b4(a5) {
1543
+ let d5 = O2.exec(a5);
1544
+ if (!d5) return false;
1545
+ let t8 = +d5[1], e4 = +d5[2], f3 = +d5[3];
1546
+ return e4 >= 1 && e4 <= 12 && f3 >= 1 && f3 <= (e4 === 2 && y3(t8) ? 29 : h10[e4]);
1547
+ }
1548
+ function m6(a5, d5) {
1549
+ if (a5 && d5) return a5 > d5 ? 1 : a5 < d5 ? -1 : 0;
1550
+ }
1551
+ var s7 = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
1552
+ function c5(a5) {
1553
+ return function(t8) {
1554
+ let e4 = s7.exec(t8);
1555
+ if (!e4) return false;
1556
+ let f3 = +e4[1], u4 = +e4[2], i5 = +e4[3], M = e4[4], l4 = e4[5] === "-" ? -1 : 1, _ = +(e4[6] || 0), p6 = +(e4[7] || 0);
1557
+ if (_ > 23 || p6 > 59 || a5 && !M) return false;
1558
+ if (f3 <= 23 && u4 <= 59 && i5 < 60) return true;
1559
+ let o11 = u4 - p6 * l4, T2 = f3 - _ * l4 - (o11 < 0 ? 1 : 0);
1560
+ return (T2 === 23 || T2 === -1) && (o11 === 59 || o11 === -1) && i5 < 61;
1561
+ };
1562
+ }
1563
+ function $2(a5, d5) {
1564
+ if (!(a5 && d5)) return;
1565
+ let t8 = (/* @__PURE__ */ new Date("2020-01-01T" + a5)).valueOf(), e4 = (/* @__PURE__ */ new Date("2020-01-01T" + d5)).valueOf();
1566
+ if (t8 && e4) return t8 - e4;
1567
+ }
1568
+ function A2(a5, d5) {
1569
+ if (!(a5 && d5)) return;
1570
+ let t8 = s7.exec(a5), e4 = s7.exec(d5);
1571
+ if (t8 && e4) return a5 = t8[1] + t8[2] + t8[3], d5 = e4[1] + e4[2] + e4[3], a5 > d5 ? 1 : a5 < d5 ? -1 : 0;
1572
+ }
1573
+ var z = /t|\s/i;
1574
+ function v6(a5) {
1575
+ let d5 = c5(a5);
1576
+ return function(e4) {
1577
+ let f3 = e4.split(z);
1578
+ return f3.length === 2 && b4(f3[0]) && d5(f3[1]);
1579
+ };
1580
+ }
1581
+ function N2(a5, d5) {
1582
+ if (!(a5 && d5)) return;
1583
+ let t8 = new Date(a5).valueOf(), e4 = new Date(d5).valueOf();
1584
+ if (t8 && e4) return t8 - e4;
1585
+ }
1586
+ function g(a5, d5) {
1587
+ if (!(a5 && d5)) return;
1588
+ let [t8, e4] = a5.split(z), [f3, u4] = d5.split(z), i5 = m6(t8, f3);
1589
+ if (i5 !== void 0) return i5 || $2(e4, u4);
1590
+ }
1591
+ var E = /\/|:/, F2 = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
1592
+ function R2(a5) {
1593
+ return E.test(a5) && F2.test(a5);
1594
+ }
1595
+ var I2 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
1596
+ function w3(a5) {
1597
+ return I2.lastIndex = 0, I2.test(a5);
1598
+ }
1599
+ var S3 = -(2 ** 31), j2 = 2 ** 31 - 1;
1600
+ function Z(a5) {
1601
+ return Number.isInteger(a5) && a5 <= j2 && a5 >= S3;
1602
+ }
1603
+ function H2(a5) {
1604
+ return Number.isInteger(a5);
1605
+ }
1606
+ function D2() {
1607
+ return true;
1608
+ }
1609
+ var Y = /[^\\]\\Z/;
1610
+ function P4(a5) {
1611
+ if (Y.test(a5)) return false;
1612
+ try {
1613
+ return new RegExp(a5), true;
1614
+ } catch {
1615
+ return false;
1616
+ }
1617
+ }
1618
+ });
1619
+
1620
+ // ../../packages/contracts/src/generated/validators/shared/J43X2ZUC.js
1621
+ var t3 = U().fullFormats.date;
1622
+
1623
+ // ../../packages/contracts/src/generated/validators/shared/HNC2DQA2.js
1624
+ function s(e4, { instancePath: a5 = "", parentData: n7, parentDataProperty: u4, rootData: l4 = e4 } = {}) {
1625
+ let m6 = null, r2 = 0;
1626
+ if (r2 === 0) if (e4 && typeof e4 == "object" && !Array.isArray(e4)) {
1627
+ let i5;
1628
+ if (e4.text === void 0 && (i5 = "text") || e4.sourceDay === void 0 && (i5 = "sourceDay")) return s.errors = [{ instancePath: a5, schemaPath: "#/required", keyword: "required", params: { missingProperty: i5 }, message: "must have required property '" + i5 + "'" }], false;
1629
+ {
1630
+ let y3 = r2;
1631
+ for (let t8 in e4) if (!(t8 === "sourceDay" || t8 === "text")) {
1632
+ return s.errors = [{ instancePath: a5, schemaPath: "#/additionalProperties", keyword: "additionalProperties", params: { additionalProperty: t8 }, message: "must NOT have additional properties" }], false;
1633
+ break;
1634
+ }
1635
+ if (y3 === r2) {
1636
+ if (e4.sourceDay !== void 0) {
1637
+ let t8 = e4.sourceDay, p6 = r2;
1638
+ if (r2 === p6 && r2 === p6) if (typeof t8 == "string") {
1639
+ if (!t3.validate(t8)) return s.errors = [{ instancePath: a5 + "/sourceDay", schemaPath: "#/properties/sourceDay/format", keyword: "format", params: { format: "date" }, message: 'must match format "date"' }], false;
1640
+ } else return s.errors = [{ instancePath: a5 + "/sourceDay", schemaPath: "#/properties/sourceDay/type", keyword: "type", params: { type: "string" }, message: "must be string" }], false;
1641
+ var o11 = p6 === r2;
1642
+ } else var o11 = true;
1643
+ if (o11) if (e4.text !== void 0) {
1644
+ let t8 = r2;
1645
+ if (typeof e4.text != "string") return s.errors = [{ instancePath: a5 + "/text", schemaPath: "#/properties/text/type", keyword: "type", params: { type: "string" }, message: "must be string" }], false;
1646
+ var o11 = t8 === r2;
1647
+ } else var o11 = true;
1648
+ }
1649
+ }
1650
+ } else return s.errors = [{ instancePath: a5, schemaPath: "#/type", keyword: "type", params: { type: "object" }, message: "must be object" }], false;
1651
+ return s.errors = m6, r2 === 0;
1652
+ }
1653
+
1654
+ // ../../packages/contracts/src/generated/validators/shared/VSRNQHIL.js
1655
+ var e = U().fullFormats["date-time"];
1656
+
1657
+ // ../../packages/contracts/src/generated/validators/shared/CA7CC2WJ.js
1658
+ var a2 = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;
1659
+
1660
+ // ../../packages/contracts/src/generated/validators/getV1Projects200.js
1661
+ var D = { description: "A listed project with its time totals. totalSeconds counts every non-voided entry, including approved agent work; agentSeconds is the agent share of that total.", properties: { agentSeconds: { minimum: 0, type: "integer" }, archivedAt: { format: "date-time", nullable: true, type: "string" }, category: { type: "string" }, color: { type: "string" }, completedAt: { format: "date-time", nullable: true, type: "string" }, createdAt: { format: "date-time", type: "string" }, description: { nullable: true, type: "string" }, details: { description: "Private details for lastOutcome, or an empty array when no outcome exists.", items: { type: "string" }, type: "array" }, estimatedHours: { nullable: true, type: "integer" }, icon: { type: "string" }, id: { format: "uuid", type: "string" }, lastActiveDay: { format: "date", nullable: true, type: "string" }, lastOutcome: { nullable: true, type: "string" }, launchDescription: { nullable: true, type: "string" }, launchUrl: { nullable: true, type: "string" }, launchedAt: { format: "date-time", nullable: true, type: "string" }, name: { type: "string" }, nextStep: { anyOf: [{ $ref: "urn:flowmind:schema:ProjectNextStep" }, { enum: [null], nullable: true, type: "object" }] }, totalSeconds: { minimum: 0, type: "integer" }, updatedAt: { format: "date-time", type: "string" } }, required: ["id", "name", "description", "color", "icon", "category", "estimatedHours", "launchedAt", "completedAt", "launchDescription", "launchUrl", "createdAt", "updatedAt", "archivedAt", "nextStep", "lastOutcome", "details", "lastActiveDay", "totalSeconds", "agentSeconds"], type: "object" };
1662
+ function n(t8, { instancePath: i5 = "", parentData: w3, parentDataProperty: k3, rootData: d5 = t8 } = {}) {
1663
+ let o11 = null, r2 = 0;
1664
+ if (r2 === 0) if (t8 && typeof t8 == "object" && !Array.isArray(t8)) {
1233
1665
  let l4;
1234
1666
  if (t8.id === void 0 && (l4 = "id") || t8.name === void 0 && (l4 = "name") || t8.description === void 0 && (l4 = "description") || t8.color === void 0 && (l4 = "color") || t8.icon === void 0 && (l4 = "icon") || t8.category === void 0 && (l4 = "category") || t8.estimatedHours === void 0 && (l4 = "estimatedHours") || t8.launchedAt === void 0 && (l4 = "launchedAt") || t8.completedAt === void 0 && (l4 = "completedAt") || t8.launchDescription === void 0 && (l4 = "launchDescription") || t8.launchUrl === void 0 && (l4 = "launchUrl") || t8.createdAt === void 0 && (l4 = "createdAt") || t8.updatedAt === void 0 && (l4 = "updatedAt") || t8.archivedAt === void 0 && (l4 = "archivedAt") || t8.nextStep === void 0 && (l4 = "nextStep") || t8.lastOutcome === void 0 && (l4 = "lastOutcome") || t8.details === void 0 && (l4 = "details") || t8.lastActiveDay === void 0 && (l4 = "lastActiveDay") || t8.totalSeconds === void 0 && (l4 = "totalSeconds") || t8.agentSeconds === void 0 && (l4 = "agentSeconds")) return n.errors = [{ instancePath: i5, schemaPath: "#/required", keyword: "required", params: { missingProperty: l4 }, message: "must have required property '" + l4 + "'" }], false;
1235
1667
  if (t8.agentSeconds !== void 0) {
@@ -3268,1165 +3700,925 @@ function h7(r2, { instancePath: s7 = "", parentData: P4, parentDataProperty: w3,
3268
3700
  let p6 = null, e4 = 0, a5 = e4;
3269
3701
  i(r2, { instancePath: s7, parentData: P4, parentDataProperty: w3, rootData: g }) || (p6 = p6 === null ? i.errors : p6.concat(i.errors), e4 = p6.length);
3270
3702
  var f3 = a5 === e4;
3271
- if (f3) {
3272
- let l4 = e4;
3273
- if (e4 === l4) if (r2 && typeof r2 == "object" && !Array.isArray(r2)) {
3274
- let m6;
3275
- if (r2.data === void 0 && (m6 = "data")) return h7.errors = [{ instancePath: s7, schemaPath: "#/allOf/1/required", keyword: "required", params: { missingProperty: m6 }, message: "must have required property '" + m6 + "'" }], false;
3276
- r2.data !== void 0 && (u3(r2.data, { instancePath: s7 + "/data", parentData: r2, parentDataProperty: "data", rootData: g }) || (p6 = p6 === null ? u3.errors : p6.concat(u3.errors), e4 = p6.length));
3277
- } else return h7.errors = [{ instancePath: s7, schemaPath: "#/allOf/1/type", keyword: "type", params: { type: "object" }, message: "must be object" }], false;
3278
- var f3 = l4 === e4;
3279
- }
3280
- return h7.errors = p6, e4 === 0;
3281
- }
3282
- var C = h7;
3283
-
3284
- // ../../packages/contracts/src/generated/endpoints/getV1HourBank.ts
3285
- var getV1HourBank = {
3286
- method: "GET",
3287
- path: "/v1/hour-bank/",
3288
- operation: "GET /v1/hour-bank/",
3289
- revision: contractRevision,
3290
- responses: {
3291
- 200: { mediaType: "application/json", schemaId: "urn:flowmind:getV1HourBank:200", validate: C }
3292
- },
3293
- error: { mediaType: "application/json", schemaId: "ApiErrorEnvelope", validate: N }
3294
- };
3295
-
3296
- // src/contracts.ts
3297
- var STAGE_TIMEOUT_MS = 3e4;
3298
- var projectStatus = (project) => {
3299
- if (project.archivedAt) return "archived";
3300
- if (project.completedAt) return "completed";
3301
- if (project.launchedAt) return "launched";
3302
- return "active";
3303
- };
3304
- var listProjects = async (request = apiRequest) => {
3305
- const data = await request(getV1Projects, {});
3306
- return data.projects;
3307
- };
3308
- var getAgentCapabilities = (request = apiRequest) => request(getV1AgentCapabilities, {});
3309
- var listAgentProjects = async (request = apiRequest) => {
3310
- const data = await request(getV1AgentProjects, {});
3311
- return data.projects;
3312
- };
3313
- var listAgentRoutines = async (request = apiRequest) => {
3314
- const data = await request(getV1AgentRoutines, {});
3315
- return data.routines;
3316
- };
3317
- var withIdempotencyKey = (idempotencyKey) => ({
3318
- headers: { "Idempotency-Key": idempotencyKey }
3319
- });
3320
- var createAgentLogProposal = async (input2, idempotencyKey, request = apiRequest) => {
3321
- const data = await request(
3322
- postV1AgentLogProposals,
3323
- { body: input2 },
3324
- { ...withIdempotencyKey(idempotencyKey), preserveRejectionMessage: true, signal: AbortSignal.timeout(STAGE_TIMEOUT_MS) }
3325
- );
3326
- return data.proposal;
3327
- };
3328
- var listAgentLogProposals = async (status, request = apiRequest) => {
3329
- const data = await request(getV1AgentLogProposals, { query: { status } });
3330
- return data.proposals;
3331
- };
3332
- var cancelAgentLogProposal = async (proposalId, idempotencyKey, request = apiRequest) => {
3333
- const data = await request(
3334
- postV1AgentLogProposalsByProposalIdCancel,
3335
- { params: { proposalId } },
3336
- withIdempotencyKey(idempotencyKey)
3337
- );
3338
- return data.proposal;
3339
- };
3340
- var parseNaturalLog = (message, request = apiRequest) => request(postV1AiLog, { body: { message } });
3341
- var confirmNaturalLog = async (input2, idempotencyKey, request = apiRequest) => {
3342
- return request(postV1AiLogConfirm, { body: input2 }, withIdempotencyKey(idempotencyKey));
3343
- };
3344
- var durationMinutesFromHours = (hours) => {
3345
- const minutes = hours * 60;
3346
- if (!Number.isSafeInteger(Math.round(minutes)) || Math.round(minutes) / 60 !== hours) {
3347
- throw new Error("Hours must resolve to whole minutes.");
3348
- }
3349
- return Math.round(minutes);
3350
- };
3351
- var resolveActiveProjectByName = (projects, projectName) => {
3352
- const matches = projects.filter((project) => project.name === projectName && project.archivedAt === null);
3353
- if (matches.length === 0) {
3354
- throw new Error(`No active project named "${projectName}"`);
3355
- }
3356
- if (matches.length > 1) {
3357
- throw new Error(`Multiple active projects named "${projectName}". Use Flowmind web to disambiguate before logging.`);
3358
- }
3359
- return matches[0];
3360
- };
3361
- var createStructuredTimeEntry = async (input2, request = apiRequest) => {
3362
- const durationMinutes = durationMinutesFromHours(input2.hours);
3363
- const now = input2.now ?? /* @__PURE__ */ new Date();
3364
- let start = new Date(now.getTime() - durationMinutes * 6e4);
3365
- if (input2.startedAt !== void 0) {
3366
- if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(input2.startedAt)) {
3367
- throw new Error("started-at must be a valid ISO date-time with a timezone.");
3368
- }
3369
- const calendarDay = /* @__PURE__ */ new Date(`${input2.startedAt.slice(0, 10)}T00:00:00.000Z`);
3370
- if (Number.isNaN(calendarDay.getTime()) || calendarDay.toISOString().slice(0, 10) !== input2.startedAt.slice(0, 10)) {
3371
- throw new Error("started-at must be a valid ISO date-time.");
3372
- }
3373
- start = new Date(input2.startedAt);
3374
- if (Number.isNaN(start.getTime())) throw new Error("started-at must be a valid ISO date-time.");
3375
- }
3376
- if (start.getTime() + durationMinutes * 6e4 > now.getTime()) {
3377
- throw new Error("Logged work cannot end in the future");
3378
- }
3379
- const prepare = async () => {
3380
- const project = resolveActiveProjectByName(await listProjects(request), input2.projectName);
3381
- return {
3382
- projectId: project.id,
3383
- startedAt: input2.startedAt === void 0 ? new Date((input2.now ?? /* @__PURE__ */ new Date()).getTime() - durationMinutes * 6e4).toISOString() : start.toISOString(),
3384
- durationMinutes,
3385
- ...input2.nextStep !== void 0 ? { nextStep: input2.nextStep } : {},
3386
- ...input2.description !== void 0 ? { description: input2.description } : {},
3387
- ...input2.note !== void 0 ? { note: input2.note } : {},
3388
- ...input2.artifactUrl !== void 0 ? { artifactUrl: input2.artifactUrl } : {}
3389
- };
3390
- };
3391
- return manualSave(["structured", input2], prepare, (body, key) => request(postV1TimeEntries, { body }, { headers: { [IDEMPOTENCY_KEY_HEADER]: key } }));
3392
- };
3393
- var getStatusSnapshot = async (request = apiRequest) => {
3394
- const [publicStats, streak, xp] = await Promise.all([
3395
- request(getV1MePublicStats, {}),
3396
- request(getV1MeStreak, {}),
3397
- request(getV1XpSummary, {})
3398
- ]);
3399
- return { publicStats, streak, xp };
3400
- };
3401
- var getDetailedStats = async (request = apiRequest) => {
3402
- const [status, badges, hourBank] = await Promise.all([
3403
- getStatusSnapshot(request),
3404
- request(getV1MeBadges, {}),
3405
- request(getV1HourBank, {})
3406
- ]);
3407
- return { ...status, badges, hourBank };
3408
- };
3409
-
3410
- // src/project-ref.ts
3411
- async function resolveAgentProject(reference) {
3412
- const wanted = reference.trim();
3413
- const projects = await listAgentProjects();
3414
- const matches = isProjectId(wanted) ? projects.filter((project) => project.id.toLowerCase() === wanted.toLowerCase()) : projects.filter((project) => project.name.toLowerCase() === wanted.toLowerCase());
3415
- if (matches.length === 1) return matches[0];
3416
- const list = (matches.length > 1 ? matches : projects).map((project) => ` ${terminalText(project.name)} (${project.id})`).join("\n");
3417
- if (matches.length > 1) {
3418
- throw new Error(`Several projects are named "${terminalText(wanted)}". Use a project id:
3419
- ${list}`);
3420
- }
3421
- throw new Error(
3422
- `No active project matches "${terminalText(wanted)}".` + (list ? `
3423
- Active projects:
3424
- ${list}` : " No active projects exist.")
3425
- );
3426
- }
3427
-
3428
- // src/repository-status.ts
3429
- import { execFile } from "node:child_process";
3430
- var GIT_TIMEOUT_MS = 3e3;
3431
- function git(root, args, absentCode) {
3432
- return new Promise((resolve3, reject) => {
3433
- execFile(
3434
- "git",
3435
- args,
3436
- {
3437
- cwd: root,
3438
- encoding: "utf8",
3439
- timeout: GIT_TIMEOUT_MS,
3440
- windowsHide: true,
3441
- env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
3442
- },
3443
- (error, stdout) => {
3444
- if (!error) resolve3(stdout);
3445
- else if (!error.killed && absentCode !== void 0 && error.code === absentCode) resolve3(null);
3446
- else reject(new Error("Git lookup failed (" + args[0] + "): " + (error.killed ? "timed out" : error.code)));
3447
- }
3448
- );
3449
- });
3450
- }
3451
- async function readRemoteLink(root) {
3452
- const branch = (await git(root, ["symbolic-ref", "--quiet", "HEAD"], 1))?.trim();
3453
- const upstream = branch ? (await git(root, ["for-each-ref", "--format=%(upstream)", branch]))?.trim() : null;
3454
- const refs = [
3455
- ...upstream?.startsWith("refs/remotes/") ? [upstream] : [],
3456
- "refs/remotes/origin/HEAD",
3457
- "refs/remotes/origin/main",
3458
- "refs/remotes/origin/master"
3459
- ];
3460
- for (const ref of new Set(refs)) {
3461
- if (await git(root, ["rev-parse", "--verify", "--quiet", ref], 1) === null) continue;
3462
- if (!(await git(root, ["ls-tree", "--name-only", ref, "--", LINK_FILE]))?.trim()) continue;
3463
- const text2 = await git(root, ["show", `${ref}:${LINK_FILE}`]);
3464
- return { ref, link: parseLink(text2, `${ref}:${LINK_FILE}`, root) };
3465
- }
3466
- return null;
3467
- }
3468
- async function remoteStatus(root) {
3469
- const remote = await readRemoteLink(root);
3470
- if (remote?.link.kind === "linked") return { status: "remote-linked", root, link: remote.link, ref: remote.ref };
3471
- if (remote?.link.kind === "ignored") return { status: "ignored", root, link: remote.link };
3472
- return { status: "unlinked", root };
3473
- }
3474
- async function repositoryStatus(cwd) {
3475
- const local = await readLink(cwd);
3476
- if (local?.kind === "linked") return { status: "linked", root: local.root, link: local };
3477
- if (local?.kind === "ignored") return { status: "ignored", root: local.root, link: local };
3478
- const root = await findGitRoot(cwd);
3479
- return root === null ? { status: "outside-git" } : remoteStatus(root);
3480
- }
3481
- async function originUrl(root) {
3482
- const url = (await git(root, ["config", "--get", "remote.origin.url"], 1))?.trim();
3483
- if (!url) return null;
3484
- let parsed;
3485
- try {
3486
- parsed = new URL(url);
3487
- } catch {
3488
- return url;
3489
- }
3490
- if (!parsed.username && !parsed.password) return url;
3491
- parsed.username = "";
3492
- parsed.password = "";
3493
- return parsed.toString();
3494
- }
3495
-
3496
- // src/agent-session.ts
3497
- var RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
3498
- var ACTIVE_WINDOW_MS = 2 * 24 * 60 * 60 * 1e3;
3499
- var REMINDER_MINUTES = 20;
3500
- var OPEN_TURN_LIMIT_MS = 12 * 60 * 60 * 1e3;
3501
- var MISSED_START_MS = 15e3;
3502
- var CLOCK_DRIFT_MS = 1e3;
3503
- var EVENTS = /* @__PURE__ */ new Set(["session", "start", "stop", "subagent", "staged", "unlinked-reminder"]);
3504
- var sessionsDir = () => join6(accountDirectory(), "agent-sessions");
3505
- var periodPoint = (p6) => ({ wall: Date.parse(p6.at), up: p6.up });
3506
- var signedInPeriods = () => ownershipPeriods().map(({ start, end }) => ({
3507
- start: periodPoint(start),
3508
- end: end && periodPoint(end)
3509
- }));
3510
- function sessionFile(sessionId) {
3511
- const name = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,127}$/.test(sessionId) ? sessionId : createHash3("sha256").update(sessionId).digest("hex");
3512
- return join6(sessionsDir(), `${name}.jsonl`);
3513
- }
3514
- async function append(sessionId, event) {
3515
- await mkdir3(sessionsDir(), { recursive: true });
3516
- await appendFile(sessionFile(sessionId), `${JSON.stringify(event)}
3517
- `, { encoding: "utf8", mode: 384 });
3518
- }
3519
- var byTime = (events) => events.sort((a5, b4) => Date.parse(a5.at) - Date.parse(b4.at));
3520
- function parseEvent(line, path) {
3521
- const value = JSON.parse(line);
3522
- if (typeof value === "object" && value !== null) {
3523
- const { event, at: at2, up, cutAt, cutUp } = value;
3524
- if (typeof event === "string" && EVENTS.has(event) && typeof at2 === "string" && !Number.isNaN(Date.parse(at2)) && (up === void 0 || Number.isFinite(up)) && (cutAt === void 0 || typeof cutAt === "string" && !Number.isNaN(Date.parse(cutAt))) && (cutUp === void 0 || Number.isFinite(cutUp))) {
3525
- if (event === "staged") {
3526
- const mark = value;
3527
- if (typeof cutAt !== "string" || typeof mark.pendingStageId !== "string" || !(mark.state === "waiting" || mark.state === "accepted" && typeof mark.proposalId === "string" || (mark.state === "rejected" || mark.state === "conflict" || mark.state === "discarded") && typeof mark.reason === "string")) throw new Error("Unreadable stage mark in " + path);
3528
- }
3529
- return value;
3530
- }
3703
+ if (f3) {
3704
+ let l4 = e4;
3705
+ if (e4 === l4) if (r2 && typeof r2 == "object" && !Array.isArray(r2)) {
3706
+ let m6;
3707
+ if (r2.data === void 0 && (m6 = "data")) return h7.errors = [{ instancePath: s7, schemaPath: "#/allOf/1/required", keyword: "required", params: { missingProperty: m6 }, message: "must have required property '" + m6 + "'" }], false;
3708
+ r2.data !== void 0 && (u3(r2.data, { instancePath: s7 + "/data", parentData: r2, parentDataProperty: "data", rootData: g }) || (p6 = p6 === null ? u3.errors : p6.concat(u3.errors), e4 = p6.length));
3709
+ } else return h7.errors = [{ instancePath: s7, schemaPath: "#/allOf/1/type", keyword: "type", params: { type: "object" }, message: "must be object" }], false;
3710
+ var f3 = l4 === e4;
3531
3711
  }
3532
- throw new Error(`Unreadable agent session record in ${path}`);
3712
+ return h7.errors = p6, e4 === 0;
3533
3713
  }
3534
- async function readEventsAt(path) {
3535
- let text2;
3536
- try {
3537
- text2 = await readFile4(path, "utf8");
3538
- } catch (error) {
3539
- if (error.code === "ENOENT") return null;
3540
- throw error;
3541
- }
3542
- return byTime(
3543
- text2.split("\n").filter((line) => line.trim() !== "").map((line) => parseEvent(line, path))
3714
+ var C = h7;
3715
+
3716
+ // ../../packages/contracts/src/generated/endpoints/getV1HourBank.ts
3717
+ var getV1HourBank = {
3718
+ method: "GET",
3719
+ path: "/v1/hour-bank/",
3720
+ operation: "GET /v1/hour-bank/",
3721
+ revision: contractRevision,
3722
+ responses: {
3723
+ 200: { mediaType: "application/json", schemaId: "urn:flowmind:getV1HourBank:200", validate: C }
3724
+ },
3725
+ error: { mediaType: "application/json", schemaId: "ApiErrorEnvelope", validate: N }
3726
+ };
3727
+
3728
+ // src/contracts.ts
3729
+ var STAGE_TIMEOUT_MS = 3e4;
3730
+ var projectStatus = (project) => {
3731
+ if (project.archivedAt) return "archived";
3732
+ if (project.completedAt) return "completed";
3733
+ if (project.launchedAt) return "launched";
3734
+ return "active";
3735
+ };
3736
+ var listProjects = async (request = apiRequest) => {
3737
+ const data = await request(getV1Projects, {});
3738
+ return data.projects;
3739
+ };
3740
+ var getAgentCapabilities = (request = apiRequest) => request(getV1AgentCapabilities, {});
3741
+ var listAgentProjects = async (request = apiRequest) => {
3742
+ const data = await request(getV1AgentProjects, {});
3743
+ return data.projects;
3744
+ };
3745
+ var listAgentRoutines = async (request = apiRequest) => {
3746
+ const data = await request(getV1AgentRoutines, {});
3747
+ return data.routines;
3748
+ };
3749
+ var withIdempotencyKey = (idempotencyKey) => ({
3750
+ headers: { "Idempotency-Key": idempotencyKey }
3751
+ });
3752
+ var createAgentLogProposal = async (input2, idempotencyKey, request = apiRequest) => {
3753
+ const data = await request(
3754
+ postV1AgentLogProposals,
3755
+ { body: input2 },
3756
+ { ...withIdempotencyKey(idempotencyKey), preserveRejectionMessage: true, signal: AbortSignal.timeout(STAGE_TIMEOUT_MS) }
3544
3757
  );
3545
- }
3546
- function parseHookInput(raw) {
3547
- const value = JSON.parse(raw);
3548
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
3549
- throw new Error("Hook input must be a JSON object.");
3550
- }
3551
- const {
3552
- session_id: sessionId,
3553
- hook_event_name: event,
3554
- cwd,
3555
- turn_id: turnId,
3556
- agent_id: agentId,
3557
- transcript_path: transcriptPath
3558
- } = value;
3559
- if (typeof sessionId !== "string" || sessionId === "") throw new Error("Hook input needs a session_id string.");
3560
- if (typeof event !== "string") throw new Error("Hook input needs a hook_event_name string.");
3561
- if (typeof cwd !== "string") throw new Error("Hook input needs a cwd string.");
3562
- if (turnId !== void 0 && turnId !== null && typeof turnId !== "string") {
3563
- throw new Error("Hook input turn_id must be a string when present.");
3564
- }
3565
- if (agentId !== void 0 && agentId !== null && typeof agentId !== "string") {
3566
- throw new Error("Hook input agent_id must be a string when present.");
3567
- }
3568
- if (transcriptPath !== void 0 && transcriptPath !== null && typeof transcriptPath !== "string") {
3569
- throw new Error("Hook input transcript_path must be a string when present.");
3758
+ return data.proposal;
3759
+ };
3760
+ var listAgentLogProposals = async (status, request = apiRequest) => {
3761
+ const data = await request(getV1AgentLogProposals, { query: { status } });
3762
+ return data.proposals;
3763
+ };
3764
+ var cancelAgentLogProposal = async (proposalId, idempotencyKey, request = apiRequest) => {
3765
+ const data = await request(
3766
+ postV1AgentLogProposalsByProposalIdCancel,
3767
+ { params: { proposalId } },
3768
+ withIdempotencyKey(idempotencyKey)
3769
+ );
3770
+ return data.proposal;
3771
+ };
3772
+ var parseNaturalLog = (message, request = apiRequest) => request(postV1AiLog, { body: { message } });
3773
+ var confirmNaturalLog = async (input2, idempotencyKey, request = apiRequest) => {
3774
+ return request(postV1AiLogConfirm, { body: input2 }, withIdempotencyKey(idempotencyKey));
3775
+ };
3776
+ var durationMinutesFromHours = (hours) => {
3777
+ const minutes = hours * 60;
3778
+ if (!Number.isSafeInteger(Math.round(minutes)) || Math.round(minutes) / 60 !== hours) {
3779
+ throw new Error("Hours must resolve to whole minutes.");
3570
3780
  }
3571
- return {
3572
- sessionId,
3573
- event,
3574
- cwd,
3575
- turnId: typeof turnId === "string" ? turnId : null,
3576
- agentId: typeof agentId === "string" && agentId !== "" ? agentId : null,
3577
- transcriptPath: typeof transcriptPath === "string" && transcriptPath !== "" ? transcriptPath : null
3578
- };
3579
- }
3580
- var proofRule = `Write the outcome as one line of at most ${AGENT_OUTCOME_MAX_LENGTH} characters: what changed. Add up to ${AGENT_DETAILS_MAX_COUNT} optional details for proof or open items, each one line of at most ${AGENT_DETAIL_MAX_LENGTH} characters.`;
3581
- function sessionContext(projectName) {
3582
- return [
3583
- `Flowmind: this repository is linked to the Flowmind project "${terminalText(projectName)}".`,
3584
- "When you finish a material unit of work here, stage it before you start the next one, with",
3585
- '`flowmind agent stage --intention "<goal>" --outcome "<what changed>" --detail "<proof or open item>" --json`,',
3586
- "adding `--artifact-url <commit, PR, or document URL>` when one exists.",
3587
- proofRule,
3588
- "When several units finish together, stage them as one proof.",
3589
- "The CLI fills in the project and the recorded session time, including the time of your subagents.",
3590
- "Subagents do not stage. Share the returned review URL with the user.",
3591
- "Do not use `flowmind log` for agent work."
3592
- ].join(" ");
3593
- }
3594
- function unstagedReminder(projectName, minutes) {
3595
- return [
3596
- `Flowmind: ${minutes} minutes of agent work in this session are not staged yet for "${terminalText(projectName)}".`,
3597
- "When the current unit of work is complete, stage it with",
3598
- '`flowmind agent stage --intention "<goal>" --outcome "<what changed>" --detail "<proof or open item>" --json`',
3599
- "and share the review URL.",
3600
- proofRule
3601
- ].join(" ");
3602
- }
3603
- var projectLabel = (link2) => `"${terminalText(link2.name)}" (${link2.projectId})`;
3604
- function remoteLinkedContext(link2) {
3605
- return [
3606
- `Flowmind: the remote already links this repository to the Flowmind project ${projectLabel(link2)},`,
3607
- "but this checkout has no .flowmind.json yet.",
3608
- "Run `git pull` to get .flowmind.json. Never create a new Flowmind project for this repository.",
3609
- 'After the pull, stage finished work with `flowmind agent stage --intention "<goal>" --outcome "<what changed>" --detail "<proof or open item>" --json`.',
3610
- proofRule
3611
- ].join(" ");
3612
- }
3613
- function unlinkedContext() {
3614
- return [
3615
- "Flowmind: this git repository is not linked to a Flowmind project.",
3616
- "If it is a project the user works on, link it before you finish:",
3617
- "1. Run `git pull` first. Another machine may have linked it already.",
3618
- "2. Look for the existing project with `flowmind projects --json`. One product is one project on every machine; match by product, repository and name.",
3619
- '3. Only for a new product, create one with `flowmind projects add "<name>" --description "<one line>" --json`.',
3620
- "4. Run `flowmind link <project-id>` and commit .flowmind.json.",
3621
- "For a third-party clone or a scratch folder, run `flowmind link --ignore`. Ask the user once when you are unsure."
3622
- ].join("\n");
3623
- }
3624
- function remoteLinkedReminder(link2, minutes) {
3625
- return [
3626
- `Flowmind: ${minutes} minutes of agent work, and this checkout still has no .flowmind.json.`,
3627
- `The remote links this repository to ${projectLabel(link2)}. Run \`git pull\`; never create a new project for it.`
3628
- ].join(" ");
3629
- }
3630
- function unlinkedReminder(minutes) {
3631
- return [
3632
- `Flowmind: ${minutes} minutes of agent work, and this repository is still not linked to a Flowmind project.`,
3633
- "Before you finish: run `git pull`, find the project with `flowmind projects --json`",
3634
- "(create one with `flowmind projects add` only for a new product), run `flowmind link <project-id>`, and commit .flowmind.json.",
3635
- "For a third-party clone or a scratch folder, run `flowmind link --ignore`."
3636
- ].join(" ");
3637
- }
3638
- async function pruneOldSessions(now) {
3639
- let names;
3640
- try {
3641
- names = await readdir4(sessionsDir());
3642
- } catch (error) {
3643
- if (error.code === "ENOENT") return;
3644
- throw error;
3781
+ return Math.round(minutes);
3782
+ };
3783
+ var resolveActiveProjectByName = (projects, projectName) => {
3784
+ const matches = projects.filter((project) => project.name === projectName && project.archivedAt === null);
3785
+ if (matches.length === 0) {
3786
+ throw new Error(`No active project named "${projectName}"`);
3645
3787
  }
3646
- for (const name of names.filter((entry) => entry.endsWith(".jsonl"))) {
3647
- const path = join6(sessionsDir(), name);
3648
- if (now.getTime() - (await stat2(path)).mtimeMs > RETENTION_MS) await rm(path, { force: true });
3788
+ if (matches.length > 1) {
3789
+ throw new Error(`Multiple active projects named "${projectName}". Use Flowmind web to disambiguate before logging.`);
3649
3790
  }
3650
- }
3651
- async function handleHook(agent, raw, now = currentMoment()) {
3652
- const input2 = parseHookInput(raw);
3653
- const at2 = now.at.toISOString();
3654
- const { up } = now;
3655
- const subagent = input2.agentId;
3656
- switch (input2.event) {
3657
- case "SessionStart": {
3658
- if (subagent !== null) return "";
3659
- await append(input2.sessionId, {
3660
- event: "session",
3661
- at: at2,
3662
- up,
3663
- sessionId: input2.sessionId,
3664
- agent,
3665
- cwd: input2.cwd,
3666
- ...input2.transcriptPath === null ? {} : { transcriptPath: input2.transcriptPath }
3667
- });
3668
- await pruneOldSessions(now.at);
3669
- const repository = await repositoryStatus(input2.cwd);
3670
- switch (repository.status) {
3671
- case "linked":
3672
- return sessionContext(repository.link.name);
3673
- case "remote-linked":
3674
- return remoteLinkedContext(repository.link);
3675
- case "unlinked":
3676
- return unlinkedContext();
3677
- case "ignored":
3678
- case "outside-git":
3679
- return "";
3680
- }
3791
+ return matches[0];
3792
+ };
3793
+ var createStructuredTimeEntry = async (input2, request = apiRequest) => {
3794
+ const durationMinutes = durationMinutesFromHours(input2.hours);
3795
+ const now = input2.now ?? /* @__PURE__ */ new Date();
3796
+ let start = new Date(now.getTime() - durationMinutes * 6e4);
3797
+ if (input2.startedAt !== void 0) {
3798
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(input2.startedAt)) {
3799
+ throw new Error("started-at must be a valid ISO date-time with a timezone.");
3681
3800
  }
3682
- case "UserPromptSubmit":
3683
- if (subagent !== null) {
3684
- await append(input2.sessionId, { event: "subagent", at: at2, up, agentId: subagent, turnId: input2.turnId });
3685
- return "";
3686
- }
3687
- await append(input2.sessionId, {
3688
- event: "start",
3689
- at: at2,
3690
- up,
3691
- sessionId: input2.sessionId,
3692
- agent,
3693
- cwd: input2.cwd,
3694
- turnId: input2.turnId
3695
- });
3696
- return promptReminder(input2.sessionId, input2.cwd, now);
3697
- case "Stop":
3698
- case "Interrupt":
3699
- if (subagent !== null) return "";
3700
- await append(input2.sessionId, { event: "stop", at: at2, up, turnId: input2.turnId });
3701
- if (agent === "codex" && input2.turnId !== null) await addMissedTurnStart(input2, now);
3702
- return "";
3703
- default:
3704
- throw new Error(`Unsupported hook event: ${input2.event}`);
3801
+ const calendarDay = /* @__PURE__ */ new Date(`${input2.startedAt.slice(0, 10)}T00:00:00.000Z`);
3802
+ if (Number.isNaN(calendarDay.getTime()) || calendarDay.toISOString().slice(0, 10) !== input2.startedAt.slice(0, 10)) {
3803
+ throw new Error("started-at must be a valid ISO date-time.");
3804
+ }
3805
+ start = new Date(input2.startedAt);
3806
+ if (Number.isNaN(start.getTime())) throw new Error("started-at must be a valid ISO date-time.");
3705
3807
  }
3706
- }
3707
- function turnIdTime(turnId) {
3708
- const hex = turnId.replaceAll("-", "");
3709
- if (!/^[0-9a-f]{32}$/i.test(hex) || hex[12] !== "7") return null;
3710
- return Number.parseInt(hex.slice(0, 12), 16);
3711
- }
3712
- var firstRecordedStart = (events, turnId) => {
3713
- const first = events.find((event) => event.event === "start" && event.turnId === turnId);
3714
- return first ? Date.parse(first.at) : null;
3808
+ if (start.getTime() + durationMinutes * 6e4 > now.getTime()) {
3809
+ throw new Error("Logged work cannot end in the future");
3810
+ }
3811
+ const prepare = async () => {
3812
+ const project = resolveActiveProjectByName(await listProjects(request), input2.projectName);
3813
+ return {
3814
+ projectId: project.id,
3815
+ startedAt: input2.startedAt === void 0 ? new Date((input2.now ?? /* @__PURE__ */ new Date()).getTime() - durationMinutes * 6e4).toISOString() : start.toISOString(),
3816
+ durationMinutes,
3817
+ ...input2.nextStep !== void 0 ? { nextStep: input2.nextStep } : {},
3818
+ ...input2.description !== void 0 ? { description: input2.description } : {},
3819
+ ...input2.note !== void 0 ? { note: input2.note } : {},
3820
+ ...input2.artifactUrl !== void 0 ? { artifactUrl: input2.artifactUrl } : {}
3821
+ };
3822
+ };
3823
+ return manualSave(["structured", input2], prepare, (body, key) => request(postV1TimeEntries, { body }, { headers: { [IDEMPOTENCY_KEY_HEADER]: key } }));
3715
3824
  };
3716
- function hooksSawStart(events, turnId) {
3717
- const first = firstRecordedStart(events, turnId);
3718
- const created = turnIdTime(turnId);
3719
- return first !== null && created !== null && first - created <= MISSED_START_MS;
3720
- }
3721
- var transcriptPathOf = (events) => events.filter((event) => event.event === "session").filter((event) => event.transcriptPath !== void 0).at(-1)?.transcriptPath ?? null;
3722
- async function codexTurnsOf(sessionId, knownPath) {
3723
- const known = knownPath === null ? null : await readCodexTurns(knownPath);
3724
- if (known !== null) return { path: knownPath, turns: known };
3725
- const found = await findCodexSessionFile(sessionId);
3726
- const turns = found === null ? null : await readCodexTurns(found);
3727
- return turns === null ? null : { path: found, turns };
3728
- }
3729
- function countBack(wall, events, now, lastWake) {
3730
- const nowWall = now.at.getTime();
3731
- if (wall > nowWall) return null;
3732
- const back = { wall, up: now.up - (nowWall - wall) };
3733
- if (lastWake !== null && lastWake <= nowWall) return lastWake <= wall ? back : null;
3734
- const before = awakeSamples(events).filter((point) => point.wall <= wall).at(-1);
3735
- return before && Math.abs(nowWall - now.up - (before.wall - before.up)) <= CLOCK_DRIFT_MS ? back : null;
3825
+ var getStatusSnapshot = async (request = apiRequest) => {
3826
+ const [publicStats, streak, xp] = await Promise.all([
3827
+ request(getV1MePublicStats, {}),
3828
+ request(getV1MeStreak, {}),
3829
+ request(getV1XpSummary, {})
3830
+ ]);
3831
+ return { publicStats, streak, xp };
3832
+ };
3833
+ var getDetailedStats = async (request = apiRequest) => {
3834
+ const [status, badges, hourBank] = await Promise.all([
3835
+ getStatusSnapshot(request),
3836
+ request(getV1MeBadges, {}),
3837
+ request(getV1HourBank, {})
3838
+ ]);
3839
+ return { ...status, badges, hourBank };
3840
+ };
3841
+
3842
+ // src/project-ref.ts
3843
+ async function resolveAgentProject(reference) {
3844
+ const wanted = reference.trim();
3845
+ const projects = await listAgentProjects();
3846
+ const matches = isProjectId(wanted) ? projects.filter((project) => project.id.toLowerCase() === wanted.toLowerCase()) : projects.filter((project) => project.name.toLowerCase() === wanted.toLowerCase());
3847
+ if (matches.length === 1) return matches[0];
3848
+ const list = (matches.length > 1 ? matches : projects).map((project) => ` ${terminalText(project.name)} (${project.id})`).join("\n");
3849
+ if (matches.length > 1) {
3850
+ throw new Error(`Several projects are named "${terminalText(wanted)}". Use a project id:
3851
+ ${list}`);
3852
+ }
3853
+ throw new Error(
3854
+ `No active project matches "${terminalText(wanted)}".` + (list ? `
3855
+ Active projects:
3856
+ ${list}` : " No active projects exist.")
3857
+ );
3736
3858
  }
3737
- var awakeSamples = (events) => events.flatMap((event) => event.event !== "unlinked-reminder" && event.up !== void 0 ? [pointOf(event.at, event.up)] : []);
3738
- function awakePointAt(wall, events, now, lastWake) {
3739
- const exact = countBack(wall, events, now, lastWake);
3740
- if (exact) return exact;
3741
- const nowWall = now.at.getTime();
3742
- if (lastWake !== null && lastWake <= nowWall) return countBack(lastWake, events, now, lastWake);
3743
- return awakeSamples(events).find((point) => point.wall > wall && point.wall < nowWall) ?? null;
3859
+
3860
+ // src/repository-status.ts
3861
+ import { execFile } from "node:child_process";
3862
+ var GIT_TIMEOUT_MS = 3e3;
3863
+ function git(root, args, absentCode) {
3864
+ return new Promise((resolve3, reject) => {
3865
+ execFile(
3866
+ "git",
3867
+ args,
3868
+ {
3869
+ cwd: root,
3870
+ encoding: "utf8",
3871
+ timeout: GIT_TIMEOUT_MS,
3872
+ windowsHide: true,
3873
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
3874
+ },
3875
+ (error, stdout) => {
3876
+ if (!error) resolve3(stdout);
3877
+ else if (!error.killed && absentCode !== void 0 && error.code === absentCode) resolve3(null);
3878
+ else reject(new Error("Git lookup failed (" + args[0] + "): " + (error.killed ? "timed out" : error.code)));
3879
+ }
3880
+ );
3881
+ });
3744
3882
  }
3745
- function missedStartEvent(events, turn, owner, now, lastWake) {
3746
- const first = firstRecordedStart(events, turn.turnId);
3747
- if (first !== null && first - turn.startedAt <= MISSED_START_MS) return null;
3748
- const point = awakePointAt(turn.startedAt, events, now, lastWake);
3749
- if (point === null) return null;
3750
- return {
3751
- event: "start",
3752
- at: new Date(point.wall).toISOString(),
3753
- ...point.up === null ? {} : { up: point.up },
3754
- sessionId: owner.sessionId,
3755
- agent: "codex",
3756
- cwd: owner.cwd,
3757
- turnId: turn.turnId,
3758
- source: "session-file"
3759
- };
3883
+ async function readRemoteLink(root) {
3884
+ const branch = (await git(root, ["symbolic-ref", "--quiet", "HEAD"], 1))?.trim();
3885
+ const upstream = branch ? (await git(root, ["for-each-ref", "--format=%(upstream)", branch]))?.trim() : null;
3886
+ const refs = [
3887
+ ...upstream?.startsWith("refs/remotes/") ? [upstream] : [],
3888
+ "refs/remotes/origin/HEAD",
3889
+ "refs/remotes/origin/main",
3890
+ "refs/remotes/origin/master"
3891
+ ];
3892
+ for (const ref of new Set(refs)) {
3893
+ if (await git(root, ["rev-parse", "--verify", "--quiet", ref], 1) === null) continue;
3894
+ if (!(await git(root, ["ls-tree", "--name-only", ref, "--", LINK_FILE]))?.trim()) continue;
3895
+ const text2 = await git(root, ["show", `${ref}:${LINK_FILE}`]);
3896
+ return { ref, link: parseLink(text2, `${ref}:${LINK_FILE}`, root) };
3897
+ }
3898
+ return null;
3760
3899
  }
3761
- async function addMissedTurnStart(input2, now) {
3762
- const turnId = input2.turnId;
3763
- const events = await readEventsAt(sessionFile(input2.sessionId)) ?? [];
3764
- if (hooksSawStart(events, turnId)) return;
3765
- const file = await codexTurnsOf(input2.sessionId, input2.transcriptPath ?? transcriptPathOf(events));
3766
- if (file === null) return;
3767
- const turn = file.turns.find((candidate) => candidate.turnId === turnId);
3768
- if (!turn) throw new Error(`The Codex session file ${file.path} has no start for turn ${turnId}.`);
3769
- const start = missedStartEvent(events, turn, input2, now, lastWakeMs());
3770
- if (start) await append(input2.sessionId, start);
3900
+ async function remoteStatus(root) {
3901
+ const remote = await readRemoteLink(root);
3902
+ if (remote?.link.kind === "linked") return { status: "remote-linked", root, link: remote.link, ref: remote.ref };
3903
+ if (remote?.link.kind === "ignored") return { status: "ignored", root, link: remote.link };
3904
+ return { status: "unlinked", root };
3771
3905
  }
3772
- var pointOf = (at2, up) => ({ wall: Date.parse(at2), up: up ?? null });
3773
- var cutOf = (event) => pointOf(event.cutAt, event.cutUp);
3774
- function span(from, to) {
3775
- const wall = to.wall - from.wall;
3776
- if (wall <= 0 || from.up !== null && to.up !== null && to.up < from.up) return { from, to, ms: 0, awake: false };
3777
- const ms = from.up === null || to.up === null ? wall : Math.min(to.up - from.up, wall);
3778
- return { from, to, ms: ms > OPEN_TURN_LIMIT_MS ? 0 : ms, awake: from.up !== null && to.up !== null };
3906
+ async function repositoryStatus(cwd) {
3907
+ const local = await readLink(cwd);
3908
+ if (local?.kind === "linked") return { status: "linked", root: local.root, link: local };
3909
+ if (local?.kind === "ignored") return { status: "ignored", root: local.root, link: local };
3910
+ const root = await findGitRoot(cwd);
3911
+ return root === null ? { status: "outside-git" } : remoteStatus(root);
3779
3912
  }
3780
- var sameEpoch = (from, to) => from.up === null || to.up === null || to.up >= from.up;
3781
- function supportedEdge(start, edge, end) {
3782
- const follows = (a5, b4) => a5.up !== null && b4.up !== null && b4.up >= a5.up && b4.up - a5.up <= b4.wall - a5.wall + CLOCK_DRIFT_MS;
3783
- return follows(start, edge) && follows(edge, end);
3913
+ async function originUrl(root) {
3914
+ const url = (await git(root, ["config", "--get", "remote.origin.url"], 1))?.trim();
3915
+ if (!url) return null;
3916
+ let parsed;
3917
+ try {
3918
+ parsed = new URL(url);
3919
+ } catch {
3920
+ return url;
3921
+ }
3922
+ if (!parsed.username && !parsed.password) return url;
3923
+ parsed.username = "";
3924
+ parsed.password = "";
3925
+ return parsed.toString();
3784
3926
  }
3785
- function ownedParts(start, end, cut, periods) {
3786
- if (end.wall <= start.wall || end.wall - start.wall > OPEN_TURN_LIMIT_MS || !sameEpoch(start, end)) return [];
3787
- return periods.flatMap((period) => {
3788
- if (period.start.wall >= end.wall || period.end && period.end.wall <= start.wall) return [];
3789
- let from = start, to = end;
3790
- if (period.start.wall > start.wall) {
3791
- if (!supportedEdge(start, period.start, end)) return [];
3792
- from = period.start;
3793
- }
3794
- if (period.end && period.end.wall < end.wall) {
3795
- if (!supportedEdge(start, period.end, end)) return [];
3796
- to = period.end;
3797
- }
3798
- if (cut && cut.wall > from.wall) from = cut;
3799
- const part = span(from, to);
3800
- return part.ms > 0 ? [part] : [];
3801
- });
3927
+
3928
+ // src/agent-session.ts
3929
+ var RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
3930
+ var ACTIVE_WINDOW_MS = 2 * 24 * 60 * 60 * 1e3;
3931
+ var REMINDER_MINUTES = 20;
3932
+ var OPEN_TURN_LIMIT_MS = 12 * 60 * 60 * 1e3;
3933
+ var MISSED_START_MS = 15e3;
3934
+ var CLOCK_DRIFT_MS = 1e3;
3935
+ var EVENTS = /* @__PURE__ */ new Set(["session", "start", "stop", "subagent", "staged", "unlinked-reminder"]);
3936
+ var sessionsDir = () => join8(accountDirectory(), "agent-sessions");
3937
+ var periodPoint = (p6) => ({ wall: Date.parse(p6.at), up: p6.up });
3938
+ var signedInPeriods = () => ownershipPeriods().map(({ start, end }) => ({
3939
+ start: periodPoint(start),
3940
+ end: end && periodPoint(end)
3941
+ }));
3942
+ function sessionFile(sessionId) {
3943
+ const name = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,127}$/.test(sessionId) ? sessionId : createHash4("sha256").update(sessionId).digest("hex");
3944
+ return join8(sessionsDir(), `${name}.jsonl`);
3802
3945
  }
3803
- function measureRecordedTime(events, now, periods) {
3804
- const first = events.find((event) => event.event === "session" || event.event === "start");
3805
- if (!first || first.event !== "session" && first.event !== "start") return null;
3806
- const nowPoint = { wall: now.at.getTime(), up: now.up };
3807
- const turns = [];
3808
- let open2 = null;
3809
- let unknownStarts = 0;
3810
- let lastStage = null;
3811
- for (const event of events) {
3812
- if (event.event === "session" || event.event === "unlinked-reminder") continue;
3813
- const point = pointOf(event.at, event.up);
3814
- switch (event.event) {
3815
- case "start":
3816
- if (event.turnId === null && (unknownStarts > 0 || open2 && open2.turnId === null)) {
3817
- unknownStarts = unknownStarts ? unknownStarts + 1 : 2;
3818
- open2 = null;
3819
- break;
3820
- }
3821
- if (open2 && open2.turnId !== null && open2.turnId === event.turnId) {
3822
- if (sameEpoch(open2.start, point)) open2.last = point;
3823
- break;
3824
- }
3825
- if (open2) turns.push({ start: open2.start, end: open2.last });
3826
- open2 = { start: point, last: point, turnId: event.turnId };
3827
- break;
3828
- case "stop": {
3829
- if (event.turnId === null && unknownStarts > 0) {
3830
- unknownStarts--;
3831
- break;
3832
- }
3833
- const matches = open2 !== null && event.turnId === open2.turnId;
3834
- if (open2 && matches) {
3835
- turns.push({ start: open2.start, end: sameEpoch(open2.start, point) ? point : open2.last });
3836
- open2 = null;
3837
- }
3838
- break;
3946
+ async function append(sessionId, event) {
3947
+ await mkdir5(sessionsDir(), { recursive: true });
3948
+ await appendFile(sessionFile(sessionId), `${JSON.stringify(event)}
3949
+ `, { encoding: "utf8", mode: 384 });
3950
+ }
3951
+ var byTime = (events) => events.sort((a5, b4) => Date.parse(a5.at) - Date.parse(b4.at));
3952
+ function parseEvent(line, path) {
3953
+ const value = JSON.parse(line);
3954
+ if (typeof value === "object" && value !== null) {
3955
+ const { event, at: at2, up, cutAt, cutUp } = value;
3956
+ if (typeof event === "string" && EVENTS.has(event) && typeof at2 === "string" && !Number.isNaN(Date.parse(at2)) && (up === void 0 || Number.isFinite(up)) && (cutAt === void 0 || typeof cutAt === "string" && !Number.isNaN(Date.parse(cutAt))) && (cutUp === void 0 || Number.isFinite(cutUp))) {
3957
+ if (event === "staged") {
3958
+ const mark = value;
3959
+ if (typeof cutAt !== "string" || typeof mark.pendingStageId !== "string" || !(mark.state === "waiting" || mark.state === "accepted" && typeof mark.proposalId === "string" || (mark.state === "rejected" || mark.state === "conflict" || mark.state === "discarded") && typeof mark.reason === "string")) throw new Error("Unreadable stage mark in " + path);
3839
3960
  }
3840
- case "staged":
3841
- if (!lastStage || cutOf(event).wall >= cutOf(lastStage).wall) lastStage = event;
3842
- break;
3843
- case "subagent":
3844
- if (open2 && open2.turnId === event.turnId && sameEpoch(open2.start, point)) open2.last = point;
3845
- break;
3961
+ return value;
3846
3962
  }
3847
3963
  }
3848
- if (open2) turns.push({ start: open2.start, end: open2.last });
3849
- const stagedCut = lastStage ? cutOf(lastStage) : null;
3850
- const counted = turns.flatMap(
3851
- ({ start, end }) => ownedParts(start, end.wall > nowPoint.wall ? nowPoint : end, stagedCut, periods)
3964
+ throw new Error(`Unreadable agent session record in ${path}`);
3965
+ }
3966
+ async function readEventsAt(path) {
3967
+ if (await readIfPresent(path + RETIRED) !== null) return null;
3968
+ const sources = await Promise.all([path + ADOPTED, path].map(async (file) => ({ file, text: await readIfPresent(file) })));
3969
+ if (sources.every((source) => source.text === null)) return null;
3970
+ return byTime(
3971
+ sources.flatMap(
3972
+ ({ file, text: text2 }) => (text2 ?? "").split("\n").filter((line) => line.trim() !== "").map((line) => parseEvent(line, file))
3973
+ )
3852
3974
  );
3853
- const unstagedMs = counted.reduce((sum, part) => sum + part.ms, 0);
3854
- const minutes = Math.floor(unstagedMs / 6e4);
3855
- let cut = { at: now.at.toISOString(), up: now.up };
3856
- let remaining = minutes * 6e4;
3857
- for (const part of counted) {
3858
- if (remaining > part.ms) {
3859
- remaining -= part.ms;
3860
- continue;
3975
+ }
3976
+ var ADOPTED = ".adopted";
3977
+ var RETIRED = ".retired";
3978
+ async function readIfPresent(path) {
3979
+ try {
3980
+ return await readFile6(path, "utf8");
3981
+ } catch (error) {
3982
+ if (error.code === "ENOENT") return null;
3983
+ throw error;
3984
+ }
3985
+ }
3986
+ var journalNames = (names) => [
3987
+ ...new Set(
3988
+ names.flatMap((name) => {
3989
+ if (name.endsWith(".jsonl")) return [name];
3990
+ for (const suffix of [ADOPTED, RETIRED]) if (name.endsWith(".jsonl" + suffix)) return [name.slice(0, -suffix.length)];
3991
+ return [];
3992
+ })
3993
+ )
3994
+ ];
3995
+ async function lastWritten(path) {
3996
+ let newest = -Infinity;
3997
+ for (const file of [path, path + ADOPTED]) {
3998
+ try {
3999
+ newest = Math.max(newest, (await stat3(file)).mtimeMs);
4000
+ } catch (error) {
4001
+ if (error.code !== "ENOENT") throw error;
3861
4002
  }
3862
- cut = {
3863
- at: new Date(part.from.wall + remaining).toISOString(),
3864
- up: part.awake && part.from.up !== null ? part.from.up + remaining : null
3865
- };
3866
- break;
3867
4003
  }
3868
- return {
3869
- agent: first.agent,
3870
- unstagedMs,
3871
- minutes,
3872
- startedAt: counted.length > 0 ? new Date(counted[0].from.wall).toISOString() : null,
3873
- cut,
3874
- lastProposalId: lastStage?.state === "accepted" ? lastStage.proposalId : null,
3875
- lastPendingStageId: lastStage && lastStage.state !== "accepted" ? lastStage.pendingStageId : null,
3876
- lastHold: lastStage?.state === "conflict" || lastStage?.state === "discarded" ? lastStage.reason : null,
3877
- lastRejection: lastStage?.state === "rejected" ? lastStage.reason : null
3878
- };
4004
+ return newest;
3879
4005
  }
3880
- async function recordedTime(sessionId, now) {
3881
- const events = await readEventsAt(sessionFile(sessionId));
3882
- return events ? measureRecordedTime(events, now, signedInPeriods()) : null;
4006
+ function currentFormOf(line, path) {
4007
+ const value = JSON.parse(line);
4008
+ if (typeof value !== "object" || value === null || value.event !== "staged" || "state" in value) {
4009
+ return parseEvent(line, path);
4010
+ }
4011
+ const { proposalId, pendingStageId, cutAt, cutUp, at: at2, up, ...rest } = value;
4012
+ const cut = cutAt === void 0 ? { cutAt: at2, ...up === void 0 ? {} : { cutUp: up } } : { cutAt, ...cutUp === void 0 ? {} : { cutUp } };
4013
+ const mark = typeof proposalId === "string" ? { pendingStageId: "legacy-" + proposalId, state: "accepted", proposalId } : { pendingStageId, state: "waiting" };
4014
+ return parseEvent(JSON.stringify({ ...rest, at: at2, ...up === void 0 ? {} : { up }, ...cut, ...mark }), path);
4015
+ }
4016
+ var SOURCES = "0.3.7-sources.json";
4017
+ async function adoptedSources() {
4018
+ const text2 = await readIfPresent(join8(sessionsDir(), SOURCES));
4019
+ if (text2 === null) return {};
4020
+ try {
4021
+ const value = JSON.parse(text2);
4022
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) return value;
4023
+ } catch (error) {
4024
+ if (!(error instanceof SyntaxError)) throw error;
4025
+ }
4026
+ return {};
3883
4027
  }
3884
- function missedTurnEvents(events, turns, owner, now, lastWake) {
3885
- const added = [];
3886
- for (const turn of turns) {
3887
- const first = firstRecordedStart(events, turn.turnId);
3888
- if (first !== null && first - turn.startedAt <= MISSED_START_MS) continue;
3889
- if (turn.endedAt === null) {
3890
- const start = missedStartEvent(events, turn, owner, now, lastWake);
3891
- if (start) added.push(start);
3892
- continue;
4028
+ async function legacyJournals(fromDir, now) {
4029
+ let names;
4030
+ try {
4031
+ names = await readdir7(fromDir);
4032
+ } catch (error) {
4033
+ if (error.code === "ENOENT") return [];
4034
+ throw error;
4035
+ }
4036
+ const journals = [];
4037
+ for (const name of names.filter((entry) => entry.endsWith(".jsonl"))) {
4038
+ const from = join8(fromDir, name);
4039
+ try {
4040
+ const info = await stat3(from);
4041
+ journals.push({ name, from, size: info.size, mtimeMs: info.mtimeMs, modified: info.mtime, expired: now - info.mtimeMs > RETENTION_MS });
4042
+ } catch (error) {
4043
+ if (error.code !== "ENOENT") throw error;
3893
4044
  }
3894
- if (turn.durationMs === null) continue;
3895
- const stop = events.find(
3896
- (event) => event.event === "stop" && event.turnId === turn.turnId
3897
- );
3898
- let end;
3899
- let awakeToEnd;
3900
- if (stop?.up !== void 0) {
3901
- end = pointOf(stop.at, stop.up);
3902
- awakeToEnd = turn.durationMs - Math.max(0, turn.endedAt - end.wall);
3903
- } else {
3904
- end = countBack(turn.endedAt, events, now, lastWake);
3905
- awakeToEnd = turn.durationMs;
3906
- if (end === null) {
3907
- const sample = awakeSamples(events).filter((point) => point.wall > turn.startedAt && point.wall <= turn.endedAt).at(-1);
3908
- if (sample) {
3909
- end = sample;
3910
- awakeToEnd = turn.durationMs - (turn.endedAt - sample.wall);
3911
- }
3912
- }
3913
- if (end) added.push({ event: "stop", at: new Date(end.wall).toISOString(), up: end.up, turnId: turn.turnId });
4045
+ }
4046
+ return journals;
4047
+ }
4048
+ var unchanged = (journal, sources) => sources[journal.name]?.size === journal.size && sources[journal.name]?.mtimeMs === journal.mtimeMs;
4049
+ async function legacyJournalsWaiting(fromDir, now = Date.now()) {
4050
+ const journals = await legacyJournals(fromDir, now);
4051
+ if (journals.length === 0) return false;
4052
+ const sources = await adoptedSources();
4053
+ return journals.some((journal) => journal.expired || !unchanged(journal, sources));
4054
+ }
4055
+ async function adoptLegacyJournals(fromDir, extendStart, now = Date.now()) {
4056
+ const sources = await adoptedSources();
4057
+ const copies = [];
4058
+ let earliest = Infinity;
4059
+ for (const journal of await legacyJournals(fromDir, now)) {
4060
+ if (unchanged(journal, sources)) continue;
4061
+ const text2 = await readIfPresent(journal.from);
4062
+ if (text2 === null) continue;
4063
+ let events;
4064
+ try {
4065
+ events = text2.split("\n").filter((line) => line.trim() !== "").map((line) => currentFormOf(line, journal.from));
4066
+ } catch {
4067
+ await rename4(journal.from, journal.from + ".unreadable");
4068
+ continue;
3914
4069
  }
3915
- if (end === null || awakeToEnd <= 0) continue;
3916
- added.push({
3917
- event: "start",
3918
- at: new Date(turn.startedAt).toISOString(),
3919
- up: end.up - awakeToEnd,
3920
- sessionId: owner.sessionId,
3921
- agent: "codex",
3922
- cwd: owner.cwd,
3923
- turnId: turn.turnId,
3924
- source: "session-file"
3925
- });
4070
+ for (const event of events) earliest = Math.min(earliest, Date.parse(event.at));
4071
+ copies.push({ journal, copy: events.map((event) => JSON.stringify(event) + "\n").join("") });
4072
+ }
4073
+ if (earliest !== Infinity) await extendStart(earliest);
4074
+ await mkdir5(sessionsDir(), { recursive: true });
4075
+ for (const { journal, copy } of copies) {
4076
+ const target = join8(sessionsDir(), journal.name + ADOPTED);
4077
+ await writeFile5(target + ".partial", copy, { mode: 384 });
4078
+ await utimes(target + ".partial", journal.modified, journal.modified);
4079
+ await rename4(target + ".partial", target);
4080
+ sources[journal.name] = { size: journal.size, mtimeMs: journal.mtimeMs };
4081
+ }
4082
+ for (const journal of await legacyJournals(fromDir, now)) {
4083
+ if (!journal.expired || !unchanged(journal, sources)) continue;
4084
+ await rm3(journal.from, { force: true });
4085
+ delete sources[journal.name];
4086
+ }
4087
+ const manifest = join8(sessionsDir(), SOURCES);
4088
+ await writeFile5(manifest + ".partial", JSON.stringify(sources), { mode: 384 });
4089
+ await rename4(manifest + ".partial", manifest);
4090
+ try {
4091
+ await rmdir(fromDir);
4092
+ } catch (error) {
4093
+ const code = error.code;
4094
+ if (code !== "ENOTEMPTY" && code !== "EEXIST" && code !== "ENOENT") throw error;
3926
4095
  }
3927
- return added;
3928
- }
3929
- async function stageTime(sessionId, now) {
3930
- const events = await readEventsAt(sessionFile(sessionId));
3931
- if (!events) return null;
3932
- const owner = events.find(
3933
- (event) => event.event === "session" || event.event === "start"
3934
- );
3935
- if (owner?.agent !== "codex") return measureRecordedTime(events, now, signedInPeriods());
3936
- const file = await codexTurnsOf(sessionId, transcriptPathOf(events));
3937
- if (file === null) return measureRecordedTime(events, now, signedInPeriods());
3938
- const added = missedTurnEvents(events, file.turns, owner, now, lastWakeMs());
3939
- return measureRecordedTime(byTime([...events, ...added]), now, signedInPeriods());
3940
4096
  }
3941
- async function promptReminder(sessionId, cwd, now) {
3942
- const link2 = await readLink(cwd);
3943
- if (link2?.kind === "ignored") return "";
3944
- if (link2?.kind === "linked") {
3945
- const recorded2 = await recordedTime(sessionId, now);
3946
- return recorded2 && recorded2.minutes >= REMINDER_MINUTES ? unstagedReminder(link2.name, recorded2.minutes) : "";
4097
+ function parseHookInput(raw) {
4098
+ const value = JSON.parse(raw);
4099
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
4100
+ throw new Error("Hook input must be a JSON object.");
3947
4101
  }
3948
- const root = await findGitRoot(cwd);
3949
- if (root === null) return "";
3950
- const events = await readEventsAt(sessionFile(sessionId)) ?? [];
3951
- if (events.some((event) => event.event === "unlinked-reminder")) return "";
3952
- const recorded = measureRecordedTime(events, now, signedInPeriods());
3953
- if (!recorded || recorded.minutes < REMINDER_MINUTES) return "";
3954
- const repository = await remoteStatus(root);
3955
- if (repository.status !== "remote-linked" && repository.status !== "unlinked") return "";
3956
- await append(sessionId, { event: "unlinked-reminder", at: now.at.toISOString() });
3957
- return repository.status === "remote-linked" ? remoteLinkedReminder(repository.link, recorded.minutes) : unlinkedReminder(recorded.minutes);
4102
+ const {
4103
+ session_id: sessionId,
4104
+ hook_event_name: event,
4105
+ cwd,
4106
+ turn_id: turnId,
4107
+ agent_id: agentId,
4108
+ transcript_path: transcriptPath
4109
+ } = value;
4110
+ if (typeof sessionId !== "string" || sessionId === "") throw new Error("Hook input needs a session_id string.");
4111
+ if (typeof event !== "string") throw new Error("Hook input needs a hook_event_name string.");
4112
+ if (typeof cwd !== "string") throw new Error("Hook input needs a cwd string.");
4113
+ if (turnId !== void 0 && turnId !== null && typeof turnId !== "string") {
4114
+ throw new Error("Hook input turn_id must be a string when present.");
4115
+ }
4116
+ if (agentId !== void 0 && agentId !== null && typeof agentId !== "string") {
4117
+ throw new Error("Hook input agent_id must be a string when present.");
4118
+ }
4119
+ if (transcriptPath !== void 0 && transcriptPath !== null && typeof transcriptPath !== "string") {
4120
+ throw new Error("Hook input transcript_path must be a string when present.");
4121
+ }
4122
+ return {
4123
+ sessionId,
4124
+ event,
4125
+ cwd,
4126
+ turnId: typeof turnId === "string" ? turnId : null,
4127
+ agentId: typeof agentId === "string" && agentId !== "" ? agentId : null,
4128
+ transcriptPath: typeof transcriptPath === "string" && transcriptPath !== "" ? transcriptPath : null
4129
+ };
3958
4130
  }
3959
- async function restoreStageMark(sessionId, mark) {
3960
- const events = await readEventsAt(sessionFile(sessionId));
3961
- const existing = events?.filter((event) => event.event === "staged" && event.pendingStageId === mark.pendingStageId);
3962
- if (existing?.some((event) => event.event === "staged" && event.state === "accepted")) return "accepted";
3963
- if (existing?.length) return "pending";
3964
- await append(sessionId, {
3965
- event: "staged",
3966
- state: "waiting",
3967
- at: mark.at,
3968
- pendingStageId: mark.pendingStageId,
3969
- cutAt: mark.cut.at,
3970
- ...mark.cut.up === null ? {} : { cutUp: mark.cut.up }
3971
- });
3972
- return "pending";
4131
+ var proofRule = `Write the outcome as one line of at most ${AGENT_OUTCOME_MAX_LENGTH} characters: what changed. Add up to ${AGENT_DETAILS_MAX_COUNT} optional details for proof or open items, each one line of at most ${AGENT_DETAIL_MAX_LENGTH} characters.`;
4132
+ function sessionContext(projectName) {
4133
+ return [
4134
+ `Flowmind: this repository is linked to the Flowmind project "${terminalText(projectName)}".`,
4135
+ "When you finish a material unit of work here, stage it before you start the next one, with",
4136
+ '`flowmind agent stage --intention "<goal>" --outcome "<what changed>" --detail "<proof or open item>" --json`,',
4137
+ "adding `--artifact-url <commit, PR, or document URL>` when one exists.",
4138
+ proofRule,
4139
+ "When several units finish together, stage them as one proof.",
4140
+ "The CLI fills in the project and the recorded session time, including the time of your subagents.",
4141
+ "Subagents do not stage. Share the returned review URL with the user.",
4142
+ "Do not use `flowmind log` for agent work."
4143
+ ].join(" ");
3973
4144
  }
3974
- async function settleStageMark(sessionId, mark, outcome) {
3975
- const events = await readEventsAt(sessionFile(sessionId));
3976
- if (events?.some((e4) => e4.event === "staged" && e4.pendingStageId === mark.pendingStageId && (outcome.state === "accepted" ? e4.state === "accepted" && e4.proposalId === outcome.proposalId : e4.state === outcome.state && e4.reason === outcome.reason))) return;
3977
- await append(sessionId, {
3978
- event: "staged",
3979
- at: mark.at,
3980
- pendingStageId: mark.pendingStageId,
3981
- ...outcome,
3982
- cutAt: mark.cut.at,
3983
- ...mark.cut.up === null ? {} : { cutUp: mark.cut.up }
3984
- });
4145
+ function unstagedReminder(projectName, minutes) {
4146
+ return [
4147
+ `Flowmind: ${minutes} minutes of agent work in this session are not staged yet for "${terminalText(projectName)}".`,
4148
+ "When the current unit of work is complete, stage it with",
4149
+ '`flowmind agent stage --intention "<goal>" --outcome "<what changed>" --detail "<proof or open item>" --json`',
4150
+ "and share the review URL.",
4151
+ proofRule
4152
+ ].join(" ");
4153
+ }
4154
+ var projectLabel = (link2) => `"${terminalText(link2.name)}" (${link2.projectId})`;
4155
+ function remoteLinkedContext(link2) {
4156
+ return [
4157
+ `Flowmind: the remote already links this repository to the Flowmind project ${projectLabel(link2)},`,
4158
+ "but this checkout has no .flowmind.json yet.",
4159
+ "Run `git pull` to get .flowmind.json. Never create a new Flowmind project for this repository.",
4160
+ 'After the pull, stage finished work with `flowmind agent stage --intention "<goal>" --outcome "<what changed>" --detail "<proof or open item>" --json`.',
4161
+ proofRule
4162
+ ].join(" ");
4163
+ }
4164
+ function unlinkedContext() {
4165
+ return [
4166
+ "Flowmind: this git repository is not linked to a Flowmind project.",
4167
+ "If it is a project the user works on, link it before you finish:",
4168
+ "1. Run `git pull` first. Another machine may have linked it already.",
4169
+ "2. Look for the existing project with `flowmind projects --json`. One product is one project on every machine; match by product, repository and name.",
4170
+ '3. Only for a new product, create one with `flowmind projects add "<name>" --description "<one line>" --json`.',
4171
+ "4. Run `flowmind link <project-id>` and commit .flowmind.json.",
4172
+ "For a third-party clone or a scratch folder, run `flowmind link --ignore`. Ask the user once when you are unsure."
4173
+ ].join("\n");
4174
+ }
4175
+ function remoteLinkedReminder(link2, minutes) {
4176
+ return [
4177
+ `Flowmind: ${minutes} minutes of agent work, and this checkout still has no .flowmind.json.`,
4178
+ `The remote links this repository to ${projectLabel(link2)}. Run \`git pull\`; never create a new project for it.`
4179
+ ].join(" ");
3985
4180
  }
3986
- async function resolveSession(explicit, cwd, now = /* @__PURE__ */ new Date()) {
3987
- if (explicit) return { sessionId: explicit, source: "flag" };
3988
- const codexThread = process.env.CODEX_THREAD_ID || void 0;
3989
- if (codexThread && await readEventsAt(sessionFile(codexThread))) return { sessionId: codexThread, source: "codex-env" };
3990
- const root = await findRepoRoot(cwd);
3991
- const active = /* @__PURE__ */ new Set();
3992
- let names = [];
4181
+ function unlinkedReminder(minutes) {
4182
+ return [
4183
+ `Flowmind: ${minutes} minutes of agent work, and this repository is still not linked to a Flowmind project.`,
4184
+ "Before you finish: run `git pull`, find the project with `flowmind projects --json`",
4185
+ "(create one with `flowmind projects add` only for a new product), run `flowmind link <project-id>`, and commit .flowmind.json.",
4186
+ "For a third-party clone or a scratch folder, run `flowmind link --ignore`."
4187
+ ].join(" ");
4188
+ }
4189
+ async function pruneOldSessions(now) {
4190
+ let names;
3993
4191
  try {
3994
- names = await readdir4(sessionsDir());
4192
+ names = await readdir7(sessionsDir());
3995
4193
  } catch (error) {
3996
- if (error.code !== "ENOENT") throw error;
3997
- }
3998
- for (const name of names.filter((entry) => entry.endsWith(".jsonl"))) {
3999
- const path = join6(sessionsDir(), name);
4000
- if (now.getTime() - (await stat2(path)).mtimeMs > ACTIVE_WINDOW_MS) continue;
4001
- const events = await readEventsAt(path) ?? [];
4002
- if (codexThread && events.some((event) => event.event === "subagent" && event.agentId === codexThread)) {
4003
- const main = events.find(
4004
- (event) => event.event === "session" || event.event === "start"
4005
- );
4006
- if (main) return { sessionId: main.sessionId, source: "subagent" };
4007
- }
4008
- let last = null;
4009
- for (const event of events) {
4010
- if (event.event === "start") last = event;
4011
- if (event.event === "stop" && event.turnId === last?.turnId) last = null;
4012
- }
4013
- if (last?.event === "start" && now.getTime() - Date.parse(last.at) < OPEN_TURN_LIMIT_MS && await findRepoRoot(last.cwd) === root) {
4014
- active.add(last.sessionId);
4015
- }
4194
+ if (error.code === "ENOENT") return;
4195
+ throw error;
4016
4196
  }
4017
- if (active.size > 1) {
4018
- throw new Error("Several agent sessions have a turn in progress in this repository. Pass --session-id.");
4197
+ for (const name of journalNames(names)) {
4198
+ const path = join8(sessionsDir(), name);
4199
+ const retiring = await readIfPresent(path + RETIRED) !== null;
4200
+ if (!retiring && now.getTime() - await lastWritten(path) <= RETENTION_MS) continue;
4201
+ if (!retiring && await lastWritten(join8(legacySessionsDir(), name)) !== -Infinity) continue;
4202
+ if (!retiring) await writeFile5(path + RETIRED, "", { mode: 384 });
4203
+ await rm3(path, { force: true });
4204
+ await rm3(path + ADOPTED, { force: true });
4205
+ await rm3(path + RETIRED, { force: true });
4019
4206
  }
4020
- const [only] = active;
4021
- if (only) return { sessionId: only, source: "active-turn" };
4022
- return codexThread ? { sessionId: codexThread, source: "codex-env" } : { sessionId: null, source: null };
4023
4207
  }
4024
-
4025
- // src/command-error.ts
4026
- var LIMIT_WORDS = {
4027
- "projects.active": (limit) => `${limit} active projects`,
4028
- "logs.personal.month": (limit) => `${limit} personal logs this month`,
4029
- "logs.agent.month": (limit) => `${limit} agent logs this month`
4030
- };
4031
- var describePlanLimit = ({ limit, limitKey, resetAt }) => {
4032
- const what = (LIMIT_WORDS[limitKey] ?? ((count) => `${count} of ${limitKey}`))(limit);
4033
- const frees = resetAt === null ? "Archiving a finished project frees a slot (archiving is final), or upgrade to Pro." : `It resets on ${new Date(resetAt).toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" })}, or upgrade to Pro.`;
4034
- return `Your plan allows ${what}. ${frees} Everything you already logged stays. Upgrade in the Flowmind web app under Settings > Plan.`;
4035
- };
4036
- var printCommandError = (action, error, json = false, extra) => {
4037
- let diagnostic;
4038
- if (error instanceof PlanLimitError) {
4039
- const message = describePlanLimit(error.details);
4040
- console.error(json ? JSON.stringify({ error: message, code: error.code, ...error.details, ...extra }) : `${terminalText(message)}${extra ? `
4041
- ${terminalText(extra.message)}` : ""}`);
4042
- process.exitCode = 1;
4043
- return;
4044
- }
4045
- if (error instanceof BridgeError) {
4046
- const context2 = error.context;
4047
- const schema = `${context2.schemaId ?? "unavailable"}${context2.schemaPath ?? ""}`;
4048
- diagnostic = `${action} failed: ${error.code} (${context2.kind}); operation=${context2.operation ?? "unavailable"}; stage=${context2.stage ?? "request"}; schema=${schema}; requestId=${context2.requestId ?? "unavailable"}`;
4049
- } else {
4050
- diagnostic = `${action} failed: ${error instanceof Error ? error.message : "Unknown error"}`;
4051
- }
4052
- if (error instanceof ApiRejectionError) {
4053
- console.error(json ? JSON.stringify({ error: error.rejectionMessage, diagnostic, ...extra }) : `${terminalText(error.rejectionMessage)}
4054
- Error: ${terminalText(diagnostic)}${extra ? `
4055
- ${terminalText(extra.message)}` : ""}`);
4056
- } else {
4057
- console.error(json ? JSON.stringify({ error: diagnostic, ...extra }) : `Error: ${terminalText(diagnostic)}${extra ? `
4058
- ${terminalText(extra.message)}` : ""}`);
4208
+ async function handleHook(agent, raw, prune, now = currentMoment()) {
4209
+ const input2 = parseHookInput(raw);
4210
+ const at2 = now.at.toISOString();
4211
+ const { up } = now;
4212
+ const subagent = input2.agentId;
4213
+ switch (input2.event) {
4214
+ case "SessionStart": {
4215
+ if (subagent !== null) return "";
4216
+ await append(input2.sessionId, {
4217
+ event: "session",
4218
+ at: at2,
4219
+ up,
4220
+ sessionId: input2.sessionId,
4221
+ agent,
4222
+ cwd: input2.cwd,
4223
+ ...input2.transcriptPath === null ? {} : { transcriptPath: input2.transcriptPath }
4224
+ });
4225
+ await prune(now.at);
4226
+ const repository = await repositoryStatus(input2.cwd);
4227
+ switch (repository.status) {
4228
+ case "linked":
4229
+ return sessionContext(repository.link.name);
4230
+ case "remote-linked":
4231
+ return remoteLinkedContext(repository.link);
4232
+ case "unlinked":
4233
+ return unlinkedContext();
4234
+ case "ignored":
4235
+ case "outside-git":
4236
+ return "";
4237
+ }
4238
+ }
4239
+ case "UserPromptSubmit":
4240
+ if (subagent !== null) {
4241
+ await append(input2.sessionId, { event: "subagent", at: at2, up, agentId: subagent, turnId: input2.turnId });
4242
+ return "";
4243
+ }
4244
+ await append(input2.sessionId, {
4245
+ event: "start",
4246
+ at: at2,
4247
+ up,
4248
+ sessionId: input2.sessionId,
4249
+ agent,
4250
+ cwd: input2.cwd,
4251
+ turnId: input2.turnId
4252
+ });
4253
+ return promptReminder(input2.sessionId, input2.cwd, now);
4254
+ case "Stop":
4255
+ case "Interrupt":
4256
+ if (subagent !== null) return "";
4257
+ await append(input2.sessionId, { event: "stop", at: at2, up, turnId: input2.turnId });
4258
+ if (agent === "codex" && input2.turnId !== null) await addMissedTurnStart(input2, now);
4259
+ return "";
4260
+ default:
4261
+ throw new Error(`Unsupported hook event: ${input2.event}`);
4059
4262
  }
4060
- process.exitCode = 1;
4061
- };
4062
-
4063
- // src/legacy-proofs.ts
4064
- import { mkdir as mkdir5, readdir as readdir6, readFile as readFile6, rename as rename3, rm as rm3, stat as stat3 } from "node:fs/promises";
4065
- import { join as join8 } from "node:path";
4066
-
4067
- // src/pending-stages.ts
4068
- import { spawn } from "node:child_process";
4069
- import { createHash as createHash4, randomUUID as randomUUID3 } from "node:crypto";
4070
- import { mkdir as mkdir4, readdir as readdir5, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
4071
- import { basename, join as join7 } from "node:path";
4072
-
4073
- // src/stored-stage-input.ts
4074
- var SOURCE_KINDS = [
4075
- "calendar",
4076
- "claude_code",
4077
- "cli",
4078
- "codex",
4079
- "github",
4080
- "linear",
4081
- "manual",
4082
- "session_log",
4083
- "other"
4084
- ];
4085
- var evidenceKinds = ["calendar_event", "commit", "issue", "linear_issue", "pr", "session_log", "other"];
4086
- var shape = (v6, keys2) => v6 !== null && typeof v6 === "object" && !Array.isArray(v6) && Object.keys(v6).every((key) => keys2.includes(key));
4087
- var text = (v6, max) => typeof v6 === "string" && v6.trim().length > 0 && [...v6].length <= max;
4088
- var optionalText = (v6, max) => v6 === void 0 || v6 === null || text(v6, max);
4089
- var member = (v6, kinds) => typeof v6 === "string" && kinds.includes(v6);
4090
- var isoDate = (v6) => typeof v6 === "string" && Number.isFinite(Date.parse(v6)) && new Date(v6).toISOString() === v6;
4091
- var keys = [
4092
- "targetKind",
4093
- "projectId",
4094
- "startedAt",
4095
- "durationMinutes",
4096
- "description",
4097
- "note",
4098
- "details",
4099
- "artifactUrl",
4100
- "source",
4101
- "evidence"
4102
- ];
4103
- function isStoredStageInput(v6) {
4104
- if (!shape(v6, keys) || v6.targetKind !== "project" || typeof v6.projectId !== "string" || !isProjectId(v6.projectId) || !isoDate(v6.startedAt) || typeof v6.durationMinutes !== "number" || !Number.isSafeInteger(v6.durationMinutes) || v6.durationMinutes < 1 || !text(v6.description, 280) || !text(v6.note, AGENT_OUTCOME_MAX_LENGTH) || /[\r\n]/.test(v6.note) || v6.artifactUrl !== void 0 && (!text(v6.artifactUrl, 2048) || !isHttpEvidenceUrl(v6.artifactUrl)))
4105
- return false;
4106
- const details = v6.details === void 0 ? [] : v6.details;
4107
- if (!Array.isArray(details) || !details.every((d5) => text(d5, AGENT_DETAIL_MAX_LENGTH) && !/[\r\n]/.test(d5)) || agentProofIssues({ outcome: v6.note, details }).length)
4108
- return false;
4109
- const source = v6.source;
4110
- return shape(source, ["kind", "externalId", "label"]) && member(source.kind, SOURCE_KINDS) && optionalText(source.externalId, 200) && optionalText(source.label, 160) && Array.isArray(v6.evidence) && v6.evidence.length <= 20 && v6.evidence.every(
4111
- (e4) => shape(e4, ["kind", "label", "externalId", "url"]) && member(e4.kind, evidenceKinds) && text(e4.label, 160) && optionalText(e4.externalId, 200) && (e4.url === null || e4.url === void 0 || text(e4.url, 500) && isHttpEvidenceUrl(e4.url))
4112
- );
4113
4263
  }
4114
-
4115
- // src/pending-stages.ts
4116
- var STAGE_KEY_IN_USE = "This stage key is already in use";
4117
- var MAX_BACKOFF_MINUTES = 60;
4118
- var outboxDir = () => join7(accountDirectory(), "pending-stages");
4119
- var takeStageLock = () => tryLock(join7(accountDirectory(), "stage-writers"));
4120
- var fileOf2 = (id) => join7(outboxDir(), id + ".json");
4121
- var nextAttempt = (attempts, now) => new Date(now.getTime() + Math.min(MAX_BACKOFF_MINUTES, 2 ** Math.max(0, attempts - 1)) * 6e4).toISOString();
4122
- var errorText = (error) => error instanceof Error ? error.message : String(error);
4123
- var classifyStageFailure = (error) => {
4124
- if (!(error instanceof BridgeError)) return { kind: "unexpected", reason: errorText(error) };
4125
- const { kind, status } = error.context;
4126
- if (kind === "transport") return { kind: "retry", reason: "The Flowmind API could not be reached in time" };
4127
- if (kind === "contract")
4128
- return {
4129
- kind: "retry",
4130
- reason: "Flowmind's answer could not be read" + (status === null ? "" : " (HTTP " + status + ")")
4131
- };
4132
- const message = error instanceof ApiRejectionError ? error.rejectionMessage : "The Flowmind API answered HTTP " + status;
4133
- if (status === 401 || status === 403) return { kind: "login", reason: message };
4134
- if (status === 409 && message === STAGE_KEY_IN_USE) return { kind: "conflict", reason: message };
4135
- if (status !== null && status >= 400 && status < 500 && status !== 408 && status !== 429)
4136
- return { kind: "reject", reason: message };
4137
- return { kind: "retry", reason: message };
4138
- };
4139
- var stageIdOf = (idempotencyKey, input2) => createHash4("sha256").update(idempotencyKey + "\n" + JSON.stringify(input2)).digest("hex").slice(0, 16);
4140
- var cutDigestOf = (id, sessionId, cut) => createHash4("sha256").update(JSON.stringify([id, sessionId, cut === null ? null : [cut.at, cut.up]])).digest("hex");
4141
- function readStage(value, fileName, legacy = false) {
4142
- const stage = value;
4143
- if (typeof stage !== "object" || stage === null) return null;
4144
- const input2 = stage.input;
4145
- const { id, sessionId, cut } = stage;
4146
- const valid = typeof id === "string" && /^[a-f0-9]{16}$/.test(id) && fileName === id + ".json" && typeof stage.idempotencyKey === "string" && stage.idempotencyKey.trim() === stage.idempotencyKey && /^[\x20-\x7e]+$/.test(stage.idempotencyKey) && typeof stage.apiUrl === "string" && isStoredStageInput(input2) && (legacy && stage.credentialFingerprint === void 0 || typeof stage.credentialFingerprint === "string" && /^[a-f0-9]{64}$/.test(stage.credentialFingerprint)) && stageIdOf(stage.idempotencyKey, input2) === id && isoDate(stage.savedAt) && isoDate(stage.nextAttemptAt) && (stage.lastAttemptAt === null || isoDate(stage.lastAttemptAt)) && (sessionId === null || typeof sessionId === "string" && sessionId.length > 0) && (cut === null || typeof cut === "object" && isoDate(cut.at) && Date.parse(cut.at) <= Date.parse(stage.savedAt) && (cut.up === null || Number.isFinite(cut.up))) && sessionId === null === (cut === null) && (stage.state === "waiting" || stage.state === "rejected" || stage.state === "conflict") && Number.isSafeInteger(stage.attempts) && stage.attempts >= 0 && (typeof stage.lastError === "string" || stage.state === "waiting" && stage.lastError === null);
4147
- if (!valid) return null;
4148
- const cutDigest = cutDigestOf(id, sessionId, cut);
4149
- if (stage.cutDigest !== cutDigest && !(legacy && stage.cutDigest === void 0)) return null;
4150
- return { ...stage, credentialFingerprint: stage.credentialFingerprint ?? null, cutDigest };
4264
+ function turnIdTime(turnId) {
4265
+ const hex = turnId.replaceAll("-", "");
4266
+ if (!/^[0-9a-f]{32}$/i.test(hex) || hex[12] !== "7") return null;
4267
+ return Number.parseInt(hex.slice(0, 12), 16);
4151
4268
  }
4152
- var writePendingStage = async (stage) => {
4153
- await mkdir4(outboxDir(), { recursive: true });
4154
- const temporary = fileOf2(stage.id) + "." + process.pid + "." + randomUUID3() + ".tmp";
4155
- await writeFile4(temporary, JSON.stringify(stage, null, 2) + "\n", { encoding: "utf8", mode: 384 });
4156
- await rename2(temporary, fileOf2(stage.id));
4269
+ var firstRecordedStart = (events, turnId) => {
4270
+ const first = events.find((event) => event.event === "start" && event.turnId === turnId);
4271
+ return first ? Date.parse(first.at) : null;
4157
4272
  };
4158
- async function saveStageBeforeSending(input2) {
4159
- const at2 = input2.now.toISOString();
4160
- const id = stageIdOf(input2.idempotencyKey, input2.stage);
4161
- const pending = {
4162
- id,
4163
- savedAt: at2,
4164
- apiUrl: API_URL,
4165
- idempotencyKey: input2.idempotencyKey,
4166
- credentialFingerprint: accountBinding().credential,
4167
- input: input2.stage,
4168
- sessionId: input2.sessionId,
4169
- cut: input2.cut,
4170
- cutDigest: cutDigestOf(id, input2.sessionId, input2.cut),
4171
- state: "waiting",
4172
- attempts: 0,
4173
- lastAttemptAt: null,
4174
- nextAttemptAt: at2,
4175
- lastError: null
4176
- };
4177
- if (!readStage(pending, pending.id + ".json"))
4178
- throw new Error("The stage request does not match the Flowmind proof contract. Check its fields before retrying.");
4179
- const { stages, unreadable } = await listPendingStages();
4180
- if (unreadable.includes(fileOf2(pending.id)) || stages.some((stage) => stage.id === pending.id)) {
4181
- throw new Error("This proof (" + pending.id + ") is already saved. See: flowmind agent pending");
4182
- }
4183
- await writePendingStage(pending);
4184
- return pending;
4273
+ function hooksSawStart(events, turnId) {
4274
+ const first = firstRecordedStart(events, turnId);
4275
+ const created = turnIdTime(turnId);
4276
+ return first !== null && created !== null && first - created <= MISSED_START_MS;
4277
+ }
4278
+ var transcriptPathOf = (events) => events.filter((event) => event.event === "session").filter((event) => event.transcriptPath !== void 0).at(-1)?.transcriptPath ?? null;
4279
+ async function codexTurnsOf(sessionId, knownPath) {
4280
+ const known = knownPath === null ? null : await readCodexTurns(knownPath);
4281
+ if (known !== null) return { path: knownPath, turns: known };
4282
+ const found = await findCodexSessionFile(sessionId);
4283
+ const turns = found === null ? null : await readCodexTurns(found);
4284
+ return turns === null ? null : { path: found, turns };
4185
4285
  }
4186
- async function recordStageFailure(stage, failure, now) {
4187
- const attempts = stage.attempts + 1;
4188
- const next = {
4189
- ...stage,
4190
- state: failure.kind === "conflict" || stage.state === "conflict" ? "conflict" : failure.kind === "reject" ? "rejected" : "waiting",
4191
- attempts,
4192
- lastAttemptAt: now.toISOString(),
4193
- nextAttemptAt: nextAttempt(attempts, now),
4194
- lastError: failure.kind === "conflict" || stage.state === "conflict" ? conflictAdvice(stage.id) : failure.reason
4286
+ function countBack(wall, events, now, lastWake) {
4287
+ const nowWall = now.at.getTime();
4288
+ if (wall > nowWall) return null;
4289
+ const back = { wall, up: now.up - (nowWall - wall) };
4290
+ if (lastWake !== null && lastWake <= nowWall) return lastWake <= wall ? back : null;
4291
+ const before = awakeSamples(events).filter((point) => point.wall <= wall).at(-1);
4292
+ return before && Math.abs(nowWall - now.up - (before.wall - before.up)) <= CLOCK_DRIFT_MS ? back : null;
4293
+ }
4294
+ var awakeSamples = (events) => events.flatMap((event) => event.event !== "unlinked-reminder" && event.up !== void 0 ? [pointOf(event.at, event.up)] : []);
4295
+ function awakePointAt(wall, events, now, lastWake) {
4296
+ const exact = countBack(wall, events, now, lastWake);
4297
+ if (exact) return exact;
4298
+ const nowWall = now.at.getTime();
4299
+ if (lastWake !== null && lastWake <= nowWall) return countBack(lastWake, events, now, lastWake);
4300
+ return awakeSamples(events).find((point) => point.wall > wall && point.wall < nowWall) ?? null;
4301
+ }
4302
+ function missedStartEvent(events, turn, owner, now, lastWake) {
4303
+ const first = firstRecordedStart(events, turn.turnId);
4304
+ if (first !== null && first - turn.startedAt <= MISSED_START_MS) return null;
4305
+ const point = awakePointAt(turn.startedAt, events, now, lastWake);
4306
+ if (point === null) return null;
4307
+ return {
4308
+ event: "start",
4309
+ at: new Date(point.wall).toISOString(),
4310
+ ...point.up === null ? {} : { up: point.up },
4311
+ sessionId: owner.sessionId,
4312
+ agent: "codex",
4313
+ cwd: owner.cwd,
4314
+ turnId: turn.turnId,
4315
+ source: "session-file"
4195
4316
  };
4196
- await writePendingStage(next);
4197
- return next;
4198
4317
  }
4199
- async function forgetStage(id) {
4200
- await rm2(fileOf2(id), { force: true });
4318
+ async function addMissedTurnStart(input2, now) {
4319
+ const turnId = input2.turnId;
4320
+ const events = await readEventsAt(sessionFile(input2.sessionId)) ?? [];
4321
+ if (hooksSawStart(events, turnId)) return;
4322
+ const file = await codexTurnsOf(input2.sessionId, input2.transcriptPath ?? transcriptPathOf(events));
4323
+ if (file === null) return;
4324
+ const turn = file.turns.find((candidate) => candidate.turnId === turnId);
4325
+ if (!turn) throw new Error(`The Codex session file ${file.path} has no start for turn ${turnId}.`);
4326
+ const start = missedStartEvent(events, turn, input2, now, lastWakeMs());
4327
+ if (start) await append(input2.sessionId, start);
4201
4328
  }
4202
- async function listPendingStages() {
4203
- let names;
4204
- try {
4205
- names = await readdir5(outboxDir());
4206
- } catch (error) {
4207
- if (error.code === "ENOENT") return { stages: [], unreadable: [] };
4208
- throw error;
4209
- }
4210
- const stages = [];
4211
- const unreadable = [];
4212
- for (const name of names.filter((entry) => entry.endsWith(".json"))) {
4213
- const path = join7(outboxDir(), name);
4214
- let text2;
4215
- try {
4216
- text2 = await readFile5(path, "utf8");
4217
- } catch (error) {
4218
- if (error.code === "ENOENT") continue;
4219
- unreadable.push(path);
4220
- continue;
4329
+ var pointOf = (at2, up) => ({ wall: Date.parse(at2), up: up ?? null });
4330
+ var cutOf = (event) => pointOf(event.cutAt, event.cutUp);
4331
+ function span(from, to) {
4332
+ const wall = to.wall - from.wall;
4333
+ if (wall <= 0 || from.up !== null && to.up !== null && to.up < from.up) return { from, to, ms: 0, awake: false };
4334
+ const ms = from.up === null || to.up === null ? wall : Math.min(to.up - from.up, wall);
4335
+ return { from, to, ms: ms > OPEN_TURN_LIMIT_MS ? 0 : ms, awake: from.up !== null && to.up !== null };
4336
+ }
4337
+ var sameEpoch = (from, to) => from.up === null || to.up === null || to.up >= from.up;
4338
+ function supportedEdge(start, edge, end) {
4339
+ const follows = (a5, b4) => a5.up !== null && b4.up !== null && b4.up >= a5.up && b4.up - a5.up <= b4.wall - a5.wall + CLOCK_DRIFT_MS;
4340
+ return follows(start, edge) && follows(edge, end);
4341
+ }
4342
+ function ownedParts(start, end, cut, periods) {
4343
+ if (end.wall <= start.wall || end.wall - start.wall > OPEN_TURN_LIMIT_MS || !sameEpoch(start, end)) return [];
4344
+ return periods.flatMap((period) => {
4345
+ if (period.start.wall >= end.wall || period.end && period.end.wall <= start.wall) return [];
4346
+ let from = start, to = end;
4347
+ if (period.start.wall > start.wall) {
4348
+ if (!supportedEdge(start, period.start, end)) return [];
4349
+ from = period.start;
4221
4350
  }
4222
- let value;
4223
- try {
4224
- value = JSON.parse(text2);
4225
- } catch {
4226
- value = null;
4351
+ if (period.end && period.end.wall < end.wall) {
4352
+ if (!supportedEdge(start, period.end, end)) return [];
4353
+ to = period.end;
4227
4354
  }
4228
- const stage = readStage(value, name);
4229
- if (stage && stage.apiUrl === API_URL) stages.push(stage);
4230
- else unreadable.push(path);
4231
- }
4232
- return { stages: stages.sort((a5, b4) => Date.parse(a5.savedAt) - Date.parse(b4.savedAt)), unreadable };
4355
+ if (cut && cut.wall > from.wall) from = cut;
4356
+ const part = span(from, to);
4357
+ return part.ms > 0 ? [part] : [];
4358
+ });
4233
4359
  }
4234
- async function restoreSavedStageMarks(measuring = false) {
4235
- const { stages, unreadable } = await listPendingStages();
4236
- if (measuring && unreadable.length) {
4237
- throw new Error(
4238
- "Cannot measure new work: unreadable reservation " + unreadable[0] + ". Check Activity. Restore the original file and run flowmind agent retry, or discard the reservation with flowmind agent discard " + basename(unreadable[0], ".json") + ". An unowned original needs flowmind agent adopt <id>."
4239
- );
4360
+ function measureRecordedTime(events, now, periods) {
4361
+ const first = events.find((event) => event.event === "session" || event.event === "start");
4362
+ if (!first || first.event !== "session" && first.event !== "start") return null;
4363
+ const nowPoint = { wall: now.at.getTime(), up: now.up };
4364
+ const turns = [];
4365
+ let open2 = null;
4366
+ let unknownStarts = 0;
4367
+ let lastStage = null;
4368
+ const settled = new Set(events.flatMap((event) => event.event === "staged" && event.state !== "waiting" ? [event.pendingStageId] : []));
4369
+ for (const event of events) {
4370
+ if (event.event === "session" || event.event === "unlinked-reminder") continue;
4371
+ const point = pointOf(event.at, event.up);
4372
+ switch (event.event) {
4373
+ case "start":
4374
+ if (event.turnId === null && (unknownStarts > 0 || open2 && open2.turnId === null)) {
4375
+ unknownStarts = unknownStarts ? unknownStarts + 1 : 2;
4376
+ open2 = null;
4377
+ break;
4378
+ }
4379
+ if (open2 && open2.turnId !== null && open2.turnId === event.turnId) {
4380
+ if (sameEpoch(open2.start, point)) open2.last = point;
4381
+ break;
4382
+ }
4383
+ if (open2) turns.push({ start: open2.start, end: open2.last });
4384
+ open2 = { start: point, last: point, turnId: event.turnId };
4385
+ break;
4386
+ case "stop": {
4387
+ if (event.turnId === null && unknownStarts > 0) {
4388
+ unknownStarts--;
4389
+ break;
4390
+ }
4391
+ const matches = open2 !== null && event.turnId === open2.turnId;
4392
+ if (open2 && matches) {
4393
+ turns.push({ start: open2.start, end: sameEpoch(open2.start, point) ? point : open2.last });
4394
+ open2 = null;
4395
+ }
4396
+ break;
4397
+ }
4398
+ case "staged":
4399
+ if (event.state === "waiting" && settled.has(event.pendingStageId)) break;
4400
+ if (!lastStage || cutOf(event).wall >= cutOf(lastStage).wall) lastStage = event;
4401
+ break;
4402
+ case "subagent":
4403
+ if (open2 && open2.turnId === event.turnId && sameEpoch(open2.start, point)) open2.last = point;
4404
+ break;
4405
+ }
4240
4406
  }
4241
- for (const stage of stages) {
4242
- if (stage.sessionId === null || stage.cut === null) continue;
4243
- const mark = { at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id };
4244
- if (await restoreStageMark(stage.sessionId, mark) === "accepted") {
4245
- await forgetStage(stage.id);
4407
+ if (open2) turns.push({ start: open2.start, end: open2.last });
4408
+ const stagedCut = lastStage ? cutOf(lastStage) : null;
4409
+ const counted = turns.flatMap(
4410
+ ({ start, end }) => ownedParts(start, end.wall > nowPoint.wall ? nowPoint : end, stagedCut, periods)
4411
+ );
4412
+ const unstagedMs = counted.reduce((sum, part) => sum + part.ms, 0);
4413
+ const minutes = Math.floor(unstagedMs / 6e4);
4414
+ let cut = { at: now.at.toISOString(), up: now.up };
4415
+ let remaining = minutes * 6e4;
4416
+ for (const part of counted) {
4417
+ if (remaining > part.ms) {
4418
+ remaining -= part.ms;
4246
4419
  continue;
4247
4420
  }
4248
- if ((stage.state === "rejected" || stage.state === "conflict") && stage.lastError !== null) {
4249
- await settleStageMark(stage.sessionId, mark, { state: stage.state, reason: stage.lastError });
4250
- }
4421
+ cut = {
4422
+ at: new Date(part.from.wall + remaining).toISOString(),
4423
+ up: part.awake && part.from.up !== null ? part.from.up + remaining : null
4424
+ };
4425
+ break;
4251
4426
  }
4252
- }
4253
- var isDue = (stage, now) => stage.state === "waiting" && stage.apiUrl === API_URL && Date.parse(stage.nextAttemptAt) <= now.getTime();
4254
- async function sendPendingStages(options) {
4255
- const clock = options.now ?? (() => /* @__PURE__ */ new Date());
4256
- const count = async () => {
4257
- const { stages, unreadable } = await listPendingStages();
4258
- return { waiting: stages.filter((stage) => stage.state === "waiting").length, unreadable };
4259
- };
4260
- const result = {
4261
- busy: false,
4262
- sent: [],
4263
- rejected: [],
4264
- conflicts: [],
4265
- stoppedBy: null,
4266
- waiting: 0,
4267
- unreadable: []
4427
+ return {
4428
+ agent: first.agent,
4429
+ unstagedMs,
4430
+ minutes,
4431
+ startedAt: counted.length > 0 ? new Date(counted[0].from.wall).toISOString() : null,
4432
+ cut,
4433
+ lastProposalId: lastStage?.state === "accepted" ? lastStage.proposalId : null,
4434
+ lastPendingStageId: lastStage && lastStage.state !== "accepted" ? lastStage.pendingStageId : null,
4435
+ lastHold: lastStage?.state === "conflict" || lastStage?.state === "discarded" ? lastStage.reason : null,
4436
+ lastRejection: lastStage?.state === "rejected" ? lastStage.reason : null
4268
4437
  };
4269
- const lock = await takeStageLock();
4270
- if (!lock) return { ...result, busy: true, ...await count() };
4271
- try {
4272
- await restoreSavedStageMarks();
4273
- const candidates = (await listPendingStages()).stages.filter((stage) => {
4274
- const now = clock();
4275
- if (stage.state === "conflict") {
4276
- if (options.manual && stage.credentialFingerprint === accountBinding().credential) return true;
4277
- if (options.manual) result.conflicts.push({ id: stage.id, reason: conflictAdvice(stage.id) });
4278
- return false;
4279
- }
4280
- return options.due ? isDue(stage, now) : stage.state === "waiting" && stage.apiUrl === API_URL;
4281
- });
4282
- for (const stage of candidates) {
4283
- let proposal;
4284
- try {
4285
- proposal = await createAgentLogProposal(stage.input, stage.idempotencyKey);
4286
- } catch (error) {
4287
- const failure = classifyStageFailure(error);
4288
- const saved = await recordStageFailure(stage, failure, clock());
4289
- if (saved.state === "conflict") {
4290
- if (stage.sessionId && stage.cut)
4291
- await settleStageMark(
4292
- stage.sessionId,
4293
- { at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id },
4294
- { state: "conflict", reason: conflictAdvice(stage.id) }
4295
- );
4296
- result.conflicts.push({ id: stage.id, reason: conflictAdvice(stage.id) });
4297
- continue;
4298
- }
4299
- if (failure.kind === "reject") {
4300
- if (stage.sessionId && stage.cut)
4301
- await settleStageMark(
4302
- stage.sessionId,
4303
- { at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id },
4304
- { state: "rejected", reason: failure.reason }
4305
- );
4306
- result.rejected.push({ id: stage.id, reason: failure.reason });
4307
- continue;
4438
+ }
4439
+ async function recordedTime(sessionId, now) {
4440
+ const events = await readEventsAt(sessionFile(sessionId));
4441
+ return events ? measureRecordedTime(events, now, signedInPeriods()) : null;
4442
+ }
4443
+ function missedTurnEvents(events, turns, owner, now, lastWake) {
4444
+ const added = [];
4445
+ for (const turn of turns) {
4446
+ const first = firstRecordedStart(events, turn.turnId);
4447
+ if (first !== null && first - turn.startedAt <= MISSED_START_MS) continue;
4448
+ if (turn.endedAt === null) {
4449
+ const start = missedStartEvent(events, turn, owner, now, lastWake);
4450
+ if (start) added.push(start);
4451
+ continue;
4452
+ }
4453
+ if (turn.durationMs === null) continue;
4454
+ const stop = events.find(
4455
+ (event) => event.event === "stop" && event.turnId === turn.turnId
4456
+ );
4457
+ let end;
4458
+ let awakeToEnd;
4459
+ if (stop?.up !== void 0) {
4460
+ end = pointOf(stop.at, stop.up);
4461
+ awakeToEnd = turn.durationMs - Math.max(0, turn.endedAt - end.wall);
4462
+ } else {
4463
+ end = countBack(turn.endedAt, events, now, lastWake);
4464
+ awakeToEnd = turn.durationMs;
4465
+ if (end === null) {
4466
+ const sample = awakeSamples(events).filter((point) => point.wall > turn.startedAt && point.wall <= turn.endedAt).at(-1);
4467
+ if (sample) {
4468
+ end = sample;
4469
+ awakeToEnd = turn.durationMs - (turn.endedAt - sample.wall);
4308
4470
  }
4309
- result.stoppedBy = failure.reason;
4310
- break;
4311
4471
  }
4312
- if (stage.sessionId && stage.cut)
4313
- await settleStageMark(
4314
- stage.sessionId,
4315
- { at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id },
4316
- { state: "accepted", proposalId: proposal.id }
4317
- );
4318
- await forgetStage(stage.id);
4319
- result.sent.push({ id: stage.id, proposal });
4472
+ if (end) added.push({ event: "stop", at: new Date(end.wall).toISOString(), up: end.up, turnId: turn.turnId });
4320
4473
  }
4321
- } finally {
4322
- await lock.release();
4474
+ if (end === null || awakeToEnd <= 0) continue;
4475
+ added.push({
4476
+ event: "start",
4477
+ at: new Date(turn.startedAt).toISOString(),
4478
+ up: end.up - awakeToEnd,
4479
+ sessionId: owner.sessionId,
4480
+ agent: "codex",
4481
+ cwd: owner.cwd,
4482
+ turnId: turn.turnId,
4483
+ source: "session-file"
4484
+ });
4323
4485
  }
4324
- return { ...result, ...await count() };
4486
+ return added;
4325
4487
  }
4326
- var conflictAdvice = (id) => "Stage key conflict (" + id + "). Delivery is unknown. Check Activity, then retry with the original credential or run flowmind agent discard " + id + ".";
4327
- async function discardReservation(id) {
4328
- if (!id || basename(id) !== id || id === "." || id === "..") return false;
4329
- const { stages, unreadable } = await listPendingStages();
4330
- const stage = stages.find((item) => item.id === id);
4331
- if (!stage && !unreadable.includes(fileOf2(id))) return false;
4332
- if (stage && stage.state !== "conflict")
4333
- throw new Error("This account already has a saved proof " + id + ". See: flowmind agent pending");
4334
- if (stage?.sessionId && stage.cut)
4335
- await settleStageMark(
4336
- stage.sessionId,
4337
- { at: stage.savedAt, cut: stage.cut, pendingStageId: id },
4338
- { state: "discarded", reason: "Discarded after checking Activity." }
4339
- );
4340
- await forgetStage(id);
4341
- return true;
4488
+ async function stageTime(sessionId, now) {
4489
+ const events = await readEventsAt(sessionFile(sessionId));
4490
+ if (!events) return null;
4491
+ const owner = events.find(
4492
+ (event) => event.event === "session" || event.event === "start"
4493
+ );
4494
+ if (owner?.agent !== "codex") return measureRecordedTime(events, now, signedInPeriods());
4495
+ const file = await codexTurnsOf(sessionId, transcriptPathOf(events));
4496
+ if (file === null) return measureRecordedTime(events, now, signedInPeriods());
4497
+ const added = missedTurnEvents(events, file.turns, owner, now, lastWakeMs());
4498
+ return measureRecordedTime(byTime([...events, ...added]), now, signedInPeriods());
4342
4499
  }
4343
- async function startDueRetry(now) {
4344
- const { stages } = await listPendingStages();
4345
- if (!stages.some((stage) => isDue(stage, now))) return false;
4346
- const script = process.argv[1];
4347
- if (!script) throw new Error("Cannot find the Flowmind CLI script to send saved stages");
4348
- const child = spawn(process.execPath, [script, "agent", "retry", "--due"], {
4349
- detached: true,
4350
- stdio: "ignore",
4351
- windowsHide: true
4500
+ async function promptReminder(sessionId, cwd, now) {
4501
+ const link2 = await readLink(cwd);
4502
+ if (link2?.kind === "ignored") return "";
4503
+ if (link2?.kind === "linked") {
4504
+ const recorded2 = await recordedTime(sessionId, now);
4505
+ return recorded2 && recorded2.minutes >= REMINDER_MINUTES ? unstagedReminder(link2.name, recorded2.minutes) : "";
4506
+ }
4507
+ const root = await findGitRoot(cwd);
4508
+ if (root === null) return "";
4509
+ const events = await readEventsAt(sessionFile(sessionId)) ?? [];
4510
+ if (events.some((event) => event.event === "unlinked-reminder")) return "";
4511
+ const recorded = measureRecordedTime(events, now, signedInPeriods());
4512
+ if (!recorded || recorded.minutes < REMINDER_MINUTES) return "";
4513
+ const repository = await remoteStatus(root);
4514
+ if (repository.status !== "remote-linked" && repository.status !== "unlinked") return "";
4515
+ await append(sessionId, { event: "unlinked-reminder", at: now.at.toISOString() });
4516
+ return repository.status === "remote-linked" ? remoteLinkedReminder(repository.link, recorded.minutes) : unlinkedReminder(recorded.minutes);
4517
+ }
4518
+ async function restoreStageMark(sessionId, mark) {
4519
+ const events = await readEventsAt(sessionFile(sessionId));
4520
+ const existing = events?.filter((event) => event.event === "staged" && event.pendingStageId === mark.pendingStageId);
4521
+ if (existing?.some((event) => event.event === "staged" && event.state === "accepted")) return "accepted";
4522
+ if (existing?.length) return "pending";
4523
+ await append(sessionId, {
4524
+ event: "staged",
4525
+ state: "waiting",
4526
+ at: mark.at,
4527
+ pendingStageId: mark.pendingStageId,
4528
+ cutAt: mark.cut.at,
4529
+ ...mark.cut.up === null ? {} : { cutUp: mark.cut.up }
4352
4530
  });
4353
- child.on("error", (error) => {
4354
- console.error("Flowmind could not start sending saved stages: " + error.message);
4355
- process.exitCode = 1;
4531
+ return "pending";
4532
+ }
4533
+ async function settleStageMark(sessionId, mark, outcome) {
4534
+ const events = await readEventsAt(sessionFile(sessionId));
4535
+ if (events?.some((e4) => e4.event === "staged" && e4.pendingStageId === mark.pendingStageId && (outcome.state === "accepted" ? e4.state === "accepted" && e4.proposalId === outcome.proposalId : e4.state === outcome.state && e4.reason === outcome.reason))) return;
4536
+ await append(sessionId, {
4537
+ event: "staged",
4538
+ at: mark.at,
4539
+ pendingStageId: mark.pendingStageId,
4540
+ ...outcome,
4541
+ cutAt: mark.cut.at,
4542
+ ...mark.cut.up === null ? {} : { cutUp: mark.cut.up }
4356
4543
  });
4357
- child.unref();
4358
- return true;
4359
4544
  }
4360
-
4361
- // src/legacy-proofs.ts
4362
- async function legacyStages() {
4363
- let names;
4545
+ async function resolveSession(explicit, cwd, now = /* @__PURE__ */ new Date()) {
4546
+ if (explicit) return { sessionId: explicit, source: "flag" };
4547
+ const codexThread = process.env.CODEX_THREAD_ID || void 0;
4548
+ if (codexThread && await readEventsAt(sessionFile(codexThread))) return { sessionId: codexThread, source: "codex-env" };
4549
+ const root = await findRepoRoot(cwd);
4550
+ const active = /* @__PURE__ */ new Set();
4551
+ let names = [];
4364
4552
  try {
4365
- names = await readdir6(legacyOutboxDir());
4553
+ names = await readdir7(sessionsDir());
4366
4554
  } catch (error) {
4367
- if (error.code === "ENOENT") return { stages: [], unreadable: [] };
4368
- throw error;
4555
+ if (error.code !== "ENOENT") throw error;
4369
4556
  }
4370
- const stages = [];
4371
- const unreadable = [];
4372
- for (const name of names.filter((entry) => entry.endsWith(".json"))) {
4373
- const path = join8(legacyOutboxDir(), name);
4374
- let value = null;
4375
- try {
4376
- value = JSON.parse(await readFile6(path, "utf8"));
4377
- } catch (error) {
4378
- if (error.code === "ENOENT") continue;
4379
- if (!(error instanceof SyntaxError)) throw error;
4557
+ for (const name of journalNames(names)) {
4558
+ const path = join8(sessionsDir(), name);
4559
+ if (now.getTime() - await lastWritten(path) > ACTIVE_WINDOW_MS) continue;
4560
+ const events = await readEventsAt(path) ?? [];
4561
+ if (codexThread && events.some((event) => event.event === "subagent" && event.agentId === codexThread)) {
4562
+ const main = events.find(
4563
+ (event) => event.event === "session" || event.event === "start"
4564
+ );
4565
+ if (main) return { sessionId: main.sessionId, source: "subagent" };
4380
4566
  }
4381
- const stage = readStage(value, name, true);
4382
- if (stage) stages.push(stage);
4383
- else unreadable.push(path);
4384
- }
4385
- return { stages, unreadable };
4386
- }
4387
- async function moveIntoAccount(stage) {
4388
- await mkdir5(outboxDir(), { recursive: true, mode: 448 });
4389
- const target = join8(outboxDir(), stage.id + ".json");
4390
- const present = await stat3(target).then(
4391
- () => true,
4392
- (error) => {
4393
- if (error.code === "ENOENT") return false;
4394
- throw error;
4567
+ let last = null;
4568
+ for (const event of events) {
4569
+ if (event.event === "start") last = event;
4570
+ if (event.event === "stop" && event.turnId === last?.turnId) last = null;
4395
4571
  }
4396
- );
4397
- if (present)
4398
- throw new Error("This account already has a reservation for " + stage.id + ". Check Activity before adopting it.");
4399
- await rename3(join8(legacyOutboxDir(), stage.id + ".json"), target);
4400
- await writePendingStage({ ...stage, credentialFingerprint: accountBinding().credential });
4401
- }
4402
- async function listUnownedProofs() {
4403
- const { stages, unreadable } = await legacyStages();
4404
- const unowned = stages.map((stage) => ({
4405
- id: stage.id,
4406
- projectId: stage.input.projectId,
4407
- durationMinutes: stage.input.durationMinutes,
4408
- savedAt: stage.savedAt,
4409
- apiUrl: stage.apiUrl
4410
- })).sort((a5, b4) => Date.parse(a5.savedAt) - Date.parse(b4.savedAt));
4411
- return { unowned, unreadable };
4412
- }
4413
- async function findUnowned(id) {
4414
- if (!/^[a-f0-9]{16}$/.test(id)) throw new Error("No unowned saved proof " + id + ". See: flowmind agent pending");
4415
- const stage = (await legacyStages()).stages.find((candidate) => candidate.id === id);
4416
- if (!stage) throw new Error("No unowned saved proof " + id + ". See: flowmind agent pending");
4417
- return stage;
4418
- }
4419
- async function adoptUnownedProof(id) {
4420
- const stage = await findUnowned(id);
4421
- if (stage.apiUrl !== API_URL)
4422
- throw new Error("Saved proof " + id + " belongs to " + stage.apiUrl + ", not " + API_URL + ".");
4423
- await moveIntoAccount(stage);
4424
- }
4425
- async function discardUnownedProof(id) {
4426
- await findUnowned(id);
4427
- await rm3(join8(legacyOutboxDir(), id + ".json"));
4572
+ if (last?.event === "start" && now.getTime() - Date.parse(last.at) < OPEN_TURN_LIMIT_MS && await findRepoRoot(last.cwd) === root) {
4573
+ active.add(last.sessionId);
4574
+ }
4575
+ }
4576
+ if (active.size > 1) {
4577
+ throw new Error("Several agent sessions have a turn in progress in this repository. Pass --session-id.");
4578
+ }
4579
+ const [only] = active;
4580
+ if (only) return { sessionId: only, source: "active-turn" };
4581
+ return codexThread ? { sessionId: codexThread, source: "codex-env" } : { sessionId: null, source: null };
4428
4582
  }
4429
4583
 
4584
+ // src/command-error.ts
4585
+ var LIMIT_WORDS = {
4586
+ "projects.active": (limit) => `${limit} active projects`,
4587
+ "logs.personal.month": (limit) => `${limit} personal logs this month`,
4588
+ "logs.agent.month": (limit) => `${limit} agent logs this month`
4589
+ };
4590
+ var describePlanLimit = ({ limit, limitKey, resetAt }) => {
4591
+ const what = (LIMIT_WORDS[limitKey] ?? ((count) => `${count} of ${limitKey}`))(limit);
4592
+ const frees = resetAt === null ? "Archiving a finished project frees a slot (archiving is final), or upgrade to Pro." : `It resets on ${new Date(resetAt).toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" })}, or upgrade to Pro.`;
4593
+ return `Your plan allows ${what}. ${frees} Everything you already logged stays. Upgrade in the Flowmind web app under Settings > Plan.`;
4594
+ };
4595
+ var printCommandError = (action, error, json = false, extra) => {
4596
+ let diagnostic;
4597
+ if (error instanceof PlanLimitError) {
4598
+ const message = describePlanLimit(error.details);
4599
+ console.error(json ? JSON.stringify({ error: message, code: error.code, ...error.details, ...extra }) : `${terminalText(message)}${extra ? `
4600
+ ${terminalText(extra.message)}` : ""}`);
4601
+ process.exitCode = 1;
4602
+ return;
4603
+ }
4604
+ if (error instanceof BridgeError) {
4605
+ const context2 = error.context;
4606
+ const schema = `${context2.schemaId ?? "unavailable"}${context2.schemaPath ?? ""}`;
4607
+ diagnostic = `${action} failed: ${error.code} (${context2.kind}); operation=${context2.operation ?? "unavailable"}; stage=${context2.stage ?? "request"}; schema=${schema}; requestId=${context2.requestId ?? "unavailable"}`;
4608
+ } else {
4609
+ diagnostic = `${action} failed: ${error instanceof Error ? error.message : "Unknown error"}`;
4610
+ }
4611
+ if (error instanceof ApiRejectionError) {
4612
+ console.error(json ? JSON.stringify({ error: error.rejectionMessage, diagnostic, ...extra }) : `${terminalText(error.rejectionMessage)}
4613
+ Error: ${terminalText(diagnostic)}${extra ? `
4614
+ ${terminalText(extra.message)}` : ""}`);
4615
+ } else {
4616
+ console.error(json ? JSON.stringify({ error: diagnostic, ...extra }) : `Error: ${terminalText(diagnostic)}${extra ? `
4617
+ ${terminalText(extra.message)}` : ""}`);
4618
+ }
4619
+ process.exitCode = 1;
4620
+ };
4621
+
4430
4622
  // src/commands/agent-stage.ts
4431
4623
  import { createHash as createHash5 } from "node:crypto";
4432
4624
  var trimRequired = (value, field) => {
@@ -4612,7 +4804,7 @@ async function resolveStageOptions(options, cwd, now) {
4612
4804
  recorded = session.sessionId ? await stageTime(session.sessionId, now) : null;
4613
4805
  if (!recorded) {
4614
4806
  throw new Error(
4615
- "No recorded agent time for this session. Install the session hooks with `flowmind hooks install`, or pass --minutes and --started-at."
4807
+ "No recorded agent time for this session. If the session hooks are not installed, run `flowmind hooks install`; for this session, pass --minutes and --started-at."
4616
4808
  );
4617
4809
  }
4618
4810
  if (recorded.minutes < 1 || recorded.startedAt === null) {
@@ -4785,6 +4977,7 @@ agentCommand.command("stage").description("Stage completed agent work as a proje
4785
4977
  await ensureAccount();
4786
4978
  lock = await takeStageLock();
4787
4979
  if (!lock) throw new Error("Another Flowmind process is staging or sending proofs. Try again when it finishes.");
4980
+ await adoptLegacyStateHeld();
4788
4981
  await restoreSavedStageMarks(options.minutes === void 0 && options.startedAt === void 0);
4789
4982
  const resolved = await resolveStageOptions({ ...stageOptions, detail }, process.cwd(), now);
4790
4983
  if (resolved.kind === "skip") {
@@ -4961,7 +5154,16 @@ agentCommand.command("hook").description('Record agent session time from Codex o
4961
5154
  try {
4962
5155
  if (!getApiKey()) return;
4963
5156
  await ensureAccount();
4964
- const context2 = await handleHook(options.agent, await readStdin());
5157
+ const pruneWhenFree = async (at2) => {
5158
+ const pruning = await takeStageLock();
5159
+ if (!pruning) return;
5160
+ try {
5161
+ await pruneOldSessions(at2);
5162
+ } finally {
5163
+ await pruning.release();
5164
+ }
5165
+ };
5166
+ const context2 = await handleHook(options.agent, await readStdin(), pruneWhenFree);
4965
5167
  if (context2) process.stdout.write(`${context2}
4966
5168
  `);
4967
5169
  await startDueRetry(/* @__PURE__ */ new Date());
@@ -5219,9 +5421,28 @@ var reviewNaturalLog = async (preview) => {
5219
5421
 
5220
5422
  // src/commands/log.ts
5221
5423
  var FLOW_SESSION_MIN_HOURS = 1.5;
5424
+ var HOUR_UNIT = "(?:h|hr|hrs|hour|hours)";
5425
+ var MINUTE_UNIT = "(?:m|min|mins|minute|minutes)";
5426
+ var HOURS = new RegExp("^(\\d+(?:[.,]\\d+)?|[.,]\\d+)(?:\\s*" + HOUR_UNIT + ")?$");
5427
+ var MINUTES = new RegExp("^(\\d+)\\s*" + MINUTE_UNIT + "$");
5428
+ var HOURS_AND_MINUTES = new RegExp("^(\\d+)\\s*" + HOUR_UNIT + "\\s*(\\d+)(?:\\s*" + MINUTE_UNIT + ")?$");
5429
+ function parseHours(value) {
5430
+ const text2 = value.trim().toLowerCase();
5431
+ let match;
5432
+ let minutes = Number.NaN;
5433
+ if (match = HOURS.exec(text2)) minutes = Number(match[1].replace(",", ".")) * 60;
5434
+ else if (match = MINUTES.exec(text2)) minutes = Number(match[1]);
5435
+ else if (match = HOURS_AND_MINUTES.exec(text2)) minutes = Number(match[1]) * 60 + Number(match[2]);
5436
+ const whole = Math.round(minutes);
5437
+ if (!Number.isSafeInteger(whole) || whole < 1) {
5438
+ throw new InvalidArgumentError2("Give the time as hours or minutes, for example 1.5, 1,5, 2h, 90m or 1h30m.");
5439
+ }
5440
+ return whole / 60;
5441
+ }
5442
+ var duration = (minutes) => [minutes >= 60 ? Math.floor(minutes / 60) + "h" : "", minutes % 60 ? minutes % 60 + "m" : ""].filter(Boolean).join(" ");
5222
5443
  function printCreated(result) {
5223
5444
  console.log(chalk3.green(`
5224
- \u2713 Logged ${result.hours}h successfully.`));
5445
+ \u2713 Logged ${duration(Math.round(result.hours * 60))} successfully.`));
5225
5446
  if (result.xpAwarded !== null) {
5226
5447
  console.log(chalk3.green(` +${result.xpAwarded} XP earned`));
5227
5448
  }
@@ -5236,16 +5457,7 @@ function printCreatedTimeEntry(result) {
5236
5457
  isFlowSession: result.isFlowSession
5237
5458
  });
5238
5459
  }
5239
- var logCommand = new Command3("log").description("Log time with natural language or explicit project/hours").helpOption("-H, --help", "display help for command").argument("[text]", 'Natural language input, e.g. "worked 2.5h on API"').option("-p, --project <project>", "Project name").option("-h, --hours <hours>", "Hours worked", (value) => {
5240
- if (!/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(value) || !Number.isFinite(Number(value)) || Number(value) <= 0) {
5241
- throw new InvalidArgumentError2("Hours must be a positive decimal number, for example 1.5.");
5242
- }
5243
- const fraction = (value.split(".")[1] ?? "").replace(/0+$/, "");
5244
- if (fraction.length > 2 || Number(fraction.padEnd(2, "0")) % 5 !== 0) {
5245
- throw new InvalidArgumentError2("Hours must resolve to whole minutes.");
5246
- }
5247
- return Number(value);
5248
- }).option("--started-at <ISO 8601 date-time>", "Session start with timezone; defaults to save time minus duration").option("-i, --intention <text>", "What the session intended to complete").option("-o, --outcome <text>", "What changed, shipped, or became clear").option("--next <text>", "Private next step saved with this session").option("-a, --artifact-url <url>", "Commit, document, design, or published artifact URL").action(async (text2, options) => {
5460
+ var logCommand = new Command3("log").description("Log time with natural language or explicit project/hours").helpOption("-H, --help", "display help for command").argument("[text]", 'Natural language input, e.g. "worked 2.5h on API"').option("-p, --project <project>", "Project name").option("-h, --hours <time>", "Time worked: 1.5, 1,5, 2h, 90m or 1h30m", parseHours).option("--started-at <ISO 8601 date-time>", "Session start with timezone; defaults to save time minus duration").option("-i, --intention <text>", "What the session intended to complete").option("-o, --outcome <text>", "What changed, shipped, or became clear").option("--next <text>", "Private next step saved with this session").option("-a, --artifact-url <url>", "Commit, document, design, or published artifact URL").action(async (text2, options) => {
5249
5461
  const usesStructuredOptions = options.next !== void 0 || options.project !== void 0 || options.hours !== void 0 || options.intention !== void 0 || options.outcome !== void 0 || options.artifactUrl !== void 0 || options.startedAt !== void 0;
5250
5462
  if (text2 && usesStructuredOptions) {
5251
5463
  console.error(chalk3.red("Use either natural language input OR -p/-h options, not both in the same command."));
@@ -5305,7 +5517,7 @@ var logCommand = new Command3("log").description("Log time with natural language
5305
5517
  // src/commands/hooks.ts
5306
5518
  import { createHash as createHash6, randomUUID as randomUUID4 } from "node:crypto";
5307
5519
  import { realpathSync } from "node:fs";
5308
- import { readFile as readFile7, link, lstat, mkdir as mkdir6, rename as rename4, rm as rm4, writeFile as writeFile5 } from "node:fs/promises";
5520
+ import { readFile as readFile7, link, lstat, mkdir as mkdir6, rename as rename5, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
5309
5521
  import { homedir as homedir2 } from "node:os";
5310
5522
  import { dirname as dirname3, join as join9 } from "node:path";
5311
5523
  import { fileURLToPath } from "node:url";
@@ -5508,7 +5720,7 @@ async function updateHookFile(agent, action, base) {
5508
5720
  const changed = () => new Error(path + " changed while Flowmind was updating it. Nothing was written; run the command again.");
5509
5721
  const removeFile = agent === "codex" && action === "uninstall" && Object.keys(next).length === 0;
5510
5722
  const temporary = path + "." + randomUUID4() + ".tmp";
5511
- if (!removeFile) await writeFile5(temporary, serialize(next), { mode: info ? info.mode & 511 : 384, flag: "wx" });
5723
+ if (!removeFile) await writeFile6(temporary, serialize(next), { mode: info ? info.mode & 511 : 384, flag: "wx" });
5512
5724
  try {
5513
5725
  let onDisk;
5514
5726
  try {
@@ -5526,7 +5738,7 @@ async function updateHookFile(agent, action, base) {
5526
5738
  if (error.code === "EEXIST") throw changed();
5527
5739
  throw error;
5528
5740
  }
5529
- } else await rename4(temporary, path);
5741
+ } else await rename5(temporary, path);
5530
5742
  } finally {
5531
5743
  await rm4(temporary, { force: true });
5532
5744
  }
@@ -5568,10 +5780,10 @@ hooksCommand.command("install").description(
5568
5780
  }
5569
5781
  await mkdir6(dirname3(target.path), { recursive: true });
5570
5782
  if (writeSkill) {
5571
- await writeFile5(target.path, content, { flag: "w", mode: 420 });
5783
+ await writeFile6(target.path, content, { flag: "w", mode: 420 });
5572
5784
  console.log(terminalLine`Wrote ${target.path}`);
5573
5785
  }
5574
- await writeFile5(target.markerPath, `${nextHash}
5786
+ await writeFile6(target.markerPath, `${nextHash}
5575
5787
  `, { flag: "w", mode: 420 });
5576
5788
  console.log(terminalLine`Wrote ${target.markerPath}`);
5577
5789
  }
@@ -5678,7 +5890,7 @@ hooksCommand.command("uninstall").description("Remove unmodified Flowmind-manage
5678
5890
  });
5679
5891
 
5680
5892
  // src/commands/link.ts
5681
- import { readdir as readdir7 } from "node:fs/promises";
5893
+ import { readdir as readdir8 } from "node:fs/promises";
5682
5894
  import { join as join10, resolve as resolve2 } from "node:path";
5683
5895
  import { Command as Command5, InvalidArgumentError as InvalidArgumentError4 } from "commander";
5684
5896
  import chalk4 from "chalk";
@@ -5696,7 +5908,7 @@ async function findRepositories(dir, depth, found) {
5696
5908
  if (depth === 0) return;
5697
5909
  let entries;
5698
5910
  try {
5699
- entries = await readdir7(dir, { withFileTypes: true });
5911
+ entries = await readdir8(dir, { withFileTypes: true });
5700
5912
  } catch (error) {
5701
5913
  const code = error.code;
5702
5914
  if (code === "EACCES" || code === "EPERM") return;
@@ -7637,7 +7849,7 @@ for (const [name, description, done] of [
7637
7849
 
7638
7850
  // src/index.ts
7639
7851
  var program = new Command12();
7640
- program.name("flowmind").description("Flowmind CLI (also installed as fm)").version("0.3.8").enablePositionalOptions();
7852
+ program.name("flowmind").description("Flowmind CLI (also installed as fm)").version("0.3.9").enablePositionalOptions();
7641
7853
  program.addCommand(agentCommand);
7642
7854
  program.addCommand(authCommand);
7643
7855
  program.addCommand(logCommand);