@abelxiaoxing/cadence 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,8 @@ Implement 只暴露 `start`、`status`、`resume`、`rebind`、`cancel` 和 `dis
20
20
  `status` 完全本地可用;相同 operation id 幂等重放,进程、会话或 Worker 更换后仍从 durable checkpoint 继续。
21
21
  Design 从第一步起统一使用 `action: "design"`:新需求通过 `start(requirement)` 进入,已有 change 通过 `start(change)` 进入,后续只携带返回的 `runId`。
22
22
  需求、决策合同与 Gate A 合同由控制面规范化并计算哈希,调用方无需 SHA-256 工具;Gate B 自动绑定当前已编译 canonical plan,原始瞬时文本不会写入 durable journal。
23
+ `validate-plan-draft` 可在 `compile-plan` 前只读运行同一套编译检查,并以结构化 `taskId` / phase / field / verification 诊断定位问题;Design finalization 的安全诊断码也会进入错误详情与可展开 TUI,而不再只显示统一失败标题。
24
+ Design status 将可调用的 `legalOperations` 与顶层 `packetActions` 分开;显式退出只能发送 `{"action":"finish"}`,不能伪装成 `operation: "finish"`。
23
25
 
24
26
  普通 artifact、错误 Red、transport、environment、stale、conflict、baseline 与验证失败都留在同一 Implement run 中恢复。
25
27
  累计验证发现 introduced failure 时会自动重开责任任务做有界修复;自动预算耗尽后 `resume` 复用已有 baseline 和 phase facts。
@@ -28,7 +30,7 @@ Design 激活期间,父模型只保留进入前已启用的 `read`、`grep`、
28
30
  Implementation Worker 不再手写 unified-diff header、hunk range、分段或哈希;它一次提交有序的 `replace`、`rewrite`、`create`、`delete` 操作。
29
31
  可信控制面在隔离 workspace 中校验精确文本、批准路径和 symlink 安全,生成并内部分块 sealed candidate;超限时保留可恢复状态而不接受截断 patch。
30
32
  OpenSpec change 制品只能通过私有 `write-artifact` / `delete-artifact` 原子操作变更;产品文件、AGENTS、`gate-a.yaml`、`ready.yaml` 和 `implement-plan.json` 对该通道不可达。
31
- Gate A/B 收据使用 schema v4,并在 Implement admission 时同时对照同一 root/change 的私有批准事实与 finalization revision/hash 事实验证。
33
+ Gate A/B 收据只有一套 canonical 结构,并在 Implement admission 时同时对照同一 root/change 的私有批准事实与 finalization revision/hash 事实验证。
32
34
 
33
35
  只有继续工作确实需要新增行为、架构/策略、依赖、路径、资源、验证或 AGENTS 权限时才进入 `approval-needed`。
34
36
  结果保留原 Implement run,明确给出 authority category、所需 Gate、引用和 `/abel-design --change <change>` 用户指引;不会自动调用另一阶段。
@@ -75,6 +77,10 @@ npm install -g @abelxiaoxing/cadence
75
77
  只有需要自定义模型、显式顺序或 failover 时,才复制 [`config/routes.example.json`](config/routes.example.json) 到项目级 `.pi/cadence/routes.json` 或用户级 `~/.pi/agent/cadence/routes.json`。
76
78
  项目文件按整文件优先;route 必须显式列入对应角色,custom route 只引用 `apiKeyEnv` 的变量名,不能把凭据值写进 JSON。
77
79
  一旦显式文件存在,它就是完整策略;损坏、缺字段或角色引用不一致会 fail closed,不会悄悄退回默认父模型。
80
+ 旧配置的顶层数字标记 `2` 会被安全迁移为当前 canonical 结构;其他编号会以专用诊断码明确拒绝。
81
+ inherited route 声明的能力会与当前父模型真实能力取交集,不能通过夸大的配置绕过 admission。
82
+ transport 与 malformed structural result 都只在声明的 route 顺序内做有界 failover;只有一个 route 时会原地重试一次,多个 route 时按顺序切换。
83
+ 相同 pause 的安全指纹与重复计数通过 `status` 暴露,凭据、URL 和原始输出仍保持私有。
78
84
  `rebind` 只能选择已获 policy 授权且能力匹配的 route,不会扩大任务边界。
79
85
 
80
86
  ## 跨项目验证合同(Cross-project verification)
@@ -9,8 +9,12 @@ The fixed algorithm order is reproduce, falsify, failing-regression, then minimu
9
9
  An evidence packet submits exactly one compact structured evidence object through `abel_submit_result` with the reported symptoms, candidate causes, confirming or refuting citations, and a supported root-cause conclusion or an explicit evidence gap.
10
10
  Only the parent may claim that command-based reproduction succeeded.
11
11
 
12
- A candidate packet submits exactly one complete unified diff: either the failing regression or, after the parent has verified that regression, the minimum repair.
13
- It includes task identity, expected parent-owned verification, risks, and typed blockers, and never claims that it ran the verification.
12
+ A candidate packet submits one complete structured patch: either the failing regression or, after the parent has verified that regression, the minimum repair.
13
+ Use ordered `replace`, `rewrite`, `create`, and `delete` operations inside the approved write set.
14
+ Never write unified-diff headers or hunk ranges because the trusted control plane generates them.
15
+ If approved context is insufficient, submit a small typed context request instead of a partial patch.
16
+ The trusted submit tool validates the accepted payload; brief accompanying text is harmless.
17
+ Never claim that you ran the parent-owned verification.
14
18
 
15
19
  You never invent a fix for an unverified root cause, change behavior contracts, select another workflow stage, or prescribe parent recovery.
16
20
  Scope-expanding repairs are reported as evidence for a user decision.
@@ -20,8 +20,15 @@ Your output is one complete structured patch delivered through `abel_submit_resu
20
20
  - every operation path is within the declared phase boundary; paths that do
