dsh-completion-guard 0.5.0 → 0.5.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.
@@ -614,7 +614,7 @@ const ORDERED_TEXT_RULES = [
614
614
  ["verify", /验证|确认|确保|\bverif(?:y|ies|ied|ying)\b|\bconfirm\b/i]
615
615
  ];
616
616
  function semanticActionFromText(text) {
617
- if (/^\s*(?:验证|确认|确保|verif(?:y|ies|ied|ying)\b|confirm\b)/i.test(text)) return "verify";
617
+ if (/^\s*(?:验证|校验|确认|确保|核对|verif(?:y|ies|ied|ying)\b|confirm\b)/i.test(text)) return "verify";
618
618
  for (const [action, pattern] of ORDERED_TEXT_RULES) if (pattern.test(text)) return action;
619
619
  return "generic_run";
620
620
  }
@@ -776,21 +776,1048 @@ function npmEscapedPackageName(packageId) {
776
776
  }
777
777
 
778
778
  //#endregion
779
- //#region src/domain/capture.ts
780
- const CLAUSE_PATTERNS = [["prohibition", /^(?:(?:do not|don't|never)(?![A-Za-z0-9_./@\\-])|禁止|不要|不得)\s*(.+)$/i], ["acceptance", /^(?:verify|confirm|ensure|验收|确认|确保)\s*(.+)$/i]];
781
- function classifyClause(text) {
782
- const normalizedText = normalizeClause(text);
783
- for (const [kind, pattern] of CLAUSE_PATTERNS) {
784
- const match = normalizedText.match(pattern);
785
- if (match) return {
786
- kind,
787
- body: normalizeClause(match[1].replace(/^[::,,\s]+/, ""))
779
+ //#region src/domain/semantics.ts
780
+ /** A clause whose head verb demands a verification rather than a change. */
781
+ const ACCEPTANCE_LEAD = /^(?:验收|验证|确认|确保|核对|检查|verify|confirm|ensure|check)/i;
782
+ /**
783
+ * The contract kind a scope maps to. A prohibition and an acceptance keep their
784
+ * own lanes; everything else is a requirement. Acceptance is decided from the
785
+ * clause's own head verb, so "确保构建通过" stays an acceptance while a
786
+ * conditional or prohibition clause is never mislabelled.
787
+ */
788
+ function kindOfScope(directive, body = "") {
789
+ if (directive === "prohibition") return "prohibition";
790
+ if (directive === "directive" && ACCEPTANCE_LEAD.test(body.trim())) return "acceptance";
791
+ return "requirement";
792
+ }
793
+ /**
794
+ * Blank out inline-code spans while preserving every byte offset, so a caller
795
+ * can classify authority against masked text and still slice the original.
796
+ * Backticks are Markdown emphasis, but they are also how a log line or a
797
+ * command is quoted — and a quoted command is data, never an order.
798
+ */
799
+ const MASK_CACHE = /* @__PURE__ */ new Map();
800
+ const MASK_CACHE_LIMIT = 64;
801
+ function maskCodeSpans(text) {
802
+ const cached = MASK_CACHE.get(text);
803
+ if (cached !== void 0) return cached;
804
+ const masked = computeMaskedSpans(text);
805
+ if (MASK_CACHE.size >= MASK_CACHE_LIMIT) MASK_CACHE.clear();
806
+ MASK_CACHE.set(text, masked);
807
+ return masked;
808
+ }
809
+ function computeMaskedSpans(text) {
810
+ const characters = text.split("");
811
+ let cursor = 0;
812
+ while (cursor < text.length) {
813
+ if (text[cursor] !== "`") {
814
+ cursor += 1;
815
+ continue;
816
+ }
817
+ const end = text.indexOf("`", cursor + 1);
818
+ if (end < 0) break;
819
+ for (let index = cursor; index <= end; index += 1) characters[index] = " ";
820
+ cursor = end + 1;
821
+ }
822
+ return characters.join("");
823
+ }
824
+ /** Actions Guard can name, shared with the command-surface manifest. */
825
+ /**
826
+ * The manifest's operation verbs. Each alternative is wrapped with word
827
+ * boundaries, so an English verb never matches inside another word ("check"
828
+ * inside "change"); a Chinese alternative is left alone because a Han character
829
+ * has no word boundary to assert.
830
+ */
831
+ const CJK_VERBS = [
832
+ "创建",
833
+ "生成",
834
+ "新建",
835
+ "写入",
836
+ "修改",
837
+ "编辑",
838
+ "更改",
839
+ "读取",
840
+ "阅读",
841
+ "打开",
842
+ "验证",
843
+ "校验",
844
+ "确认",
845
+ "确保",
846
+ "检查",
847
+ "核对",
848
+ "运行",
849
+ "执行",
850
+ "拉取",
851
+ "同步",
852
+ "更新",
853
+ "下载",
854
+ "安装",
855
+ "部署",
856
+ "上传",
857
+ "提交",
858
+ "推送",
859
+ "发布",
860
+ "升级",
861
+ "重启",
862
+ "重新启动",
863
+ "重载",
864
+ "合并",
865
+ "继续",
866
+ "撤销",
867
+ "删除"
868
+ ];
869
+ /** Every CJK action word, longest first so 重新启动 wins over 新. */
870
+ const CJK_VERB_PATTERN = `(?:${[...CJK_VERBS].sort((a, b) => b.length - a.length).join("|")})`;
871
+ /** The same words as literal strings, for exact scanning without regex escapes. */
872
+ const CJK_VERB_WORDS = [...CJK_VERBS].sort((a, b) => b.length - a.length);
873
+ const ACTION_VERB_PATTERN = `(?:${COMMAND_SURFACE_MANIFEST.operationVerbs.map((entry) => entry.pattern.split("|").map((alternative) => /^[A-Za-z]/.test(alternative.trim()) ? `\\b${alternative.trim()}\\b` : alternative.trim()).join("|")).join("|")}|${CJK_VERB_PATTERN})`;
874
+ const ACTION_VERB = new RegExp(ACTION_VERB_PATTERN, "i");
875
+ /** Operation verbs beyond the guard action surface (local work and diagnosis). */
876
+ const WORK_VERB = /创建|生成|新建|写入|修改|编辑|运行|执行|编写|撰写|部署|安装|升级|提交|下载|上传|拉取|同步|重启|测试|检查|验证|确认|修复|更新|清理|整理|记录|构建|编译|重构|迁移|删除|回滚|发布|推送|合并|继续|恢复|还原|回滚|实现|\b(?:build|create|write|modify|change|edit|run|fix|update|install|push|publish|test|verify|check|commit|deploy|migrate|remove|delete|restart|revert|refactor|inspect|fetch|pull|implement)\b/i;
877
+ /** Explanatory framings: an action named afterwards is an object, not an order. */
878
+ const EXPLAIN_VERB = /解释|说明|讲解|介绍|阐述|分析|讨论|描述|科普|什么意思|是什么意思|有什么(?:作用|影响|区别)|\bexplain\b|\bdescribe\b|\bclarify\b|\bwhat\s+does\b|\bwhat\s+is\b|\bhow\s+does\b|\bmeaning\s+of\b/i;
879
+ /** Interrogative framings that make a scope a question rather than an order. */
880
+ const QUESTION_SCOPE = /[??]|是否|是不是|为什么|为何|怎么|如何|什么|哪些|哪一种|能否|可否|要不要|该不该|由谁|是谁|\b(?:whether|which|why|should|could|would)\b/i;
881
+ const NEGATORS = [
882
+ ["不要", "zh"],
883
+ ["不用", "zh"],
884
+ ["不得", "zh"],
885
+ ["不许", "zh"],
886
+ ["不准", "zh"],
887
+ ["不能", "zh"],
888
+ ["不必", "zh"],
889
+ ["无需", "zh"],
890
+ ["毋须", "zh"],
891
+ ["勿", "zh"],
892
+ ["别", "zh"],
893
+ ["甭", "zh"],
894
+ ["不", "zh"],
895
+ ["do not", "en"],
896
+ ["does not", "en"],
897
+ ["did not", "en"],
898
+ ["don't", "en"],
899
+ ["doesn't", "en"],
900
+ ["won't", "en"],
901
+ ["can't", "en"],
902
+ ["cannot", "en"],
903
+ ["never", "en"],
904
+ ["avoid", "en"],
905
+ ["without", "en"],
906
+ ["no longer", "en"]
907
+ ];
908
+ /** Characters that end one coordinated scope and may begin the next. */
909
+ const SEPARATORS = new Set([
910
+ ",",
911
+ ",",
912
+ "、",
913
+ ";",
914
+ ";",
915
+ "。",
916
+ ".",
917
+ "!",
918
+ "!",
919
+ "?",
920
+ "?",
921
+ ":",
922
+ ":",
923
+ "\n",
924
+ "\r"
925
+ ]);
926
+ const CONNECTORS = [
927
+ "但是",
928
+ "不过",
929
+ "然而",
930
+ "同时",
931
+ "并且",
932
+ "而且",
933
+ "以及",
934
+ "然后",
935
+ "接着",
936
+ "而是",
937
+ "但",
938
+ "而",
939
+ "也",
940
+ "并",
941
+ "且",
942
+ "又",
943
+ "再",
944
+ "就",
945
+ "则"
946
+ ];
947
+ const ENGLISH_CONNECTORS = [
948
+ "but",
949
+ "and",
950
+ "then",
951
+ "also",
952
+ "however",
953
+ "yet"
954
+ ];
955
+ const CONNECTOR_PATTERN = `(?:${[...CONNECTORS].sort((a, b) => b.length - a.length).join("|")}|${ENGLISH_CONNECTORS.join("|")})`;
956
+ const CONTINUATION_AFTER_SEPARATOR = new RegExp(`^\\s*${CONNECTOR_PATTERN}`, "i");
957
+ /**
958
+ * Instruction openings that make the text after a bare conjunction its own
959
+ * clause. "并检查 GUI 效果" is a second instruction; "并在本地仓库记录" is
960
+ * handled separately as a locative, and anything else stays one object list.
961
+ */
962
+ const CROSS_CLAUSE_HEAD = /^\s*(?:检查|查看|确认|验证|测试|运行|执行|安装|应用|更新|升级|记录|提交|推送|发布|部署|重启|重新启动|创建|新建|生成|修改|编辑|拉取|抓取|删除|回滚|清理|整理|实现|完成)/u;
963
+ /**
964
+ * Openings that make the text after a conjunction a DISTINCT instruction rather
965
+ * than the second half of one action. "并确认全部通过" completes the action
966
+ * before it, so 确认 is deliberately absent here.
967
+ */
968
+ const DISTINCT_CLAUSE_HEAD = /^\s*(?:检查|查看|测试|验证|运行|执行|安装|应用|更新|升级|提交|推送|发布|部署|重启|重新启动|创建|新建|生成|修改|编辑|拉取|抓取|删除|回滚|清理|整理)/u;
969
+ /**
970
+ * A place clause that follows a coordinating conjunction: the shape of "并在
971
+ * 本地仓库记录", where the conjunction joins an action to where it happens
972
+ * rather than to a second action.
973
+ */
974
+ const LOCATIVE_CLAUSE = /^\s*在.{1,40}?(?:记录|保存|写入)$/u;
975
+ const USER_ACTOR_PATTERNS = [
976
+ /(?:由|让|给|请)\s*(?:我|本人|我们)/,
977
+ /(?:我|我们)(?:自己|本人)?\s*(?:来|去|会|将|要)?\s*(?:手动|亲自|自行)?\s*(?:重启|重新启动|升级|安装|更新|执行|运行|操作|完成|处理|部署|发布|推送|合并|确认|登录|审批|提供|准备|搭建|检查|验证|测试)/,
978
+ /\bI(?:'ll| will| am going to| myself)\b/i,
979
+ /\b(?:on my own|by myself)\b/i
980
+ ];
981
+ const AGENT_ACTOR_PATTERNS = [
982
+ /(?:由|让|请)\s*(?:你|您|助手|代理)/,
983
+ /(?:你|您)(?:来|去|会|将|要|负责|自己)/,
984
+ /\byou (?:should|must|need to|will|are to)\b/i
985
+ ];
986
+ const OUTPUT_NOUN = /命令|脚本|指令|步骤|清单|说明|文档|模板|command|script|instructions?|checklist|snippet/i;
987
+ const OUTPUT_REQUEST = /(?:给|帮|替|为)(?:我|我们)?\s*(?:写|生成|整理|列|准备|提供|输出|来)|生成(?:一|两|几)?(?:条|个|份)|输出(?:一|个|份)?|列出|列一(?:下|个)|\b(?:provide|write|generate|outline|list|draft)\b|give\s+me/i;
988
+ const CONDITION_MARKERS = [
989
+ ["如果", "prefix"],
990
+ ["假如", "prefix"],
991
+ ["倘若", "prefix"],
992
+ ["若是", "prefix"],
993
+ ["一旦", "prefix"],
994
+ ["除非", "prefix"],
995
+ ["只有", "prefix"],
996
+ ["只要", "prefix"],
997
+ ["等到", "prefix"],
998
+ ["若", "prefix"],
999
+ ["在", "prefix"],
1000
+ ["if", "prefix"],
1001
+ ["unless", "prefix"],
1002
+ ["once", "prefix"],
1003
+ ["when", "prefix"],
1004
+ ["provided that", "prefix"],
1005
+ ["after", "prefix"],
1006
+ ["之后", "suffix"],
1007
+ ["以后", "suffix"],
1008
+ ["才", "suffix"],
1009
+ ["再", "suffix"]
1010
+ ];
1011
+ const RESUME_MARKER = /(?:收到|得到|等到|等待|经)\s*.{0,12}?(?:明确|显式|最终)?\s*(?:回报|回复|答复|确认|批准|同意|授权|指示|通知)|(?:我|用户)(?:明确|最终)?\s*(?:确认|回复|回报|批准|同意|授权)(?:后再|之后|后|以后)?|after\s+(?:I|the user)\s+(?:confirm|reply|approve|authorize)|once\s+(?:I|the user)\s+(?:confirm|reply|approve)|waiting\s+for\s+(?:the\s+)?(?:user|you)/i;
1012
+ /**
1013
+ * The resumption event itself, without the request prefix a scope may open
1014
+ * with. Used to locate the event inside a scope rather than at its start.
1015
+ */
1016
+ const RESUMPTION_EVENT = /(?:收到|得到|等到|等待)\s*.{0,12}?(?:确认|回复|回报|批准|同意|授权|指示|通知)\s*(?:后再|之后|后|以后|再)|(?:我|用户)(?:明确|最终)?\s*(?:确认|回复|回报|批准|同意|授权)\s*(?:后再|之后|后|以后|再)|(?:after|once)\s+(?:I|the user)\s+(?:confirm|reply|approve|authorize)|waiting\s+for\s+(?:the\s+)?(?:user|you)/i;
1017
+ /**
1018
+ * A scope that OPENS with the resumption event it waits on. Anchored at the
1019
+ * start and greedy, so the match runs to the end of the event itself
1020
+ * ("收到我的确认后"): the condition is what the scope says after it.
1021
+ */
1022
+ const RESUME_SCOPE_MARKER = /^(?:请在|请|麻烦|帮我|需要你|务必)?\s*(?:(?:收到|得到|等到|等待)\s*.{0,12}?(?:确认|回复|回报|批准|同意|授权|指示|通知)\s*(?:后再|之后|后|以后|再)|(?:我|用户)(?:明确|最终)?\s*(?:确认|回复|回报|批准|同意|授权)\s*(?:后再|之后|后|以后|再)|(?:after|once)\s+(?:I|the user)\s+(?:confirm|reply|approve|authorize)|waiting\s+for\s+(?:the\s+)?(?:user|you))/i;
1023
+ const NARRATIVE_PAST = /(?:已经|已|刚刚|刚才|此前|之前)(?:经)?(?:推送|发布|提交|安装|升级|重启|合并|完成|修改|更新|删除|创建|写入)|\b(?:already|have|has|had)\s+(?:been\s+)?(?:pushed|published|committed|installed|upgraded|restarted|merged|completed|finished|modified|updated)\b/i;
1024
+ /**
1025
+ * Completion aspects that turn a clause into a report: a verb finished with
1026
+ * 了/过/完了/好了 states what happened, so it orders nothing. A directive never
1027
+ * carries them ("修改 README" is an order, "修改了 README" is a report).
1028
+ */
1029
+ const NARRATIVE_ASPECT = /(?:完了|好了|过了)|(?:已经|已|刚刚|刚才|此前|之前)[\p{Script=Han}]{0,4}(?:了|过)|\b(?:was|were|has been|have been)\b/iu;
1030
+ const NARRATIVE_DIRECTIVE = /请|需要你|帮我|麻烦|务必|\b(?:please|must)\b/i;
1031
+ const UNRESOLVED_SCOPE = /^(?:看看|看一下|瞅瞅|研究一下|了解|随便|maybe|perhaps|somehow|figure\s+out)/i;
1032
+ /**
1033
+ * A completed confirmation receipt: the root reports that the event it was
1034
+ * waiting for already happened. It reserves nothing, so it must not mint a
1035
+ * wait, and it is not work either.
1036
+ */
1037
+ const CONFIRMATION_RECEIPT = /^(?:我)?\s*(?:已|已经)?\s*(?:收到|得到|等到|等待)(?:了|过)?\s*(?:我|你|您|用户)?\s*的?\s*.{0,12}?(?:确认|回复|回报|批准|同意|授权|指示|通知)\s*(?:了|啦|过|收到)\s*[。..!!]?$/u;
1038
+ const SENTENCE_END = new Set([
1039
+ "。",
1040
+ "!",
1041
+ "?",
1042
+ "!",
1043
+ "?",
1044
+ "\n",
1045
+ "\r"
1046
+ ]);
1047
+ function isWordBoundary(text, index) {
1048
+ if (index <= 0) return true;
1049
+ return !/[\p{L}\p{N}_]/u.test(text[index - 1]);
1050
+ }
1051
+ /**
1052
+ * The first negator in `text` at or after `from`.
1053
+ *
1054
+ * A multi-word English negator is matched at BOTH of its words ("do not"), so a
1055
+ * caller scanning for a negated verb does not have to know where the phrase
1056
+ * began. `index` 0 is always a boundary; later positions are boundaries only
1057
+ * when the preceding character is not a word character.
1058
+ */
1059
+ function firstNegation(text, from = 0) {
1060
+ for (let cursor = from; cursor < text.length; cursor += 1) {
1061
+ const token = negatorAt(text, cursor);
1062
+ if (token) return {
1063
+ index: cursor,
1064
+ token
788
1065
  };
789
1066
  }
1067
+ }
1068
+ /** Match a negator at exactly `index`, the longest alternative winning. */
1069
+ function negatorAt(text, index) {
1070
+ const lower = text.toLowerCase();
1071
+ const candidates = NEGATORS.filter(([token]) => lower.startsWith(token, index)).sort((a, b) => b[0].length - a[0].length || a[0].localeCompare(b[0]));
1072
+ for (const [token] of candidates) {
1073
+ if (token.length === 1 && /[\u3400-\u9fff]/.test(token)) {
1074
+ if (!/[\p{Script=Han}\p{L}\p{N}]/u.test(text[index + 1] ?? "")) continue;
1075
+ }
1076
+ if (/^[a-z]/.test(token)) {
1077
+ if (!isWordBoundary(text, index)) continue;
1078
+ if (/[\p{L}\p{N}_-]/u.test(text[index + token.length] ?? "")) continue;
1079
+ const after = text[index + token.length] ?? "";
1080
+ if (/[./@\\]/u.test(after) && !/\s/u.test(text[index + token.length + 1] ?? "")) continue;
1081
+ }
1082
+ return token;
1083
+ }
1084
+ }
1085
+ /**
1086
+ * Index of the first action verb at or after `offset`.
1087
+ *
1088
+ * A vocabulary entry that a multi-character action immediately continues is the
1089
+ * first character of that word rather than a verb of its own — the 升 of 升级,
1090
+ * the 然 of 然后 — so the longer action is chosen instead. Without that rule
1091
+ * "然后完成…" reads as two verbs and every condition analysis downstream anchors
1092
+ * on the wrong one.
1093
+ */
1094
+ function firstActionVerb(text, offset = 0, before = text.length) {
1095
+ const matches = actionVerbMatches(text, offset, before);
1096
+ return matches.length > 0 ? matches[0].index : -1;
1097
+ }
1098
+ /**
1099
+ * Every action word in `[offset, before)`, ordered by position, with the words
1100
+ * that are only a prefix of a longer action removed (the 升 of 升级, the 然 of
1101
+ * 然后). The remaining candidates are the verbs an instruction can be about.
1102
+ */
1103
+ function actionVerbMatches(text, offset = 0, before = text.length) {
1104
+ const span = text.slice(offset, before);
1105
+ const earliest = [];
1106
+ for (const pattern of [ACTION_VERB, WORK_VERB]) {
1107
+ const match = pattern.exec(span);
1108
+ if (match && !(match[0].length === 1 && /[A-Za-z]/.test(match[0]))) earliest.push({
1109
+ index: offset + match.index,
1110
+ length: match[0].length
1111
+ });
1112
+ }
1113
+ return earliest.sort((a, b) => a.index - b.index);
1114
+ }
1115
+ /**
1116
+ * True when a negator's scope covers the verb starting at `index`.
1117
+ *
1118
+ * The negator has to be phrase-initial, so the 不 of 手动 and the 无 of 无论 are
1119
+ * not read as bans; a contrast or list separator between the negator and the
1120
+ * verb ends its scope ("不仅…而且运行" keeps the run positive).
1121
+ */
1122
+ function verbIsNegated(text, index) {
1123
+ const ceiling = Math.min(index, 12);
1124
+ for (let back = 1; back <= ceiling; back += 1) {
1125
+ const at = index - back;
1126
+ const token = negatorAt(text, at);
1127
+ if (!token || at + token.length > index) continue;
1128
+ if (at > 0 && /[\u3400-\u9fff]/.test(text[at - 1])) continue;
1129
+ if (/[,,、;;。!!??\n\r]/.test(text.slice(at + token.length, index))) continue;
1130
+ return true;
1131
+ }
1132
+ return false;
1133
+ }
1134
+ /** True when an unnegated operation verb occurs inside `[from, to)`. */
1135
+ function hasPositiveVerb(text, from, to) {
1136
+ const index = firstActionVerb(text, from, to);
1137
+ if (index < 0) return false;
1138
+ return !verbIsNegated(text, index);
1139
+ }
1140
+ /**
1141
+ * The verb a negator bans. A Chinese negator may put an adverb between itself
1142
+ * and its verb ("不正式发布"), so candidate verbs are walked in order and the
1143
+ * first one that is a real word rather than part of the preceding word wins.
1144
+ */
1145
+ function bannedVerbIndex(text, afterNegator, before) {
1146
+ const span = text.slice(afterNegator, before);
1147
+ for (const word of CJK_VERB_WORDS) {
1148
+ const at = span.indexOf(word);
1149
+ if (at < 0) continue;
1150
+ return afterNegator + at;
1151
+ }
1152
+ const candidates = actionVerbMatches(text, afterNegator, before);
1153
+ for (const candidate of actionVerbMatches(text, 0, afterNegator)) candidates.push(candidate);
1154
+ if (candidates.length === 0) return -1;
1155
+ return candidates.map((candidate) => candidate.index).reduce((best, index) => Math.abs(index - afterNegator) < Math.abs(best - afterNegator) ? index : best);
1156
+ }
1157
+ /**
1158
+ * A resumption condition: everything a scope says before the event that ends
1159
+ * the wait ("收到我的确认后再推送" waits for the confirmation, so the push is
1160
+ * not executable yet). Leading request words are not part of the condition, and
1161
+ * a marker separated from the scope start by more than a clause belongs to a
1162
+ * different statement.
1163
+ */
1164
+ function resumptionConditionOf(scope) {
1165
+ const text = scope.text;
1166
+ if (scope.directive === "conditional") return text.replace(/^(?:请在|请|麻烦|帮我|需要你|务必)\s*/u, "").trim() || void 0;
1167
+ return leadingResumptionCondition(text, firstActionVerb(maskCodeSpans(text)) >= 0);
1168
+ }
1169
+ function leadingResumptionCondition(text, hasAction) {
1170
+ const masked = maskCodeSpans(text);
1171
+ const marker = RESUME_SCOPE_MARKER.exec(masked);
1172
+ if (!marker) return void 0;
1173
+ const guarded = masked.slice(marker[0].length).replace(/^[\s,,、::]+/u, "").replace(/^(?:再|才|就|则|即)\s*/u, "").trim().replace(/[。..!!??]+$/u, "");
1174
+ if (guarded && /[;;。]/u.test(guarded)) return void 0;
1175
+ if (!hasAction) return void 0;
1176
+ return marker[0].replace(/^(?:请在|请|麻烦|帮我|需要你|务必)\s*/u, "").trim() || void 0;
1177
+ }
1178
+ /** End index of the negated span beginning at `start`. */
1179
+ function negatedSpanEnd(text, start) {
1180
+ for (let cursor = start + 1; cursor < text.length; cursor += 1) {
1181
+ const character = text[cursor];
1182
+ if (character === "\n" || character === "\r") return cursor;
1183
+ if (character === "。" || character === "!" || character === "?" || character === "!" || character === "?") return cursor;
1184
+ if (character === "." && (cursor + 1 >= text.length || /\s/.test(text[cursor + 1]))) return cursor;
1185
+ if (character === "但" && text[cursor + 1] !== "是") return cursor;
1186
+ if (character === "而" && text[cursor + 1] === "是") return cursor;
1187
+ if (character === ";" || character === ";") return cursor;
1188
+ if (negatorAt(text, cursor)) return cursor;
1189
+ if (!SEPARATORS.has(character)) continue;
1190
+ const rest = text.slice(cursor + 1);
1191
+ if (CONTINUATION_AFTER_SEPARATOR.test(rest)) continue;
1192
+ if (character === "," || character === "," || character === "、") return cursor + 1;
1193
+ if (hasPositiveVerb(text, cursor + 1, text.length)) return cursor;
1194
+ }
1195
+ return text.length;
1196
+ }
1197
+ /**
1198
+ * Whether a run that follows a negator names an action directly ("推送、不
1199
+ * 发布" → true, "任何改动" → false). Only the guard's own action surface counts:
1200
+ * consultative verbs such as 完成 are deliberately absent, so "尚未完成" stays a
1201
+ * statement instead of becoming a ban.
1202
+ */
1203
+ function namesActionSpan(text) {
1204
+ const match = /^[^\p{Script=Han}A-Za-z]*([\p{Script=Han}A-Za-z][\p{Script=Han}A-Za-z0-9_-]*)/u.exec(text);
1205
+ if (!match) return false;
1206
+ const head = match[1];
1207
+ for (const entry of COMMAND_SURFACE_MANIFEST.operationVerbs) if (new RegExp(`^(?:${entry.pattern})$`, "i").test(head)) return true;
1208
+ return false;
1209
+ }
1210
+ /**
1211
+ * Split one message into scopes, in source order.
1212
+ *
1213
+ * The working list holds `[text, offset]` runs of the original message. A run is
1214
+ * resolved into one scope as soon as a rule matches; otherwise the runner splits
1215
+ * it into a head and a tail and pushes the tail back, so the split is iterative
1216
+ * and no run is ever re-read out of order.
1217
+ *
1218
+ * A negation opens a scope covering every action it governs — the scope ends at
1219
+ * a new positive verb, at a contrast, or (for a coordinated ban such as
1220
+ * "不推送、不发布") at the end of the run. A separator that is *followed by a
1221
+ * coordinating conjunction* also ends the current scope: "修复代码,但不推送"
1222
+ * is a task plus a ban, while "更新皮肤中心并在本地仓库记录" stays one
1223
+ * coordinated scope until the conjunction itself.
1224
+ */
1225
+ function scopeOf(raw, options = {}) {
1226
+ const source = raw.trim();
1227
+ if (!source) return [];
1228
+ const pending = [{
1229
+ text: source,
1230
+ offset: 0
1231
+ }];
1232
+ const resolved = [];
1233
+ const emitted = /* @__PURE__ */ new Set();
1234
+ const push = (entry) => {
1235
+ const key = `${entry.offset}\u0000${entry.scope.directive}\u0000${entry.scope.text}`;
1236
+ if (emitted.has(key)) return;
1237
+ emitted.add(key);
1238
+ resolved.push(entry);
1239
+ };
1240
+ while (pending.length > 0) {
1241
+ const run = pending.pop();
1242
+ const text = run.text.trim();
1243
+ if (!text) continue;
1244
+ const inheritedCondition = run.inherited;
1245
+ const offset = run.offset + run.text.indexOf(text);
1246
+ const masked = maskCodeSpans(text);
1247
+ const conditionPrefix = prefixConditionIndex(masked.toLowerCase());
1248
+ const negation = firstNegation(masked);
1249
+ let earliestVerb = firstActionVerb(masked, conditionPrefix !== void 0 ? lastBoundaryIndex(text, conditionPrefix) : 0);
1250
+ if (earliestVerb < 0 && conditionPrefix !== void 0) {
1251
+ const tail$1 = expressionTailVerb(masked, conditionPrefix);
1252
+ if (tail$1 > conditionPrefix) earliestVerb = tail$1;
1253
+ }
1254
+ const negationIndex = negation ? negation.index : -1;
1255
+ const banScanEnd = masked.length;
1256
+ const bannedVerb = negation ? bannedVerbIndex(masked, negationIndex + negation.token.length, banScanEnd) : -1;
1257
+ const negationBansAction = negation !== void 0 && (bannedVerb >= 0 || namesActionSpan(masked.slice(negationIndex + negation.token.length, banScanEnd)));
1258
+ negation !== void 0 && bannedVerb >= 0 && /^[\s::,,、]*$/u.test(masked.slice(negationIndex + negation.token.length, bannedVerb));
1259
+ const earliestNegation = negationBansAction ? negationIndex : -1;
1260
+ const earliestNegationToken = negationBansAction ? negation.token : "";
1261
+ const locativePrefix = conditionPrefix !== void 0 && text[conditionPrefix] === "在" && /^在.{1,24}?(?:记录|保存|写入|提交|运行|执行|测试|检查|验证|完成)/u.test(text.slice(conditionPrefix, earliestVerb));
1262
+ if (conditionPrefix !== void 0 && earliestNegation < 0 && !locativePrefix) {
1263
+ const conditional = conditionSplit(text, conditionPrefix, earliestVerb, options);
1264
+ if (conditional) {
1265
+ for (const scope of conditional) push({
1266
+ scope,
1267
+ offset: offset + (scope.start ?? 0)
1268
+ });
1269
+ continue;
1270
+ }
1271
+ }
1272
+ if (earliestNegation >= 0) {
1273
+ const head$1 = text.slice(0, earliestNegation).trim();
1274
+ const end$1 = negatedSpanEnd(masked, earliestNegation);
1275
+ const banText = text.slice(earliestNegation, end$1).trim();
1276
+ const tail$1 = text.slice(end$1).replace(/^[\s。..!!??,,;;、]+/, "").trim();
1277
+ if (tail$1) pending.push({
1278
+ text: tail$1,
1279
+ offset: offset + end$1,
1280
+ ...inheritedCondition ? { inherited: inheritedCondition } : {}
1281
+ });
1282
+ if (banText) {
1283
+ const clauseStart = conditionPrefix !== void 0 && conditionPrefix < earliestNegation ? lastBoundaryIndex(text, conditionPrefix) : -1;
1284
+ const conditionText = inheritedCondition ?? (clauseStart >= 0 ? stripConditionConnector(text.slice(clauseStart, earliestNegation)) : "");
1285
+ push({
1286
+ offset,
1287
+ scope: {
1288
+ text: banText,
1289
+ body: stripNegators(banText, earliestNegationToken) || banText,
1290
+ directive: "prohibition",
1291
+ ...conditionText ? { condition: conditionText } : {}
1292
+ }
1293
+ });
1294
+ }
1295
+ if (head$1) pending.push({
1296
+ text: head$1,
1297
+ offset
1298
+ });
1299
+ continue;
1300
+ }
1301
+ const end = positiveScopeEnd(masked, options);
1302
+ const head = text.slice(0, end).trim();
1303
+ const tail = text.slice(end).trim();
1304
+ if (tail) pending.push({
1305
+ text: tail,
1306
+ offset: offset + end
1307
+ });
1308
+ if (head) {
1309
+ const inherited = conditionPrefix !== void 0 && conditionPrefix < head.length ? text.slice(conditionPrefix, head.length).replace(/^[\s,,、;;::]+/, "").trim() : "";
1310
+ push({
1311
+ offset,
1312
+ scope: {
1313
+ text: head,
1314
+ body: stripConnectors(head),
1315
+ directive: classifyPositive(head),
1316
+ ...inherited ? { condition: inherited } : {}
1317
+ }
1318
+ });
1319
+ }
1320
+ }
1321
+ const seen = /* @__PURE__ */ new Set();
1322
+ return resolved.sort((a, b) => a.offset - b.offset).filter((entry) => {
1323
+ const key = `${entry.offset}\u0000${entry.scope.directive}\u0000${entry.scope.text}`;
1324
+ if (seen.has(key)) return false;
1325
+ seen.add(key);
1326
+ return true;
1327
+ }).map((entry) => entry.scope).filter((scope) => /[\p{L}\p{N}]/u.test(scope.body) && /[\p{L}\p{N}]/u.test(scope.text));
1328
+ }
1329
+ function positiveScopeEnd(masked, options = {}) {
1330
+ const limit = masked.length;
1331
+ let cursor = 0;
1332
+ while (cursor < limit && (masked[cursor] === "," || masked[cursor] === "," || masked[cursor] === "、" || masked[cursor] === "并" || masked[cursor] === "且" || /\s/u.test(masked[cursor]))) cursor += 1;
1333
+ while (cursor < limit) {
1334
+ const character = masked.slice(cursor, cursor + 1);
1335
+ if (SENTENCE_END.has(character)) return cursor + 1;
1336
+ if (character === ";" || character === ";") return cursor + 1;
1337
+ if (!(character === "," || character === "," || character === "、" || character === "并" || character === "且")) {
1338
+ cursor += 1;
1339
+ continue;
1340
+ }
1341
+ const rest = masked.slice(cursor + 1);
1342
+ if (options.coordinationSplit === false) {
1343
+ cursor += 1;
1344
+ continue;
1345
+ }
1346
+ if (/^\s*(?:直到|直至|一直到)\s*/u.test(rest)) {
1347
+ cursor += 1;
1348
+ continue;
1349
+ }
1350
+ if (CONTINUATION_AFTER_SEPARATOR.test(rest)) return cursor + 1;
1351
+ if (/^\s*(?:由|让|请|给)\s*(?:你|您|我|本人)/u.test(rest)) return cursor + 1;
1352
+ CROSS_CLAUSE_HEAD.test(rest);
1353
+ const locative = LOCATIVE_CLAUSE.test(rest);
1354
+ const conjunctionSeparator = character === "并" || character === "且";
1355
+ const comma = character === "," || character === ",";
1356
+ const enumeration = character === "、";
1357
+ if (comma) {
1358
+ if (!(conjunctionSeparator || CONTINUATION_AFTER_SEPARATOR.test(rest))) {
1359
+ cursor += 1;
1360
+ continue;
1361
+ }
1362
+ return cursor;
1363
+ }
1364
+ if (enumeration) {
1365
+ if (!locative) {
1366
+ cursor += 1;
1367
+ continue;
1368
+ }
1369
+ return cursor;
1370
+ }
1371
+ if (!(DISTINCT_CLAUSE_HEAD.test(rest) || locative)) {
1372
+ cursor += 1;
1373
+ continue;
1374
+ }
1375
+ return cursor;
1376
+ }
1377
+ return masked.length;
1378
+ }
1379
+ function conditionSplit(text, conditionPrefix, verb, options = {}) {
1380
+ const masked = maskCodeSpans(text);
1381
+ const lower = masked.toLowerCase();
1382
+ if (conditionPrefix === void 0) return suffixConditionSplit(text, lower, masked, options);
1383
+ if (verb < 0) return void 0;
1384
+ const candidates = conditionCandidates(text, lower, masked, conditionPrefix);
1385
+ if (candidates.length === 0) return void 0;
1386
+ const guardedScope = candidates[0];
1387
+ const markerIndex = guardedScope.start ?? 0;
1388
+ const guardedStart = guardedScope.start ?? 0;
1389
+ const condition = guardedScope.condition ?? "";
1390
+ if (!condition.trim() || !guardedScope.text.trim()) return void 0;
1391
+ const scopes = [];
1392
+ const clauseStart = lastBoundaryIndex(text, markerIndex);
1393
+ const lead = text.slice(0, clauseStart).trim();
1394
+ if (lead) scopes.push(...scopeOf(lead, options));
1395
+ const conditionClause = text.slice(clauseStart, guardedStart).trim();
1396
+ if (conditionClause) scopes.push({
1397
+ text: conditionClause,
1398
+ body: conditionClause,
1399
+ directive: "conditional",
1400
+ condition: condition.trim(),
1401
+ start: clauseStart
1402
+ });
1403
+ scopes.push({
1404
+ text: guardedScope.text.trim(),
1405
+ body: guardedScope.text.replace(/^[\s,,、;;::]+/, "").replace(/^(?:才|再|就|则|即)\s*/, "").trim(),
1406
+ directive: "directive",
1407
+ condition: condition.trim(),
1408
+ start: guardedStart
1409
+ });
1410
+ return scopes;
1411
+ }
1412
+ /** Index just past the last clause separator at or before `index`. */
1413
+ function lastBoundaryIndex(text, index) {
1414
+ let cursor = index;
1415
+ while (cursor > 0) {
1416
+ const character = text[cursor - 1];
1417
+ if (character === "在") break;
1418
+ if (character === ";" || character === ";" || character === "。" || character === "!" || character === "?" || character === "!" || character === "?" || character === "\n" || character === "\r") return cursor;
1419
+ cursor -= 1;
1420
+ }
1421
+ return 0;
1422
+ }
1423
+ /**
1424
+ * A trailing condition marker ("…才…") needs no prefix marker when it sits
1425
+ * directly before the action it guards: "收到我的明确回报后再继续".
1426
+ */
1427
+ function suffixConditionSplit(text, lower, masked, options = {}) {
1428
+ const verb = firstActionVerb(masked);
1429
+ if (verb < 0) return void 0;
1430
+ const marker = /(?:之后|以后|后再|后才|再继续|才继续|再|才)/u.exec(text.slice(verb));
1431
+ if (!marker) return void 0;
1432
+ const condition = trimConditionTail(text.slice(0, verb + marker.index));
1433
+ const guarded = text.slice(verb + marker[0].length);
1434
+ if (!condition || !guarded.trim()) return void 0;
1435
+ return [{
1436
+ text: condition,
1437
+ body: condition,
1438
+ directive: "conditional",
1439
+ condition
1440
+ }, {
1441
+ text: guarded.trim(),
1442
+ body: guarded.replace(/^[\s,,、;;::]+/, "").replace(/^(?:才|再|就|则|即)\s*/, "").trim(),
1443
+ directive: "directive",
1444
+ condition,
1445
+ start: verb + marker[0].length
1446
+ }];
1447
+ }
1448
+ /**
1449
+ * The condition clauses inside one clause run, each paired with the action it
1450
+ * guards. A marker that appears after the action's own verb but allows nothing
1451
+ * before that verb is not a condition at all — "confirm" contains "if", and
1452
+ * "We ship after the test passes" carries a subject the marker does not guard.
1453
+ */
1454
+ function conditionCandidates(text, lower, masked, conditionPrefix) {
1455
+ const clauseStart = lastBoundaryIndex(text, conditionPrefix);
1456
+ const clause = lower.slice(clauseStart);
1457
+ const candidates = [];
1458
+ for (const [token, kind] of CONDITION_MARKERS) {
1459
+ if (kind !== "prefix" || token === "在") continue;
1460
+ const index = prefixIndexOf(clause, token);
1461
+ if (index < 0) continue;
1462
+ const absolute = clauseStart + index;
1463
+ if (firstNegation(masked.slice(absolute))) continue;
1464
+ let after = firstActionVerb(masked, absolute + token.length);
1465
+ if (after < 0) {
1466
+ const tail = expressionTailVerb(masked, absolute + token.length);
1467
+ if (tail >= absolute + token.length) after = tail;
1468
+ }
1469
+ const before = firstActionVerb(masked, clauseStart, absolute);
1470
+ if (after >= 0 && masked.slice(absolute + token.length, after).trim().length === 0) {
1471
+ candidates.push({
1472
+ markerAt: absolute,
1473
+ condition: trimConditionTail(text.slice(clauseStart, absolute)),
1474
+ guarded: text.slice(after),
1475
+ guardedAt: after
1476
+ });
1477
+ continue;
1478
+ }
1479
+ if (after < 0) {
1480
+ if (before < 0 || !/^(?:[\p{L}\p{N}]+[\s]*){0,3}[\p{L}\p{N}]+$/u.test(text.slice(clauseStart, absolute).trim())) continue;
1481
+ candidates.push({
1482
+ markerAt: absolute,
1483
+ condition: trimConditionTail(text.slice(absolute + token.length)),
1484
+ guarded: text.slice(clauseStart, absolute).trim(),
1485
+ guardedAt: clauseStart
1486
+ });
1487
+ continue;
1488
+ }
1489
+ if (absolute >= before && before >= 0) continue;
1490
+ candidates.push({
1491
+ markerAt: absolute,
1492
+ condition: trimConditionTail(text.slice(absolute + token.length, after)),
1493
+ guarded: text.slice(after),
1494
+ guardedAt: after
1495
+ });
1496
+ }
1497
+ if (candidates.length === 0) return [];
1498
+ const best = candidates.reduce((left, right) => right.markerAt < left.markerAt ? right : left);
1499
+ const guarded = {
1500
+ text: best.guarded.trim(),
1501
+ body: best.guarded.replace(/^[\s,,、;;::]+/, "").replace(/^(?:才|再|就|则|即)\s*/, "").trim(),
1502
+ directive: "directive",
1503
+ condition: best.condition,
1504
+ start: best.guardedAt
1505
+ };
1506
+ return best.markerAt > 0 ? [guarded] : [guarded];
1507
+ }
1508
+ /**
1509
+ * The condition a marker supplies: everything between the clause start and the
1510
+ * guarded action, without the connector that introduces the ban ("除非…否则不要
1511
+ * 合并" → "除非我明确说可以").
1512
+ */
1513
+ function stripConditionConnector(value) {
1514
+ return value.replace(/^[\s,,、;;::]+/, "").replace(/[\s,,、;;::]*(?:否则|不然|then|otherwise)[\s,,、;;:]*$/i, "").trim();
1515
+ }
1516
+ /** Drop the temporal tail a condition marker may leave behind ("之后", "以后"). */
1517
+ function trimConditionTail(value) {
1518
+ return value.replace(/[\s,,、;;::]+$/, "").replace(/(?:之后|以后|后)$/, "").trim();
1519
+ }
1520
+ /**
1521
+ * A known action word at the end of the clause, used when the guarded action is
1522
+ * expressed as a plain Chinese verb: "若…才推送" ends in 推送, which the action
1523
+ * surface does not treat as a verb because it is the object of 才. Only a closed
1524
+ * vocabulary is accepted, so ordinary prose is never mistaken for an action.
1525
+ */
1526
+ const BOUND_ACTION_TAIL = /(创建|生成|写入|修改|编辑|运行|执行|编写|撰写|部署|安装|升级|提交|下载|上传|拉取|同步|重启|测试|检查|验证|确认|修复|更新|清理|整理|记录|构建|编译|重构|迁移|删除|回滚|发布|推送|实现|合并|提交|回退|检查)[。..!!??,,;;、\s]*$/u;
1527
+ function expressionTailVerb(masked, from) {
1528
+ const slice = masked.slice(from);
1529
+ const match = BOUND_ACTION_TAIL.exec(slice);
1530
+ return match ? from + match.index : -1;
1531
+ }
1532
+ /** Index of the first prefix condition marker, skipping a locative 在. */
1533
+ function prefixConditionIndex(lower) {
1534
+ const head = lower;
1535
+ let best;
1536
+ for (const [token, kind] of CONDITION_MARKERS) {
1537
+ if (kind !== "prefix" || token === "在") continue;
1538
+ const index = prefixIndexOf(head, token);
1539
+ if (index < 0) continue;
1540
+ if (best === void 0 || index < best) best = index;
1541
+ }
1542
+ return best;
1543
+ }
1544
+ /**
1545
+ * Word-bounded matcher per English marker, compiled once. Building the pattern
1546
+ * inside the scan recompiled it for every marker of every scope, which
1547
+ * dominated capture cost on long messages.
1548
+ */
1549
+ const ENGLISH_MARKER_MATCHERS = /* @__PURE__ */ new Map();
1550
+ function englishMarkerMatcher(token) {
1551
+ let matcher = ENGLISH_MARKER_MATCHERS.get(token);
1552
+ if (!matcher) {
1553
+ matcher = new RegExp(`(?:^|[^\\p{L}\\p{N}_])${escapeRegExp(token)}(?![\\p{L}\\p{N}_])`, "iu");
1554
+ ENGLISH_MARKER_MATCHERS.set(token, matcher);
1555
+ }
1556
+ return matcher;
1557
+ }
1558
+ function prefixIndexOf(text, token) {
1559
+ if (/^[a-z]/.test(token)) {
1560
+ const match = englishMarkerMatcher(token).exec(text);
1561
+ return match ? match.index + (match[0].length - token.length) : -1;
1562
+ }
1563
+ if (token === "在") {
1564
+ const match = /在[^。!?;]{0,24}?(?:之前|以前)/.exec(text);
1565
+ return match ? match.index : -1;
1566
+ }
1567
+ return text.indexOf(token);
1568
+ }
1569
+ function escapeRegExp(value) {
1570
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1571
+ }
1572
+ function stripNegators(text, matched) {
1573
+ let value = text.trim();
1574
+ value = value.replace(new RegExp(`^${CONNECTOR_PATTERN}\\s*`, "i"), "").trim();
1575
+ if (matched && value.toLowerCase().startsWith(matched.toLowerCase())) value = value.slice(matched.length);
1576
+ return stripNegatorsPrefix(value).trim();
1577
+ }
1578
+ function stripNegatorsPrefix(value) {
1579
+ let text = value;
1580
+ for (let guard = 0; guard < 8; guard += 1) {
1581
+ const trimmed = text.replace(/^[\s,,、;;::]+/, "");
1582
+ let changed = trimmed !== text;
1583
+ text = trimmed;
1584
+ for (const [token] of NEGATORS) {
1585
+ if (!text.toLowerCase().startsWith(token.toLowerCase())) continue;
1586
+ if (/^[a-z]/.test(token) && /[\p{L}\p{N}_]/u.test(text[token.length] ?? "")) continue;
1587
+ text = text.slice(token.length);
1588
+ changed = true;
1589
+ break;
1590
+ }
1591
+ if (!changed) break;
1592
+ }
1593
+ return text;
1594
+ }
1595
+ function stripConnectors(text) {
1596
+ return text.replace(new RegExp(`^${CONNECTOR_PATTERN}\\s*`, "i"), "").trim();
1597
+ }
1598
+ /** Classify a non-negated scope. */
1599
+ function classifyPositive(text) {
1600
+ const masked = maskCodeSpans(text);
1601
+ if (firstActionVerb(masked) < 0 && firstActionVerb(text) >= 0) return "informational";
1602
+ const explain = EXPLAIN_VERB.exec(masked);
1603
+ if (explain) {
1604
+ const verb = firstActionVerb(masked);
1605
+ if (verb < 0 || verb >= explain.index) return "informational";
1606
+ }
1607
+ if (QUESTION_SCOPE.test(masked)) return "informational";
1608
+ if (UNRESOLVED_SCOPE.test(masked)) return "informational";
1609
+ if ((NARRATIVE_PAST.test(masked) || NARRATIVE_ASPECT.test(masked)) && !NARRATIVE_DIRECTIVE.test(masked)) return "narrative";
1610
+ if (CONFIRMATION_RECEIPT.test(masked.trim())) return "narrative";
1611
+ return "directive";
1612
+ }
1613
+ function executeeOf(text, directive) {
1614
+ if (directive === "informational" || directive === "narrative" || directive === "conditional") return "unresolved";
1615
+ const masked = maskCodeSpans(text);
1616
+ if (USER_ACTOR_PATTERNS.some((pattern) => pattern.test(masked))) return "user";
1617
+ if (AGENT_ACTOR_PATTERNS.some((pattern) => pattern.test(masked))) return "agent";
1618
+ return "agent";
1619
+ }
1620
+ /** True when the scope asks for command/instruction TEXT rather than execution. */
1621
+ function isOutputRequest(text) {
1622
+ const masked = maskCodeSpans(text);
1623
+ return OUTPUT_NOUN.test(masked) && OUTPUT_REQUEST.test(masked);
1624
+ }
1625
+ function dispositionOf(scope, executee) {
1626
+ if (scope.directive === "prohibition") return "prohibition";
1627
+ if (scope.directive === "informational" || scope.directive === "narrative") return "informational";
1628
+ if (scope.directive === "conditional") return "conditional_wait";
1629
+ if (scope.condition) return "conditional_wait";
1630
+ if (executee === "user") return "human_actor";
1631
+ if (isOutputRequest(scope.text)) return "informational";
1632
+ return "executable_now";
1633
+ }
1634
+ function resumeEventOf(scope) {
1635
+ const match = RESUME_MARKER.exec(scope.condition ?? scope.text);
1636
+ return match ? match[0].trim() : void 0;
1637
+ }
1638
+ function interpret(scope) {
1639
+ const executee = executeeOf(scope.text, scope.directive);
1640
+ const resumption = scope.condition === void 0 && (scope.directive === "directive" || scope.directive === "conditional") ? resumptionConditionOf(scope) : void 0;
1641
+ const conditioned = resumption ? {
1642
+ ...scope,
1643
+ condition: resumption
1644
+ } : scope;
1645
+ const authorityDisposition = dispositionOf(conditioned, executee);
1646
+ const resumeEvent = scope.directive === "directive" ? resumeEventOf(conditioned) : void 0;
1647
+ const method = scope.directive === "prohibition" ? void 0 : semanticMethod(scope.body);
790
1648
  return {
791
- kind: "requirement",
792
- body: normalizedText
1649
+ text: scope.text,
1650
+ body: scope.body,
1651
+ directive: scope.directive,
1652
+ executee,
1653
+ ...conditioned.condition ? { condition: conditioned.condition } : {},
1654
+ ...resumeEvent ? { resumeEvent } : {},
1655
+ immediatelyExecutable: authorityDisposition === "executable_now",
1656
+ authorityDisposition,
1657
+ ...method ? { method } : {},
1658
+ fingerprint: fingerprintOf([
1659
+ scope.text,
1660
+ scope.body,
1661
+ scope.directive,
1662
+ executee,
1663
+ authorityDisposition,
1664
+ conditioned.condition ?? "",
1665
+ resumeEvent ?? "",
1666
+ method ?? ""
1667
+ ].join("\0"))
1668
+ };
1669
+ }
1670
+ function semanticMethod(text) {
1671
+ const match = /(?:用|使用|通过|借助|利用|以)\s*([A-Za-z][A-Za-z0-9_-]*)/.exec(text) ?? /\b(?:via|using|use|with)\s+(?:the\s+)?([A-Za-z][A-Za-z0-9_-]*)/i.exec(text);
1672
+ return match ? match[1].toLowerCase() : void 0;
1673
+ }
1674
+ /**
1675
+ * A short, stable identity for one interpretation: a content hash of the
1676
+ * interpretation itself, so a replay of identical bytes reproduces it exactly
1677
+ * and two different readings never collide.
1678
+ */
1679
+ function fingerprintOf(source) {
1680
+ let hash = 2166136261;
1681
+ for (let index = 0; index < source.length; index += 1) {
1682
+ hash ^= source.charCodeAt(index);
1683
+ hash = Math.imul(hash, 16777619) >>> 0;
1684
+ }
1685
+ return `i${hash.toString(16).padStart(8, "0")}`;
1686
+ }
1687
+ /** Interpret one already-segmented clause. */
1688
+ function interpretClause(text, options = {}) {
1689
+ const normalized = normalizeClause(text);
1690
+ const scopes = scopeOf(normalized, options);
1691
+ if (scopes.length === 0) return interpret({
1692
+ text: normalized,
1693
+ body: normalized,
1694
+ directive: "informational"
1695
+ });
1696
+ if (scopes.length === 1) return interpret(scopes[0]);
1697
+ const directive = scopes.some((scope) => scope.directive === "directive") ? "directive" : scopes.some((scope) => scope.directive === "prohibition") ? "prohibition" : "informational";
1698
+ return interpret({
1699
+ text: normalized,
1700
+ body: scopes.map((scope) => scope.body).join(";"),
1701
+ directive
1702
+ });
1703
+ }
1704
+ /** Interpret a whole message into independent scopes, in source order. */
1705
+ function interpretMessage(text, options = {}) {
1706
+ return scopeOf(normalizeClause(text), options).flatMap((scope) => splitTrailingResumption(scope)).map((scope) => interpret(scope));
1707
+ }
1708
+ /**
1709
+ * Whether the item is an executable obligation right now. A prohibition is a
1710
+ * standing constraint, a human-owned action belongs to the user, a conditional
1711
+ * action waits for its condition, and an explanation is not work. None of them
1712
+ * may block completion or be certified as agent work.
1713
+ *
1714
+ * An item without an interpretation is a legacy or fixture item created before
1715
+ * this module existed; it keeps its historical executable reading.
1716
+ */
1717
+ function isExecutableItem(item) {
1718
+ if (item.kind === "prohibition") return false;
1719
+ if (item.waitAuthorization !== void 0) return false;
1720
+ if (item.authorityDisposition === void 0) return true;
1721
+ if (item.authorityDisposition !== "executable_now") return false;
1722
+ return item.executee === void 0 || item.executee === "agent";
1723
+ }
1724
+ /** Whether an item is an open obligation for certification purposes. */
1725
+ function isOpenObligation(item) {
1726
+ return item.status === "pending" && isExecutableItem(item);
1727
+ }
1728
+ /**
1729
+ * The action a scope names. `semanticActionFromText` maps the command surface,
1730
+ * but a prohibition keeps a bare verb as its body ("不要提交并推送" → 提交并推送),
1731
+ * and the closed CJK vocabulary is consulted first so such a ban is still
1732
+ * recorded against the action it forbids.
1733
+ */
1734
+ function semanticActionOfScope(body, source = body, isProhibition = false) {
1735
+ const masked = maskCodeSpans(source);
1736
+ const negation = isProhibition ? {
1737
+ index: 0,
1738
+ token: ""
1739
+ } : firstNegation(masked);
1740
+ if (negation) {
1741
+ const banned = bannedVerbIndex(masked, negation.index + negation.token.length, masked.length);
1742
+ if (banned >= 0) {
1743
+ const action = semanticActionFromText(CJK_VERB_WORDS.find((entry) => masked.startsWith(entry, banned)) ?? masked.slice(banned, banned + 2));
1744
+ if (action !== "generic_run") return action;
1745
+ }
1746
+ }
1747
+ return semanticActionFromText(body);
1748
+ }
1749
+ /**
1750
+ * Split a scope whose action is stated after a resumption clause: "请先测试,
1751
+ * 收到我的确认后再推送" runs the test now and reserves the push for the
1752
+ * confirmation. Only a comma-separated split is used, so the earlier action
1753
+ * keeps its own executable meaning and the later one waits.
1754
+ */
1755
+ function splitTrailingResumption(scope) {
1756
+ if (scope.condition !== void 0 || scope.directive === "prohibition") return [scope];
1757
+ const masked = maskCodeSpans(scope.text);
1758
+ const marker = RESUMPTION_EVENT.exec(masked);
1759
+ if (!marker || marker.index === 0) return [scope];
1760
+ if (firstActionVerb(masked.slice(0, marker.index)) < 0) return [scope];
1761
+ const boundary = masked.slice(0, marker.index).search(/[,,][^,,]*$/);
1762
+ if (boundary < 0) return [scope];
1763
+ const head = scope.text.slice(0, boundary + 1).trim();
1764
+ const rest = scope.text.slice(boundary + 1).trim();
1765
+ if (!head || !rest) return [scope];
1766
+ if (firstActionVerb(maskCodeSpans(head)) < 0) return [scope];
1767
+ if (firstActionVerb(maskCodeSpans(rest)) < 0) return [scope];
1768
+ return [{
1769
+ text: head,
1770
+ body: head,
1771
+ directive: classifyPositive(head)
1772
+ }, {
1773
+ text: rest,
1774
+ body: rest,
1775
+ directive: "directive"
1776
+ }];
1777
+ }
1778
+ /**
1779
+ * Every stateful action the clause names, in source order. A clause may order
1780
+ * more than one ("安装插件,重启 DSH"); each is a separate evidence obligation
1781
+ * even though the clause stays one top-level item.
1782
+ */
1783
+ function statefulActionsOfScope(body) {
1784
+ const masked = maskCodeSpans(body);
1785
+ const found = [];
1786
+ const consider = (at, word) => {
1787
+ const action = semanticActionFromText(word);
1788
+ if (isStatefulAction(action)) found.push({
1789
+ at,
1790
+ action
1791
+ });
793
1792
  };
1793
+ for (const word of CJK_VERB_WORDS) {
1794
+ let at = masked.indexOf(word);
1795
+ while (at >= 0) {
1796
+ consider(at, word);
1797
+ at = masked.indexOf(word, at + word.length);
1798
+ }
1799
+ }
1800
+ for (const match of masked.matchAll(/\b(?:install|apply|restart|reload|commit|push|publish|pull|fetch|create|modify|edit)\b/gi)) consider(match.index, match[0]);
1801
+ found.sort((a, b) => a.at - b.at);
1802
+ const ordered = [];
1803
+ for (const entry of found) if (ordered.at(-1) !== entry.action) ordered.push(entry.action);
1804
+ return ordered;
1805
+ }
1806
+ /** Actions this interpretation names, in source order (diagnostics only). */
1807
+ function namedActions(text) {
1808
+ return interpretMessage(text).map((scope) => semanticActionOfScope(scope.body)).filter((action) => action !== "generic_run");
1809
+ }
1810
+
1811
+ //#endregion
1812
+ //#region src/domain/capture.ts
1813
+ /**
1814
+ * Whether a clause opens with an explicit ban. The lane question ("is this a
1815
+ * constraint or a duty?") is answered by {@link ScopeInterpretation}; this stays
1816
+ * exported because the framing/segmentation callers ask it directly.
1817
+ */
1818
+ function classifyClause(text) {
1819
+ const [first] = interpretMessage(normalizeClause(text));
1820
+ return first ? kindOfScope(first.directive, first.body) : "requirement";
794
1821
  }
795
1822
  const METHOD_TOOL = "(?:bash|shell|powershell|pwsh|git|read|write|edit|node|python|python3|npm|pnpm|tsc|vitest)";
796
1823
  const METHOD_ALIASES = {
@@ -845,21 +1872,71 @@ function isInformationalMessage(text) {
845
1872
  function extractOperation(text) {
846
1873
  for (const [operation, pattern] of OPERATION_PATTERNS) if (pattern.test(text)) return operation;
847
1874
  }
848
- const TARGET_TOKEN = "(?:`[^`]+`|\"[^\"]+\"|'[^']+'|[\\p{L}\\p{N}@][\\p{L}\\p{N}@._/\\\\:+%?&=#\\[\\]-]*)";
1875
+ /**
1876
+ * A target token, as a human writes it.
1877
+ *
1878
+ * A path and a bare word do not start the same way: "/work/repo" and "./repo"
1879
+ * open with a separator, so a leading character class of letters, digits and
1880
+ * "@" cannot match them at all, and the field silently falls back to an
1881
+ * unrelated value. Paths therefore get their own branch, which requires the
1882
+ * separator plus at least one more character — a lone "/" is punctuation, not
1883
+ * a path.
1884
+ */
1885
+ const TARGET_TAIL = "[\\p{L}\\p{N}@._/\\\\:+%?&=#\\[\\]-]";
1886
+ const TARGET_TOKEN = `(?:\`[^\`]+\`|"[^"]+"|'[^']+'|${`[.~]*[\\\\/]${TARGET_TAIL}+`}|${`[\\p{L}\\p{N}@]${TARGET_TAIL}*`})`;
849
1887
  function unquoteTargetToken(value) {
850
1888
  if (!value) return void 0;
851
1889
  const trimmed = value.trim().replace(/[.,;,。;]+$/, "");
852
1890
  const unquoted = /^(?:`([^`]+)`|"([^"]+)"|'([^']+)')$/.exec(trimmed);
853
1891
  return (unquoted?.[1] ?? unquoted?.[2] ?? unquoted?.[3] ?? trimmed) || void 0;
854
1892
  }
1893
+ /**
1894
+ * The value of a labelled field ("repository X", "版本:1.2.3").
1895
+ *
1896
+ * The label must END where it ends: a label that is only a prefix of a longer
1897
+ * word is skipped, so the literal word "repository" is never read as the label
1898
+ * "repo" followed by the value "sitory".
1899
+ */
855
1900
  function labeledToken(text, labels) {
856
- return unquoteTargetToken(new RegExp(`(?:${labels})\\s*(?:[:=:]|为|是)?\\s*(${TARGET_TOKEN})`, "iu").exec(text)?.[1]);
1901
+ const label = new RegExp(`(?:${labels})`, "iu");
1902
+ const after = new RegExp(`^(?![\\p{L}\\p{N}_])\\s*(?:[:=:]|为|是)?\\s*(${TARGET_TOKEN})`, "iu");
1903
+ const OTHER_LABEL = /^(?:to|from|on|into|with|at|using|version|profile|registry|remote|refspec|branch|service|repository|repo|包|插件|制品|服务|仓库|版本|配置档|远端|分支|注册表)$/i;
1904
+ let cursor = 0;
1905
+ while (cursor <= text.length) {
1906
+ const match = label.exec(text.slice(cursor));
1907
+ if (!match) return void 0;
1908
+ cursor = cursor + match.index + match[0].length;
1909
+ const value = unquoteTargetToken(after.exec(text.slice(cursor))?.[1]);
1910
+ if (value && !OTHER_LABEL.test(value)) return value;
1911
+ if (cursor >= text.length) return void 0;
1912
+ }
857
1913
  }
1914
+ /**
1915
+ * The object a verb acts on. The verb is matched first, then — separately — an
1916
+ * optional noun that has to end at a word boundary, and only the text AFTER
1917
+ * that noun is the target. Matching the noun and the token in one pattern let
1918
+ * the noun eat a prefix of the real word ("repository" consumed as "repo" +
1919
+ * "sitory"), which captured "sitory" as a repository name.
1920
+ */
858
1921
  function actionObjectToken(text, verbs, nouns) {
859
- const token = unquoteTargetToken(new RegExp(`(?:${verbs})\\s*(?:(?:${nouns})\\s*)?(?:[:=:]|为)?\\s*(${TARGET_TOKEN})`, "iu").exec(text)?.[1]);
1922
+ const verb = new RegExp(`(?:${verbs})`, "iu").exec(text);
1923
+ if (!verb) return void 0;
1924
+ let cursor = verb.index + verb[0].length;
1925
+ const noun = new RegExp(`^\\s*(?:${nouns})(?![\\p{L}\\p{N}_])`, "iu").exec(text.slice(cursor));
1926
+ if (noun) cursor += noun[0].length;
1927
+ else cursor += text.slice(cursor).match(/^\s*[\p{Script=Han}]{0,2}\s*/u)?.[0].length ?? 0;
1928
+ const rest = text.slice(cursor).replace(/^\s*(?:[:=:]|为)?\s*/u, "");
1929
+ const token = unquoteTargetToken(new RegExp(`^(${TARGET_TOKEN})`, "u").exec(rest)?.[1]);
860
1930
  if (!token || /^(?:the|a|an|this|that|to|from|in|on|into|with|package|plugin|artifact|service|repository|repo|包|插件|制品|服务|仓库)$/i.test(token)) return void 0;
861
1931
  return token;
862
1932
  }
1933
+ /** The verbs that name each repository-facing action, for unlabelled objects. */
1934
+ const GIT_OBJECT_VERB = {
1935
+ push: "push|推送",
1936
+ pull: "pull|拉取",
1937
+ fetch: "fetch|抓取|获取",
1938
+ commit: "commit|提交"
1939
+ };
863
1940
  function splitPackageSpec(spec) {
864
1941
  if (!spec) return {};
865
1942
  const at = spec.lastIndexOf("@");
@@ -927,7 +2004,7 @@ function captureRequestedTarget(action, text, subject, surface) {
927
2004
  } };
928
2005
  }
929
2006
  if (action === "pull" || action === "fetch" || action === "commit" || action === "push") {
930
- const repository = labeledToken(text, "repository|repo|仓库") ?? (subject !== "scope" ? subject : void 0);
2007
+ const repository = labeledToken(text, "repository|repo|仓库") ?? actionObjectToken(text, GIT_OBJECT_VERB[action], "repository|repo|仓库") ?? (subject !== "scope" ? subject : void 0);
931
2008
  if (!repository) return {
932
2009
  target: {},
933
2010
  reasonCode: "requested_target_repository_missing"
@@ -966,29 +2043,29 @@ function extractArtifactPaths(text) {
966
2043
  }
967
2044
  return [...found];
968
2045
  }
969
- function segmentClauses(text, captureVersion = "v042") {
2046
+ function segmentClauses(text, options = {}) {
970
2047
  const normalized = normalizeClause(text);
971
2048
  if (!normalized) return [];
972
- const parts = normalized.split(/(?<=[。!?;])|(?<=[.!?])(?=\s|$)|(?<=(?:^|[\s。!?;.!?,,;:]))(?=(?:(?:do not|don't|never)(?![A-Za-z0-9_./@\\-])|禁止|不要|不得))/i).flatMap((part) => captureVersion === "v041" || classifyClause(part.trim()).kind === "prohibition" ? [part] : part.split(/(?<!一)(?:并且?|以及|同时)(?=(?:验证(?!结果|全部通过|通过|成功)|确认(?!结果|全部通过|通过|成功|完成)|确保(?!结果|全部通过|通过|成功)|检查(?!结果)|更新|记录|安装|应用|重启|提交|推送|发布|在.{0,40}记录))|\s+and\s+(?=(?:verify|confirm|install|apply|restart|commit|push|publish|record)\b)/i)).map((part) => part.trim()).filter(Boolean);
973
- const segments = [];
974
- for (const part of parts) {
975
- const { kind, body } = classifyClause(part);
976
- segments.push({
977
- kind,
978
- body,
979
- paths: extractArtifactPaths(body)
980
- });
981
- }
982
- return segments;
2049
+ return interpretMessage(normalized, options).filter((interpretation) => interpretation.text.trim().length > 0).map((interpretation) => ({
2050
+ kind: kindOfScope(interpretation.directive, interpretation.body),
2051
+ body: interpretation.body,
2052
+ text: interpretation.text,
2053
+ paths: extractArtifactPaths(interpretation.body),
2054
+ interpretation
2055
+ }));
983
2056
  }
984
2057
  /**
985
2058
  * Build a GuardItem from an already-classified clause body and a resolved
986
2059
  * verification subject/surface.
2060
+ *
2061
+ * The optional `interpretation` carries the scope reading taken from the same
2062
+ * bytes. It is passed through rather than re-derived, so the obligation lane and
2063
+ * the authority of one clause cannot disagree between callers.
987
2064
  */
988
- function captureItem(kind, body, sourceMessageId, id, revision, subject, surface, method, operation, captureVersion = "v042") {
2065
+ function captureItem(kind, body, sourceMessageId, id, revision, subject, surface, method, operation, interpretation) {
989
2066
  const sanitized = sanitizeClauseText(body);
990
- const unsupportedVisual = captureVersion === "v042" && /\bGUI\b|界面|视觉|截图|颜色|布局|视觉效果/i.test(sanitized);
991
- const semanticAction = unsupportedVisual ? "generic_run" : semanticActionFromText(sanitized);
2067
+ const unsupportedVisual = /\bGUI\b|界面|视觉|截图|颜色|布局|视觉效果/i.test(sanitized);
2068
+ const semanticAction = unsupportedVisual ? "generic_run" : semanticActionOfScope(sanitized, interpretation?.text ?? sanitized, kind === "prohibition");
992
2069
  const capturedTarget = captureRequestedTarget(semanticAction, sanitized, subject, surface);
993
2070
  const effectiveOperation = semanticAction === "verify" ? "verify" : operation;
994
2071
  const item = {
@@ -1015,9 +2092,18 @@ function captureItem(kind, body, sourceMessageId, id, revision, subject, surface
1015
2092
  targetCaptureStatus: capturedTarget.reasonCode ? "clarification_required" : "resolved",
1016
2093
  ...capturedTarget.reasonCode ? { targetCaptureReasonCode: capturedTarget.reasonCode } : {},
1017
2094
  taskKind: kind === "prohibition" ? void 0 : classifyTaskIntent(sanitized),
1018
- authority: "root_instruction"
2095
+ authority: "root_instruction",
2096
+ ...kind === "requirement" ? buildActionPlan(sanitized, subject, surface, semanticAction) : {},
2097
+ ...interpretation ? {
2098
+ directive: interpretation.directive,
2099
+ executee: interpretation.executee,
2100
+ authorityDisposition: interpretation.authorityDisposition,
2101
+ ...interpretation.condition ? { condition: interpretation.condition } : {},
2102
+ ...interpretation.resumeEvent ? { resumeEvent: interpretation.resumeEvent } : {},
2103
+ interpretationFingerprint: interpretation.fingerprint
2104
+ } : {}
1019
2105
  };
1020
- if (/(?:等待|暂停|等).{0,12}(?:用户|你|您|我).{0,12}(?:选择|确认|输入)(?:.{0,8}(?:后|再)?继续)?|收到.{0,8}(?:用户|你|您|我)?的?确认.{0,8}(?:后)?再继续|\bwait for (?:the )?(?:user|your)\b|\bcontinue only after (?:the )?(?:user's?|your) confirmation\b/i.test(sanitized)) item.waitAuthorization = {
2106
+ if ((interpretation ? interpretation.authorityDisposition !== "executable_now" && (interpretation.resumeEvent !== void 0 || interpretation.authorityDisposition === "conditional_wait") : false) || /(?:等待|暂停|等).{0,12}(?:用户|你|您|我).{0,12}(?:选择|确认|输入)(?:.{0,8}(?:后|再)?继续)?|收到.{0,8}(?:用户|你|您|我)?的?确认.{0,8}(?:后)?再继续|\bwait for (?:the )?(?:user|your)\b|\bcontinue only after (?:the )?(?:user's?|your) confirmation\b/i.test(sanitized)) item.waitAuthorization = {
1021
2107
  kind: "root_explicit_wait",
1022
2108
  id: `wait:${id}:${sha256(sanitized).slice(0, 12)}`
1023
2109
  };
@@ -1035,16 +2121,33 @@ function captureItem(kind, body, sourceMessageId, id, revision, subject, surface
1035
2121
  };
1036
2122
  return item;
1037
2123
  }
2124
+ /** The stateful actions a clause names, with the target captured for each. */
2125
+ function buildActionPlan(body, subject, surface, primary) {
2126
+ const actions = statefulActionsOfScope(body);
2127
+ if (actions.length === 0 && isStatefulAction(primary)) actions.push(primary);
2128
+ if (actions.length <= 1) return {};
2129
+ return { actionPlan: actions.map((action) => {
2130
+ const captured = captureRequestedTarget(action, body, subject, surface);
2131
+ return {
2132
+ action,
2133
+ requestedTarget: captured.target,
2134
+ targetCaptureStatus: captured.reasonCode ? "clarification_required" : "resolved",
2135
+ ...captured.reasonCode ? { targetCaptureReasonCode: captured.reasonCode } : {}
2136
+ };
2137
+ }) };
2138
+ }
1038
2139
  /**
1039
2140
  * Capture one contract clause. Every captured item receives a concrete
1040
2141
  * verification contract: a named artifact path (artifact surface) or the
1041
2142
  * session scope (scope surface), so an unrelated file read can never close it.
1042
2143
  */
1043
- function captureClause(text, sourceMessageId, id, revision, scope = {}) {
1044
- const { kind, body } = classifyClause(text);
2144
+ function captureClause(text, sourceMessageId, id, revision, scope = {}, options = {}) {
2145
+ const [interpretation] = interpretMessage(text, options);
2146
+ const kind = interpretation ? kindOfScope(interpretation.directive, interpretation.body) : "requirement";
2147
+ const body = interpretation?.body ?? text;
1045
2148
  const path$1 = extractArtifactPaths(sanitizeClauseText(body))[0] ?? "";
1046
2149
  const surface = path$1 ? "artifact" : "scope";
1047
- return captureItem(kind, body, sourceMessageId, id, revision, path$1 || scope.cwd || "scope", surface, extractMethod(body), extractOperation(body));
2150
+ return captureItem(kind, body, sourceMessageId, id, revision, path$1 || scope.cwd || "scope", surface, interpretation?.method ?? extractMethod(body), extractOperation(body), interpretation);
1048
2151
  }
1049
2152
 
1050
2153
  //#endregion
@@ -1115,6 +2218,19 @@ function deriveItemDiagnosis(p, item) {
1115
2218
  },
1116
2219
  attempt_fingerprint: fingerprint(p, item, "certified")
1117
2220
  };
2221
+ if (item.status === "pending" && item.waitAuthorization?.kind === "root_explicit_wait") return {
2222
+ ...base,
2223
+ certification: "unavailable",
2224
+ reason_code: "root_condition_pending",
2225
+ repairability: "user_input_required",
2226
+ missing_fields: [],
2227
+ missing_facets: [],
2228
+ next_action: {
2229
+ kind: "none",
2230
+ resume_condition: `Wait for the matching trusted root input: ${item.resumeEvent ?? item.condition ?? item.normalizedText}. Keep this obligation pending; do not execute it or collect effect evidence before release.`
2231
+ },
2232
+ attempt_fingerprint: fingerprint(p, item, "root_condition_pending")
2233
+ };
1118
2234
  if (action !== "generic_run" && !item.legacyFlags?.length && item.targetCaptureStatus === "clarification_required") {
1119
2235
  const missingFields = item.targetCaptureReasonCode ? [TARGET_FIELD_REASONS[item.targetCaptureReasonCode] ?? item.targetCaptureReasonCode] : [];
1120
2236
  return {
@@ -1630,11 +2746,11 @@ function rebindResponse(p, args) {
1630
2746
  revision: item.revision,
1631
2747
  kind: item.kind,
1632
2748
  status: item.status,
1633
- semantic_action: item.semanticAction,
1634
- target_capture_status: item.targetCaptureStatus
2749
+ ...item.semanticAction !== void 0 ? { semantic_action: item.semanticAction } : {},
2750
+ ...item.targetCaptureStatus !== void 0 ? { target_capture_status: item.targetCaptureStatus } : {}
1635
2751
  },
1636
2752
  diagnosis: deriveItemDiagnosis(p, item),
1637
- pending_proposal_id: pendingProposal?.id
2753
+ ...pendingProposal ? { pending_proposal_id: pendingProposal.id } : {}
1638
2754
  };
1639
2755
  }
1640
2756
  const proposal = p.rebindProposals.get(args.proposal_id ?? "");
@@ -1810,6 +2926,8 @@ function createProjection() {
1810
2926
  lastGuardEventSeq: -1,
1811
2927
  continuationAttempts: /* @__PURE__ */ new Map(),
1812
2928
  persistenceCorrectionAttempts: /* @__PURE__ */ new Map(),
2929
+ noProgressClaims: /* @__PURE__ */ new Map(),
2930
+ handledControlSeqs: /* @__PURE__ */ new Set(),
1813
2931
  rebindRejections: /* @__PURE__ */ new Map(),
1814
2932
  integrity: "valid"
1815
2933
  };
@@ -1836,52 +2954,593 @@ function currentContractDigest(projection) {
1836
2954
  }
1837
2955
 
1838
2956
  //#endregion
1839
- //#region src/domain/digest.ts
1840
- /**
1841
- * Digest v3 canonical manifest derivation for Context Guard certificates.
1842
- *
1843
- * This is the DSH-side implementation of the frozen cross-language digest
1844
- * contract documented in `docs/SEMANTIC_COMPATIBILITY.md`. The canonical
1845
- * fixture lives in codex-context-guard (`tests/fixtures/conformance/digest_v3`)
1846
- * and is byte-mirrored under `tests/fixtures/conformance/digest_v3` together
1847
- * with `UPSTREAM_PIN.json`; the vitest suite re-derives all 29 golden vectors
1848
- * and fails on any byte difference. Any change to the algorithm, separators,
1849
- * typed token language, allowlists, or serialization is a new digest version
1850
- * and must regenerate the vectors in both repositories.
1851
- *
1852
- * Fail-closed rules pinned here: lone surrogates are rejected before hashing,
1853
- * values are never Unicode-normalized, dynamic keys must match the snake_case
1854
- * grammar, collections reject duplicate members, canonical maps sort by
1855
- * semantic key bytes (never by encoded field bytes), and predicate digests are
1856
- * always recomputed from the actual parameter payload.
1857
- */
1858
- var DigestError = class extends Error {};
1859
- const MAX_ENCODED_NAME_BYTES = 256;
1860
- const MAX_SEMANTIC_KEY_BYTES = 64;
1861
- const MAX_VALUE_BYTES = 4096;
1862
- const MAX_FIELDS_PER_RECORD = 128;
1863
- const MAX_PRED_PARAMS_BYTES = 4096;
1864
- const DYNAMIC_KEY_RE = /^[a-z0-9_]{1,64}$/;
1865
- const PACKAGE_NAME_RE = /^[@a-z0-9._/-]{1,128}$/;
1866
- const ENUM_TOKEN_RE = /^[a-z0-9][a-z0-9_-]*$/;
1867
- const HEX_RE = /^[0-9a-f]+$/;
1868
- const SURFACE_ENUM = [
1869
- "artifact",
1870
- "ui",
1871
- "visual",
1872
- "scope"
1873
- ];
1874
- const OUTCOME_ENUM = [
1875
- "success",
1876
- "failure",
1877
- "unknown",
1878
- "durability-unknown"
1879
- ];
1880
- const EVIDENCE_ROLE_ENUM = [
1881
- "resolution",
1882
- "effect",
1883
- "state"
1884
- ];
2957
+ //#region src/domain/boundary.ts
2958
+ /** Bounded, replay-derived qualifications that callers may cite verbatim. */
2959
+ function availableBoundaryQualifications(projection) {
2960
+ const rows = [];
2961
+ for (const item of projection.items.values()) {
2962
+ if (item.status !== "pending") continue;
2963
+ if (item.waitAuthorization) rows.push({
2964
+ id: item.waitAuthorization.id,
2965
+ kind: item.waitAuthorization.kind,
2966
+ disposition: "user_wait",
2967
+ source: "root_contract",
2968
+ status: "pending"
2969
+ });
2970
+ if (item.deferAuthorization) rows.push({
2971
+ id: item.deferAuthorization.id,
2972
+ kind: item.deferAuthorization.kind,
2973
+ disposition: "deferred",
2974
+ source: "root_contract",
2975
+ status: "pending"
2976
+ });
2977
+ }
2978
+ for (const operation of projection.externalOperations.values()) {
2979
+ if (operation.epoch !== projection.epoch || operation.status !== "pending" && operation.status !== "running") continue;
2980
+ rows.push({
2981
+ id: operation.id,
2982
+ kind: "external_operation_pending",
2983
+ disposition: "external_wait",
2984
+ source: "trusted_adapter",
2985
+ status: operation.status
2986
+ });
2987
+ }
2988
+ return rows.sort((a, b) => a.id.localeCompare(b.id)).slice(0, 32);
2989
+ }
2990
+ function qualificationReason(projection, request) {
2991
+ const ids = new Set(request.qualificationIds);
2992
+ if (ids.size !== request.qualificationIds.length || ids.size === 0) return "boundary_qualification_ids_invalid";
2993
+ if (request.disposition === "user_wait") {
2994
+ if (request.qualificationKind !== "root_explicit_wait" && request.qualificationKind !== "user_decision_item") return "boundary_qualification_kind_mismatch";
2995
+ const known$1 = new Set([...projection.items.values()].filter((item) => item.status === "pending" && item.waitAuthorization?.kind === request.qualificationKind).map((item) => item.waitAuthorization.id));
2996
+ return request.qualificationIds.every((id) => known$1.has(id)) ? void 0 : "boundary_disposition_unqualified";
2997
+ }
2998
+ if (request.disposition === "external_wait") {
2999
+ if (request.qualificationKind !== "external_operation_pending") return "boundary_qualification_kind_mismatch";
3000
+ return request.qualificationIds.every((id) => {
3001
+ const operation = projection.externalOperations.get(id);
3002
+ return operation?.epoch === projection.epoch && (operation.status === "running" || operation.status === "pending");
3003
+ }) ? void 0 : "boundary_disposition_unqualified";
3004
+ }
3005
+ if (request.disposition === "guard_bounded_stop") {
3006
+ if (request.qualificationKind !== "guard_no_progress") return "boundary_qualification_kind_mismatch";
3007
+ const fingerprint$1 = request.qualificationIds.length === 1 ? request.qualificationIds[0] : void 0;
3008
+ if (!fingerprint$1 || fingerprint$1 !== progressFingerprint(projection)) return "boundary_disposition_unqualified";
3009
+ return (projection.noProgressClaims.get(fingerprint$1)?.size ?? 0) >= NO_PROGRESS_TURNS_BEFORE_STOP - 1 ? void 0 : "boundary_disposition_unqualified";
3010
+ }
3011
+ if (request.qualificationKind !== "root_explicit_defer") return "boundary_qualification_kind_mismatch";
3012
+ const known = new Set([...projection.items.values()].filter((item) => item.status === "pending" && item.deferAuthorization?.kind === request.qualificationKind).map((item) => item.deferAuthorization.id));
3013
+ return request.qualificationIds.every((id) => known.has(id)) ? void 0 : "boundary_disposition_unqualified";
3014
+ }
3015
+ function qualifyBoundary(projection, request) {
3016
+ const contractSha256 = currentContractDigest(projection);
3017
+ const reason = projection.integrity !== "valid" ? "boundary_integrity_invalid" : projection.hostStatus !== "supported" && projection.currentGoalRef ? "boundary_host_lock_unsupported" : qualificationReason(projection, request);
3018
+ const manifest = {
3019
+ protocolVersion: "1",
3020
+ disposition: request.disposition,
3021
+ qualificationKind: request.qualificationKind,
3022
+ qualificationIds: [...request.qualificationIds].sort(),
3023
+ epoch: projection.epoch,
3024
+ contractRevision: projection.contractRevision,
3025
+ contractSha256,
3026
+ goalRef: projection.currentGoalRef ?? null
3027
+ };
3028
+ const candidateSha256 = sha256(JSON.stringify(manifest));
3029
+ return {
3030
+ protocolVersion: "1",
3031
+ id: `B${projection.boundaries.length + 1}`,
3032
+ disposition: request.disposition,
3033
+ qualificationKind: request.qualificationKind,
3034
+ qualificationIds: [...request.qualificationIds],
3035
+ epoch: projection.epoch,
3036
+ contractRevision: projection.contractRevision,
3037
+ contractSha256,
3038
+ ...projection.currentGoalRef ? { goalRef: { ...projection.currentGoalRef } } : {},
3039
+ candidateSha256,
3040
+ ...request.callId ? { callId: request.callId } : {},
3041
+ persistedResult: reason ? "rejected" : "accepted",
3042
+ reasonCode: reason ?? "boundary_persisted_accepted"
3043
+ };
3044
+ }
3045
+ /**
3046
+ * Reconstruct the immutable candidate against the latest replay projection.
3047
+ * A persisted acceptance is not effectuation authority after any contract,
3048
+ * Goal, epoch, or qualification change.
3049
+ */
3050
+ function isCurrentAcceptedBoundary(projection, boundary) {
3051
+ if (boundary.persistedResult !== "accepted" || boundary.epoch !== projection.epoch || boundary.contractRevision !== projection.contractRevision || boundary.contractSha256 !== currentContractDigest(projection)) return false;
3052
+ const currentGoal = projection.currentGoalRef;
3053
+ if (boundary.goalRef ? !currentGoal || !sameRef(currentGoal, boundary.goalRef) : currentGoal !== void 0) return false;
3054
+ const reconstructed = qualifyBoundary(projection, {
3055
+ disposition: boundary.disposition,
3056
+ qualificationKind: boundary.qualificationKind,
3057
+ qualificationIds: boundary.qualificationIds,
3058
+ ...boundary.callId ? { callId: boundary.callId } : {}
3059
+ });
3060
+ return reconstructed.persistedResult === "accepted" && reconstructed.candidateSha256 === boundary.candidateSha256;
3061
+ }
3062
+ function sameRef(state, ref) {
3063
+ return state?.id === ref.id && state.revision === ref.revision;
3064
+ }
3065
+ /**
3066
+ * Effectuate only a replay-confirmed accepted boundary. The first disarm result
3067
+ * and an independent get() must both read the same active Goal ref as disarmed.
3068
+ * A failure after disarm may have taken effect is never auto-rearmed.
3069
+ */
3070
+ async function effectuateBoundary(boundary, access) {
3071
+ const base = {
3072
+ boundaryId: boundary.id,
3073
+ ...boundary.goalRef ? { goalRef: boundary.goalRef } : {}
3074
+ };
3075
+ if (boundary.persistedResult !== "accepted") return {
3076
+ ...base,
3077
+ reasonCode: "boundary_not_accepted",
3078
+ stopAllowed: false,
3079
+ resumeRequired: false
3080
+ };
3081
+ if (access.requalify) try {
3082
+ if (!await access.requalify()) return {
3083
+ ...base,
3084
+ reasonCode: "boundary_pre_effect_failure",
3085
+ stopAllowed: false,
3086
+ resumeRequired: false
3087
+ };
3088
+ } catch {
3089
+ return {
3090
+ ...base,
3091
+ reasonCode: "boundary_pre_effect_failure",
3092
+ stopAllowed: false,
3093
+ resumeRequired: false
3094
+ };
3095
+ }
3096
+ if (!boundary.goalRef) return {
3097
+ ...base,
3098
+ reasonCode: "boundary_no_goal_safe_yield",
3099
+ stopAllowed: true,
3100
+ resumeRequired: false
3101
+ };
3102
+ let before;
3103
+ try {
3104
+ before = await access.get();
3105
+ } catch {
3106
+ return {
3107
+ ...base,
3108
+ reasonCode: "boundary_pre_effect_failure",
3109
+ stopAllowed: false,
3110
+ resumeRequired: false
3111
+ };
3112
+ }
3113
+ if (!sameRef(before, boundary.goalRef) || before?.phase !== "active") return {
3114
+ ...base,
3115
+ reasonCode: "boundary_goal_ref_stale",
3116
+ stopAllowed: false,
3117
+ resumeRequired: false
3118
+ };
3119
+ if (before.activation === "disarmed") return {
3120
+ ...base,
3121
+ reasonCode: "boundary_already_disarmed",
3122
+ stopAllowed: true,
3123
+ resumeRequired: false
3124
+ };
3125
+ let firstReadback;
3126
+ try {
3127
+ firstReadback = await access.disarm();
3128
+ } catch {
3129
+ return {
3130
+ ...base,
3131
+ reasonCode: "boundary_post_effect_unknown",
3132
+ stopAllowed: false,
3133
+ resumeRequired: true
3134
+ };
3135
+ }
3136
+ if (!firstReadback || !sameRef(firstReadback, boundary.goalRef) || firstReadback.phase !== "active") return {
3137
+ ...base,
3138
+ reasonCode: "boundary_post_effect_unknown",
3139
+ stopAllowed: false,
3140
+ resumeRequired: true
3141
+ };
3142
+ if (firstReadback.activation !== "disarmed") return {
3143
+ ...base,
3144
+ reasonCode: "boundary_readback_still_armed",
3145
+ stopAllowed: false,
3146
+ resumeRequired: false
3147
+ };
3148
+ try {
3149
+ const independent = await access.get();
3150
+ if (!sameRef(independent, boundary.goalRef) || independent?.phase !== "active") return {
3151
+ ...base,
3152
+ reasonCode: "boundary_post_effect_unknown",
3153
+ stopAllowed: false,
3154
+ resumeRequired: true
3155
+ };
3156
+ if (independent.activation !== "disarmed") return {
3157
+ ...base,
3158
+ reasonCode: "boundary_readback_still_armed",
3159
+ stopAllowed: false,
3160
+ resumeRequired: false
3161
+ };
3162
+ } catch {
3163
+ return {
3164
+ ...base,
3165
+ reasonCode: "boundary_post_effect_unknown",
3166
+ stopAllowed: false,
3167
+ resumeRequired: true
3168
+ };
3169
+ }
3170
+ return {
3171
+ ...base,
3172
+ reasonCode: "boundary_effectuated",
3173
+ stopAllowed: true,
3174
+ resumeRequired: false
3175
+ };
3176
+ }
3177
+
3178
+ //#endregion
3179
+ //#region src/domain/goal-gate.ts
3180
+ function hasCurrentCertificate(projection) {
3181
+ const checkpoint = projection.checkpoints.at(-1);
3182
+ let reason;
3183
+ if (projection.integrity !== "valid") reason = "integrity_invalid";
3184
+ else if (projection.hostStatus !== "supported") reason = "host_lock_unsupported";
3185
+ else if (!checkpoint || checkpoint.result !== "certified") reason = "certificate_missing";
3186
+ else if (checkpoint.epoch !== projection.epoch) reason = "stale_epoch";
3187
+ else if (checkpoint.sessionRefDigest !== projection.sessionRefDigest) reason = "foreign_session";
3188
+ else if (checkpoint.hostLockDigest !== projection.hostLockDigest) reason = "stale_host_lock";
3189
+ else if (checkpoint.contractRevision !== projection.contractRevision) reason = "stale_contract_revision";
3190
+ else if (projection.currentGoalRef ? checkpoint.goalRef?.id !== projection.currentGoalRef.id || checkpoint.goalRef.revision !== projection.currentGoalRef.revision : checkpoint.goalRef !== void 0) reason = "stale_goal_ref";
3191
+ projection.certificateStatusReason = reason;
3192
+ return reason === void 0;
3193
+ }
3194
+ /**
3195
+ * Denies `update_goal(action=complete)` while the guard is enabled and no
3196
+ * current completion certificate exists. The gate itself has no bypass; a
3197
+ * workflow that genuinely finished but cannot certify (for example a contract
3198
+ * polluted by session-layer talk, or evidence that lives in another session)
3199
+ * has three explicit remediation routes:
3200
+ *
3201
+ * 1. `/context-guard off` disables the guard, so completion is no longer
3202
+ * gated. Use only after the user confirms the work is actually done.
3203
+ * 2. `/context-guard clear` supersedes every pending requirement and
3204
+ * acceptance under a `CLEAR:<revision>` sentinel (prohibitions are
3205
+ * retained) and bumps the contract revision; an empty-binding checkpoint
3206
+ * can then certify while the guard stays enabled.
3207
+ * 3. `update_goal(action=blocked)` records the blocker truthfully, which is
3208
+ * never denied by this gate.
3209
+ */
3210
+ function goalCompletionDenial(projection, toolName, argumentsValue, configuredToolName = "update_goal") {
3211
+ if (toolName !== configuredToolName || typeof argumentsValue !== "object" || argumentsValue === null) return void 0;
3212
+ if (argumentsValue.action !== "complete") return void 0;
3213
+ if (!projection.enabled) return void 0;
3214
+ const args = argumentsValue;
3215
+ if (projection.hostStatus !== "supported") return `Context Guard denial [stale_host]: host lock is unsupported or unavailable (${projection.hostReasonCode ?? "unknown_host"}).`;
3216
+ if (!projection.currentGoalRef) return "Context Guard denial [no_goal]: no current Goal reference is available.";
3217
+ if (args.goal_id !== projection.currentGoalRef.id || args.revision !== projection.currentGoalRef.revision) return "Context Guard denial [stale_goal_ref]: update_goal must use the exact current goal_id and revision.";
3218
+ if (hasCurrentCertificate(projection)) return void 0;
3219
+ if (projection.certificateStatusReason === "stale_host_lock") return "Context Guard denial [stale_host]: the completion certificate belongs to a different host identity.";
3220
+ if (projection.certificateStatusReason === "stale_goal_ref") return "Context Guard denial [stale_goal_ref]: the completion certificate belongs to a different Goal reference.";
3221
+ return projection.integrity === "valid" ? "Context Guard denial [certificate_missing]: a current completion certificate is required." : "Context Guard denial [certificate_missing]: integrity is unknown or corrupt, so no current certificate is usable.";
3222
+ }
3223
+
3224
+ //#endregion
3225
+ //#region src/domain/stop-policy.ts
3226
+ /**
3227
+ * What "relevant progress" means, as one value.
3228
+ *
3229
+ * The inputs are the recorded state a caller could not have faked without
3230
+ * changing the work itself: the epoch and contract revision, the open items and
3231
+ * their blockers, the qualified evidence set, the boundary qualifications
3232
+ * available right now, and the Goal's identity and activation. Deliberately
3233
+ * absent: timestamps, event counts, wording, checkpoint bodies, and the Goal
3234
+ * *revision* — editing a Goal's text is not progress, and treating it as such
3235
+ * would let a re-statement reset the stop budget.
3236
+ */
3237
+ /**
3238
+ * How many times the same progress fingerprint must be observed at a turn
3239
+ * boundary before Guard stops the automatic continuation.
3240
+ *
3241
+ * The first sighting is a baseline, not a stalled turn: it is the state a turn
3242
+ * either advanced to or started from, and the host's driver owns continuation
3243
+ * there. The second sighting is the first turn that produced nothing new, which
3244
+ * earns the one diagnosis and correction opportunity. The third is the bounded
3245
+ * stop. The count is a resource bound on repetition, never a way to declare the
3246
+ * task finished.
3247
+ */
3248
+ const NO_PROGRESS_TURNS_BEFORE_STOP = 3;
3249
+ /** Marks the durable no-progress record; replay reads the budget from these. */
3250
+ const NO_PROGRESS_RECORD_PREFIX = "Context Guard no-progress record: ";
3251
+ /**
3252
+ * The identity of the turn boundary a decision is taken at.
3253
+ *
3254
+ * Guard does not own the host's turn counter, and a retry must be recognisable
3255
+ * as the same boundary rather than as a new one. The last durable event is that
3256
+ * identity: it is derivable from the log alone, it is stable across a reload,
3257
+ * and it only advances when the session actually records something new.
3258
+ */
3259
+ function decisionBoundaryKey(projection) {
3260
+ return projection.hostTurn;
3261
+ }
3262
+ function progressFingerprint(projection) {
3263
+ const open = [...projection.items.values()].filter((item) => item.status === "pending").map((item) => `${item.id}:${item.revision}:${item.normalizedText}`).sort();
3264
+ const evidence = [...projection.evidence.values()].filter((row) => row.epoch === projection.epoch && row.outcome === "success").map((row) => row.id).sort();
3265
+ const qualifications = availableBoundaryQualifications(projection).map((row) => `${row.id}:${row.status}`).sort();
3266
+ return JSON.stringify({
3267
+ epoch: projection.epoch,
3268
+ contractRevision: projection.contractRevision,
3269
+ open,
3270
+ evidence,
3271
+ qualifications,
3272
+ goal: projection.currentGoalRef?.id ?? null
3273
+ });
3274
+ }
3275
+ const QUOTED = /["'“”‘’`].*?(?:complete|done|finished|完成|做完|搞定).*?["'“”‘’`]/i;
3276
+ const EXAMPLE = /\b(?:for example|e\.g\.|such as|like saying|例如|比如|举例|作为一个例子)\b/i;
3277
+ const QUESTION = /\?[ \t]*$|\b(?:should|could|would|can|will|what|how|whether)\b.*\?/i;
3278
+ const TRAILING_NEGATION = /\b(?:not (?:yet |quite |fully )?(?:complete|done|finished)|isn'?t (?:complete|done|finished)|hasn'?t (?:been )?(?:completed|finished)|尚未完成|还没完成|未完成|没有完成|还未完成)\b/i;
3279
+ const CONDITIONAL = /\b(?:if|unless|once|when|whenever|provided that|只要|如果|假如|一旦|除非)\b/i;
3280
+ const PARTIAL_ONLY = /\b(?:step|phase|stage|milestone)\s+\d+\b|第[一二三四五六七八九十\d]+\s*(?:步|阶段|环节)|(?:第一步|第二步|第三步)/i;
3281
+ const WHOLE_COMPLETION_EN = /\b(?:the )?(?:task|work|job|everything|all tasks?|all work) (?:is|are) (?:now )?(?:complete|done|finished|completed)\b|\b(?:task|work) (?:has been )?(?:completed|finished)\b|\ball (?:tasks|work|requirements) (?:have been )?(?:completed|done|met)\b/i;
3282
+ const WHOLE_COMPLETION_ZH = /(?:任务|工作|所有任务|全部工作|整体)(?:已经|已)?(?:全部)?(?:完成|搞定|做完)|(?:已|已经)(?:全部|所有)?(?:完成|搞定)(?:了)?(?:全部|所有)?(?:任务|工作)?/i;
3283
+ /** Bare completion confirmations, e.g. "Done." or "搞定了。" */
3284
+ const BARE_COMPLETION = /^(?:done|finished|completed|all\s+done)[.!]?$|^(?:已完成|完成了|搞定了|搞定|完成|done)[。..!!]?$/i;
3285
+ /** Continuation intent following a claim makes it partial, not whole-task. */
3286
+ const CONTINUATION = /接下来|下一步|然后|接着|继续|再去|最后再|还差|剩下|剩余|第二步|第三步|,\s*(?:next|then|after that|moving on)\b/i;
3287
+ function looksQuotedOrExemplary(text) {
3288
+ return QUOTED.test(text) || EXAMPLE.test(text);
3289
+ }
3290
+ function isWholeTaskCompletionClaim(text) {
3291
+ const normalized = normalizeClause(text);
3292
+ if (!normalized) return false;
3293
+ if (QUESTION.test(normalized)) return false;
3294
+ if (TRAILING_NEGATION.test(normalized)) return false;
3295
+ if (CONDITIONAL.test(normalized)) return false;
3296
+ if (CONTINUATION.test(normalized)) return false;
3297
+ if (looksQuotedOrExemplary(normalized)) return false;
3298
+ if (PARTIAL_ONLY.test(normalized) && !WHOLE_COMPLETION_EN.test(normalized) && !WHOLE_COMPLETION_ZH.test(normalized)) return false;
3299
+ const firstLine = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
3300
+ if (BARE_COMPLETION.test(normalizeTitleLine(firstLine))) return leadingBareCompletionClaim(text);
3301
+ return BARE_COMPLETION.test(normalized) || WHOLE_COMPLETION_EN.test(normalized) || WHOLE_COMPLETION_ZH.test(normalized);
3302
+ }
3303
+ const DECORATION_LEAD = /^\s*(?:[\p{Extended_Pictographic}\u2764\u2705\u2714\u2716\u2728\u274C\u26A0\u2611\u2612\u2713\u2717\u274E\u2B50\u2B55\u2022\u00B7\u25E6\u25AA\u25AB\u25CF\u25CB\u25A0\u25A1\u2013\u2014-]|\uFE0F|\uFE0E|\u200D)+/u;
3304
+ /** Strip a leading run of decorative glyphs from a title line. */
3305
+ function stripDecorationPrefix(text) {
3306
+ let value = text;
3307
+ let previous = "";
3308
+ while (value !== previous) {
3309
+ previous = value;
3310
+ value = value.replace(DECORATION_LEAD, "");
3311
+ }
3312
+ return value.replace(/^\s+/, "");
3313
+ }
3314
+ /**
3315
+ * Normalize a title line for the bare-completion test. Markdown heading markers,
3316
+ * fully-wrapping emphasis (`**…**`, `__…__`, `*…*`, `_…_`), and a leading run of
3317
+ * decorative glyphs are removed ITERATIVELY until stable, because stripping one
3318
+ * layer may expose another (`## ✅ **完成。**`). Blockquotes (`>`), quoted
3319
+ * titles, and examples are left untouched so they still fail closed.
3320
+ */
3321
+ function normalizeTitleLine(line) {
3322
+ let value = line.trim();
3323
+ if (value.startsWith(">")) return value;
3324
+ let previous = "";
3325
+ while (value !== previous) {
3326
+ previous = value;
3327
+ value = value.replace(/^#{1,6}\s+/, "").replace(/^\*\*(.+?)\*\*$/, "$1").replace(/^__(.+?)__$/, "$1").replace(/^\*(.+?)\*$/, "$1").replace(/^_(.+?)_$/, "$1");
3328
+ value = stripDecorationPrefix(value);
3329
+ }
3330
+ return value;
3331
+ }
3332
+ /**
3333
+ * A reply whose first non-empty line is a standalone bare completion ("完成。"
3334
+ * or "Done.") followed by a results summary. The whole text no longer matches
3335
+ * the single-line BARE_COMPLETION anchor, but the summary must still be treated
3336
+ * as a whole-task completion claim.
3337
+ */
3338
+ function leadingBareCompletionClaim(text) {
3339
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
3340
+ const first = lines[0];
3341
+ if (!first || !BARE_COMPLETION.test(normalizeTitleLine(first))) return false;
3342
+ const rest = normalizeClause(lines.slice(1).join("\n"));
3343
+ if (!rest) return true;
3344
+ if (CONTINUATION.test(rest)) return false;
3345
+ if (TRAILING_NEGATION.test(rest)) return false;
3346
+ if (CONDITIONAL.test(rest)) return false;
3347
+ if (looksQuotedOrExemplary(rest)) return false;
3348
+ if (PARTIAL_ONLY.test(rest)) return false;
3349
+ return true;
3350
+ }
3351
+ function classifyCompletionClaim(text) {
3352
+ const normalized = normalizeClause(text);
3353
+ if (/waiting for (?:you|the user|input|your)|please (?:review|confirm|approve)|等待(?:您|你|用户)|请(?:确认|审阅|批准)/i.test(normalized)) return "user_wait";
3354
+ if (/waiting for (?:the )?(?:result|output|response|build|test|deployment)|等待(?:结果|输出|构建|测试|部署|响应)/i.test(normalized)) return "external_wait";
3355
+ if (isWholeTaskCompletionClaim(normalized)) return "complete";
3356
+ return "report";
3357
+ }
3358
+ /** Assistant prose is retained only as a bounded diagnostic observation. */
3359
+ function observeAssistantOutcome(text) {
3360
+ const disposition = classifyCompletionClaim(text);
3361
+ if (disposition === "complete") return {
3362
+ kind: "completion_claim",
3363
+ reasonCode: "assistant_completion_claim_observed"
3364
+ };
3365
+ if (disposition === "user_wait") return {
3366
+ kind: "user_wait_claim",
3367
+ reasonCode: "assistant_user_wait_claim_observed"
3368
+ };
3369
+ if (disposition === "external_wait") return {
3370
+ kind: "external_wait_claim",
3371
+ reasonCode: "assistant_external_wait_claim_observed"
3372
+ };
3373
+ return {
3374
+ kind: "report",
3375
+ reasonCode: "assistant_report_observed"
3376
+ };
3377
+ }
3378
+ /**
3379
+ * Stop Protocol 2.0 decision. This function deliberately has no assistant-text
3380
+ * parameter: completion wording, quotation, negation and translation cannot
3381
+ * steer the protocol. A structured root persistence authorization may request
3382
+ * one fallback correction; subsequent attempts safe-yield. An active, armed
3383
+ * Goal remains exclusively owned by the host Goal Round Driver.
3384
+ */
3385
+ function decideTurnBoundary(projection) {
3386
+ if (!projection.enabled) return {
3387
+ action: "stop",
3388
+ reason: "guard_disabled"
3389
+ };
3390
+ if (projection.integrity !== "valid") return {
3391
+ action: "stop",
3392
+ reason: "integrity_invalid_safe_yield"
3393
+ };
3394
+ if (hasCurrentCertificate(projection)) return {
3395
+ action: "stop",
3396
+ reason: "current_certificate"
3397
+ };
3398
+ const boundary = projection.boundaries.at(-1);
3399
+ if (boundary?.persistedResult === "accepted" && boundary.epoch === projection.epoch && boundary.contractRevision === projection.contractRevision) return {
3400
+ action: "stop",
3401
+ reason: "accepted_boundary_pending_effectuation"
3402
+ };
3403
+ if (projection.currentGoalPhase === "active" && projection.currentGoalActivation === "armed") {
3404
+ const fingerprint$1 = progressFingerprint(projection);
3405
+ const claims = projection.noProgressClaims.get(fingerprint$1) ?? /* @__PURE__ */ new Map();
3406
+ const hostTurn = decisionBoundaryKey(projection);
3407
+ if (hostTurn === void 0) return {
3408
+ action: "stop",
3409
+ reason: "no_progress_identity_unavailable"
3410
+ };
3411
+ const boundaryKey = String(hostTurn);
3412
+ const prior = [...claims].filter(([key]) => key !== boundaryKey).length;
3413
+ const claim = {
3414
+ fingerprint: fingerprint$1,
3415
+ boundaryKey,
3416
+ attempt: prior + 1
3417
+ };
3418
+ if (prior === 0) return {
3419
+ action: "stop",
3420
+ reason: "goal_round_driver_owns_continuation",
3421
+ noProgressClaim: claim
3422
+ };
3423
+ if (prior < NO_PROGRESS_TURNS_BEFORE_STOP - 1) return {
3424
+ action: "continue",
3425
+ reason: "no_progress_diagnosis_steer",
3426
+ noProgressClaim: claim
3427
+ };
3428
+ return {
3429
+ action: "stop",
3430
+ reason: "no_progress_bounded_disarm"
3431
+ };
3432
+ }
3433
+ if (projection.currentGoalRef) return {
3434
+ action: "stop",
3435
+ reason: projection.currentGoalPhase === "paused" ? "goal_paused_by_user_safe_yield" : "goal_not_continuable_safe_yield"
3436
+ };
3437
+ if ([...projection.items.values()].some((item) => item.status === "pending" && item.persistenceAuthorization)) {
3438
+ const key = `${projection.epoch}:${projection.contractRevision}`;
3439
+ const attempts = projection.persistenceCorrectionAttempts.get(key) ?? 0;
3440
+ if (attempts < 1) {
3441
+ projection.persistenceCorrectionAttempts.set(key, attempts + 1);
3442
+ return {
3443
+ action: "continue",
3444
+ reason: "protocol_correction_steer"
3445
+ };
3446
+ }
3447
+ }
3448
+ return {
3449
+ action: "stop",
3450
+ reason: "safe_yield_pending_preserved"
3451
+ };
3452
+ }
3453
+ function decideTurnStopping(projection, _assistantText, _turn, _maxAttempts) {
3454
+ return decideTurnBoundary(projection);
3455
+ }
3456
+ /**
3457
+ * Whether the last trusted ROOT instruction asked to pause.
3458
+ *
3459
+ * The source filter is the contract, not a heuristic: a quoted log, a tool
3460
+ * result, a plugin notice or a model message is not a `user/message` with
3461
+ * `source.kind === 'user'`, so none of them can reach this function at all, and
3462
+ * neither can the model's own summary of one. A negated pause ("不要暂停") is not
3463
+ * a pause request, and the check is anchored to a clause head so a pause word
3464
+ * mentioned inside a longer instruction is not a control request.
3465
+ */
3466
+ function latestRootInstruction(events) {
3467
+ for (let index = events.length - 1; index >= 0; index -= 1) {
3468
+ const event = events[index];
3469
+ if (event.type !== "user/message") continue;
3470
+ const data = event.data;
3471
+ if (data.source?.kind !== "user") continue;
3472
+ const text = (data.content ?? []).filter((part) => part?.type === "text").map((part) => part.text ?? "").join("\n");
3473
+ if (text.trim()) return {
3474
+ text,
3475
+ seq: event.seq ?? 0
3476
+ };
3477
+ }
3478
+ }
3479
+ /** Marks a root control request Guard has already carried to the host. */
3480
+ const CONTROL_RECORD_PREFIX = "Context Guard control record: ";
3481
+ const PAUSE_REQUEST = /(?:^|[。!?;;,,、\s])(?:请|麻烦)?\s*(?:先)?\s*(?:暂停|停一下|停一停|先停|暂时停止)(?:一下|下|吧)?\s*(?:[。!?;;,,、]|$)|\b(?:please\s+)?(?:pause|hold\s+on|stop\s+for\s+now)\b/i;
3482
+ const NEGATED_PAUSE = /(?:不要|不用|别|无需|不必)\s*(?:先)?\s*(?:暂停|停)|\b(?:do\s+not|don't|never)\s+(?:pause|stop)\b/i;
3483
+ function isRootPauseRequest(text) {
3484
+ if (NEGATED_PAUSE.test(text)) return false;
3485
+ return PAUSE_REQUEST.test(text);
3486
+ }
3487
+ function latestAssistantText(events) {
3488
+ for (let index = events.length - 1; index >= 0; index--) {
3489
+ const event = events[index];
3490
+ if (event.type !== "assistant/message") continue;
3491
+ const text = event.data.message?.content?.filter((block$1) => block$1.type === "text").map((block$1) => block$1.text ?? "").join("\n") ?? "";
3492
+ if (text.trim()) return text;
3493
+ }
3494
+ return "";
3495
+ }
3496
+
3497
+ //#endregion
3498
+ //#region src/domain/digest.ts
3499
+ /**
3500
+ * Digest v3 canonical manifest derivation for Context Guard certificates.
3501
+ *
3502
+ * This is the DSH-side implementation of the frozen cross-language digest
3503
+ * contract documented in `docs/SEMANTIC_COMPATIBILITY.md`. The canonical
3504
+ * fixture lives in codex-context-guard (`tests/fixtures/conformance/digest_v3`)
3505
+ * and is byte-mirrored under `tests/fixtures/conformance/digest_v3` together
3506
+ * with `UPSTREAM_PIN.json`; the vitest suite re-derives all 29 golden vectors
3507
+ * and fails on any byte difference. Any change to the algorithm, separators,
3508
+ * typed token language, allowlists, or serialization is a new digest version
3509
+ * and must regenerate the vectors in both repositories.
3510
+ *
3511
+ * Fail-closed rules pinned here: lone surrogates are rejected before hashing,
3512
+ * values are never Unicode-normalized, dynamic keys must match the snake_case
3513
+ * grammar, collections reject duplicate members, canonical maps sort by
3514
+ * semantic key bytes (never by encoded field bytes), and predicate digests are
3515
+ * always recomputed from the actual parameter payload.
3516
+ */
3517
+ var DigestError = class extends Error {};
3518
+ const MAX_ENCODED_NAME_BYTES = 256;
3519
+ const MAX_SEMANTIC_KEY_BYTES = 64;
3520
+ const MAX_VALUE_BYTES = 4096;
3521
+ const MAX_FIELDS_PER_RECORD = 128;
3522
+ const MAX_PRED_PARAMS_BYTES = 4096;
3523
+ const DYNAMIC_KEY_RE = /^[a-z0-9_]{1,64}$/;
3524
+ const PACKAGE_NAME_RE = /^[@a-z0-9._/-]{1,128}$/;
3525
+ const ENUM_TOKEN_RE = /^[a-z0-9][a-z0-9_-]*$/;
3526
+ const HEX_RE = /^[0-9a-f]+$/;
3527
+ const SURFACE_ENUM = [
3528
+ "artifact",
3529
+ "ui",
3530
+ "visual",
3531
+ "scope"
3532
+ ];
3533
+ const OUTCOME_ENUM = [
3534
+ "success",
3535
+ "failure",
3536
+ "unknown",
3537
+ "durability-unknown"
3538
+ ];
3539
+ const EVIDENCE_ROLE_ENUM = [
3540
+ "resolution",
3541
+ "effect",
3542
+ "state"
3543
+ ];
1885
3544
  const PRED_PARAMS_KIND_ENUM = ["inline", "manifest"];
1886
3545
  /** Frozen canonical key vocabulary; product manifests draw allowlists from it. */
1887
3546
  const PRODUCT_KEY_VOCABULARY = [
@@ -2777,6 +4436,10 @@ function renderRecoveryPacket(projection, options = {}) {
2777
4436
  };
2778
4437
  const requirement = (item) => {
2779
4438
  const diagnosis = deriveItemDiagnosis(projection, item);
4439
+ if (diagnosis.reason_code === "root_condition_pending") {
4440
+ if (add(`[${clip(item.id, 20)}] root_condition_pending; wait for trusted root: ${item.resumeEvent ?? item.condition ?? item.normalizedText}; do not execute before release`, compact ? 160 : 310)) count++;
4441
+ return;
4442
+ }
2780
4443
  const remedy = diagnosis.repairability === "agent_repairable" ? "Collect matching evidence; checkpoint" : diagnosis.repairability === "historical_gap" ? "Read back observed state; do not re-execute" : diagnosis.certification === "unsupported" ? "Deliver honestly; stays uncertified unless a fresh instruction names a supported action" : "Restore audited host/adapter capability";
2781
4444
  if (add(`[${clip(item.id, 20)}] ${diagnosis.reason_code}; ${compact ? remedy : diagnosis.next_action.resume_condition ?? remedy}; ${clip(item.normalizedText, 70)}`, compact ? 110 : 310)) count++;
2782
4445
  };
@@ -2866,8 +4529,9 @@ function evidenceProblem(projection, item, binding) {
2866
4529
  offendingEvidenceIds: notSuccess
2867
4530
  };
2868
4531
  const requiredAction = item.semanticAction ?? "generic_run";
4532
+ const compatibleWith = [requiredAction, ...(item.actionPlan ?? []).map((entry) => entry.action)];
2869
4533
  const facts = citedEvidence(projection, binding);
2870
- const incompatible = facts.filter((fact) => !actionCompatible(requiredAction, fact.semanticAction ?? "generic_run"));
4534
+ const incompatible = facts.filter((fact) => !compatibleWith.some((action) => actionCompatible(action, fact.semanticAction ?? "generic_run")));
2871
4535
  if (incompatible.length) {
2872
4536
  const compatibleCount = facts.length - incompatible.length;
2873
4537
  return {
@@ -3205,6 +4869,13 @@ function certifyCheckpoint(projection, bindings, id, commit = true) {
3205
4869
  });
3206
4870
  continue;
3207
4871
  }
4872
+ if (item.actionPlan && item.actionPlan.length > 0) {
4873
+ const planProblem = bindingActionPlanProblem(projection, item, binding);
4874
+ if (planProblem) {
4875
+ rejectedBindings.push(planProblem);
4876
+ continue;
4877
+ }
4878
+ }
3208
4879
  if (!binding.evidenceIds.length) {
3209
4880
  rejectedBindings.push({
3210
4881
  itemId: item.id,
@@ -3302,230 +4973,97 @@ function certifyCheckpoint(projection, bindings, id, commit = true) {
3302
4973
  };
3303
4974
  }
3304
4975
  }
3305
- function openItems(projection) {
3306
- return [...projection.items.values()].filter((item) => item.status === "pending" && item.kind !== "prohibition").map((item) => item.id);
3307
- }
3308
-
3309
- //#endregion
3310
- //#region src/domain/boundary.ts
3311
- /** Bounded, replay-derived qualifications that callers may cite verbatim. */
3312
- function availableBoundaryQualifications(projection) {
3313
- const rows = [];
3314
- for (const item of projection.items.values()) {
3315
- if (item.status !== "pending") continue;
3316
- if (item.waitAuthorization) rows.push({
3317
- id: item.waitAuthorization.id,
3318
- kind: item.waitAuthorization.kind,
3319
- disposition: "user_wait",
3320
- source: "root_contract",
3321
- status: "pending"
3322
- });
3323
- if (item.deferAuthorization) rows.push({
3324
- id: item.deferAuthorization.id,
3325
- kind: item.deferAuthorization.kind,
3326
- disposition: "deferred",
3327
- source: "root_contract",
3328
- status: "pending"
3329
- });
3330
- }
3331
- for (const operation of projection.externalOperations.values()) {
3332
- if (operation.epoch !== projection.epoch || operation.status !== "pending" && operation.status !== "running") continue;
3333
- rows.push({
3334
- id: operation.id,
3335
- kind: "external_operation_pending",
3336
- disposition: "external_wait",
3337
- source: "trusted_adapter",
3338
- status: operation.status
3339
- });
3340
- }
3341
- return rows.sort((a, b) => a.id.localeCompare(b.id)).slice(0, 32);
3342
- }
3343
- function qualificationReason(projection, request) {
3344
- const ids = new Set(request.qualificationIds);
3345
- if (ids.size !== request.qualificationIds.length || ids.size === 0) return "boundary_qualification_ids_invalid";
3346
- if (request.disposition === "user_wait") {
3347
- if (request.qualificationKind !== "root_explicit_wait" && request.qualificationKind !== "user_decision_item") return "boundary_qualification_kind_mismatch";
3348
- const known$1 = new Set([...projection.items.values()].filter((item) => item.status === "pending" && item.waitAuthorization?.kind === request.qualificationKind).map((item) => item.waitAuthorization.id));
3349
- return request.qualificationIds.every((id) => known$1.has(id)) ? void 0 : "boundary_disposition_unqualified";
3350
- }
3351
- if (request.disposition === "external_wait") {
3352
- if (request.qualificationKind !== "external_operation_pending") return "boundary_qualification_kind_mismatch";
3353
- return request.qualificationIds.every((id) => {
3354
- const operation = projection.externalOperations.get(id);
3355
- return operation?.epoch === projection.epoch && (operation.status === "running" || operation.status === "pending");
3356
- }) ? void 0 : "boundary_disposition_unqualified";
3357
- }
3358
- if (request.qualificationKind !== "root_explicit_defer") return "boundary_qualification_kind_mismatch";
3359
- const known = new Set([...projection.items.values()].filter((item) => item.status === "pending" && item.deferAuthorization?.kind === request.qualificationKind).map((item) => item.deferAuthorization.id));
3360
- return request.qualificationIds.every((id) => known.has(id)) ? void 0 : "boundary_disposition_unqualified";
3361
- }
3362
- function qualifyBoundary(projection, request) {
3363
- const contractSha256 = currentContractDigest(projection);
3364
- const reason = projection.integrity !== "valid" ? "boundary_integrity_invalid" : projection.hostStatus !== "supported" && projection.currentGoalRef ? "boundary_host_lock_unsupported" : qualificationReason(projection, request);
3365
- const manifest = {
3366
- protocolVersion: "1",
3367
- disposition: request.disposition,
3368
- qualificationKind: request.qualificationKind,
3369
- qualificationIds: [...request.qualificationIds].sort(),
3370
- epoch: projection.epoch,
3371
- contractRevision: projection.contractRevision,
3372
- contractSha256,
3373
- goalRef: projection.currentGoalRef ?? null
3374
- };
3375
- const candidateSha256 = sha256(JSON.stringify(manifest));
3376
- return {
3377
- protocolVersion: "1",
3378
- id: `B${projection.boundaries.length + 1}`,
3379
- disposition: request.disposition,
3380
- qualificationKind: request.qualificationKind,
3381
- qualificationIds: [...request.qualificationIds],
3382
- epoch: projection.epoch,
3383
- contractRevision: projection.contractRevision,
3384
- contractSha256,
3385
- ...projection.currentGoalRef ? { goalRef: { ...projection.currentGoalRef } } : {},
3386
- candidateSha256,
3387
- ...request.callId ? { callId: request.callId } : {},
3388
- persistedResult: reason ? "rejected" : "accepted",
3389
- reasonCode: reason ?? "boundary_persisted_accepted"
3390
- };
3391
- }
3392
- /**
3393
- * Reconstruct the immutable candidate against the latest replay projection.
3394
- * A persisted acceptance is not effectuation authority after any contract,
3395
- * Goal, epoch, or qualification change.
3396
- */
3397
- function isCurrentAcceptedBoundary(projection, boundary) {
3398
- if (boundary.persistedResult !== "accepted" || boundary.epoch !== projection.epoch || boundary.contractRevision !== projection.contractRevision || boundary.contractSha256 !== currentContractDigest(projection)) return false;
3399
- const currentGoal = projection.currentGoalRef;
3400
- if (boundary.goalRef ? !currentGoal || !sameRef(currentGoal, boundary.goalRef) : currentGoal !== void 0) return false;
3401
- const reconstructed = qualifyBoundary(projection, {
3402
- disposition: boundary.disposition,
3403
- qualificationKind: boundary.qualificationKind,
3404
- qualificationIds: boundary.qualificationIds,
3405
- ...boundary.callId ? { callId: boundary.callId } : {}
3406
- });
3407
- return reconstructed.persistedResult === "accepted" && reconstructed.candidateSha256 === boundary.candidateSha256;
3408
- }
3409
- function sameRef(state, ref) {
3410
- return state?.id === ref.id && state.revision === ref.revision;
3411
- }
3412
4976
  /**
3413
- * Effectuate only a replay-confirmed accepted boundary. The first disarm result
3414
- * and an independent get() must both read the same active Goal ref as disarmed.
3415
- * A failure after disarm may have taken effect is never auto-rearmed.
4977
+ * Per-action closure check for a multi-action clause. Each planned action needs
4978
+ * a matching closure whose resolved target matches the target captured for that
4979
+ * action, whose cited evidence succeeded, and whose evidence is not older than
4980
+ * the item revision it is closing.
3416
4981
  */
3417
- async function effectuateBoundary(boundary, access) {
3418
- const base = {
3419
- boundaryId: boundary.id,
3420
- ...boundary.goalRef ? { goalRef: boundary.goalRef } : {}
3421
- };
3422
- if (boundary.persistedResult !== "accepted") return {
3423
- ...base,
3424
- reasonCode: "boundary_not_accepted",
3425
- stopAllowed: false,
3426
- resumeRequired: false
3427
- };
3428
- if (access.requalify) try {
3429
- if (!await access.requalify()) return {
3430
- ...base,
3431
- reasonCode: "boundary_pre_effect_failure",
3432
- stopAllowed: false,
3433
- resumeRequired: false
3434
- };
3435
- } catch {
3436
- return {
3437
- ...base,
3438
- reasonCode: "boundary_pre_effect_failure",
3439
- stopAllowed: false,
3440
- resumeRequired: false
3441
- };
3442
- }
3443
- if (!boundary.goalRef) return {
3444
- ...base,
3445
- reasonCode: "boundary_no_goal_safe_yield",
3446
- stopAllowed: true,
3447
- resumeRequired: false
3448
- };
3449
- let before;
3450
- try {
3451
- before = await access.get();
3452
- } catch {
3453
- return {
3454
- ...base,
3455
- reasonCode: "boundary_pre_effect_failure",
3456
- stopAllowed: false,
3457
- resumeRequired: false
3458
- };
3459
- }
3460
- if (!sameRef(before, boundary.goalRef) || before?.phase !== "active") return {
3461
- ...base,
3462
- reasonCode: "boundary_goal_ref_stale",
3463
- stopAllowed: false,
3464
- resumeRequired: false
3465
- };
3466
- if (before.activation === "disarmed") return {
3467
- ...base,
3468
- reasonCode: "boundary_already_disarmed",
3469
- stopAllowed: true,
3470
- resumeRequired: false
3471
- };
3472
- let firstReadback;
3473
- try {
3474
- firstReadback = await access.disarm();
3475
- } catch {
3476
- return {
3477
- ...base,
3478
- reasonCode: "boundary_post_effect_unknown",
3479
- stopAllowed: false,
3480
- resumeRequired: true
4982
+ function bindingActionPlanProblem(projection, item, binding) {
4983
+ const plan = item.actionPlan ?? [];
4984
+ const closures = binding.actionBindings ?? [];
4985
+ if (closures.length !== plan.length) return {
4986
+ itemId: item.id,
4987
+ reason: `the clause orders ${plan.map((entry) => entry.action).join(" + ")}; ${closures.length} action closure(s) supplied`,
4988
+ reasonCode: "action_plan_incomplete",
4989
+ hint: closingHint(projection, item)
4990
+ };
4991
+ const ordered = [...closures].sort((a, b) => a.order - b.order);
4992
+ for (const [index, planned] of plan.entries()) {
4993
+ const closure = ordered[index];
4994
+ if (!closure || closure.action !== planned.action) return {
4995
+ itemId: item.id,
4996
+ reason: `action closure ${index + 1} must be '${planned.action}' in the clause's order`,
4997
+ reasonCode: "action_plan_order_mismatch"
3481
4998
  };
3482
- }
3483
- if (!firstReadback || !sameRef(firstReadback, boundary.goalRef) || firstReadback.phase !== "active") return {
3484
- ...base,
3485
- reasonCode: "boundary_post_effect_unknown",
3486
- stopAllowed: false,
3487
- resumeRequired: true
3488
- };
3489
- if (firstReadback.activation !== "disarmed") return {
3490
- ...base,
3491
- reasonCode: "boundary_readback_still_armed",
3492
- stopAllowed: false,
3493
- resumeRequired: false
3494
- };
3495
- try {
3496
- const independent = await access.get();
3497
- if (!sameRef(independent, boundary.goalRef) || independent?.phase !== "active") return {
3498
- ...base,
3499
- reasonCode: "boundary_post_effect_unknown",
3500
- stopAllowed: false,
3501
- resumeRequired: true
4999
+ if (planned.targetCaptureStatus !== "resolved") return {
5000
+ itemId: item.id,
5001
+ reason: `the clause does not identify an exact target for '${planned.action}'`,
5002
+ reasonCode: planned.targetCaptureReasonCode ?? "action_plan_target_missing",
5003
+ hint: closingHint(projection, item)
3502
5004
  };
3503
- if (independent.activation !== "disarmed") return {
3504
- ...base,
3505
- reasonCode: "boundary_readback_still_armed",
3506
- stopAllowed: false,
3507
- resumeRequired: false
5005
+ if (!tuplesEqual(planned.requestedTarget, closure.resolvedTarget)) return {
5006
+ itemId: item.id,
5007
+ reason: `the closure for '${planned.action}' resolves a different target than the clause captured`,
5008
+ reasonCode: "action_plan_target_mismatch"
3508
5009
  };
3509
- } catch {
3510
- return {
3511
- ...base,
3512
- reasonCode: "boundary_post_effect_unknown",
3513
- stopAllowed: false,
3514
- resumeRequired: true
5010
+ const reused = closure.evidenceIds.filter((id) => closures.some((other) => other !== closure && other.evidenceIds.includes(id)));
5011
+ if (reused.length > 0) return {
5012
+ itemId: item.id,
5013
+ reason: `evidence cited for '${planned.action}' also closes another action`,
5014
+ reasonCode: "action_plan_evidence_reused",
5015
+ offendingEvidenceIds: reused
3515
5016
  };
5017
+ if (closure.evidenceIds.length === 0) return {
5018
+ itemId: item.id,
5019
+ reason: `no evidence cited for '${planned.action}'`,
5020
+ reasonCode: "action_plan_evidence_missing"
5021
+ };
5022
+ const cited = closure.evidenceIds.map((id) => projection.evidence.get(id));
5023
+ const missing = closure.evidenceIds.filter((id) => !projection.evidence.has(id));
5024
+ if (missing.length > 0) return {
5025
+ itemId: item.id,
5026
+ reason: `cited evidence for '${planned.action}' is missing`,
5027
+ reasonCode: "evidence_missing",
5028
+ offendingEvidenceIds: missing
5029
+ };
5030
+ for (const [position, evidence] of cited.entries()) {
5031
+ if (!evidence) continue;
5032
+ if (evidence.epoch !== projection.epoch) return {
5033
+ itemId: item.id,
5034
+ reason: `evidence for '${planned.action}' belongs to another epoch`,
5035
+ reasonCode: "evidence_wrong_epoch",
5036
+ offendingEvidenceIds: [closure.evidenceIds[position]]
5037
+ };
5038
+ if (evidence.outcome !== "success") return {
5039
+ itemId: item.id,
5040
+ reason: `evidence for '${planned.action}' did not succeed`,
5041
+ reasonCode: "action_plan_evidence_not_successful",
5042
+ offendingEvidenceIds: [closure.evidenceIds[position]]
5043
+ };
5044
+ if (evidence.toolResultSeq < 0) return {
5045
+ itemId: item.id,
5046
+ reason: `evidence for '${planned.action}' predates the item`,
5047
+ reasonCode: "action_plan_evidence_predates_item",
5048
+ offendingEvidenceIds: [closure.evidenceIds[position]]
5049
+ };
5050
+ if (evidence.semanticAction && evidence.semanticAction !== planned.action) return {
5051
+ itemId: item.id,
5052
+ reason: `evidence for '${planned.action}' records '${evidence.semanticAction}'`,
5053
+ reasonCode: "action_plan_action_mismatch",
5054
+ offendingEvidenceIds: [closure.evidenceIds[position]]
5055
+ };
5056
+ }
3516
5057
  }
3517
- return {
3518
- ...base,
3519
- reasonCode: "boundary_effectuated",
3520
- stopAllowed: true,
3521
- resumeRequired: false
3522
- };
5058
+ }
5059
+ function openItems(projection) {
5060
+ return [...projection.items.values()].filter((item) => item.status === "pending" && item.kind !== "prohibition").map((item) => item.id);
3523
5061
  }
3524
5062
 
3525
5063
  //#endregion
3526
5064
  //#region src/domain/contract-segment.ts
3527
5065
  const REFERENCE_FRAME = /(?:以下|下面|下列|附上|粘贴|提供).{0,12}(?:报告|材料|内容|记录|日志).{0,12}(?:供参考|参考|如下)|(?:for reference|pasted|attached|following).{0,16}(?:report|material|log)/i;
3528
- const INSTRUCTION_SIGNAL = /(?:请|需要|必须|务必|禁止|不要|不得|运行|执行|修改|创建|读取|验证|检查|安装|拉取|提交|推送|发布|重启)|\b(?:please|must|shall|do not|run|execute|modify|create|read|verify|check|install|pull|commit|push|publish|restart)\b/i;
5066
+ const INSTRUCTION_SIGNAL = /(?:请|需要|必须|务必|禁止|不要|不得|运行|执行|修改|创建|读取|验证|检查|安装|应用|拉取|抓取|获取|同步|提交|推送|发布|重启|升级|更新)|\b(?:please|must|shall|do not|run|execute|modify|create|read|verify|check|install|apply|pull|fetch|commit|push|publish|restart|upgrade|update)\b/i;
3529
5067
  const ADOPTION_SIGNAL = /(?:按照|依照|采用|执行).{0,16}(?:下面|以下|报告|材料|第\s*([0-9一二三四五六七八九十]+)\s*节).{0,16}(?:全部执行|执行|作为验收|作为要求)|(?:把|将).{0,16}(?:上一条|前述|上述).{0,8}(?:报告|材料).{0,12}第\s*([0-9一二三四五六七八九十]+)\s*节.{0,20}(?:执行|采用)|(?:adopt|follow|apply).{0,20}(?:section\s+(\d+)|below|report)/i;
3530
5068
  const PREVIOUS_REFERENCE_ADOPTION = /(?:把|将).{0,16}(?:上一条|前述|上述).{0,8}(?:报告|材料).{0,12}第\s*[0-9一二三四五六七八九十]+\s*节.{0,20}(?:执行|采用)|(?:adopt|follow|apply).{0,16}(?:the\s+)?(?:previous|above).{0,12}(?:report|material).{0,12}section\s+\d+/i;
3531
5069
  function chineseNumber(value) {
@@ -4049,14 +5587,502 @@ const RC1_HOST_PACKAGES = [
4049
5587
  integrity
4050
5588
  }));
4051
5589
 
5590
+ //#endregion
5591
+ //#region src/domain/rc015-host.ts
5592
+ /**
5593
+ * Exact 33-row DSH 0.1.5-rc.1 core graph.
5594
+ *
5595
+ * Provenance: every row is the npm registry `dist.integrity` of the exact
5596
+ * published tarball for the named version, read from
5597
+ * `https://registry.npmjs.org/<name>/0.1.5-rc.1` (and `4.0.2` for
5598
+ * `@deepseek-ai/cordis`, which is versioned independently of DSH). The single
5599
+ * resolver for this graph is an isolated DSH installation plus the repository
5600
+ * worktree lockfile, both installed from the public registry.
5601
+ *
5602
+ * This is a REGISTRY-DERIVED graph, not a natively audited one: the cohort
5603
+ * carries `auditedPlatforms: []` until a native macOS/Windows host audit runs,
5604
+ * and `auditProvenance: 'registry-derived-pending-native-audit'` is bound into
5605
+ * the host-lock digest so a certificate can never claim a native pass this round
5606
+ * did not produce. (`acceptedPlatforms` is the separate, wider gate: this cohort
5607
+ * accepts evaluation on both platforms while claiming an audit on neither.)
5608
+ * `dshmarket` is deliberately absent: market identity is verified independently
5609
+ * by the action adapter and never participates in the core lock.
5610
+ *
5611
+ * The row-name set is unchanged from the historical 0.1.2-rc.1 cohort's 33
5612
+ * core rows: no package entered or left the audited core graph, so a future
5613
+ * reader must not infer a graph change from the version bump alone. The count
5614
+ * is asserted from this list, never assumed.
5615
+ */
5616
+ const RC015_HOST_PACKAGES = [
5617
+ [
5618
+ "@deepseek-ai/cordis",
5619
+ "4.0.2",
5620
+ "sha512-asOnXP1TzFSFQlHb1iegDZp0z/8WD1c7YNrwJR/Tx2bzNuMXfcekE/I67Iv6SQXeLB4csxqCngzQKANP7gdw0g=="
5621
+ ],
5622
+ [
5623
+ "@deepseek-ai/dsh",
5624
+ "0.1.5-rc.1",
5625
+ "sha512-rmNmzQCg3oIc1z8xH7izRSOuy1TNzq+/NILyfM+7e8DKOyV+yBtg47WEsqR2SiIe1ATec3L/rUa1YhIcfQ2XEg=="
5626
+ ],
5627
+ [
5628
+ "@deepseek-ai/dsh-agent",
5629
+ "0.1.5-rc.1",
5630
+ "sha512-obIPyTSjq1y0Yhasm3mLhK5BW6Ge0VQoRT8FBt0ooLsct2+pFAjgyd7GP3KzFaz7zaEhQJ/NNEmCaU0KOsYutg=="
5631
+ ],
5632
+ [
5633
+ "@deepseek-ai/dsh-agent-loop",
5634
+ "0.1.5-rc.1",
5635
+ "sha512-FcpsiXMHR7M3UwZtC6CYzhmU9xhvbFuuEQisfUqW7c+G6oVhrX9kg8ytI50jXZempcdmVHZLyNeUcuDmgGtx7Q=="
5636
+ ],
5637
+ [
5638
+ "@deepseek-ai/dsh-attachment",
5639
+ "0.1.5-rc.1",
5640
+ "sha512-uTBtB/LDlYgPI6i9Ac9jaK/bV1wN8cDTZBUkec80Yg3qMzW6K74wvBv5lPmoiXQgp4q3eOmDNEmodSS2ZxxVdg=="
5641
+ ],
5642
+ [
5643
+ "@deepseek-ai/dsh-bash-sandbox",
5644
+ "0.1.5-rc.1",
5645
+ "sha512-mQ+/0Fo3LTIX+4k3m+P4y9e9IA6/BeNHrWW19U+Un4hBo5JtTwWhAaJN1nCdV6mG5tveXbIe2tpiv65GJdvt8w=="
5646
+ ],
5647
+ [
5648
+ "@deepseek-ai/dsh-commands",
5649
+ "0.1.5-rc.1",
5650
+ "sha512-OMk0uVNbr2RdsItcIigp/2boGulqjei1uoQH3/DZYHB+aWWRXEYa6rk6vW3t82lnkd/6nvyQFV8eccQWZ2PQXQ=="
5651
+ ],
5652
+ [
5653
+ "@deepseek-ai/dsh-fs",
5654
+ "0.1.5-rc.1",
5655
+ "sha512-F+loGiwsT09YpRONuFv0+bevEdfmbKBFjxxM8JkDOXpgwk1JXdGNSy7Kp4vXXVh3GcmYkd8VA7iB7NuVp51ytQ=="
5656
+ ],
5657
+ [
5658
+ "@deepseek-ai/dsh-fs-local",
5659
+ "0.1.5-rc.1",
5660
+ "sha512-Qyqs9l+ZENq4PL7EwBpisvXcqJvCxGTrD/zH0Zj+S0eZee1kXrQjkY0gjW6dxAosGlLL7hZu0kdaD7TPxtKVcA=="
5661
+ ],
5662
+ [
5663
+ "@deepseek-ai/dsh-fs-observation-policy",
5664
+ "0.1.5-rc.1",
5665
+ "sha512-TGu/2UrZS8KWr6x3sKLce7u0KoGrq5l+RK7ITd79n2WNzSCcAdzN5tsxUEo8JgfHij5S0QRgID0Q8DOx/6iQew=="
5666
+ ],
5667
+ [
5668
+ "@deepseek-ai/dsh-fs-sandbox",
5669
+ "0.1.5-rc.1",
5670
+ "sha512-np+3EdQ86w609DwyaEUFGEHjSQ5i2NFypQxcM9sB+zX6DVSUR9sA2W/fmnStFJdsSzvgXYTCnJiBhlKnAXPf0g=="
5671
+ ],
5672
+ [
5673
+ "@deepseek-ai/dsh-goal",
5674
+ "0.1.5-rc.1",
5675
+ "sha512-RF+cHqV0O7xkoqkhIst6NhMAwqXXAjTf7Z+H7FLHtZBvFAawU8zC7n7/tmS4P7rZYV+XYFYCDdNoI0GwPReCYA=="
5676
+ ],
5677
+ [
5678
+ "@deepseek-ai/dsh-host-plugin-inventory",
5679
+ "0.1.5-rc.1",
5680
+ "sha512-xCOJ1nTW2s5etl18QhBBGpcOxiDfGxofe+4pd90/ZX+vW1vAhaHqyTYSRucOQVGyJb9zvdWCg7R3GcBYn6pUrQ=="
5681
+ ],
5682
+ [
5683
+ "@deepseek-ai/dsh-host-webserver",
5684
+ "0.1.5-rc.1",
5685
+ "sha512-5kOu9kb0AuRN60/zwPTRcki801ozgnWAFwS1QtQ4ZNgCYIbAiU8gwJHY1//qEpUOuHS+26k+Tqq5/WCJmLGE6Q=="
5686
+ ],
5687
+ [
5688
+ "@deepseek-ai/dsh-jobs",
5689
+ "0.1.5-rc.1",
5690
+ "sha512-0AWlZLcIpwdtV9A9fVeJ7b9jpXX0494fPL594gE/Kp1q9jHYyerIulrMHZa17kpu9W59cb1AJNPQy2xN6VDX7g=="
5691
+ ],
5692
+ [
5693
+ "@deepseek-ai/dsh-jobs-local",
5694
+ "0.1.5-rc.1",
5695
+ "sha512-19sCxqUKduNO8E3YICSzOfajpBLPXbK/3p40GlxR6bGcOhxqhyH3TPdQS6UQjwNa5bUXHiW9GyB1xUWUUFGAJA=="
5696
+ ],
5697
+ [
5698
+ "@deepseek-ai/dsh-llm",
5699
+ "0.1.5-rc.1",
5700
+ "sha512-KPKJFTNLjURphuF4NlS8DRK94CUYL/dKB8Hzg/22jtxAFO2paX3ifL0vhdPq9xPbcyplaF7LYlf0+3+pXFTnTg=="
5701
+ ],
5702
+ [
5703
+ "@deepseek-ai/dsh-pwsh-sandbox",
5704
+ "0.1.5-rc.1",
5705
+ "sha512-QRD6PfcQuaRwUTn9EMIx15l1erRqk0erUamcAB3sxPyZOMWIFP7w2cp44Va8RGGDEUb9ZB1vRUH6cF4FAKSZRw=="
5706
+ ],
5707
+ [
5708
+ "@deepseek-ai/dsh-sandbox",
5709
+ "0.1.5-rc.1",
5710
+ "sha512-xT+oTsSE7tRZVqqcj2qDZJARoK6A+Dmhf3CWPqlaFG/83Zh41kwcW2YWE4sA+vCt2pI2iqLYRw1SftSGawOtVQ=="
5711
+ ],
5712
+ [
5713
+ "@deepseek-ai/dsh-sandbox-policy",
5714
+ "0.1.5-rc.1",
5715
+ "sha512-jLeny81NVsAiEWW8+MqmtkXJfu8CSFDPiuR8W4I/bZ7TNHrPTN7jPuk1/JlphkU4bq1N1a3iLvsDyHK+56ZLVA=="
5716
+ ],
5717
+ [
5718
+ "@deepseek-ai/dsh-session",
5719
+ "0.1.5-rc.1",
5720
+ "sha512-0YBBrzkCbVEJolS/OpD0DZMSozmYxUTZopiG76MXInjOFBW9J4ca1a8WUjJjqelP1nJTmWGKXp92HcvNEny0Dg=="
5721
+ ],
5722
+ [
5723
+ "@deepseek-ai/dsh-shell",
5724
+ "0.1.5-rc.1",
5725
+ "sha512-8V7iGmfsXDFMyftwQXemh1QLqcUysvy+bYWXr620/1B/sMn3oU8dhXvT8i4uD6VkMy2rds3XScUQ3XBl7ByMLA=="
5726
+ ],
5727
+ [
5728
+ "@deepseek-ai/dsh-shell-env",
5729
+ "0.1.5-rc.1",
5730
+ "sha512-OO4AmGqqHUWRPK1PSroO/TJG3rNwoAgIMx56S3aiM7v4cUtNmUtMQ71lv9kOezcW+2VlHxfup5jhzpYIQAIiSw=="
5731
+ ],
5732
+ [
5733
+ "@deepseek-ai/dsh-subprocess-local",
5734
+ "0.1.5-rc.1",
5735
+ "sha512-TKcqaIf1fJzjraXhwmSAQAqkPMvIjS0Y7b9fC4n7+G8eQpb3gaF/eJXn6Tx4OgFSDV5R/NLUqHaU/ogxTjdWhQ=="
5736
+ ],
5737
+ [
5738
+ "@deepseek-ai/dsh-system-prompt",
5739
+ "0.1.5-rc.1",
5740
+ "sha512-RAdO9biQoga1vAVTQY9J7THexiOE1FOd1Nli021MQt+Zf73c83BSd2DwXb0WDilLaMeSxZPaIRRqoXpJlpmLIA=="
5741
+ ],
5742
+ [
5743
+ "@deepseek-ai/dsh-tool-bash",
5744
+ "0.1.5-rc.1",
5745
+ "sha512-BfZ4R40I7AJFcjHgMkzh3unrp5S7mZT+szUUz4tkbMrAkgHfOdkIHhQ/BTgxWBuFzD18OfAMJ/RnegrBCFVKWw=="
5746
+ ],
5747
+ [
5748
+ "@deepseek-ai/dsh-tool-fs",
5749
+ "0.1.5-rc.1",
5750
+ "sha512-BWLWJCJxCECFHmS8gHbnyNJlSTG+KVbVMz73Qduoo+ABeDvWj6cFVXTewAUA9jFEIS3xzJcNilsV1g5DGaEnPw=="
5751
+ ],
5752
+ [
5753
+ "@deepseek-ai/dsh-tool-goal",
5754
+ "0.1.5-rc.1",
5755
+ "sha512-5NCniCOoCeXYXMZNPCmGlrOYIGjnGddTJVOCXNqqAcwxtOxHAoEHTtphaohIa/4Vy7mmRIHgO1KUii443ky1Bw=="
5756
+ ],
5757
+ [
5758
+ "@deepseek-ai/dsh-tool-jobs",
5759
+ "0.1.5-rc.1",
5760
+ "sha512-SIgxnjQHl6KE+kpt7VjYCI3aw5DCeztCKGxuJzpLdqJkSoVtewp1Ofz0/Pg1R4DIIaGR8unRyqpZH8qf8uIpzA=="
5761
+ ],
5762
+ [
5763
+ "@deepseek-ai/dsh-tool-pwsh",
5764
+ "0.1.5-rc.1",
5765
+ "sha512-UmWePfsJIUfFVj2UFyh8wacqxSXYrABGoBHXWjYFlfqpXt7rfJL31rOl8N1+uYypAVCxe4O2IquuJxfYAVhBLA=="
5766
+ ],
5767
+ [
5768
+ "@deepseek-ai/dsh-tools",
5769
+ "0.1.5-rc.1",
5770
+ "sha512-I5AUxKTqUrC0nvRO4UcpU+f65P+nKs5BUrS2nqZehhFZ2rVxhUAJ7YdORcY6pVkBTB15nPr5gK0WPgwyfS217w=="
5771
+ ],
5772
+ [
5773
+ "@deepseek-ai/dsh-user-approval",
5774
+ "0.1.5-rc.1",
5775
+ "sha512-fSxEBvHQnozIh5HV31t2BNodYzyv7iE2srna7p5k0Y/3R9c6DdoZyQZ9momcN9gloraq8HK5+cvrYHzgc4LYPQ=="
5776
+ ],
5777
+ [
5778
+ "@deepseek-ai/dsh-web-app",
5779
+ "0.1.5-rc.1",
5780
+ "sha512-9V2GPqEs0A+LFJVVPt7FQK//U8oM9S0TDhl6MqO7zQftimfnH8ruQZkEZXg9zXXEWULCW0vY1DtsPjvMepA8Mw=="
5781
+ ]
5782
+ ].map(([name, version, integrity]) => ({
5783
+ name,
5784
+ version,
5785
+ integrity
5786
+ }));
5787
+
5788
+ //#endregion
5789
+ //#region src/domain/rc015-rc2-host.ts
5790
+ /** Exact npm registry identities for DSH 0.1.5-rc.2 (Cordis 4.0.2).
5791
+ * Native acceptance is recorded separately; these rows are registry-derived.
5792
+ */
5793
+ const RC015_RC2_HOST_PACKAGES = [
5794
+ {
5795
+ "name": "@deepseek-ai/cordis",
5796
+ "version": "4.0.2",
5797
+ "integrity": "sha512-asOnXP1TzFSFQlHb1iegDZp0z/8WD1c7YNrwJR/Tx2bzNuMXfcekE/I67Iv6SQXeLB4csxqCngzQKANP7gdw0g=="
5798
+ },
5799
+ {
5800
+ "name": "@deepseek-ai/dsh",
5801
+ "version": "0.1.5-rc.2",
5802
+ "integrity": "sha512-8Xc8hCQHcIWRmTCVU/xZdp6/qMsWMeAd2ObChKDEsfhUPJFXx6H0lgeb1DxUMD86HZrrVN+1bCvn1ppjZ/fOxw=="
5803
+ },
5804
+ {
5805
+ "name": "@deepseek-ai/dsh-agent",
5806
+ "version": "0.1.5-rc.2",
5807
+ "integrity": "sha512-SlUL1riZmVLwMUR3jo9CP/R1cxov9dHkCJDh6JQW3fSZJVCIdPygBRlAweCUDvHxAEmPpFHxE/U3NmSUbX+vQQ=="
5808
+ },
5809
+ {
5810
+ "name": "@deepseek-ai/dsh-agent-loop",
5811
+ "version": "0.1.5-rc.2",
5812
+ "integrity": "sha512-24wvqVlqFmdqJ2Bhcku/vKeNy+qWSmeEeN7lvcUB+WkhcF0/Ra2G3WwcVmnahJA4+w0AKdW3W7v+YYcO/jKJpg=="
5813
+ },
5814
+ {
5815
+ "name": "@deepseek-ai/dsh-attachment",
5816
+ "version": "0.1.5-rc.2",
5817
+ "integrity": "sha512-S6b8/WjqzGw+dMDLRXnq+tbijDGkQh38yE+zpQytX2/w/mPR3VzGj5r6McS01WwD76vXR8WFoheSCLyCAro8WQ=="
5818
+ },
5819
+ {
5820
+ "name": "@deepseek-ai/dsh-bash-sandbox",
5821
+ "version": "0.1.5-rc.2",
5822
+ "integrity": "sha512-y8vwK6jf4gPq8mG71zWure803NjfqJ66Nvzr1FTB8An+IF68hT2eU+hHJCByvP0u6FuiLPRptuX/im2CxcNo1g=="
5823
+ },
5824
+ {
5825
+ "name": "@deepseek-ai/dsh-commands",
5826
+ "version": "0.1.5-rc.2",
5827
+ "integrity": "sha512-ODc9h2Jig+Lo4XLdxqHpHjSXsBFeUCth6Y/rToor4KVRWQMedUARlq5otPyB1lYHyQh2DonNs2uf7g3mvag57A=="
5828
+ },
5829
+ {
5830
+ "name": "@deepseek-ai/dsh-fs",
5831
+ "version": "0.1.5-rc.2",
5832
+ "integrity": "sha512-6DHTquXPbpYdykGayqYaXSI9t668tDCoswH/bDezV+nwj4LxjrfY/smEtgp7nC4ubyoKf1hmjpNfUonGC9e7aA=="
5833
+ },
5834
+ {
5835
+ "name": "@deepseek-ai/dsh-fs-local",
5836
+ "version": "0.1.5-rc.2",
5837
+ "integrity": "sha512-akUTz9D/N0ruSOzytZ8SZ330SdzG75fd7DzHsJ7KJzSif/QM0Y+RpOmGMnjlJzit4HDV2A4Etn80wSzstyp82A=="
5838
+ },
5839
+ {
5840
+ "name": "@deepseek-ai/dsh-fs-observation-policy",
5841
+ "version": "0.1.5-rc.2",
5842
+ "integrity": "sha512-AntY5dfkTL8WugNGHkJxCEffaTFHqdXaYm7ZrerexnLiZQscDRRjf9zI00JwOmlFM/IrioCuLf5cERfhZN5GYw=="
5843
+ },
5844
+ {
5845
+ "name": "@deepseek-ai/dsh-fs-sandbox",
5846
+ "version": "0.1.5-rc.2",
5847
+ "integrity": "sha512-eUxNsnM+TsjGw5OleOIcAhMnFhmQ4OAZoBYeiRMSeOMuCKWEjhxUGN8S8Hg1HxPaZVeIrUV7qFsNQzhehKj7wg=="
5848
+ },
5849
+ {
5850
+ "name": "@deepseek-ai/dsh-goal",
5851
+ "version": "0.1.5-rc.2",
5852
+ "integrity": "sha512-atFJaoijwAz5yZ119f82I7jMx3tGCwXOz6qoY0Likb2c5DpumWZTJgs5L19OhKbhvEW+r2MAC4MYKaUxtrbb0Q=="
5853
+ },
5854
+ {
5855
+ "name": "@deepseek-ai/dsh-host-plugin-inventory",
5856
+ "version": "0.1.5-rc.2",
5857
+ "integrity": "sha512-U7RTRLRs+O18ru6KzUy7LvMk/IgXnkWSalogg0abfA+QU020XZi+UpwZqM567RzC7MtpxijQk8bulGunu0lifw=="
5858
+ },
5859
+ {
5860
+ "name": "@deepseek-ai/dsh-host-webserver",
5861
+ "version": "0.1.5-rc.2",
5862
+ "integrity": "sha512-lFgGm9wDrHiTBANzsdoWzdfPSjWYuDFwCoNQ4Uko57Fo5XASL2unfRHGm1xZ828rwEYuGwRvJHMOuoP/17VmlA=="
5863
+ },
5864
+ {
5865
+ "name": "@deepseek-ai/dsh-jobs",
5866
+ "version": "0.1.5-rc.2",
5867
+ "integrity": "sha512-C3rBEuWhtDBlxMeKykFvSfBwjSPxkLsvKCFq8BrFdjDmZC1lI9GooMjPZkPxXVbogVrcOBaYVtJdOYJ4+rIpQg=="
5868
+ },
5869
+ {
5870
+ "name": "@deepseek-ai/dsh-jobs-local",
5871
+ "version": "0.1.5-rc.2",
5872
+ "integrity": "sha512-PCDLSktONJ+If3QCcPlRskVLXIa8hG/l0pBIgKNllz4mb7CmYlJ5w1H9pWyrFZllMO0Wm94ZS9aP3SnUcJ8R1g=="
5873
+ },
5874
+ {
5875
+ "name": "@deepseek-ai/dsh-llm",
5876
+ "version": "0.1.5-rc.2",
5877
+ "integrity": "sha512-Z7BVsBkK24SE4EItQeow8PHms/9GP0DSTi337vTAa/RY7tNg2Snz3INcXUj6CPZfvntQr1in9op9wLI+rfNsqA=="
5878
+ },
5879
+ {
5880
+ "name": "@deepseek-ai/dsh-pwsh-sandbox",
5881
+ "version": "0.1.5-rc.2",
5882
+ "integrity": "sha512-AYTAiy8wQwVoHO47e+hnG73GvHdmg+bKy3bFnrPDANCqnRZh9TJIbkxHxtJnfpBnNCcs2fhhCo6yHtGFYl7KHg=="
5883
+ },
5884
+ {
5885
+ "name": "@deepseek-ai/dsh-sandbox",
5886
+ "version": "0.1.5-rc.2",
5887
+ "integrity": "sha512-OTOR6Jj9cey5YkhALG0TBwZ/Z3t986aczH6fLbzoIIegix+gwNaEBOqCWs+exVJ0Z2QuN/ItXzn+xHxW8Y0dcA=="
5888
+ },
5889
+ {
5890
+ "name": "@deepseek-ai/dsh-sandbox-policy",
5891
+ "version": "0.1.5-rc.2",
5892
+ "integrity": "sha512-QyQSCyLFxljkvmsVWJG0xUrYTiXr1DVOxHWURf7EHnbmvYZgg2B+nFcI58+IeUB2qbB35B1ClW5NQsKjOgDm2g=="
5893
+ },
5894
+ {
5895
+ "name": "@deepseek-ai/dsh-session",
5896
+ "version": "0.1.5-rc.2",
5897
+ "integrity": "sha512-y+klWiGAWR4m4cc4ylurA0cW63673B4N8cr2ANMimweDZAfxL4XVBC7WiD/5DT2DtIhYmVZhz/niyS/WbniUTA=="
5898
+ },
5899
+ {
5900
+ "name": "@deepseek-ai/dsh-shell",
5901
+ "version": "0.1.5-rc.2",
5902
+ "integrity": "sha512-BfmNN6X0NHN2XleW0fCbtFXedXEppeDQ2oa3WsOTuhvnBftl6QWMWWMM59weE4xXker5fzqOnoJdeBR8D9qR9Q=="
5903
+ },
5904
+ {
5905
+ "name": "@deepseek-ai/dsh-shell-env",
5906
+ "version": "0.1.5-rc.2",
5907
+ "integrity": "sha512-fFSrfhxfvVYfDxsOuV0cjAeC/PWweW+86uOJWT+2paHXOSgM6MSDe3eTd5DHRDYnjb5XutTYu32pR49cVBHMug=="
5908
+ },
5909
+ {
5910
+ "name": "@deepseek-ai/dsh-subprocess-local",
5911
+ "version": "0.1.5-rc.2",
5912
+ "integrity": "sha512-DmG3lcQlAh8bTKfeyYM44cfRyJktbLL8drXrVfOvGrFk0zSs5eTSLcfhIKLT3jFJ5CKFriHYZPLdRT6Alvyy4w=="
5913
+ },
5914
+ {
5915
+ "name": "@deepseek-ai/dsh-system-prompt",
5916
+ "version": "0.1.5-rc.2",
5917
+ "integrity": "sha512-VtmZVKqBMJ7kzskHu0jY+Jth7jSuKMG8QB3MBPzJe9M0LL4YPMWsGKt9gGky9RXolk/ugAy4YZEv1gUqouVofA=="
5918
+ },
5919
+ {
5920
+ "name": "@deepseek-ai/dsh-tool-bash",
5921
+ "version": "0.1.5-rc.2",
5922
+ "integrity": "sha512-f4LmiZkZSfJfvBcEzV4q5J83VL79l/+ncLkwnJyHzsjvJbW5qFxzCy4p5FXfY/CflG0taG14/p42UJzb9qEhrQ=="
5923
+ },
5924
+ {
5925
+ "name": "@deepseek-ai/dsh-tool-fs",
5926
+ "version": "0.1.5-rc.2",
5927
+ "integrity": "sha512-/3AUx+V1UxVfl24fm10hwRbJnHwpBkRDHniOeocGknMvASheRiKnHpnnj9EszFRLYSpe2PRFSMuyuhXgYyd3MQ=="
5928
+ },
5929
+ {
5930
+ "name": "@deepseek-ai/dsh-tool-goal",
5931
+ "version": "0.1.5-rc.2",
5932
+ "integrity": "sha512-nZ0NkUxvtAsrPAd9NMXt+4kS7WPn8xIvXCwVunKfBwbrnGnFCNmIsTQoHF44J6q9VbMeSCN2eyj07w6ej3Hf+g=="
5933
+ },
5934
+ {
5935
+ "name": "@deepseek-ai/dsh-tool-jobs",
5936
+ "version": "0.1.5-rc.2",
5937
+ "integrity": "sha512-v4y56H3FsVBF2jUJLGLHdPeKaKJxLT4Zx/oii45UmJv5t8Bp3uCTVUoq/mfSFVuFY66q6rHJwgIOKWc4ZO3ySw=="
5938
+ },
5939
+ {
5940
+ "name": "@deepseek-ai/dsh-tool-pwsh",
5941
+ "version": "0.1.5-rc.2",
5942
+ "integrity": "sha512-rnHM3Jqlr7rthwfPYysXPq6zH+K8JTDqbE+xzjkbo1WIBKBZeSGr8kPjqdCZa7eUtWhmV/RFBP1qmBgDjEZaWg=="
5943
+ },
5944
+ {
5945
+ "name": "@deepseek-ai/dsh-tools",
5946
+ "version": "0.1.5-rc.2",
5947
+ "integrity": "sha512-k2yZuJJtszaU9lzr2aBtdeFMINrkdlk4ellbtrMokA2oySVqJmAM8dv+u9RtzduD3aRRqyr2i2hWrTACz0qOrA=="
5948
+ },
5949
+ {
5950
+ "name": "@deepseek-ai/dsh-user-approval",
5951
+ "version": "0.1.5-rc.2",
5952
+ "integrity": "sha512-8UpMEnEyMo6mYEVELBo0DC2iG7aJJfFMTNkU+DKD3c5Ut7ySRDzt1iRr1qni/CzqlTIpQBAkGaG77qk4q/v1bg=="
5953
+ },
5954
+ {
5955
+ "name": "@deepseek-ai/dsh-web-app",
5956
+ "version": "0.1.5-rc.2",
5957
+ "integrity": "sha512-Ng7YVDt9txh2BlLmu6B+V677c1bihbh/rq3EOtZK4b0JWtgdNgYb1KPIED8+aK2i+FghrCrIy6X60AyJ45eOvw=="
5958
+ }
5959
+ ];
5960
+
5961
+ //#endregion
5962
+ //#region src/domain/host-version.ts
5963
+ /**
5964
+ * DSH host version support policy.
5965
+ *
5966
+ * Context Guard 0.5.2 supports exactly the two registered DSH host releases:
5967
+ * `0.1.5-rc.2` (latest) and `0.1.5-rc.1` (verified minimum). Package discovery,
5968
+ * npm installation, and the exported support range use the same newest-first
5969
+ * exact union, so an unregistered stable or future prerelease is never advertised
5970
+ * merely because it sorts above the minimum.
5971
+ *
5972
+ * The minimum comparison remains a diagnostic layer for distinguishing an old
5973
+ * host from an at-or-above-floor but unregistered host. It never substitutes for
5974
+ * the exact support set or the complete 33-package host graph.
5975
+ */
5976
+ /** Lowest supported DSH host version. DSH packages version independently of Cordis. */
5977
+ const MIN_SUPPORTED_HOST_VERSION = "0.1.5-rc.1";
5978
+ /** Latest DSH release with a registered complete host graph. */
5979
+ const LATEST_SUPPORTED_HOST_VERSION = "0.1.5-rc.2";
5980
+ /** Exact endpoints supported by the current release, newest first. */
5981
+ const SUPPORTED_HOST_VERSIONS = [LATEST_SUPPORTED_HOST_VERSION, MIN_SUPPORTED_HOST_VERSION];
5982
+ /** Exact npm range shared by package discovery and peer dependency declarations. */
5983
+ const SUPPORTED_HOST_RANGE = SUPPORTED_HOST_VERSIONS.join(" || ");
5984
+ const VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
5985
+ function parseHostVersion(value) {
5986
+ const match = VERSION_PATTERN.exec(value.trim());
5987
+ if (!match) return void 0;
5988
+ const parts = [
5989
+ match[1],
5990
+ match[2],
5991
+ match[3]
5992
+ ].map(Number);
5993
+ if (parts.some((part) => !Number.isSafeInteger(part) || part < 0)) return void 0;
5994
+ const prerelease = match[4] ? match[4].split(".") : [];
5995
+ if (prerelease.some((identifier) => identifier.length === 0)) return void 0;
5996
+ return {
5997
+ major: parts[0],
5998
+ minor: parts[1],
5999
+ patch: parts[2],
6000
+ prerelease
6001
+ };
6002
+ }
6003
+ function comparePrerelease(a, b) {
6004
+ if (a.length === 0 && b.length === 0) return 0;
6005
+ if (a.length === 0) return 1;
6006
+ if (b.length === 0) return -1;
6007
+ const length = Math.max(a.length, b.length);
6008
+ for (let index = 0; index < length; index += 1) {
6009
+ const left = a[index];
6010
+ const right = b[index];
6011
+ if (left === void 0) return -1;
6012
+ if (right === void 0) return 1;
6013
+ const leftNumeric = /^\d+$/.test(left);
6014
+ const rightNumeric = /^\d+$/.test(right);
6015
+ if (leftNumeric && rightNumeric) {
6016
+ const difference = Number(left) - Number(right);
6017
+ if (difference !== 0) return difference < 0 ? -1 : 1;
6018
+ continue;
6019
+ }
6020
+ if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
6021
+ if (left !== right) return left < right ? -1 : 1;
6022
+ }
6023
+ return 0;
6024
+ }
6025
+ /**
6026
+ * SemVer precedence comparison, including the prerelease rules. Returns
6027
+ * `undefined` for a value that is not a version this module can order, so an
6028
+ * unparseable host version fails closed rather than sorting as "newer".
6029
+ */
6030
+ function compareHostVersions(a, b) {
6031
+ const left = parseHostVersion(a);
6032
+ const right = parseHostVersion(b);
6033
+ if (!left || !right) return void 0;
6034
+ if (left.major !== right.major) return left.major < right.major ? -1 : 1;
6035
+ if (left.minor !== right.minor) return left.minor < right.minor ? -1 : 1;
6036
+ if (left.patch !== right.patch) return left.patch < right.patch ? -1 : 1;
6037
+ return comparePrerelease(left.prerelease, right.prerelease);
6038
+ }
6039
+ /** Decide the version-policy half of host support. Never a substitute for the graph lock. */
6040
+ function evaluateMinimumHostVersion(version, minimum = MIN_SUPPORTED_HOST_VERSION) {
6041
+ const comparison = compareHostVersions(version, minimum);
6042
+ if (comparison === void 0) return {
6043
+ status: "unparseable",
6044
+ version,
6045
+ minimum,
6046
+ reasonCode: "host_version_unparseable"
6047
+ };
6048
+ return comparison < 0 ? {
6049
+ status: "below_minimum",
6050
+ version,
6051
+ minimum,
6052
+ reasonCode: "host_version_below_minimum"
6053
+ } : {
6054
+ status: "supported",
6055
+ version,
6056
+ minimum,
6057
+ reasonCode: "host_version_supported"
6058
+ };
6059
+ }
6060
+ /** Whether npm's exact public support union admits this host version. */
6061
+ function satisfiesSupportedHostRange(version) {
6062
+ const normalized = version.trim();
6063
+ if (!parseHostVersion(normalized)) return false;
6064
+ return SUPPORTED_HOST_VERSIONS.some((supported) => compareHostVersions(normalized, supported) === 0);
6065
+ }
6066
+
4052
6067
  //#endregion
4053
6068
  //#region src/domain/host-lock.ts
4054
6069
  /**
4055
- * Capability expectations shared by every audited host cohort. The rc.1 audit
4056
- * found one host API change: Session event reads moved from `events` to
4057
- * `snapshotEvents()`. Guard adapts that API locally while the event vocabulary,
4058
- * flush contract, Goal disarm, `update_goal` gating, tool definition, and
4059
- * renderer terminal markers remain compatible.
6070
+ * Capability expectations shared by every registered cohort.
6071
+ *
6072
+ * Every row is a host contract Guard actually consumes, re-checked against the
6073
+ * 0.1.5-rc.1 package surfaces: `ctx.sessions.flush()` still returns whether a
6074
+ * durability listener participated; `tools.guard()` is still a monotonic
6075
+ * post-policy denial; the Goal service still exposes `get`/`disarm` with a
6076
+ * disarming `pause`; the `update_goal` tool is still the pinned pre-commit gate;
6077
+ * `ctx.jobs.get()` still yields the `dsh.jobs.v1` status vocabulary; and
6078
+ * `dsh-tool-fs` still registers `read`/`write`/`edit` with the same parameter
6079
+ * and result contract (`dsh.fs-tools.v1`).
6080
+ *
6081
+ * What is NOT a row, because it changed rather than stayed compatible: the
6082
+ * Session event API and vocabulary. Guard 0.5.1 supports only V3
6083
+ * `snapshotEvents()` and refuses a session that does not expose it, so a V2
6084
+ * host is rejected by the cohort's exact package rows before any capability row
6085
+ * is consulted.
4060
6086
  */
4061
6087
  const AUDITED_CAPABILITY_ROWS = [
4062
6088
  {
@@ -4116,20 +6142,32 @@ const AUDITED_CAPABILITY_ROWS = [
4116
6142
  }
4117
6143
  }))
4118
6144
  ];
4119
- function defineCohort(id, supportedGoalVersions, auditedPlatforms, packages) {
6145
+ function defineCohort(id, supportedGoalVersions, auditedPlatforms, packages, auditProvenance = "native-audited", acceptedPlatforms = auditedPlatforms) {
4120
6146
  return {
4121
6147
  id,
4122
6148
  manifestVersion: 1,
4123
6149
  supportedGoalVersions,
4124
6150
  auditedPlatforms,
6151
+ acceptedPlatforms,
6152
+ auditProvenance,
4125
6153
  packages,
4126
- capabilities: [{
4127
- name: "host_cohort",
4128
- value: {
4129
- k: "s",
4130
- v: id
4131
- }
4132
- }, ...AUDITED_CAPABILITY_ROWS]
6154
+ capabilities: [
6155
+ {
6156
+ name: "host_cohort",
6157
+ value: {
6158
+ k: "s",
6159
+ v: id
6160
+ }
6161
+ },
6162
+ {
6163
+ name: "host_audit_provenance",
6164
+ value: {
6165
+ k: "s",
6166
+ v: auditProvenance
6167
+ }
6168
+ },
6169
+ ...AUDITED_CAPABILITY_ROWS
6170
+ ]
4133
6171
  };
4134
6172
  }
4135
6173
  /**
@@ -4325,8 +6363,8 @@ const ALPHA2_DSHMARKET_139_HOST_PACKAGES = ALPHA2_HOST_PACKAGES.map((row) => row
4325
6363
  /**
4326
6364
  * Historical audited host cohort registry. Every entry keeps the exact package
4327
6365
  * identities audited natively for a past Guard release (CG-DSH-001 whole-graph
4328
- * contracts). These are historical verification facts only: since 0.5.0 the
4329
- * active support target is `0.1.2-rc.1`, so an installed graph from any of
6366
+ * contracts). These are historical verification facts only: since 0.5.1 the
6367
+ * active support targets are `0.1.5-rc.1` and `0.1.5-rc.2`, so an installed graph from any of
4330
6368
  * these cohorts — including previous RCs and alphas — is no longer an active
4331
6369
  * support entry and fails closed in `evaluateHostLock`.
4332
6370
  */
@@ -4506,15 +6544,22 @@ const LEGACY_HOST_COHORTS = [
4506
6544
  defineCohort("dsh-0.1.2-alpha.2", ["0.1.2-alpha.2"], ["posix", "windows"], ALPHA2_HOST_PACKAGES),
4507
6545
  defineCohort("dsh-0.1.2-alpha.2-dshmarket-1.39.0", ["0.1.2-alpha.2"], ["posix", "windows"], ALPHA2_DSHMARKET_139_HOST_PACKAGES),
4508
6546
  defineCohort("dsh-0.1.2-alpha.3", ["0.1.2-alpha.3"], ["posix", "windows"], ALPHA3_HOST_PACKAGES),
4509
- defineCohort("dsh-0.1.2-rc.1", ["0.1.2-rc.1"], ["posix", "windows"], RC1_HOST_PACKAGES)
6547
+ defineCohort("dsh-0.1.2-rc.1", ["0.1.2-rc.1"], ["posix", "windows"], RC1_HOST_PACKAGES),
6548
+ defineCohort("dsh-0.1.5-rc.1", ["0.1.5-rc.1"], [], RC015_HOST_PACKAGES, "registry-derived-pending-native-audit", ["posix", "windows"])
4510
6549
  ];
6550
+ /** Baseline cohort retained for callers that need a default fixture. */
6551
+ const ACTIVE_HOST_COHORT_ID = "dsh-0.1.5-rc.1";
6552
+ const ACTIVE_HOST_COHORT_IDS = [ACTIVE_HOST_COHORT_ID, "dsh-0.1.5-rc.2"];
4511
6553
  /** Core-lock/v1 separates optional market identity from the audited DSH graph.
4512
- * The active support target is exactly one audited cohort, `0.1.2-rc.1`:
6554
+ * The active support targets are the exact registered rc.1 and rc.2 graphs:
4513
6555
  * historical cohorts stay in `LEGACY_HOST_COHORTS` as verification data but are
4514
6556
  * never silently re-labelled as accepted active locks, and an installed
4515
- * historical graph fails closed under `evaluateHostLock`.
6557
+ * historical graph fails closed under `evaluateHostLock`. The version policy
6558
+ * (the exact rc.2-or-rc.1 public set) and the graph lock are separate
6559
+ * judgments: a host that has not been registered here is "unverified / pending
6560
+ * audit", never supported by version order alone.
4516
6561
  */
4517
- const HOST_COHORTS = LEGACY_HOST_COHORTS.filter((cohort) => cohort.id === "dsh-0.1.2-rc.1").map((cohort) => ({
6562
+ const HOST_COHORTS = [...LEGACY_HOST_COHORTS, defineCohort("dsh-0.1.5-rc.2", ["0.1.5-rc.2"], [], RC015_RC2_HOST_PACKAGES, "registry-derived-pending-native-audit", ["posix", "windows"])].filter((cohort) => ACTIVE_HOST_COHORT_IDS.includes(cohort.id)).map((cohort) => ({
4518
6563
  ...cohort,
4519
6564
  id: `${cohort.id}-core-v1`,
4520
6565
  manifestVersion: 2,
@@ -4534,17 +6579,30 @@ const HOST_COHORTS = LEGACY_HOST_COHORTS.filter((cohort) => cohort.id === "dsh-0
4534
6579
  v: "dsh-core/v1"
4535
6580
  }
4536
6581
  },
6582
+ {
6583
+ name: "host_audit_provenance",
6584
+ value: {
6585
+ k: "s",
6586
+ v: cohort.auditProvenance
6587
+ }
6588
+ },
4537
6589
  ...AUDITED_CAPABILITY_ROWS
4538
6590
  ]
4539
6591
  }));
4540
6592
  /**
4541
- * rc.1 audited package identities: the active support cohort since 0.5.0. The
4542
- * audited cohort is an atomic whole-graph contract (CG-DSH-001): any drifted,
4543
- * duplicated, unknown-version, unbound, OR MISSING row fails the whole lock
4544
- * closed (`host_lock_missing`); no capability inherits independence from a
4545
- * partially present graph.
6593
+ * Baseline fixture package identities (DSH 0.1.5-rc.1). The cohort
6594
+ * is an atomic whole-graph contract (CG-DSH-001): any drifted, duplicated,
6595
+ * unknown-version, unbound, OR MISSING row fails the whole lock closed
6596
+ * (`host_lock_missing`); no capability inherits independence from a partially
6597
+ * present graph.
4546
6598
  */
4547
6599
  const EXPECTED_HOST_PACKAGES = HOST_COHORTS[0].packages;
6600
+ /**
6601
+ * The `@deepseek-ai/dsh` launcher version of the baseline fixture, read from the
6602
+ * cohort rows rather than hardcoded, so a cohort bump cannot leave a stale
6603
+ * literal behind in the target-inspection path.
6604
+ */
6605
+ const ACTIVE_HOST_LAUNCHER_VERSION = EXPECTED_HOST_PACKAGES.find((row) => row.name === "@deepseek-ai/dsh")?.version;
4548
6606
  const packageNames = (...names) => new Set(names);
4549
6607
  const BASE_HOST_PACKAGES = packageNames("@deepseek-ai/cordis", "@deepseek-ai/dsh-agent", "@deepseek-ai/dsh-commands", "@deepseek-ai/dsh-llm", "@deepseek-ai/dsh-session", "@deepseek-ai/dsh-tools");
4550
6608
  const GOAL_HOST_PACKAGES = packageNames("@deepseek-ai/dsh-goal", "@deepseek-ai/dsh-tool-goal");
@@ -4559,6 +6617,16 @@ const HOST_CAPABILITY_PACKAGE_GROUPS = {
4559
6617
  filesystem: packageNames("@deepseek-ai/dsh-tool-fs", "@deepseek-ai/dsh-fs", "@deepseek-ai/dsh-fs-local", "@deepseek-ai/dsh-fs-sandbox", "@deepseek-ai/dsh-fs-observation-policy", "@deepseek-ai/dsh-sandbox", "@deepseek-ai/dsh-sandbox-policy", "@deepseek-ai/dsh-user-approval", "@deepseek-ai/dsh-attachment", "@deepseek-ai/dsh-system-prompt")
4560
6618
  };
4561
6619
  /**
6620
+ * The host version a package graph records, for the version-policy decision.
6621
+ *
6622
+ * Every DSH package versions with the host, so the graph's own `dsh` row is the
6623
+ * version the caller is running. A graph without that row leaves the version
6624
+ * unknown, and an unknown version is not treated as supported.
6625
+ */
6626
+ function hostVersionFromPackages(rows) {
6627
+ return rows.find((row) => row.name === "@deepseek-ai/dsh")?.version;
6628
+ }
6629
+ /**
4562
6630
  * Atomically select the audited cohort for one supplied package graph. A
4563
6631
  * graph matches a cohort only when every row carries version and integrity,
4564
6632
  * each exactly equals that cohort's audited row, and the graph covers the
@@ -4587,7 +6655,7 @@ function selectHostCohort(rows, platform) {
4587
6655
  const candidates = HOST_COHORTS.filter((cohort) => identityMatches.every((matches) => matches.includes(cohort)));
4588
6656
  const consistentCohort = candidates.filter((cohort) => cohort.packages.length === rows.length && cohort.packages.every((expected) => rows.filter((row) => row.name === expected.name).length === 1))[0] ?? candidates[0];
4589
6657
  if (consistentCohort !== void 0 && unboundCount === 0) {
4590
- if (platform && !consistentCohort.auditedPlatforms.includes(platform)) return {
6658
+ if (platform && !consistentCohort.acceptedPlatforms.includes(platform)) return {
4591
6659
  cohort: consistentCohort,
4592
6660
  consistent: false,
4593
6661
  reasonCode: "host_cohort_platform_not_audited"
@@ -4702,13 +6770,17 @@ function evaluateHostLock(rows, context = {}) {
4702
6770
  const missingPackages = cohort.packages.map((row) => row.name).filter((name) => (counts.get(name) ?? 0) === 0).sort((a, b) => a.localeCompare(b));
4703
6771
  const registryNames = new Set(HOST_COHORTS.flatMap((entry) => entry.packages.map((row) => row.name)));
4704
6772
  const unknown = supplied.find((row) => !registryNames.has(row.name));
6773
+ const hostVersionValue = context.hostVersion ?? hostVersionFromPackages(supplied);
6774
+ const hostVersion = hostVersionValue === void 0 ? void 0 : evaluateMinimumHostVersion(hostVersionValue);
4705
6775
  const baseResult = {
4706
6776
  digest: digest$1,
4707
6777
  goalAvailable,
4708
6778
  packages: supplied,
4709
6779
  capabilities,
4710
6780
  cohortId: cohort.id,
6781
+ auditProvenance: cohort.auditProvenance,
4711
6782
  missingPackages,
6783
+ ...hostVersion ? { hostVersion } : {},
4712
6784
  ...context.platform ? { platform: context.platform } : {},
4713
6785
  ...context.profileKind ? { profileKind: context.profileKind } : {}
4714
6786
  };
@@ -4784,6 +6856,12 @@ function evaluateHostLock(rows, context = {}) {
4784
6856
  reasonCode: failure.reasonCode
4785
6857
  };
4786
6858
  }
6859
+ if (hostVersion?.status === "below_minimum" || hostVersion?.status === "unparseable") return {
6860
+ ...baseResult,
6861
+ status: "unsupported",
6862
+ goalAvailable: false,
6863
+ reasonCode: hostVersion.status === "below_minimum" ? "host_lock_version_below_minimum" : "host_lock_version_unparseable"
6864
+ };
4787
6865
  return {
4788
6866
  ...baseResult,
4789
6867
  status: "supported"
@@ -4998,52 +7076,6 @@ function bindExecutableIdentity(resolution, effect) {
4998
7076
  }
4999
7077
  const DEFAULT_HOST_LOCK = evaluateHostLock(EXPECTED_HOST_PACKAGES);
5000
7078
 
5001
- //#endregion
5002
- //#region src/domain/goal-gate.ts
5003
- function hasCurrentCertificate(projection) {
5004
- const checkpoint = projection.checkpoints.at(-1);
5005
- let reason;
5006
- if (projection.integrity !== "valid") reason = "integrity_invalid";
5007
- else if (projection.hostStatus !== "supported") reason = "host_lock_unsupported";
5008
- else if (!checkpoint || checkpoint.result !== "certified") reason = "certificate_missing";
5009
- else if (checkpoint.epoch !== projection.epoch) reason = "stale_epoch";
5010
- else if (checkpoint.sessionRefDigest !== projection.sessionRefDigest) reason = "foreign_session";
5011
- else if (checkpoint.hostLockDigest !== projection.hostLockDigest) reason = "stale_host_lock";
5012
- else if (checkpoint.contractRevision !== projection.contractRevision) reason = "stale_contract_revision";
5013
- else if (projection.currentGoalRef ? checkpoint.goalRef?.id !== projection.currentGoalRef.id || checkpoint.goalRef.revision !== projection.currentGoalRef.revision : checkpoint.goalRef !== void 0) reason = "stale_goal_ref";
5014
- projection.certificateStatusReason = reason;
5015
- return reason === void 0;
5016
- }
5017
- /**
5018
- * Denies `update_goal(action=complete)` while the guard is enabled and no
5019
- * current completion certificate exists. The gate itself has no bypass; a
5020
- * workflow that genuinely finished but cannot certify (for example a contract
5021
- * polluted by session-layer talk, or evidence that lives in another session)
5022
- * has three explicit remediation routes:
5023
- *
5024
- * 1. `/context-guard off` disables the guard, so completion is no longer
5025
- * gated. Use only after the user confirms the work is actually done.
5026
- * 2. `/context-guard clear` supersedes every pending requirement and
5027
- * acceptance under a `CLEAR:<revision>` sentinel (prohibitions are
5028
- * retained) and bumps the contract revision; an empty-binding checkpoint
5029
- * can then certify while the guard stays enabled.
5030
- * 3. `update_goal(action=blocked)` records the blocker truthfully, which is
5031
- * never denied by this gate.
5032
- */
5033
- function goalCompletionDenial(projection, toolName, argumentsValue, configuredToolName = "update_goal") {
5034
- if (toolName !== configuredToolName || typeof argumentsValue !== "object" || argumentsValue === null) return void 0;
5035
- if (argumentsValue.action !== "complete") return void 0;
5036
- if (!projection.enabled) return void 0;
5037
- const args = argumentsValue;
5038
- if (projection.hostStatus !== "supported") return `Context Guard denial [stale_host]: host lock is unsupported or unavailable (${projection.hostReasonCode ?? "unknown_host"}).`;
5039
- if (!projection.currentGoalRef) return "Context Guard denial [no_goal]: no current Goal reference is available.";
5040
- if (args.goal_id !== projection.currentGoalRef.id || args.revision !== projection.currentGoalRef.revision) return "Context Guard denial [stale_goal_ref]: update_goal must use the exact current goal_id and revision.";
5041
- if (hasCurrentCertificate(projection)) return void 0;
5042
- if (projection.certificateStatusReason === "stale_host_lock") return "Context Guard denial [stale_host]: the completion certificate belongs to a different host identity.";
5043
- if (projection.certificateStatusReason === "stale_goal_ref") return "Context Guard denial [stale_goal_ref]: the completion certificate belongs to a different Goal reference.";
5044
- return projection.integrity === "valid" ? "Context Guard denial [certificate_missing]: a current completion certificate is required." : "Context Guard denial [certificate_missing]: integrity is unknown or corrupt, so no current certificate is usable.";
5045
- }
5046
-
5047
7079
  //#endregion
5048
7080
  //#region src/domain/shell-parse.ts
5049
7081
  const TWO_CHAR_OPS = new Set([
@@ -5781,13 +7813,19 @@ function structuredTerminalFacts(meta) {
5781
7813
  const rawSignal = record.signal;
5782
7814
  if (rawSignal !== void 0 && rawSignal !== null) return {
5783
7815
  exitCode: typeof rawExit === "number" ? rawExit : void 0,
5784
- negative: true
7816
+ negative: true,
7817
+ marked: true
5785
7818
  };
5786
7819
  if (typeof rawExit === "number") return {
5787
7820
  exitCode: rawExit,
5788
- negative: false
7821
+ negative: false,
7822
+ marked: true
5789
7823
  };
5790
7824
  }
7825
+ /** `[exit code: N]`, `[shell exited: code N]`, `[Command finished with exit code N]`. */
7826
+ const TERMINAL_EXIT_MARKER = /^\[(?:exit code|shell exited: code|command finished with exit code)\s*:?\s*(\d+)\]$/;
7827
+ /** Negative markers with no exit code of their own. */
7828
+ const TERMINAL_NEGATIVE_MARKER = /^\[(?:timed out[^\]]*|sandbox[^\]]*|killed by signal[^\]]*|shell killed by signal[^\]]*|shell exited|command timed out or oom|interrupted[^\]]*)\]$/;
5791
7829
  function extractTerminalFacts(textContent) {
5792
7830
  const lines = textContent.split(/\r?\n/);
5793
7831
  let index = lines.length - 1;
@@ -5800,19 +7838,23 @@ function extractTerminalFacts(textContent) {
5800
7838
  const timeoutIntroAtHead = resetStripped && lines.length > 0 && PERSISTENT_TIMEOUT_INTRO.test(lines[0].trim());
5801
7839
  let exitCode;
5802
7840
  let negative = timeoutIntroAtHead;
7841
+ let marked = timeoutIntroAtHead;
5803
7842
  while (index >= 0) {
5804
- const line = lines[index].trim();
5805
- const exitMatch = line.match(/^\[(?:exit code|shell exited: code)\s*:?\s*(\d+)\]$/);
5806
- const negativeLine = /^\[(?:timed out|sandbox[^\]]*|killed by signal[^\]]*|shell killed by signal[^\]]*|shell exited|interrupted[^\]]*)[^\]]*\]$/i.test(line);
7843
+ const line = lines[index].trim().toLowerCase();
7844
+ const exitMatch = line.match(TERMINAL_EXIT_MARKER);
5807
7845
  if (exitMatch) {
5808
7846
  if (exitCode === void 0) exitCode = Number(exitMatch[1]);
5809
- } else if (negativeLine) negative = true;
5810
- else break;
7847
+ marked = true;
7848
+ } else if (TERMINAL_NEGATIVE_MARKER.test(line)) {
7849
+ negative = true;
7850
+ marked = true;
7851
+ } else break;
5811
7852
  index -= 1;
5812
7853
  }
5813
7854
  return {
5814
7855
  exitCode,
5815
- negative
7856
+ negative,
7857
+ marked
5816
7858
  };
5817
7859
  }
5818
7860
  function metaUrls(meta) {
@@ -6030,7 +8072,8 @@ function extractToolSubject(call, result, defaultCwd, hostLock) {
6030
8072
  const commandCwd = typeof args.workdir === "string" ? args.workdir : defaultCwd;
6031
8073
  const action = structured?.semanticAction ?? semanticActionFromCommand(command);
6032
8074
  const deterministic = commandDetails.status === "supported" && !backgrounded && isDeterministicCheck(command);
6033
- const outcome = backgrounded ? "unknown" : result.error || terminal.negative ? "failure" : terminal.exitCode === void 0 ? call.name === "bash" || call.name === "pwsh" ? "success" : "unknown" : terminal.exitCode === 0 ? "success" : "failure";
8075
+ const unmarkedSuccessAllowed = (call.name === "bash" || call.name === "pwsh") && !terminal.marked;
8076
+ const outcome = backgrounded ? "unknown" : result.error || terminal.negative ? "failure" : terminal.exitCode === void 0 ? unmarkedSuccessAllowed ? "success" : "unknown" : terminal.exitCode === 0 ? "success" : "failure";
6034
8077
  const subject = {
6035
8078
  capabilities: ["shell", ...deterministic ? ["deterministic-check"] : []],
6036
8079
  subjects: unique(commandDetails.subjects),
@@ -6241,14 +8284,22 @@ function resolveArtifact(path$1, scope) {
6241
8284
  if (/^[A-Za-z]:[\\/]/.test(path$1) || path$1.startsWith("/") || path$1.startsWith("\\")) return path$1;
6242
8285
  return `${scope.cwd.replace(/[\\/]+$/, "")}/${path$1}`;
6243
8286
  }
6244
- /** Capture one canonical root text through the authority-block segmentation.
8287
+ /**
8288
+ * Capture one canonical root text through the authority-block segmentation.
6245
8289
  * `prefix` keeps the historical `m<seq>` source identity; a remainder uses
6246
- * `m<seq>:r` so confirmation follow-ups stay traceable to their message. */
6247
- function captureRootText(projection, text, seq, scope, protocolBoundarySeq, captureBoundarySeq, priorRootMessages, prefix = `m${seq}`) {
8290
+ * `m<seq>:r` so confirmation follow-ups stay traceable to their message.
8291
+ *
8292
+ * `legacy` marks a message that predates the first protocol boundary in this
8293
+ * log. Capture semantics are now version-independent (see `domain/semantics.ts`),
8294
+ * but a pre-boundary message keeps the historical authority relabelling rule:
8295
+ * an item whose action/target could not be derived deterministically stays
8296
+ * `legacy_authority_unclassified` instead of being retroactively authorized.
8297
+ */
8298
+ function captureRootText(projection, text, seq, scope, legacy, priorRootMessages, prefix = `m${seq}`, coordinationSplit = true) {
6248
8299
  const blocks = segmentAuthorityBlocks(text, priorRootMessages);
6249
8300
  for (const block$1 of blocks) {
6250
8301
  if (!block$1.capture) continue;
6251
- insertItems(projection, block$1.text, `${prefix}:${block$1.blockId}`, scope, block$1.authority === "root_adoption" ? "root_adoption" : "root_instruction", protocolBoundarySeq !== void 0 && seq < protocolBoundarySeq, block$1.kind === "instruction" || block$1.authority === "root_adoption", captureBoundarySeq !== void 0 && seq < captureBoundarySeq || captureBoundarySeq === void 0 && protocolBoundarySeq !== void 0 ? "v041" : "v042");
8302
+ insertItems(projection, block$1.text, `${prefix}:${block$1.blockId}`, scope, block$1.authority === "root_adoption" ? "root_adoption" : "root_instruction", legacy, block$1.kind === "instruction" || block$1.authority === "root_adoption", coordinationSplit);
6252
8303
  }
6253
8304
  priorRootMessages.push(text);
6254
8305
  if (priorRootMessages.length > 16) priorRootMessages.shift();
@@ -6259,16 +8310,30 @@ function captureRootText(projection, text, seq, scope, protocolBoundarySeq, capt
6259
8310
  * item, so evidence for one file cannot close a message that also covers other
6260
8311
  * files or embeds prohibitions.
6261
8312
  */
6262
- function insertItems(projection, text, sourceMessageId, scope, authority = "root_instruction", legacy = false, legacyAuthorityProven = false, captureVersion = "v042") {
8313
+ function insertItems(projection, text, sourceMessageId, scope, authority = "root_instruction", legacy = false, legacyAuthorityProven = false, coordinationSplit = true) {
6263
8314
  const before = new Set(projection.items.keys());
6264
- for (const segment of segmentClauses(text, captureVersion)) {
8315
+ for (const segment of segmentClauses(text, { coordinationSplit })) {
6265
8316
  if (classifyUserInteraction(segment.body) === "conversational") continue;
6266
8317
  if (segment.kind === "requirement" && segment.paths.length === 0 && isInstructionFraming(segment.body)) continue;
6267
- if (segment.kind === "prohibition" || segment.paths.length === 0) {
6268
- insert(projection, segment.kind, segment.body, sourceMessageId, scope.cwd || "scope", "scope", captureVersion);
8318
+ if (segment.paths.length === 0) {
8319
+ insert(projection, segment, sourceMessageId, scope.cwd || "scope", "scope");
6269
8320
  continue;
6270
8321
  }
6271
- for (const path$1 of segment.paths) insert(projection, segment.kind, segment.body, sourceMessageId, resolveArtifact(path$1, scope), "artifact", captureVersion);
8322
+ for (const path$1 of segment.paths) insert(projection, segment, sourceMessageId, resolveArtifact(path$1, scope), "artifact");
8323
+ }
8324
+ for (const [id, item] of projection.items) {
8325
+ if (before.has(id)) continue;
8326
+ if (item.kind !== "requirement" || item.waitAuthorization || item.authorityDisposition === "conditional_wait") continue;
8327
+ for (const [otherId, other] of projection.items) {
8328
+ if (otherId === id || other.status !== "pending") continue;
8329
+ if (!other.waitAuthorization || other.kind !== "requirement") continue;
8330
+ if (other.semanticAction !== item.semanticAction) continue;
8331
+ const action = item.semanticAction;
8332
+ if (!action || !isStatefulAction(action)) continue;
8333
+ if (!requestedTargetMatchesResolved(action, other.requestedTarget, item.requestedTarget)) continue;
8334
+ supersedeItem(projection.items, otherId, item);
8335
+ break;
8336
+ }
6272
8337
  }
6273
8338
  for (const [id, item] of projection.items) {
6274
8339
  if (before.has(id)) continue;
@@ -6283,11 +8348,13 @@ function insertItems(projection, text, sourceMessageId, scope, authority = "root
6283
8348
  else item.authority = authority;
6284
8349
  }
6285
8350
  }
6286
- function insert(projection, kind, body, sourceMessageId, subject, surface, captureVersion) {
8351
+ function insert(projection, segment, sourceMessageId, subject, surface) {
6287
8352
  const revision = projection.contractRevision + 1;
6288
- const id = nextId(projection.items, kind);
6289
- const item = captureItem(kind, body, sourceMessageId, id, revision, subject, surface, extractMethod(body), extractOperation(body), captureVersion);
6290
- const duplicate = [...projection.items.values()].find((existing) => existing.kind === kind && existing.status === "pending" && existing.textSha256 === item.textSha256 && existing.verification.subject === subject);
8353
+ const id = nextId(projection.items, segment.kind);
8354
+ const method = extractMethod(segment.body);
8355
+ const operation = extractOperation(segment.body);
8356
+ const item = captureItem(segment.kind, segment.body, sourceMessageId, id, revision, subject, surface, method, operation, segment.interpretation);
8357
+ const duplicate = [...projection.items.values()].find((existing) => existing.kind === segment.kind && existing.status === "pending" && existing.textSha256 === item.textSha256 && existing.verification.subject === subject);
6291
8358
  if (duplicate) supersedeItem(projection.items, duplicate.id, item);
6292
8359
  else projection.items.set(id, item);
6293
8360
  projection.contractRevision = item.revision;
@@ -6296,7 +8363,7 @@ function insert(projection, kind, body, sourceMessageId, subject, surface, captu
6296
8363
  * Pure, deterministic re-derivation of the guard projection from the DSH
6297
8364
  * native event log. Context Guard never writes custom session events, so every
6298
8365
  * piece of state is derived from `command/run`, `user/message`, `tool/call`,
6299
- * `tool/result`, `tool/code-dispatch-start`, `tool/code-dispatch`, and
8366
+ * `tool/result`, `tool/ptc-dispatch-start`, `tool/ptc-dispatch`, and
6300
8367
  * `compaction/summary`.
6301
8368
  */
6302
8369
  function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLock = DEFAULT_HOST_LOCK) {
@@ -6348,19 +8415,63 @@ function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLoc
6348
8415
  compacted = true;
6349
8416
  lastCompactionSeq = event.seq;
6350
8417
  break;
8418
+ case "turn/start": {
8419
+ const started = asRecord(event.data);
8420
+ if (typeof started?.turn === "number" && Number.isSafeInteger(started.turn)) projection.hostTurn = started.turn;
8421
+ break;
8422
+ }
6351
8423
  case "user/message": {
6352
8424
  if (isProtocolBoundaryNotice(event) || isProtocolBoundaryNotice(event, CAPTURE_V042_NOTICE) || isProtocolBoundaryNotice(event, PROTOCOL_V4_NOTICE)) break;
8425
+ {
8426
+ const record = asRecord(event.data);
8427
+ const recordSource = asRecord(record?.source);
8428
+ const recordText = extractTextContent(record?.content ?? []);
8429
+ if (recordSource?.kind === "plugin" && recordSource.plugin === "context-guard" && recordText.startsWith(NO_PROGRESS_RECORD_PREFIX)) {
8430
+ const parsed = asRecord(parseArguments(recordText.slice(NO_PROGRESS_RECORD_PREFIX.length)));
8431
+ const fingerprint$1 = typeof parsed?.fingerprint === "string" ? parsed.fingerprint : void 0;
8432
+ const attempt = typeof parsed?.attempt === "number" && Number.isSafeInteger(parsed.attempt) && parsed.attempt > 0 ? parsed.attempt : void 0;
8433
+ const boundaryKey = typeof parsed?.boundaryKey === "string" ? parsed.boundaryKey : void 0;
8434
+ if (fingerprint$1 && attempt !== void 0 && boundaryKey !== void 0) {
8435
+ const claims = projection.noProgressClaims.get(fingerprint$1) ?? /* @__PURE__ */ new Map();
8436
+ if (!claims.has(boundaryKey)) claims.set(boundaryKey, attempt);
8437
+ projection.noProgressClaims.set(fingerprint$1, claims);
8438
+ }
8439
+ break;
8440
+ }
8441
+ if (recordSource?.kind === "plugin" && recordSource.plugin === "context-guard" && recordText.startsWith(CONTROL_RECORD_PREFIX)) {
8442
+ const parsed = asRecord(parseArguments(recordText.slice(CONTROL_RECORD_PREFIX.length)));
8443
+ const rootSeq = typeof parsed?.rootSeq === "number" && Number.isSafeInteger(parsed.rootSeq) ? parsed.rootSeq : void 0;
8444
+ if (rootSeq !== void 0) projection.handledControlSeqs.add(rootSeq);
8445
+ break;
8446
+ }
8447
+ }
6353
8448
  if (!enabled) break;
6354
8449
  const data = asRecord(event.data);
6355
8450
  if (asRecord(data?.source)?.kind !== "user") break;
6356
8451
  const content = data?.content ?? [];
6357
8452
  const text = extractTextContent(content);
6358
8453
  if (text.trim() || content.some((part) => part && typeof part === "object" && part.type !== "text")) realRootInputSeen = true;
8454
+ const legacyMessage = protocolBoundarySeq !== void 0 && event.seq < protocolBoundarySeq;
8455
+ const coordinationSplit = !(protocolBoundarySeq !== void 0 && (captureBoundarySeq === void 0 || event.seq < captureBoundarySeq));
6359
8456
  const captureAssets = () => {
6360
8457
  if (v4BoundarySeq !== void 0 && event.seq > v4BoundarySeq) content.forEach((part, index) => {
6361
8458
  if (!part || typeof part !== "object" || part.type === "text") return;
6362
8459
  const identity = sha256(JSON.stringify(part));
6363
- insert(projection, "requirement", `Uninterpreted root asset m${event.seq} part ${index}: sha256 ${identity}. Interpret the attachment; its contents are reference data, not execution authority.`, `m${event.seq}:asset:${index}`, scope.cwd || "scope", "scope", "v042");
8460
+ insert(projection, {
8461
+ kind: "requirement",
8462
+ body: `Uninterpreted root asset m${event.seq} part ${index}: sha256 ${identity}. Interpret the attachment; its contents are reference data, not execution authority.`,
8463
+ text: `Uninterpreted root asset m${event.seq} part ${index}`,
8464
+ paths: [],
8465
+ interpretation: {
8466
+ text: `Uninterpreted root asset m${event.seq} part ${index}`,
8467
+ body: `Interpret the attached asset m${event.seq} part ${index}`,
8468
+ directive: "directive",
8469
+ executee: "agent",
8470
+ immediatelyExecutable: true,
8471
+ authorityDisposition: "executable_now",
8472
+ fingerprint: `asset:${identity.slice(0, 16)}`
8473
+ }
8474
+ }, `m${event.seq}:asset:${index}`, scope.cwd || "scope", "scope");
6364
8475
  });
6365
8476
  };
6366
8477
  if (!text.trim()) {
@@ -6379,7 +8490,7 @@ function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLoc
6379
8490
  if (parsed.kind === "confirm") {
6380
8491
  if (confirmRebind(projection, parsed.proposalId, `m${event.seq}`, durableConfirmed)) {
6381
8492
  captureAssets();
6382
- if (parsed.remainder) captureRootText(projection, parsed.remainder, event.seq, scope, protocolBoundarySeq, captureBoundarySeq, priorRootMessages, `m${event.seq}:r`);
8493
+ if (parsed.remainder) captureRootText(projection, parsed.remainder, event.seq, scope, legacyMessage, priorRootMessages, `m${event.seq}:r`, coordinationSplit);
6383
8494
  break;
6384
8495
  }
6385
8496
  } else if (parsed.kind !== "none") {
@@ -6391,14 +8502,14 @@ function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLoc
6391
8502
  };
6392
8503
  const stripped = text.split(/\r?\n/).filter((line) => !CONFIRM_LINE_PATTERN.test(line.trim())).join("\n");
6393
8504
  if (!stripped.trim()) break;
6394
- captureRootText(projection, stripped, event.seq, scope, protocolBoundarySeq, captureBoundarySeq, priorRootMessages);
8505
+ captureRootText(projection, stripped, event.seq, scope, legacyMessage, priorRootMessages, `m${event.seq}`, coordinationSplit);
6395
8506
  break;
6396
8507
  }
6397
8508
  }
6398
8509
  captureAssets();
6399
8510
  if (isInformationalMessage(text)) break;
6400
8511
  if (classifyUserInteraction(text) === "conversational") break;
6401
- captureRootText(projection, text, event.seq, scope, protocolBoundarySeq, captureBoundarySeq, priorRootMessages);
8512
+ captureRootText(projection, text, event.seq, scope, legacyMessage, priorRootMessages, `m${event.seq}`, coordinationSplit);
6402
8513
  break;
6403
8514
  }
6404
8515
  case "goal/change": {
@@ -6459,7 +8570,16 @@ function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLoc
6459
8570
  } } : {},
6460
8571
  ...typeof record?.resolution_evidence_id === "string" ? { resolutionEvidenceId: record.resolution_evidence_id } : {},
6461
8572
  ...typeof record?.effect_evidence_id === "string" ? { effectEvidenceId: record.effect_evidence_id } : {},
6462
- ...Array.isArray(record?.state_evidence_ids) ? { stateEvidenceIds: record.state_evidence_ids.map(String) } : {}
8573
+ ...Array.isArray(record?.state_evidence_ids) ? { stateEvidenceIds: record.state_evidence_ids.map(String) } : {},
8574
+ ...Array.isArray(record?.action_bindings) ? { actionBindings: record.action_bindings.map((entry) => {
8575
+ const closure = asRecord(entry);
8576
+ return {
8577
+ action: String(closure?.action ?? ""),
8578
+ evidenceIds: Array.isArray(closure?.evidence_ids) ? closure.evidence_ids.map(String) : [],
8579
+ resolvedTarget: asRecord(closure?.resolved_target) ?? {},
8580
+ order: Number(closure?.order ?? 0)
8581
+ };
8582
+ }) } : {}
6463
8583
  };
6464
8584
  }) : [];
6465
8585
  } else if (call.name === "context_guard_boundary") {
@@ -6474,7 +8594,7 @@ function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLoc
6474
8594
  pendingCalls.set(callId, call);
6475
8595
  break;
6476
8596
  }
6477
- case "tool/code-dispatch-start": {
8597
+ case "tool/ptc-dispatch-start": {
6478
8598
  if (!enabled) break;
6479
8599
  const data = asRecord(event.data);
6480
8600
  const subCallId = String(data?.subCallId ?? "");
@@ -6487,10 +8607,10 @@ function deriveProjection(sourceEvents, config, scope, durableConfirmed, hostLoc
6487
8607
  break;
6488
8608
  }
6489
8609
  case "tool/result":
6490
- case "tool/code-dispatch": {
8610
+ case "tool/ptc-dispatch": {
6491
8611
  if (!enabled) break;
6492
8612
  const data = asRecord(event.data);
6493
- const isDispatch = event.type === "tool/code-dispatch";
8613
+ const isDispatch = event.type === "tool/ptc-dispatch";
6494
8614
  const message = asRecord(data?.message);
6495
8615
  const source = asRecord(message?.source);
6496
8616
  const callId = String(source?.callId ?? (isDispatch ? data?.subCallId : "") ?? "");
@@ -6677,169 +8797,6 @@ function lifecyclePhase(input) {
6677
8797
  return input.realInputSeen ? "active" : "armed";
6678
8798
  }
6679
8799
 
6680
- //#endregion
6681
- //#region src/domain/stop-policy.ts
6682
- const QUOTED = /["'“”‘’`].*?(?:complete|done|finished|完成|做完|搞定).*?["'“”‘’`]/i;
6683
- const EXAMPLE = /\b(?:for example|e\.g\.|such as|like saying|例如|比如|举例|作为一个例子)\b/i;
6684
- const QUESTION = /\?[ \t]*$|\b(?:should|could|would|can|will|what|how|whether)\b.*\?/i;
6685
- const TRAILING_NEGATION = /\b(?:not (?:yet |quite |fully )?(?:complete|done|finished)|isn'?t (?:complete|done|finished)|hasn'?t (?:been )?(?:completed|finished)|尚未完成|还没完成|未完成|没有完成|还未完成)\b/i;
6686
- const CONDITIONAL = /\b(?:if|unless|once|when|whenever|provided that|只要|如果|假如|一旦|除非)\b/i;
6687
- const PARTIAL_ONLY = /\b(?:step|phase|stage|milestone)\s+\d+\b|第[一二三四五六七八九十\d]+\s*(?:步|阶段|环节)|(?:第一步|第二步|第三步)/i;
6688
- const WHOLE_COMPLETION_EN = /\b(?:the )?(?:task|work|job|everything|all tasks?|all work) (?:is|are) (?:now )?(?:complete|done|finished|completed)\b|\b(?:task|work) (?:has been )?(?:completed|finished)\b|\ball (?:tasks|work|requirements) (?:have been )?(?:completed|done|met)\b/i;
6689
- const WHOLE_COMPLETION_ZH = /(?:任务|工作|所有任务|全部工作|整体)(?:已经|已)?(?:全部)?(?:完成|搞定|做完)|(?:已|已经)(?:全部|所有)?(?:完成|搞定)(?:了)?(?:全部|所有)?(?:任务|工作)?/i;
6690
- /** Bare completion confirmations, e.g. "Done." or "搞定了。" */
6691
- const BARE_COMPLETION = /^(?:done|finished|completed|all\s+done)[.!]?$|^(?:已完成|完成了|搞定了|搞定|完成|done)[。..!!]?$/i;
6692
- /** Continuation intent following a claim makes it partial, not whole-task. */
6693
- const CONTINUATION = /接下来|下一步|然后|接着|继续|再去|最后再|还差|剩下|剩余|第二步|第三步|,\s*(?:next|then|after that|moving on)\b/i;
6694
- function looksQuotedOrExemplary(text) {
6695
- return QUOTED.test(text) || EXAMPLE.test(text);
6696
- }
6697
- function isWholeTaskCompletionClaim(text) {
6698
- const normalized = normalizeClause(text);
6699
- if (!normalized) return false;
6700
- if (QUESTION.test(normalized)) return false;
6701
- if (TRAILING_NEGATION.test(normalized)) return false;
6702
- if (CONDITIONAL.test(normalized)) return false;
6703
- if (CONTINUATION.test(normalized)) return false;
6704
- if (looksQuotedOrExemplary(normalized)) return false;
6705
- if (PARTIAL_ONLY.test(normalized) && !WHOLE_COMPLETION_EN.test(normalized) && !WHOLE_COMPLETION_ZH.test(normalized)) return false;
6706
- const firstLine = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
6707
- if (BARE_COMPLETION.test(normalizeTitleLine(firstLine))) return leadingBareCompletionClaim(text);
6708
- return BARE_COMPLETION.test(normalized) || WHOLE_COMPLETION_EN.test(normalized) || WHOLE_COMPLETION_ZH.test(normalized);
6709
- }
6710
- const DECORATION_LEAD = /^\s*(?:[\p{Extended_Pictographic}\u2764\u2705\u2714\u2716\u2728\u274C\u26A0\u2611\u2612\u2713\u2717\u274E\u2B50\u2B55\u2022\u00B7\u25E6\u25AA\u25AB\u25CF\u25CB\u25A0\u25A1\u2013\u2014-]|\uFE0F|\uFE0E|\u200D)+/u;
6711
- /** Strip a leading run of decorative glyphs from a title line. */
6712
- function stripDecorationPrefix(text) {
6713
- let value = text;
6714
- let previous = "";
6715
- while (value !== previous) {
6716
- previous = value;
6717
- value = value.replace(DECORATION_LEAD, "");
6718
- }
6719
- return value.replace(/^\s+/, "");
6720
- }
6721
- /**
6722
- * Normalize a title line for the bare-completion test. Markdown heading markers,
6723
- * fully-wrapping emphasis (`**…**`, `__…__`, `*…*`, `_…_`), and a leading run of
6724
- * decorative glyphs are removed ITERATIVELY until stable, because stripping one
6725
- * layer may expose another (`## ✅ **完成。**`). Blockquotes (`>`), quoted
6726
- * titles, and examples are left untouched so they still fail closed.
6727
- */
6728
- function normalizeTitleLine(line) {
6729
- let value = line.trim();
6730
- if (value.startsWith(">")) return value;
6731
- let previous = "";
6732
- while (value !== previous) {
6733
- previous = value;
6734
- value = value.replace(/^#{1,6}\s+/, "").replace(/^\*\*(.+?)\*\*$/, "$1").replace(/^__(.+?)__$/, "$1").replace(/^\*(.+?)\*$/, "$1").replace(/^_(.+?)_$/, "$1");
6735
- value = stripDecorationPrefix(value);
6736
- }
6737
- return value;
6738
- }
6739
- /**
6740
- * A reply whose first non-empty line is a standalone bare completion ("完成。"
6741
- * or "Done.") followed by a results summary. The whole text no longer matches
6742
- * the single-line BARE_COMPLETION anchor, but the summary must still be treated
6743
- * as a whole-task completion claim.
6744
- */
6745
- function leadingBareCompletionClaim(text) {
6746
- const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
6747
- const first = lines[0];
6748
- if (!first || !BARE_COMPLETION.test(normalizeTitleLine(first))) return false;
6749
- const rest = normalizeClause(lines.slice(1).join("\n"));
6750
- if (!rest) return true;
6751
- if (CONTINUATION.test(rest)) return false;
6752
- if (TRAILING_NEGATION.test(rest)) return false;
6753
- if (CONDITIONAL.test(rest)) return false;
6754
- if (looksQuotedOrExemplary(rest)) return false;
6755
- if (PARTIAL_ONLY.test(rest)) return false;
6756
- return true;
6757
- }
6758
- function classifyCompletionClaim(text) {
6759
- const normalized = normalizeClause(text);
6760
- if (/waiting for (?:you|the user|input|your)|please (?:review|confirm|approve)|等待(?:您|你|用户)|请(?:确认|审阅|批准)/i.test(normalized)) return "user_wait";
6761
- if (/waiting for (?:the )?(?:result|output|response|build|test|deployment)|等待(?:结果|输出|构建|测试|部署|响应)/i.test(normalized)) return "external_wait";
6762
- if (isWholeTaskCompletionClaim(normalized)) return "complete";
6763
- return "report";
6764
- }
6765
- /** Assistant prose is retained only as a bounded diagnostic observation. */
6766
- function observeAssistantOutcome(text) {
6767
- const disposition = classifyCompletionClaim(text);
6768
- if (disposition === "complete") return {
6769
- kind: "completion_claim",
6770
- reasonCode: "assistant_completion_claim_observed"
6771
- };
6772
- if (disposition === "user_wait") return {
6773
- kind: "user_wait_claim",
6774
- reasonCode: "assistant_user_wait_claim_observed"
6775
- };
6776
- if (disposition === "external_wait") return {
6777
- kind: "external_wait_claim",
6778
- reasonCode: "assistant_external_wait_claim_observed"
6779
- };
6780
- return {
6781
- kind: "report",
6782
- reasonCode: "assistant_report_observed"
6783
- };
6784
- }
6785
- /**
6786
- * Stop Protocol 2.0 decision. This function deliberately has no assistant-text
6787
- * parameter: completion wording, quotation, negation and translation cannot
6788
- * steer the protocol. A structured root persistence authorization may request
6789
- * one fallback correction; subsequent attempts safe-yield. An active, armed
6790
- * Goal remains exclusively owned by the host Goal Round Driver.
6791
- */
6792
- function decideTurnBoundary(projection) {
6793
- if (!projection.enabled) return {
6794
- action: "stop",
6795
- reason: "guard_disabled"
6796
- };
6797
- if (projection.integrity !== "valid") return {
6798
- action: "stop",
6799
- reason: "integrity_invalid_safe_yield"
6800
- };
6801
- if (hasCurrentCertificate(projection)) return {
6802
- action: "stop",
6803
- reason: "current_certificate"
6804
- };
6805
- const boundary = projection.boundaries.at(-1);
6806
- if (boundary?.persistedResult === "accepted" && boundary.epoch === projection.epoch && boundary.contractRevision === projection.contractRevision) return {
6807
- action: "stop",
6808
- reason: "accepted_boundary_pending_effectuation"
6809
- };
6810
- if (projection.currentGoalPhase === "active" && projection.currentGoalActivation === "armed") return {
6811
- action: "stop",
6812
- reason: "goal_round_driver_owns_continuation"
6813
- };
6814
- if ([...projection.items.values()].some((item) => item.status === "pending" && item.persistenceAuthorization)) {
6815
- const key = `${projection.epoch}:${projection.contractRevision}`;
6816
- const attempts = projection.persistenceCorrectionAttempts.get(key) ?? 0;
6817
- if (attempts < 1) {
6818
- projection.persistenceCorrectionAttempts.set(key, attempts + 1);
6819
- return {
6820
- action: "continue",
6821
- reason: "protocol_correction_steer"
6822
- };
6823
- }
6824
- }
6825
- return {
6826
- action: "stop",
6827
- reason: "safe_yield_pending_preserved"
6828
- };
6829
- }
6830
- function decideTurnStopping(projection, _assistantText, _turn, _maxAttempts) {
6831
- return decideTurnBoundary(projection);
6832
- }
6833
- function latestAssistantText(events) {
6834
- for (let index = events.length - 1; index >= 0; index--) {
6835
- const event = events[index];
6836
- if (event.type !== "assistant/message") continue;
6837
- const text = event.data.message?.content?.filter((block$1) => block$1.type === "text").map((block$1) => block$1.text ?? "").join("\n") ?? "";
6838
- if (text.trim()) return text;
6839
- }
6840
- return "";
6841
- }
6842
-
6843
8800
  //#endregion
6844
8801
  //#region src/domain/git-adapter.ts
6845
8802
  const GIT_COMMAND_MANIFEST_IDS = {
@@ -7129,25 +9086,81 @@ async function executeRevalidatedGitEffect(resolved, manifest, target, currentSt
7129
9086
  //#endregion
7130
9087
  //#region src/domain/session-events.ts
7131
9088
  /**
7132
- * Read a stable snapshot from both legacy DSH Sessions and the rc.1 Session
7133
- * API. rc.1 replaced the public `events` getter with `snapshotEvents()`; the
7134
- * structural adapter keeps older audited cohorts working without widening the
7135
- * accepted event contract.
9089
+ * Read a validated, stable event snapshot from the DSH Session V3 API.
9090
+ *
9091
+ * Session V3 replaced the V2 `events` getter with `snapshotEvents()`. Context
9092
+ * Guard supports only the V3 API: a session object that does not expose that
9093
+ * method is an unsupported host, never a reason to fall back to a legacy
9094
+ * accessor. Failing loud here keeps a V2-shaped object from being projected as
9095
+ * if its events had V3 semantics — the two vocabularies differ (surfaces,
9096
+ * `assistant/chunk` vs embedded streams, `session/end-seed` payload), so a
9097
+ * silent fallback would derive contract state from a log it cannot read.
9098
+ *
9099
+ * Guard is a READER of the durable log, so the envelope check below is the one
9100
+ * part of log validation it owns itself. The host validates a session it
9101
+ * constructs or restores; Guard additionally refuses a snapshot that is not a
9102
+ * sequence of event envelopes, because a projection that silently dropped or
9103
+ * mis-numbered an event would fabricate contract state rather than report a
9104
+ * damaged log.
9105
+ *
9106
+ * The V3 contract also asks a reader to refuse an unrecognized event type that
9107
+ * is not marked `ignorable`. Guard does NOT implement that half, deliberately:
9108
+ * the host's persistence reader already refuses such a log before publishing a
9109
+ * Session, and a whitelist of event types Guard happens to know would
9110
+ * false-refuse a healthy host whose composition registers a required event type
9111
+ * through a third-party plugin. The full rationale is in
9112
+ * `UPSTREAM_API_AUDIT.md`; revisit it there rather than adding a whitelist here.
9113
+ */
9114
+ const SESSION_API_UNSUPPORTED = "session_api_unsupported";
9115
+ const SESSION_EVENT_ENVELOPE_INVALID = "session_event_envelope_invalid";
9116
+ var SessionApiError = class extends Error {
9117
+ code;
9118
+ constructor(message, code = SESSION_API_UNSUPPORTED) {
9119
+ super(message);
9120
+ this.name = "SessionApiError";
9121
+ this.code = code;
9122
+ }
9123
+ };
9124
+ /**
9125
+ * Refuse a snapshot that is not a contiguous, correctly enveloped V3 log.
9126
+ *
9127
+ * `seq` must be a non-negative safe integer and `type` a non-empty string.
9128
+ * Contiguity is checked against the snapshot's own first sequence rather than
9129
+ * against zero, because a ranged read legitimately starts later.
7136
9130
  */
9131
+ function assertEventEnvelopes(events) {
9132
+ let expected;
9133
+ for (let index = 0; index < events.length; index += 1) {
9134
+ const event = events[index];
9135
+ if (!event || typeof event !== "object" || Array.isArray(event)) throw new SessionApiError(`snapshot event ${index} is not an object`, SESSION_EVENT_ENVELOPE_INVALID);
9136
+ const record = event;
9137
+ if (typeof record.type !== "string" || record.type.length === 0) throw new SessionApiError(`snapshot event ${index} has no event type`, SESSION_EVENT_ENVELOPE_INVALID);
9138
+ if (typeof record.seq !== "number" || !Number.isSafeInteger(record.seq) || record.seq < 0) throw new SessionApiError(`snapshot event ${index} has no sequence number`, SESSION_EVENT_ENVELOPE_INVALID);
9139
+ if (expected !== void 0 && record.seq !== expected) throw new SessionApiError(`snapshot event ${index} breaks sequence contiguity`, SESSION_EVENT_ENVELOPE_INVALID);
9140
+ expected = record.seq + 1;
9141
+ }
9142
+ }
7137
9143
  function snapshotSessionEvents(session) {
7138
- if (!session || typeof session !== "object") return [];
9144
+ if (!session || typeof session !== "object") throw new SessionApiError("a DSH Session object is required");
7139
9145
  const source = session;
7140
- if (typeof source.snapshotEvents === "function") {
7141
- const events = source.snapshotEvents.call(session);
7142
- return Array.isArray(events) ? events : [];
7143
- }
7144
- return Array.isArray(source.events) ? source.events : [];
9146
+ if (typeof source.snapshotEvents !== "function") throw new SessionApiError("session does not expose the DSH Session V3 snapshotEvents() API");
9147
+ const events = source.snapshotEvents.call(session);
9148
+ if (!Array.isArray(events)) throw new SessionApiError("snapshotEvents() did not return an event list");
9149
+ assertEventEnvelopes(events);
9150
+ return events;
7145
9151
  }
7146
9152
 
7147
9153
  //#endregion
7148
9154
  //#region src/domain/host-resolver.ts
7149
- /** Names audited in any registered cohort; rows outside the union are unknown. */
7150
- const CRITICAL_NAMES = [...new Set(HOST_COHORTS.flatMap((cohort) => cohort.packages.map((row) => row.name)))];
9155
+ /**
9156
+ * Names registered in any cohort; rows outside the union are unknown.
9157
+ *
9158
+ * Sorted, not inherited from cohort row order: the active cohort's own listing
9159
+ * order is a presentation choice, and letting it decide the resolution order of
9160
+ * `packageRowsFromPnpmLock` would make an unrelated cohort re-ordering look like
9161
+ * a lock-reading change.
9162
+ */
9163
+ const CRITICAL_NAMES = [...new Set(HOST_COHORTS.flatMap((cohort) => cohort.packages.map((row) => row.name)))].sort((a, b) => a.localeCompare(b));
7151
9164
  const HOST_LOCK_MARKER_BEGIN = "# >>> BEGIN DSH COMPLETION GUARD HOST LOCK (managed) >>>";
7152
9165
  const HOST_LOCK_MARKER_END = "# <<< END DSH COMPLETION GUARD HOST LOCK (managed) <<<";
7153
9166
  var HostProfileError = class extends Error {
@@ -7204,13 +9217,34 @@ function packageRowsFromPnpmLock(text, names = CRITICAL_NAMES) {
7204
9217
  return entries;
7205
9218
  });
7206
9219
  }
9220
+ /**
9221
+ * The production host verdict: the version floor and the exact-graph audit,
9222
+ * combined into the one answer a caller acts on.
9223
+ *
9224
+ * The two facts stay separable — `hostVersion` is always reported on the
9225
+ * evaluation — but a host below the supported floor is refused here even when
9226
+ * its graph matches an audited cohort, because no graph can lift a version
9227
+ * floor. Keeping this combination out of `evaluateHostLock` leaves that
9228
+ * function a pure graph audit, so a graph verdict is never overwritten by a
9229
+ * version verdict inside it.
9230
+ */
9231
+ function combineHostPolicy(evaluation) {
9232
+ const version = evaluation.hostVersion;
9233
+ if (version?.status !== "below_minimum" && version?.status !== "unparseable") return evaluation;
9234
+ return {
9235
+ ...evaluation,
9236
+ status: "unsupported",
9237
+ goalAvailable: false,
9238
+ reasonCode: version.status === "below_minimum" ? "host_lock_version_below_minimum" : "host_lock_version_unparseable"
9239
+ };
9240
+ }
7207
9241
  function resolveInstalledHostLock(moduleUrl = import.meta.url) {
7208
9242
  const lockPath = findUp(dirname(fileURLToPath(moduleUrl)), "pnpm-lock.yaml");
7209
- if (!lockPath) return evaluateHostLock([]);
9243
+ if (!lockPath) return combineHostPolicy(evaluateHostLock([]));
7210
9244
  try {
7211
- return evaluateHostLock(packageRowsFromPnpmLock(readFileSync(lockPath, "utf8")));
9245
+ return combineHostPolicy(evaluateHostLock(packageRowsFromPnpmLock(readFileSync(lockPath, "utf8"))));
7212
9246
  } catch {
7213
- return evaluateHostLock([]);
9247
+ return combineHostPolicy(evaluateHostLock([]));
7214
9248
  }
7215
9249
  }
7216
9250
  function activeGraphRecords(packageMapText) {
@@ -7379,13 +9413,14 @@ function inspectTargetHostGraph(runtimeRoot, profileRoot) {
7379
9413
  platform: process.platform === "win32" ? "windows" : "posix",
7380
9414
  profileKind: "headless"
7381
9415
  });
7382
- if (evaluation.status !== "supported" || evaluation.cohortId !== "dsh-0.1.2-rc.1-core-v1") throw new HostProfileError("target_runtime_unsupported", "dependency-free inspection requires the audited rc.1 core");
9416
+ const selectedCohort = HOST_COHORTS.find((cohort) => cohort.id === evaluation.cohortId);
9417
+ if (evaluation.status !== "supported" || !selectedCohort) throw new HostProfileError("target_runtime_unsupported", "dependency-free inspection requires the active audited core cohort");
7383
9418
  const { records, reachable } = activeGraphRecords(mapText);
7384
9419
  const launcher = realpathSync(join(modules, "@deepseek-ai", "dsh"));
7385
9420
  const anchor = join(launcher, "package.json");
7386
9421
  const host = readJsonObject(anchor, "target_runtime_unsupported");
7387
9422
  const launcherId = [...reachable].filter((id) => id === "@deepseek-ai/dsh" || id.startsWith("@deepseek-ai/dsh@"));
7388
- if (launcherId.length !== 1 || host.name !== "@deepseek-ai/dsh" || host.version !== "0.1.2-rc.1" || typeof records[launcherId[0]].url !== "string" || realpathSync(resolve(modules, records[launcherId[0]].url)) !== launcher || !within(modules, launcher)) throw new HostProfileError("target_runtime_unsupported", "launcher differs from the active runtime importer");
9423
+ if (launcherId.length !== 1 || host.name !== "@deepseek-ai/dsh" || host.version !== selectedCohort.packages.find((row) => row.name === "@deepseek-ai/dsh")?.version || typeof records[launcherId[0]].url !== "string" || realpathSync(resolve(modules, records[launcherId[0]].url)) !== launcher || !within(modules, launcher)) throw new HostProfileError("target_runtime_unsupported", "launcher differs from the active runtime importer");
7389
9424
  const bundleRows = names.map((name) => {
7390
9425
  const packageRoot = packageFromAnchor(anchor, name);
7391
9426
  const ids = [...reachable].filter((id) => id === name || id.startsWith(`${name}@`));
@@ -7868,4 +9903,4 @@ function proofEvidenceConstraints(evidence, obligation) {
7868
9903
  }
7869
9904
 
7870
9905
  //#endregion
7871
- export { isRunExecutable as $, itemDiagnosis as $t, revalidateGitPrestate as A, canonicalizePath as An, MIN_RECOVERY_CHAR_BUDGET as At, lifecyclePhase as B, createProjection as Bt, GIT_COMMAND_TEMPLATES as C, semanticActionFromText as Cn, segmentAuthorityBlocks as Ct, executeRevalidatedGitEffect as D, validateManifest as Dn, qualifyBoundary as Dt, createGitPrestateEnvelope as E, COMMAND_SURFACE_MANIFEST as En, isCurrentAcceptedBoundary as Et, isWholeTaskCompletionClaim as F, sha256 as Fn, bindingSatisfies as Ft, deriveProjection as G, rebindAttemptKey as Gt, CAPTURE_V042_NOTICE as H, proposeRebind as Ht, latestAssistantText as I, evidenceCoverage as It, extractTextContent as J, CONFIRM_LINE_PATTERN as Jt, supersedeItem as K, rebindResponse as Kt, observeAssistantOutcome as L, evidenceMatchesItem as Lt, classifyCompletionClaim as M, normalizeClause as Mn, openItems$1 as Mt, decideTurnBoundary as N, sanitizeClauseText as Nn, recoveryDigest as Nt, gitCommandMatchesTarget as O, classifyTaskIntent as On, certifyCheckpoint as Ot, decideTurnStopping as P, sanitizeUrl as Pn, renderRecoveryPacket as Pt, canonicalArgvFromCommand as Q, evidenceAvailabilityReason as Qt, FIRST_STEP_GUIDANCE as R, isVerifyingCapability as Rt, GIT_COMMAND_MANIFEST_IDS as S, semanticActionFromCommand as Sn, authorityCaptureCounts as St, commitTreeSnapshotDigest as T, validateActionTarget as Tn, effectuateBoundary as Tt, PROTOCOL_V3_NOTICE as U, proposeRebindOutcome as Ut, previewFirstStepInjection as V, confirmRebind as Vt, PROTOCOL_V4_NOTICE as W, proposeRebindV042 as Wt, isDeterministicCheck as X, parseConfirmationMessage as Xt, extractToolSubject as Y, isFrozenV042RebindResponse as Yt, withDurability as Z, deriveItemDiagnosis as Zt, readActiveHostGraph as _, SUPPORTED_EVIDENCE_ADAPTERS as _n, evaluateHostLock as _t, createProofManifest as a, extractMethod as an, ALPHA2_HOST_PACKAGES as at, verifyComposedHostLockDump as b, requestedTargetAuthorizesMutation as bn, RC1_HOST_PACKAGES as bt, sessionQuery as c, segmentClauses as cn, EXPECTED_HOST_PACKAGES as ct, hostLockContextFromComposedDump as d, ACTION_MANIFEST as dn, HOST_COHORTS as dt, relevantEvidence as en, parsePwshCommand as et, hostLockRowsFromComposedDump as f, ACTION_MANIFEST_VERSION as fn, LEGACY_HOST_COHORTS as ft, packageRowsFromPnpmLock as g, STOP_PROTOCOL_VERSION as gn, evaluateHostCapability as gt, packageRowsFromActiveGraph as h, STATEFUL_ACTIONS as hn, evaluateExternalWaitCapability as ht, canonicalProjection as i, extractArtifactPaths as in, ALPHA2_DSHMARKET_139_HOST_PACKAGES as it, verifiedLinearCommitReadback as j, digestStrings as jn, closingHint as jt, parseGitCommandManifest as k, classifyUserInteraction as kn, DEFAULT_RECOVERY_CHAR_BUDGET as kt, validateProofManifest as l, canonicalRegistryBase as ln, GOAL_HOST_PACKAGES as lt, inspectTargetHostGraph as m, SEMANTIC_ACTIONS as mn, bindLiveGoalCapability as mt, PROOF_PROTOCOL_VERSION as n, captureItem as nn, goalCompletionDenial as nt, proofDigest as o, extractOperation as on, BASE_HOST_PACKAGES as ot, injectActiveProfileHostLock as p, CERTIFICATE_VERSION as pn, bindExecutableIdentity as pt, evidenceFromPersistedToolResult as q, replayRebindResult as qt, bindProofToProjection as r, classifyClause as rn, hasCurrentCertificate as rt, proofEvidenceConstraints as s, isInformationalMessage as sn, DEFAULT_HOST_LOCK as st, PROOF_KINDS as t, captureClause as tn, parseShellCommand as tt, HostProfileError as u, npmEscapedPackageName as un, HOST_CAPABILITY_PACKAGE_GROUPS as ut, resolveActiveProfileHostLock as v, actionCompatible as vn, evaluateToolSurfaceCapability as vt, commitIndexSnapshotDigest as w, validateActionManifest as wn, availableBoundaryQualifications as wt, snapshotSessionEvents as x, requestedTargetMatchesResolved as xn, ALPHA3_HOST_PACKAGES as xt, resolveInstalledHostLock as y, isStatefulAction as yn, selectHostCohort as yt, claimedBatchHasRealRootInput as z, currentContractDigest as zt };
9906
+ export { parseShellCommand as $, isStatefulAction as $n, isWholeTaskCompletionClaim as $t, createGitPrestateEnvelope as A, extractMethod as An, RC1_HOST_PACKAGES as At, CAPTURE_V042_NOTICE as B, namedActions as Bn, renderRecoveryPacket as Bt, SESSION_EVENT_ENVELOPE_INVALID as C, evidenceAvailabilityReason as Cn, SUPPORTED_HOST_VERSIONS as Ct, GIT_COMMAND_TEMPLATES as D, captureItem as Dn, satisfiesSupportedHostRange as Dt, GIT_COMMAND_MANIFEST_IDS as E, captureClause as En, parseHostVersion as Et, verifiedLinearCommitReadback as F, interpretMessage as Fn, DEFAULT_RECOVERY_CHAR_BUDGET as Ft, evidenceFromPersistedToolResult as G, ACTION_MANIFEST as Gn, CONTROL_RECORD_PREFIX as Gt, PROTOCOL_V4_NOTICE as H, statefulActionsOfScope as Hn, evidenceCoverage as Ht, FIRST_STEP_GUIDANCE as I, isExecutableItem as In, MIN_RECOVERY_CHAR_BUDGET as It, isDeterministicCheck as J, SEMANTIC_ACTIONS as Jn, classifyCompletionClaim as Jt, extractTextContent as K, ACTION_MANIFEST_VERSION as Kn, NO_PROGRESS_RECORD_PREFIX as Kt, claimedBatchHasRealRootInput as L, isOpenObligation as Ln, closingHint as Lt, gitCommandMatchesTarget as M, isInformationalMessage as Mn, authorityCaptureCounts as Mt, parseGitCommandManifest as N, segmentClauses as Nn, segmentAuthorityBlocks as Nt, commitIndexSnapshotDigest as O, classifyClause as On, RC015_RC2_HOST_PACKAGES as Ot, revalidateGitPrestate as P, interpretClause as Pn, certifyCheckpoint as Pt, parsePwshCommand as Q, actionCompatible as Qn, isRootPauseRequest as Qt, lifecyclePhase as R, kindOfScope as Rn, openItems$1 as Rt, SESSION_API_UNSUPPORTED as S, deriveItemDiagnosis as Sn, SUPPORTED_HOST_RANGE as St, snapshotSessionEvents as T, relevantEvidence as Tn, evaluateMinimumHostVersion as Tt, deriveProjection as U, canonicalRegistryBase as Un, evidenceMatchesItem as Ut, PROTOCOL_V3_NOTICE as V, semanticActionOfScope as Vn, bindingSatisfies as Vt, supersedeItem as W, npmEscapedPackageName as Wn, isVerifyingCapability as Wt, canonicalArgvFromCommand as X, STOP_PROTOCOL_VERSION as Xn, decideTurnStopping as Xt, withDurability as Y, STATEFUL_ACTIONS as Yn, decideTurnBoundary as Yt, isRunExecutable as Z, SUPPORTED_EVIDENCE_ADAPTERS as Zn, decisionBoundaryKey as Zt, packageRowsFromPnpmLock as _, rebindResponse as _n, evaluateToolSurfaceCapability as _t, createProofManifest as a, hasCurrentCertificate as an, validateActionTarget as ar, BASE_HOST_PACKAGES as at, resolveInstalledHostLock as b, isFrozenV042RebindResponse as bn, LATEST_SUPPORTED_HOST_VERSION as bt, sessionQuery as c, isCurrentAcceptedBoundary as cn, classifyTaskIntent as cr, GOAL_HOST_PACKAGES as ct, combineHostPolicy as d, createProjection as dn, digestStrings as dr, LEGACY_HOST_COHORTS as dt, latestAssistantText as en, requestedTargetAuthorizesMutation as er, ACTIVE_HOST_COHORT_ID as et, hostLockContextFromComposedDump as f, confirmRebind as fn, normalizeClause as fr, bindExecutableIdentity as ft, packageRowsFromActiveGraph as g, rebindAttemptKey as gn, evaluateHostLock as gt, inspectTargetHostGraph as h, proposeRebindV042 as hn, sha256 as hr, evaluateHostCapability as ht, canonicalProjection as i, goalCompletionDenial as in, validateActionManifest as ir, ALPHA2_HOST_PACKAGES as it, executeRevalidatedGitEffect as j, extractOperation as jn, ALPHA3_HOST_PACKAGES as jt, commitTreeSnapshotDigest as k, extractArtifactPaths as kn, RC015_HOST_PACKAGES as kt, validateProofManifest as l, qualifyBoundary as ln, classifyUserInteraction as lr, HOST_CAPABILITY_PACKAGE_GROUPS as lt, injectActiveProfileHostLock as m, proposeRebindOutcome as mn, sanitizeUrl as mr, evaluateExternalWaitCapability as mt, PROOF_PROTOCOL_VERSION as n, observeAssistantOutcome as nn, semanticActionFromCommand as nr, ACTIVE_HOST_LAUNCHER_VERSION as nt, proofDigest as o, availableBoundaryQualifications as on, COMMAND_SURFACE_MANIFEST as or, DEFAULT_HOST_LOCK as ot, hostLockRowsFromComposedDump as p, proposeRebind as pn, sanitizeClauseText as pr, bindLiveGoalCapability as pt, extractToolSubject as q, CERTIFICATE_VERSION as qn, NO_PROGRESS_TURNS_BEFORE_STOP as qt, bindProofToProjection as r, progressFingerprint as rn, semanticActionFromText as rr, ALPHA2_DSHMARKET_139_HOST_PACKAGES as rt, proofEvidenceConstraints as s, effectuateBoundary as sn, validateManifest as sr, EXPECTED_HOST_PACKAGES as st, PROOF_KINDS as t, latestRootInstruction as tn, requestedTargetMatchesResolved as tr, ACTIVE_HOST_COHORT_IDS as tt, HostProfileError as u, currentContractDigest as un, canonicalizePath as ur, HOST_COHORTS as ut, readActiveHostGraph as v, replayRebindResult as vn, hostVersionFromPackages as vt, SessionApiError as w, itemDiagnosis as wn, compareHostVersions as wt, verifyComposedHostLockDump as x, parseConfirmationMessage as xn, MIN_SUPPORTED_HOST_VERSION as xt, resolveActiveProfileHostLock as y, CONFIRM_LINE_PATTERN as yn, selectHostCohort as yt, previewFirstStepInjection as z, maskCodeSpans as zn, recoveryDigest as zt };