@itookit/dsht 0.3.8 → 0.5.1

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.
Files changed (130) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +30 -11
  3. package/README.zh.md +30 -11
  4. package/dist/cli/dsht.js +203 -18
  5. package/dist/cli/startup.d.ts +40 -0
  6. package/dist/cli/startup.js +295 -0
  7. package/dist/cli/trace-summary.d.ts +78 -0
  8. package/dist/cli/trace-summary.js +241 -0
  9. package/dist/cli/verifier.d.ts +60 -0
  10. package/dist/cli/verifier.js +242 -0
  11. package/dist/contracts.d.ts +344 -0
  12. package/dist/contracts.js +1 -0
  13. package/dist/controller/commands.d.ts +47 -0
  14. package/dist/controller/commands.js +322 -0
  15. package/dist/controller/connection.d.ts +11 -29
  16. package/dist/controller/connection.js +26 -60
  17. package/dist/controller/controller.d.ts +616 -166
  18. package/dist/controller/controller.js +1395 -146
  19. package/dist/controller/index.d.ts +8 -1
  20. package/dist/controller/index.js +5 -0
  21. package/dist/controller/loop-contract.d.ts +136 -0
  22. package/dist/controller/loop-contract.js +308 -0
  23. package/dist/controller/loop-prompts-schema.d.ts +56 -0
  24. package/dist/controller/loop-prompts-schema.js +144 -0
  25. package/dist/controller/loop-prompts.d.ts +55 -0
  26. package/dist/controller/loop-prompts.generated.d.ts +104 -0
  27. package/dist/controller/loop-prompts.generated.js +185 -0
  28. package/dist/controller/loop-prompts.js +104 -0
  29. package/dist/controller/loop-protocols.d.ts +39 -0
  30. package/dist/controller/loop-protocols.js +115 -0
  31. package/dist/controller/loop.d.ts +275 -0
  32. package/dist/controller/loop.js +378 -0
  33. package/dist/controller/prompts.d.ts +54 -0
  34. package/dist/controller/prompts.js +162 -0
  35. package/dist/controller/trace-log.d.ts +45 -0
  36. package/dist/controller/trace-log.js +144 -0
  37. package/dist/controller/verifier.d.ts +126 -0
  38. package/dist/controller/verifier.js +75 -0
  39. package/dist/cost/index.d.ts +1 -1
  40. package/dist/cost/index.js +1 -1
  41. package/dist/cost/ledger.d.ts +0 -1
  42. package/dist/cost/ledger.js +0 -1
  43. package/dist/json.d.ts +18 -0
  44. package/dist/json.js +19 -0
  45. package/dist/references.d.ts +25 -0
  46. package/dist/references.js +26 -0
  47. package/dist/session/connection-view.d.ts +2 -11
  48. package/dist/session/controller.d.ts +73 -72
  49. package/dist/session/controller.js +185 -209
  50. package/dist/session/history.d.ts +6 -18
  51. package/dist/session/history.js +1 -24
  52. package/dist/session/index.d.ts +9 -4
  53. package/dist/session/index.js +7 -3
  54. package/dist/session/info.d.ts +25 -52
  55. package/dist/session/info.js +39 -25
  56. package/dist/session/markdown.js +1 -1
  57. package/dist/session/math.js +1 -1
  58. package/dist/session/mutation-gate.d.ts +51 -0
  59. package/dist/session/mutation-gate.js +73 -0
  60. package/dist/session/navigation.d.ts +2 -89
  61. package/dist/session/navigation.js +2 -129
  62. package/dist/session/peek.d.ts +38 -0
  63. package/dist/session/peek.js +103 -0
  64. package/dist/session/references.d.ts +2 -20
  65. package/dist/session/references.js +1 -26
  66. package/dist/session/runtime.d.ts +26 -0
  67. package/dist/session/runtime.js +28 -0
  68. package/dist/session/telemetry.d.ts +12 -13
  69. package/dist/session/telemetry.js +27 -58
  70. package/dist/session/transcript.d.ts +0 -6
  71. package/dist/session/transcript.js +2 -15
  72. package/dist/session/types.d.ts +25 -0
  73. package/dist/session/types.js +0 -1
  74. package/dist/session-title.d.ts +9 -0
  75. package/dist/session-title.js +21 -0
  76. package/dist/shell/controller.d.ts +31 -1
  77. package/dist/shell/controller.js +34 -2
  78. package/dist/shell/index.d.ts +3 -3
  79. package/dist/shell/index.js +2 -2
  80. package/dist/shell/runner.d.ts +10 -0
  81. package/dist/shell/runner.js +48 -9
  82. package/dist/slash/index.d.ts +10 -0
  83. package/dist/slash/index.js +7 -0
  84. package/dist/slash/parse.d.ts +166 -0
  85. package/dist/slash/parse.js +259 -0
  86. package/dist/slash/pipeline.d.ts +140 -0
  87. package/dist/slash/pipeline.js +115 -0
  88. package/dist/slash/registry.d.ts +88 -0
  89. package/dist/slash/registry.js +177 -0
  90. package/dist/state.d.ts +14 -4
  91. package/dist/state.js +3 -2
  92. package/dist/text.d.ts +28 -0
  93. package/dist/text.js +55 -0
  94. package/dist/transport/events.d.ts +104 -0
  95. package/dist/transport/events.js +149 -0
  96. package/dist/transport/wire.d.ts +9 -17
  97. package/dist/transport/wire.js +2 -27
  98. package/dist/ui/app.js +856 -441
  99. package/dist/ui/chat/header.js +1 -1
  100. package/dist/ui/chat/history-view.d.ts +1 -1
  101. package/dist/ui/chat/loop-status.d.ts +11 -0
  102. package/dist/ui/chat/loop-status.js +28 -0
  103. package/dist/ui/chat/navigation-model.d.ts +86 -0
  104. package/dist/ui/chat/navigation-model.js +107 -0
  105. package/dist/ui/chat/shell-view.d.ts +15 -2
  106. package/dist/ui/chat/shell-view.js +37 -3
  107. package/dist/ui/chat/status.d.ts +47 -3
  108. package/dist/ui/chat/status.js +65 -50
  109. package/dist/ui/chat/viewport.d.ts +1 -1
  110. package/dist/ui/dialogs/cost.d.ts +21 -4
  111. package/dist/ui/dialogs/cost.js +7 -12
  112. package/dist/ui/dialogs/index.d.ts +22 -5
  113. package/dist/ui/dialogs/index.js +19 -3
  114. package/dist/ui/dialogs/loop.d.ts +43 -0
  115. package/dist/ui/dialogs/loop.js +224 -0
  116. package/dist/ui/dialogs/peek.d.ts +25 -0
  117. package/dist/ui/dialogs/peek.js +35 -0
  118. package/dist/ui/dialogs/picker.d.ts +2 -0
  119. package/dist/ui/dialogs/picker.js +4 -2
  120. package/dist/ui/input/mouse.d.ts +12 -2
  121. package/dist/ui/input/mouse.js +20 -7
  122. package/dist/ui/input/references.d.ts +1 -1
  123. package/dist/ui/status/model.d.ts +7 -0
  124. package/dist/ui/status/model.js +5 -0
  125. package/dist/ui/theme/index.d.ts +1 -1
  126. package/package.json +6 -4
  127. package/dist/ui/commands/parse.d.ts +0 -104
  128. package/dist/ui/commands/parse.js +0 -135
  129. package/dist/ui/commands/registry.d.ts +0 -33
  130. package/dist/ui/commands/registry.js +0 -73
