oh-my-opencode-cohub 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +221 -65
- package/dist/plan-gate-audit.d.ts +50 -0
- package/dist/plan-gate-audit.d.ts.map +1 -0
- package/dist/plan-gate-audit.test.d.ts +2 -0
- package/dist/plan-gate-audit.test.d.ts.map +1 -0
- package/dist/plan-gate.d.ts +17 -0
- package/dist/plan-gate.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -206,7 +206,7 @@ function registerCoHubAgents() {
|
|
|
206
206
|
→ 没有 → **立即停下来,先调用 request_plan_approval**
|
|
207
207
|
→ 新用户消息已撤销批准?→ **重新展示方案并再次调用 request_plan_approval**
|
|
208
208
|
</自检清单>
|
|
209
|
-
`, model: "deepseek/deepseek-v4-pro", variant: "max" },
|
|
209
|
+
`, model: "deepseek/deepseek-v4-pro", variant: "max", permission: { "plan-execute": "ask" } },
|
|
210
210
|
"co-oracle": { description: "战略顾问", mode: "subagent", prompt: `你是 Oracle——战略技术顾问和代码审查者。
|
|
211
211
|
|
|
212
212
|
**角色**: 高智商调试、架构决策、代码审查、简化、工程指导。
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAsIlD,QAAA,MAAM,WAAW,EAAE,MAkelB,CAAC;AAEF,eAAO,MAAM,MAAM,QAAc,CAAC;AAClC,eAAe,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -673,6 +673,13 @@ import { tool } from "@opencode-ai/plugin";
|
|
|
673
673
|
|
|
674
674
|
class PlanApprovalManager {
|
|
675
675
|
sessions = new Map;
|
|
676
|
+
audit;
|
|
677
|
+
constructor(audit) {
|
|
678
|
+
this.audit = audit;
|
|
679
|
+
}
|
|
680
|
+
getGeneration(sessionID) {
|
|
681
|
+
return this.sessions.get(sessionID)?.generation ?? 0;
|
|
682
|
+
}
|
|
676
683
|
observeUserMessage(sessionID, agent) {
|
|
677
684
|
if (agent !== "co-orchestrator") {
|
|
678
685
|
if (!this.sessions.has(sessionID)) {
|
|
@@ -680,10 +687,18 @@ class PlanApprovalManager {
|
|
|
680
687
|
}
|
|
681
688
|
}
|
|
682
689
|
const state = this.sessions.get(sessionID) ?? { generation: 0 };
|
|
690
|
+
const oldApprovedGeneration = state.approvedGeneration;
|
|
683
691
|
state.generation += 1;
|
|
684
692
|
delete state.approvedGeneration;
|
|
685
693
|
delete state.plan;
|
|
686
694
|
this.sessions.set(sessionID, state);
|
|
695
|
+
this.audit?.record({
|
|
696
|
+
event: "revoked",
|
|
697
|
+
session: sessionID.slice(0, 20),
|
|
698
|
+
generation: state.generation,
|
|
699
|
+
revoked: oldApprovedGeneration,
|
|
700
|
+
agent: agent && agent !== "co-orchestrator" ? agent : undefined
|
|
701
|
+
});
|
|
687
702
|
return true;
|
|
688
703
|
}
|
|
689
704
|
approve(sessionID, plan) {
|
|
@@ -696,6 +711,13 @@ class PlanApprovalManager {
|
|
|
696
711
|
}
|
|
697
712
|
state.approvedGeneration = state.generation;
|
|
698
713
|
state.plan = plan;
|
|
714
|
+
this.audit?.record({
|
|
715
|
+
event: "approved",
|
|
716
|
+
session: sessionID.slice(0, 20),
|
|
717
|
+
generation: state.generation,
|
|
718
|
+
fileCount: plan.files.length,
|
|
719
|
+
summaryLen: plan.summary.length
|
|
720
|
+
});
|
|
699
721
|
}
|
|
700
722
|
isApproved(sessionID) {
|
|
701
723
|
const state = this.sessions.get(sessionID);
|
|
@@ -714,6 +736,32 @@ class PlanApprovalManager {
|
|
|
714
736
|
` + (state.plan ? ` plan_summary: ${state.plan.summary.slice(0, 120)}
|
|
715
737
|
` + ` plan_files: ${state.plan.files.slice(0, 5).join(", ")}${state.plan.files.length > 5 ? "..." : ""}
|
|
716
738
|
` : "") + `</plan_gate>`;
|
|
739
|
+
}
|
|
740
|
+
getInjectionText(sessionID) {
|
|
741
|
+
const state = this.sessions.get(sessionID);
|
|
742
|
+
if (!state || state.generation === 0)
|
|
743
|
+
return null;
|
|
744
|
+
const approved = this.isApproved(sessionID);
|
|
745
|
+
const status = approved ? "已批准" : "未批准";
|
|
746
|
+
const lines = [];
|
|
747
|
+
lines.push("");
|
|
748
|
+
lines.push("--- 行为准则(本次 LLM 请求注入,不持久化) ---");
|
|
749
|
+
lines.push(`当前 generation: ${state.generation},方案状态: ${status}`);
|
|
750
|
+
lines.push("");
|
|
751
|
+
lines.push("核心规则:");
|
|
752
|
+
lines.push("1. 需要改代码 -> 先输出方案 -> todowrite -> request_plan_approval -> 等弹窗确认");
|
|
753
|
+
lines.push("2. 文件操作 -> 委派子代理,禁止亲自使用 read/grep/edit/bash/write");
|
|
754
|
+
lines.push("3. 未批准方案时禁止委派 co-fixer / co-designer");
|
|
755
|
+
lines.push("4. 每个新用户消息自动撤销旧批准");
|
|
756
|
+
lines.push("");
|
|
757
|
+
if (approved) {
|
|
758
|
+
lines.push("可委派 co-fixer / co-designer");
|
|
759
|
+
lines.push("");
|
|
760
|
+
}
|
|
761
|
+
lines.push("自检:需要改代码?-> 先方案 委派 fixer?-> 查状态锁");
|
|
762
|
+
lines.push("--- 注入结束 ---");
|
|
763
|
+
return lines.join(`
|
|
764
|
+
`);
|
|
717
765
|
}
|
|
718
766
|
cleanup(sessionID) {
|
|
719
767
|
this.sessions.delete(sessionID);
|
|
@@ -748,23 +796,42 @@ function createRequestPlanApprovalTool(planManager) {
|
|
|
748
796
|
const truncatedSummary = summary.length > 200 ? summary.slice(0, 200) + "..." : summary;
|
|
749
797
|
const truncatedFiles = files.length > 8 ? [...files.slice(0, 8), `...等 ${files.length} 个文件`] : files;
|
|
750
798
|
const truncatedVerification = verification.length > 150 ? verification.slice(0, 150) + "..." : verification;
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
metadata: {
|
|
760
|
-
session_id: sessionID,
|
|
761
|
-
generation: String(planManager["sessions"]?.get(sessionID)?.generation ?? 0),
|
|
762
|
-
summary,
|
|
763
|
-
file_count: String(files.length)
|
|
764
|
-
}
|
|
799
|
+
const currentGen = planManager.getGeneration(sessionID);
|
|
800
|
+
planManager.audit?.record({
|
|
801
|
+
event: "approval_requested",
|
|
802
|
+
session: sessionID.slice(0, 20),
|
|
803
|
+
generation: currentGen,
|
|
804
|
+
agent: "co-orchestrator",
|
|
805
|
+
fileCount: files.length,
|
|
806
|
+
summaryLen: summary.length
|
|
765
807
|
});
|
|
808
|
+
try {
|
|
809
|
+
await toolContext.ask({
|
|
810
|
+
permission: "plan-execute",
|
|
811
|
+
always: [],
|
|
812
|
+
patterns: [
|
|
813
|
+
`\uD83D\uDCCB 方案: ${truncatedSummary}`,
|
|
814
|
+
`\uD83D\uDCC1 文件 (${files.length}个): ${truncatedFiles.join(", ")}`,
|
|
815
|
+
`\uD83D\uDD0D 验证: ${truncatedVerification}`
|
|
816
|
+
],
|
|
817
|
+
metadata: {
|
|
818
|
+
session_id: sessionID,
|
|
819
|
+
generation: String(currentGen),
|
|
820
|
+
summary,
|
|
821
|
+
file_count: String(files.length)
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
} catch (err) {
|
|
825
|
+
planManager.audit?.record({
|
|
826
|
+
event: "rejected",
|
|
827
|
+
session: sessionID.slice(0, 20),
|
|
828
|
+
generation: currentGen,
|
|
829
|
+
reason: "permission_rejected"
|
|
830
|
+
});
|
|
831
|
+
throw err;
|
|
832
|
+
}
|
|
766
833
|
planManager.approve(sessionID, { summary, files, verification });
|
|
767
|
-
return `✅ 方案已批准(generation ${
|
|
834
|
+
return `✅ 方案已批准(generation ${currentGen})。
|
|
768
835
|
` + `现在可以委派 @co-fixer / @co-designer 执行。
|
|
769
836
|
` + `\uD83D\uDCCB ${summary}
|
|
770
837
|
` + `\uD83D\uDCC1 ${files.length} 个文件
|
|
@@ -774,6 +841,60 @@ function createRequestPlanApprovalTool(planManager) {
|
|
|
774
841
|
return { request_plan_approval };
|
|
775
842
|
}
|
|
776
843
|
|
|
844
|
+
// src/plan-gate-audit.ts
|
|
845
|
+
import * as fs from "node:fs";
|
|
846
|
+
import * as path from "node:path";
|
|
847
|
+
|
|
848
|
+
class BoundedPlanGateAudit {
|
|
849
|
+
events = [];
|
|
850
|
+
logPath;
|
|
851
|
+
maxEvents = 50;
|
|
852
|
+
constructor(logPath) {
|
|
853
|
+
this.logPath = logPath;
|
|
854
|
+
this.restore();
|
|
855
|
+
}
|
|
856
|
+
restore() {
|
|
857
|
+
try {
|
|
858
|
+
const data = fs.readFileSync(this.logPath, "utf-8");
|
|
859
|
+
const parsed = JSON.parse(data);
|
|
860
|
+
if (parsed && parsed.version === 1 && Array.isArray(parsed.events)) {
|
|
861
|
+
this.events = parsed.events;
|
|
862
|
+
}
|
|
863
|
+
} catch {
|
|
864
|
+
this.events = [];
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
record(e) {
|
|
868
|
+
try {
|
|
869
|
+
const event = { ...e, at: new Date().toISOString() };
|
|
870
|
+
this.events.push(event);
|
|
871
|
+
if (this.events.length > this.maxEvents) {
|
|
872
|
+
this.events = this.events.slice(-this.maxEvents);
|
|
873
|
+
}
|
|
874
|
+
this.flush();
|
|
875
|
+
} catch (err) {
|
|
876
|
+
console.warn("[BoundedPlanGateAudit] record 失败:", err);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
flush() {
|
|
880
|
+
try {
|
|
881
|
+
const data = JSON.stringify({ version: 1, events: this.events });
|
|
882
|
+
const tmpPath = this.logPath + ".tmp";
|
|
883
|
+
const dir = path.dirname(this.logPath);
|
|
884
|
+
if (!fs.existsSync(dir)) {
|
|
885
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
886
|
+
}
|
|
887
|
+
fs.writeFileSync(tmpPath, data, "utf-8");
|
|
888
|
+
try {
|
|
889
|
+
fs.unlinkSync(this.logPath);
|
|
890
|
+
} catch {}
|
|
891
|
+
fs.renameSync(tmpPath, this.logPath);
|
|
892
|
+
} catch (err) {
|
|
893
|
+
console.warn("[BoundedPlanGateAudit] flush 失败:", err);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
777
898
|
// src/context/types.ts
|
|
778
899
|
var DEFAULT_CONTEXT_CONFIG = {
|
|
779
900
|
strategy: {
|
|
@@ -849,12 +970,12 @@ function extractRelevantFiles(messages, maxFiles, windowSize) {
|
|
|
849
970
|
if (part.type === "tool" && part.state) {
|
|
850
971
|
const state = part.state;
|
|
851
972
|
const input = state.input ?? {};
|
|
852
|
-
const
|
|
853
|
-
if (
|
|
854
|
-
if (!fileMap.has(
|
|
855
|
-
fileMap.set(
|
|
973
|
+
const path2 = extractPath(input);
|
|
974
|
+
if (path2) {
|
|
975
|
+
if (!fileMap.has(path2)) {
|
|
976
|
+
fileMap.set(path2, { path: path2, summary: "" });
|
|
856
977
|
}
|
|
857
|
-
const existing = fileMap.get(
|
|
978
|
+
const existing = fileMap.get(path2);
|
|
858
979
|
if (typeof input.offset === "number") {
|
|
859
980
|
const limit = typeof input.limit === "number" ? input.limit : 50;
|
|
860
981
|
existing.lines = `${input.offset}-${input.offset + limit}`;
|
|
@@ -1177,15 +1298,15 @@ class ContextEngine {
|
|
|
1177
1298
|
}
|
|
1178
1299
|
|
|
1179
1300
|
// src/config/loader.ts
|
|
1180
|
-
import * as
|
|
1181
|
-
import * as
|
|
1301
|
+
import * as fs2 from "node:fs";
|
|
1302
|
+
import * as path2 from "node:path";
|
|
1182
1303
|
import * as os from "node:os";
|
|
1183
|
-
var CONFIG_PATH =
|
|
1304
|
+
var CONFIG_PATH = path2.join(os.homedir(), ".config", "opencode", "oh-my-opencode-cohub.json");
|
|
1184
1305
|
function loadCoHubConfig() {
|
|
1185
1306
|
try {
|
|
1186
|
-
if (!
|
|
1307
|
+
if (!fs2.existsSync(CONFIG_PATH))
|
|
1187
1308
|
return {};
|
|
1188
|
-
const raw =
|
|
1309
|
+
const raw = fs2.readFileSync(CONFIG_PATH, "utf-8");
|
|
1189
1310
|
return JSON.parse(raw);
|
|
1190
1311
|
} catch {
|
|
1191
1312
|
return {};
|
|
@@ -1222,12 +1343,12 @@ async function abortSession(client, sessionId) {
|
|
|
1222
1343
|
await client.session.abort({ path: { id: sessionId } });
|
|
1223
1344
|
} catch {}
|
|
1224
1345
|
}
|
|
1225
|
-
async function promptWithTimeout(client,
|
|
1226
|
-
const sessionId =
|
|
1346
|
+
async function promptWithTimeout(client, path3, body, timeoutMs, directory) {
|
|
1347
|
+
const sessionId = path3.id;
|
|
1227
1348
|
let timer;
|
|
1228
1349
|
try {
|
|
1229
1350
|
const promptPromise = client.session.prompt({
|
|
1230
|
-
path:
|
|
1351
|
+
path: path3,
|
|
1231
1352
|
body,
|
|
1232
1353
|
query: directory ? { directory } : undefined
|
|
1233
1354
|
});
|
|
@@ -1534,8 +1655,8 @@ function createCouncilTool(ctx, councilManager) {
|
|
|
1534
1655
|
}
|
|
1535
1656
|
|
|
1536
1657
|
// src/index.ts
|
|
1537
|
-
import * as
|
|
1538
|
-
import * as
|
|
1658
|
+
import * as fs3 from "node:fs";
|
|
1659
|
+
import * as path3 from "node:path";
|
|
1539
1660
|
import * as os2 from "node:os";
|
|
1540
1661
|
function loadFileOverrides(projectDir) {
|
|
1541
1662
|
const overrides = {};
|
|
@@ -1555,21 +1676,21 @@ function loadFileOverrides(projectDir) {
|
|
|
1555
1676
|
];
|
|
1556
1677
|
const searchDirs = [];
|
|
1557
1678
|
if (projectDir) {
|
|
1558
|
-
searchDirs.push(
|
|
1679
|
+
searchDirs.push(path3.join(projectDir, ".opencode", "oh-my-opencode-cohub"));
|
|
1559
1680
|
}
|
|
1560
|
-
searchDirs.push(
|
|
1681
|
+
searchDirs.push(path3.join(os2.homedir(), ".config", "opencode", "oh-my-opencode-cohub"));
|
|
1561
1682
|
for (const agent of agentNames) {
|
|
1562
1683
|
for (const dir of searchDirs) {
|
|
1563
|
-
const replacePath =
|
|
1564
|
-
if (!overrides[agent]?.replace &&
|
|
1684
|
+
const replacePath = path3.join(dir, `${agent}.md`);
|
|
1685
|
+
if (!overrides[agent]?.replace && fs3.existsSync(replacePath)) {
|
|
1565
1686
|
try {
|
|
1566
|
-
overrides[agent] = { ...overrides[agent], replace:
|
|
1687
|
+
overrides[agent] = { ...overrides[agent], replace: fs3.readFileSync(replacePath, "utf-8") };
|
|
1567
1688
|
} catch {}
|
|
1568
1689
|
}
|
|
1569
|
-
const appendPath =
|
|
1570
|
-
if (!overrides[agent]?.append &&
|
|
1690
|
+
const appendPath = path3.join(dir, `${agent}_append.md`);
|
|
1691
|
+
if (!overrides[agent]?.append && fs3.existsSync(appendPath)) {
|
|
1571
1692
|
try {
|
|
1572
|
-
overrides[agent] = { ...overrides[agent], append:
|
|
1693
|
+
overrides[agent] = { ...overrides[agent], append: fs3.readFileSync(appendPath, "utf-8") };
|
|
1573
1694
|
} catch {}
|
|
1574
1695
|
}
|
|
1575
1696
|
}
|
|
@@ -1598,28 +1719,30 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1598
1719
|
}
|
|
1599
1720
|
const promptOverrides = { ...configOverrides, ...fileOverrides };
|
|
1600
1721
|
const tracker = new TaskTracker;
|
|
1601
|
-
const
|
|
1722
|
+
const STATE_DIR = path3.join(os2.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub");
|
|
1723
|
+
const STATE_FILE = path3.join(STATE_DIR, "tracker-state.json");
|
|
1724
|
+
const PLAN_GATE_AUDIT_PATH = path3.join(STATE_DIR, "plan-gate-audit.json");
|
|
1725
|
+
const planAudit = new BoundedPlanGateAudit(PLAN_GATE_AUDIT_PATH);
|
|
1726
|
+
const planManager = new PlanApprovalManager(planAudit);
|
|
1602
1727
|
const planTools = createRequestPlanApprovalTool(planManager);
|
|
1603
|
-
const STATE_DIR = path2.join(os2.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub");
|
|
1604
|
-
const STATE_FILE = path2.join(STATE_DIR, "tracker-state.json");
|
|
1605
1728
|
function syncTrackerState(sessionId) {
|
|
1606
1729
|
try {
|
|
1607
|
-
if (!
|
|
1608
|
-
|
|
1730
|
+
if (!fs3.existsSync(STATE_DIR)) {
|
|
1731
|
+
fs3.mkdirSync(STATE_DIR, { recursive: true });
|
|
1609
1732
|
}
|
|
1610
1733
|
const state = {
|
|
1611
1734
|
updatedAt: Date.now(),
|
|
1612
1735
|
runningAgents: tracker.getRunningAgents(sessionId),
|
|
1613
1736
|
runningCount: tracker.getRunningCount(sessionId)
|
|
1614
1737
|
};
|
|
1615
|
-
|
|
1738
|
+
fs3.writeFileSync(STATE_FILE, JSON.stringify(state), "utf-8");
|
|
1616
1739
|
} catch {}
|
|
1617
1740
|
}
|
|
1618
|
-
const AGENT_CONFIG_FILE =
|
|
1741
|
+
const AGENT_CONFIG_FILE = path3.join(STATE_DIR, "cohub-state.json");
|
|
1619
1742
|
function syncAgentConfig() {
|
|
1620
1743
|
try {
|
|
1621
|
-
if (!
|
|
1622
|
-
|
|
1744
|
+
if (!fs3.existsSync(STATE_DIR)) {
|
|
1745
|
+
fs3.mkdirSync(STATE_DIR, { recursive: true });
|
|
1623
1746
|
}
|
|
1624
1747
|
const configs = agents.map((a) => {
|
|
1625
1748
|
const modelStr = a.config.model;
|
|
@@ -1634,7 +1757,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1634
1757
|
provider
|
|
1635
1758
|
};
|
|
1636
1759
|
});
|
|
1637
|
-
|
|
1760
|
+
fs3.writeFileSync(AGENT_CONFIG_FILE, JSON.stringify({ updatedAt: Date.now(), agents: configs }), "utf-8");
|
|
1638
1761
|
} catch {}
|
|
1639
1762
|
}
|
|
1640
1763
|
const agents = [
|
|
@@ -1806,7 +1929,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1806
1929
|
c.agent[name] = config;
|
|
1807
1930
|
}
|
|
1808
1931
|
try {
|
|
1809
|
-
|
|
1932
|
+
fs3.writeFileSync(path3.join(STATE_DIR, "config-hook-ran.json"), JSON.stringify({ ran: true, count: agents.length }));
|
|
1810
1933
|
} catch {}
|
|
1811
1934
|
},
|
|
1812
1935
|
"tool.execute.before": async (input2, output) => {
|
|
@@ -1814,9 +1937,24 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1814
1937
|
const beforeArgs = output.args ?? {};
|
|
1815
1938
|
const beforeSubagentType = typeof beforeArgs.subagent_type === "string" ? beforeArgs.subagent_type : "";
|
|
1816
1939
|
if ((beforeSubagentType === "co-fixer" || beforeSubagentType === "co-designer") && !planManager.isApproved(input2.sessionID)) {
|
|
1940
|
+
planAudit.record({
|
|
1941
|
+
event: "task_denied",
|
|
1942
|
+
session: input2.sessionID?.slice(0, 20) ?? "?",
|
|
1943
|
+
generation: planManager.getGeneration(input2.sessionID),
|
|
1944
|
+
agent: beforeSubagentType,
|
|
1945
|
+
reason: "unapproved"
|
|
1946
|
+
});
|
|
1817
1947
|
throw new Error(`[CoHub 方案批准门禁] 当前 session 尚未通过方案批准。
|
|
1818
1948
|
` + `请先:1) 输出可验证方案 → 2) todowrite 记录 → 3) 调用 request_plan_approval 弹出确认框 → ` + `4) 用户在弹窗中允许后,才能委派 ${beforeSubagentType}。`);
|
|
1819
1949
|
}
|
|
1950
|
+
if (beforeSubagentType === "co-fixer" || beforeSubagentType === "co-designer") {
|
|
1951
|
+
planAudit.record({
|
|
1952
|
+
event: "task_allowed",
|
|
1953
|
+
session: input2.sessionID?.slice(0, 20) ?? "?",
|
|
1954
|
+
generation: planManager.getGeneration(input2.sessionID),
|
|
1955
|
+
agent: beforeSubagentType
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1820
1958
|
}
|
|
1821
1959
|
try {
|
|
1822
1960
|
if (input2.tool === "task") {
|
|
@@ -1852,19 +1990,19 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1852
1990
|
if (details) {
|
|
1853
1991
|
output.args[targetField] += details;
|
|
1854
1992
|
}
|
|
1855
|
-
const logPath =
|
|
1993
|
+
const logPath = path3.join(os2.tmpdir(), "opencode", "ctx-diag.log");
|
|
1856
1994
|
try {
|
|
1857
|
-
const stat =
|
|
1995
|
+
const stat = fs3.statSync(logPath);
|
|
1858
1996
|
if (stat.size > 50 * 1024) {
|
|
1859
|
-
const lines =
|
|
1997
|
+
const lines = fs3.readFileSync(logPath, "utf-8").trim().split(`
|
|
1860
1998
|
`);
|
|
1861
|
-
|
|
1999
|
+
fs3.writeFileSync(logPath, lines.slice(-30).join(`
|
|
1862
2000
|
`) + `
|
|
1863
2001
|
`);
|
|
1864
2002
|
}
|
|
1865
2003
|
} catch {}
|
|
1866
2004
|
const finalPrompt = typeof output.args?.prompt === "string" ? output.args.prompt : "";
|
|
1867
|
-
|
|
2005
|
+
fs3.appendFileSync(logPath, JSON.stringify({
|
|
1868
2006
|
time: new Date().toISOString(),
|
|
1869
2007
|
session: input2.sessionID?.slice(0, 20) ?? "?",
|
|
1870
2008
|
subagent: subagentType,
|
|
@@ -1931,20 +2069,38 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1931
2069
|
},
|
|
1932
2070
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
1933
2071
|
try {
|
|
2072
|
+
if (!output.messages || !Array.isArray(output.messages))
|
|
2073
|
+
return;
|
|
2074
|
+
let lastUserMsg;
|
|
2075
|
+
let sessionID;
|
|
2076
|
+
for (let i = output.messages.length - 1;i >= 0; i--) {
|
|
2077
|
+
const m = output.messages[i];
|
|
2078
|
+
if (m.info.role === "user") {
|
|
2079
|
+
lastUserMsg = m;
|
|
2080
|
+
sessionID = m.info?.sessionID;
|
|
2081
|
+
break;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
if (!lastUserMsg)
|
|
2085
|
+
return;
|
|
1934
2086
|
const board = tracker.getBoardText();
|
|
1935
|
-
if (board
|
|
1936
|
-
for (let
|
|
1937
|
-
const
|
|
1938
|
-
if (
|
|
1939
|
-
|
|
1940
|
-
const part = msg.parts[j];
|
|
1941
|
-
if (part.type === "text") {
|
|
1942
|
-
part.text += `
|
|
2087
|
+
if (board) {
|
|
2088
|
+
for (let j = lastUserMsg.parts.length - 1;j >= 0; j--) {
|
|
2089
|
+
const part = lastUserMsg.parts[j];
|
|
2090
|
+
if (part.type === "text") {
|
|
2091
|
+
part.text += `
|
|
1943
2092
|
|
|
1944
2093
|
` + board;
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2094
|
+
break;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
const planInject = sessionID ? planManager.getInjectionText(sessionID) : null;
|
|
2099
|
+
if (planInject) {
|
|
2100
|
+
for (let j = lastUserMsg.parts.length - 1;j >= 0; j--) {
|
|
2101
|
+
const part = lastUserMsg.parts[j];
|
|
2102
|
+
if (part.type === "text") {
|
|
2103
|
+
part.text += planInject;
|
|
1948
2104
|
break;
|
|
1949
2105
|
}
|
|
1950
2106
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlanGate 有界审计日志
|
|
3
|
+
*
|
|
4
|
+
* 记录方案批准生命周期事件(批准请求、批准、拒绝、撤销、门禁拦截等)。
|
|
5
|
+
* 有界缓冲区(上限 50 条),原子写入(tmp + rename),
|
|
6
|
+
* 所有方法 fail-open(绝不 throw)。
|
|
7
|
+
*/
|
|
8
|
+
export interface AuditEvent {
|
|
9
|
+
/** ISO timestamp(由 record() 自动填充) */
|
|
10
|
+
at: string;
|
|
11
|
+
/** 事件类型 */
|
|
12
|
+
event: string;
|
|
13
|
+
/** sessionID 截断前 20 字符 */
|
|
14
|
+
session: string;
|
|
15
|
+
/** 事件发生时该 session 的 generation */
|
|
16
|
+
generation: number;
|
|
17
|
+
/** 被撤销的 generation(仅 revoked 事件) */
|
|
18
|
+
revoked?: number;
|
|
19
|
+
/** 代理名称(如 "co-fixer" / "co-designer") */
|
|
20
|
+
agent?: string;
|
|
21
|
+
/** 拒绝原因(如 "unapproved" / "permission_rejected") */
|
|
22
|
+
reason?: string;
|
|
23
|
+
/** 涉及文件数量 */
|
|
24
|
+
fileCount?: number;
|
|
25
|
+
/** 方案摘要长度,不记录原文 */
|
|
26
|
+
summaryLen?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare class BoundedPlanGateAudit {
|
|
29
|
+
private events;
|
|
30
|
+
private readonly logPath;
|
|
31
|
+
private readonly maxEvents;
|
|
32
|
+
/**
|
|
33
|
+
* @param logPath 审计日志文件路径(如 STATE_DIR/plan-gate-audit.json)
|
|
34
|
+
*/
|
|
35
|
+
constructor(logPath: string);
|
|
36
|
+
/** 从已有文件恢复事件 */
|
|
37
|
+
private restore;
|
|
38
|
+
/**
|
|
39
|
+
* 追加审计事件。
|
|
40
|
+
* 超过 50 条自动裁剪(保留最近 50 条),然后调用 flush()。
|
|
41
|
+
* fail-open:内部错误仅 console.warn,不 throw。
|
|
42
|
+
*/
|
|
43
|
+
record(e: Omit<AuditEvent, 'at'>): void;
|
|
44
|
+
/**
|
|
45
|
+
* 将当前缓冲区原子写入磁盘(tmp + rename)。
|
|
46
|
+
* fail-open:内部错误仅 console.warn,不 throw。
|
|
47
|
+
*/
|
|
48
|
+
flush(): void;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=plan-gate-audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-gate-audit.d.ts","sourceRoot":"","sources":["../src/plan-gate-audit.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,MAAM,WAAW,UAAU;IACzB,qCAAqC;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW;IACX,KAAK,EAAE,MAAM,CAAC;IACd,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAMD,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAM;IAEhC;;OAEG;gBACS,OAAO,EAAE,MAAM;IAS3B,gBAAgB;IAChB,OAAO,CAAC,OAAO;IAiBf;;;;OAIG;IACH,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,IAAI;IAiBvC;;;OAGG;IACH,KAAK,IAAI,IAAI;CAoBd"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-gate-audit.test.d.ts","sourceRoot":"","sources":["../src/plan-gate-audit.test.ts"],"names":[],"mappings":""}
|
package/dist/plan-gate.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* 通过 request_plan_approval 工具和 ctx.ask() 实现原生确认框门禁。
|
|
7
7
|
*/
|
|
8
8
|
import { tool } from '@opencode-ai/plugin';
|
|
9
|
+
import { BoundedPlanGateAudit } from './plan-gate-audit';
|
|
9
10
|
export interface PlanInfo {
|
|
10
11
|
summary: string;
|
|
11
12
|
files: string[];
|
|
@@ -14,6 +15,16 @@ export interface PlanInfo {
|
|
|
14
15
|
export declare class PlanApprovalManager {
|
|
15
16
|
/** sessionID → SessionPlanState */
|
|
16
17
|
private sessions;
|
|
18
|
+
/** 有界审计日志(可选) */
|
|
19
|
+
readonly audit?: BoundedPlanGateAudit;
|
|
20
|
+
/**
|
|
21
|
+
* @param audit 可选的审计日志实例
|
|
22
|
+
*/
|
|
23
|
+
constructor(audit?: BoundedPlanGateAudit);
|
|
24
|
+
/**
|
|
25
|
+
* 获取指定 session 当前 generation(用于审计记录)。
|
|
26
|
+
*/
|
|
27
|
+
getGeneration(sessionID: string): number;
|
|
17
28
|
/**
|
|
18
29
|
* 观察用户消息。仅在 orchestrator session 上建立状态。
|
|
19
30
|
* @param sessionID 当前 session ID
|
|
@@ -36,6 +47,12 @@ export declare class PlanApprovalManager {
|
|
|
36
47
|
* 仅对已登记的 orchestrator session 返回内容。
|
|
37
48
|
*/
|
|
38
49
|
getSystemContext(sessionID: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* 生成注入到消息末尾的行为准则文本(利用 recency bias 对抗注意力衰减)。
|
|
52
|
+
* 仅对已观察的 orchestrator session 返回内容,否则返回 null。
|
|
53
|
+
* 不超过 25 行,不持久化到 DB(由调用方注入到 messages.transform)。
|
|
54
|
+
*/
|
|
55
|
+
getInjectionText(sessionID: string): string | null;
|
|
39
56
|
/**
|
|
40
57
|
* 清理指定 session 的状态。
|
|
41
58
|
*/
|
package/dist/plan-gate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan-gate.d.ts","sourceRoot":"","sources":["../src/plan-gate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"plan-gate.d.ts","sourceRoot":"","sources":["../src/plan-gate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAmB,MAAM,mBAAmB,CAAC;AAM1E,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAYD,qBAAa,mBAAmB;IAC9B,mCAAmC;IACnC,OAAO,CAAC,QAAQ,CAAuC;IACvD,iBAAiB;IACjB,QAAQ,CAAC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAEtC;;OAEG;gBACS,KAAK,CAAC,EAAE,oBAAoB;IAIxC;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAIxC;;;;;OAKG;IACH,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO;IA6B9D;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI;IAwBhD;;OAEG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAMtC;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAc3C;;;;OAIG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IA2BlD;;OAEG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIhC;;OAEG;IACH,IAAI,kBAAkB,IAAI,MAAM,CAE/B;CACF;AAQD;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAC3C,WAAW,EAAE,mBAAmB,GAC/B,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC,CAiGzC"}
|