@useshifu/coding-harness 0.3.28 → 0.3.30
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/README.md +33 -5
- package/bin/continuity-preparation.js +186 -0
- package/bin/continuity-state.js +202 -0
- package/bin/native-preparation.js +63 -31
- package/bin/opencode-v2-account.js +186 -0
- package/bin/shifu-harness.js +228 -54
- package/bin/source-review.js +28 -2
- package/package.json +1 -1
- package/skills/shifu-context/SKILL.md +1 -1
- package/skills/shifu-sync/SKILL.md +7 -1
package/bin/shifu-harness.js
CHANGED
|
@@ -46,7 +46,7 @@ const MAX_TURNS_PER_SEGMENT = 12;
|
|
|
46
46
|
const MAX_TITLE_LENGTH = 160;
|
|
47
47
|
const MAX_WORK_DETAIL_LENGTH = 600;
|
|
48
48
|
const CONFIG_VERSION = 2;
|
|
49
|
-
const RUNNER_VERSION = "0.3.
|
|
49
|
+
const RUNNER_VERSION = "0.3.30";
|
|
50
50
|
const REDACTION_VERSION = 5;
|
|
51
51
|
const LEGACY_REDACTION_VERSION = 4;
|
|
52
52
|
const MAX_PREPARATIONS_PER_RUN = 4;
|
|
@@ -109,13 +109,9 @@ function claudeSessionsRoot() {
|
|
|
109
109
|
function opencodeQuery(query) {
|
|
110
110
|
if (process.env.SHIFU_OPENCODE_DB && !process.env.SHIFU_OPENCODE_BIN) {
|
|
111
111
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
116
|
-
});
|
|
117
|
-
return JSON.parse(sqliteOutput);
|
|
118
|
-
} catch {
|
|
112
|
+
return openCodeDatabaseQuery(process.env.SHIFU_OPENCODE_DB, query);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error.code === "SHIFU_SQLITE_UNAVAILABLE") throw error;
|
|
119
115
|
throw new Error("Could not read OpenCode session metadata. Ensure the opencode command is installed and its local database is available.");
|
|
120
116
|
}
|
|
121
117
|
}
|
|
@@ -137,16 +133,13 @@ function opencodeQuery(query) {
|
|
|
137
133
|
try { fs.closeSync(fd); } catch {}
|
|
138
134
|
fd = undefined;
|
|
139
135
|
}
|
|
140
|
-
const dbPath = process.env.SHIFU_OPENCODE_DB ||
|
|
136
|
+
const dbPath = process.env.SHIFU_OPENCODE_DB || openCodeDatabasePath();
|
|
141
137
|
if (fs.existsSync(dbPath)) {
|
|
142
138
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
});
|
|
148
|
-
return JSON.parse(sqliteOutput);
|
|
149
|
-
} catch {}
|
|
139
|
+
return openCodeDatabaseQuery(dbPath, query);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error.code === "SHIFU_SQLITE_UNAVAILABLE") throw error;
|
|
142
|
+
}
|
|
150
143
|
}
|
|
151
144
|
throw new Error("Could not read OpenCode session metadata. Ensure the opencode command is installed and its local database is available.");
|
|
152
145
|
} finally {
|
|
@@ -157,6 +150,56 @@ function opencodeQuery(query) {
|
|
|
157
150
|
}
|
|
158
151
|
}
|
|
159
152
|
|
|
153
|
+
function openCodeDatabasePath() {
|
|
154
|
+
try {
|
|
155
|
+
const { resolveNativeBinary, nativeEnvironment } = require("./native-preparation.js");
|
|
156
|
+
const location = execFileSync(resolveNativeBinary("opencode"), ["debug", "paths", "db"], {
|
|
157
|
+
env: nativeEnvironment(), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 10_000, maxBuffer: 1024 * 1024,
|
|
158
|
+
}).trim();
|
|
159
|
+
if (path.isAbsolute(location) && !/[\r\n\0]/.test(location)) return location;
|
|
160
|
+
} catch {}
|
|
161
|
+
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"), "opencode", "opencode.db");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function openCodeDatabaseQuery(dbPath, query) {
|
|
165
|
+
function read(sql) {
|
|
166
|
+
let output;
|
|
167
|
+
try {
|
|
168
|
+
output = execFileSync("sqlite3", ["-readonly", "-json", "-cmd", ".timeout 5000", dbPath, sql], {
|
|
169
|
+
encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 10_000, maxBuffer: 64 * 1024 * 1024,
|
|
170
|
+
});
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error.code !== "ENOENT") throw error;
|
|
173
|
+
const unavailable = new Error("Reading the local OpenCode database requires sqlite3. Install sqlite3 and retry; no work was synced.");
|
|
174
|
+
unavailable.code = "SHIFU_SQLITE_UNAVAILABLE";
|
|
175
|
+
throw unavailable;
|
|
176
|
+
}
|
|
177
|
+
return output.trim() ? JSON.parse(output) : [];
|
|
178
|
+
}
|
|
179
|
+
const tables = read("SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('session_v2', 'session_message')");
|
|
180
|
+
if (tables.length !== 2) return read(query);
|
|
181
|
+
// V2 stores typed messages, ordered by sequence, instead of separate parts.
|
|
182
|
+
// Read-only CTEs retain the existing adapter contract without changing its database.
|
|
183
|
+
const hasDirectory = read("PRAGMA table_info(session_v2)").some((column) => column.name === "directory");
|
|
184
|
+
const projection = `WITH
|
|
185
|
+
session AS (SELECT id, time_created, time_updated, ${hasDirectory ? "directory" : "NULL AS directory"} FROM session_v2),
|
|
186
|
+
message AS (
|
|
187
|
+
SELECT id, session_id, seq, time_created, json_object(
|
|
188
|
+
'role', type, 'finish', CASE WHEN json_extract(data, '$.time.completed') IS NOT NULL THEN json_extract(data, '$.finish') END,
|
|
189
|
+
'error', json_extract(data, '$.error'), 'modelID', json_extract(data, '$.model.id'), 'storageVersion', 2
|
|
190
|
+
) AS data FROM session_message WHERE type IN ('user', 'assistant', 'idle')
|
|
191
|
+
),
|
|
192
|
+
part AS (
|
|
193
|
+
SELECT id || '_user' AS id, id AS message_id, json_object('type', 'text', 'text', json_extract(data, '$.text')) AS data
|
|
194
|
+
FROM session_message WHERE type = 'user'
|
|
195
|
+
UNION ALL
|
|
196
|
+
SELECT m.id || '_' || printf('%08d', p.key), m.id, p.value
|
|
197
|
+
FROM session_message m, json_each(m.data, '$.content') p WHERE m.type = 'assistant'
|
|
198
|
+
) `;
|
|
199
|
+
return read(projection + query.replaceAll("ORDER BY s.time_updated, m.time_created", "ORDER BY s.time_updated, m.seq")
|
|
200
|
+
.replaceAll("ORDER BY m.time_created, p.id", "ORDER BY m.seq, p.id"));
|
|
201
|
+
}
|
|
202
|
+
|
|
160
203
|
function opencodeSessions(rows) {
|
|
161
204
|
const sessions = new Map();
|
|
162
205
|
for (const row of rows) {
|
|
@@ -202,6 +245,7 @@ function validateSyncOptions(command, start = 3) {
|
|
|
202
245
|
"sync-files": common,
|
|
203
246
|
"scheduled-sync": common,
|
|
204
247
|
pending: common,
|
|
248
|
+
"retry-preparation": [...common, "--session-ref", "--from-turn"],
|
|
205
249
|
approve: common,
|
|
206
250
|
install: common,
|
|
207
251
|
connect: [...common, ...setup, ...syncSettings, "--api-url"],
|
|
@@ -254,6 +298,7 @@ function writeJSON(file, value) {
|
|
|
254
298
|
}
|
|
255
299
|
|
|
256
300
|
function readConfig(harness) {
|
|
301
|
+
if (fs.existsSync(path.join(configRoot(), `${harness}-connection-transition.json`))) throw new Error("An interrupted connection replacement is held locally. Run connect again with the selected connection before syncing.");
|
|
257
302
|
const config = readJSON(configPath(harness), undefined);
|
|
258
303
|
if (!config || typeof config.token !== "string" || typeof config.apiUrl !== "string") {
|
|
259
304
|
throw new Error(`No ${HARNESS_NAMES[harness]} connection is configured. Run connect first.`);
|
|
@@ -589,7 +634,7 @@ function reviewSourcePacket(harness, sessionRef, fromTurn, toTurn, scheduled = f
|
|
|
589
634
|
|
|
590
635
|
function sourceRecords(harness, sessionRef) {
|
|
591
636
|
if (harness === "opencode") {
|
|
592
|
-
return opencodeQuery(`SELECT m.id AS messageId, m.time_created AS createdAt, json_extract(m.data, '$.role') AS role, json_extract(m.data, '$.finish') AS finish, json_extract(m.data, '$.error') AS error, p.data AS part FROM message m LEFT JOIN part p ON p.message_id = m.id WHERE m.session_id = ${JSON.stringify(sessionRef)} ORDER BY m.time_created, p.id`);
|
|
637
|
+
return opencodeQuery(`SELECT m.id AS messageId, m.time_created AS createdAt, json_extract(m.data, '$.role') AS role, json_extract(m.data, '$.finish') AS finish, json_extract(m.data, '$.error') AS error, json_extract(m.data, '$.storageVersion') AS storageVersion, p.data AS part FROM message m LEFT JOIN part p ON p.message_id = m.id WHERE m.session_id = ${JSON.stringify(sessionRef)} ORDER BY m.time_created, p.id`);
|
|
593
638
|
}
|
|
594
639
|
const file = sessionFile(harness, sessionRef);
|
|
595
640
|
try { return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line)); }
|
|
@@ -1231,7 +1276,7 @@ function copyRunner() {
|
|
|
1231
1276
|
const installedDependency = path.join(configRoot(), "node_modules", name);
|
|
1232
1277
|
if (!sameFile(dependency, installedDependency)) copyDirectory(dependency, installedDependency);
|
|
1233
1278
|
}
|
|
1234
|
-
for (const name of ["source-review.js", "v5-payload.js", "native-preparation.js", "context-preferences.js", "setup-options.js", "initial-history.js", "onboarding-ui.js"]) {
|
|
1279
|
+
for (const name of ["source-review.js", "v5-payload.js", "native-preparation.js", "opencode-v2-account.js", "context-preferences.js", "setup-options.js", "initial-history.js", "onboarding-ui.js", "continuity-preparation.js", "continuity-state.js"]) {
|
|
1235
1280
|
const source = path.join(__dirname, name);
|
|
1236
1281
|
const destination = path.join(configRoot(), name);
|
|
1237
1282
|
if (!sameFile(source, destination)) {
|
|
@@ -1515,7 +1560,7 @@ async function connect(harness, readKey = readSecret, choices) {
|
|
|
1515
1560
|
const token = (await readKey()).trim();
|
|
1516
1561
|
if (!TOKEN_PATTERN.test(token)) throw new Error("The connection key is invalid.");
|
|
1517
1562
|
const apiUrl = requireSafeApiUrl(option("--api-url") || existing?.apiUrl || process.env.SHIFU_API_URL || DEFAULT_API_URL);
|
|
1518
|
-
await validateConnection(apiUrl, token, harness);
|
|
1563
|
+
const connectionInfo = await validateConnection(apiUrl, token, harness);
|
|
1519
1564
|
const savedPolicy = existing && normalizedPolicy(existing);
|
|
1520
1565
|
const currentPolicy = savedPolicy?.onboardingComplete ? savedPolicy : { intervalHours: DEFAULT_INTERVAL_HOURS, approvalMode: "automatic", excludedSessionRefs: savedPolicy?.excludedSessionRefs || [] };
|
|
1521
1566
|
const selected = await chooseSyncPolicy(currentPolicy);
|
|
@@ -1528,15 +1573,17 @@ async function connect(harness, readKey = readSecret, choices) {
|
|
|
1528
1573
|
install(harness, true, choices.contextPreferences);
|
|
1529
1574
|
const scheduleFile = scheduleEnabled ? installSchedule(harness) : undefined;
|
|
1530
1575
|
if (!scheduleEnabled && savedPolicy?.scheduleEnabled) removeSchedule(harness);
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1576
|
+
const connection = { ...existing, ...choices, version: CONFIG_VERSION, apiUrl, token, harness, syncPolicy, inputFolder, capabilities: connectionInfo.capabilities || {} };
|
|
1577
|
+
const committed = await withSyncLock(harness, async () => {
|
|
1578
|
+
if (JSON.stringify(readJSON(configPath(harness), undefined)) !== JSON.stringify(existing)) throw new Error("The connection changed during setup. Run connect again.");
|
|
1579
|
+
require("./continuity-state.js").commitConnection(configRoot(), harness, existing, connection);
|
|
1580
|
+
refreshManagedContextInstructions(harness, choices.contextPreferences);
|
|
1581
|
+
if (refreshContextKey) writePrivateText(openCodeTokenPath(), token);
|
|
1582
|
+
const continuity = continuityState(harness, connection);
|
|
1583
|
+
continuity.load(); continuity.dictionary();
|
|
1584
|
+
scheduleNextRun(harness, syncPolicy.intervalHours);
|
|
1585
|
+
});
|
|
1586
|
+
if (committed?.skipped) throw new Error("Another Shifu sync is running. Retry connect after it finishes; the connection was not replaced.");
|
|
1540
1587
|
console.error(`${HARNESS_NAMES[harness]} is connected. ${scheduleEnabled ? `It will sync every ${syncPolicy.intervalHours} hours with ${syncPolicy.approvalMode} approval.` : "The sync schedule is off."}`);
|
|
1541
1588
|
console.error(`Shifu sync folder: ${inputFolder}. Place .txt, .md, .rtf, or .docx files there. Only grounded contributions will become claims.`);
|
|
1542
1589
|
if (conflicts.length) console.error(`Existing Shifu shortcut(s) were left unchanged: ${conflicts.join(", ")}.`);
|
|
@@ -2175,10 +2222,11 @@ function retryDelay(response, attempt) {
|
|
|
2175
2222
|
return Math.min(base * (2 ** attempt), 30_000);
|
|
2176
2223
|
}
|
|
2177
2224
|
|
|
2178
|
-
async function postSync(config, input) {
|
|
2225
|
+
async function postSync(config, input, beforeAttempt, onPermanentFailure) {
|
|
2179
2226
|
const body = JSON.stringify(input.force ? { ...input, forceRequestId: randomUUID() } : input);
|
|
2180
2227
|
let lastError;
|
|
2181
2228
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt += 1) {
|
|
2229
|
+
beforeAttempt?.();
|
|
2182
2230
|
let response;
|
|
2183
2231
|
try {
|
|
2184
2232
|
response = await fetch(`${config.apiUrl}/v1/connectors/coding-sessions/syncs`, {
|
|
@@ -2195,7 +2243,7 @@ async function postSync(config, input) {
|
|
|
2195
2243
|
const payload = await response.json().catch(() => undefined);
|
|
2196
2244
|
if (response.ok) return payload?.data;
|
|
2197
2245
|
lastError = new Error(payload?.error?.message || `Shifu rejected the sync (${response.status}).`);
|
|
2198
|
-
if (response.status !== 429 && response.status < 500) throw lastError;
|
|
2246
|
+
if (response.status !== 429 && response.status < 500) { onPermanentFailure?.(); throw lastError; }
|
|
2199
2247
|
if (attempt + 1 < MAX_RETRIES) await sleep(retryDelay(response, attempt));
|
|
2200
2248
|
}
|
|
2201
2249
|
throw lastError;
|
|
@@ -2404,20 +2452,27 @@ async function sendSync(harness, input, config = readConfig(harness)) {
|
|
|
2404
2452
|
validateSync(input, harness);
|
|
2405
2453
|
const state = readState(harness);
|
|
2406
2454
|
const checkpoint = state.sessions[input.sessionRef];
|
|
2455
|
+
const store = continuityState(harness, config);
|
|
2456
|
+
const frozen = store.find(input);
|
|
2407
2457
|
const expected = (checkpoint?.lastSyncedTurn || 0) + 1;
|
|
2408
2458
|
const force = input.force || Boolean(input.supersedesSyncId);
|
|
2409
|
-
if (!force && input.fromTurn !== expected) throw new Error(`This local checkpoint expects turn ${expected}. Review the pending segment before retrying.`);
|
|
2459
|
+
if (!force && !frozen && input.fromTurn !== expected) throw new Error(`This local checkpoint expects turn ${expected}. Review the pending segment before retrying.`);
|
|
2410
2460
|
if (force && reviewSegment(harness, input.sessionRef, input.fromTurn, input.toTurn).turnCount < input.toTurn) throw new Error("The original local turns must still exist before force syncing this range.");
|
|
2411
|
-
if (!force) input = queuePayload(harness, input);
|
|
2461
|
+
if (!force && (!frozen || input.toTurn > (checkpoint?.lastSyncedTurn || 0))) input = queuePayload(harness, input);
|
|
2412
2462
|
let profile;
|
|
2413
2463
|
if (!force) {
|
|
2414
2464
|
try { profile = profileForSegment(harness, input); }
|
|
2415
2465
|
catch { console.error("AI Profile source is unavailable; the prepared activity payload remains retryable."); }
|
|
2416
2466
|
}
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2467
|
+
let receipt;
|
|
2468
|
+
try {
|
|
2469
|
+
receipt = frozen?.receipt || await postSync(config, input, frozen ? () => store.reserveBase(input) : undefined, frozen ? () => store.update(frozen.key, (entry) => { entry.baseStatus = "needs_review"; entry.reason = "base_rejected"; }) : undefined);
|
|
2470
|
+
if (!validSyncReceipt(receipt, input)) throw new Error("Shifu returned a checkpoint that does not match this reviewed segment. Local state was not advanced.");
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
if (frozen && store.find(input).baseAttempts >= 10) store.update(frozen.key, (entry) => { entry.baseStatus = "needs_review"; entry.reason = "base_transport_budget"; });
|
|
2473
|
+
throw error;
|
|
2420
2474
|
}
|
|
2475
|
+
if (frozen) store.bind(input, receipt);
|
|
2421
2476
|
if (profile) {
|
|
2422
2477
|
state.profilePending ||= {};
|
|
2423
2478
|
state.profilePending[`${profile.sessionRef}:${profile.fromTurn}:${profile.toTurn}`] = profile;
|
|
@@ -2526,27 +2581,107 @@ async function reconcilePreparedSession(harness, sessionRef, config) {
|
|
|
2526
2581
|
saveState(harness, state);
|
|
2527
2582
|
}
|
|
2528
2583
|
|
|
2584
|
+
function continuityState(harness, config = readConfig(harness)) {
|
|
2585
|
+
return require("./continuity-state.js").continuityStore(configRoot(), harness, config);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
function preparationPacket(harness, segment, config) {
|
|
2589
|
+
if (normalizedPolicy(config).excludedSessionRefs.includes(segment.sessionRef)) throw new Error("This session is excluded from preparation.");
|
|
2590
|
+
if (!initialHistoryEligibility(segment, segment.fromTurn, config).eligible) throw new Error("This range is outside the saved initial-history selection.");
|
|
2591
|
+
const records = sourceRecords(harness, segment.sessionRef);
|
|
2592
|
+
let minimumTurn = Math.max(1, segment.fromTurn - 6);
|
|
2593
|
+
for (let turn = segment.fromTurn - 1; turn >= minimumTurn; turn -= 1) {
|
|
2594
|
+
if (!initialHistoryEligibility(segment, turn, config).eligible) { minimumTurn = turn + 1; break; }
|
|
2595
|
+
}
|
|
2596
|
+
const packet = require("./source-review.js").continuitySourcePacket({ harness, sessionRef: segment.sessionRef, fromTurn: segment.fromTurn, toTurn: segment.toTurn }, records, minimumTurn);
|
|
2597
|
+
let workspace;
|
|
2598
|
+
if (harness === "codex") workspace = records.find((record) => record.type === "session_meta")?.payload?.cwd;
|
|
2599
|
+
else if (harness === "claude_code") workspace = records.find((record) => typeof record.cwd === "string")?.cwd;
|
|
2600
|
+
else {
|
|
2601
|
+
try { workspace = opencodeQuery(`SELECT directory FROM session WHERE id = ${JSON.stringify(segment.sessionRef)}`)[0]?.directory; }
|
|
2602
|
+
catch { /* No trusted directory means a session-specific scope, never the scheduler workspace. */ }
|
|
2603
|
+
}
|
|
2604
|
+
return { packet, workspace: createHash("sha256").update(typeof workspace === "string" && workspace ? `workspace:${workspace}` : `${harness}:session:${segment.sessionRef}`).digest("hex") };
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2529
2607
|
async function prepareV5Payload(harness, segment, prepare = require("./native-preparation.js").prepareNative) {
|
|
2530
|
-
const
|
|
2608
|
+
const config = readConfig(harness);
|
|
2609
|
+
const store = continuityState(harness, config);
|
|
2610
|
+
const prior = store.load().attempts[`${segment.sessionRef}:${segment.fromTurn}`];
|
|
2611
|
+
if (prior?.payloadKey) {
|
|
2612
|
+
const frozen = store.load().outbox.find((entry) => entry.key === prior.payloadKey);
|
|
2613
|
+
if (!frozen) throw new Error("Frozen preparation is unavailable; continuity needs review.");
|
|
2614
|
+
return frozen.payload;
|
|
2615
|
+
}
|
|
2616
|
+
if (prior?.toTurn) segment = { ...segment, toTurn: prior.toTurn };
|
|
2617
|
+
const { packet, workspace } = preparationPacket(harness, segment, config);
|
|
2618
|
+
const dictionary = store.dictionary();
|
|
2619
|
+
dictionary.scopes[workspace] ||= { scopeRef: `s_${randomUUID().replaceAll("-", "")}`, entities: {} };
|
|
2620
|
+
store.saveDictionary(dictionary);
|
|
2621
|
+
const relevantBindings = () => {
|
|
2622
|
+
const content = [...packet.sources, ...packet.referenceSources].map((source) => source.content).join("\n").toLocaleLowerCase();
|
|
2623
|
+
return Object.values(dictionary.scopes[workspace].entities).filter((entry) => entry.createdBy !== `${packet.sessionRef}:${packet.fromTurn}` && content.includes(entry.mention.toLocaleLowerCase()))
|
|
2624
|
+
.map(({ key, mention, kind }) => ({ bindingRef: key, mention, kind }));
|
|
2625
|
+
};
|
|
2626
|
+
packet.entityBindings = relevantBindings();
|
|
2627
|
+
while (packet.referenceSources.length && Buffer.byteLength(JSON.stringify(packet)) + 1 > 256 * 1024) {
|
|
2628
|
+
const oldest = packet.referenceSources[0].turn;
|
|
2629
|
+
packet.referenceSources = packet.referenceSources.filter((source) => source.turn > oldest);
|
|
2630
|
+
packet.referenceCoverage.limited = true;
|
|
2631
|
+
packet.referenceCoverage.droppedTurns += 1;
|
|
2632
|
+
packet.referenceCoverage.fromTurn = packet.referenceSources[0]?.turn || packet.fromTurn;
|
|
2633
|
+
packet.entityBindings = relevantBindings();
|
|
2634
|
+
}
|
|
2635
|
+
if (Buffer.byteLength(JSON.stringify(packet)) + 1 > 256 * 1024) throw new Error("The selected source packet exceeds 128 messages or 256 KiB. Nothing was truncated or synced.");
|
|
2636
|
+
if (packet.entityBindings.length > 32) throw new Error("Relevant private entity bindings exceed the bounded packet; this range needs review.");
|
|
2637
|
+
const bindings = { workspace, scopeRef: dictionary.scopes[workspace].scopeRef, catalog: require("./continuity-preparation.js").CATALOG };
|
|
2531
2638
|
const deadline = Date.now() + 180_000;
|
|
2532
2639
|
let validationFeedback;
|
|
2533
2640
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
2534
2641
|
const remainingMs = deadline - Date.now();
|
|
2535
2642
|
if (remainingMs <= 0) throw new Error("Native preparation timed out; the source remains pending.");
|
|
2536
|
-
const
|
|
2643
|
+
const reservation = store.reservation(packet, bindings);
|
|
2644
|
+
let output;
|
|
2537
2645
|
try {
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2646
|
+
output = await prepare(harness, packet, { validationFeedback, timeoutMs: remainingMs, continuity: true });
|
|
2647
|
+
const candidateDictionary = structuredClone(dictionary);
|
|
2648
|
+
const result = require("./continuity-preparation.js").bindContinuity(packet, output, candidateDictionary, workspace);
|
|
2649
|
+
validateSync(result.payload, harness);
|
|
2650
|
+
if (result.assessment.disposition === "no_safe_context" && reservation.attempt < 2 && packet.sources.length) throw new Error("Recheck this proposed empty result against the original sources; preserve meaningful context or confirm a legitimate neutral empty result.");
|
|
2651
|
+
store.saveDictionary(candidateDictionary);
|
|
2652
|
+
return store.freeze(packet, result).payload;
|
|
2541
2653
|
} catch (error) {
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
// Failed model output is never queued, echoed, or sent to Shifu.
|
|
2654
|
+
store.failure(packet);
|
|
2655
|
+
if (reservation.attempt >= 2 || output === undefined) throw error;
|
|
2545
2656
|
validationFeedback = preparationValidationFeedback(error, output);
|
|
2546
2657
|
}
|
|
2547
2658
|
}
|
|
2548
2659
|
}
|
|
2549
2660
|
|
|
2661
|
+
async function retryPreparation(harness) {
|
|
2662
|
+
const sessionRef = option("--session-ref");
|
|
2663
|
+
const fromTurn = numberOption("--from-turn");
|
|
2664
|
+
if (!sessionRefIsSafe(sessionRef) || !Number.isSafeInteger(fromTurn) || fromTurn < 1) throw new Error("Choose the held session and original from-turn explicitly.");
|
|
2665
|
+
const result = await withSyncLock(harness, async () => {
|
|
2666
|
+
const store = continuityState(harness);
|
|
2667
|
+
const state = store.load();
|
|
2668
|
+
const identity = `${sessionRef}:${fromTurn}`;
|
|
2669
|
+
const held = state.attempts[identity];
|
|
2670
|
+
if (!held || held.payloadKey || state.outbox.some((entry) => entry.sessionRef === sessionRef && entry.fromTurn === fromTurn) || readPendingQueue(harness).segments.some((input) => input.sessionRef === sessionRef && input.toTurn >= fromTurn)) {
|
|
2671
|
+
throw new Error("Only an unqueued preparation hold can be retried; frozen payloads keep their exact transport path.");
|
|
2672
|
+
}
|
|
2673
|
+
if ((readState(harness).sessions[sessionRef]?.lastSyncedTurn || 0) >= fromTurn) throw new Error("Accepted ranges require separately authorized historical recovery.");
|
|
2674
|
+
const session = localSessions(harness).find((item) => item.sessionRef === sessionRef);
|
|
2675
|
+
if (!session) throw new Error("The original session is unavailable; the hold was preserved.");
|
|
2676
|
+
held.attempts = 0; held.status = "unresolved"; delete held.reason;
|
|
2677
|
+
store.save(state);
|
|
2678
|
+
const payload = await prepareV5Payload(harness, { ...session, fromTurn, toTurn: held.toTurn });
|
|
2679
|
+
queuePayload(harness, payload);
|
|
2680
|
+
console.error("The selected preparation is queued for its existing approval and transport policy.");
|
|
2681
|
+
});
|
|
2682
|
+
if (result?.skipped) throw new Error("Another sync is running; the hold was preserved.");
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2550
2685
|
function preparationValidationFeedback(error, output) {
|
|
2551
2686
|
const fields = [["title", output?.title, 160], ["summary", output?.summary, 1100]];
|
|
2552
2687
|
for (const name of ["decisions", "verification"]) {
|
|
@@ -2589,6 +2724,7 @@ function preparationState(harness, queue) {
|
|
|
2589
2724
|
}
|
|
2590
2725
|
|
|
2591
2726
|
async function preparePendingQueue(harness, config, blockedSessions = new Set()) {
|
|
2727
|
+
restoreContinuityQueue(harness, config);
|
|
2592
2728
|
let queue = prunePendingQueue(harness);
|
|
2593
2729
|
const previousFailures = new Set((queue.preparationErrors || []).map((error) => error.sessionRef));
|
|
2594
2730
|
const errors = [];
|
|
@@ -2638,8 +2774,13 @@ async function preparePendingQueue(harness, config, blockedSessions = new Set())
|
|
|
2638
2774
|
async function drainPendingQueue(harness, config, blockedSessions) {
|
|
2639
2775
|
let synced = 0;
|
|
2640
2776
|
let failures = 0;
|
|
2641
|
-
|
|
2642
|
-
|
|
2777
|
+
const store = continuityState(harness, config);
|
|
2778
|
+
const entries = store.load().outbox.filter((entry) => entry.baseStatus !== "needs_review" && (entry.baseStatus !== "accepted" || entry.toTurn > (readState(harness).sessions[entry.sessionRef]?.lastSyncedTurn || 0)));
|
|
2779
|
+
const queued = eligibleQueuedSegments(readPendingQueue(harness), config);
|
|
2780
|
+
const excluded = new Set(normalizedPolicy(config).excludedSessionRefs);
|
|
2781
|
+
const inputs = [...queued, ...entries.filter((entry) => !excluded.has(entry.sessionRef) && !queued.some((input) => JSON.stringify(input) === JSON.stringify(entry.payload))).map((entry) => entry.payload)];
|
|
2782
|
+
for (const input of inputs) {
|
|
2783
|
+
if (blockedSessions.has(input.sessionRef) || store.find(input)?.baseStatus === "needs_review") continue;
|
|
2643
2784
|
try { await sendSync(harness, input, config); synced += 1; }
|
|
2644
2785
|
catch (error) {
|
|
2645
2786
|
failures += 1;
|
|
@@ -2650,11 +2791,35 @@ async function drainPendingQueue(harness, config, blockedSessions) {
|
|
|
2650
2791
|
return { synced, failures };
|
|
2651
2792
|
}
|
|
2652
2793
|
|
|
2794
|
+
async function flushContinuityAttachments(harness, config) {
|
|
2795
|
+
const store = continuityState(harness, config);
|
|
2796
|
+
if (!store.load().outbox.some((entry) => entry.baseStatus === "accepted" && !["attached", "not_applicable", "needs_review"].includes(entry.continuityStatus))) return;
|
|
2797
|
+
let capable = false;
|
|
2798
|
+
try {
|
|
2799
|
+
const connection = await validateConnection(config.apiUrl, config.token, harness);
|
|
2800
|
+
capable = connection.capabilities?.continuityAttachment === 1;
|
|
2801
|
+
} catch { /* Base receipts remain valid while capability discovery is unavailable. */ }
|
|
2802
|
+
await require("./continuity-state.js").flushContinuity(store, config, capable);
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
function restoreContinuityQueue(harness, config) {
|
|
2806
|
+
const queue = readPendingQueue(harness);
|
|
2807
|
+
const state = readState(harness);
|
|
2808
|
+
for (const entry of continuityState(harness, config).load().outbox) {
|
|
2809
|
+
if (entry.toTurn > (state.sessions[entry.sessionRef]?.lastSyncedTurn || 0) && entry.baseStatus !== "needs_review" && !queue.segments.some((payload) => JSON.stringify(payload) === JSON.stringify(entry.payload))) queuePayload(harness, entry.payload);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2653
2813
|
function eligibleQueuedSegments(queue, config) {
|
|
2654
2814
|
const excluded = new Set(normalizedPolicy(config).excludedSessionRefs);
|
|
2655
2815
|
return queue.segments.filter((input) => !excluded.has(input.sessionRef));
|
|
2656
2816
|
}
|
|
2657
2817
|
|
|
2818
|
+
function continuityRecoveries(harness, config, queued) {
|
|
2819
|
+
const excluded = new Set(normalizedPolicy(config).excludedSessionRefs);
|
|
2820
|
+
return continuityState(harness, config).load().outbox.filter((entry) => entry.baseStatus === "awaiting_receipt" && !excluded.has(entry.sessionRef) && !queued.some((payload) => JSON.stringify(payload) === JSON.stringify(entry.payload))).map((entry) => entry.payload);
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2658
2823
|
function approvalNotificationInvocation(harness, count, sessionRef) {
|
|
2659
2824
|
const prompt = `Shifu sync approval: ${count} redacted segment${count === 1 ? " is" : "s are"} waiting locally. Tell the user to ask you to show the pending Shifu sync and approve it once. Do not inspect files, run tools, or sync anything in this session.`;
|
|
2660
2825
|
if (harness === "codex") {
|
|
@@ -2706,7 +2871,9 @@ function notifyManualApproval(harness, count, existingSessionRef) {
|
|
|
2706
2871
|
async function showPending(harness) {
|
|
2707
2872
|
const config = readConfig(harness);
|
|
2708
2873
|
const result = await withSyncLock(harness, async () => {
|
|
2709
|
-
|
|
2874
|
+
const queue = await preparePendingQueue(harness, config);
|
|
2875
|
+
queue.segments = [...eligibleQueuedSegments(queue, config), ...continuityRecoveries(harness, config, queue.segments)];
|
|
2876
|
+
console.log(JSON.stringify(queue, null, 2));
|
|
2710
2877
|
});
|
|
2711
2878
|
if (result?.skipped) throw new Error("Another Shifu sync is already preparing this queue. Retry later.");
|
|
2712
2879
|
}
|
|
@@ -2714,18 +2881,21 @@ async function showPending(harness) {
|
|
|
2714
2881
|
async function approvePending(harness) {
|
|
2715
2882
|
const config = readConfig(harness);
|
|
2716
2883
|
const result = await withSyncLock(harness, async () => {
|
|
2884
|
+
restoreContinuityQueue(harness, config);
|
|
2717
2885
|
const queue = prunePendingQueue(harness);
|
|
2718
2886
|
queue.segments = eligibleQueuedSegments(queue, config);
|
|
2719
|
-
|
|
2887
|
+
const recoveries = continuityRecoveries(harness, config, queue.segments);
|
|
2888
|
+
if (queue.segments.length === 0 && recoveries.length === 0) {
|
|
2720
2889
|
console.error("There is no prepared Shifu activity to approve. Run pending to prepare the next ranges.");
|
|
2721
2890
|
return;
|
|
2722
2891
|
}
|
|
2723
|
-
console.error(`\nThese ${queue.segments.length} redacted segment(s) will be sent to Shifu:\n${JSON.stringify(queue.segments, null, 2)}`);
|
|
2892
|
+
console.error(`\nThese ${queue.segments.length + recoveries.length} redacted segment(s) will be sent to Shifu:\n${JSON.stringify([...queue.segments, ...recoveries], null, 2)}`);
|
|
2724
2893
|
if (!hasFlag("--approved") && !(await confirm("Approve all pending segments"))) {
|
|
2725
2894
|
console.error("Nothing was sent. The prepared queue is unchanged.");
|
|
2726
2895
|
return;
|
|
2727
2896
|
}
|
|
2728
2897
|
const sent = await drainPendingQueue(harness, config, new Set());
|
|
2898
|
+
await flushContinuityAttachments(harness, config);
|
|
2729
2899
|
if (sent.failures) throw new Error("Some approved segments could not be acknowledged. Their exact payloads remain pending for retry.");
|
|
2730
2900
|
});
|
|
2731
2901
|
if (result?.skipped) throw new Error("Another Shifu sync is already running. Nothing was sent; retry later.");
|
|
@@ -2758,9 +2928,12 @@ async function scheduledSync(harness) {
|
|
|
2758
2928
|
}
|
|
2759
2929
|
}
|
|
2760
2930
|
await flushPendingProfiles(harness, config);
|
|
2931
|
+
restoreContinuityQueue(harness, config);
|
|
2932
|
+
await flushContinuityAttachments(harness, config);
|
|
2761
2933
|
if (policy.approvalMode === "manual") {
|
|
2762
2934
|
const queue = await preparePendingQueue(harness, config);
|
|
2763
|
-
const
|
|
2935
|
+
const queued = eligibleQueuedSegments(queue, config);
|
|
2936
|
+
const eligible = [...queued, ...continuityRecoveries(harness, config, queued)];
|
|
2764
2937
|
const pendingFingerprint = createHash("sha256").update(eligible.map((segment) => `${segment.sessionRef}:${segment.fromTurn}:${segment.toTurn}`).join("\n")).digest("hex");
|
|
2765
2938
|
const changed = state.schedule?.pendingFingerprint !== pendingFingerprint;
|
|
2766
2939
|
let approvalSessionRef = state.schedule?.approvalSessionRef;
|
|
@@ -2818,6 +2991,7 @@ async function scheduledSync(harness) {
|
|
|
2818
2991
|
nextRunAt: Date.now() + (failures || failedFiles || pendingProfiles || remaining ? SCHEDULER_TICK_MINUTES / 60 : policy.intervalHours) * 3_600_000,
|
|
2819
2992
|
};
|
|
2820
2993
|
}
|
|
2994
|
+
await flushContinuityAttachments(harness, config);
|
|
2821
2995
|
const latest = readState(harness);
|
|
2822
2996
|
latest.schedule = state.schedule;
|
|
2823
2997
|
saveState(harness, latest);
|
|
@@ -2848,7 +3022,7 @@ function status(harness) {
|
|
|
2848
3022
|
if (!eligibility.eligible) initialHistoryBlocked.push({ sessionRef: session.sessionRef, reason: eligibility.reason, cutoffAt: eligibility.cutoffAt });
|
|
2849
3023
|
}
|
|
2850
3024
|
}
|
|
2851
|
-
console.log(JSON.stringify({ harness, runnerVersion: RUNNER_VERSION, apiUrl: config.apiUrl, inputFolder: folder, inputFolderError, contextPreferences: config.contextPreferences === undefined ? null : normalizeContextPreferences(config.contextPreferences), historicalContext: true, initialBackfill: config.initialBackfill || null, initialHistoryBlocked, syncPolicy: normalizedPolicy(config), schedule: state.schedule || null, pendingSegments: pending.segments.length, completedFiles: entries.filter((entry) => typeof entry === "string").length, pendingFiles: entries.filter((entry) => typeof entry === "object" && entry?.jobId).length, unsupportedFiles, sessions: state.sessions }, null, 2));
|
|
3025
|
+
console.log(JSON.stringify({ continuity: continuityState(harness, config).summary(), harness, runnerVersion: RUNNER_VERSION, apiUrl: config.apiUrl, inputFolder: folder, inputFolderError, contextPreferences: config.contextPreferences === undefined ? null : normalizeContextPreferences(config.contextPreferences), historicalContext: true, initialBackfill: config.initialBackfill || null, initialHistoryBlocked, syncPolicy: normalizedPolicy(config), schedule: state.schedule || null, pendingSegments: pending.segments.length, completedFiles: entries.filter((entry) => typeof entry === "string").length, pendingFiles: entries.filter((entry) => typeof entry === "object" && entry?.jobId).length, unsupportedFiles, sessions: state.sessions }, null, 2));
|
|
2852
3026
|
}
|
|
2853
3027
|
|
|
2854
3028
|
async function syncFilesNow(harness) {
|
|
@@ -3088,21 +3262,21 @@ async function main() {
|
|
|
3088
3262
|
if (command === "sync-files") return syncFilesNow(harness);
|
|
3089
3263
|
if (command === "scheduled-sync") return scheduledSync(harness);
|
|
3090
3264
|
if (command === "pending") return showPending(harness);
|
|
3265
|
+
if (command === "retry-preparation") return retryPreparation(harness);
|
|
3091
3266
|
if (command === "approve") return approvePending(harness);
|
|
3092
3267
|
if (command === "status") return status(harness);
|
|
3093
3268
|
if (command === "sessions" || command === "discover" || (command === "session" && (subCommand === "discover" || subCommand === "list" || !subCommand || subCommand.startsWith("-")))) return sessions(harness);
|
|
3094
3269
|
if (command === "review") return review(harness);
|
|
3095
3270
|
if (command === "review-source") return console.log(JSON.stringify(reviewSourcePacket(harness, option("--session-ref"), numberOption("--from-turn"), numberOption("--to-turn")), null, 2));
|
|
3096
|
-
throw new Error("Use install, connect, mcp-setup, mcp-headers, disconnect, config update, status, sessions, review, review-source, sync, sync-files, pending, approve, or scheduled-sync.");
|
|
3271
|
+
throw new Error("Use install, connect, mcp-setup, mcp-headers, disconnect, config update, status, sessions, review, review-source, sync, sync-files, pending, retry-preparation, approve, or scheduled-sync.");
|
|
3097
3272
|
}
|
|
3098
3273
|
|
|
3099
3274
|
if (require.main === module) {
|
|
3100
3275
|
main()
|
|
3101
|
-
.then(() => process.exit(0))
|
|
3102
3276
|
.catch((error) => {
|
|
3103
3277
|
console.error(error.message);
|
|
3104
|
-
process.
|
|
3278
|
+
process.exitCode = 1;
|
|
3105
3279
|
});
|
|
3106
3280
|
}
|
|
3107
3281
|
|
|
3108
|
-
module.exports = { setupMcp, olderPackageVersion, updateRecommendedPackage, installPackageVersion, approvalNotificationInvocation, claudeReview, claudeSession, codexReview, codexSession, configPath, configuredSyncPolicy, connect, defaultInputFolder, disconnect, finalAssistantNote, install, installDestination, localSessions, normalizedPolicy, notificationSessionRef, opencodeQuery, opencodeReview, opencodeSessions, payloadForSegment, pendingSegments, prepareV5Payload, prepareInputFolder, profileForSegment, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, reviewSourcePacket, schedulerArtifact, statePath, syncWorkFiles, textIsSafe, unsyncedCodexSessions, validateAIProfileObservation, validateConnection, validateInputFolder, validSyncReceipt, validateApprovalMode, validateIntervalHours, validateSync, workFiles };
|
|
3282
|
+
module.exports = { setupMcp, olderPackageVersion, updateRecommendedPackage, installPackageVersion, approvalNotificationInvocation, claudeReview, claudeSession, codexReview, codexSession, configPath, configuredSyncPolicy, connect, defaultInputFolder, disconnect, finalAssistantNote, install, installDestination, localSessions, normalizedPolicy, notificationSessionRef, opencodeQuery, opencodeReview, opencodeSessions, payloadForSegment, pendingSegments, prepareV5Payload, preparationPacket, continuityState, flushContinuityAttachments, prepareInputFolder, profileForSegment, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, reviewSourcePacket, schedulerArtifact, statePath, syncWorkFiles, textIsSafe, unsyncedCodexSessions, validateAIProfileObservation, validateConnection, validateInputFolder, validSyncReceipt, validateApprovalMode, validateIntervalHours, validateSync, workFiles };
|
package/bin/source-review.js
CHANGED
|
@@ -191,10 +191,36 @@ function completedSourceTurn(selection, records) {
|
|
|
191
191
|
if (message.role === "user") {
|
|
192
192
|
turnCount += 1;
|
|
193
193
|
closed = false;
|
|
194
|
-
} else if (message.role === "assistant") closed = message.finish === "stop" && message.error == null;
|
|
194
|
+
} else if (message.role === "assistant") closed = message.storageVersion !== 2 && message.finish === "stop" && message.error == null;
|
|
195
|
+
else if (message.storageVersion === 2 && message.role === "idle") closed = true;
|
|
195
196
|
}
|
|
196
197
|
} else throw new Error("Unsupported original-source harness.");
|
|
197
198
|
return Math.max(0, turnCount - (closed ? 0 : 1));
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
|
|
201
|
+
function continuitySourcePacket(selection, records, minimumTurn = 1) {
|
|
202
|
+
const packet = scheduledSourcePacket(selection, records);
|
|
203
|
+
let first = Math.max(1, selection.fromTurn - 6, minimumTurn);
|
|
204
|
+
const adapters = { codex: codexSources, claude_code: claudeSources, opencode: openCodeSources };
|
|
205
|
+
const references = first < selection.fromTurn
|
|
206
|
+
? adapters[selection.harness]({ ...selection, fromTurn: first, toTurn: selection.fromTurn - 1 }, records, true).sources : [];
|
|
207
|
+
const originalFirst = first;
|
|
208
|
+
let referenceSources = references;
|
|
209
|
+
function oversized() {
|
|
210
|
+
return Buffer.byteLength(JSON.stringify(referenceSources)) > 64 * 1024 ||
|
|
211
|
+
packet.sources.length + referenceSources.length > MAX_SOURCE_MESSAGES ||
|
|
212
|
+
Buffer.byteLength(JSON.stringify({ ...packet, referenceSources })) + 512 > MAX_PACKET_BYTES;
|
|
213
|
+
}
|
|
214
|
+
while (referenceSources.length && oversized()) {
|
|
215
|
+
first = referenceSources[0].turn + 1;
|
|
216
|
+
referenceSources = references.filter((source) => source.turn >= first);
|
|
217
|
+
}
|
|
218
|
+
return { ...packet, referenceSources, referenceCoverage: {
|
|
219
|
+
fromTurn: referenceSources.length ? referenceSources[0].turn : selection.fromTurn,
|
|
220
|
+
toTurn: selection.fromTurn - 1,
|
|
221
|
+
limited: first > Math.max(1, selection.fromTurn - 6) || minimumTurn > Math.max(1, selection.fromTurn - 6),
|
|
222
|
+
droppedTurns: Math.max(0, first - originalFirst),
|
|
223
|
+
} };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { selectedSourcePacket, scheduledSourcePacket, continuitySourcePacket, completedSourceTurn };
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ Before first connection or MCP personalization, ask for any choices that are not
|
|
|
12
12
|
When asked to connect Shifu context, run:
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
|
-
npx --yes @useshifu/coding-harness@0.3.
|
|
15
|
+
npx --yes @useshifu/coding-harness@0.3.30 mcp-setup --harness codex
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
MCP setup verifies the connection and tools, saves the choices and installs instructions; it does not sync sessions or upload the selected history. History selection governs eligible work for subsequent sync. Do not start sync as part of this setup unless the user separately asks for it. If a saved connection exists, reuse its key and do not run `connect` again.
|
|
@@ -117,7 +117,7 @@ For an explicit sync request, first read `status` to determine the saved approva
|
|
|
117
117
|
|
|
118
118
|
Use `kind: "activity"` for general harness work; it does not require a technical scope. Evidence may be empty when the reviewed source contains no durable context. Never invent impact, ownership, verification, or outcomes. A requested check, assistant proposal, or reported result must retain that status even when it is useful context.
|
|
119
119
|
|
|
120
|
-
Make each supported item useful for later recall: retain the described action, feature or project purpose, specific problem, approach, and reported verification or outcome with its status and ownership intact. Preserve supported technologies, design patterns, configuration choices, and measured results; these are useful work details, not identifiers to redact. A descriptive project label such as "payment reconciliation service" is useful; synthesize it from the work's purpose instead of copying a raw project label that might contain a company or person's name. Preserve supported decisions and their rationale, accepted and rejected tradeoffs, lessons, and observed outcomes. Keep proposals explicitly labeled as proposals; do not turn an agent suggestion into a user-adopted decision or infer personal ownership. Use statements, per-item work context, linked decisions and verification, and permitted source metadata. Keep distinct facts separate, including two fixes in one feature that addressed different problems.
|
|
120
|
+
Make each supported item useful for later recall: retain the described action, feature or project purpose, specific problem, approach, and reported verification or outcome with its status and ownership intact. Preserve supported technologies, design patterns, configuration choices, and measured results; these are useful work details, not identifiers to redact. A descriptive project label such as "payment reconciliation service" is useful; synthesize it from the work's purpose instead of copying a raw project label that might contain a company or person's name. Preserve supported decisions and their rationale, accepted and rejected tradeoffs, lessons, and observed outcomes. Keep proposals explicitly labeled as proposals; do not turn an agent suggestion into a user-adopted decision or infer personal ownership. Use statements, per-item work context, linked decisions and verification, and permitted source metadata. Keep distinct facts separate, including two fixes in one feature that addressed different problems. Preserve the code-permitted public technology labels Turbopuffer, PostgreSQL, Go, TypeScript, React and Node.js when grounded and relevant. This does not authorize confidential customer or employer relationships. Omit other company and person names, customer details, proprietary identifiers, paths, code snippets, credentials, and unsupported impact. If a detail cannot be safely described, keep the supported parts. Prefer a summary that names the main distinct work items over a generic count alone.
|
|
121
121
|
|
|
122
122
|
```json
|
|
123
123
|
{
|
|
@@ -157,3 +157,9 @@ Only when the user explicitly asks to force-sync, add `--force` to `sessions` wi
|
|
|
157
157
|
Re-review each returned range with `review --harness codex --session-ref <sessionRef> --force --from-turn <fromTurn> --to-turn <toTurn>` and the same window (or `--all` for explicitly selected backfill/count selection). No prior sync ID is required. Read the returned final notes semantically and synthesize meaningful, attributable work evidence; do not use `payloadForSegment` or its unattended keyword projection for force sync. Completed work remains supported when the note uses Markdown such as `**Implemented**` or names a technique in inline code such as `Rust`. Retain supported technologies, design patterns, the problem, approach, and verified result while redacting names and sensitive identifiers. Never send an empty evidence list or a title-only replacement. If the notes support no work items, report that range as not replaced and leave its existing work and checkpoints untouched. Keep the returned reference and range unchanged and include `force: true` in the prepared payload. Send with `sync --harness codex --force` (plus `--approved` only after manual approval). If accepted ranges changed and partially overlap the selection, stop and re-discover/re-review the complete ranges instead of guessing boundaries.
|
|
158
158
|
|
|
159
159
|
Force sync creates missing ranges or replaces existing exact ranges, preserves sync history, and never resets or lowers checkpoints. Automatic retries preserve the accepted result even if another force sync finishes meanwhile. A new invocation is a new explicit force request. Existing AI Profile observations stay unchanged. Do not add force to scheduled sync, approval queues, or file sync. Existing conversations keep their context; start a new chat in Shifu to use the latest work context.
|
|
160
|
+
|
|
161
|
+
## Continuity preparation and review holds
|
|
162
|
+
|
|
163
|
+
Scheduled preparation and pending review use bounded original lookback, code-bound entity placeholders and a separate local continuity outbox. Never add continuity fields to a v5 payload or rewrite a frozen queued body. `status` separates preparation, base receipt and companion attachment. A base receipt does not establish successful continuity attachment. Missing server capability leaves the frozen metadata pending. Do not guess a target sync from turn ranges.
|
|
164
|
+
|
|
165
|
+
A preparation hold survives restarts and stops after two native attempts for unchanged input. Inspect its code-owned reason. Genuine neutral empties stay valid; never create filler evidence to clear a hold. A specific authorized retry of an unqueued held range can use `retry-preparation --harness codex --session-ref <opaque-session> --from-turn <original-start>`; this cannot replay accepted history or regenerate frozen transport. Replacing the key or API endpoint quarantines old checkpoints, queues, local entity dictionaries and continuity metadata. An interrupted replacement holds sync until connect resumes with the same selected connection. Do not upload, display or copy the raw-name dictionary into chat.
|