@@ -1,5 +1,12 @@
1
1
  /** Controller domain: the application facade and the connection it owns. */
2
2
  export { Controller } from './controller.ts';
3
- export type { HistorySearch, RemovalTarget, State } from './controller.ts';
3
+ export type { ControllerOptions, HistorySearch, RemovalTarget, SavedPrompt, State } from './controller.ts';
4
+ export { removalIntent, runCommand } from './commands.ts';
5
+ export type { CommandPort, RunnableCommand } from './commands.ts';
6
+ export { loopProtocolFor, loopProtocolNames, roundStandard } from './loop-protocols.ts';
7
+ export { LOOP_MARKER, LOOP_STATUSES, followUpContract, resultContract } from './loop-contract.ts';
8
+ export { ScoredLoop, parseLoopResult, resolveLoop } from './loop.ts';
9
+ export type { LoopLimits, LoopProtocol, LoopResult, LoopStepResult } from './loop.ts';
10
+ export { PromptStore, MAX_PROMPT_CHARS, MAX_SAVED_PROMPTS } from './prompts.ts';
4
11
  export { ConnectionController } from './connection.ts';
5
12
  export type { ConnectionListener, ConnectionOptions } from './connection.ts';
@@ -1,3 +1,8 @@
1
1
  /** Controller domain: the application facade and the connection it owns. */
2
2
  export { Controller } from "./controller.js";
3
+ export { removalIntent, runCommand } from "./commands.js";
4
+ export { loopProtocolFor, loopProtocolNames, roundStandard } from "./loop-protocols.js";
5
+ export { LOOP_MARKER, LOOP_STATUSES, followUpContract, resultContract } from "./loop-contract.js";
6
+ export { ScoredLoop, parseLoopResult, resolveLoop } from "./loop.js";
7
+ export { PromptStore, MAX_PROMPT_CHARS, MAX_SAVED_PROMPTS } from "./prompts.js";
3
8
  export { ConnectionController } from "./connection.js";
