@useshifu/coding-harness 0.3.5 → 0.3.6
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 +8 -0
- package/bin/shifu-harness.js +179 -6
- package/package.json +1 -1
- package/skills/shifu-sync/SKILL.md +6 -2
package/README.md
CHANGED
|
@@ -56,3 +56,11 @@ printf '%s' '{"harness":"codex","sessionRef":"opaque-session-id","fromTurn":1,"t
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Automatic mode sends immediately. Manual mode asks once unless the calling harness already obtained approval and passes `--approved`.
|
|
59
|
+
|
|
60
|
+
## AI Profile observations
|
|
61
|
+
|
|
62
|
+
AI Profile observations are created automatically with each coding-session activity sync, under the same sync policy. The local runner reads user turns to detect a small set of behavioral signals, then sends only fixed, redacted statements and turn numbers. It never sends prompts, transcripts, tool output, code, file paths, names, or secrets for AI Profile. Model and thinking labels are included only when recognized in local harness metadata. If an observation fails, its redacted payload remains in a local retry queue while activity sync continues; an already accepted observation is kept.
|
|
63
|
+
|
|
64
|
+
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.
|
|
65
|
+
|
|
66
|
+
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.
|
package/bin/shifu-harness.js
CHANGED
|
@@ -25,6 +25,10 @@ const CLOUD_CREDENTIAL_PATTERN = /\bAKIA[0-9A-Z]{16}\b|AIza[0-9A-Za-z_-]{20,}/;
|
|
|
25
25
|
const EVIDENCE_KINDS = new Set(["implementation", "decision", "activity"]);
|
|
26
26
|
const EVIDENCE_FIELDS = new Set(["kind", "title", "statement", "scope", "area", "itemRef", "verificationRefs"]);
|
|
27
27
|
const SYNC_FIELDS = new Set(["harness", "sessionRef", "fromTurn", "toTurn", "title", "summary", "evidence", "verification", "decisions", "redactionVersion"]);
|
|
28
|
+
const AI_PROFILE_FIELDS = new Set(["harness", "sessionRef", "fromTurn", "toTurn", "category", "model", "thinking", "evidence"]);
|
|
29
|
+
const AI_PROFILE_EVIDENCE_FIELDS = new Set(["dimension", "statement", "signal", "turn"]);
|
|
30
|
+
const AI_PROFILE_DIMENSIONS = new Set(["task_selection", "goal_clarity", "context_sharing", "constraint_setting", "work_decomposition", "iterative_steering", "critical_judgment", "verification", "application", "responsible_use"]);
|
|
31
|
+
const AI_PROFILE_CATEGORIES = new Set(["system_design", "detailed_design", "implementation", "debugging_testing", "production_support", "research_analysis", "product_planning", "writing_communication", "marketing_growth", "learning", "other"]);
|
|
28
32
|
const SCOPE_LEVELS = new Set(["unit", "module", "service", "system", "product"]);
|
|
29
33
|
const WORK_AREAS = new Set(["architecture", "api", "backend", "connector", "database", "documentation", "frontend", "performance", "security", "testing", "tooling"]);
|
|
30
34
|
const ITEM_REF_PATTERN = /^[A-Za-z0-9_-]{1,80}$/;
|
|
@@ -509,12 +513,17 @@ function opencodeReview(sessionRef, fromTurn, toTurn) {
|
|
|
509
513
|
|
|
510
514
|
function reviewSegment(harness, sessionRef, fromTurn, toTurn) {
|
|
511
515
|
if (harness === "opencode") return opencodeReview(sessionRef, fromTurn, toTurn);
|
|
516
|
+
const selectedFile = sessionFile(harness, sessionRef);
|
|
517
|
+
return harness === "codex" ? codexReview(selectedFile, fromTurn, toTurn) : claudeReview(selectedFile, fromTurn, toTurn);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function sessionFile(harness, sessionRef) {
|
|
512
521
|
const files = harness === "codex" ? sessionFiles(codexSessionsRoot()) : sessionFiles(claudeSessionsRoot());
|
|
513
522
|
const selectedFile = files.map((file) => ({ file, session: harness === "codex" ? codexSession(file) : claudeSession(file) }))
|
|
514
523
|
.filter(({ session }) => session?.sessionRef === sessionRef)
|
|
515
524
|
.sort((left, right) => right.session.turnCount - left.session.turnCount || right.session.updatedAt.localeCompare(left.session.updatedAt))[0]?.file;
|
|
516
525
|
if (!selectedFile) throw new Error(`No local ${HARNESS_NAMES[harness]} session matches ${sessionRef}.`);
|
|
517
|
-
return
|
|
526
|
+
return selectedFile;
|
|
518
527
|
}
|
|
519
528
|
|
|
520
529
|
function reviewCandidates(notes) {
|
|
@@ -622,6 +631,129 @@ function payloadForSegment(harness, segment) {
|
|
|
622
631
|
};
|
|
623
632
|
}
|
|
624
633
|
|
|
634
|
+
const PROFILE_SIGNALS = [
|
|
635
|
+
{ dimension: "task_selection", pattern: /\b(?:use (?:ai|an? agent) (?:to|for)|ask (?:ai|an? agent) to|delegate .+ to (?:ai|an? agent))\b/i, statement: "User explicitly chose a task to delegate to AI." },
|
|
636
|
+
{ dimension: "goal_clarity", pattern: /\b(?:build|create|write|draft|implement|fix|design|produce)\b/i, statement: "User named a concrete deliverable or change." },
|
|
637
|
+
{ dimension: "context_sharing", pattern: /\b(?:context|background|for example|here is)\b/i, statement: "User provided background or an example for the task." },
|
|
638
|
+
{ dimension: "constraint_setting", pattern: /\b(?:must|only|without|avoid|do not|don't|should not|cannot|can't)\b/i, statement: "User stated an explicit constraint on the work." },
|
|
639
|
+
{ dimension: "work_decomposition", pattern: /(?:^|\n)\s*(?:\d+[.)]|[-*])\s|\bfirst\b[\s\S]*\bthen\b/i, statement: "User broke the request into steps or listed parts." },
|
|
640
|
+
{ dimension: "critical_judgment", pattern: /\b(?:compare|alternative|tradeoffs?|why|challenge|question the assumption)\b/i, statement: "User asked to examine a reason or alternative." },
|
|
641
|
+
{ dimension: "verification", pattern: /\b(?:test|verify|validate|check|review the diff|prove)\b/i, statement: "User requested a check or test of the result." },
|
|
642
|
+
{ dimension: "application", pattern: /\b(?:deploy|ship|publish|merge|release)\b/i, statement: "User requested that the result be put into use." },
|
|
643
|
+
{ dimension: "responsible_use", pattern: /\b(?:redact|privacy|private data|raw data|secret|credential|permission)\b/i, statement: "User called out a privacy, secret, or permission boundary." },
|
|
644
|
+
];
|
|
645
|
+
const ITERATION_PATTERN = /\b(?:instead|change|revise|adjust|fix|more|less|not what|actually)\b/i;
|
|
646
|
+
const PROFILE_CATEGORIZATION = [
|
|
647
|
+
{ category: "production_support", pattern: /\b(?:incident|outage|production support|on.call)\b/i },
|
|
648
|
+
{ category: "debugging_testing", pattern: /\b(?:debug|bug|failing test|regression|troubleshoot)\b/i },
|
|
649
|
+
{ category: "system_design", pattern: /\b(?:system design|architecture|high.level design|hld)\b/i },
|
|
650
|
+
{ category: "detailed_design", pattern: /\b(?:detailed design|low.level design|lld|api design|schema design)\b/i },
|
|
651
|
+
{ category: "marketing_growth", pattern: /\b(?:marketing|growth|campaign|conversion)\b/i },
|
|
652
|
+
{ category: "writing_communication", pattern: /\b(?:write|draft|article|post|email|feedback|documentation)\b/i },
|
|
653
|
+
{ category: "research_analysis", pattern: /\b(?:research|analy[sz]e|compare|investigate)\b/i },
|
|
654
|
+
{ category: "product_planning", pattern: /\b(?:roadmap|product plan|feature plan|prioriti[sz]e)\b/i },
|
|
655
|
+
{ category: "learning", pattern: /\b(?:learn|explain|teach|tutorial)\b/i },
|
|
656
|
+
{ category: "implementation", pattern: /\b(?:implement|build|code|refactor|frontend|backend|api|test)\b/i },
|
|
657
|
+
];
|
|
658
|
+
|
|
659
|
+
function canonicalModel(value) {
|
|
660
|
+
if (typeof value !== "string") return undefined;
|
|
661
|
+
const known = value.match(/^(gpt-(?:6-(?:astra|sol|luna)|5\.6-(?:sol|terra|luna)|5\.5)|o[134](?:-mini)?)(?:$|[-.])/i);
|
|
662
|
+
if (known) return known[1].toLowerCase();
|
|
663
|
+
const claude = value.match(/^claude-(opus|sonnet|haiku)-(\d+(?:-\d+)?)(?:$|-)/i);
|
|
664
|
+
return claude ? `claude-${claude[1].toLowerCase()}-${claude[2]}` : undefined;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function canonicalThinking(value) {
|
|
668
|
+
return ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"].includes(value) ? value : undefined;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function localProfileTurns(harness, segment) {
|
|
672
|
+
const turns = [];
|
|
673
|
+
if (harness === "opencode") {
|
|
674
|
+
const rows = opencodeQuery(`SELECT m.id AS messageId, json_extract(m.data, '$.role') AS role, json_extract(m.data, '$.modelID') AS model, p.data AS part FROM message m LEFT JOIN part p ON p.message_id = m.id WHERE m.session_id = ${JSON.stringify(segment.sessionRef)} ORDER BY m.time_created, p.id`);
|
|
675
|
+
let lastMessage;
|
|
676
|
+
let turn = 0;
|
|
677
|
+
for (const row of rows) {
|
|
678
|
+
if (row.messageId !== lastMessage) {
|
|
679
|
+
lastMessage = row.messageId;
|
|
680
|
+
if (row.role === "user") { turn += 1; turns.push({ turn, text: "" }); }
|
|
681
|
+
if (row.role === "assistant" && turn > 0) turns[turn - 1].model ||= canonicalModel(row.model);
|
|
682
|
+
}
|
|
683
|
+
if (row.role !== "user" || turn === 0 || typeof row.part !== "string") continue;
|
|
684
|
+
try { const part = JSON.parse(row.part); if (part.type === "text" && typeof part.text === "string") turns[turn - 1].text += part.text; } catch {}
|
|
685
|
+
}
|
|
686
|
+
} else if (harness === "claude_code") {
|
|
687
|
+
let turn = 0;
|
|
688
|
+
for (const entry of claudeEntries(sessionFile(harness, segment.sessionRef))) {
|
|
689
|
+
if (isClaudeUserTurn(entry)) {
|
|
690
|
+
turn += 1;
|
|
691
|
+
const content = entry.message.content;
|
|
692
|
+
turns.push({ turn, text: typeof content === "string" ? content : content.filter((part) => part.type === "text").map((part) => part.text).join("\n") });
|
|
693
|
+
} else if (entry.type === "assistant" && turn > 0) {
|
|
694
|
+
turns[turn - 1].model ||= canonicalModel(entry.message?.model);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
let turn = 0;
|
|
699
|
+
for (const line of fs.readFileSync(sessionFile(harness, segment.sessionRef), "utf8").split("\n")) {
|
|
700
|
+
if (!line) continue;
|
|
701
|
+
const entry = JSON.parse(line);
|
|
702
|
+
if (entry.type === "response_item" && entry.payload?.type === "message" && entry.payload.role === "user") {
|
|
703
|
+
turn += 1;
|
|
704
|
+
turns.push({ turn, text: entry.payload.content?.filter((part) => part.type === "input_text").map((part) => part.text).join("\n") || "" });
|
|
705
|
+
} else if (entry.type === "turn_context" && turn > 0) {
|
|
706
|
+
turns[turn - 1].model ||= canonicalModel(entry.payload?.model);
|
|
707
|
+
turns[turn - 1].thinking ||= canonicalThinking(entry.payload?.effort);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return turns.filter(({ turn }) => turn >= segment.fromTurn && turn <= segment.toTurn);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function profileForSegment(harness, segment) {
|
|
715
|
+
const turns = localProfileTurns(harness, segment);
|
|
716
|
+
if (turns.length !== segment.toTurn - segment.fromTurn + 1) throw new Error("The AI Profile source turns were not found locally; the segment remains pending.");
|
|
717
|
+
const evidence = [];
|
|
718
|
+
const observed = new Set();
|
|
719
|
+
const categories = [];
|
|
720
|
+
for (const { turn, text } of turns) {
|
|
721
|
+
for (const signal of PROFILE_SIGNALS) {
|
|
722
|
+
if (!observed.has(signal.dimension) && signal.pattern.test(text)) {
|
|
723
|
+
evidence.push({ dimension: signal.dimension, statement: signal.statement, signal: "demonstrated", turn });
|
|
724
|
+
observed.add(signal.dimension);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (turn > 1 && !observed.has("iterative_steering") && ITERATION_PATTERN.test(text)) {
|
|
728
|
+
evidence.push({ dimension: "iterative_steering", statement: "User gave a follow-up direction to change the work.", signal: "demonstrated", turn });
|
|
729
|
+
observed.add("iterative_steering");
|
|
730
|
+
}
|
|
731
|
+
const category = PROFILE_CATEGORIZATION.find((item) => item.pattern.test(text))?.category;
|
|
732
|
+
if (category) categories.push(category);
|
|
733
|
+
}
|
|
734
|
+
const models = [...new Set(turns.map((turn) => turn.model).filter(Boolean))];
|
|
735
|
+
const thinking = [...new Set(turns.map((turn) => turn.thinking).filter(Boolean))];
|
|
736
|
+
const observation = {
|
|
737
|
+
harness, sessionRef: segment.sessionRef, fromTurn: segment.fromTurn, toTurn: segment.toTurn,
|
|
738
|
+
category: categories[0] || "other", evidence,
|
|
739
|
+
};
|
|
740
|
+
if (models.length === 1 && turns.every((turn) => turn.model)) observation.model = models[0];
|
|
741
|
+
if (thinking.length === 1 && turns.every((turn) => turn.thinking)) observation.thinking = thinking[0];
|
|
742
|
+
validateGeneratedProfile(observation, harness);
|
|
743
|
+
return observation;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function validateGeneratedProfile(profile, harness) {
|
|
747
|
+
validateAIProfileObservation(profile, harness);
|
|
748
|
+
const statements = new Map(PROFILE_SIGNALS.map((signal) => [signal.dimension, signal.statement]));
|
|
749
|
+
statements.set("iterative_steering", "User gave a follow-up direction to change the work.");
|
|
750
|
+
if (profile.evidence.some((item) => item.signal !== "demonstrated" || statements.get(item.dimension) !== item.statement) ||
|
|
751
|
+
(profile.model !== undefined && canonicalModel(profile.model) !== profile.model) ||
|
|
752
|
+
(profile.thinking !== undefined && canonicalThinking(profile.thinking) !== profile.thinking)) {
|
|
753
|
+
throw new Error("Only locally generated AI Profile signals may be synced.");
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
625
757
|
function unsyncedCodexSessions(harness, hours, turns = MAX_TURNS_PER_SEGMENT, selectedRef, includeOlder = false) {
|
|
626
758
|
if (![24, 48, 72].includes(hours)) throw new Error("Choose --hours 24, 48, or 72.");
|
|
627
759
|
if (!Number.isInteger(turns) || turns < 1 || turns > MAX_TURNS_PER_SEGMENT) throw new Error("Choose --turns between 1 and 12.");
|
|
@@ -1062,6 +1194,16 @@ function validateSync(input, harness) {
|
|
|
1062
1194
|
if (input.redactionVersion !== 2 && input.redactionVersion !== 3 && input.redactionVersion !== REDACTION_VERSION) throw new Error(`Use redactionVersion ${REDACTION_VERSION} for new syncs.`);
|
|
1063
1195
|
}
|
|
1064
1196
|
|
|
1197
|
+
function validateAIProfileObservation(input, harness) {
|
|
1198
|
+
if (!input || typeof input !== "object" || Object.keys(input).some((field) => !AI_PROFILE_FIELDS.has(field)) || input.harness !== harness || typeof input.sessionRef !== "string" || !/^[A-Za-z0-9_-]{8,200}$/.test(input.sessionRef)) throw new Error("Use a reviewed AI Profile observation for this harness and an opaque session reference.");
|
|
1199
|
+
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 range of at most 12 turns.");
|
|
1200
|
+
if (!AI_PROFILE_CATEGORIES.has(input.category)) throw new Error("Choose a supported AI Profile category.");
|
|
1201
|
+
for (const field of ["model", "thinking"]) {
|
|
1202
|
+
if (input[field] !== undefined && (typeof input[field] !== "string" || input[field].length > (field === "model" ? 100 : 40) || /[\x00-\x1f\x7f\\@`<>]/.test(input[field]) || input[field].includes("://") || ["secret", "sk-", "ghp_"].some((part) => input[field].toLowerCase().includes(part)))) throw new Error(`Use safe ${field} metadata or omit it.`);
|
|
1203
|
+
}
|
|
1204
|
+
if (!Array.isArray(input.evidence) || input.evidence.length > 20 || !input.evidence.every((item) => item && typeof item === "object" && Object.keys(item).every((field) => AI_PROFILE_EVIDENCE_FIELDS.has(field)) && AI_PROFILE_DIMENSIONS.has(item.dimension) && ["demonstrated", "limiting"].includes(item.signal) && Number.isInteger(item.turn) && item.turn >= input.fromTurn && item.turn <= input.toTurn && textIsSafe(item.statement, 240))) throw new Error("Use at most 20 redacted, turn-cited AI-use observations; omit unsupported claims.");
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1065
1207
|
function validSyncReceipt(receipt, input) {
|
|
1066
1208
|
return receipt?.sessionRef === input.sessionRef && receipt.acceptedToTurn === input.toTurn && Number.isInteger(receipt.lastSyncedTurn) &&
|
|
1067
1209
|
(receipt.duplicate ? receipt.lastSyncedTurn >= input.toTurn : receipt.lastSyncedTurn === input.toTurn);
|
|
@@ -1106,6 +1248,28 @@ async function postSync(config, input) {
|
|
|
1106
1248
|
throw lastError;
|
|
1107
1249
|
}
|
|
1108
1250
|
|
|
1251
|
+
async function flushPendingProfiles(harness, config) {
|
|
1252
|
+
const pending = readState(harness).profilePending || {};
|
|
1253
|
+
for (const [key, profile] of Object.entries(pending)) {
|
|
1254
|
+
try {
|
|
1255
|
+
validateGeneratedProfile(profile, harness);
|
|
1256
|
+
const response = await fetch(`${config.apiUrl}/v1/connectors/coding-sessions/ai-profile-observations`, {
|
|
1257
|
+
method: "POST",
|
|
1258
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.token}` },
|
|
1259
|
+
body: JSON.stringify(profile),
|
|
1260
|
+
signal: AbortSignal.timeout(30_000),
|
|
1261
|
+
});
|
|
1262
|
+
const receipt = await response.json().catch(() => undefined);
|
|
1263
|
+
if (response.status !== 409 && (!response.ok || receipt?.data?.accepted !== true)) throw new Error(`HTTP ${response.status}`);
|
|
1264
|
+
const state = readState(harness);
|
|
1265
|
+
delete state.profilePending[key];
|
|
1266
|
+
saveState(harness, state);
|
|
1267
|
+
} catch {
|
|
1268
|
+
console.error(`AI Profile observation for ${profile.sessionRef} turns ${profile.fromTurn}–${profile.toTurn} remains pending.`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1109
1273
|
function scanWorkFiles(folder) {
|
|
1110
1274
|
const entries = fs.readdirSync(validateInputFolder(folder), { withFileTypes: true })
|
|
1111
1275
|
.filter((entry) => entry.isFile() && entry.name !== INPUT_FOLDER_README_NAME);
|
|
@@ -1289,12 +1453,16 @@ async function sendSync(harness, input, config = readConfig(harness)) {
|
|
|
1289
1453
|
const checkpoint = state.sessions[input.sessionRef];
|
|
1290
1454
|
const expected = (checkpoint?.lastSyncedTurn || 0) + 1;
|
|
1291
1455
|
if (input.fromTurn !== expected) throw new Error(`This local checkpoint expects turn ${expected}. Review the pending segment before retrying.`);
|
|
1456
|
+
const profile = profileForSegment(harness, input);
|
|
1292
1457
|
const receipt = await postSync(config, input);
|
|
1293
1458
|
if (!validSyncReceipt(receipt, input)) {
|
|
1294
1459
|
throw new Error("Shifu returned a checkpoint that does not match this reviewed segment. Local state was not advanced.");
|
|
1295
1460
|
}
|
|
1461
|
+
state.profilePending ||= {};
|
|
1462
|
+
state.profilePending[`${profile.sessionRef}:${profile.fromTurn}:${profile.toTurn}`] = profile;
|
|
1296
1463
|
state.sessions[input.sessionRef] = { turnsSinceSync: 0, lastSyncedAt: Date.now(), lastSyncedTurn: receipt.lastSyncedTurn, syncDue: false };
|
|
1297
1464
|
saveState(harness, state);
|
|
1465
|
+
await flushPendingProfiles(harness, config);
|
|
1298
1466
|
console.error(receipt.duplicate ? "Shifu already had this exact segment. Local checkpoint recovered." : `Synced through turn ${receipt.lastSyncedTurn}.`);
|
|
1299
1467
|
return receipt;
|
|
1300
1468
|
}
|
|
@@ -1449,6 +1617,7 @@ async function scheduledSync(harness) {
|
|
|
1449
1617
|
console.error(`Shifu input folder remains pending: ${error.message}`);
|
|
1450
1618
|
}
|
|
1451
1619
|
}
|
|
1620
|
+
await flushPendingProfiles(harness, config);
|
|
1452
1621
|
if (policy.approvalMode === "manual") {
|
|
1453
1622
|
const queue = rebuildPendingQueue(harness);
|
|
1454
1623
|
const pendingFingerprint = createHash("sha256").update(queue.segments.map((segment) => `${segment.sessionRef}:${segment.fromTurn}:${segment.toTurn}`).join("\n")).digest("hex");
|
|
@@ -1466,19 +1635,21 @@ async function scheduledSync(harness) {
|
|
|
1466
1635
|
notificationError = error?.message || "Could not create the approval session.";
|
|
1467
1636
|
}
|
|
1468
1637
|
}
|
|
1638
|
+
const pendingProfiles = Object.keys(readState(harness).profilePending || {}).length;
|
|
1469
1639
|
state.schedule = {
|
|
1470
1640
|
...state.schedule,
|
|
1471
1641
|
lastAttemptAt: startedAt,
|
|
1472
|
-
lastResult: notificationError ? "awaiting_approval_notification_failed" : failedFiles ? "partial_failure" : "awaiting_approval",
|
|
1642
|
+
lastResult: notificationError ? "awaiting_approval_notification_failed" : failedFiles || pendingProfiles ? "partial_failure" : "awaiting_approval",
|
|
1473
1643
|
submittedFiles,
|
|
1474
1644
|
pendingFiles,
|
|
1475
1645
|
failedFiles,
|
|
1476
1646
|
unsupportedFiles,
|
|
1647
|
+
pendingProfiles,
|
|
1477
1648
|
pendingSegments: queue.segments.length,
|
|
1478
1649
|
pendingFingerprint,
|
|
1479
1650
|
approvalSessionRef,
|
|
1480
1651
|
notificationError,
|
|
1481
|
-
nextRunAt: startedAt + (notificationError || failedFiles ? SCHEDULER_TICK_MINUTES / 60 : policy.intervalHours) * 3_600_000,
|
|
1652
|
+
nextRunAt: startedAt + (notificationError || failedFiles || pendingProfiles ? SCHEDULER_TICK_MINUTES / 60 : policy.intervalHours) * 3_600_000,
|
|
1482
1653
|
};
|
|
1483
1654
|
} else {
|
|
1484
1655
|
const blockedSessions = new Set();
|
|
@@ -1493,16 +1664,18 @@ async function scheduledSync(harness) {
|
|
|
1493
1664
|
console.error(`${HARNESS_NAMES[harness]} session ${segment.sessionRef} remains pending: ${error.message}`);
|
|
1494
1665
|
}
|
|
1495
1666
|
}
|
|
1667
|
+
const pendingProfiles = Object.keys(readState(harness).profilePending || {}).length;
|
|
1496
1668
|
state.schedule = {
|
|
1497
1669
|
lastAttemptAt: startedAt,
|
|
1498
|
-
lastResult: failures || failedFiles ? "partial_failure" : "complete",
|
|
1670
|
+
lastResult: failures || failedFiles || pendingProfiles ? "partial_failure" : "complete",
|
|
1499
1671
|
syncedSegments: synced,
|
|
1500
1672
|
failedSessions: failures,
|
|
1501
1673
|
submittedFiles,
|
|
1502
1674
|
pendingFiles,
|
|
1503
1675
|
failedFiles,
|
|
1504
1676
|
unsupportedFiles,
|
|
1505
|
-
|
|
1677
|
+
pendingProfiles,
|
|
1678
|
+
nextRunAt: startedAt + (failures || failedFiles || pendingProfiles ? SCHEDULER_TICK_MINUTES / 60 : policy.intervalHours) * 3_600_000,
|
|
1506
1679
|
};
|
|
1507
1680
|
}
|
|
1508
1681
|
const latest = readState(harness);
|
|
@@ -1610,4 +1783,4 @@ if (require.main === module) {
|
|
|
1610
1783
|
});
|
|
1611
1784
|
}
|
|
1612
1785
|
|
|
1613
|
-
module.exports = { approvalNotificationInvocation, claudeReview, claudeSession, codexReview, codexSession, configPath, configuredSyncPolicy, connect, defaultInputFolder, disconnect, finalAssistantNote, install, installDestination, localSessions, normalizedPolicy, notificationSessionRef, opencodeQuery, opencodeReview, opencodeSessions, payloadForSegment, pendingSegments, prepareInputFolder, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, schedulerArtifact, statePath, syncWorkFiles, textIsSafe, unsyncedCodexSessions, validateConnection, validateInputFolder, validSyncReceipt, validateApprovalMode, validateIntervalHours, validateSync, workFiles };
|
|
1786
|
+
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 };
|
package/package.json
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: shifu-sync
|
|
3
|
-
description: Connect, configure, inspect, or sync coding-harness activity and local work files to Shifu. Use when the user asks to connect Shifu, change sync settings, inspect pending activity, approve a queued coding sync, or sync recent harness work or input-folder files.
|
|
3
|
+
description: Connect, configure, inspect, or sync coding-harness activity, AI Profile observations, and local work files to Shifu. Use when the user asks to connect Shifu, change sync settings, inspect pending activity, approve a queued coding sync, or sync recent harness work, AI-use evidence, or input-folder files.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Shifu sync
|
|
7
7
|
|
|
8
8
|
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
9
|
|
|
10
|
-
Never include user prompts, raw transcripts, tool output, source code, commands, credentials, URLs, file paths, names, customer details, or proprietary identifiers in a coding-session payload. The local coding runner reads final assistant notes
|
|
10
|
+
Never include user prompts, raw transcripts, tool output, source code, commands, credentials, URLs, file paths, names, customer details, or proprietary identifiers in a coding-session payload. 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.
|
|
11
11
|
|
|
12
12
|
Only describe work attributable to the user. A team action item, meeting discussion, or another person's contribution is not evidence that the user did it. If a final note does not support a specific user contribution, do not invent one.
|
|
13
13
|
|
|
14
|
+
## AI Profile observations
|
|
15
|
+
|
|
16
|
+
Every accepted coding-session sync also queues a locally derived AI Profile observation for the same turns. No separate command or approval is needed. The runner sends only fixed statements for detected behavior and recognized model metadata, with no raw prompt text. Unsupported dimensions remain unscored. If profile scoring is unavailable, the redacted observation remains local for the next sync while activity sync continues.
|
|
17
|
+
|
|
14
18
|
## Connect or reconfigure
|
|
15
19
|
|
|
16
20
|
When the user asks to connect, run:
|