@useshifu/coding-harness 0.3.2 → 0.3.4

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 CHANGED
@@ -14,7 +14,7 @@ Connect installs the local instruction and scheduled runner, saves the connectio
14
14
  - automatic coding-session sync (manual approval remains available for coding sessions);
15
15
  - one input folder created at `~/Desktop/Shifu` on macOS, with a `README.md` explaining what it is for and shortcuts at `~/Downloads/Shifu` and `~/Documents/Shifu` when those parent folders exist.
16
16
 
17
- Place `.txt`, `.md`, `.rtf`, or `.docx` files directly in the input folder. Its `README.md` is never synced and an existing one is not overwritten. Screenshots and images are not processed. Existing files are picked up on the next scheduled run; unchanged files are not sent again. A failed file stays pending for retry. Folder files never require a claim-review step, even when coding-session sync is in manual mode. Shifu creates claims only when the file supports a specific contribution by the connected user; meeting action items or another person's work are not treated as the user's claims.
17
+ Place `.txt`, `.md`, `.rtf`, or `.docx` files up to 20 MB directly in the input folder. Its `README.md` is never synced and an existing one is not overwritten. Screenshots, images, links, subfolders, and unknown formats are not uploaded; unsupported regular files are counted in sync status. Existing files are picked up on the next scheduled run; unchanged files are not sent again. A failed file stays pending for retry. Folder files never require a claim-review step, even when coding-session sync is in manual mode. Shifu creates claims only when the file supports a specific contribution by the connected user; meeting action items or another person's work are not treated as the user's claims.
18
18
 
19
19
  For named meeting transcripts, set your display name in Shifu Profile so your speaker turns can be identified. If the speaker cannot be matched, uncertain contributions are skipped.
20
20
 
@@ -41,7 +41,9 @@ npx @useshifu/coding-harness sync-files --harness codex
41
41
 
42
42
  Automatic scheduled runs send each accepted segment and advance its local checkpoint only after the server returns a matching receipt. Manual scheduled runs rebuild an accumulated local queue without advancing checkpoints and create or resume one content-free harness session when the pending count changes; one approval sends the queue in order. Network failures and server throttling are retried, while rejected segments stay pending.
43
43
 
44
- The same scheduled run checks the input folder. `sync-files` checks it immediately without changing the schedule. It sends supported file bytes over the configured HTTPS connection (or localhost HTTP for development); the server keeps the raw bytes transient and returns a final processing receipt before the local file checkpoint advances. Files larger than 5 MB are left pending with an error. The scanner reads regular files directly in the chosen folder and ignores symlinks and subfolders.
44
+ The same scheduled run checks the input folder. `sync-files` checks it immediately without changing the schedule. Before sending new or changed files, the runner polls unfinished jobs from its local checkpoints. It sends supported file bytes over the configured HTTPS connection (or localhost HTTP for development). The server accepts each file as a background job and processes its text in chunks; upload does not wait for claim extraction. A later scheduled or manual sync polls the job's result. Only `processed`, `no_claims`, or `duplicate` finalizes the local checkpoint; `partial` reopens failed chunks on the next sync. On this upgrade, completed file checkpoints are replayed once so the new extraction can retain suggestions; in-flight jobs are polled first and replayed after finishing. Server-side idempotency prevents already published claims from being duplicated. Files larger than 20 MB remain pending with an error. The scanner reads regular files directly in the chosen folder and ignores symlinks and subfolders. Unknown formats are reported as unsupported rather than treated as successful syncs.
45
+
46
+ The file-job result reports published claims separately from needs-evidence suggestions. A `no_claims` result can still contain suggestions: these are possible contributions awaiting evidence or attribution, not verified claims. The runner reports the suggestion count but never labels those suggestions as established work. It does not print the file's text, candidate statements, or source excerpts to logs. Use Shifu to inspect the suggestions and their evidence status. A result with zero claims and zero suggestions means no user-attributable contribution was extracted; it does not mean the upload failed.
45
47
 