@@ -0,0 +1,136 @@
1
+ /** The result contract every loop protocol appends, so one parser serves them all.
2
+ *
3
+ * A protocol owns its objective text; this module owns how the reply is scored and what block must
4
+ * end it. Keeping it in one place is what lets every `/loop` record and any later protocol
5
+ * share `parseLoopResult` and the same verification wording.
6
+ */
7
+ import { type LoopLimits, type LoopResult, type PriorVerdict } from './loop.ts';
8
+ /** Fence marker shared by every loop protocol's result block. */
9
+ export declare const LOOP_MARKER = "dsht-loop";
10
+ /** The status values a result block may declare. */
11
+ export declare const LOOP_STATUSES = "done|retry|blocked|abstained";
12
+ /** How a verifier may stop a round early, shared by the work brief and the verdict brief.
13
+ *
14
+ * Both stops are claims about the task, so each is written the same way wherever it is read: a
15
+ * reason a person can check, no score to hide behind, and a label that agrees with the status. A
16
+ * block that breaks any of these is not a verdict at all, so the run reports verification unusable
17
+ * instead of guessing what was meant.
18
+ * @returns The rules as lines, ready to append to a brief.
19
+ */
20
+ export declare function earlyStopLines(): string[];
21
+ /** What a protocol tells the verifier, beyond the score threshold.
22
+ *
23
+ * A protocol that names its steps supplies its own `standard` (its checklist for that step); the
24
+ * operator's `/verify` text is folded in by the protocol that has one, so this module never has to
25
+ * guess where a standard came from.
26
+ */
27
+ export interface VerificationBrief {
28
+ /** Standard the verifier scores against, checked item by item. */
29
+ standard?: string;
30
+ /** Workspace artifact the verifier must read, when the protocol requires one. */
31
+ artifact?: string;
32
+ /** What this step is about, when the protocol names its steps. */
33
+ focus?: string;
34
+ /** This step ends a run over the whole record, so this version may not break any earlier round. */
35
+ final?: boolean;
36
+ }
37
+ /** How the reply is scored, by whom, and the block that must end it.
38
+ *
39
+ * The verifier is a fresh subagent because the agent that produced the artifact cannot judge it
40
+ * fairly, and a fresh context cannot see the conversation, so the artifact must be on disk.
41
+ * @param kind - Protocol kind the block must declare.
42
+ * @param limits - Resolved run limits.
43
+ * @param step - Step in flight.
44
+ * @param attempt - Attempt in flight.
45
+ * @param brief - Standard, artifact and focus the verifier needs.
46
+ * @param mode - `subagent` asks the agent to spawn a grader; `forked` says one is started for it.
47
+ * @returns The contract as lines, ready to append to a brief.
48
+ */
49
+ export declare function resultContract(kind: string, limits: LoopLimits, step: number, attempt: number, brief?: VerificationBrief, mode?: 'subagent' | 'forked', selfScoring?: boolean): string[];
50
+ /** The shorter clause a later attempt ends with, once the protocol is already in context.
51
+ * @param kind - Protocol kind the block must declare.
52
+ * @param step - Step in flight.
53
+ * @param attempt - Attempt in flight.
54
+ * @returns The clause as one line.
55
+ */
56
+ export declare function followUpContract(kind: string, step: number, attempt: number, selfScoring?: boolean): string;
57
+ /** What the forked verifier must judge, and where its verdict has to land.
58
+ *
59
+ * The verifier runs as its own `dsht` process against its own session, so it cannot see the review
60
+ * conversation at all: the artifact on disk and this brief are its whole input, and the verdict file
61
+ * is its whole output.
62
+ */
63
+ export interface VerdictBrief extends VerificationBrief {
64
+ /** Identity of this verification, echoed into the verdict so a file cannot be misread. */
65
+ verificationId: string;
66
+ /** Protocol kind the verdict must declare. */
67
+ kind: string;
68
+ /** Step in flight. */
69
+ step: number;
70
+ /** Attempt in flight. */
71
+ attempt: number;
72
+ /** Absolute path of the JSON file the verifier session must write. */
73
+ file: string;
74
+ /** What the previous attempt on this step concluded, when there was one. */
75
+ previous?: PriorVerdict;
76
+ /** Every earlier round's requirements, when this step has to re-check the whole record. */
77
+ coverage?: readonly {
78
+ title: string;
79
+ checks: string;
80
+ }[];
81
+ /** The heading the round's section carries in the artifact, as the client checks it.
82
+ *
83
+ * The verifier marks the section it judged, and the client marks the section it accepts: giving both
84
+ * the same string is what keeps a section from another run against another document out of the round.
85
+ */
86
+ marker?: string;
87
+ /** The run's record variables, resolved: what this run is about, e.g. `path` for the reviewed file.
88
+ *
89
+ * The verifier cannot see the review conversation, so without these it can only guess the subject
90
+ * from the artifact — and an artifact left by an earlier run against another document then reads as
91
+ * this run's (a live run scored the previous document twice).
92
+ */
93
+ vars?: Readonly<Record<string, string>>;
94
+ }
95
+ /** What a retry must be told, so it answers the verdict instead of guessing.
96
+ *
97
+ * A score alone leaves the reviewer re-deriving what was wrong; the verifier's own findings are the
98
+ * only account of why the attempt failed, so they are quoted back verbatim.
99
+ * @param result - Verdict that ended the previous attempt.
100
+ * @returns Lines to append to the follow-up prompt, empty when the verdict explained nothing.
101
+ */
102
+ export declare function findingsLines(result: LoopResult): string[];
103
+ /** The prompt one forked verifier session receives.
104
+ *
105
+ * It states its independence, the evidence it must gather, the artifact it must not change, and the
106
+ * exact file it must write, because that file is the only channel back to the review.
107
+ * @param brief - Kind, step, attempt, path, standard, artifact and focus.
108
+ * @returns The prompt as one string.
109
+ */
110
+ export declare function verdictBrief(brief: VerdictBrief): string;
111
+ /** Undefined escape sequences made literal, so one backslash cannot discard a whole verdict.
112
+ *
113
+ * A model writing a regular expression or a Windows path inside a JSON string emits `\d` or `\C`
114
+ * without doubling the backslash, and `JSON.parse` then rejects the entire object — spending a review
115
+ * attempt on a formatting slip (a live run lost a valid `score 8.4 · done` verdict to `\d+\.\d+`
116
+ * sitting in `evidence`). Only escapes JSON does not define are rewritten, and the identity check
117
+ * below still decides which round a candidate belongs to, so this cannot admit another round's verdict.
118
+ * @param text - One candidate object's text.
119
+ * @returns The same object with undefined escapes doubled.
120
+ */
121
+ export declare function repairJsonEscapes(text: string): string;
122
+ /** Read one forked verifier's verdict.
123
+ *
124
+ * The reply is written by another process and by a model, so it is treated as untrusted input: the
125
+ * identity has to match the round being judged, or a stale or quoted object would score the wrong
126
+ * attempt. Later objects win, because the instruction is to end the reply with the verdict.
127
+ * @param text - Reply or file contents.
128
+ * @param expect - Identity, kind, step and attempt the verdict must declare.
129
+ * @returns The verdict's usable fields, or undefined when no candidate declared this round.
130
+ */
131
+ export declare function parseVerdict(text: string, expect: {
132
+ verificationId: string;
133
+ kind: string;
134
+ step: number;
135
+ attempt: number;
136
+ }): LoopResult | undefined;
@@ -0,0 +1,308 @@
1
+ /** The result contract every loop protocol appends, so one parser serves them all.
2
+ *
3
+ * A protocol owns its objective text; this module owns how the reply is scored and what block must
4
+ * end it. Keeping it in one place is what lets every `/loop` record and any later protocol
5
+ * share `parseLoopResult` and the same verification wording.
6
+ */
7
+ import { readResultFields } from "./loop.js";
8
+ /** Fence marker shared by every loop protocol's result block. */
9
+ export const LOOP_MARKER = 'dsht-loop';
10
+ /** The status values a result block may declare. */
11
+ export const LOOP_STATUSES = 'done|retry|blocked|abstained';
12
+ /** How a verifier may stop a round early, shared by the work brief and the verdict brief.
13
+ *
14
+ * Both stops are claims about the task, so each is written the same way wherever it is read: a
15
+ * reason a person can check, no score to hide behind, and a label that agrees with the status. A
16
+ * block that breaks any of these is not a verdict at all, so the run reports verification unusable
17
+ * instead of guessing what was meant.
18
+ * @returns The rules as lines, ready to append to a brief.
19
+ */
20
+ export function earlyStopLines() {
21
+ return [
22
+ '提前停下(只有这两种,都必须给 reason,都不许给 score):',
23
+ '- 任务在当前约束下被证明无法完成:status 用 "blocked"(可选 exit_reason: "cannot-fix"),'
24
+ + 'reason 写清为什么不可完成、已经排除过哪些路径。',
25
+ '- 必须由人决定才能继续:status 用 "abstained"(可选 exit_reason: "needs-human"),'
26
+ + 'reason 写清需要人决定什么,needs 写清具体要人提供什么。',
27
+ 'exit_reason 与 status 必须一致;缺 reason、同时给出两种判断、或带着 score 提前停下,'
28
+ + '都会被当成「没有可用判断」,本轮不计分。',
29
+ 'explanation 是可选的说明(例如「本轮无需改动」):它只作解释,不改变评分,也不跳过任何未验证的范围。',
30
+ ];
31
+ }
32
+ /** How the reply is scored, by whom, and the block that must end it.
33
+ *
34
+ * The verifier is a fresh subagent because the agent that produced the artifact cannot judge it
35
+ * fairly, and a fresh context cannot see the conversation, so the artifact must be on disk.
36
+ * @param kind - Protocol kind the block must declare.
37
+ * @param limits - Resolved run limits.
38
+ * @param step - Step in flight.
39
+ * @param attempt - Attempt in flight.
40
+ * @param brief - Standard, artifact and focus the verifier needs.
41
+ * @param mode - `subagent` asks the agent to spawn a grader; `forked` says one is started for it.
42
+ * @returns The contract as lines, ready to append to a brief.
43
+ */
44
+ export function resultContract(kind, limits, step, attempt, brief = {}, mode = 'subagent', selfScoring = mode !== 'forked') {
45
+ const { standard, artifact, focus, final } = brief;
46
+ return [
47
+ '评分与验证:',
48
+ `1. 产出物必须落到工作区${artifact === undefined ? '' : `(${artifact})`},因为验证者在全新上下文里看不到本对话。`,
49
+ ...(mode === 'forked'
50
+ ? [
51
+ '2. 本轮由 dsht 启动的独立验证进程单独评分:它有自己的 session、自己的上下文,会读产出物并自己取证,你无法影响它的判断。',
52
+ '3. 不要 spawn 子代理替你评分,也不要自评;只要完成本步工作,并在回复里简要列出改了什么、依据是什么。',
53
+ ...(selfScoring
54
+ // The verifier's verdict normally decides, but the operator allowed the reply block to stand
55
+ // in when it cannot judge — so that block still has to be there.
56
+ ? ['4. 结尾仍需按下面的格式给出块:验证进程无法判断时,本轮采用它。']
57
+ // Nothing reads a block here, so asking for one costs output tokens and shows the reader a
58
+ // score that moves nothing (the confusing part of the old wording).
59
+ : ['4. 不要输出 dsht-loop 块:本轮的分数只来自那个独立验证进程,回复里的块不会被读取。']),
60
+ ]
61
+ : [
62
+ '2. 每次尝试都要 spawn 一个全新的 verifier 子代理(subagent,独立上下文),把「原始目标 + 本步焦点 + 产出物 + 评分标准」交给它独立打分;不要用主回复替代它的判断。',
63
+ '3. 只有当前环境确实没有 subagent 能力时,才允许自评,并在 status 中注明 self-scored。',
64
+ ]),
65
+ ...(focus === undefined ? [] : ['', `本步焦点:${focus}`]),
66
+ ...(final === true ? ['', `本轮是本次 run(第 ${limits.from}–${limits.to} 轮)的收尾轮:写这一版时,前面每一轮已经满足的要求都必须仍然满足;`
67
+ + '若为了本轮改动而破坏了任何前序要求,必须在本轮改回,否则本轮不算完成。'] : []),
68
+ '',
69
+ ...(standard === undefined
70
+ ? ['评分标准:未提供;按原始目标的完成度评分。']
71
+ : ['评分标准(逐条对照):', standard]),
72
+ ...(selfScoring ? [
73
+ `分数为 0–10(允许小数)。score 小于 ${limits.score} 时 status 必须是 retry,并列出仍未解决的问题。`,
74
+ ...earlyStopLines(),
75
+ `结尾必须输出唯一一个 \`\`\`${LOOP_MARKER} 代码块,并且它必须是回复正文的最后内容:`,
76
+ `{"kind":"${kind}","step":${step},"attempt":${attempt},"score":X,"status":"${LOOP_STATUSES}","evidence":"...","top_findings":["..."]}`,
77
+ 'status 为 blocked 或 abstained 时不要 score,改为给 "reason";abstained 再加上 "needs"。',
78
+ 'evidence 必须给出评分的依据:跑过的命令与结果、看到的具体失败、或验证者引用的原文。',
79
+ ] : [
80
+ '结论写在工作正文里即可(发现的问题、依据、改法):本轮的分数、status 与 findings 都由那个独立验证进程给出,'
81
+ + '它读产出物和你写在产出物里的结论,不读你的回复格式。',
82
+ ]),
83
+ ];
84
+ }
85
+ /** The shorter clause a later attempt ends with, once the protocol is already in context.
86
+ * @param kind - Protocol kind the block must declare.
87
+ * @param step - Step in flight.
88
+ * @param attempt - Attempt in flight.
89
+ * @returns The clause as one line.
90
+ */
91
+ export function followUpContract(kind, step, attempt, selfScoring = true) {
92
+ return selfScoring
93
+ ? `结尾仍然只输出一个 \`\`\`${LOOP_MARKER} JSON 块,`
94
+ + `kind=${kind}、step=${step}、attempt=${attempt}、score 为本次评分、status 为 ${LOOP_STATUSES}。`
95
+ : '结尾不需要输出 dsht-loop 块:本轮分数同样只由独立验证进程给出。';
96
+ }
97
+ /** What a retry must be told, so it answers the verdict instead of guessing.
98
+ *
99
+ * A score alone leaves the reviewer re-deriving what was wrong; the verifier's own findings are the
100
+ * only account of why the attempt failed, so they are quoted back verbatim.
101
+ * @param result - Verdict that ended the previous attempt.
102
+ * @returns Lines to append to the follow-up prompt, empty when the verdict explained nothing.
103
+ */
104
+ export function findingsLines(result) {
105
+ const lines = [];
106
+ if (result.evidence !== undefined)
107
+ lines.push(`评分依据(来自验证):${result.evidence}`);
108
+ if (result.findings !== undefined && result.findings.length > 0) {
109
+ lines.push('验证者认为仍未解决的问题:');
110
+ for (const [index, finding] of result.findings.entries())
111
+ lines.push(`${index + 1}. ${finding}`);
112
+ lines.push('逐条处理上面的问题,不要只做与它们无关的改动。');
113
+ }
114
+ return lines.length === 0 ? [] : ['', ...lines];
115
+ }
116
+ /** The prompt one forked verifier session receives.
117
+ *
118
+ * It states its independence, the evidence it must gather, the artifact it must not change, and the
119
+ * exact file it must write, because that file is the only channel back to the review.
120
+ * @param brief - Kind, step, attempt, path, standard, artifact and focus.
121
+ * @returns The prompt as one string.
122
+ */
123
+ export function verdictBrief(brief) {
124
+ const { verificationId: identity, kind, step, attempt, standard, artifact, focus, previous, coverage, vars, marker } = brief;
125
+ return [
126
+ `你是独立验证者:验证 kind=${kind} 的第 ${step} 轮第 ${attempt} 次尝试。`,
127
+ '你没有本次评审的对话上下文,也不属于被验证的 session;你的判断只能来自磁盘上的产出物和你自己跑出来的证据。',
128
+ '',
129
+ ...(artifact === undefined ? [] : [`待验证产出物:${artifact}(在工作区中,自行阅读;不要修改它)。`]),
130
+ ...(vars === undefined || Object.keys(vars).length === 0 ? [] : [
131
+ `本次 run 的记录变量:${Object.entries(vars).map(([key, value]) => `${key}=${value}`).join('、')}`,
132
+ '产出物必须属于这次 run 所指的同一个对象(例如同一份被评审文档)。小节内容谈的是别的对象、'
133
+ + '或明显来自更早的 run 时,本轮按不满足处理。',
134
+ ]),
135
+ ...(marker === undefined ? [] : [
136
+ `本轮在产出物中的小节标题:${marker}`,
137
+ '以这个标题定位本轮小节;标题不符、只有同名但不同对象的旧小节、或该小节缺失时,本轮按不满足处理。',
138
+ ]),
139
+ ...(focus === undefined ? [] : [`本轮焦点:${focus}`]),
140
+ ...(coverage === undefined || coverage.length === 0 ? [] : [
141
+ '',
142
+ `这是本次 run 覆盖全部 ${coverage.length + 1} 轮的最后一次验证,所以本轮不只看本轮焦点:`
143
+ + '本版产出物必须**同时**仍然满足下面每一轮的要求。逐轮复核,把被后来的改动破坏的要求写进 top_findings 并据此扣分——'
144
+ + '本轮通过意味着整份产出物通过,而不只是最后这一轮通过。',
145
+ '',
146
+ '前面各轮的要求(逐轮复核):',
147
+ ...coverage.map(round => `【${round.title}】\n${round.checks}`),
148
+ ]),
149
+ ...(previous === undefined ? [] : [
150
+ '',
151
+ `上一次(第 ${previous.step} 轮第 ${previous.attempt} 次)验证给出的分数是 ${previous.result.score ?? '未给出'}:`,
152
+ ...(previous.result.findings === undefined || previous.result.findings.length === 0
153
+ ? ['(没有留下具体问题清单。)']
154
+ : ['上次仍未解决的问题:', ...previous.result.findings.map((finding, index) => `${index + 1}. ${finding}`)]),
155
+ '请优先逐条确认这些问题是否真的已经解决;没有解决的必须继续计入本轮评分。',
156
+ ]),
157
+ '',
158
+ '验证要求:',
159
+ '1. 亲自核对,不要相信任何未经验证的说法:跑命令、读源码、对照文档与实现。',
160
+ '2. 逐条对照下面的评分标准,指出每条是满足、部分满足还是不满足。',
161
+ '3. 不要修改产出物;你只负责判断。',
162
+ '',
163
+ ...(standard === undefined
164
+ ? ['评分标准:未提供;按产出的完成度与准确性评分。']
165
+ : ['评分标准(逐条对照):', standard]),
166
+ '',
167
+ `分数为 0–10(允许小数)。score 小于 8 时 status 必须是 retry 并列出仍未解决的问题。`,
168
+ ...earlyStopLines(),
169
+ '',
170
+ '返回方式:在回复正文的最后输出唯一一个 JSON 对象,不要加代码块围栏,也不要用工具去写文件:',
171
+ `{"verificationId":"${identity}","kind":"${kind}","step":${step},"attempt":${attempt},"score":X,"status":"${LOOP_STATUSES}","evidence":"...","top_findings":["..."]}`,
172
+ 'status 为 blocked 或 abstained 时不要 score,改为给 "reason";abstained 再加上 "needs"。',
173
+ 'evidence 必须写出评分依据:跑过的命令与结果、看到的具体失败、或引用的原文。',
174
+ '客户端会读取你回复里的这个对象并落盘;不要自己创建、修改或删除 verdict 文件。',
175
+ ].join('\n');
176
+ }
177
+ /** Undefined escape sequences made literal, so one backslash cannot discard a whole verdict.
178
+ *
179
+ * A model writing a regular expression or a Windows path inside a JSON string emits `\d` or `\C`
180
+ * without doubling the backslash, and `JSON.parse` then rejects the entire object — spending a review
181
+ * attempt on a formatting slip (a live run lost a valid `score 8.4 · done` verdict to `\d+\.\d+`
182
+ * sitting in `evidence`). Only escapes JSON does not define are rewritten, and the identity check
183
+ * below still decides which round a candidate belongs to, so this cannot admit another round's verdict.
184
+ * @param text - One candidate object's text.
185
+ * @returns The same object with undefined escapes doubled.
186
+ */
187
+ export function repairJsonEscapes(text) {
188
+ // Written with explicit characters because the whole job is counting backslashes: a `\\` in this
189
+ // file is one character, and the repair has to produce two.
190
+ const backslash = String.fromCharCode(92);
191
+ const defined = `"${backslash}/bfnrt`;
192
+ let out = '';
193
+ for (let index = 0; index < text.length; index += 1) {
194
+ const char = text[index];
195
+ if (char !== backslash) {
196
+ out += char;
197
+ continue;
198
+ }
199
+ const next = text[index + 1];
200
+ if (next === undefined) {
201
+ out += backslash + backslash;
202
+ continue;
203
+ }
204
+ const valid = defined.includes(next)
205
+ || next === 'u' && /^[0-9a-fA-F]{4}$/.test(text.slice(index + 2, index + 6));
206
+ out += valid ? char + next : backslash + backslash + next;
207
+ index += 1;
208
+ }
209
+ return out;
210
+ }
211
+ /** Every top-level JSON object in one reply, last one first.
212
+ *
213
+ * A review reply is prose with evidence in it, so the verdict is not the only braces in the text: a
214
+ * quoted snippet before or after it used to be swallowed by a first-brace-to-last-brace slice and
215
+ * made the whole verdict unparsable. Each balanced top-level object is a candidate instead.
216
+ * Braces inside strings are ignored: evidence quotes the record, which is full of `{{placeholders}}`,
217
+ * and counting those as nesting ended the candidate early, so a complete verdict read as unparsable.
218
+ * @param text - Reply or file contents.
219
+ * @returns Parsed objects, most recent first.
220
+ */
221
+ function jsonObjects(text) {
222
+ const objects = [];
223
+ for (let index = 0; index < text.length; index += 1) {
224
+ // A JSON object starts with `{"`, so prose quotes cannot desynchronize the scan and a brace
225
+ // inside a string (evidence quoting `{{placeholders}}`) cannot end the candidate early.
226
+ if (text[index] !== '{' || text[index + 1] !== '"')
227
+ continue;
228
+ const candidateStart = index;
229
+ const end = objectEnd(text, candidateStart);
230
+ if (end === -1)
231
+ continue;
232
+ index = end - 1;
233
+ const span = text.slice(candidateStart, end);
234
+ const parsed = parseObject(span);
235
+ if (parsed !== undefined)
236
+ objects.push(parsed);
237
+ }
238
+ return objects.reverse();
239
+ }
240
+ /** Index just past the `}` that closes the object opened at `start`, or -1 when it never closes.
241
+ * @param text - Text to scan.
242
+ * @param start - Index of the opening `{`.
243
+ * @returns The exclusive end index, or -1.
244
+ */
245
+ function objectEnd(text, start) {
246
+ let depth = 0;
247
+ let inString = false;
248
+ let escaped = false;
249
+ for (let index = start; index < text.length; index += 1) {
250
+ const char = text[index];
251
+ if (inString) {
252
+ if (escaped)
253
+ escaped = false;
254
+ else if (char === '\\')
255
+ escaped = true;
256
+ else if (char === '"')
257
+ inString = false;
258
+ continue;
259
+ }
260
+ if (char === '"') {
261
+ inString = true;
262
+ continue;
263
+ }
264
+ if (char === '{')
265
+ depth += 1;
266
+ else if (char === '}') {
267
+ depth -= 1;
268
+ if (depth === 0)
269
+ return index + 1;
270
+ }
271
+ }
272
+ return -1;
273
+ }
274
+ /** One candidate object, parsed as written and then with its undefined escapes repaired.
275
+ * @param span - Text from `{` to its matching `}`.
276
+ * @returns The object, or undefined when both readings fail.
277
+ */
278
+ function parseObject(span) {
279
+ for (const candidate of [span, repairJsonEscapes(span)]) {
280
+ try {
281
+ const parsed = JSON.parse(candidate);
282
+ if (typeof parsed === 'object' && parsed !== null)
283
+ return parsed;
284
+ }
285
+ catch { /* try the repaired reading, then give up on this candidate */ }
286
+ }
287
+ return undefined;
288
+ }
289
+ /** Read one forked verifier's verdict.
290
+ *
291
+ * The reply is written by another process and by a model, so it is treated as untrusted input: the
292
+ * identity has to match the round being judged, or a stale or quoted object would score the wrong
293
+ * attempt. Later objects win, because the instruction is to end the reply with the verdict.
294
+ * @param text - Reply or file contents.
295
+ * @param expect - Identity, kind, step and attempt the verdict must declare.
296
+ * @returns The verdict's usable fields, or undefined when no candidate declared this round.
297
+ */
298
+ export function parseVerdict(text, expect) {
299
+ for (const body of jsonObjects(text)) {
300
+ // The identity is checked before the round, so a verdict from another run is never usable.
301
+ if (body.verificationId !== expect.verificationId)
302
+ continue;
303
+ if (body.kind !== expect.kind || body.step !== expect.step || body.attempt !== expect.attempt)
304
+ continue;
305
+ return readResultFields(body);
306
+ }
307
+ return undefined;
308
+ }
@@ -0,0 +1,56 @@
1
+ /** The shape of loop.yaml and the rules an editor must not break.
2
+ *
3
+ * Kept apart from `loop-prompts.ts` so the build script can validate a YAML file before the
4
+ * generated module exists: this module imports nothing, while the renderer imports the generated
5
+ * data. The validator is the single copy of the schema — the generator and the tests both call it.
6
+ */
7
+ /** One round: the label the progress line shows, and the checklist that round is judged against. */
8
+ export interface LoopRoundText {
9
+ readonly title: string;
10
+ readonly checks: string;
11
+ }
12
+ /** One protocol record as it appears in loop.yaml. */
13
+ export interface LoopProtocolText {
14
+ /** Progress label; may use the record's own `vars`, e.g. `Designdoc review · {{path}}`. */
15
+ readonly title: string;
16
+ readonly steps: number;
17
+ readonly artifact?: string;
18
+ /** Line the artifact must contain once the round is done; a hard condition no score can override. */
19
+ readonly artifactMarker?: string;
20
+ readonly fallbackLabel: string;
21
+ readonly verifyFocus?: string;
22
+ /** Extra requirements folded on top of every round's rubric; replaces the old `/verify`. */
23
+ readonly standard?: string;
24
+ /** Fixed inputs this record's templates may use (a document path, a target, a threshold…). */
25
+ readonly vars?: Readonly<Record<string, string>>;
26
+ /** Per-record score / tries, overriding the global defaults below. */
27
+ readonly defaults?: {
28
+ readonly score?: number;
29
+ readonly tries?: number;
30
+ };
31
+ /** Which phase a step starts in: verifying the existing artifact, or working on it. */
32
+ readonly starts?: 'verify' | 'work';
33
+ readonly rounds: readonly LoopRoundText[];
34
+ readonly brief: readonly string[];
35
+ readonly followUp: readonly string[];
36
+ }
37
+ /** The whole document; the generated module is cast to this shape. */
38
+ export interface LoopPromptSource {
39
+ readonly version: number;
40
+ readonly defaults: {
41
+ readonly score: number;
42
+ readonly tries: number;
43
+ };
44
+ readonly protocols: Readonly<Record<string, LoopProtocolText>>;
45
+ }
46
+ /** Placeholders every template may use; a record's own `vars` add to these. */
47
+ export declare const LOOP_PLACEHOLDERS: readonly string[];
48
+ /** Record names that belong to `/loop` itself, so a protocol cannot shadow a subcommand. */
49
+ export declare const RESERVED_PROTOCOL_NAMES: readonly string[];
50
+ /** Check one parsed document against the schema the renderer relies on.
51
+ *
52
+ * Every message names the field at fault, because the only reader is a maintainer editing YAML.
53
+ * @param source - Parsed loop.yaml, or anything else that claims to be one.
54
+ * @returns One message per problem; an empty list means valid.
55
+ */
56
+ export declare function validateLoopPrompts(source: unknown): string[];