instar 0.28.84 → 0.28.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/intent.d.ts.map +1 -1
- package/dist/commands/intent.js +29 -2
- package/dist/commands/intent.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +14 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/DecisionJournal.d.ts +66 -3
- package/dist/core/DecisionJournal.d.ts.map +1 -1
- package/dist/core/DecisionJournal.js +116 -3
- package/dist/core/DecisionJournal.js.map +1 -1
- package/dist/core/DispatchExecutor.d.ts +74 -1
- package/dist/core/DispatchExecutor.d.ts.map +1 -1
- package/dist/core/DispatchExecutor.js +120 -1
- package/dist/core/DispatchExecutor.js.map +1 -1
- package/dist/core/EvolutionManager.d.ts +60 -0
- package/dist/core/EvolutionManager.d.ts.map +1 -1
- package/dist/core/EvolutionManager.js +118 -0
- package/dist/core/EvolutionManager.js.map +1 -1
- package/dist/core/InitiativeTracker.d.ts +82 -15
- package/dist/core/InitiativeTracker.d.ts.map +1 -1
- package/dist/core/InitiativeTracker.js +502 -31
- package/dist/core/InitiativeTracker.js.map +1 -1
- package/dist/core/LearnSkillBridge.d.ts +119 -0
- package/dist/core/LearnSkillBridge.d.ts.map +1 -0
- package/dist/core/LearnSkillBridge.js +160 -0
- package/dist/core/LearnSkillBridge.js.map +1 -0
- package/dist/core/types.d.ts +34 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +131 -14
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.d.ts +32 -0
- package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.js +82 -5
- package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
- package/dist/threadline/mcp-stdio-entry.js +56 -0
- package/dist/threadline/mcp-stdio-entry.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/upgrades/0.28.85.md +64 -0
- package/upgrades/side-effects/0.28.83.md +27 -0
- package/upgrades/side-effects/taskflow-phase4.md +377 -0
- package/upgrades/side-effects/threadline-discover-relay-and-trust.md +122 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase2.md +148 -0
- package/upgrades/side-effects/wikiclaim-evidence-phase3.md +125 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Side-Effects Review — WikiClaim Evidence Phase 2 (EvolutionManager + DispatchExecutor producers)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `wikiclaim-evidence-phase2`
|
|
4
|
+
**Date:** 2026-05-10
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** required (interaction with Phase 1 + TaskFlow Phase 3a, atomicity, signal-vs-authority compliance, error isolation)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Wires Phase 1's `SemanticMemory.rememberWithEvidence` / `addEvidence` API into the two Phase-2 producers identified by the spec (§ Producers, lines 215+):
|
|
11
|
+
|
|
12
|
+
- **EvolutionManager** — every new `EvolutionProposal` becomes a `pattern` MemoryEntity (the "cluster"). On `addProposal()` the cluster entity is created with evidence parsed from the proposal's `source` field (currently: `feedback:<id>` → `kind:'feedback'`). A new public `addClusterEvidence(proposalId, evidence)` method appends evidence atomically as more inputs join the cluster. The proposal record persists `entityId?: string` so the cluster reference survives reload.
|
|
13
|
+
- **DispatchExecutor** — `execute()` now accepts an optional `DispatchEvidenceContext` carrying the source `clusterEntityId`, optional `dispatchId`, optional `priorRunIds`, and optional `priorDispatchEntityIds`. On successful execution the executor records a `decision` MemoryEntity with evidence rows: `pattern-entity` (cluster), `ledger-entry` (dispatch id), `job-run` (prior runs), and `pattern-entity` (prior decision entities, with note `"supersedes prior dispatch decision"`). The supersedes semantics is carried by the edge, not by a `supersedes-evidence` kind, because spec § Producers restricts DispatchExecutor's allowlist to `pattern-entity, job-run, ledger-entry`.
|
|
14
|
+
|
|
15
|
+
Producers emit signals (evidence rows). They do NOT gate proposal flow or dispatch flow. Phase 1's narrowing-only / kind-allowlist / cap / cycle-bound checks are the structural guardrails — Phase 2 just routes data through them.
|
|
16
|
+
|
|
17
|
+
What lands:
|
|
18
|
+
- `EvolutionProposal.entityId?: string` (new optional field). Existing JSON state files load with `entityId === undefined`; legacy proposals keep working unchanged.
|
|
19
|
+
- `EvolutionManager.setSemanticMemory(memory)` wiring. Without this call the manager is unchanged from Phase 1 — `addProposal()` is unaltered, no cluster entity is created, `addClusterEvidence()` is a no-op.
|
|
20
|
+
- `EvolutionManager.addClusterEvidence(proposalId, evidence)` public method.
|
|
21
|
+
- `DispatchExecutor.setSemanticMemory(memory)` wiring + `getLastDecisionEntityId()` accessor for chaining.
|
|
22
|
+
- `DispatchExecutor.execute(payload, evidenceCtx?)` — second argument optional, backwards compatible with all existing callers.
|
|
23
|
+
- `DispatchEvidenceContext` exported interface.
|
|
24
|
+
- 17 new vitest cases (real SQLite, no mocks) — 11 for EvolutionManager, 6 for DispatchExecutor including the spec's cross-product integration test.
|
|
25
|
+
|
|
26
|
+
Files touched:
|
|
27
|
+
- `src/core/types.ts` — `EvolutionProposal.entityId?: string`.
|
|
28
|
+
- `src/core/EvolutionManager.ts` — SemanticMemory field + 4 new methods (`setSemanticMemory`, `getSemanticMemory`, `addClusterEvidence`, `createClusterEntity`/`buildInitialClusterEvidence` private).
|
|
29
|
+
- `src/core/DispatchExecutor.ts` — SemanticMemory field + `lastDecisionEntityId` + 4 new methods + `DispatchEvidenceContext` interface; `execute()` extended with optional `evidenceCtx` parameter.
|
|
30
|
+
- `tests/unit/evolution-manager-evidence.test.ts` — new file, 11 cases.
|
|
31
|
+
- `tests/unit/dispatch-executor-evidence.test.ts` — new file, 6 cases including cross-product integration.
|
|
32
|
+
- `upgrades/side-effects/wikiclaim-evidence-phase2.md` (this file).
|
|
33
|
+
|
|
34
|
+
## Decision-point inventory
|
|
35
|
+
|
|
36
|
+
- `EvolutionManager.createClusterEntity` — **add** — best-effort `rememberWithEvidence` call wrapped in try/catch; failures log + return null, never propagate to the proposal flow.
|
|
37
|
+
- `EvolutionManager.buildInitialClusterEvidence` — **add** — pure function, recognizes the `feedback:<id>` source pattern. No LLM, no policy judgment.
|
|
38
|
+
- `EvolutionManager.addClusterEvidence` — **add** — thin pass-through to `SemanticMemory.addEvidence`. Producer kind is hardcoded `'EvolutionManager'`.
|
|
39
|
+
- `DispatchExecutor.recordDispatchDecision` — **add** — best-effort `rememberWithEvidence` call wrapped in try/catch; failures log + return null.
|
|
40
|
+
- `DispatchExecutor.buildDispatchEvidence` — **add** — pure function constructing the evidence array per spec § Producers example shape.
|
|
41
|
+
- Allowlist enforcement — **inherits Phase 1**, no new gate. `EvolutionManager` and `DispatchExecutor` already appear in `PRODUCER_KIND_ALLOWLIST`.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 1. Over-block
|
|
46
|
+
|
|
47
|
+
**Cluster-entity creation may fail silently.** `createClusterEntity` swallows `EvidencePolicyError` (and any other thrown error) and logs a warning. Rationale: the proposal flow is the source of truth — JSON state must persist even when SemanticMemory is offline / mid-rebuild / hits a transient SQL error. Tests cover the failing path (`addProposal` still succeeds when memory is not wired). The trade-off is observability: a future operator-facing health probe should surface "proposals without `entityId`" as a divergence signal (deferred — outside Phase 2 scope).
|
|
48
|
+
|
|
49
|
+
**`feedback:<id>` is the only recognized source pattern in this phase.** A proposal with `source: "user:Justin"` or `source: "session:ABC"` produces an empty evidence array on the cluster. This is by design: `EvolutionManager`'s allowlist permits `feedback`, `pattern-entity`, `supersedes-evidence` — not `message` or `session`. Other prefixes will be wired by other producers (DecisionJournal in Phase 3, /learn skill in Phase 3) without extending EvolutionManager's surface. The cluster entity is still useful (inverse traceability lights up as soon as `addClusterEvidence` is called), and the empty-evidence case is tested.
|
|
50
|
+
|
|
51
|
+
**`DispatchExecutor` cannot emit `supersedes-evidence` kind.** Per spec § Producers, DispatchExecutor's allowlist is `pattern-entity, job-run, ledger-entry`. The supersedes relationship between two dispatch decisions is captured by a `pattern-entity` row pointing at the prior decision entity, with `note: "supersedes prior dispatch decision"`. The semantics is preserved; only the kind label is different. Tests assert the row appears with the correct sourceId + note. This deviation from the task-prompt's casual language ("`kind: 'supersedes-evidence'` if it overrides a prior decision") is deliberate and Phase-1-allowlist-conformant — extending the allowlist would be a separate spec amendment.
|
|
52
|
+
|
|
53
|
+
## 2. Under-block
|
|
54
|
+
|
|
55
|
+
- **No idempotency on `addProposal` cluster creation.** Calling `addProposal` twice with identical inputs creates two cluster entities (different UUIDs). This matches the existing `addProposal` semantics — proposals get unique IDs by counter, so duplicate proposals are an upstream concern. No new under-block.
|
|
56
|
+
- **No write-time verification of `clusterEntityId` in `DispatchEvidenceContext`.** A caller can pass a non-existent or unrelated cluster id; the resulting evidence row is a dangling `pattern-entity` reference. Per spec § Producers / "Cross-store sourceId integrity", this is best-effort by design — consumers tolerate dangling refs by displaying `[source unavailable]`. The inverse query still finds the dangling row and surfaces it with the entity id, which is the correct behavior.
|
|
57
|
+
- **Producer-crash partial-write coverage inherits Phase 1 atomicity.** Every multi-row write goes through `rememberWithEvidence` or `addEvidence`, both of which wrap inserts in a single better-sqlite3 transaction. Test `atomic: array append rolls back fully when one row violates a policy gate` verifies this end-to-end through EvolutionManager.
|
|
58
|
+
- **Evidence-rate flooding still has no per-caller rate limit.** The spec's "10 evidence/sec/producer" rate-limit is deferred to Phase 5 hardening per the convergence report. The per-entity cap (50 default, configurable to 500) is the active backstop.
|
|
59
|
+
|
|
60
|
+
## 3. Level-of-abstraction fit
|
|
61
|
+
|
|
62
|
+
Right layer. Each producer integrates at the existing entry-point of its own concern:
|
|
63
|
+
- `EvolutionManager.addProposal` is the canonical cluster-creation moment — adding the cluster entity here means future callers (REST endpoint, autonomous evolution, /feedback handler) get evidence emission "for free" without touching their code.
|
|
64
|
+
- `DispatchExecutor.execute` is the canonical dispatch-decision moment — the new `evidenceCtx` parameter is opt-in, so legacy callers continue to work unchanged.
|
|
65
|
+
|
|
66
|
+
The `SemanticMemory` write API surface (`rememberWithEvidence`, `addEvidence`) does not move; the producers just pass producer-id + evidence arrays, exactly as spec § Producers prescribes. No business logic leaks into `SemanticMemory`; no SQL leaks into the producers.
|
|
67
|
+
|
|
68
|
+
## 4. Signal vs authority compliance
|
|
69
|
+
|
|
70
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
71
|
+
|
|
72
|
+
- [x] **No** — this change has no judgment-level block/allow surface.
|
|
73
|
+
|
|
74
|
+
Every interaction in Phase 2 is signal-only:
|
|
75
|
+
- Producers EMIT evidence rows. They do not consume them, do not gate flow on them, do not block any outbound action based on what's in `entity_evidence`.
|
|
76
|
+
- The Phase 1 `assertProducerKindsAllowed` / `assertNarrowingOnly` checks throw `EvidencePolicyError` from inside `SemanticMemory.addEvidence`. These are mechanic-level structural validations (constant-map lookup + ordinal ordering), not judgment. They are exempt under "Hard-invariant validation" in the principle.
|
|
77
|
+
- `createClusterEntity` and `recordDispatchDecision` swallow ALL errors from the policy gates — a buggy emit-site (e.g., "EvolutionManager tries to write `kind:'commit'`") is logged and the proposal/dispatch flow continues. The principle is preserved: brittle policy checks at the storage edge cannot brick the agent's primary control flow.
|
|
78
|
+
- The `clusterEntityId` field on `DispatchEvidenceContext` is a foreign reference, not a precondition. A missing or stale cluster id does not block dispatch; it produces a dangling evidence row that consumers display as `[source unavailable]`.
|
|
79
|
+
|
|
80
|
+
## 5. Interactions
|
|
81
|
+
|
|
82
|
+
- **Phase 1 (`tests/unit/semantic-memory-evidence.test.ts`)**: Unchanged — 21/21 still pass. Phase 2 adds new callers of the Phase 1 API; it does not modify the API.
|
|
83
|
+
- **TaskFlow Phase 3a (`tests/unit/evolution-manager-taskflow-dualwrite.test.ts`)**: Unchanged — 10/10 still pass. Phase 2 inserts `createClusterEntity` BEFORE `state.proposals.push` and BEFORE `dualWriteCreate`, but the order is invariant: TaskFlow shadow-write happens regardless of cluster creation outcome (the `void` discards the promise; cluster-create errors are caught locally). The `addProposal` return type and behavior are identical.
|
|
84
|
+
- **`updateProposalStatus`**: Untouched. Cluster entity is created at proposal birth and never re-created. Status transitions do not append evidence in this phase (Phase 3 may add `kind:'session'` rows for "implementing" / "rejected" sessions when DecisionJournal lands, but that's the next PR's concern).
|
|
85
|
+
- **DivergenceChecker**: Untouched. Operates on TaskFlow vs JSON state; the cluster MemoryEntity is parallel and read-side only.
|
|
86
|
+
- **Existing `DispatchExecutor` callers** (`src/commands/server.ts:4145`): Unaffected — the new `evidenceCtx` parameter is optional. Nothing in the production path passes it yet; that wiring lands when an upstream caller (e.g., dispatch handler in server.ts) gets the cluster context. This is the correct phasing — Phase 2 makes the surface available; Phase 4 (HTTP endpoints) and the dashboard exercise it end-to-end.
|
|
87
|
+
- **Cascade-delete (Phase 1)**: Continues to fire. If a proposal's cluster entity is forgotten, all its evidence + any decision entities citing it via dangling `pattern-entity` references survive — the reference becomes "[source unavailable]" by design.
|
|
88
|
+
- **Embedding generation**: The cluster entity name + content are non-empty (proposal title + description), so embeddings will generate normally. The decision entity's name is `dispatch:<id>`, content is `payload.description` — same path. No embedding-pipeline change.
|
|
89
|
+
|
|
90
|
+
## 6. External surfaces
|
|
91
|
+
|
|
92
|
+
- **Other agents on the same machine**: None. The new methods are in-process Type and class additions.
|
|
93
|
+
- **Other users of the install base**: New optional `EvolutionProposal.entityId` field. Existing JSON state files load with the field as `undefined`. New writes that don't go through SemanticMemory wiring also leave it `undefined`. No breaking change.
|
|
94
|
+
- **External systems**: None.
|
|
95
|
+
- **Persistent state**: Same SQLite tables as Phase 1 (`entities`, `entity_evidence`). New rows only when `setSemanticMemory` is called by the server's wiring step; tests construct managers directly and verify.
|
|
96
|
+
- **Privacy posture**: Unchanged — cluster entities default to `privacyScope: 'shared-project'`, decision entities default to `privacyScope: 'shared-project'`. Producers do not write more permissive tiers (the cluster is created at `shared-project`, evidence inherits unless the caller explicitly narrows). Test `privacy narrowing — rejects evidence with privacyTier wider than the cluster scope` verifies the constraint fires through the new producer call site.
|
|
97
|
+
|
|
98
|
+
## 7. Rollback cost
|
|
99
|
+
|
|
100
|
+
- **Hot-fix release**: Pure additive change. `git revert <merge-commit>` ships as the next patch. The cluster entities and decision entities created during the live window remain in `entities` + `entity_evidence` and are harmless (read-only side data); they can be cleaned up by a one-shot `DELETE FROM entities WHERE source LIKE 'evolution:%' OR source LIKE 'dispatch:%'` if desired (cascade-delete handles their evidence rows).
|
|
101
|
+
- **Data migration**: None. New optional field on a JSON state file; legacy files load fine with `entityId: undefined`.
|
|
102
|
+
- **Agent state repair**: None. Existing proposals continue to work without `entityId`; new `addClusterEvidence` calls on legacy proposals are no-ops by design (test coverage included).
|
|
103
|
+
- **User visibility**: None. No user-facing surface area in Phase 2 — the cluster entity / decision entity / inverse-query traceability lights up at Phase 4 (HTTP endpoints) and Phase 5 (dashboard). Phase 2 is wiring-only.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Conclusion
|
|
108
|
+
|
|
109
|
+
WikiClaim Phase 2 wires the Phase 1 typed-evidence API into EvolutionManager (cluster creation + incremental evidence) and DispatchExecutor (decision recording with cluster + ledger + job-run + supersedes evidence). All emission paths are best-effort and signal-only — failures inside `SemanticMemory` policy gates are logged and the host flow (proposal save, dispatch execute) continues uninterrupted. The producer-kind allowlist Phase 1 ships is exercised end-to-end by tests including the spec's cross-product `feedback → cluster → dispatch → findCitations` integration. 17 new tests pass alongside the Phase 1 / TaskFlow Phase 3a / SemanticMemory regression suites (80 total green). Cleared to ship pending second-pass concurrence.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Second-pass review
|
|
114
|
+
|
|
115
|
+
**Reviewer:** independent adversarial pass (author re-read with phase-1-baseline + spec-conformance + signal-vs-authority + atomicity lenses).
|
|
116
|
+
**Independent read of the artifact: concur after addressing 3 round-1 concerns.**
|
|
117
|
+
|
|
118
|
+
Round-1 concerns:
|
|
119
|
+
|
|
120
|
+
1. **Spec deviation: `supersedes-evidence` kind for prior dispatch supersedes.** The task prompt and the spec narrative both reach for `kind: 'supersedes-evidence'`. DispatchExecutor's allowlist (Phase 1, frozen) does not include it. Resolution: documented above under § Over-block as a deliberate spec-deviation that preserves the semantics via `pattern-entity` rows + `note: "supersedes prior dispatch decision"`. Allowlist-extension is a separate spec amendment — not snuck in this PR.
|
|
121
|
+
|
|
122
|
+
2. **Cluster-creation-before-JSON-save ordering**: if `createClusterEntity` succeeds but `saveEvolution` later fails (disk full / writeFile error), the cluster entity is orphaned (`source: evolution:EVO-XYZ` with no JSON proposal). Resolution: the orphan cluster entity is benign (read-only) and identifiable by source pattern + tags, so a future cleanup query can remove it. This matches the existing TaskFlow Phase 3a posture (`dualWriteCreate` is also fired before save, with the same "JSON is the source of truth, side-effects are best-effort" stance). Tightening this would require restructuring `addProposal` into a two-phase commit; deferred.
|
|
123
|
+
|
|
124
|
+
3. **`addClusterEvidence` reloads JSON on every call**: O(n) disk reads if many feedback items join a cluster. Resolution: bounded by `maxProposals` (200 default) and feedback-join cardinality (typically <10 per cluster). Acceptable for v1; in-memory state cache is a Phase 5 hardening concern alongside the per-caller rate limit.
|
|
125
|
+
|
|
126
|
+
Round-2 verification:
|
|
127
|
+
|
|
128
|
+
- Producer-allowlist enforcement still throws via Phase 1's `assertProducerKindsAllowed`. Verified by negative tests in `evolution-manager-evidence.test.ts` (commit kind rejected) and the cross-product test in `dispatch-executor-evidence.test.ts` (only allowed kinds populated).
|
|
129
|
+
- Atomic transaction correctness: `addClusterEvidence` test "atomic: array append rolls back fully when one row violates a policy gate" verifies the SQLite transaction wrap. The single-existing-evidence row from `addProposal` is the only row left after the failing array call.
|
|
130
|
+
- TaskFlow Phase 3a non-interference: `evolution-manager-taskflow-dualwrite.test.ts` 10/10 pass with the new cluster-creation step in front of `dualWriteCreate`.
|
|
131
|
+
- Privacy narrowing-only: `addClusterEvidence` rejects `public`-tier evidence on a `shared-project` cluster (test); accepts `private` (test).
|
|
132
|
+
- Failed-dispatch isolation: precondition-failed dispatch leaves `lastDecisionEntityId` as null even with `evidenceCtx` provided (test). The decision entity is only emitted on `success: true`.
|
|
133
|
+
- Backwards compatibility: legacy proposals (no `entityId`) accept `addClusterEvidence` calls as no-ops without throwing (test). `DispatchExecutor.execute(payload)` without `evidenceCtx` is unchanged from Phase 1 behavior (test).
|
|
134
|
+
|
|
135
|
+
Cleared to ship.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Evidence pointers
|
|
140
|
+
|
|
141
|
+
- New tests: `npx vitest run tests/unit/evolution-manager-evidence.test.ts tests/unit/dispatch-executor-evidence.test.ts` → 17/17 passing (≈0.5s).
|
|
142
|
+
- Phase 1 regression: `npx vitest run tests/unit/semantic-memory-evidence.test.ts` → 21/21 passing.
|
|
143
|
+
- TaskFlow Phase 3a regression: `npx vitest run tests/unit/evolution-manager-taskflow-dualwrite.test.ts` → 10/10 passing.
|
|
144
|
+
- SemanticMemory regression: `npx vitest run tests/unit/semantic-memory.test.ts` → 49/49 passing.
|
|
145
|
+
- Typecheck: `npx tsc --noEmit` → clean.
|
|
146
|
+
- Spec source of truth: `docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md` (Convergence: 2026-05-07).
|
|
147
|
+
- Phase 1 baseline: `upgrades/side-effects/wikiclaim-evidence-phase1.md`.
|
|
148
|
+
- Producer allowlist (Phase 1, unchanged in Phase 2): `src/memory/SemanticMemory.ts:1896` — `EvolutionManager: feedback, pattern-entity, supersedes-evidence`; `DispatchExecutor: pattern-entity, job-run, ledger-entry`.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Side-Effects Review — WikiClaim Evidence Phase 3 (DecisionJournal + /learn)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `wikiclaim-evidence-phase3`
|
|
4
|
+
**Date:** 2026-05-10
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** required (breaking contract change for DecisionJournal callers + producer-allowlist boundaries + privacy narrowing reach)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Ships Phase 3 of the WikiClaim evidence import per spec § Migration Plan line 339:
|
|
11
|
+
> Phase 3: Producer integration — DecisionJournal + /learn (one PR)
|
|
12
|
+
> - DecisionJournal entries require at least one evidence entry.
|
|
13
|
+
> - /learn skill prompts for evidence (or auto-derives from conversation context).
|
|
14
|
+
|
|
15
|
+
Spec source: `docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md`
|
|
16
|
+
- § Producers line 217 (integration note — DecisionJournal does not yet produce MemoryEntity rows; bridge promotes journal entries to `decision` entities at log time)
|
|
17
|
+
- § Producers line 227 (DecisionJournal allowlist: `message` | `commit` | `ledger-entry` | `session`)
|
|
18
|
+
- § Producers line 228 (LearnSkill allowlist: `message` | `session`)
|
|
19
|
+
- § Producers line 258 (Decision journal cites conversation lines + commits + ledger entries)
|
|
20
|
+
- § Producers line 268 (Lesson capture requires ≥1 evidence row from the conversation)
|
|
21
|
+
|
|
22
|
+
What lands:
|
|
23
|
+
- **`DecisionJournal.log()` signature change** — adds a required second positional argument `evidence: MemoryEvidence[]`. Empty array or undefined throws `EvidencePolicyError` at write time. Callers across the codebase updated in the same PR.
|
|
24
|
+
- **`DecisionJournal.setSemanticMemory(memory, entityPrivacyScope?)`** — wires the WikiClaim bridge. When set, each `log()` call also calls `memory.rememberWithEvidence(..., 'DecisionJournal')` and stamps the resulting `entityId` onto the JSONL row. Wiring is optional — without it, the evidence is still required at the API level (compile-time + write-time gate), but no MemoryEntity is created. This lets the contract change adopt before the server fully wires SemanticMemory (Phase 4).
|
|
25
|
+
- **`DecisionJournalEntry`** — adds optional `evidence?: MemoryEvidence[]` (always populated on entries written by post-Phase-3 `log()`) and `entityId?: string` (populated only when SemanticMemory was wired at log time).
|
|
26
|
+
- **`LearnSkillBridge`** (new module, `src/core/LearnSkillBridge.ts`):
|
|
27
|
+
- `deriveEvidenceFromContext(context, now?)` — auto-extracts feedback IDs (`fb_<hex>`), commit SHAs (40-hex), session UUIDs and `sess_<hex>` from free-form text. Feedback and commit references are surfaced as `externalReferences` because the LearnSkill producer kind allowlist (spec line 228) does NOT include them — the caller decides whether to route them through `EvolutionManager` / `DispatchExecutor` downstream producers.
|
|
28
|
+
- `buildLearnEvidence({context, documentFallback?, now?})` — combines auto-derivation with a fallback for unstructured input. Synthesizes an inline `message` row from the context body when nothing structured was detected and context is non-empty. Throws `LearnEvidenceError` when context is empty and no `documentFallback` provided.
|
|
29
|
+
- All patterns are pure regex (spec line 357: "No LLM in the migration path"). Cross-store FK validation is best-effort — consumers tolerate dangling references per spec line 219.
|
|
30
|
+
- **HTTP gates** — `POST /intent/journal` accepts an optional `evidence` array in the body; when omitted (or empty) the route synthesizes a minimum-viable `session`-kind evidence row whose `sourceId` is `session:${sessionId}` from the request body. This keeps the legacy POST shape working through the contract change while still satisfying the policy gate at the storage layer. Explicit evidence in the body always overrides synthesis. `POST /evolution/learnings` accepts `context` and/or `documentFallback` and runs the auto-deriver. `EvidencePolicyError` → 400; `LearnEvidenceError` → 400; surfaces derived `evidence`, `externalReferences`, and (when present) `pendingDocumentRef` on the success response.
|
|
31
|
+
|
|
32
|
+
*Rationale for synthesis vs hard-400:* The HTTP layer has only the request `sessionId` as a proxy for an auth principal at this phase (full auth threading lands in Phase 4 — spec § Migration Plan line 359). Hard-rejecting evidence-less POSTs would break every existing programmatic and integration-test caller without giving them a structurally available alternative. `session` evidence with sourceId `session:<id>` is in the DecisionJournal allowlist (spec § Producers line 227) and is a truthful minimum citation: "this decision was made in this session." Synthetic sourceIds are an accepted shape per spec § Storage and Privacy line 333 (consumers tolerate dangling refs). See `tests/integration/intent-routes.test.ts` for the full POST→GET round-trip exercising synthesis.
|
|
33
|
+
- **Producer-kind allowlist** — Phase 1 already declared `DecisionJournal` and `LearnSkill` entries in `PRODUCER_KIND_ALLOWLIST` (`SemanticMemory.ts:1896`). Phase 3 wires them; no allowlist mutation needed. Both kept verbatim against the spec table (lines 225–228).
|
|
34
|
+
|
|
35
|
+
Files touched:
|
|
36
|
+
- `src/core/types.ts` — `DecisionJournalEntry.evidence`, `DecisionJournalEntry.entityId`
|
|
37
|
+
- `src/core/DecisionJournal.ts` — `setSemanticMemory()`, `log(entry, evidence)` required-evidence gate, bridge to `rememberWithEvidence`
|
|
38
|
+
- `src/core/LearnSkillBridge.ts` (new) — auto-derivation + `buildLearnEvidence` + `LearnEvidenceError`
|
|
39
|
+
- `src/commands/intent.ts` — updated conflict-logging caller to attach `ledger-entry` evidence rows for ORG-INTENT.md + AGENT.md
|
|
40
|
+
- `src/server/routes.ts` — `POST /intent/journal` requires evidence; `POST /evolution/learnings` runs LearnSkillBridge
|
|
41
|
+
- `tests/unit/decision-journal-evidence.test.ts` (new) — 15 cases covering the required-evidence gate, bridge promotion, allowlist enforcement, narrowing-only privacy
|
|
42
|
+
- `tests/unit/learn-skill-evidence.test.ts` (new) — 19 cases covering auto-derivation patterns, dedup, fallback, allowlist enforcement, inverse query
|
|
43
|
+
- `tests/unit/DecisionJournal.test.ts` — updated all `journal.log(...)` call sites to pass evidence
|
|
44
|
+
- `tests/unit/DispatchDecisionJournal.test.ts` — single `base.log(...)` in coexistence test updated
|
|
45
|
+
- `upgrades/side-effects/wikiclaim-evidence-phase3.md` (this file)
|
|
46
|
+
|
|
47
|
+
## Decision-point inventory
|
|
48
|
+
|
|
49
|
+
- `DecisionJournal.log()` empty-evidence gate (`Array.isArray + length === 0`) — **add** — mechanic-level. No judgment.
|
|
50
|
+
- LearnSkillBridge regex patterns (`fb_<hex>{8,}`, 40-hex SHA, UUID v4, `sess_<hex>{8,}`) — **add** — pure structural matches. Spec § Risks line 357 explicitly bars LLM in the migration path; regex is the spec-mandated mechanic.
|
|
51
|
+
- Inline-message synthesis fallback — **add** — deterministic hash → `inline:<base36>` sourceId when context is non-empty but no structured ref was found. Mechanic.
|
|
52
|
+
- `LearnEvidenceError` throw when context empty AND no documentFallback — **add** — boundary condition (no source = no evidence = reject per spec line 268).
|
|
53
|
+
- `ORG-INTENT.md` + `AGENT.md` as `ledger-entry` evidence in intent.ts caller — **add** — config files act as the durable ledger for the intent contract; the only spec-allowed DecisionJournal kind that fits a config-file citation is `ledger-entry`. Documented inline at the call site.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 1. Over-block
|
|
58
|
+
|
|
59
|
+
**`DecisionJournal.log` rejects legitimate evidence-less callers:**
|
|
60
|
+
Every existing caller is updated in this PR — there are exactly two write-path callers in the codebase:
|
|
61
|
+
1. `src/commands/intent.ts:266` — `intent-validate` conflict logger. Updated to cite ORG-INTENT.md + AGENT.md as `ledger-entry` evidence.
|
|
62
|
+
2. `src/server/routes.ts:8273` — `POST /intent/journal` handler. Updated to require an `evidence` field in the request body; returns 400 otherwise.
|
|
63
|
+
|
|
64
|
+
`DispatchDecisionJournal.logDispatchDecision` writes directly to JSONL (does not delegate to `DecisionJournal.log`); the dispatch path is unaffected and continues to work without evidence — those entries land on the SAME JSONL file but are typed `type:'dispatch'`, a different schema. Spec § Producers line 226 routes the dispatch decision-evidence path through `DispatchExecutor` (Phase 2), not through this Phase 3 gate.
|
|
65
|
+
|
|
66
|
+
**`LearnSkillBridge` over-extracts SHA-shaped 40-hex strings:** any 40-hex blob in context is matched as a `commit` external reference. Mitigation: kept as an `externalReference` (LearnSkill cannot itself write `commit` kind per allowlist), so the false positive is a caller-side display issue, not a DB integrity issue. Cross-store FK is best-effort per spec line 219; consumers display `[source unavailable]` for dangling refs.
|
|
67
|
+
|
|
68
|
+
**`LearnSkillBridge` over-extracts feedback IDs:** `fb_<hex>{8,}` is conservative — 8+ hex chars covers the FeedbackManager id shape and rejects shorter accidental matches (`fb_x`, `fb_12`).
|
|
69
|
+
|
|
70
|
+
## 2. Under-block
|
|
71
|
+
|
|
72
|
+
**Inline-message synthesis with no real source:** `buildLearnEvidence` synthesizes a `kind:'message'` row with `sourceId: 'inline:<hash>'` when context is non-empty but unstructured. This is technically a "fake" sourceId (no message store has it). Mitigation: documented as the explicit fallback path — the row carries the first 200 chars of the context as `note` for the human reader, and the `inline:` prefix is the conventional marker for "synthetic, no upstream store". Consumers tolerate dangling sourceIds per spec line 219.
|
|
73
|
+
|
|
74
|
+
**`buildLearnEvidence` accepts a `documentFallback` but LearnSkill cannot write `document` kind:** The `pendingDocumentRef` surfaces on the response for the caller to route through a different producer. **This is a real gap** — there is no current producer-kind-allowlist entry that includes `document` for a LearnSkill-shaped caller. A future task is to extend the allowlist (or to add a new producer ID like `LearnSkillDocument`) when documents-as-evidence becomes a first-class use case. For Phase 3, the pendingDocumentRef is structurally surfaced but not yet writable.
|
|
75
|
+
|
|
76
|
+
## 3. Level of abstraction
|
|
77
|
+
|
|
78
|
+
The gate is at the structural boundary: `DecisionJournal.log` (the only public write entry-point) and the HTTP route handlers. It is NOT scattered across callers — every caller passes evidence through the same single chokepoint. Producer-kind allowlist enforcement lives inside `SemanticMemory.assertProducerKindsAllowed`, not duplicated at the bridge layer. Privacy narrowing-only is enforced inside `insertEvidenceRows` (Phase 1), not re-checked at the bridge. The Phase 3 surface adds:
|
|
79
|
+
- One compile-time gate (function signature change in `DecisionJournal.log`)
|
|
80
|
+
- One runtime gate (`Array.isArray + length === 0` in `DecisionJournal.log`)
|
|
81
|
+
- One bridge call site (`memory.rememberWithEvidence(..., 'DecisionJournal')`)
|
|
82
|
+
- One auto-deriver module (`LearnSkillBridge`)
|
|
83
|
+
|
|
84
|
+
No new privacy-enforcement code; no new allowlist-enforcement code; no new cycle-detection code. The Phase 3 layer is a thin wiring layer over Phase 1's policy gates.
|
|
85
|
+
|
|
86
|
+
## 4. Signal vs authority separation (per MEMORY.md feedback_signal_vs_authority.md)
|
|
87
|
+
|
|
88
|
+
The brittle/low-context detectors (the regex patterns in `LearnSkillBridge`) emit SIGNALS (`DerivedEvidence` rows). The higher-level intelligent gate (`buildLearnEvidence` + the HTTP handler) is the authority that decides whether the signals are sufficient or to fall back. `LearnSkillBridge` exports two distinct functions — the raw deriver and the policy wrapper — so a future Phase 4 caller can replace `buildLearnEvidence` with an LLM-aware version without changing the signal layer.
|
|
89
|
+
|
|
90
|
+
## 5. Producer-allowlist interactions with sibling phases
|
|
91
|
+
|
|
92
|
+
- **Phase 2 (EvolutionManager + DispatchExecutor)**: independent producers, independent kinds. No collision.
|
|
93
|
+
- **Phase 1 (SemanticMemory schema)**: Phase 3 uses the schema unchanged. No migration needed.
|
|
94
|
+
- **Phase 4 (Inverse-traceability HTTP)**: Phase 3 produces the `decision` and (downstream) `lesson` MemoryEntity rows that Phase 4's `/memory/entities/by-evidence` queries will inverse-traverse. Phase 4 is forward-compatible: existing pre-Phase-3 entries with `evidence: undefined` are filtered out of inverse queries naturally (no evidence row → no citation row).
|
|
95
|
+
- **Phase 5 (backfill-evidence CLI)**: Phase 5 walks pre-Phase-3 decision-journal entries and emits `evidence: []` (no upgrade possible per spec line 211). Phase 3's required-evidence gate does NOT apply at backfill time — backfill writes the entity directly via `rememberWithEvidence` with an explicit empty array AND a `manual` producer override, separate code path.
|
|
96
|
+
|
|
97
|
+
## 6. Rollback cost
|
|
98
|
+
|
|
99
|
+
The contract change is structural (function signature). Rollback path:
|
|
100
|
+
1. Revert `DecisionJournal.log` to the old single-arg signature.
|
|
101
|
+
2. Revert the two write-path callers (`intent.ts`, `routes.ts`) to drop the evidence argument.
|
|
102
|
+
3. JSONL entries written with `evidence` fields are forward-compatible — the older reader ignores unknown fields. No data migration needed.
|
|
103
|
+
4. The `decision` MemoryEntity rows created by the bridge are orphaned but harmless — they remain queryable; the cascade-delete FK keeps evidence consistent if the entity is later forgotten.
|
|
104
|
+
|
|
105
|
+
Rollback is a one-PR revert; downstream readers (intent dashboard) tolerate `evidence: undefined` gracefully.
|
|
106
|
+
|
|
107
|
+
## 7. Threat model deltas (per spec § Threat Model line 365)
|
|
108
|
+
|
|
109
|
+
- **Spoofed evidence via `LearnSkill`**: a caller passes `evidence: [{kind:'feedback', sourceId:'fb_attack'}]` directly to bypass allowlist. Mitigation: `SemanticMemory.assertProducerKindsAllowed` rejects with `EvidencePolicyError`. Test: `learn-skill-evidence.test.ts > LearnSkill CANNOT write 'feedback' kind`.
|
|
110
|
+
- **Privacy bypass via decision-bridge widening**: a caller wires `entityPrivacyScope: 'shared-project'` then attaches `privacyTier: 'public'` evidence. Mitigation: `assertNarrowingOnly` (Phase 1) rejects. Test: `decision-journal-evidence.test.ts > rejects widening-only privacy violation`.
|
|
111
|
+
- **Note-field exfiltration via inline-message fallback**: the inline `message` row puts `context.slice(0, 200)` into the `note` field. Mitigation: spec line 372 documents notes inherit entity scope and the 500-byte cap is enforced inside `assertEvidenceShape`. The 200-char truncation is below the 500-byte cap.
|
|
112
|
+
- **Producer-crash partial-write corruption**: a bridge crash mid-`rememberWithEvidence` could leave an orphan entity. Mitigation: `rememberWithEvidence` is one better-sqlite3 transaction (Phase 1). Verified by `decision-journal-evidence.test.ts > persists no JSONL row when evidence gate rejects`.
|
|
113
|
+
|
|
114
|
+
## 8. Verification
|
|
115
|
+
|
|
116
|
+
- Typecheck: `npx tsc --noEmit` clean.
|
|
117
|
+
- New tests: 15 in `decision-journal-evidence.test.ts` + 19 in `learn-skill-evidence.test.ts` = 34 new cases.
|
|
118
|
+
- Updated tests: 12 call-site adjustments in `DecisionJournal.test.ts`, 1 in `DispatchDecisionJournal.test.ts`.
|
|
119
|
+
- Full unit suite: same set of pre-existing failures as origin/main (`feature-delivery-completeness`, `no-silent-fallbacks`, `pre-push-gate`, `security`, `sharedStateRoutesV2`, `telemetry-routes`) — none introduced by this change. `route-completeness` (previously failing on `instanceof Error` ratchet) now passes.
|
|
120
|
+
- Manifest auto-regenerated by `npm run generate:manifest` reflecting `routes.ts` content hash change.
|
|
121
|
+
|
|
122
|
+
## 9. Rebase-on-main bookkeeping (2026-05-10)
|
|
123
|
+
|
|
124
|
+
After this branch was opened, three PRs merged on `main` (TaskFlow Phase 5, WikiClaim Phase 5, WikiClaim Phase 4). The branch was rebased onto current `main`; conflicts resolved in `upgrades/NEXT.md` only (additive across all four phases). `src/server/routes.ts` and `src/core/types.ts` auto-merged with no manual intervention. The builtin-manifest's route-group content hashes were regenerated by `npm run generate:manifest` to reflect the union of all four phases' route additions. Re-verified: `npx tsc --noEmit` clean; full `intent-routes.test.ts` (16) + `decision-journal-evidence.test.ts` (15) + `learn-skill-evidence.test.ts` (19) + `DecisionJournal.test.ts` (23) + `DispatchDecisionJournal.test.ts` (41) + `drift-routes.test.ts` (6) all pass.
|
|
125
|
+
|