21
21
  not need a change may remain untouched;
22
22
  - preserve the expected parent-owned verification contract without claiming a result;
23
- - use a typed `context-request` for bounded context, task splitting, or an
24
- approval-boundary blocker instead of submitting a partial patch.
23
+ - use a typed `context-request` for bounded context or task splitting instead
24
+ of submitting a partial patch; classify every ref as a requested path with
25
+ `read | write` access, a source citation with path and line, or a
26
+ contract-diagnostic ref;
27
+ - report only the missing context and structural facts: never select
28
+ `approval-needed`, `boundary-review-needed`, a Gate, or a workflow stage;
29
+ - an `AGENTS.md` path is never a Worker write path;
30
+ - report an AGENTS read need separately, and leave every sealed AGENTS contract
31
+ and managed-block update to the parent control plane.
25
32
 
26
33
  If the submit tool rejects malformed operations, use its concrete error to correct the submission once in this same disposable session; a second rejected structural submission ends the session.
27
34
 
@@ -1,5 +1,4 @@
1
1
  {
2
- "version": 2,
3
2
  "routes": {
4
3
  "parent": {
5
4
  "kind": "inherited",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abelxiaoxing/cadence",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Four explicit Abel workflow prompts with stage-isolated design and diagnosis, durable resumable implementation, and three package-owned professional Agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -152,13 +152,22 @@ The plan admits only shell-free `vitest`, `package-script`, `static-check`, or o
152
152
  Pin local runners, package scripts, arguments, `minTests`, classifications, and `noInstall` behavior.
153
153
  Reject shell operators, implicit downloads, absolute/escaping paths, missing local capability, and unsupported verification shapes during Design readiness.
154
154
 
155
- After current Gate A approval, send the complete draft through `write-artifact` only to the relative path `plan-draft.json`, then invoke code-owned compilation:
155
+ After current Gate A approval, send the complete draft through `write-artifact` only to the relative path `plan-draft.json`.
156
+ Before mutation-owning compilation, use the read-only typed preflight; it validates the same fixed draft and returns task/output counts plus hashes without installing canonical files or journaling a plan revision:
157
+
158
+ ```json
159
+ {"action":"design","request":{"operation":"validate-plan-draft","runId":"<run-id>"}}
160
+ ```
161
+
162
+ If preflight fails, use its safe structured diagnostics (`code`, and when applicable `taskId`, `phase`, `field`, `category`, `owner`, `verificationId`, or `outputId`) to repair the exact boundary.
163
+ Do not bisect the draft blindly or repeat an unchanged validation/finalization request.
164
+ After preflight succeeds, invoke code-owned compilation:
156
165
 
157
166
  ```json
158
167
  {"action":"design","request":{"operation":"compile-plan","runId":"<run-id>","operationId":"<unique-operation>"}}
159
168
  ```
160
169
 
161
- The compiler reads only that fixed safe path, validates capability and graph closure, atomically installs canonical `implement-plan.json`, and returns its raw and canonical hashes.
170
+ The compiler reads only that fixed safe path, validates capability and graph closure again, atomically installs canonical `implement-plan.json`, and returns its raw and canonical hashes.
162
171
  Do not hand-assemble canonical plan bytes, hashes, generated task Markdown, or receipts.
163
172
  The canonical `ImplementPlan` contains:
164
173
 
@@ -196,8 +205,8 @@ Finalization revalidates both current private Gate proofs, the stored plan, stri
196
205
  It atomically installs canonical `gate-a.yaml`, writes `ready.yaml` last, rereads the installed delivery, removes `plan-draft.json`, marks the Design run completed, and returns `deliveryRevision` plus `receiptHash`.
197
206
  Any failure leaves Design nonterminal and installs no new `ready.yaml`.
198
207
 
199
- Receipt schema version 4 binds the Gate A and Gate B approval revisions, canonical contract hashes, and owner-private record hashes.
200
- `ready.yaml` references the plan path, schema version, raw-byte hash, canonical plan hash, executable verification closure, Gate A hash, artifact hashes, and traceability hash.
208
+ The canonical receipts bind the Gate A and Gate B approval revisions, canonical contract hashes, and owner-private record hashes.
209
+ `ready.yaml` references the plan path, raw-byte hash, canonical plan hash, executable verification closure, Gate A hash, artifact hashes, and traceability hash.
201
210
  It does not embed caller-owned runtime task state, completed/blocked arrays, snapshots, launch identities, or retry budgets.
202
211
 
203
212
  Only the sealed task checkboxes may be normalized from `[x]`/`[X]` to `[ ]` when validating the tracked `tasks.md` hash.
@@ -219,6 +228,8 @@ Report `READY_TO_IMPLEMENT` only when strict validation passes, both Gates have
219
228
  Successful finalization deactivates private dispatch before an unrelated later request.
220
229
  Gate waits remain active for direct follow-up; if the user explicitly ends an unfinished Design interaction, send `{"action":"finish"}`.
221
230
  Otherwise report the current Design state, retained evidence, and the one unresolved decision or artifact that prevents readiness.
231
+ Design `status` exposes only Design `legalOperations`; explicit stage exit is separately exposed as top-level `packetActions: ["finish"]`.
232
+ Never send `operation: "finish"`; exit only with `{"action":"finish"}`.
222
233
 
223
234
  Do not implement product code and do not archive, publish, release, stage, or commit implicitly.
224
235
 
@@ -21,10 +21,10 @@ Neither Gate grants tool permission, repository permission, identity, or authori
21
21
  Design and Implement may run in fresh contexts.
22
22
  Trusted delivery requires:
23
23
 
24
- - canonical receipt-schema-v4 Gate A and Gate B proofs for the same root/change/schema, each binding its approval revision, canonical contract hash, and owner-private journal record hash;
24
+ - canonical Gate A and Gate B proofs for the same root/change/schema, each binding its approval revision, canonical contract hash, and owner-private journal record hash;
25
25
  - an owner-private finalization fact binding the exact delivery revision, ready-receipt raw hash, both Gate proofs, and canonical plan hash;
26
26
  - safe relative artifact paths and exact raw SHA-256 bindings;
27
- - a canonical `implement-plan.json` with schema version, raw hash, and canonical hash;
27
+ - a canonical `implement-plan.json` with raw hash and canonical hash;
28
28
  - OpenSpec strict validity and planning completeness;
29
29
  - Requirement → Scenario → Verification → Task traceability exactly once;
30
30
  - executable verification closure and complete output/path/dependency/AGENTS contracts.
@@ -54,7 +54,7 @@ Cross-stage calls fail closed.
54
54
 
55
55
  ## Durable Design control
56
56
 
57
- Design uses one closed private `action: "design"` family from its first operation onward: `start`, `status`, `bind-change`, `record-decision`, `approve-gate`, `write-artifact`, `delete-artifact`, `compile-plan`, and `finalize-delivery`.
57
+ Design uses one closed private `action: "design"` family from its first operation onward: `start`, `status`, `bind-change`, `record-decision`, `approve-gate`, `write-artifact`, `delete-artifact`, `validate-plan-draft`, `compile-plan`, and `finalize-delivery`.
58
58
  `start` accepts either the complete transient requirement or an existing change name; `status` uses the returned run id; `bind-change` names a provisional run only after current Gate A approval.
59
59
  The control plane normalizes and hashes transient requirement, decision-contract, and Gate-A contract text itself, and Gate B binds a plan compiled after the current Gate A approval and latest substantive decisions without a caller-supplied hash.
60
60
  The owner-private SQLite journal retains only normalized evidence identities/hashes, versioned decision hashes and refs, Gate proof facts, canonical compiled-plan identity/bytes, finalization identity, and idempotent operation outcomes.
@@ -73,6 +73,7 @@ Code-owned `gate-a.yaml` and `ready.yaml` receipts remain forbidden; `implement-
73
73
  All filesystem-mutating Design operations serialize against the durable expiring finalization lease.
74
74
  A competing or stale owner cannot inspect, replace, remove, or commit another operation's receipt bytes, and cleanup is fenced by the current operation's raw receipt hash.
75
75
  After current Gate A, the parent sends the fixed `plan-draft.json` input only through `write-artifact`.
76
+ Read-only `validate-plan-draft` runs the same compiler checks without installing files or recording a plan revision and returns bounded task/phase/field diagnostics; unchanged invalid requests are not retried blindly.
76
77
  `compile-plan` owns canonical plan construction and installation.
77
78
  Gate B binds that exact stored canonical plan hash.
78
79
  `finalize-delivery` validates current proofs, OpenSpec, artifacts, traceability, and executable closure, writes Gate A and ready receipts atomically with `ready.yaml` last, rereads them, removes the draft, and completes Design.
@@ -199,5 +200,6 @@ Its absence pauses that verification only and does not block unrelated tasks or
199
200
  Tool settlement is not completion.
200
201
  Only durable `completed` after cumulative apply and postconditions receives success.
201
202
  Successful Design finalization, terminal Implement settlement, Diagnose `finish`, explicit stage finish, and session shutdown deactivate only `abel_dispatch`, drain packets, clear the parent bridge, and clear active stage identity.
203
+ Design status separates `legalOperations` from top-level `packetActions`; `finish` is never a Design operation and is sent only as `{"action":"finish"}`.
202
204
  Gate waits and resumable pauses remain active for direct follow-up.
203
205
  Never archive, publish, release, stage, or commit implicitly.
@@ -32,7 +32,6 @@ import type {
32
32
 
33
33
  const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
34
34
  const SHA256 = /^[a-f0-9]{64}$/u;
35
- const JOURNAL_VERSION = 2 as const;
36
35
 
37
36
  type TransactionState =
38
37
  | "prepared"
@@ -58,7 +57,6 @@ interface TransactionEvent {
58
57
  }
59
58
 
60
59
  interface TransactionJournal {
61
- version: typeof JOURNAL_VERSION;
62
60
  transactionId: string;
63
61
  consumerRoot: string;
64
62
  baselineRevisionId: string;
@@ -208,7 +206,7 @@ function hashBytes(bytes: Uint8Array): string {
208
206
 
209
207
  function journalHash(journal: TransactionJournal): string {
210
208
  return createHash("sha256")
211
- .update("cadence-apply-transaction-v2\0")
209
+ .update("cadence-apply-transaction\0")
212
210
  .update(JSON.stringify(journal))
213
211
  .digest("hex");
214
212
  }
@@ -449,7 +447,6 @@ function validateJournal(value: unknown): TransactionJournal {
449
447
  }
450
448
  const journal = value as TransactionJournal;
451
449
  if (
452
- journal.version !== JOURNAL_VERSION ||
453
450
  !IDENTIFIER.test(journal.transactionId) ||
454
451
  !path.isAbsolute(journal.consumerRoot) ||
455
452
  !SHA256.test(journal.baselineRevisionId) ||
@@ -821,7 +818,6 @@ export class ApplyTransaction {
821
818
  ].sort();
822
819
  for (const hash of retainedHashes) this.#artifacts.retain(hash);
823
820
  const journal: TransactionJournal = {
824
- version: JOURNAL_VERSION,
825
821
  transactionId: input.transactionId,
826
822
  consumerRoot,
827
823
  baselineRevisionId: baseline.revisionId,
@@ -26,6 +26,7 @@ import {
26
26
  type CandidateArtifactSubmission,
27
27
  type CandidateArtifactSubmissionResult,
28
28
  createCandidateArtifactTool,
29
+ createStructuredPatchTool,
29
30
  createSubmitTool,
30
31
  type FinalCategory,
31
32
  type SubmitClassification,
@@ -200,10 +201,6 @@ function structuralSubmitCount(message: AssistantMessage | undefined) {
200
201
  ).length;
201
202
  }
202
203
 
203
- function hasOneStructuralSubmit(message: AssistantMessage | undefined) {
204
- return message?.role === "assistant" && structuralSubmitCount(message) === 1;
205
- }
206
-
207
204
  function safeChildError(failure: ChildFailure): string {
208
205
  switch (failure.kind) {
209
206
  case "environment":
@@ -308,19 +305,34 @@ export async function runChildSession(input: {
308
305
  ledgerProjection?: unknown;
309
306
  captureObservations?: boolean;
310
307
  candidateArtifact?: CandidateArtifactSubmission;
308
+ structuredPatch?: {
309
+ workspaceRoot: string;
310
+ writePaths: string[];
311
+ deletePaths: string[];
312
+ };
311
313
  onStreamStart?: () => void;
312
314
  onStreamProgress?: () => void;
313
315
  }): Promise<ChildSessionResult> {
314
316
  const candidateProtocol = input.candidateArtifact !== undefined;
315
317
  const submit = input.candidateArtifact
316
318
  ? createCandidateArtifactTool(input.candidateArtifact)
317
- : createSubmitTool({
318
- requestId: input.requestId,
319
- taskId: input.taskId,
320
- role: input.role,
321
- phase: input.phase ?? "red",
322
- output: input.output,
323
- });
319
+ : input.structuredPatch
320
+ ? createStructuredPatchTool({
321
+ requestId: input.requestId,
322
+ taskId: input.taskId,
323
+ role: input.role,
324
+ phase: input.phase ?? "red",
325
+ workspaceRoot: input.structuredPatch.workspaceRoot,
326
+ writePaths: input.structuredPatch.writePaths,
327
+ deletePaths: input.structuredPatch.deletePaths,
328
+ })
329
+ : createSubmitTool({
330
+ requestId: input.requestId,
331
+ taskId: input.taskId,
332
+ role: input.role,
333
+ phase: input.phase ?? "red",
334
+ output: input.output,
335
+ });
324
336
  const observationCollector = input.captureObservations
325
337
  ? new ScopedObservationCollector()
326
338
  : undefined;
@@ -396,19 +408,16 @@ export async function runChildSession(input: {
396
408
  const assistants =
397
409
  session?.messages.filter((m) => m.role === "assistant") ?? [];
398
410
  const last = assistants.at(-1);
399
- const lastSubmit = assistants.findLast((message) =>
400
- hasStructuralSubmit(message),
401
- );
402
411
  const structuralAttempts = assistants.reduce(
403
412
  (count, message) => count + structuralSubmitCount(message),
404
413
  0,
405
414
  );
406
415
  let finalCategory: FinalCategory;
407
- if (!last) {
408
- finalCategory = "no-final-assistant";
409
- } else if (hasOneStructuralSubmit(lastSubmit)) {
416
+ if (submit.getResult() !== undefined) {
410
417
  finalCategory =
411
418
  structuralAttempts > 1 ? "multiple-submit" : "single-submit-only";
419
+ } else if (!last) {
420
+ finalCategory = "no-final-assistant";
412
421
  } else {
413
422
  const content = finalDeliveryContent(last);
414
423
  finalCategory =
@@ -481,7 +490,7 @@ export async function runChildSession(input: {
481
490
  cwd: input.cwd,
482
491
  modelRuntime: input.modelRuntime,
483
492
  model: input.model,
484
- thinkingLevel: "off",
493
+ thinkingLevel: "low",
485
494
  tools: toolNames,
486
495
  customTools,
487
496
  resourceLoader: new EmptyResourceLoader(effectivePrompt),
@@ -563,13 +572,18 @@ export async function runChildSession(input: {
563
572
  const result = submit.getResult();
564
573
  const attempts = submit.getAttempts();
565
574
  const classification = classifySession();
575
+ const terminalSubmitFailure = submit.getFailure();
566
576
  const candidateComplete =
567
577
  candidateProtocol &&
568
578
  result !== undefined &&
569
579
  (result.kind === "sealed-candidate" || result.kind === "context-request");
570
- if (!result || (candidateProtocol && !candidateComplete)) {
580
+ if (
581
+ !result ||
582
+ terminalSubmitFailure !== undefined ||
583
+ (candidateProtocol && !candidateComplete)
584
+ ) {
571
585
  const failure = withSubmitDetails(
572
- submit.getFailure() ??
586
+ terminalSubmitFailure ??
573
587
  input.failureOverride?.() ??
574
588
  transportFailure() ??
575
589
  noStructuralSubmit(),
@@ -589,32 +603,9 @@ export async function runChildSession(input: {
589
603
  ...observationMetadata(),
590
604
  };
591
605
  }
592
- const assistants = session.messages.filter((m) => m.role === "assistant");
593
- const final = assistants.at(-1);
594
- if (
595
- !(candidateProtocol
596
- ? candidateComplete && structuralSubmitCount(final) === 1
597
- : hasOneStructuralSubmit(final))
598
- ) {
599
- const failure = {
600
- kind: "artifact",
601
- code: "invalid-structural-result",
602
- stage: "child-finalization",
603
- details: submitDetails(classification),
604
- } as const;
605
- disposeOnce();
606
- return {
607
- ok: false,
608
- error: safeChildError(failure),
609
- failure,
610
- failureKind: "failed",
611
- transportFailure: false,
612
- disposeCount,
613
- usage: usage.total(),
614
- classification,
615
- ...observationMetadata(),
616
- };
617
- }
606
+ // The trusted submit tool already validates and seals the one accepted
607
+ // result. Harmless text or read-only calls do not alter that admission;
608
+ // another terminal submission does.
618
609
  disposeOnce();
619
610
  return {
620
611
  ok: true,