@shgroup/dsh-serenity-hooks 1.48.1 → 1.48.2
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/dsh.plugin.json +1 -1
- package/lib/git-ops.d.ts +17 -3
- package/lib/index.js +197 -65
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.48.
|
|
3
|
+
"version": "1.48.2",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag;后两个按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.5-rc.2(deepseek-ai/deepseek-harness)。",
|
|
6
6
|
"engines": {
|
package/lib/git-ops.d.ts
CHANGED
|
@@ -2,13 +2,23 @@
|
|
|
2
2
|
* git-ops.ts — container_git 纯操作层(cc_git → container_git,v1.30;零 DSH 依赖)
|
|
3
3
|
*
|
|
4
4
|
* 行为对齐 osp(opencode-serenity-plugin/src/git/cc-git-tool.ts)——osp 是 ACC 工具 spec:
|
|
5
|
-
* - status:JSON {clean, files:[{status,file}], summary}
|
|
6
|
-
* - commit
|
|
7
|
-
* - push
|
|
5
|
+
* - status:JSON {clean, files:[{status,file}], summary, ahead, behind}
|
|
6
|
+
* - commit:默认「只提交指定 paths」;未给 paths ⇒ 整仓 add -A(并在回执里显式告知)
|
|
7
|
+
* - commit:**默认随后 push**(`noPush` 可关);push 结果如实附在回执里
|
|
8
|
+
* - push:成功判据 = **`git push --porcelain` 的 stdout 里读到 ref 行**(绝不采信"stderr 为空")
|
|
8
9
|
* - pull:git fetch + merge --ff-only;up-to-date / [REJECTED] 建议
|
|
9
10
|
* - log:--oneline [-n <count>]
|
|
10
11
|
* - diff:git diff [--cached] [<ref>] [-- <path>]
|
|
11
12
|
* 保留 dsp 增强:localstore git 合规联动(S134:deny 且 .gitignore 未覆盖 → status 提示 / commit 拒绝)。
|
|
13
|
+
*
|
|
14
|
+
* 🔴 2026-09-26 修(tiangong-serenity 报障):**push 曾把"超时被杀"报成成功**。
|
|
15
|
+
* 机制(**实测**,非推断):`spawnSync` 超时**不设 `err.killed`**(实测 `code=ETIMEDOUT` /
|
|
16
|
+
* `signal=SIGTERM` / `status=null` / `stderr` 长度 0),而 `execFileSync` **成功时丢弃 stderr**;
|
|
17
|
+
* 于是"超时被杀"与"真成功"都交出 `{stdout:'', stderr:''}` —— 旧判据 `if (!stderr) → 成功`
|
|
18
|
+
* 把两者判成同一件事,回显 `Pushed to origin/<branch>` 而远程 ref 从未变。
|
|
19
|
+
* ⇒ 本版三处硬修:① `git()` 交出**结构化结果**(ok/timedOut/status),守卫一律看它,不再看
|
|
20
|
+
* "stderr 是否为空";② push 的成功判据改为**可核验事实**(`--porcelain` 的 ref 行);
|
|
21
|
+
* ③ 超时/未确认一律**不报成功**,并给出 `git ls-remote` 核验指令。
|
|
12
22
|
*/
|
|
13
23
|
import type { JsonValue } from './json.js';
|
|
14
24
|
type GitAction = 'status' | 'commit' | 'push' | 'log' | 'pull' | 'diff';
|
|
@@ -16,6 +26,10 @@ export declare const GIT_ACTIONS: readonly GitAction[];
|
|
|
16
26
|
interface GitArgs {
|
|
17
27
|
action: GitAction;
|
|
18
28
|
message?: string;
|
|
29
|
+
/** commit:只提交这些路径(不传 ⇒ 整仓 `add -A`) */
|
|
30
|
+
paths?: string[];
|
|
31
|
+
/** commit:只提交到本地、不随后推送(默认提交后自动推送) */
|
|
32
|
+
noPush?: boolean;
|
|
19
33
|
/** log 条数(对齐 osp 参数 n,默认 10,max 100) */
|
|
20
34
|
count?: number;
|
|
21
35
|
/** diff: 显示暂存区变更(--cached) */
|
package/lib/index.js
CHANGED
|
@@ -611,13 +611,23 @@ function createKitTool(ctx) {
|
|
|
611
611
|
* git-ops.ts — container_git 纯操作层(cc_git → container_git,v1.30;零 DSH 依赖)
|
|
612
612
|
*
|
|
613
613
|
* 行为对齐 osp(opencode-serenity-plugin/src/git/cc-git-tool.ts)——osp 是 ACC 工具 spec:
|
|
614
|
-
* - status:JSON {clean, files:[{status,file}], summary}
|
|
615
|
-
* - commit
|
|
616
|
-
* - push
|
|
614
|
+
* - status:JSON {clean, files:[{status,file}], summary, ahead, behind}
|
|
615
|
+
* - commit:默认「只提交指定 paths」;未给 paths ⇒ 整仓 add -A(并在回执里显式告知)
|
|
616
|
+
* - commit:**默认随后 push**(`noPush` 可关);push 结果如实附在回执里
|
|
617
|
+
* - push:成功判据 = **`git push --porcelain` 的 stdout 里读到 ref 行**(绝不采信"stderr 为空")
|
|
617
618
|
* - pull:git fetch + merge --ff-only;up-to-date / [REJECTED] 建议
|
|
618
619
|
* - log:--oneline [-n <count>]
|
|
619
620
|
* - diff:git diff [--cached] [<ref>] [-- <path>]
|
|
620
621
|
* 保留 dsp 增强:localstore git 合规联动(S134:deny 且 .gitignore 未覆盖 → status 提示 / commit 拒绝)。
|
|
622
|
+
*
|
|
623
|
+
* 🔴 2026-09-26 修(tiangong-serenity 报障):**push 曾把"超时被杀"报成成功**。
|
|
624
|
+
* 机制(**实测**,非推断):`spawnSync` 超时**不设 `err.killed`**(实测 `code=ETIMEDOUT` /
|
|
625
|
+
* `signal=SIGTERM` / `status=null` / `stderr` 长度 0),而 `execFileSync` **成功时丢弃 stderr**;
|
|
626
|
+
* 于是"超时被杀"与"真成功"都交出 `{stdout:'', stderr:''}` —— 旧判据 `if (!stderr) → 成功`
|
|
627
|
+
* 把两者判成同一件事,回显 `Pushed to origin/<branch>` 而远程 ref 从未变。
|
|
628
|
+
* ⇒ 本版三处硬修:① `git()` 交出**结构化结果**(ok/timedOut/status),守卫一律看它,不再看
|
|
629
|
+
* "stderr 是否为空";② push 的成功判据改为**可核验事实**(`--porcelain` 的 ref 行);
|
|
630
|
+
* ③ 超时/未确认一律**不报成功**,并给出 `git ls-remote` 核验指令。
|
|
621
631
|
*/
|
|
622
632
|
const GIT_ACTIONS = [
|
|
623
633
|
"status",
|
|
@@ -627,48 +637,132 @@ const GIT_ACTIONS = [
|
|
|
627
637
|
"pull",
|
|
628
638
|
"diff"
|
|
629
639
|
];
|
|
630
|
-
/**
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
640
|
+
/** 本地动作超时(ms) */
|
|
641
|
+
const DEFAULT_LOCAL_TIMEOUT_MS = 3e4;
|
|
642
|
+
/** 网络动作(fetch/push/pull)超时(ms)——大体量推送可能远超 30s(tiangong 实例:>60s) */
|
|
643
|
+
const DEFAULT_NET_TIMEOUT_MS = 18e4;
|
|
644
|
+
/**
|
|
645
|
+
* 超时可覆盖(`DSH_localTimeout()` / `DSH_netTimeout()`)。
|
|
646
|
+
* 🔵 两个用途:① 慢远端/大体量推送的用户可自行放宽 ② 测试可在**不真等 180s** 的前提下
|
|
647
|
+
* 驱动"超时"这条路径(本仓对它的回归测试就靠这个缝)。
|
|
648
|
+
* ⚠️ **每次调用现读**(不在模块加载时固化)——否则测试改了 env 也影响不到已加载的模块。
|
|
649
|
+
*/
|
|
650
|
+
function timeoutMs(envName, dflt) {
|
|
651
|
+
const v = Number(process.env[envName] ?? "");
|
|
652
|
+
return Number.isFinite(v) && v > 0 ? v : dflt;
|
|
653
|
+
}
|
|
654
|
+
const localTimeout = () => timeoutMs("DSH_GIT_TIMEOUT_MS", DEFAULT_LOCAL_TIMEOUT_MS);
|
|
655
|
+
const netTimeout = () => timeoutMs("DSH_GIT_NET_TIMEOUT_MS", DEFAULT_NET_TIMEOUT_MS);
|
|
656
|
+
/**
|
|
657
|
+
* 执行 git 并交出**结构化结果**。
|
|
658
|
+
* 🔴 超时判据必须用 `code==='ETIMEDOUT' || signal!=null && status==null`——
|
|
659
|
+
* `spawnSync` 超时**不设 `err.killed`**(2026-09-26 实测;旧守卫 `err.killed ? … : err.stderr`
|
|
660
|
+
* 因此是死代码,超时被当成"stderr 为空 = 成功")。
|
|
661
|
+
*/
|
|
662
|
+
function git(root, args, timeoutMs = localTimeout()) {
|
|
634
663
|
try {
|
|
664
|
+
const stdout = execFileSync("git", args, {
|
|
665
|
+
cwd: root,
|
|
666
|
+
encoding: "utf8",
|
|
667
|
+
stdio: [
|
|
668
|
+
"ignore",
|
|
669
|
+
"pipe",
|
|
670
|
+
"pipe"
|
|
671
|
+
],
|
|
672
|
+
maxBuffer: 1048576,
|
|
673
|
+
timeout: timeoutMs
|
|
674
|
+
});
|
|
635
675
|
return {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
"pipe",
|
|
642
|
-
"pipe"
|
|
643
|
-
],
|
|
644
|
-
maxBuffer: 1048576,
|
|
645
|
-
timeout: GIT_TIMEOUT_MS
|
|
646
|
-
}).trimEnd(),
|
|
647
|
-
stderr: ""
|
|
676
|
+
ok: true,
|
|
677
|
+
stdout: String(stdout).trimEnd(),
|
|
678
|
+
stderr: "",
|
|
679
|
+
timedOut: false,
|
|
680
|
+
status: 0
|
|
648
681
|
};
|
|
649
682
|
} catch (err) {
|
|
683
|
+
const timedOut = err?.killed === true || err?.code === "ETIMEDOUT" || err?.signal != null && err?.status == null;
|
|
650
684
|
return {
|
|
651
|
-
|
|
652
|
-
|
|
685
|
+
ok: false,
|
|
686
|
+
stdout: (err?.stdout?.toString() ?? "").trimEnd(),
|
|
687
|
+
stderr: (err?.stderr?.toString() ?? "").trimEnd(),
|
|
688
|
+
timedOut,
|
|
689
|
+
status: typeof err?.status === "number" ? err.status : null
|
|
653
690
|
};
|
|
654
691
|
}
|
|
655
692
|
}
|
|
656
693
|
function getCurrentBranch(root) {
|
|
657
|
-
const
|
|
694
|
+
const r = git(root, [
|
|
658
695
|
"rev-parse",
|
|
659
696
|
"--abbrev-ref",
|
|
660
697
|
"HEAD"
|
|
661
698
|
]);
|
|
662
|
-
if (!stdout) throw new Error("container_git: cannot determine current branch");
|
|
663
|
-
return stdout;
|
|
699
|
+
if (!r.ok || !r.stdout) throw new Error("container_git: cannot determine current branch");
|
|
700
|
+
return r.stdout;
|
|
664
701
|
}
|
|
665
702
|
function hasChanges(root) {
|
|
666
|
-
|
|
667
|
-
|
|
703
|
+
return git(root, [
|
|
704
|
+
"-c",
|
|
705
|
+
"core.quotepath=false",
|
|
706
|
+
"status",
|
|
707
|
+
"--porcelain"
|
|
708
|
+
]).stdout.length > 0;
|
|
668
709
|
}
|
|
669
710
|
function hasRemote(root) {
|
|
670
|
-
|
|
671
|
-
|
|
711
|
+
return git(root, ["remote"]).stdout.length > 0;
|
|
712
|
+
}
|
|
713
|
+
/** 推送成功与否的**唯一**核验指令(给使用者,也写进回执) */
|
|
714
|
+
function verifyHint(branch) {
|
|
715
|
+
return `🔴 核验(唯一判据):git ls-remote origin ${branch},或 git fetch && git rev-parse origin/${branch} —— 与本地 HEAD 比对。**不要采信任何回执。**`;
|
|
716
|
+
}
|
|
717
|
+
const REJECTED_TEXT = (branch) => `[REJECTED] Push to origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local branch is behind. Suggested actions:\n 1. Use bash: git fetch origin ${branch}\n 2. Inspect remote changes: git log HEAD..origin/${branch}\n 3. Merge or rebase: git merge origin/${branch} or git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add ... && git commit\n 5. Push again: container_git push`;
|
|
718
|
+
/**
|
|
719
|
+
* 推送并把结果**如实**翻成文本(`hard` = 作为独立动作时应抛错)。
|
|
720
|
+
* 🔴 成功判据 = `--porcelain` 的 **stdout 里有 ref 行**;退出码 0 但读不到 ref 行 ⇒ **不视为成功**。
|
|
721
|
+
*/
|
|
722
|
+
function pushCore(root) {
|
|
723
|
+
if (!hasRemote(root)) return {
|
|
724
|
+
text: "⚠️ 未配置 remote ⇒ **未推送**(本地提交不受影响)。加 remote: git remote add origin <url>",
|
|
725
|
+
hard: true
|
|
726
|
+
};
|
|
727
|
+
const branch = getCurrentBranch(root);
|
|
728
|
+
git(root, [
|
|
729
|
+
"fetch",
|
|
730
|
+
"origin",
|
|
731
|
+
branch
|
|
732
|
+
], netTimeout());
|
|
733
|
+
const r = git(root, [
|
|
734
|
+
"push",
|
|
735
|
+
"--porcelain",
|
|
736
|
+
"origin",
|
|
737
|
+
branch
|
|
738
|
+
], netTimeout());
|
|
739
|
+
if (r.timedOut) return {
|
|
740
|
+
text: `[TIMEOUT] git push 在 ${netTimeout() / 1e3}s 内未结束 —— 🔴 **推送是否成功【未确认】**(既未成功也未失败)。\n原始输出: ${r.stdout || "(空)"}\n${r.stderr ? `stderr: ${r.stderr}\n` : ""}${verifyHint(branch)}`,
|
|
741
|
+
hard: true
|
|
742
|
+
};
|
|
743
|
+
const refLines = r.stdout.split("\n").filter((l) => /^[ =*+!-]\t/.test(l));
|
|
744
|
+
if (refLines.some((l) => l.startsWith("!"))) return {
|
|
745
|
+
text: REJECTED_TEXT(branch),
|
|
746
|
+
hard: false
|
|
747
|
+
};
|
|
748
|
+
const updated = refLines.filter((l) => /^[ *+-]/.test(l));
|
|
749
|
+
const upToDate = refLines.length > 0 && refLines.every((l) => l.startsWith("="));
|
|
750
|
+
if (updated.length > 0 && !upToDate) return {
|
|
751
|
+
text: `✅ 已推送 origin/${branch}\n${updated.join("\n")}`,
|
|
752
|
+
hard: false
|
|
753
|
+
};
|
|
754
|
+
if (upToDate) return {
|
|
755
|
+
text: `origin/${branch} 已是最新(无可推送的新提交)\n${refLines.join("\n")}`,
|
|
756
|
+
hard: false
|
|
757
|
+
};
|
|
758
|
+
if (!r.ok) return {
|
|
759
|
+
text: `[FAILED] git push 退出码 ${r.status ?? "未知"} —— **未推送**\n${r.stderr || r.stdout || "(无输出)"}\n${verifyHint(branch)}`,
|
|
760
|
+
hard: true
|
|
761
|
+
};
|
|
762
|
+
return {
|
|
763
|
+
text: `[UNVERIFIED] git push 退出码 0,但**未从 --porcelain 读到 ref 行** ⇒ 🔴 **不视为成功**。\n原始输出: ${r.stdout || "(空)"}\n${verifyHint(branch)}`,
|
|
764
|
+
hard: true
|
|
765
|
+
};
|
|
672
766
|
}
|
|
673
767
|
function runGit(root, args) {
|
|
674
768
|
switch (args.action) {
|
|
@@ -679,7 +773,8 @@ function runGit(root, args) {
|
|
|
679
773
|
"status",
|
|
680
774
|
"--porcelain"
|
|
681
775
|
]);
|
|
682
|
-
if (r.
|
|
776
|
+
if (r.timedOut) throw new Error(`status timed out (${localTimeout() / 1e3}s)`);
|
|
777
|
+
if (!r.ok) throw new Error(`status failed: ${r.stderr || r.stdout || "(no output)"}`);
|
|
683
778
|
const files = (r.stdout ? r.stdout.split("\n") : []).map((line) => {
|
|
684
779
|
return {
|
|
685
780
|
status: line.slice(0, 2).trim() || "??",
|
|
@@ -691,6 +786,21 @@ function runGit(root, args) {
|
|
|
691
786
|
files,
|
|
692
787
|
summary: files.length === 0 ? "(clean)" : `${files.length} file(s) with changes`
|
|
693
788
|
};
|
|
789
|
+
if (hasRemote(root)) try {
|
|
790
|
+
const branch = getCurrentBranch(root);
|
|
791
|
+
const rr = git(root, [
|
|
792
|
+
"rev-list",
|
|
793
|
+
"--left-right",
|
|
794
|
+
"--count",
|
|
795
|
+
`origin/${branch}...HEAD`
|
|
796
|
+
]);
|
|
797
|
+
if (rr.ok && /^\d+\s+\d+$/.test(rr.stdout)) {
|
|
798
|
+
const [behind, ahead] = rr.stdout.split(/\s+/).map(Number);
|
|
799
|
+
out.ahead = ahead;
|
|
800
|
+
out.behind = behind;
|
|
801
|
+
if (ahead > 0) out.summary = `${out.summary} | 🔴 本地领先 origin/${branch} **${ahead} 笔未推送**`;
|
|
802
|
+
}
|
|
803
|
+
} catch {}
|
|
694
804
|
const ls = checkLocalstoreGitCompliance(root);
|
|
695
805
|
if (!ls.ok) out.warning = ls.reason;
|
|
696
806
|
return out;
|
|
@@ -699,36 +809,44 @@ function runGit(root, args) {
|
|
|
699
809
|
const ls = checkLocalstoreGitCompliance(root);
|
|
700
810
|
if (!ls.ok) throw new Error(ls.reason);
|
|
701
811
|
if (!args.message || args.message.trim() === "") throw new Error("container_git commit: missing required arg \"message\"");
|
|
702
|
-
|
|
703
|
-
const
|
|
704
|
-
if (
|
|
705
|
-
const
|
|
812
|
+
const paths = (args.paths ?? []).map((p) => p.trim()).filter((p) => p !== "");
|
|
813
|
+
const scoped = paths.length > 0;
|
|
814
|
+
if (!scoped && !hasChanges(root)) return "(nothing to commit — working tree clean)";
|
|
815
|
+
const addR = git(root, scoped ? [
|
|
816
|
+
"add",
|
|
817
|
+
"--",
|
|
818
|
+
...paths
|
|
819
|
+
] : ["add", "-A"]);
|
|
820
|
+
if (addR.timedOut) throw new Error(`container_git commit: git add timed out (${localTimeout() / 1e3}s)`);
|
|
821
|
+
if (!addR.ok) throw new Error(`container_git commit: git add failed\n${addR.stderr || addR.stdout}`);
|
|
822
|
+
const commitR = git(root, scoped ? [
|
|
823
|
+
"commit",
|
|
824
|
+
"-m",
|
|
825
|
+
args.message,
|
|
826
|
+
"--",
|
|
827
|
+
...paths
|
|
828
|
+
] : [
|
|
706
829
|
"commit",
|
|
707
830
|
"-m",
|
|
708
831
|
args.message
|
|
709
832
|
]);
|
|
710
|
-
if (
|
|
711
|
-
if (
|
|
712
|
-
|
|
833
|
+
if (commitR.timedOut) throw new Error(`container_git commit: git commit timed out (${localTimeout() / 1e3}s)`);
|
|
834
|
+
if (`${commitR.stdout}\n${commitR.stderr}`.includes("nothing to commit")) return "(nothing to commit — working tree clean)";
|
|
835
|
+
if (!commitR.ok) throw new Error(`container_git commit: git commit failed\n${commitR.stderr || commitR.stdout}`);
|
|
836
|
+
const lines = [(commitR.stdout || commitR.stderr || "committed").trimEnd()];
|
|
837
|
+
if (!scoped) lines.push("ℹ️ 本次是**整仓**提交(未传 `paths`)—— 包含工作树里**所有**改动,可能含其它轨迹的在途文件。要只提交指定路径 ⇒ 传 `paths`。");
|
|
838
|
+
if (args.noPush) {
|
|
839
|
+
lines.push("⚠️ 按 `noPush` 只提交到**本地** —— 尚未推送到 origin。推送:container_git push");
|
|
840
|
+
return lines.join("\n");
|
|
841
|
+
}
|
|
842
|
+
const p = pushCore(root);
|
|
843
|
+
lines.push(p.hard ? `🔴 **已本地提交,但未推送** ——\n${p.text}` : p.text);
|
|
844
|
+
return lines.join("\n");
|
|
713
845
|
}
|
|
714
846
|
case "push": {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
git(root, [
|
|
719
|
-
"fetch",
|
|
720
|
-
"origin",
|
|
721
|
-
branch
|
|
722
|
-
]);
|
|
723
|
-
} catch {}
|
|
724
|
-
const { stdout, stderr } = git(root, [
|
|
725
|
-
"push",
|
|
726
|
-
"origin",
|
|
727
|
-
branch
|
|
728
|
-
]);
|
|
729
|
-
if (stderr.includes("non-fast-forward") || stderr.includes("rejected") || stderr.includes("[rejected]")) return `[REJECTED] Push to origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local branch is behind. Suggested actions:\n 1. Use bash: git fetch origin ${branch}\n 2. Inspect remote changes: git log HEAD..origin/${branch}\n 3. Merge or rebase: git merge origin/${branch} or git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add ... && git commit\n 5. Push again: container_git push`;
|
|
730
|
-
if (!stderr || stderr.includes("->") || stderr === "") return stdout || `Pushed to origin/${branch}`;
|
|
731
|
-
throw new Error(`container_git push failed:\n${stderr}`);
|
|
847
|
+
const p = pushCore(root);
|
|
848
|
+
if (p.hard) throw new Error(p.text);
|
|
849
|
+
return p.text;
|
|
732
850
|
}
|
|
733
851
|
case "pull": {
|
|
734
852
|
if (!hasRemote(root)) throw new Error("container_git pull: no remote configured. Add one with:\n git remote add origin <url>");
|
|
@@ -737,26 +855,29 @@ function runGit(root, args) {
|
|
|
737
855
|
"fetch",
|
|
738
856
|
"origin",
|
|
739
857
|
branch
|
|
740
|
-
]);
|
|
741
|
-
if (fetchResult.
|
|
858
|
+
], netTimeout());
|
|
859
|
+
if (fetchResult.timedOut) return `[TIMEOUT] git fetch 在 ${netTimeout() / 1e3}s 内未结束 —— **是否已拉到【未确认】**;未做合并。`;
|
|
860
|
+
if (!fetchResult.ok) return `[WARN] fetch failed:\n${fetchResult.stderr || fetchResult.stdout}`;
|
|
742
861
|
const revResult = git(root, [
|
|
743
862
|
"rev-list",
|
|
744
863
|
"--count",
|
|
745
864
|
"HEAD..FETCH_HEAD"
|
|
746
865
|
]);
|
|
747
|
-
if (revResult.
|
|
866
|
+
if (!revResult.ok) return `[WARN] cannot check ahead count:\n${revResult.stderr || revResult.stdout}`;
|
|
748
867
|
if (revResult.stdout === "0" || revResult.stdout === "") return "Already up to date.";
|
|
749
868
|
const mergeResult = git(root, [
|
|
750
869
|
"merge",
|
|
751
870
|
"--ff-only",
|
|
752
871
|
"FETCH_HEAD"
|
|
753
872
|
]);
|
|
754
|
-
if (
|
|
873
|
+
if (mergeResult.timedOut) return `[TIMEOUT] git merge 在 ${localTimeout() / 1e3}s 内未结束 —— **合并是否完成【未确认】**。`;
|
|
874
|
+
if (mergeResult.ok) {
|
|
755
875
|
const msg = mergeResult.stdout || "Pulled successfully.";
|
|
756
876
|
return msg.endsWith("\n") ? msg.trimEnd() : msg;
|
|
757
877
|
}
|
|
758
|
-
|
|
759
|
-
|
|
878
|
+
const mErr = `${mergeResult.stderr}\n${mergeResult.stdout}`;
|
|
879
|
+
if (mErr.includes("non-fast-forward") || mErr.includes("Not possible to fast-forward") || mErr.includes("rejected") || mErr.includes("could not be applied")) return `[REJECTED] Pull from origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local history diverged from remote (non-fast-forward). Suggested actions:\n 1. Inspect differences: container_git log HEAD..origin/${branch}\n 2. Merge manually with bash: git merge origin/${branch}\n 3. Or rebase: git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add <file> && git commit\n 5. Push: container_git push`;
|
|
880
|
+
throw new Error(`container_git pull failed:\n${mergeResult.stderr || mergeResult.stdout}`);
|
|
760
881
|
}
|
|
761
882
|
case "log": {
|
|
762
883
|
const n = Math.min(Math.max(args.count ?? 10, 1), 100);
|
|
@@ -768,7 +889,8 @@ function runGit(root, args) {
|
|
|
768
889
|
"-n",
|
|
769
890
|
String(n)
|
|
770
891
|
]);
|
|
771
|
-
if (r.
|
|
892
|
+
if (r.timedOut) throw new Error(`git log timed out (${localTimeout() / 1e3}s)`);
|
|
893
|
+
if (!r.ok) throw new Error(`git log failed: ${r.stderr || r.stdout || "(no output)"}`);
|
|
772
894
|
if (!r.stdout) return "(no commits)";
|
|
773
895
|
return r.stdout;
|
|
774
896
|
}
|
|
@@ -777,10 +899,11 @@ function runGit(root, args) {
|
|
|
777
899
|
if (args.staged) diffArgs.push("--cached");
|
|
778
900
|
if (args.ref) diffArgs.push(args.ref);
|
|
779
901
|
if (args.path) diffArgs.push("--", args.path);
|
|
780
|
-
const
|
|
781
|
-
if (
|
|
782
|
-
if (!
|
|
783
|
-
return
|
|
902
|
+
const r = git(root, diffArgs);
|
|
903
|
+
if (r.timedOut) throw new Error(`git diff timed out (${localTimeout() / 1e3}s)`);
|
|
904
|
+
if (!r.ok) return `[WARN] git diff failed:\n${r.stderr || r.stdout || "(no output)"}`;
|
|
905
|
+
if (!r.stdout) return "(no diff)";
|
|
906
|
+
return r.stdout;
|
|
784
907
|
}
|
|
785
908
|
default: throw new Error(`Unknown action: ${args.action}`);
|
|
786
909
|
}
|
|
@@ -798,7 +921,7 @@ function renderText$7(value) {
|
|
|
798
921
|
}
|
|
799
922
|
const gitTool = defineTool({
|
|
800
923
|
name: "container_git",
|
|
801
|
-
description: "CCC git operations (cc-git semantics, aligned with osp). status/commit/push/log/pull/diff
|
|
924
|
+
description: "CCC git operations (cc-git semantics, aligned with osp). status/commit/push/log/pull/diff. commit 默认**随后自动 push**(noPush 可关);commit 传 paths ⇒ 只提交这些路径(不传=整仓 add -A,回执会显式告知)。push 成功判据 = --porcelain 的 ref 行(**超时/未确认一律不报成功**,回执给 `git ls-remote` 核验指令);非快进 push/pull 输出 [REJECTED] + 建议(never auto-force)。merge/rebase/conflict resolution go through bash.",
|
|
802
925
|
parameters: {
|
|
803
926
|
action: {
|
|
804
927
|
type: "string",
|
|
@@ -810,6 +933,15 @@ const gitTool = defineTool({
|
|
|
810
933
|
type: "string",
|
|
811
934
|
description: "Commit message (required for commit)"
|
|
812
935
|
},
|
|
936
|
+
paths: {
|
|
937
|
+
type: "array",
|
|
938
|
+
items: { type: "string" },
|
|
939
|
+
description: "commit: 只提交这些路径(如 [\"docs/a.md\",\"src/b.ts\"])。不传 = 整仓 `git add -A`(会吸走其它轨迹的在途文件,回执会显式提示)"
|
|
940
|
+
},
|
|
941
|
+
noPush: {
|
|
942
|
+
type: "boolean",
|
|
943
|
+
description: "commit: 只提交到本地,不随后推送(默认提交后自动推送)"
|
|
944
|
+
},
|
|
813
945
|
count: {
|
|
814
946
|
type: "integer",
|
|
815
947
|
description: "Log count (default 10, max 100; runtime clamp 1-100)"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.48.
|
|
3
|
+
"version": "1.48.2",
|
|
4
4
|
"description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.7-rc.1。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|