46
48
  The connector captures any useful harness activity, including engineering, product discovery, writing, interview feedback, research, and planning. Version-four payloads support generic `activity` items without a technical scope and accept a safe session-level summary when no item survives redaction. Version 1–3 clients remain supported.
47
49
 
@@ -41,15 +41,19 @@ const MIN_INTERVAL_HOURS = 0.25;
41
41
  const MAX_INTERVAL_HOURS = 720;
42
42
  const SCHEDULER_TICK_MINUTES = 15;
43
43
  const MAX_RETRIES = 4;
44
- const MAX_WORK_FILE_BYTES = 5 * 1024 * 1024;
45
- const WORK_FILE_SYNC_VERSION = 2;
44
+ const MAX_WORK_FILE_BYTES = 20 * 1024 * 1024;
45
+ const WORK_FILE_SYNC_VERSION = 4;
46
46
  const WORK_FILE_EXTENSIONS = new Set([".txt", ".md", ".rtf", ".docx"]);
47
+ const FINAL_WORK_FILE_STATUSES = new Set(["processed", "no_claims", "duplicate"]);
48
+ const ACTIVE_WORK_FILE_STATUSES = new Set(["queued", "processing", "retrying"]);
47
49
  const INPUT_FOLDER_README_NAME = "README.md";
48
50
  const INPUT_FOLDER_README = `# Shifu sync folder
49
51
 
50
52
  Put work logs, meeting transcripts, and other work documents directly in this folder. Shifu checks it every hour by default and keeps claims only when the file supports a contribution you made. Other people's work and meeting action items alone are not your claims.
51
53
 
52
- Supported files: .txt, .md, .rtf, and .docx. Subfolders, links, images, and screenshots are not processed. This README is not synced.
54
+ Supported files: .txt, .md, .rtf, and .docx, up to 20 MB each. Subfolders, links, images, and screenshots are not processed. This README is not synced. Unsupported files are counted but never uploaded.
55
+
56
+ Uploading starts a background job. Shifu checks unfinished jobs before the next scheduled or manual sync; a file is complete only when the server reports its final result. A partial result stays pending for retry. A completed file may have published claims, needs-evidence suggestions, or neither; suggestions are not verified claims.
53
57
 
54
58
  Files you add are sent to Shifu for parsing and sanitization; they are not redacted on this device. Only place files here if you want them processed. For named meeting transcripts, set your display name in Shifu Profile so your speaker turns can be matched.
55
59
  `;
@@ -990,18 +994,25 @@ async function postSync(config, input) {
990
994
  throw lastError;
991
995
  }
992
996
 
997
+ function scanWorkFiles(folder) {
998
+ const entries = fs.readdirSync(validateInputFolder(folder), { withFileTypes: true })
999
+ .filter((entry) => entry.isFile() && entry.name !== INPUT_FOLDER_README_NAME);
1000
+ return {
1001
+ files: entries.filter((entry) => WORK_FILE_EXTENSIONS.has(path.extname(entry.name).toLowerCase()))
1002
+ .map((entry) => path.join(folder, entry.name)).sort(),
1003
+ unsupported: entries.filter((entry) => !WORK_FILE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())).length,
1004
+ };
1005
+ }
1006
+
993
1007
  function workFiles(folder) {
994
- return fs.readdirSync(validateInputFolder(folder), { withFileTypes: true })
995
- .filter((entry) => entry.isFile() && entry.name !== INPUT_FOLDER_README_NAME && WORK_FILE_EXTENSIONS.has(path.extname(entry.name).toLowerCase()))
996
- .map((entry) => path.join(folder, entry.name))
997
- .sort();
1008
+ return scanWorkFiles(folder).files;
998
1009
  }
999
1010
 
