@useshifu/coding-harness 0.3.9 → 0.3.11
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 +20 -0
- package/bin/shifu-harness.js +246 -37
- package/commands/shifu-sync.md +7 -3
- package/package.json +1 -1
- package/skills/shifu-sync/SKILL.md +15 -5
package/README.md
CHANGED
|
@@ -32,6 +32,8 @@ To disconnect this device, run `npx --yes @useshifu/coding-harness disconnect --
|
|
|
32
32
|
|
|
33
33
|
## Operate
|
|
34
34
|
|
|
35
|
+
Version `0.3.11` adds rolling days-based windows and explicit force replacement of reviewed segments. Force replacement requires backend migration 049 and the matching connector endpoints to be deployed first.
|
|
36
|
+
|
|
35
37
|
```sh
|
|
36
38
|
npx --yes @useshifu/coding-harness status --harness codex
|
|
37
39
|
npx --yes @useshifu/coding-harness sessions --harness codex --hours 24
|
|
@@ -43,6 +45,8 @@ npx --yes @useshifu/coding-harness sync-files --harness codex
|
|
|
43
45
|
|
|
44
46
|
To select by session count instead of a time window, run `sessions --harness codex --count 5`. It selects the five most recently updated eligible sessions, newest first, without an hours cutoff. Current and setup sessions are excluded. `selectedSessionRefs` fixes that selection; `sessions` contains pending segments and `skippedSessions` reports already-synced sessions. Skipped sessions are not replaced with older ones. Review each pending session with `review --harness codex --session-ref <sessionRef> --all`. After an accepted segment, request the next with `sessions --harness codex --session-ref <sessionRef> --all` until that selected session is exhausted. Keep the original references instead of selecting another five.
|
|
45
47
|
|
|
48
|
+
Time-window selection accepts any positive whole number of days or hours. `sessions --harness codex --days 3` uses the same window as `--hours 72`; pass the same window to `review`. `--days 7` also works. Mixed, missing, invalid, or unsupported options fail explicitly. A recently active session with older unsynced turns appears in `blockedSessions`: sync cannot skip those turns. Explicitly select that session with `--session-ref <sessionRef> --all` to backfill in chronological segments of at most 12 turns.
|
|
49
|
+
|
|
46
50
|
Manual session sync runs immediately, independently of the hourly schedule. If a segment fails, report it as failed/pending; a checkpoint advances only after a matching server receipt. Report completed, already-synced, and failed session counts separately.
|
|
47
51
|
|
|
48
52
|
To upgrade an existing installation, run `npx --yes @useshifu/coding-harness@latest install --harness codex` (or `claude_code` / `opencode`). It replaces the local runner and harness instruction while preserving the connection, checkpoints, and schedule. Reconnecting is unnecessary. `status` includes `runnerVersion` so you can verify the installed version.
|
|
@@ -70,3 +74,19 @@ AI Profile observations are created automatically with each coding-session activ
|
|
|
70
74
|
The category is one of `system_design`, `detailed_design`, `implementation`, `debugging_testing`, `production_support`, `research_analysis`, `product_planning`, `writing_communication`, `marketing_growth`, `learning`, or `other`. Evidence dimensions are `task_selection`, `goal_clarity`, `context_sharing`, `constraint_setting`, `work_decomposition`, `iterative_steering`, `critical_judgment`, `verification`, `application`, and `responsible_use`. Automatic observations cite the relevant turn and never infer a limiting behavior from silence. Unsupported dimensions remain unscored.
|
|
71
75
|
|
|
72
76
|
The AI Profile page shows detected categories, model and thinking labels, and Jev scores supported by these limited observations. Local signals do not prove work quality or establish that another model setting would have performed better. Identical retries are safe.
|
|
77
|
+
|
|
78
|
+
## Recommended package updates
|
|
79
|
+
|
|
80
|
+
The connection endpoint returns `recommendedPackageVersion`, currently pinned to `0.3.10` (publish this release before deploying the recommendation). Before `sync`, `sync-files`, `approve`, or `scheduled-sync`, the runner compares its embedded release version with that property. An older runner installs the exact recommended release through npm and resumes the original command with the updated runner. Equal or newer runners continue unchanged. Explicitly recommended major releases follow the same comparison rule. Missing or invalid recommendations, connection failures, and npm failures do not block sync. The connection, pending work, checkpoints, and schedule are preserved.
|
|
81
|
+
|
|
82
|
+
This update mechanism first ships in `0.3.10`. Runners from earlier releases need a one-time installation of a release containing it (`npx --yes @useshifu/coding-harness@0.3.11 install --harness codex`). Raise the server recommendation only after the target release is published and its backend requirements are deployed.
|
|
83
|
+
|
|
84
|
+
## Replace previously synced work
|
|
85
|
+
|
|
86
|
+
An explicit force request re-reviews the original accepted segments and replaces their current work evidence and claims while retaining history. It does not reset or lower checkpoints, and never changes scheduled sync or AI Profile observations.
|
|
87
|
+
|
|
88
|
+
1. Discover accepted targets with `sessions --harness codex --days 3 --force` (or a requested hours/count selection). Keep that initial target list fixed; new pending turns still use ordinary sync.
|
|
89
|
+
2. Re-review each target with `review --harness codex --session-ref <sessionRef> --days 3 --force --from-turn <fromTurn> --to-turn <toTurn> --supersedes-sync-id <supersedesSyncId>`. The original local turns must exist. Explicit backfill/count selections use `--all`.
|
|
90
|
+
3. Prepare and redact the payload again, keeping the exact session reference and turn range and adding its `supersedesSyncId`. Pipe it to `sync --harness codex --force`, following the saved manual/automatic approval policy.
|
|
91
|
+
|
|
92
|
+
A changed target is rejected so newer work cannot be overwritten accidentally. Re-discover and re-review before attempting that replacement again. Identical retries are safe. Existing conversations retain their context; start a new chat in Shifu to use the latest context.
|
package/bin/shifu-harness.js
CHANGED
|
@@ -4,7 +4,9 @@ const fs = require("node:fs");
|
|
|
4
4
|
const { createHash } = require("node:crypto");
|
|
5
5
|
const os = require("node:os");
|
|
6
6
|
const path = require("node:path");
|
|
7
|
-
const { execFileSync } = require("node:child_process");
|
|
7
|
+
const { execFile, execFileSync, spawn } = require("node:child_process");
|
|
8
|
+
const { promisify } = require("node:util");
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
8
10
|
const readline = require("node:readline/promises");
|
|
9
11
|
|
|
10
12
|
const DEFAULT_API_URL = "https://api.useshifu.com";
|
|
@@ -23,7 +25,7 @@ const CREDENTIAL_ASSIGNMENT_PATTERN = /\b(?:token|secret|password|api[_-]?key)\s
|
|
|
23
25
|
const CLOUD_CREDENTIAL_PATTERN = /\bAKIA[0-9A-Z]{16}\b|AIza[0-9A-Za-z_-]{20,}/;
|
|
24
26
|
const EVIDENCE_KINDS = new Set(["implementation", "decision", "activity"]);
|
|
25
27
|
const EVIDENCE_FIELDS = new Set(["kind", "title", "statement", "scope", "area", "itemRef", "verificationRefs"]);
|
|
26
|
-
const SYNC_FIELDS = new Set(["harness", "sessionRef", "fromTurn", "toTurn", "title", "summary", "evidence", "verification", "decisions", "redactionVersion"]);
|
|
28
|
+
const SYNC_FIELDS = new Set(["harness", "sessionRef", "fromTurn", "toTurn", "title", "summary", "evidence", "verification", "decisions", "redactionVersion", "supersedesSyncId"]);
|
|
27
29
|
const AI_PROFILE_FIELDS = new Set(["harness", "sessionRef", "fromTurn", "toTurn", "category", "model", "thinking", "evidence"]);
|
|
28
30
|
const AI_PROFILE_EVIDENCE_FIELDS = new Set(["dimension", "statement", "signal", "turn"]);
|
|
29
31
|
const AI_PROFILE_DIMENSIONS = new Set(["task_selection", "goal_clarity", "context_sharing", "constraint_setting", "work_decomposition", "iterative_steering", "critical_judgment", "verification", "application", "responsible_use"]);
|
|
@@ -37,7 +39,7 @@ const MAX_TURNS_PER_SEGMENT = 12;
|
|
|
37
39
|
const MAX_TITLE_LENGTH = 160;
|
|
38
40
|
const MAX_WORK_DETAIL_LENGTH = 600;
|
|
39
41
|
const CONFIG_VERSION = 2;
|
|
40
|
-
const RUNNER_VERSION = "0.3.
|
|
42
|
+
const RUNNER_VERSION = "0.3.11";
|
|
41
43
|
const REDACTION_VERSION = 4;
|
|
42
44
|
const DEFAULT_INTERVAL_HOURS = 1;
|
|
43
45
|
const LEGACY_INTERVAL_HOURS = 4;
|
|
@@ -176,6 +178,43 @@ function hasFlag(name) {
|
|
|
176
178
|
return process.argv.includes(name);
|
|
177
179
|
}
|
|
178
180
|
|
|
181
|
+
function validateSyncOptions(command, start = 3) {
|
|
182
|
+
const common = ["--harness", "--config-root"];
|
|
183
|
+
const options = {
|
|
184
|
+
sessions: [...common, "--hours", "--days", "--count", "--turns", "--session-ref"],
|
|
185
|
+
review: [...common, "--hours", "--days", "--turns", "--session-ref", "--from-turn", "--to-turn", "--supersedes-sync-id"],
|
|
186
|
+
sync: common,
|
|
187
|
+
"sync-files": common,
|
|
188
|
+
"scheduled-sync": common,
|
|
189
|
+
pending: common,
|
|
190
|
+
approve: common,
|
|
191
|
+
};
|
|
192
|
+
const flags = {
|
|
193
|
+
sessions: ["--all", "--force"], review: ["--all", "--force"], sync: ["--approved", "--force"], approve: ["--approved"],
|
|
194
|
+
};
|
|
195
|
+
if (!options[command]) return;
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
for (let index = start; index < process.argv.length; index += 1) {
|
|
198
|
+
const argument = process.argv[index];
|
|
199
|
+
if (seen.has(argument)) throw new Error(`Provide ${argument} only once.`);
|
|
200
|
+
seen.add(argument);
|
|
201
|
+
if ((flags[command] || []).includes(argument)) continue;
|
|
202
|
+
if (!options[command].includes(argument)) throw new Error(`Unsupported option ${argument} for ${command}.`);
|
|
203
|
+
const value = process.argv[++index];
|
|
204
|
+
if (value === undefined || value.startsWith("--")) throw new Error(`Provide a value for ${argument}.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function syncWindowHours() {
|
|
209
|
+
if (hasFlag("--days") && hasFlag("--hours")) throw new Error("Use either --days or --hours, not both.");
|
|
210
|
+
const name = hasFlag("--days") ? "--days" : "--hours";
|
|
211
|
+
const value = numberOption(name, 24);
|
|
212
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new Error(`Choose a positive whole number for ${name}.`);
|
|
213
|
+
const hours = name === "--days" ? value * 24 : value;
|
|
214
|
+
if (!Number.isSafeInteger(hours) || hours * 3_600_000 > 8_640_000_000_000_000) throw new Error("The requested sync window is too large.");
|
|
215
|
+
return hours;
|
|
216
|
+
}
|
|
217
|
+
|
|
179
218
|
function readJSON(file, fallback) {
|
|
180
219
|
try {
|
|
181
220
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
@@ -562,6 +601,7 @@ const WORK_ACTIONS = [
|
|
|
562
601
|
{ key: "reviewed", title: "Reviewed", pattern: /^(?:I\s+|We\s+)?(?:reviewed|audited)\b/i },
|
|
563
602
|
{ key: "investigated", title: "Investigated", pattern: /^(?:I\s+|We\s+)?(?:investigated|researched|evaluated|compared)\b/i },
|
|
564
603
|
{ key: "deployed", title: "Deployed", pattern: /^(?:I\s+|We\s+)?(?:deployed|published)\b/i },
|
|
604
|
+
{ key: "decided", title: "Decided", pattern: /^(?:I\s+|We\s+)?(?:decided|chose|selected)\b/i },
|
|
565
605
|
];
|
|
566
606
|
|
|
567
607
|
const WORK_OBJECTS = [
|
|
@@ -572,6 +612,11 @@ const WORK_OBJECTS = [
|
|
|
572
612
|
{ key: "claim_extraction", title: "claim extraction", pattern: /\b(?:claim extraction|claim generation|claim matching)\b/i },
|
|
573
613
|
{ key: "retry", title: "retry handling", pattern: /\b(?:retries|retry)\b/i },
|
|
574
614
|
{ key: "cache", title: "cache behavior", pattern: /\b(?:cache|caching|ttl)\b/i },
|
|
615
|
+
{ key: "outbox", title: "transactional outbox", pattern: /\b(?:transactional outbox|outbox pattern)\b/i },
|
|
616
|
+
{ key: "token_bucket", title: "token bucket limiter", pattern: /\btoken bucket limiter\b/i },
|
|
617
|
+
{ key: "actor_scheduler", title: "actor scheduler", pattern: /\bactor scheduler\b/i },
|
|
618
|
+
{ key: "quota_balancing", title: "quota balancing", pattern: /\bquota balancing\b/i },
|
|
619
|
+
{ key: "shard_rebalancing", title: "shard rebalancing", pattern: /\bshard rebalancing\b/i },
|
|
575
620
|
{ key: "navigation", title: "navigation", pattern: /\b(?:navigation|sidebar|sub.?nav)\b/i },
|
|
576
621
|
{ key: "sync", title: "sync behavior", pattern: /\b(?:sync|synchronization|checkpoint)\b/i },
|
|
577
622
|
{ key: "auth", title: "authentication", pattern: /\b(?:authentication|sign.?in|login)\b/i },
|
|
@@ -626,6 +671,39 @@ const WORK_DETAILS = [
|
|
|
626
671
|
{ title: "mobile layout", pattern: /\bmobile layout\b/i },
|
|
627
672
|
];
|
|
628
673
|
|
|
674
|
+
// These terms are safe to retain without copying surrounding private prose.
|
|
675
|
+
const TECHNICAL_DETAILS = [
|
|
676
|
+
{ title: "Redis", pattern: /\bRedis\b/i },
|
|
677
|
+
{ title: "Kafka", pattern: /\bKafka\b/i },
|
|
678
|
+
{ title: "Postgres", pattern: /\bPostgres(?:ql)?\b/i },
|
|
679
|
+
{ title: "MongoDB", pattern: /\bMongoDB\b/i },
|
|
680
|
+
{ title: "React", pattern: /\bReact\b/i },
|
|
681
|
+
{ title: "Node.js", pattern: /\bNode\.js\b/i },
|
|
682
|
+
{ title: "TypeScript", pattern: /\bTypeScript\b/i },
|
|
683
|
+
{ title: "Python", pattern: /\bPython\b/i },
|
|
684
|
+
{ title: "Spring Boot", pattern: /\bSpring Boot\b/i },
|
|
685
|
+
{ title: "Kubernetes", pattern: /\bKubernetes\b/i },
|
|
686
|
+
{ title: "Docker", pattern: /\bDocker\b/i },
|
|
687
|
+
{ title: "GraphQL", pattern: /\bGraphQL\b/i },
|
|
688
|
+
{ title: "gRPC", pattern: /\bgRPC\b/i },
|
|
689
|
+
{ title: "cache-aside", pattern: /\bcache.aside\b/i },
|
|
690
|
+
{ title: "transactional outbox", pattern: /\btransactional outbox\b/i },
|
|
691
|
+
{ title: "circuit breaker", pattern: /\bcircuit breaker\b/i },
|
|
692
|
+
{ title: "Builder Pattern", pattern: /\bBuilder Pattern\b/i },
|
|
693
|
+
{ title: "event sourcing", pattern: /\bevent sourcing\b/i },
|
|
694
|
+
{ title: "CQRS", pattern: /\bCQRS\b/i },
|
|
695
|
+
{ title: "Saga Pattern", pattern: /\bSaga Pattern\b/i },
|
|
696
|
+
{ title: "Repository Pattern", pattern: /\bRepository Pattern\b/i },
|
|
697
|
+
{ title: "Dependency Injection", pattern: /\bDependency Injection\b/i },
|
|
698
|
+
{ title: "OpenTelemetry", pattern: /\bOpenTelemetry\b/i },
|
|
699
|
+
{ title: "Temporal", pattern: /\bTemporal\b/i },
|
|
700
|
+
{ title: "Rust", pattern: /\bRust\b/i },
|
|
701
|
+
{ title: "Tokio", pattern: /\bTokio\b/i },
|
|
702
|
+
{ title: "consistent hashing", pattern: /\bconsistent hashing\b/i },
|
|
703
|
+
{ title: "virtual nodes", pattern: /\bvirtual nodes\b/i },
|
|
704
|
+
{ title: "adaptive jitter", pattern: /\badaptive jitter\b/i },
|
|
705
|
+
];
|
|
706
|
+
|
|
629
707
|
function controlledContexts(text) {
|
|
630
708
|
const matches = WORK_CONTEXTS.flatMap((item) => {
|
|
631
709
|
const match = item.pattern.exec(text);
|
|
@@ -640,15 +718,27 @@ function controlledContexts(text) {
|
|
|
640
718
|
}
|
|
641
719
|
|
|
642
720
|
function controlledDetails(text) {
|
|
643
|
-
|
|
721
|
+
const known = [...WORK_DETAILS, ...TECHNICAL_DETAILS];
|
|
722
|
+
const ttl = /\b(\d{1,3})[ -]hour TTL\b/i.exec(text);
|
|
723
|
+
const latency = /\bp(?:50|90|95|99) latency from (\d{1,4}) ms to (\d{1,4}) ms\b/i.exec(text);
|
|
724
|
+
const reductionPattern = /\b(?:cutting|cut|reducing|reduced|decreasing|decreased|lowering|lowered)\s+(queue time|memory(?: usage)?)\s+by\s*(\d{1,3}(?:\.\d{1,2})?)%(?![\d.])/i;
|
|
725
|
+
const reduction = reductionPattern.exec(text);
|
|
726
|
+
if (ttl) known.push({ title: `${ttl[1]}-hour TTL`, pattern: /\b\d{1,3}[ -]hour TTL\b/i });
|
|
727
|
+
if (latency) known.push({ title: latency[0], pattern: /\bp(?:50|90|95|99) latency from \d{1,4} ms to \d{1,4} ms\b/i });
|
|
728
|
+
if (reduction && Number(reduction[2]) <= 100) {
|
|
729
|
+
const metric = reduction[1].toLowerCase().startsWith("memory") ? "memory usage" : "queue time";
|
|
730
|
+
known.push({ title: `${metric} reduced by ${reduction[2]}%`, pattern: reductionPattern });
|
|
731
|
+
}
|
|
732
|
+
return known.map((item) => ({ item, index: item.pattern.exec(text)?.index }))
|
|
644
733
|
.filter(({ index }) => index !== undefined)
|
|
645
734
|
.sort((left, right) => left.index - right.index)
|
|
646
|
-
.
|
|
647
|
-
.
|
|
735
|
+
.map(({ item }) => item.title)
|
|
736
|
+
.filter((detail, index, details) => details.indexOf(detail) === index)
|
|
737
|
+
.slice(0, 6);
|
|
648
738
|
}
|
|
649
739
|
|
|
650
740
|
function withoutNamedLabels(text) {
|
|
651
|
-
return text.replace(/\b(?:Project|Client|Customer|Company)\s+[A-Z][A-Za-z0-9_-]*/
|
|
741
|
+
return text.replace(/\b(?:Project|Client|Customer|Company)\s+[A-Z][A-Za-z0-9_-]*/gi, "");
|
|
652
742
|
}
|
|
653
743
|
|
|
654
744
|
function safeActivityEvidence(notes) {
|
|
@@ -666,7 +756,7 @@ function safeActivityEvidence(notes) {
|
|
|
666
756
|
sectionContexts = /\b(?:project|feature|flow|service|system|product)\b/i.test(label) ? controlledContexts(label) || [] : [];
|
|
667
757
|
continue;
|
|
668
758
|
}
|
|
669
|
-
const line = rawLine.trim().replace(/^[-*]\s+/, "").split(/[
|
|
759
|
+
const line = rawLine.trim().replace(/^[-*]\s+/, "").split(/[;—]|[.!?](?=\s|$)/, 1)[0];
|
|
670
760
|
const clauses = line.split(/\s+(?:and|but|while)\s+/i);
|
|
671
761
|
const nextAction = clauses.findIndex((clause, index) => index > 0 && WORK_ACTIONS.some((item) => item.pattern.test(clause)));
|
|
672
762
|
const workClause = withoutNamedLabels(nextAction > 0 ? clauses.slice(0, nextAction).join(" and ") : line);
|
|
@@ -710,7 +800,7 @@ function payloadForSegment(harness, segment) {
|
|
|
710
800
|
summary,
|
|
711
801
|
evidence,
|
|
712
802
|
verification: [],
|
|
713
|
-
decisions:
|
|
803
|
+
decisions: evidence.filter((item) => item.title.startsWith("Decided ")).slice(0, MAX_SYNC_ITEMS).map((item) => item.statement),
|
|
714
804
|
redactionVersion: REDACTION_VERSION,
|
|
715
805
|
};
|
|
716
806
|
}
|
|
@@ -839,7 +929,7 @@ function validateGeneratedProfile(profile, harness) {
|
|
|
839
929
|
}
|
|
840
930
|
|
|
841
931
|
function selectedSessionSegments(harness, { hours = 24, turns = MAX_TURNS_PER_SEGMENT, selectedRef, includeOlder = false, count } = {}) {
|
|
842
|
-
if (count === undefined && !
|
|
932
|
+
if (count === undefined && (!Number.isSafeInteger(hours) || hours < 1)) throw new Error("Choose a positive whole number for --hours.");
|
|
843
933
|
if (count !== undefined && (!Number.isSafeInteger(count) || count < 1)) throw new Error("Choose a positive whole number for --count.");
|
|
844
934
|
if (!Number.isInteger(turns) || turns < 1 || turns > MAX_TURNS_PER_SEGMENT) throw new Error("Choose --turns between 1 and 12.");
|
|
845
935
|
const since = Date.now() - hours * 3_600_000;
|
|
@@ -940,8 +1030,14 @@ async function readSecret(input = process.stdin, output = process.stderr) {
|
|
|
940
1030
|
|
|
941
1031
|
function copyRunner() {
|
|
942
1032
|
fs.mkdirSync(configRoot(), { recursive: true, mode: 0o700 });
|
|
943
|
-
|
|
944
|
-
|
|
1033
|
+
const temporary = `${installedRunnerPath()}.${process.pid}.tmp`;
|
|
1034
|
+
try {
|
|
1035
|
+
fs.copyFileSync(__filename, temporary);
|
|
1036
|
+
fs.chmodSync(temporary, 0o700);
|
|
1037
|
+
fs.renameSync(temporary, installedRunnerPath());
|
|
1038
|
+
} finally {
|
|
1039
|
+
fs.rmSync(temporary, { force: true });
|
|
1040
|
+
}
|
|
945
1041
|
}
|
|
946
1042
|
|
|
947
1043
|
function copyDirectory(source, destination) {
|
|
@@ -1160,6 +1256,7 @@ async function validateConnection(apiUrl, token, harness) {
|
|
|
1160
1256
|
if (!response.ok) throw new Error(`Shifu could not verify the connection key (HTTP ${response.status}). Nothing was changed.`);
|
|
1161
1257
|
const result = await response.json().catch(() => null);
|
|
1162
1258
|
if (result?.data?.connected !== true) throw new Error("Shifu returned an invalid connection check. Nothing was changed.");
|
|
1259
|
+
return result.data;
|
|
1163
1260
|
}
|
|
1164
1261
|
|
|
1165
1262
|
async function connect(harness, readKey = readSecret) {
|
|
@@ -1232,6 +1329,7 @@ function sessionRefIsSafe(value) {
|
|
|
1232
1329
|
function validateSync(input, harness) {
|
|
1233
1330
|
if (!input || Object.keys(input).some((field) => !SYNC_FIELDS.has(field))) throw new Error("Remove unsupported fields from the reviewed sync payload.");
|
|
1234
1331
|
if (!input || input.harness !== harness || !sessionRefIsSafe(input.sessionRef)) throw new Error("Add the current opaque sessionRef before syncing.");
|
|
1332
|
+
if (Object.hasOwn(input, "supersedesSyncId") && (!Number.isSafeInteger(input.supersedesSyncId) || input.supersedesSyncId < 1)) throw new Error("Use the positive supersedesSyncId returned by force discovery.");
|
|
1235
1333
|
if (!Number.isInteger(input.fromTurn) || !Number.isInteger(input.toTurn) || input.fromTurn < 1 || input.toTurn < input.fromTurn || input.toTurn > 1_000_000 || input.toTurn - input.fromTurn >= MAX_TURNS_PER_SEGMENT) throw new Error("Use a valid incremental range of at most 12 turns.");
|
|
1236
1334
|
if (input.redactionVersion >= 3 && !textIsSafe(input.title, MAX_TITLE_LENGTH)) throw new Error("Add a concise, redacted title for this reviewed sync.");
|
|
1237
1335
|
if (!textIsSafe(input.summary, 1200)) throw new Error("The summary is empty, too long, or contains sensitive content. Redact it before syncing.");
|
|
@@ -1275,8 +1373,8 @@ function validateAIProfileObservation(input, harness) {
|
|
|
1275
1373
|
}
|
|
1276
1374
|
|
|
1277
1375
|
function validSyncReceipt(receipt, input) {
|
|
1278
|
-
return receipt?.sessionRef === input.sessionRef && receipt.acceptedToTurn === input.toTurn && Number.isInteger(receipt.lastSyncedTurn) &&
|
|
1279
|
-
(receipt.duplicate ? receipt.lastSyncedTurn >= input.toTurn : receipt.lastSyncedTurn === input.toTurn);
|
|
1376
|
+
return receipt?.sessionRef === input.sessionRef && receipt.acceptedToTurn === input.toTurn && Number.isInteger(receipt.lastSyncedTurn) && receipt.lastSyncedTurn <= 1_000_000 &&
|
|
1377
|
+
(receipt.duplicate || input.supersedesSyncId ? receipt.lastSyncedTurn >= input.toTurn : receipt.lastSyncedTurn === input.toTurn);
|
|
1280
1378
|
}
|
|
1281
1379
|
|
|
1282
1380
|
function sleep(milliseconds) {
|
|
@@ -1522,18 +1620,21 @@ async function sendSync(harness, input, config = readConfig(harness)) {
|
|
|
1522
1620
|
const state = readState(harness);
|
|
1523
1621
|
const checkpoint = state.sessions[input.sessionRef];
|
|
1524
1622
|
const expected = (checkpoint?.lastSyncedTurn || 0) + 1;
|
|
1525
|
-
if (input.fromTurn !== expected) throw new Error(`This local checkpoint expects turn ${expected}. Review the pending segment before retrying.`);
|
|
1526
|
-
|
|
1623
|
+
if (!input.supersedesSyncId && input.fromTurn !== expected) throw new Error(`This local checkpoint expects turn ${expected}. Review the pending segment before retrying.`);
|
|
1624
|
+
if (input.supersedesSyncId && reviewSegment(harness, input.sessionRef, input.fromTurn, input.toTurn).turnCount < input.toTurn) throw new Error("The original local turns must still exist before replacing this sync.");
|
|
1625
|
+
const profile = input.supersedesSyncId ? undefined : profileForSegment(harness, input);
|
|
1527
1626
|
const receipt = await postSync(config, input);
|
|
1528
1627
|
if (!validSyncReceipt(receipt, input)) {
|
|
1529
1628
|
throw new Error("Shifu returned a checkpoint that does not match this reviewed segment. Local state was not advanced.");
|
|
1530
1629
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1630
|
+
if (profile) {
|
|
1631
|
+
state.profilePending ||= {};
|
|
1632
|
+
state.profilePending[`${profile.sessionRef}:${profile.fromTurn}:${profile.toTurn}`] = profile;
|
|
1633
|
+
}
|
|
1634
|
+
state.sessions[input.sessionRef] = { turnsSinceSync: 0, lastSyncedAt: Date.now(), lastSyncedTurn: Math.max(checkpoint?.lastSyncedTurn || 0, receipt.lastSyncedTurn), syncDue: false };
|
|
1534
1635
|
saveState(harness, state);
|
|
1535
|
-
await flushPendingProfiles(harness, config);
|
|
1536
|
-
console.error(receipt.duplicate ? "Shifu already had this exact segment. Local checkpoint recovered." : `Synced through turn ${receipt.lastSyncedTurn}.`);
|
|
1636
|
+
if (profile) await flushPendingProfiles(harness, config);
|
|
1637
|
+
console.error(receipt.duplicate ? "Shifu already had this exact segment. Local checkpoint recovered." : input.supersedesSyncId ? `Replaced the reviewed sync for turns ${input.fromTurn}–${input.toTurn}.` : `Synced through turn ${receipt.lastSyncedTurn}.`);
|
|
1537
1638
|
return receipt;
|
|
1538
1639
|
}
|
|
1539
1640
|
|
|
@@ -1571,6 +1672,7 @@ async function sync(harness) {
|
|
|
1571
1672
|
const config = readConfig(harness);
|
|
1572
1673
|
const input = readJSON(0, undefined);
|
|
1573
1674
|
validateSync(input, harness);
|
|
1675
|
+
if (hasFlag("--force") !== Boolean(input.supersedesSyncId)) throw new Error("Force replacement requires both --force and the reviewed payload's supersedesSyncId. Use ordinary sync for new segments.");
|
|
1574
1676
|
const policy = normalizedPolicy(config);
|
|
1575
1677
|
if (policy.approvalMode === "manual" && !hasFlag("--approved")) {
|
|
1576
1678
|
console.error(`\nThis exact redacted segment will be sent to Shifu:\n${JSON.stringify(input, null, 2)}`);
|
|
@@ -1776,23 +1878,75 @@ async function syncFilesNow(harness) {
|
|
|
1776
1878
|
if (result.failures) throw new Error("Some input files need retry. They will be checked on the next sync.");
|
|
1777
1879
|
}
|
|
1778
1880
|
|
|
1779
|
-
function
|
|
1780
|
-
|
|
1881
|
+
async function acceptedSegments(config, sessionRef) {
|
|
1882
|
+
const segments = [];
|
|
1883
|
+
let cursor = 0;
|
|
1884
|
+
do {
|
|
1885
|
+
const url = new URL(`${config.apiUrl}/v1/connectors/coding-sessions/syncs`);
|
|
1886
|
+
url.searchParams.set("sessionRef", sessionRef);
|
|
1887
|
+
if (cursor) url.searchParams.set("cursor", String(cursor));
|
|
1888
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${config.token}` }, signal: AbortSignal.timeout(30_000) });
|
|
1889
|
+
const payload = await response.json().catch(() => undefined);
|
|
1890
|
+
if (!response.ok) throw new Error(payload?.error?.message || `Could not inspect accepted sync segments (${response.status}).`);
|
|
1891
|
+
const page = payload?.data;
|
|
1892
|
+
if (!Array.isArray(page?.segments) || page.segments.length > 100 || page.segments.some((segment, index) =>
|
|
1893
|
+
!Number.isSafeInteger(segment.id) || segment.id < 1 || segment.sessionRef !== sessionRef ||
|
|
1894
|
+
!Number.isInteger(segment.fromTurn) || !Number.isInteger(segment.toTurn) || segment.fromTurn < 1 ||
|
|
1895
|
+
segment.toTurn < segment.fromTurn || segment.toTurn - segment.fromTurn >= MAX_TURNS_PER_SEGMENT ||
|
|
1896
|
+
segment.toTurn > 1_000_000 || segment.fromTurn <= (index ? page.segments[index - 1].toTurn : cursor))) {
|
|
1897
|
+
throw new Error("Shifu returned invalid accepted sync segments. No replacement was sent.");
|
|
1898
|
+
}
|
|
1899
|
+
segments.push(...page.segments);
|
|
1900
|
+
const next = page.nextCursor;
|
|
1901
|
+
if (next === undefined || next === null || next === "") return segments;
|
|
1902
|
+
const nextTurn = Number(next);
|
|
1903
|
+
if (typeof next !== "string" || !/^[1-9]\d*$/.test(next) || !Number.isInteger(nextTurn) || nextTurn <= cursor || nextTurn !== page.segments.at(-1)?.toTurn) throw new Error("Shifu returned an invalid sync-segment cursor.");
|
|
1904
|
+
cursor = nextTurn;
|
|
1905
|
+
} while (cursor < 1_000_000);
|
|
1906
|
+
throw new Error("Shifu returned an invalid sync-segment cursor.");
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
async function sessions(harness) {
|
|
1910
|
+
const config = readConfig(harness);
|
|
1781
1911
|
const count = option("--count");
|
|
1782
1912
|
if (hasFlag("--count") && count === undefined) throw new Error("Provide a value for --count.");
|
|
1783
|
-
if (count !== undefined &&
|
|
1784
|
-
const hours =
|
|
1913
|
+
if (count !== undefined && (hasFlag("--hours") || hasFlag("--days"))) throw new Error("Use --count without --hours or --days.");
|
|
1914
|
+
const hours = syncWindowHours();
|
|
1785
1915
|
const turns = numberOption("--turns", MAX_TURNS_PER_SEGMENT);
|
|
1786
1916
|
const sessionRef = option("--session-ref");
|
|
1787
1917
|
const all = hasFlag("--all");
|
|
1788
1918
|
if (count !== undefined && (sessionRef || all)) throw new Error("Use --count without --session-ref or --all.");
|
|
1789
1919
|
if (all && !sessionRef) throw new Error("Use --all only with an explicitly selected --session-ref.");
|
|
1790
1920
|
const countValue = count === undefined ? undefined : Number(count);
|
|
1921
|
+
if (hasFlag("--force")) {
|
|
1922
|
+
if (hasFlag("--turns")) throw new Error("Force replacement uses the original stored ranges; do not provide --turns.");
|
|
1923
|
+
const selected = selectedSessionSegments(harness, { hours, selectedRef: sessionRef, includeOlder: true, count: countValue });
|
|
1924
|
+
const replacements = [];
|
|
1925
|
+
const unavailable = [];
|
|
1926
|
+
const since = Date.now() - hours * 3_600_000;
|
|
1927
|
+
const metadataByRef = new Map();
|
|
1928
|
+
for (const metadata of localSessions(harness)) {
|
|
1929
|
+
if (metadata.turnCount >= (metadataByRef.get(metadata.sessionRef)?.turnCount || 0)) metadataByRef.set(metadata.sessionRef, metadata);
|
|
1930
|
+
}
|
|
1931
|
+
for (const session of selected) {
|
|
1932
|
+
const metadata = metadataByRef.get(session.sessionRef);
|
|
1933
|
+
for (const segment of await acceptedSegments(config, session.sessionRef)) {
|
|
1934
|
+
if (segment.toTurn > session.turnCount) {
|
|
1935
|
+
unavailable.push({ sessionRef: session.sessionRef, supersedesSyncId: segment.id, reason: "local_turns_missing" });
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
if (!all && count === undefined && !(Date.parse(metadata.turnTimes[segment.fromTurn - 1]) >= since)) continue;
|
|
1939
|
+
replacements.push({ sessionRef: session.sessionRef, fromTurn: segment.fromTurn, toTurn: segment.toTurn, supersedesSyncId: segment.id });
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
console.log(JSON.stringify({ harness, ...(count === undefined ? { hours } : { count: countValue, selectedSessionRefs: selected.map((item) => item.sessionRef) }), force: true, sessions: replacements, unavailableSegments: unavailable }, null, 2));
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1791
1945
|
const selected = selectedSessionSegments(harness, { hours, turns, selectedRef: sessionRef, includeOlder: all, count: countValue });
|
|
1792
1946
|
const pending = selected.filter((session) => session.fromTurn <= session.toTurn);
|
|
1793
1947
|
const blocked = all || count !== undefined ? [] : unsyncedCodexSessions(harness, { hours, turns, selectedRef: sessionRef, includeOlder: true })
|
|
1794
1948
|
.filter((session) => !pending.some((item) => item.sessionRef === session.sessionRef))
|
|
1795
|
-
.map(({ sessionRef, oldestUnsyncedAt }) => ({ sessionRef, oldestUnsyncedAt }));
|
|
1949
|
+
.map(({ sessionRef, oldestUnsyncedAt }) => ({ sessionRef, oldestUnsyncedAt, reason: "older_unsynced_turns", nextStep: "Select this session explicitly with --session-ref and --all to backfill in order." }));
|
|
1796
1950
|
console.log(JSON.stringify(count === undefined
|
|
1797
1951
|
? { harness, hours, turns, sessions: pending, blockedSessions: blocked }
|
|
1798
1952
|
: {
|
|
@@ -1804,32 +1958,43 @@ function sessions(harness) {
|
|
|
1804
1958
|
}, null, 2));
|
|
1805
1959
|
}
|
|
1806
1960
|
|
|
1807
|
-
function review(harness) {
|
|
1808
|
-
readConfig(harness);
|
|
1961
|
+
async function review(harness) {
|
|
1962
|
+
const config = readConfig(harness);
|
|
1809
1963
|
const sessionRef = option("--session-ref");
|
|
1810
1964
|
if (typeof sessionRef !== "string" || sessionRef.length < 8) throw new Error(`Provide an opaque ${HARNESS_NAMES[harness]} --session-ref from the sessions command.`);
|
|
1811
1965
|
const state = readState(harness);
|
|
1812
|
-
|
|
1966
|
+
let fromTurn = (state.sessions[sessionRef]?.lastSyncedTurn || 0) + 1;
|
|
1813
1967
|
const selected = localSessions(harness).filter((session) => session.sessionRef === sessionRef)
|
|
1814
1968
|
.sort((left, right) => right.turnCount - left.turnCount || right.updatedAt.localeCompare(left.updatedAt))[0];
|
|
1815
1969
|
if (!selected) throw new Error(`No local ${HARNESS_NAMES[harness]} session matches this reference.`);
|
|
1816
|
-
const hours =
|
|
1817
|
-
if (![24, 48, 72].includes(hours)) throw new Error("Choose --hours 24, 48, or 72.");
|
|
1818
|
-
if (!hasFlag("--all") && !(Date.parse(selected.turnTimes[fromTurn - 1]) >= Date.now() - hours * 3_600_000)) {
|
|
1819
|
-
throw new Error("This session's next unsynced turn predates the selected window. Ask for this specific session and use --all to review its backlog.");
|
|
1820
|
-
}
|
|
1970
|
+
const hours = syncWindowHours();
|
|
1821
1971
|
const turns = numberOption("--turns", MAX_TURNS_PER_SEGMENT);
|
|
1822
1972
|
if (!Number.isInteger(turns) || turns < 1 || turns > MAX_TURNS_PER_SEGMENT) throw new Error("Choose --turns between 1 and 12.");
|
|
1823
|
-
|
|
1973
|
+
let toTurn = Math.min(selected.turnCount, fromTurn + turns - 1);
|
|
1974
|
+
let supersedesSyncId;
|
|
1975
|
+
if (hasFlag("--force")) {
|
|
1976
|
+
if (hasFlag("--turns")) throw new Error("Force review uses the original stored range; do not provide --turns.");
|
|
1977
|
+
fromTurn = numberOption("--from-turn");
|
|
1978
|
+
toTurn = numberOption("--to-turn");
|
|
1979
|
+
supersedesSyncId = numberOption("--supersedes-sync-id");
|
|
1980
|
+
const target = (await acceptedSegments(config, sessionRef)).find((segment) => segment.id === supersedesSyncId && segment.fromTurn === fromTurn && segment.toTurn === toTurn);
|
|
1981
|
+
if (!target || toTurn > selected.turnCount) throw new Error("Use the exact current segment and supersedesSyncId from sessions --force; the original local turns must still exist.");
|
|
1982
|
+
} else if (["--from-turn", "--to-turn", "--supersedes-sync-id"].some(hasFlag)) {
|
|
1983
|
+
throw new Error("Explicit replacement boundaries require --force.");
|
|
1984
|
+
}
|
|
1824
1985
|
if (fromTurn > toTurn) throw new Error(`This ${HARNESS_NAMES[harness]} session has no unsynced turns to review.`);
|
|
1986
|
+
if (!hasFlag("--all") && !(Date.parse(selected.turnTimes[fromTurn - 1]) >= Date.now() - hours * 3_600_000)) {
|
|
1987
|
+
throw new Error("This session's selected turn predates the selected window. Ask for this specific session and use --all to review its backlog.");
|
|
1988
|
+
}
|
|
1825
1989
|
const { notes } = reviewSegment(harness, sessionRef, fromTurn, toTurn);
|
|
1826
1990
|
const candidates = reviewCandidates(notes);
|
|
1827
1991
|
console.log(JSON.stringify({
|
|
1828
1992
|
sessionRef,
|
|
1829
1993
|
fromTurn,
|
|
1830
1994
|
toTurn,
|
|
1995
|
+
...(supersedesSyncId ? { supersedesSyncId } : {}),
|
|
1831
1996
|
oldestUnsyncedAt: selected.turnTimes[fromTurn - 1] || null,
|
|
1832
|
-
source: "Local assistant final notes for
|
|
1997
|
+
source: "Local assistant final notes for the selected user turns. Candidates are suggestions, not verified or guaranteed redacted; inspect them before constructing a Shifu payload.",
|
|
1833
1998
|
notes,
|
|
1834
1999
|
candidates,
|
|
1835
2000
|
draft: { title: null, summary: null, evidence: candidates.map((candidate) => ({ ...candidate, title: null })) },
|
|
@@ -1838,11 +2003,55 @@ function review(harness) {
|
|
|
1838
2003
|
}, null, 2));
|
|
1839
2004
|
}
|
|
1840
2005
|
|
|
2006
|
+
function olderPackageVersion(current, recommended) {
|
|
2007
|
+
const pattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
2008
|
+
if (!pattern.test(current) || !pattern.test(recommended)) return false;
|
|
2009
|
+
const left = current.split(".").map(BigInt);
|
|
2010
|
+
const right = recommended.split(".").map(BigInt);
|
|
2011
|
+
for (let index = 0; index < 3; index += 1) {
|
|
2012
|
+
if (left[index] !== right[index]) return left[index] < right[index];
|
|
2013
|
+
}
|
|
2014
|
+
return false;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
async function installPackageVersion(version, harness) {
|
|
2018
|
+
await execFileAsync("npm", ["exec", "--yes", `--package=@useshifu/coding-harness@${version}`, "--", "shifu-harness", "install", "--harness", harness, "--config-root", configRoot()], {
|
|
2019
|
+
timeout: 60_000,
|
|
2020
|
+
env: { ...process.env, PATH: `${process.env.PATH || ""}${path.delimiter}${path.dirname(process.execPath)}`, SHIFU_UPDATE_CHECKED: "1" },
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
async function updateRecommendedPackage(harness, { currentVersion = RUNNER_VERSION, fetchConnection = validateConnection, installVersion = installPackageVersion } = {}) {
|
|
2025
|
+
try {
|
|
2026
|
+
const config = readConfig(harness);
|
|
2027
|
+
const connection = await fetchConnection(config.apiUrl, config.token, harness);
|
|
2028
|
+
const recommended = connection?.recommendedPackageVersion;
|
|
2029
|
+
if (!olderPackageVersion(currentVersion, recommended)) return false;
|
|
2030
|
+
await installVersion(recommended, harness);
|
|
2031
|
+
console.error(`Updated Shifu coding harness to recommended version ${recommended}.`);
|
|
2032
|
+
return true;
|
|
2033
|
+
} catch {
|
|
2034
|
+
console.error("Could not check or install the recommended Shifu package. Continuing with the current runner.");
|
|
2035
|
+
return false;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
|
|
1841
2039
|
async function main() {
|
|
1842
2040
|
if (option("--config-root")) process.env.SHIFU_CONFIG_ROOT = path.resolve(option("--config-root"));
|
|
1843
2041
|
const command = process.argv[2];
|
|
1844
2042
|
const subCommand = process.argv[3];
|
|
2043
|
+
validateSyncOptions(command === "discover" || command === "session" ? "sessions" : command, command === "session" && ["discover", "list"].includes(subCommand) ? 4 : 3);
|
|
1845
2044
|
const harness = requireHarness(option("--harness"));
|
|
2045
|
+
if (["sync", "sync-files", "scheduled-sync", "approve"].includes(command) && process.env.SHIFU_UPDATE_CHECKED !== "1" && await updateRecommendedPackage(harness)) {
|
|
2046
|
+
const child = spawn(process.execPath, [installedRunnerPath(), ...process.argv.slice(2)], {
|
|
2047
|
+
env: { ...process.env, SHIFU_UPDATE_CHECKED: "1" }, stdio: "inherit",
|
|
2048
|
+
});
|
|
2049
|
+
await new Promise((resolve, reject) => {
|
|
2050
|
+
child.once("error", reject);
|
|
2051
|
+
child.once("exit", (code) => code === 0 ? resolve() : reject(new Error("The updated Shifu sync failed; pending work is preserved.")));
|
|
2052
|
+
});
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
1846
2055
|
if (command === "install") return install(harness);
|
|
1847
2056
|
if (command === "connect") return connect(harness);
|
|
1848
2057
|
if (command === "disconnect") return disconnect(harness);
|
|
@@ -1867,4 +2076,4 @@ if (require.main === module) {
|
|
|
1867
2076
|
});
|
|
1868
2077
|
}
|
|
1869
2078
|
|
|
1870
|
-
module.exports = { approvalNotificationInvocation, claudeReview, claudeSession, codexReview, codexSession, configPath, configuredSyncPolicy, connect, defaultInputFolder, disconnect, finalAssistantNote, install, installDestination, localSessions, normalizedPolicy, notificationSessionRef, opencodeQuery, opencodeReview, opencodeSessions, payloadForSegment, pendingSegments, prepareInputFolder, profileForSegment, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, schedulerArtifact, statePath, syncWorkFiles, textIsSafe, unsyncedCodexSessions, validateAIProfileObservation, validateConnection, validateInputFolder, validSyncReceipt, validateApprovalMode, validateIntervalHours, validateSync, workFiles };
|
|
2079
|
+
module.exports = { olderPackageVersion, updateRecommendedPackage, installPackageVersion, approvalNotificationInvocation, claudeReview, claudeSession, codexReview, codexSession, configPath, configuredSyncPolicy, connect, defaultInputFolder, disconnect, finalAssistantNote, install, installDestination, localSessions, normalizedPolicy, notificationSessionRef, opencodeQuery, opencodeReview, opencodeSessions, payloadForSegment, pendingSegments, prepareInputFolder, profileForSegment, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, schedulerArtifact, statePath, syncWorkFiles, textIsSafe, unsyncedCodexSessions, validateAIProfileObservation, validateConnection, validateInputFolder, validSyncReceipt, validateApprovalMode, validateIntervalHours, validateSync, workFiles };
|
package/commands/shifu-sync.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
description: Inspect or sync redacted OpenCode activity using the saved Shifu policy.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
Before sync, the runner installs Shifu's exact `recommendedPackageVersion` only if its current version is older. Equal or newer versions are left alone. Update failures allow sync to continue; do not independently install npm latest.
|
|
6
|
+
|
|
5
7
|
Run `npx --yes @useshifu/coding-harness status --harness opencode` first.
|
|
6
8
|
|
|
7
9
|
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.
|
|
@@ -10,11 +12,13 @@ If manual approval has queued scheduled work, run `pending --harness opencode`,
|
|
|
10
12
|
|
|
11
13
|
For an explicit sync-now request:
|
|
12
14
|
|
|
13
|
-
1. For a time-window request, run `sessions --harness opencode --hours 24`, using
|
|
14
|
-
2. Review each returned segment with `review --harness opencode --session-ref <sessionRef> --hours <hours>`. For count-based selection, use `review --harness opencode --session-ref <sessionRef> --all` so older sessions are allowed.
|
|
15
|
-
3. Prepare a redaction-version-four payload from final assistant notes only. Use `activity` evidence for writing, feedback, research, planning, product work, and any work that is not clearly an engineering implementation or decision. Activity evidence has no technical scope. Preserve the supported action, feature or project purpose, specific problem, approach, and verified result. Synthesize a descriptive project label (such as "payment reconciliation service") from its purpose; never copy a raw label that could identify a company or person. Keep distinct contributions separate, including different fixes within one feature. An empty evidence list advances the checkpoint without creating a claim.
|
|
15
|
+
1. For a time-window request, run `sessions --harness opencode --hours 24`, using the positive whole number of hours requested. A day request uses `--days N`; `--days 3` and `--hours 72` are identical. Never combine the two flags. For a request by number of recent sessions, run `sessions --harness opencode --count <count>`; this ignores the hours window and returns the most recently updated sessions first.
|
|
16
|
+
2. Report `blockedSessions` when older unsynced turns prevent the requested window from syncing; user-selected backfill uses `--session-ref <sessionRef> --all` and keeps chronological 12-turn segments. Review each returned segment with `review --harness opencode --session-ref <sessionRef> --hours <hours>`. Use the same hours or days value for discovery and review. For count-based selection, use `review --harness opencode --session-ref <sessionRef> --all` so older sessions are allowed.
|
|
17
|
+
3. Prepare a redaction-version-four payload from final assistant notes only. Use `activity` evidence for writing, feedback, research, planning, product work, and any work that is not clearly an engineering implementation or decision. Activity evidence has no technical scope. Preserve the supported action, feature or project purpose, specific problem, approach, technologies and design patterns used, and verified result. Synthesize a descriptive project label (such as "payment reconciliation service") from its purpose; never copy a raw label that could identify a company or person. Keep distinct contributions separate, including different fixes within one feature. An empty evidence list advances the checkpoint without creating a claim.
|
|
16
18
|
4. In automatic mode, pipe the payload to `sync --harness opencode` without asking again. In manual mode, show the exact payload, ask once, and then use `sync --approved --harness opencode`.
|
|
17
19
|
5. Keep the returned session reference and turn range unchanged. Never upload prompts, raw transcripts, tool output, code, paths, commands, URLs, credentials, company or person names, customer details, or proprietary identifiers.
|
|
18
20
|
6. For a count-based sync, keep `selectedSessionRefs` from the first discovery and review only pending `sessions`. `skippedSessions` reports already-synced selections; do not replace them with older sessions. If one has remaining turns, request its next segment with `sessions --harness opencode --session-ref <sessionRef> --all`. Stop after those selected sessions are exhausted. Report completed, already-synced, and failed/pending sessions separately. Manual sync runs immediately, independently of the hourly schedule.
|
|
19
21
|
|
|
20
22
|
To connect or replace a key, run `npx --yes @useshifu/coding-harness connect --harness opencode`. Connect asks for a key with a normal visible terminal prompt and verifies it with Shifu before reporting success. Do not ask the user to paste the key into chat. If this tool terminal cannot run the command or accept input, ask the user to run it in their own interactive terminal and report the output; do not claim the connection succeeded. To disconnect locally, run `npx --yes @useshifu/coding-harness disconnect --harness opencode`. New connections use one-hour automatic sync and `~/Desktop/Shifu` on macOS by default. To change the interval, approval policy, or folder later, run `config update --harness opencode` with the desired flags (for example, `--input-folder <existing-readable-folder>`).
|
|
23
|
+
|
|
24
|
+
For an explicit user force-sync request, use `sessions --harness opencode --force` with the requested window/count. Keep its initial targets fixed. Each accepted segment includes `supersedesSyncId` and exact original boundaries. Re-review with `review --harness opencode --session-ref <sessionRef> --force --from-turn <fromTurn> --to-turn <toTurn> --supersedes-sync-id <supersedesSyncId>` using the same window or explicit `--all`. Redact the final notes again and add the returned `supersedesSyncId` to the prepared payload; send with `sync --harness opencode --force`, following the saved approval policy. New pending turns use ordinary sync. Never guess boundaries, replace a changed target, or force scheduled/file sync. Checkpoints and history are preserved; identical retries are safe. Work replacements leave AI Profile observations unchanged. Start a new chat in Shifu to use the latest work context.
|
package/package.json
CHANGED
|
@@ -5,6 +5,8 @@ description: Connect, configure, inspect, or sync coding-harness activity, AI Pr
|
|
|
5
5
|
|
|
6
6
|
# Shifu sync
|
|
7
7
|
|
|
8
|
+
Before manual or scheduled sync, the runner checks Shifu's `recommendedPackageVersion`. It automatically installs that exact release only when the installed version is older; equal or newer versions are left alone. Do not independently install npm latest. If the check or update fails, continue syncing with the current runner. Connection settings, pending work, checkpoints, and the schedule are preserved.
|
|
9
|
+
|
|
8
10
|
Shifu records useful work completed through the harness. Work may be engineering, product discovery, writing, interview feedback, research, planning, or another user-directed activity. Do not force non-engineering work into engineering categories.
|
|
9
11
|
|
|
10
12
|
Never include user prompts, raw transcripts, tool output, source code, commands, credentials, URLs, file paths, company or person names, customer details, or proprietary identifiers in a coding-session payload. Synthesize descriptive project labels from their purpose rather than copying a raw name. The local coding runner reads final assistant notes for career activity and scans user turns locally for fixed AI Profile signals; it sends neither raw source. Files placed in the user's chosen Shifu input folder are a separate path: the runner sends supported files over TLS for server-side parsing and sanitization. Do not describe folder files as locally redacted.
|
|
@@ -53,18 +55,18 @@ Show the returned list and ask for one approval for the whole list. After approv
|
|
|
53
55
|
|
|
54
56
|
## Sync now
|
|
55
57
|
|
|
56
|
-
For an explicit sync request, first read `status` to determine the saved approval mode. Manual sync runs immediately and does not wait for the hourly schedule. Use the time window the user names; otherwise use 24 hours.
|
|
58
|
+
For an explicit sync request, first read `status` to determine the saved approval mode. Manual sync runs immediately and does not wait for the hourly schedule. Use the time window the user names; otherwise use 24 hours. Use `--days N` for a positive whole number of days or `--hours N` for a positive whole number of hours. Days use exactly the same selection and review rules as hours (`--days 3` equals `--hours 72`). Never combine them or silently substitute the default window. If the user specifies a number of recent sessions, use count-based selection instead; it has no hours cutoff and orders sessions from latest to earliest. It selects the latest eligible sessions, excluding current/setup sessions, and reports already-synced selections in `skippedSessions` without replacing them with older sessions.
|
|
57
59
|
|
|
58
|
-
1. For a time-window sync, run `npx --yes @useshifu/coding-harness sessions --harness codex --hours <
|
|
59
|
-
2.
|
|
60
|
-
3. For each returned segment, run `npx --yes @useshifu/coding-harness review --harness codex --session-ref <sessionRef> --hours <
|
|
60
|
+
1. For a time-window sync, run `npx --yes @useshifu/coding-harness sessions --harness codex --hours <hours>`. For a count-based sync, run `npx --yes @useshifu/coding-harness sessions --harness codex --count <count>`; do not add `--hours` or `--days`.
|
|
61
|
+
2. Report `blockedSessions` honestly: these recently active sessions have older unsynced turns and cannot skip ahead. Explain that explicit backfill is needed. For a user-selected older backlog, add `--session-ref <sessionRef> --all`; process its original turns chronologically in segments of at most 12.
|
|
62
|
+
3. For each returned segment, run `npx --yes @useshifu/coding-harness review --harness codex --session-ref <sessionRef> --hours <hours>`. Use the same `--days` or `--hours` value for discovery and review. For count-based selection, use `--all` so the review has no time cutoff.
|
|
61
63
|
4. Prepare a version-four payload from the returned final-assistant notes. Keep the returned `sessionRef`, `fromTurn`, and `toTurn` unchanged.
|
|
62
64
|
5. In automatic mode, send it without another approval. In manual mode, show the exact payload and ask once; after approval, pass `--approved` to the sync command.
|
|
63
65
|
6. For a time-window sync, repeat discovery after accepted segments until the selected window is exhausted. For a count-based sync, keep `selectedSessionRefs` from the first discovery and review only pending `sessions`. If a selected session has remaining turns, request its next segment with `sessions --harness codex --session-ref <sessionRef> --all`; stop after those selected sessions are exhausted. Report completed, already-synced, and failed/pending sessions separately. A failed session does not count as completed.
|
|
64
66
|
|
|
65
67
|
Use `kind: "activity"` for general harness work; it does not require a technical scope. Use `implementation` or `decision` only when the note clearly supports that engineering classification, and then use the smallest reviewed scope among `unit`, `module`, `service`, `system`, and `product`. Evidence may be empty when the safe final note supports only a session-level title and summary. Never invent impact, ownership, verification, or outcomes.
|
|
66
68
|
|
|
67
|
-
Make each supported item useful for later recall: retain the user's action, the feature or project purpose, the specific problem, the approach, and a verified result when the final note supports each part. 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. Keep distinct contributions separate, including two fixes in one feature that addressed different problems. Omit names of companies and people, customer details, proprietary identifiers, paths, code snippets, 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.
|
|
69
|
+
Make each supported item useful for later recall: retain the user's action, the feature or project purpose, the specific problem, the approach, and a verified result when the final note supports each part. 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. Keep distinct contributions separate, including two fixes in one feature that addressed different problems. Omit names of companies and people, 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.
|
|
68
70
|
|
|
69
71
|
```json
|
|
70
72
|
{
|
|
@@ -94,3 +96,11 @@ printf '%s' '<redacted JSON>' | npx --yes @useshifu/coding-harness sync --approv
|
|
|
94
96
|
```
|
|
95
97
|
|
|
96
98
|
If a transport or server error remains after the runner's retries, report that the checkpoint was not advanced. Do not alter the segment or skip ahead.
|
|
99
|
+
|
|
100
|
+
## Explicit force replacement
|
|
101
|
+
|
|
102
|
+
Only when the user explicitly asks to force-sync or replace previously synced work, add `--force` to `sessions` with their requested window or count. It returns current server segments with `supersedesSyncId` and their original `fromTurn`/`toTurn`; never guess replacement ranges from local checkpoints. Force discovery lists accepted segments only. New pending turns still use ordinary sync. Keep the initial returned targets fixed rather than repeating force discovery after each replacement.
|
|
103
|
+
|
|
104
|
+
Re-review each returned target with `review --harness codex --session-ref <sessionRef> --force --from-turn <fromTurn> --to-turn <toTurn> --supersedes-sync-id <supersedesSyncId>` and the same window (or `--all` for explicitly selected backfill/count selection). The original local turns must still exist. Inspect and redact the final notes again, preserving supported work detail. Add that exact `supersedesSyncId` to the prepared payload and send with `sync --harness codex --force` (plus `--approved` only after manual approval). Preserve the original reference and turn range. If another replacement already changed the target, stop and re-discover/re-review; do not silently force over newer work.
|
|
105
|
+
|
|
106
|
+
Force replacement preserves sync history and never lowers checkpoints. Identical retries are safe. It replaces work evidence and claims for that segment; 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.
|