1000
1011
  function readWorkFile(file) {
1001
1012
  const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
1002
1013
  try {
1003
1014
  const before = fs.fstatSync(descriptor);
1004
- if (!before.isFile() || before.size > MAX_WORK_FILE_BYTES) throw new Error("Only regular files up to 5 MB are supported.");
1015
+ if (!before.isFile() || before.size > MAX_WORK_FILE_BYTES) throw new Error("Only regular files up to 20 MB are supported.");
1005
1016
  const bytes = fs.readFileSync(descriptor);
1006
1017
  const after = fs.fstatSync(descriptor);
1007
1018
  if (bytes.length > MAX_WORK_FILE_BYTES || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
@@ -1021,11 +1032,11 @@ async function postWorkFile(config, file, contents) {
1021
1032
  const form = new FormData();
1022
1033
  form.append("file", new Blob([contents.bytes]), path.basename(file));
1023
1034
  form.append("sourceModifiedAt", contents.modifiedAt);
1024
- response = await fetch(`${config.apiUrl}/v1/connectors/work-files`, {
1035
+ response = await fetch(`${config.apiUrl}/v1/connectors/work-files/jobs`, {
1025
1036
  method: "POST",
1026
1037
  headers: { Authorization: `Bearer ${config.token}` },
1027
1038
  body: form,
1028
- signal: AbortSignal.timeout(15 * 60_000),
1039
+ signal: AbortSignal.timeout(2 * 60_000),
1029
1040
  });
1030
1041
  } catch (error) {
1031
1042
  lastError = new Error(`Could not send ${path.basename(file)} to Shifu: ${error?.cause?.code || error?.message || "unknown transport error"}.`);
@@ -1035,7 +1046,7 @@ async function postWorkFile(config, file, contents) {
1035
1046
  const payload = await response.json().catch(() => undefined);
1036
1047
  if (response.ok) {
1037
1048
  const receipt = payload?.data;
1038
- if (["processed", "no_claims", "duplicate"].includes(receipt?.status) && Number.isInteger(receipt?.claimsCreated) && receipt.claimsCreated >= 0) return receipt;
1049
+ if (validWorkFileJob(receipt)) return receipt;
1039
1050
  throw new Error(`Shifu returned an invalid receipt for ${path.basename(file)}. Local state was not advanced.`);
1040
1051
  }
1041
1052
  lastError = new Error(payload?.error?.message || `Shifu rejected ${path.basename(file)} (${response.status}).`);
@@ -1045,32 +1056,119 @@ async function postWorkFile(config, file, contents) {
1045
1056
  throw lastError;
1046
1057
  }
1047
1058
 
1059
+ function validWorkFileJob(receipt) {
1060
+ return typeof receipt?.id === "string" && receipt.id.length > 0 &&
1061
+ (ACTIVE_WORK_FILE_STATUSES.has(receipt.status) || FINAL_WORK_FILE_STATUSES.has(receipt.status) || receipt.status === "partial") &&
1062
+ Number.isInteger(receipt.claimsCreated) && receipt.claimsCreated >= 0 &&
1063
+ (receipt.candidateCount === undefined || (Number.isInteger(receipt.candidateCount) && receipt.candidateCount >= 0));
1064
+ }
1065
+
1066
+ function workFileResult(job) {
1067
+ const claims = `${job.claimsCreated} claim${job.claimsCreated === 1 ? "" : "s"}`;
1068
+ const candidates = job.candidateCount > 0 ? `, ${job.candidateCount} needs-evidence suggestion${job.candidateCount === 1 ? "" : "s"}` : "";
1069
+ return `${job.status} (${claims}${candidates})`;
1070
+ }
1071
+
1072
+ function workFileEntries(state, folder) {
1073
+ if (state.workFiles?.folder !== folder) return {};
1074
+ const files = state.workFiles.files || {};
1075
+ if (state.workFiles.version === WORK_FILE_SYNC_VERSION) return files;
1076
+ // Replay old completed checkpoints once, but never abandon an accepted job.
1077
+ return Object.fromEntries(Object.entries(files)
1078
+ .filter(([, entry]) => entry && typeof entry === "object" && entry.jobId)
1079
+ .map(([name, entry]) => [name, { ...entry, replayOnCompletion: true }]));
1080
+ }
1081
+
1082
+ async function getWorkFileJob(config, jobId) {
1083
+ let lastError;
1084
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt += 1) {
1085
+ let response;
1086
+ try {
1087
+ response = await fetch(`${config.apiUrl}/v1/connectors/work-files/jobs/${encodeURIComponent(jobId)}`, {
1088
+ headers: { Authorization: `Bearer ${config.token}` },
1089
+ signal: AbortSignal.timeout(30_000),
1090
+ });
1091
+ } catch (error) {
1092
+ lastError = new Error(`Could not check a Shifu file job: ${error?.cause?.code || error?.message || "unknown transport error"}.`);
1093
+ if (attempt + 1 < MAX_RETRIES) await sleep(retryDelay(undefined, attempt));
1094
+ continue;
1095
+ }
1096
+ const payload = await response.json().catch(() => undefined);
1097
+ if (response.ok) {
1098
+ if (validWorkFileJob(payload?.data) && payload.data.id === jobId) return payload.data;
1099
+ throw new Error("Shifu returned an invalid file-job status. Local state was not advanced.");
1100
+ }
1101
+ lastError = new Error(payload?.error?.message || `Shifu could not check a file job (${response.status}).`);
1102
+ if (response.status !== 429 && response.status < 500) throw lastError;
1103
+ if (attempt + 1 < MAX_RETRIES) await sleep(retryDelay(response, attempt));
1104
+ }
1105
+ throw lastError;
1106
+ }
1107
+
1108
+ function saveWorkFileEntry(harness, folder, name, entry) {
1109
+ const state = readState(harness);
1110
+ const files = workFileEntries(state, folder);
1111
+ state.workFiles = { folder, version: WORK_FILE_SYNC_VERSION, files: { ...files, [name]: entry } };
1112
+ saveState(harness, state);
1113
+ }
1114
+
1048
1115
  async function syncWorkFiles(harness, config = readConfig(harness)) {
1049
1116
  if (typeof config.inputFolder !== "string") throw new Error("No input folder is configured. Run connect again to enable file sync.");
1050
1117
  const folder = validateInputFolder(configuredInputFolder(config));
1051
1118
  const state = readState(harness);
1052
- const prior = state.workFiles?.folder === folder && state.workFiles.version === WORK_FILE_SYNC_VERSION ? state.workFiles.files || {} : {};
1053
- let synced = 0;
1054
- let failures = 0;
1055
- for (const file of workFiles(folder)) {
1119
+ const prior = workFileEntries(state, folder);
1120
+ let submitted = 0;
1121
+ const failedFiles = new Set();
1122
+ const unverifiedJobs = new Set();
1123
+ for (const [name, entry] of Object.entries(prior)) {
1124
+ if (typeof entry !== "object" || !entry?.jobId) continue;
1125
+ try {
1126
+ const job = await getWorkFileJob(config, entry.jobId);
1127
+ if (FINAL_WORK_FILE_STATUSES.has(job.status)) {
1128
+ console.error(`${name}: ${workFileResult(job)}.`);
1129
+ if (entry.replayOnCompletion) {
1130
+ prior[name] = { ...entry, status: "partial" };
1131
+ saveWorkFileEntry(harness, folder, name, prior[name]);
1132
+ } else {
1133
+ saveWorkFileEntry(harness, folder, name, entry.sha256);
1134
+ prior[name] = entry.sha256;
1135
+ }
1136
+ } else {
1137
+ prior[name] = { ...entry, status: job.status };
1138
+ saveWorkFileEntry(harness, folder, name, prior[name]);
1139
+ if (job.status === "partial") console.error(`${name}: partially processed; failed chunks will be retried on this sync.`);
1140
+ }
1141
+ } catch (error) {
1142
+ failedFiles.add(name);
1143
+ unverifiedJobs.add(name);
1144
+ console.error(`${name} status remains pending: ${error.message}`);
1145
+ }
1146
+ }
1147
+ const scan = scanWorkFiles(folder);
1148
+ if (scan.unsupported) console.error(`${scan.unsupported} unsupported file${scan.unsupported === 1 ? "" : "s"} skipped (only .txt, .md, .rtf, and .docx are uploaded).`);
1149
+ for (const file of scan.files) {
1056
1150
  const name = path.basename(file);
1057
1151
  try {
1152
+ if (unverifiedJobs.has(name)) continue;
1058
1153
  const contents = readWorkFile(file);
1059
- if (prior[name] === contents.sha256) continue;
1060
- const receipt = await postWorkFile(config, file, contents);
1061
- const latest = readState(harness);
1062
- const files = latest.workFiles?.folder === folder && latest.workFiles.version === WORK_FILE_SYNC_VERSION ? latest.workFiles.files || {} : {};
1063
- latest.workFiles = { folder, version: WORK_FILE_SYNC_VERSION, files: { ...files, [name]: contents.sha256 } };
1064
- saveState(harness, latest);
1065
- prior[name] = contents.sha256;
1066
- synced += 1;
1067
- console.error(`${name}: ${receipt.status} (${receipt.claimsCreated} claim${receipt.claimsCreated === 1 ? "" : "s"}).`);
1154
+ const previous = prior[name];
1155
+ if (previous === contents.sha256 || (previous?.sha256 === contents.sha256 && previous.status !== "partial")) continue;
1156
+ const job = await postWorkFile(config, file, contents);
1157
+ const entry = FINAL_WORK_FILE_STATUSES.has(job.status) ? contents.sha256 : { sha256: contents.sha256, jobId: job.id, status: job.status };
1158
+ saveWorkFileEntry(harness, folder, name, entry);
1159
+ prior[name] = entry;
1160
+ submitted += 1;
1161
+ console.error(`${name}: ${FINAL_WORK_FILE_STATUSES.has(job.status) ? workFileResult(job) : `${job.status} (processing in background)`}.`);
1068
1162
  } catch (error) {
1069
- failures += 1;
1163
+ failedFiles.add(name);
1070
1164
  console.error(`${name} remains pending: ${error.message}`);
1071
1165
  }
1072
1166
  }
1073
- return { synced, failures };
1167
+ const pending = Object.values(prior).filter((entry) => typeof entry === "object" && entry?.jobId).length;
1168
+ for (const [name, entry] of Object.entries(prior)) {
1169
+ if (entry?.status === "partial") failedFiles.add(name);
1170
+ }
1171
+ return { submitted, pending, failures: failedFiles.size, unsupported: scan.unsupported };
1074
1172
  }
1075
1173
 
1076
1174
  async function sendSync(harness, input, config = readConfig(harness)) {
@@ -1223,13 +1321,17 @@ async function scheduledSync(harness) {
1223
1321
  const startedAt = Date.now();
1224
1322
  let failures = 0;
1225
1323
  let synced = 0;
1226
- let syncedFiles = 0;
1324
+ let submittedFiles = 0;
1325
+ let pendingFiles = 0;
1227
1326
  let failedFiles = 0;
1327
+ let unsupportedFiles = 0;
1228
1328
  if (config.inputFolder) {
1229
1329
  try {
1230
1330
  const files = await syncWorkFiles(harness, config);
1231
1331
  failedFiles = files.failures;
1232
- syncedFiles = files.synced;
1332
+ submittedFiles = files.submitted;
1333
+ pendingFiles = files.pending;
1334
+ unsupportedFiles = files.unsupported;
1233
1335
  } catch (error) {
1234
1336
  failedFiles = 1;
1235
1337
  console.error(`Shifu input folder remains pending: ${error.message}`);
@@ -1256,8 +1358,10 @@ async function scheduledSync(harness) {
1256
1358
  ...state.schedule,
1257
1359
  lastAttemptAt: startedAt,
1258
1360
  lastResult: notificationError ? "awaiting_approval_notification_failed" : failedFiles ? "partial_failure" : "awaiting_approval",
1259
- syncedFiles,
1361
+ submittedFiles,
1362
+ pendingFiles,
1260
1363
  failedFiles,
1364
+ unsupportedFiles,
1261
1365
  pendingSegments: queue.segments.length,
1262
1366
  pendingFingerprint,
1263
1367
  approvalSessionRef,
@@ -1282,8 +1386,10 @@ async function scheduledSync(harness) {
1282
1386
  lastResult: failures || failedFiles ? "partial_failure" : "complete",
1283
1387
  syncedSegments: synced,
1284
1388
  failedSessions: failures,
1285
- syncedFiles,
1389
+ submittedFiles,
1390
+ pendingFiles,
1286
1391
  failedFiles,
1392
+ unsupportedFiles,
1287
1393
  nextRunAt: startedAt + (failures || failedFiles ? SCHEDULER_TICK_MINUTES / 60 : policy.intervalHours) * 3_600_000,
1288
1394
  };
1289
1395
  }
@@ -1298,14 +1404,21 @@ function status(harness) {
1298
1404
  const config = readConfig(harness);
1299
1405
  const state = readState(harness);
1300
1406
  const pending = readJSON(pendingPath(harness), { segments: [] });
1301
- console.log(JSON.stringify({ harness, apiUrl: config.apiUrl, inputFolder: config.inputFolder || null, syncPolicy: normalizedPolicy(config), schedule: state.schedule || null, pendingSegments: pending.segments.length, syncedFiles: Object.keys(state.workFiles?.files || {}).length, sessions: state.sessions }, null, 2));
1407
+ const folder = config.inputFolder ? configuredInputFolder(config) : null;
1408
+ const entries = folder ? Object.values(workFileEntries(state, folder)) : [];
1409
+ let unsupportedFiles = null;
1410
+ let inputFolderError = null;
1411
+ if (folder) {
1412
+ try { unsupportedFiles = scanWorkFiles(folder).unsupported; } catch (error) { inputFolderError = error.message; }
1413
+ }
1414
+ console.log(JSON.stringify({ harness, apiUrl: config.apiUrl, inputFolder: folder, inputFolderError, 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));
1302
1415
  }
1303
1416
 
1304
1417
  async function syncFilesNow(harness) {
1305
1418
  const result = await withSyncLock(harness, () => syncWorkFiles(harness));
1306
1419
  if (result?.skipped) throw new Error("Another Shifu sync is already running. Files remain pending.");
1307
- console.error(`Checked the input folder: ${result.synced} sent, ${result.failures} pending.`);
1308
- if (result.failures) throw new Error("Some input files remain pending. Retry sync-files after resolving the errors above.");
1420
+ console.error(`Checked the input folder: ${result.submitted} submitted, ${result.pending} processing, ${result.failures} needing retry, ${result.unsupported} unsupported.`);
1421
+ if (result.failures) throw new Error("Some input files need retry. They will be checked on the next sync.");
1309
1422
  }
1310
1423
 
1311
1424
  function sessions(harness) {
@@ -4,7 +4,7 @@ description: Inspect or sync redacted OpenCode activity using the saved Shifu po
4
4
 
5
5
  Run `npx @useshifu/coding-harness status --harness opencode` first.
6
6
 
7
- For an explicit Shifu input-folder sync, run `sync-files --harness opencode`. It processes supported `.txt`, `.md`, `.rtf`, and `.docx` files in the configured folder without a claim-review step. Do not treat another speaker's work or a meeting action item as the user's contribution. The server parses and sanitizes files; the local runner does not redact file contents.
7
+ For an explicit Shifu input-folder sync, run `sync-files --harness opencode`. It polls unfinished jobs before submitting new or changed `.txt`, `.md`, `.rtf`, and `.docx` files up to 20 MB. Upload starts asynchronous server processing; check status on the next manual or scheduled sync. Only a final `processed`, `no_claims`, or `duplicate` result completes the local checkpoint; a `partial` result remains retryable. Completed file checkpoints from the previous runner are replayed once; existing in-flight jobs are polled before replay. Report published claims and needs-evidence suggestions separately. `no_claims` may still include suggestions, which are not verified user contributions. Unsupported files are counted but not uploaded. No claim-review step is needed. Do not treat another speaker's work or a meeting action item as the user's contribution. The server parses and sanitizes files; the local runner does not redact file contents, and file text or excerpts must not appear in logs.
8
8
 
9
9
  If manual approval has queued scheduled work, run `pending --harness opencode`, show the complete redacted list, and ask once. After approval, run `approve --approved --harness opencode`.
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useshifu/coding-harness",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Scheduled, redacted coding-harness activity sync for Shifu",
5
5
  "bin": {
6
6
  "coding-harness": "bin/shifu-harness.js",
@@ -19,7 +19,7 @@ When the user asks to connect, run:
19
19
  npx @useshifu/coding-harness connect --harness codex
20
20
  ```
21
21
 
22
- `connect` installs the current runner and a local schedule with no policy questions. New connections default to hourly automatic sync and `~/Desktop/Shifu` as the single input folder on macOS. It creates shortcuts at `~/Downloads/Shifu` and `~/Documents/Shifu` when possible without replacing conflicting items. Supported files are `.txt`, `.md`, `.rtf`, and `.docx` directly inside the folder; images and screenshots are not supported. If a connection already exists, `connect` keeps its key, checkpoints, and chosen policy. It also excludes the setup session when the harness exposes its current opaque session reference. No content is sent during setup.
22
+ `connect` installs the current runner and a local schedule with no policy questions. New connections default to hourly automatic sync and `~/Desktop/Shifu` as the single input folder on macOS. It creates shortcuts at `~/Downloads/Shifu` and `~/Documents/Shifu` when possible without replacing conflicting items. Supported files are `.txt`, `.md`, `.rtf`, and `.docx` up to 20 MB directly inside the folder; images and screenshots are not supported. Unknown formats are counted but never uploaded. If a connection already exists, `connect` keeps its key, checkpoints, and chosen policy. It also excludes the setup session when the harness exposes its current opaque session reference. No content is sent during setup.
23
23
 
24
24
  When the user asks to change the interval, approval mode, or input folder, run:
25
25
 
@@ -33,7 +33,9 @@ Use only the flags the user wants to change; existing settings are preserved. Th
33
33
 
34
34
  Run `npx @useshifu/coding-harness status --harness codex` to inspect the saved policy, next scheduled run, pending count, and checkpoints.
35
35
 
36
- For an explicit input-folder sync, run `npx @useshifu/coding-harness sync-files --harness codex`. It sends supported files automatically and advances each local checkpoint only after a final server receipt. No claim review or manual approval is needed for files. If a file fails, leave it pending and report the error; do not claim it was synced.
36
+ For an explicit input-folder sync, run `npx @useshifu/coding-harness sync-files --harness codex`. It first polls previously accepted file jobs, then submits new or changed supported files. Uploading starts background processing; it does not wait for extraction. A later manual or scheduled sync polls status again. Only `processed`, `no_claims`, or `duplicate` advances a file's completed checkpoint; `partial` is retried on the next sync. The upgraded runner replays old completed checkpoints once, while preserving and polling in-flight jobs before their replay. No claim review or manual approval is needed for files. Report submitted, processing, retry-needed, unsupported, published-claim, and needs-evidence suggestion counts separately. Do not claim a submitted file was fully processed. If a file fails, leave it pending and report the error. Never print file text, candidate statements, or excerpts into logs.
37
+
38
+ The folder runner accepts nonempty `.txt`, `.md`, `.rtf`, and `.docx` files up to 20 MB; the server accepts at most 500,000 extractable characters per file and splits that text into bounded chunks. Empty, malformed, binary, and renamed unsupported files cannot yield claims. A `no_claims` result means processing finished with no published claims; it may still have retained needs-evidence suggestions. Suggestions are possible contributions with unresolved evidence or attribution, not verified claims. If both counts are zero, no attributable contribution was extracted; upload itself may still have succeeded. For a named transcript, confirm the user's Shifu Profile display name matches their speaker label before expecting automated attribution. Never promise a claim from every file or infer one from an action item. The upload has a transport deadline, but model processing has no upload-request timeout; failed chunks back off and can be retried without republishing completed chunks.
37
39
 
38
40
  In manual mode, scheduled runs accumulate redacted segments locally. Run:
39
41