wendkeep 0.76.5 → 0.76.7
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/CHANGELOG.md +26 -0
- package/README.en.md +7 -2
- package/README.md +7 -2
- package/docs/en/commands/memory.md +8 -1
- package/docs/en/commands/operating-profiles.md +6 -0
- package/docs/pt-BR/commands/memory.md +8 -2
- package/docs/pt-BR/commands/operating-profiles.md +6 -0
- package/hooks/active-context-handoff-evidence.mjs +196 -0
- package/hooks/active-context-store.mjs +5 -2
- package/hooks/change-nag.mjs +7 -3
- package/hooks/evidence-context.mjs +20 -5
- package/hooks/operating-profile-runtime.mjs +23 -1
- package/hooks/operating-profile-task-store.mjs +122 -18
- package/hooks/session-stop.mjs +34 -20
- package/package.json +2 -2
- package/src/profile.mjs +21 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.76.7] — 2026-08-22
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Handoff causal no Stop.** Com `active_contexts` inicializado, work session, repository,
|
|
12
|
+
worktree, branch e change vêm do contexto chamador; payload divergente falha antes de CAS,
|
|
13
|
+
nota, outbox ou ledger e não publica estado parcial.
|
|
14
|
+
- **Evidência da change correta.** O lifecycle coleta verdict, sensores e ADR apenas da change do
|
|
15
|
+
active context causal, sem reutilizar `change_slug` legado ou link global de uma sessão irmã.
|
|
16
|
+
- **Recall automático escopado.** `UserPromptSubmit` permanece somente leitura, sem migrar pointer
|
|
17
|
+
ou registry; exclui evidência de sessão/change irmã ativa e preserva material global/histórico.
|
|
18
|
+
`/brain-recall` explícito segue global.
|
|
19
|
+
- **Compatibilidade conservadora.** O comportamento anterior permanece somente quando o registry
|
|
20
|
+
ainda não possui nenhum campo do store contextual, inclusive quando `active_contexts` está vazio.
|
|
21
|
+
|
|
22
|
+
## [0.76.6] — 2026-08-22
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Task lease causal.** `operating_profile_task` passa a pertencer ao `active_contexts` da
|
|
27
|
+
worktree/work session; rotas temporárias irmãs não se sobrescrevem nem ampliam autorização.
|
|
28
|
+
- **CLI e hooks escopados.** `profile route/status`, os hooks de operating profile e o consumo no
|
|
29
|
+
Stop resolvem a lease do chamador e preservam o sibling byte a byte por revision/CAS.
|
|
30
|
+
- **Fallback legado fail-closed.** Vault sem registry contextual conserva a lease na sessão;
|
|
31
|
+
depois da inicialização, uma lease global não é copiada nem aplicada sem identidade provada.
|
|
32
|
+
|
|
7
33
|
## [0.76.5] — 2026-08-22
|
|
8
34
|
|
|
9
35
|
### Added
|
package/README.en.md
CHANGED
|
@@ -343,6 +343,11 @@ selects the caller explicitly. `CURRENT_DELIVERY` is only a derived projection w
|
|
|
343
343
|
single, unambiguous context; hooks consult only the causal delivery, and an ID from another context
|
|
344
344
|
fails with `WENDKEEP_DELIVERY_CONTEXT_MISMATCH`.
|
|
345
345
|
|
|
346
|
+
`operating_profile_task` belongs to the causal work session's active context. `profile route` and
|
|
347
|
+
`profile status --session <id>`, hooks, and Stop read/consume only that lease; a temporary route
|
|
348
|
+
never crosses into a sibling. Legacy session fallback exists only without `active_contexts`;
|
|
349
|
+
an initialized registry never copies a global authorization without proven identity.
|
|
350
|
+
|
|
346
351
|
```bash
|
|
347
352
|
npx wendkeep delivery start release-0-74-0 --allow git:merge --allow git:push --allow publish --source-change <slug> --source-commit <sha> --session <id>
|
|
348
353
|
npx wendkeep delivery status release-0-74-0 --session <id>
|
|
@@ -381,8 +386,8 @@ Hot memory now separates human authorship, operational state, and evidence:
|
|
|
381
386
|
- **`SHARED_MEMORY.md` is bounded generated operational state.** The `Stop` hook turns the session handoff into sanitized events; the projector deterministically reduces the ledger and publishes a verifiable revision, cursor, and hash. Admission prioritizes critical operational state and never publishes beyond 48 lines/6 KiB; omitted events remain in the append-only authority and surface only as verifiable counts. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
|
|
382
387
|
- **`MEMORY_EVENTS.jsonl` is the append-only authority.** `Stop` makes events durable in the outbox before acknowledging the attempt; the projector runs outside the registry lock and retries reuse the same IDs. Repeating an identical `event_id`/payload is a no-op; reusing the ID with different bytes is observable corruption.
|
|
383
388
|
- **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event; promotion preserves the selected event's JSON type, session, activation/epoch, and source turn.
|
|
384
|
-
- **Registers are scoped.** `git.local-head`, handoffs, verdicts, and change status carry project, work-session, change, branch, or worktree scope. Two branches do not create a global conflict; only events in the same scope and causal lineage may advance automatically.
|
|
385
|
-
- **`EVIDENCE_INDEX.jsonl` is local recall.** Markdown is chunked by headings and blocks without requiring the Observer. Ranking combines BM25, exact phrases, field weights, authority, validity, bounded recency, and source diversity
|
|
389
|
+
- **Registers are scoped.** `git.local-head`, handoffs, verdicts, and change status carry project, work-session, change, branch, or worktree scope. Two branches do not create a global conflict; only events in the same scope and causal lineage may advance automatically. Once `active_contexts` is initialized, `Stop` derives `work_session_id`, `repository_id`, `worktree_id`, branch, and change from the causal active context; a divergent handoff fails before CAS, note, outbox, or ledger mutation. Legacy compatibility exists only without the contextual store.
|
|
390
|
+
- **`EVIDENCE_INDEX.jsonl` is local recall.** Markdown is chunked by headings and blocks without requiring the Observer. Ranking combines BM25, exact phrases, field weights, authority, validity, bounded recency, and source diversity. The automatic `UserPromptSubmit` hook is read-only: it never migrates `CURRENT_CHANGE.md` or mutates the registry; it excludes passages owned by an active sibling session or change while preserving global and historical evidence. Explicit `/brain-recall` remains global.
|
|
386
391
|
|
|
387
392
|
Artifacts stay under `.brain/` only. Sanitization strips secrets, tokens, local paths, transcripts, and harness payloads both before persistence and before injection. Events carry a `project_id`, and one vault never accepts another project's events.
|
|
388
393
|
|
package/README.md
CHANGED
|
@@ -343,6 +343,11 @@ selects the caller explicitly. `CURRENT_DELIVERY` is only a derived projection w
|
|
|
343
343
|
single, unambiguous context; hooks consult only the causal delivery, and an ID from another context
|
|
344
344
|
fails with `WENDKEEP_DELIVERY_CONTEXT_MISMATCH`.
|
|
345
345
|
|
|
346
|
+
`operating_profile_task` belongs to the causal work session's active context. `profile route` and
|
|
347
|
+
`profile status --session <id>`, hooks, and Stop read/consume only that lease; a temporary route
|
|
348
|
+
never crosses into a sibling. Legacy session fallback exists only without `active_contexts`;
|
|
349
|
+
an initialized registry never copies a global authorization without proven identity.
|
|
350
|
+
|
|
346
351
|
```bash
|
|
347
352
|
npx wendkeep delivery start release-0-74-0 --allow git:merge --allow git:push --allow publish --source-change <slug> --source-commit <sha> --session <id>
|
|
348
353
|
npx wendkeep delivery status release-0-74-0 --session <id>
|
|
@@ -381,8 +386,8 @@ Hot memory now separates human authorship, operational state, and evidence:
|
|
|
381
386
|
- **`SHARED_MEMORY.md` is bounded generated operational state.** The `Stop` hook turns the session handoff into sanitized events; the projector deterministically reduces the ledger and publishes a verifiable revision, cursor, and hash. Admission prioritizes critical operational state and never publishes beyond 48 lines/6 KiB; omitted events remain in the append-only authority and surface only as verifiable counts. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
|
|
382
387
|
- **`MEMORY_EVENTS.jsonl` is the append-only authority.** `Stop` makes events durable in the outbox before acknowledging the attempt; the projector runs outside the registry lock and retries reuse the same IDs. Repeating an identical `event_id`/payload is a no-op; reusing the ID with different bytes is observable corruption.
|
|
383
388
|
- **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event; promotion preserves the selected event's JSON type, session, activation/epoch, and source turn.
|
|
384
|
-
- **Registers are scoped.** `git.local-head`, handoffs, verdicts, and change status carry project, work-session, change, branch, or worktree scope. Two branches do not create a global conflict; only events in the same scope and causal lineage may advance automatically.
|
|
385
|
-
- **`EVIDENCE_INDEX.jsonl` is local recall.** Markdown is chunked by headings and blocks without requiring the Observer. Ranking combines BM25, exact phrases, field weights, authority, validity, bounded recency, and source diversity
|
|
389
|
+
- **Registers are scoped.** `git.local-head`, handoffs, verdicts, and change status carry project, work-session, change, branch, or worktree scope. Two branches do not create a global conflict; only events in the same scope and causal lineage may advance automatically. Once `active_contexts` is initialized, `Stop` derives `work_session_id`, `repository_id`, `worktree_id`, branch, and change from the causal active context; a divergent handoff fails before CAS, note, outbox, or ledger mutation. Legacy compatibility exists only without the contextual store.
|
|
390
|
+
- **`EVIDENCE_INDEX.jsonl` is local recall.** Markdown is chunked by headings and blocks without requiring the Observer. Ranking combines BM25, exact phrases, field weights, authority, validity, bounded recency, and source diversity. The automatic `UserPromptSubmit` hook is read-only: it never migrates `CURRENT_CHANGE.md` or mutates the registry; it excludes passages owned by an active sibling session or change while preserving global and historical evidence. Explicit `/brain-recall` remains global.
|
|
386
391
|
|
|
387
392
|
Artifacts stay under `.brain/` only. Sanitization strips secrets, tokens, local paths, transcripts, and harness payloads both before persistence and before injection. Events carry a `project_id`, and one vault never accepts another project's events.
|
|
388
393
|
|
|
@@ -115,10 +115,17 @@ npx wendkeep validate-memory --vault <v2-vault>
|
|
|
115
115
|
`change.<slug>.status` compete only inside the same scope. Automatic resolution still requires
|
|
116
116
|
the same project and causal lineage; incompatible decisions, constraints, and blockers remain
|
|
117
117
|
human-curated. An ambiguous key is omitted from SHARED without removing CORE or independent keys.
|
|
118
|
+
Once `active_contexts` is initialized, `Stop` derives `work_session_id`, `repository_id`,
|
|
119
|
+
`worktree_id`, branch, and `change_slug` from the causal active context. A divergent handoff
|
|
120
|
+
identity fails before CAS, note, outbox, or ledger mutation; legacy fallback exists only without
|
|
121
|
+
the contextual store.
|
|
118
122
|
- `.brain/EVIDENCE_INDEX.jsonl` chunks documents by heading and block and records file, heading,
|
|
119
123
|
type, change, session, work session, authority, date, validity, and hash. `/brain-recall` and the
|
|
120
124
|
`UserPromptSubmit` hook use BM25, exact phrase, field weights, authority, validity, bounded
|
|
121
|
-
recency, and diversity to return the matching passage with provenance.
|
|
125
|
+
recency, and diversity to return the matching passage with provenance. Automatic recall is
|
|
126
|
+
read-only: it never migrates `CURRENT_CHANGE.md` or mutates the registry; it excludes rows owned
|
|
127
|
+
by an active sibling session or change while preserving global or historical rows with no active
|
|
128
|
+
owner. Explicit `/brain-recall` remains global.
|
|
122
129
|
- Every memory path validates the physical topology of `.brain`, ledger, outbox, CORE, SHARED,
|
|
123
130
|
candidates, registry, notes, backups, temporary files, and sidecars before reading or writing.
|
|
124
131
|
Junctions, symlinks, reparse points, or hardlinks fail closed without touching external bytes.
|
|
@@ -130,6 +130,12 @@ ownership to the native LLM harness.
|
|
|
130
130
|
session identity.
|
|
131
131
|
- `profile route` requires `--session` and `--reason`, accepts only the four adaptive profiles,
|
|
132
132
|
and records lease id, reason, turn/sequence, and timestamp without touching persistent profiles.
|
|
133
|
+
- `operating_profile_task` belongs to the causal session/work session's active context. `profile route`
|
|
134
|
+
creates and `profile status --session <id>` reads only that lease; hooks use the same caller
|
|
135
|
+
context and an accepted Stop consumes it by `lease_id` with revision/CAS without changing a sibling.
|
|
136
|
+
- Legacy fallback of the lease in the session exists only without `active_contexts`. With an
|
|
137
|
+
initialized registry, a global lease absent from the context is neither copied nor applied;
|
|
138
|
+
missing, ambiguous, or stale context fails closed without a partial mutation.
|
|
133
139
|
- `.wendkeep.json` stays on `schemaVersion: 1`; the additive field is, for example,
|
|
134
140
|
`"harness": { "profile": "GOVERN" }`. A legacy binding without it also resolves to `GOVERN`.
|
|
135
141
|
- A corrupt binding never means `OFF`. When the payload or legacy integration identifies one
|
|
@@ -114,11 +114,17 @@ npx wendkeep validate-memory --vault <cofre-v2>
|
|
|
114
114
|
- Registradores como `git.local-head`, `handoff.latest`, `quality.latest-*` e
|
|
115
115
|
`change.<slug>.status` só competem dentro do mesmo escopo. Resolução automática ainda exige o
|
|
116
116
|
mesmo projeto e linhagem causal; decisões, constraints e blockers incompatíveis permanecem sob
|
|
117
|
-
curadoria. Uma chave ambígua é omitida de SHARED sem remover CORE ou chaves independentes.
|
|
117
|
+
curadoria. Uma chave ambígua é omitida de SHARED sem remover CORE ou chaves independentes. Com
|
|
118
|
+
`active_contexts` inicializado, o `Stop` deriva `work_session_id`, `repository_id`, `worktree_id`,
|
|
119
|
+
branch e `change_slug` do active context causal. Identidade divergente no handoff falha antes de
|
|
120
|
+
CAS, nota, outbox ou ledger; o fallback legado só existe sem o store contextual.
|
|
118
121
|
- `.brain/EVIDENCE_INDEX.jsonl` divide documentos por headings e blocos e registra arquivo,
|
|
119
122
|
heading, tipo, change, sessão, work session, autoridade, data, validade e hash. `/brain-recall`
|
|
120
123
|
e o hook `UserPromptSubmit` usam BM25, frase exata, pesos por campo, autoridade, validade,
|
|
121
|
-
recência limitada e diversidade para retornar o trecho do match com proveniência.
|
|
124
|
+
recência limitada e diversidade para retornar o trecho do match com proveniência. No recall
|
|
125
|
+
automático, o hook é somente leitura: não migra `CURRENT_CHANGE.md` nem altera o registry;
|
|
126
|
+
exclui linhas pertencentes a sessão ou change irmã ativa e preserva linhas globais ou históricas
|
|
127
|
+
sem owner ativo. `/brain-recall` explícito permanece global.
|
|
122
128
|
- Toda rota de memória valida a topologia física de `.brain`, ledger, outbox, CORE, SHARED,
|
|
123
129
|
candidates, registry, notas, backups, temporários e sidecars antes de ler ou escrever. Junction,
|
|
124
130
|
symlink, reparse point ou hardlink falham fechados sem tocar bytes externos. Locks publicam owner
|
|
@@ -131,6 +131,12 @@ harness nativo da LLM.
|
|
|
131
131
|
projeto; com `--session`, grava override, origem e timestamp sem trocar a identidade da sessão.
|
|
132
132
|
- `profile route` exige `--session` e `--reason`, aceita somente os quatro perfis adaptativos e
|
|
133
133
|
grava `lease_id`, motivo, turno/sequência e timestamp sem tocar no perfil persistente.
|
|
134
|
+
- `operating_profile_task` pertence ao active context da work session/sessão causal. `profile route`
|
|
135
|
+
cria e `profile status --session <id>` consulta somente essa lease; os hooks usam o mesmo contexto
|
|
136
|
+
do chamador e o Stop aceito consome por `lease_id` com revision/CAS, sem alterar o sibling.
|
|
137
|
+
- O fallback legado de lease na sessão existe apenas sem `active_contexts`. Com o registry
|
|
138
|
+
inicializado, uma lease global ausente do contexto não é copiada nem aplicada; contexto
|
|
139
|
+
ausente, ambíguo ou stale falha fechado sem mutação parcial.
|
|
134
140
|
- `.wendkeep.json` continua em `schemaVersion: 1`; o campo aditivo usa, por exemplo,
|
|
135
141
|
`"harness": { "profile": "GOVERN" }`. Binding legado sem o campo também resolve `GOVERN`.
|
|
136
142
|
- Binding corrompido nunca equivale a `OFF`. Quando o payload ou a integração legada identifica
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { readSessionRegistry } from './obsidian-common.mjs';
|
|
2
|
+
import { resolveActiveContext } from './active-context-store.mjs';
|
|
3
|
+
import { resolveRuntimeActiveContext } from '../src/active-context-runtime.mjs';
|
|
4
|
+
|
|
5
|
+
const IDENTITY_FIELDS = Object.freeze([
|
|
6
|
+
['work_session_id', 'workSessionId'],
|
|
7
|
+
['repository_id', 'repositoryId'],
|
|
8
|
+
['worktree_id', 'worktreeId'],
|
|
9
|
+
['branch', 'branch'],
|
|
10
|
+
['change_slug', 'changeSlug'],
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function contextError(code, message) {
|
|
14
|
+
const error = new Error(message);
|
|
15
|
+
error.code = code;
|
|
16
|
+
return error;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function text(value) {
|
|
20
|
+
return String(value ?? '').trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function contextsOf(registry) {
|
|
24
|
+
return registry?.active_contexts && typeof registry.active_contexts === 'object'
|
|
25
|
+
&& !Array.isArray(registry.active_contexts)
|
|
26
|
+
? registry.active_contexts : {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function activeContextRegistryInitialized(registry = {}) {
|
|
30
|
+
return Object.hasOwn(registry, 'active_contexts')
|
|
31
|
+
|| Object.hasOwn(registry, 'active_contexts_schema')
|
|
32
|
+
|| Object.hasOwn(registry, 'active_contexts_revision');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertAuthority(identity, context) {
|
|
36
|
+
if (!identity || !context || context.state !== 'active') {
|
|
37
|
+
throw contextError(
|
|
38
|
+
'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND',
|
|
39
|
+
'active context causal não foi resolvido antes do Stop',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
for (const [contextField, identityField] of [
|
|
43
|
+
['project_id', 'projectId'],
|
|
44
|
+
['repository_id', 'repositoryId'],
|
|
45
|
+
['worktree_id', 'worktreeId'],
|
|
46
|
+
['work_session_id', 'workSessionId'],
|
|
47
|
+
]) {
|
|
48
|
+
if (!text(context[contextField]) || text(context[contextField]) !== text(identity[identityField])) {
|
|
49
|
+
throw contextError(
|
|
50
|
+
'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH',
|
|
51
|
+
`${contextField} do active context não corresponde à sessão causal`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!text(context.branch) || text(context.branch) !== text(identity.branch)) {
|
|
56
|
+
throw contextError(
|
|
57
|
+
'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH',
|
|
58
|
+
'branch do active context não corresponde à sessão causal',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveHandoffEvidenceAuthority(vaultBase, {
|
|
64
|
+
input = {},
|
|
65
|
+
sessionId = '',
|
|
66
|
+
projectRoot = input.cwd || process.cwd(),
|
|
67
|
+
registry = readSessionRegistry(vaultBase),
|
|
68
|
+
resolveCommand = resolveRuntimeActiveContext,
|
|
69
|
+
resolveContext = resolveActiveContext,
|
|
70
|
+
} = {}) {
|
|
71
|
+
if (!activeContextRegistryInitialized(registry)) {
|
|
72
|
+
return { mode: 'legacy', identity: null, context: null };
|
|
73
|
+
}
|
|
74
|
+
const identity = resolveCommand({
|
|
75
|
+
vaultBase,
|
|
76
|
+
projectRoot,
|
|
77
|
+
sessionId,
|
|
78
|
+
});
|
|
79
|
+
if (!identity) {
|
|
80
|
+
throw contextError(
|
|
81
|
+
'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND',
|
|
82
|
+
'registry contextual inicializado sem identidade causal para o Stop',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const context = resolveContext(vaultBase, { ...identity, sessionId });
|
|
86
|
+
assertAuthority(identity, context);
|
|
87
|
+
return { mode: 'contextual', identity, context };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolveReadOnlyEvidenceActiveContext({
|
|
91
|
+
vaultBase,
|
|
92
|
+
projectRoot = process.cwd(),
|
|
93
|
+
sessionId = '',
|
|
94
|
+
} = {}) {
|
|
95
|
+
const authority = resolveHandoffEvidenceAuthority(vaultBase, {
|
|
96
|
+
input: { cwd: projectRoot },
|
|
97
|
+
projectRoot,
|
|
98
|
+
sessionId,
|
|
99
|
+
});
|
|
100
|
+
return authority.mode === 'contextual' ? authority.identity : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function suppliedShared(input) {
|
|
104
|
+
const supplied = input?.shared || input?.handoff?.shared;
|
|
105
|
+
return supplied && typeof supplied === 'object' && !Array.isArray(supplied)
|
|
106
|
+
? { ...supplied }
|
|
107
|
+
: {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function suppliedIdentityValues(input, shared, snake, camel) {
|
|
111
|
+
const direct = input?.shared && typeof input.shared === 'object' && !Array.isArray(input.shared)
|
|
112
|
+
? input.shared : {};
|
|
113
|
+
const nested = input?.handoff?.shared && typeof input.handoff.shared === 'object'
|
|
114
|
+
&& !Array.isArray(input.handoff.shared)
|
|
115
|
+
? input.handoff.shared : {};
|
|
116
|
+
return [
|
|
117
|
+
shared[snake], shared[camel],
|
|
118
|
+
direct[snake], direct[camel],
|
|
119
|
+
nested[snake], nested[camel],
|
|
120
|
+
input?.[snake], input?.[camel],
|
|
121
|
+
]
|
|
122
|
+
.map(text)
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildCausalSharedHandoff({ input = {}, entry = {}, authority } = {}) {
|
|
127
|
+
const shared = suppliedShared(input);
|
|
128
|
+
if (authority?.mode !== 'contextual') {
|
|
129
|
+
const workSessionId = text(
|
|
130
|
+
shared.work_session_id
|
|
131
|
+
|| shared.workSessionId
|
|
132
|
+
|| input.work_session_id
|
|
133
|
+
|| input.workSessionId
|
|
134
|
+
|| entry?.work_session_id,
|
|
135
|
+
);
|
|
136
|
+
if (!shared.work_session_id && workSessionId) shared.work_session_id = workSessionId;
|
|
137
|
+
return Object.keys(shared).length ? shared : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
assertAuthority(authority.identity, authority.context);
|
|
141
|
+
const expected = {
|
|
142
|
+
workSessionId: text(authority.identity.workSessionId),
|
|
143
|
+
repositoryId: text(authority.identity.repositoryId),
|
|
144
|
+
worktreeId: text(authority.identity.worktreeId),
|
|
145
|
+
branch: text(authority.identity.branch || authority.context.branch),
|
|
146
|
+
changeSlug: text(authority.context.change_slug),
|
|
147
|
+
};
|
|
148
|
+
for (const [snake, camel] of IDENTITY_FIELDS) {
|
|
149
|
+
const suppliedValues = suppliedIdentityValues(input, shared, snake, camel);
|
|
150
|
+
if (suppliedValues.some((supplied) => supplied !== expected[camel])) {
|
|
151
|
+
throw contextError(
|
|
152
|
+
'WENDKEEP_HANDOFF_CONTEXT_MISMATCH',
|
|
153
|
+
`${snake} do handoff não corresponde ao active context causal`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
delete shared[camel];
|
|
157
|
+
if (expected[camel]) shared[snake] = expected[camel];
|
|
158
|
+
else delete shared[snake];
|
|
159
|
+
}
|
|
160
|
+
return Object.keys(shared).length ? shared : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function causalChangeSlug(entry = {}, authority = {}) {
|
|
164
|
+
return authority?.mode === 'contextual'
|
|
165
|
+
? text(authority.context?.change_slug)
|
|
166
|
+
: text(entry?.change_slug);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function scopeEvidenceRows(rows, { activeContext = null, registry = {} } = {}) {
|
|
170
|
+
if (!Array.isArray(rows) || !activeContextRegistryInitialized(registry)) return rows;
|
|
171
|
+
|
|
172
|
+
const callerWorkSession = text(activeContext?.workSessionId ?? activeContext?.work_session_id);
|
|
173
|
+
const sessions = registry.sessions || {};
|
|
174
|
+
const activeContexts = Object.values(contextsOf(registry))
|
|
175
|
+
.filter((context) => context?.state === 'active');
|
|
176
|
+
const activeChangeOwners = new Map();
|
|
177
|
+
for (const context of activeContexts) {
|
|
178
|
+
const slug = text(context.change_slug);
|
|
179
|
+
if (slug) activeChangeOwners.set(slug, text(context.work_session_id));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return rows.filter((row) => {
|
|
183
|
+
const sessionId = text(row?.session_id);
|
|
184
|
+
const rowWorkSession = text(row?.work_session_id)
|
|
185
|
+
|| text(sessionId && sessions[sessionId]?.work_session_id);
|
|
186
|
+
if (sessionId && !rowWorkSession) return false;
|
|
187
|
+
if (rowWorkSession && rowWorkSession !== callerWorkSession) return false;
|
|
188
|
+
if (rowWorkSession && !callerWorkSession) return false;
|
|
189
|
+
|
|
190
|
+
const changeSlug = text(row?.change_slug);
|
|
191
|
+
const owner = activeChangeOwners.get(changeSlug) || '';
|
|
192
|
+
if (owner && owner !== callerWorkSession) return false;
|
|
193
|
+
if (owner && !callerWorkSession) return false;
|
|
194
|
+
return true;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -139,6 +139,7 @@ export function mutateActiveContext(vaultBase, identity, updater, {
|
|
|
139
139
|
expectedRevision,
|
|
140
140
|
now = new Date().toISOString(),
|
|
141
141
|
mutateRegistry = mutateSessionRegistry,
|
|
142
|
+
projectLegacy = true,
|
|
142
143
|
} = {}) {
|
|
143
144
|
const normalized = normalizeIdentity(identity);
|
|
144
145
|
const key = activeContextKey(normalized);
|
|
@@ -191,8 +192,10 @@ export function mutateActiveContext(vaultBase, identity, updater, {
|
|
|
191
192
|
registry.active_contexts = { ...contexts, [key]: next };
|
|
192
193
|
return { key, context: structuredClone(next), registryRevision: registry.active_contexts_revision };
|
|
193
194
|
});
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
if (projectLegacy) {
|
|
196
|
+
projectLegacyActiveChange(vaultBase);
|
|
197
|
+
projectLegacyActiveDelivery(vaultBase);
|
|
198
|
+
}
|
|
196
199
|
return result;
|
|
197
200
|
}
|
|
198
201
|
|
package/hooks/change-nag.mjs
CHANGED
|
@@ -15,11 +15,11 @@ import {
|
|
|
15
15
|
} from './operating-profile-runtime.mjs';
|
|
16
16
|
import { consumeSessionTaskOperatingProfile } from './operating-profile-task-store.mjs';
|
|
17
17
|
|
|
18
|
-
export function nagDecision(input, vaultBase, { profile = 'GOVERN' } = {}) {
|
|
18
|
+
export function nagDecision(input, vaultBase, { profile = 'GOVERN', context = null } = {}) {
|
|
19
19
|
if (input && input.stop_hook_active) return null; // anti-loop: sempre primeiro
|
|
20
20
|
const policy = hookProfilePolicy(profile);
|
|
21
21
|
if (!policy.harness) return null;
|
|
22
|
-
const gate = quickGateState(vaultBase);
|
|
22
|
+
const gate = quickGateState(vaultBase, { context });
|
|
23
23
|
if (!gate || !gate.openTasks) return null;
|
|
24
24
|
const sid = input?.session_id || input?.sessionId || '';
|
|
25
25
|
const sentinelId = profileSentinelId(sid, profile);
|
|
@@ -39,12 +39,16 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
39
39
|
? null
|
|
40
40
|
: runtime.bindingError
|
|
41
41
|
? { decision: 'block', reason: profileRuntimeError(runtime.bindingError) }
|
|
42
|
-
: nagDecision(input, runtime.vaultBase, {
|
|
42
|
+
: nagDecision(input, runtime.vaultBase, {
|
|
43
|
+
profile: runtime.profile,
|
|
44
|
+
context: runtime.activeContext,
|
|
45
|
+
});
|
|
43
46
|
if (!decision && runtime.taskLease?.state === 'active') {
|
|
44
47
|
consumeSessionTaskOperatingProfile(
|
|
45
48
|
runtime.vaultBase,
|
|
46
49
|
runtime.identity?.canonicalConversationId || input?.session_id || input?.sessionId || '',
|
|
47
50
|
runtime.taskLease.lease_id,
|
|
51
|
+
{ context: runtime.activeContext },
|
|
48
52
|
);
|
|
49
53
|
}
|
|
50
54
|
writeHookOutput(decision || {});
|
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// UserPromptSubmit: bounded, read-only retrieval from the local evidence index.
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
|
|
4
|
+
import { readHookInput, readSessionRegistry, writeHookOutput } from './obsidian-common.mjs';
|
|
5
5
|
import { loadEvidenceIndex, recallEvidence, renderEvidenceContext } from './evidence-recall.mjs';
|
|
6
6
|
import { sanitizeMemoryText } from './memory-schema.mjs';
|
|
7
7
|
import { resolveHookOperatingProfile } from './operating-profile-runtime.mjs';
|
|
8
|
+
import {
|
|
9
|
+
resolveReadOnlyEvidenceActiveContext,
|
|
10
|
+
scopeEvidenceRows,
|
|
11
|
+
} from './active-context-handoff-evidence.mjs';
|
|
8
12
|
import { isBootstrapPrompt } from '../packages/integrations/src/prompt-content.mjs';
|
|
9
13
|
|
|
10
14
|
export function buildPromptEvidenceContext(vaultBase, prompt, {
|
|
11
|
-
topK = 3, maxBytes = 3072, rows = null,
|
|
15
|
+
topK = 3, maxBytes = 3072, rows = null, activeContext = null, registry = null,
|
|
12
16
|
} = {}) {
|
|
13
17
|
const query = sanitizeMemoryText(String(prompt || '')).trim();
|
|
14
18
|
if (!query || isBootstrapPrompt(query)) return '';
|
|
15
19
|
const evidence = rows || loadEvidenceIndex(vaultBase);
|
|
16
|
-
|
|
20
|
+
const scoped = scopeEvidenceRows(evidence, {
|
|
21
|
+
activeContext,
|
|
22
|
+
registry: registry || readSessionRegistry(vaultBase),
|
|
23
|
+
});
|
|
24
|
+
if (!scoped.length) return '';
|
|
17
25
|
return sanitizeMemoryText(renderEvidenceContext(
|
|
18
|
-
recallEvidence(
|
|
26
|
+
recallEvidence(scoped, query, { topK }),
|
|
19
27
|
{ maxBytes },
|
|
20
28
|
));
|
|
21
29
|
}
|
|
@@ -23,13 +31,20 @@ export function buildPromptEvidenceContext(vaultBase, prompt, {
|
|
|
23
31
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
24
32
|
try {
|
|
25
33
|
const input = readHookInput();
|
|
26
|
-
const runtime = resolveHookOperatingProfile({
|
|
34
|
+
const runtime = resolveHookOperatingProfile({
|
|
35
|
+
input,
|
|
36
|
+
activeContextResolver: resolveReadOnlyEvidenceActiveContext,
|
|
37
|
+
});
|
|
27
38
|
if (runtime.bindingError) {
|
|
28
39
|
writeHookOutput({});
|
|
29
40
|
} else {
|
|
30
41
|
const context = buildPromptEvidenceContext(
|
|
31
42
|
runtime.vaultBase,
|
|
32
43
|
input.prompt || input.user_prompt || '',
|
|
44
|
+
{
|
|
45
|
+
activeContext: runtime.activeContext,
|
|
46
|
+
registry: readSessionRegistry(runtime.vaultBase),
|
|
47
|
+
},
|
|
33
48
|
);
|
|
34
49
|
writeHookOutput(context ? {
|
|
35
50
|
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context },
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
import { findProjectBinding, resolveProjectVault } from '../src/project-vault.mjs';
|
|
12
12
|
import { readSessionRegistry } from './obsidian-common.mjs';
|
|
13
13
|
import { resolveSessionIdentity } from './session-identity.mjs';
|
|
14
|
+
import { resolveCommandActiveContext } from '../src/active-context-runtime.mjs';
|
|
15
|
+
import { sessionTaskOperatingProfile } from './operating-profile-task-store.mjs';
|
|
14
16
|
|
|
15
17
|
function bindingDiagnostic(error, configPath = '') {
|
|
16
18
|
return {
|
|
@@ -131,6 +133,7 @@ export function resolveHookOperatingProfile({
|
|
|
131
133
|
resolution = null,
|
|
132
134
|
config = null,
|
|
133
135
|
provider,
|
|
136
|
+
activeContextResolver = resolveCommandActiveContext,
|
|
134
137
|
} = {}) {
|
|
135
138
|
const initialResolution = resolution || resolveProjectVault({ input });
|
|
136
139
|
const matched = matchingProjectBinding(initialResolution, input);
|
|
@@ -152,9 +155,26 @@ export function resolveHookOperatingProfile({
|
|
|
152
155
|
const entry = identity.state === 'resolved'
|
|
153
156
|
? readSessionRegistry(vaultResolution.base).sessions?.[identity.canonicalConversationId] || null
|
|
154
157
|
: null;
|
|
158
|
+
let activeContext = null;
|
|
159
|
+
let contextError = null;
|
|
160
|
+
if (!bindingError && identity.state === 'resolved') {
|
|
161
|
+
try {
|
|
162
|
+
activeContext = activeContextResolver({
|
|
163
|
+
vaultBase: vaultResolution.base,
|
|
164
|
+
projectRoot: input?.cwd || vaultResolution.projectRoot || process.cwd(),
|
|
165
|
+
sessionId: identity.canonicalConversationId,
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
contextError = bindingDiagnostic(error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
155
171
|
const base = sessionOverride(entry) || project;
|
|
156
172
|
const taskLease = evaluateTaskOperatingProfileLease(
|
|
157
|
-
|
|
173
|
+
contextError ? null : sessionTaskOperatingProfile(
|
|
174
|
+
vaultResolution.base,
|
|
175
|
+
identity.canonicalConversationId || '',
|
|
176
|
+
{ context: activeContext },
|
|
177
|
+
),
|
|
158
178
|
taskLeaseContext(entry, input, identity.canonicalConversationId || ''),
|
|
159
179
|
);
|
|
160
180
|
const selected = taskLease.state === 'active'
|
|
@@ -176,6 +196,8 @@ export function resolveHookOperatingProfile({
|
|
|
176
196
|
baseProfile: base.profile,
|
|
177
197
|
baseSource: base.source,
|
|
178
198
|
taskLease,
|
|
199
|
+
activeContext,
|
|
200
|
+
...(contextError ? { contextError } : {}),
|
|
179
201
|
resolution: vaultResolution,
|
|
180
202
|
...(bindingError ? { bindingError } : {}),
|
|
181
203
|
};
|
|
@@ -3,7 +3,8 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import {
|
|
4
4
|
createTaskOperatingProfileLease,
|
|
5
5
|
} from '../src/operating-profile.mjs';
|
|
6
|
-
import { mutateSessionRegistry } from './obsidian-common.mjs';
|
|
6
|
+
import { mutateSessionRegistry, readSessionRegistry } from './obsidian-common.mjs';
|
|
7
|
+
import { mutateActiveContext, resolveActiveContext } from './active-context-store.mjs';
|
|
7
8
|
|
|
8
9
|
function isoTimestamp(now) {
|
|
9
10
|
if (typeof now === 'string') return now;
|
|
@@ -17,33 +18,93 @@ function missingSessionError(sessionId) {
|
|
|
17
18
|
return error;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function activeContextRequiredError() {
|
|
22
|
+
const error = new Error('active context causal é obrigatório quando o registry contextual está inicializado');
|
|
23
|
+
error.code = 'WENDKEEP_ACTIVE_CONTEXT_REQUIRED';
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hasActiveContextRegistry(registry) {
|
|
28
|
+
return Boolean(registry && (
|
|
29
|
+
Object.hasOwn(registry, 'active_contexts')
|
|
30
|
+
|| Object.hasOwn(registry, 'active_contexts_schema')
|
|
31
|
+
|| Object.hasOwn(registry, 'active_contexts_revision')
|
|
32
|
+
));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function leaseFromSession(current, profile, { reason, leaseId, issuedAt, sessionId }) {
|
|
36
|
+
const turnId = typeof current.last_prompt_turn_id === 'string'
|
|
37
|
+
? current.last_prompt_turn_id.trim()
|
|
38
|
+
: '';
|
|
39
|
+
const hasRegisteredTurn = Boolean(
|
|
40
|
+
turnId
|
|
41
|
+
&& current.turn_sequences
|
|
42
|
+
&& Object.hasOwn(current.turn_sequences, turnId)
|
|
43
|
+
&& current.turn_sequences[turnId] === current.last_turn_sequence
|
|
44
|
+
);
|
|
45
|
+
return createTaskOperatingProfileLease({
|
|
46
|
+
profile,
|
|
47
|
+
reason,
|
|
48
|
+
sessionId,
|
|
49
|
+
turnId,
|
|
50
|
+
turnSequence: hasRegisteredTurn ? current.last_turn_sequence : undefined,
|
|
51
|
+
leaseId,
|
|
52
|
+
issuedAt,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function sessionTaskOperatingProfile(vaultBase, sessionId, { context = null } = {}) {
|
|
57
|
+
if (context) {
|
|
58
|
+
try { return resolveActiveContext(vaultBase, context).operating_profile_task || null; }
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error?.code === 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND') {
|
|
61
|
+
const registry = readSessionRegistry(vaultBase);
|
|
62
|
+
if (hasActiveContextRegistry(registry)) return null;
|
|
63
|
+
return registry.sessions?.[sessionId]?.operating_profile_task || null;
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const registry = readSessionRegistry(vaultBase);
|
|
69
|
+
if (hasActiveContextRegistry(registry)) return null;
|
|
70
|
+
return registry.sessions?.[sessionId]?.operating_profile_task || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
20
73
|
export function setSessionTaskOperatingProfile(vaultBase, sessionId, profile, {
|
|
21
74
|
reason,
|
|
22
75
|
leaseId = randomUUID(),
|
|
23
76
|
now,
|
|
77
|
+
context = null,
|
|
78
|
+
mutateContext = mutateActiveContext,
|
|
24
79
|
} = {}) {
|
|
25
80
|
const issuedAt = isoTimestamp(now);
|
|
81
|
+
if (context) {
|
|
82
|
+
let lease;
|
|
83
|
+
mutateContext(vaultBase, context, (active) => ({
|
|
84
|
+
...active,
|
|
85
|
+
operating_profile_task: lease,
|
|
86
|
+
state: 'active',
|
|
87
|
+
}), {
|
|
88
|
+
now: issuedAt,
|
|
89
|
+
projectLegacy: false,
|
|
90
|
+
mutateRegistry: (base, mutator) => mutateSessionRegistry(base, (registry) => {
|
|
91
|
+
const sessions = registry.sessions || (registry.sessions = {});
|
|
92
|
+
if (!Object.hasOwn(sessions, sessionId)) throw missingSessionError(sessionId);
|
|
93
|
+
lease = leaseFromSession(sessions[sessionId], profile, {
|
|
94
|
+
reason, leaseId, issuedAt, sessionId,
|
|
95
|
+
});
|
|
96
|
+
return mutator(registry);
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
return lease;
|
|
100
|
+
}
|
|
26
101
|
return mutateSessionRegistry(vaultBase, (registry) => {
|
|
102
|
+
if (hasActiveContextRegistry(registry)) throw activeContextRequiredError();
|
|
27
103
|
const sessions = registry.sessions || (registry.sessions = {});
|
|
28
104
|
if (!Object.hasOwn(sessions, sessionId)) throw missingSessionError(sessionId);
|
|
29
105
|
const current = sessions[sessionId];
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
: '';
|
|
33
|
-
const hasRegisteredTurn = Boolean(
|
|
34
|
-
turnId
|
|
35
|
-
&& current.turn_sequences
|
|
36
|
-
&& Object.hasOwn(current.turn_sequences, turnId)
|
|
37
|
-
&& current.turn_sequences[turnId] === current.last_turn_sequence
|
|
38
|
-
);
|
|
39
|
-
const lease = createTaskOperatingProfileLease({
|
|
40
|
-
profile,
|
|
41
|
-
reason,
|
|
42
|
-
sessionId,
|
|
43
|
-
turnId,
|
|
44
|
-
turnSequence: hasRegisteredTurn ? current.last_turn_sequence : undefined,
|
|
45
|
-
leaseId,
|
|
46
|
-
issuedAt,
|
|
106
|
+
const lease = leaseFromSession(current, profile, {
|
|
107
|
+
reason, leaseId, issuedAt, sessionId,
|
|
47
108
|
});
|
|
48
109
|
sessions[sessionId] = {
|
|
49
110
|
...current,
|
|
@@ -56,10 +117,53 @@ export function setSessionTaskOperatingProfile(vaultBase, sessionId, profile, {
|
|
|
56
117
|
|
|
57
118
|
export function consumeSessionTaskOperatingProfile(vaultBase, sessionId, leaseId, {
|
|
58
119
|
now,
|
|
120
|
+
context = null,
|
|
121
|
+
mutateContext = mutateActiveContext,
|
|
59
122
|
} = {}) {
|
|
60
123
|
if (!sessionId || !leaseId) return false;
|
|
61
124
|
const consumedAt = isoTimestamp(now);
|
|
125
|
+
if (context) {
|
|
126
|
+
let current;
|
|
127
|
+
try { current = resolveActiveContext(vaultBase, context); }
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error?.code !== 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND') throw error;
|
|
130
|
+
const registry = readSessionRegistry(vaultBase);
|
|
131
|
+
if (hasActiveContextRegistry(registry)) return false;
|
|
132
|
+
return consumeSessionTaskOperatingProfile(vaultBase, sessionId, leaseId, {
|
|
133
|
+
now: consumedAt,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const lease = current.operating_profile_task;
|
|
137
|
+
if (!lease || lease.state !== 'active' || lease.lease_id !== leaseId) return false;
|
|
138
|
+
let consumed = false;
|
|
139
|
+
try {
|
|
140
|
+
mutateContext(vaultBase, context, (active) => {
|
|
141
|
+
const activeLease = active.operating_profile_task;
|
|
142
|
+
if (!activeLease || activeLease.state !== 'active' || activeLease.lease_id !== leaseId) {
|
|
143
|
+
return active;
|
|
144
|
+
}
|
|
145
|
+
consumed = true;
|
|
146
|
+
return {
|
|
147
|
+
...active,
|
|
148
|
+
operating_profile_task: {
|
|
149
|
+
...activeLease,
|
|
150
|
+
state: 'consumed',
|
|
151
|
+
consumed_at: consumedAt,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}, {
|
|
155
|
+
expectedRevision: current.revision,
|
|
156
|
+
now: consumedAt,
|
|
157
|
+
projectLegacy: false,
|
|
158
|
+
});
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error?.code === 'WENDKEEP_ACTIVE_CONTEXT_STALE') return false;
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
return consumed;
|
|
164
|
+
}
|
|
62
165
|
return mutateSessionRegistry(vaultBase, (registry) => {
|
|
166
|
+
if (hasActiveContextRegistry(registry)) throw activeContextRequiredError();
|
|
63
167
|
const current = registry.sessions?.[sessionId];
|
|
64
168
|
const lease = current?.operating_profile_task;
|
|
65
169
|
if (!lease || lease.state !== 'active' || lease.lease_id !== leaseId) return false;
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -19,6 +19,11 @@ import { mutateSessionNote } from './session-note-io.mjs';
|
|
|
19
19
|
import { appendIterationOutcome } from './session-iteration-outcome.mjs';
|
|
20
20
|
import { applyDerivedSections, provenanceSessions } from './derived-sections.mjs';
|
|
21
21
|
import { buildSessionMemoryEvents, collectLifecycleEvidence } from './memory-handoff.mjs';
|
|
22
|
+
import {
|
|
23
|
+
buildCausalSharedHandoff,
|
|
24
|
+
causalChangeSlug,
|
|
25
|
+
resolveHandoffEvidenceAuthority,
|
|
26
|
+
} from './active-context-handoff-evidence.mjs';
|
|
22
27
|
import { enqueueMemoryEvent, projectMemoryOutbox } from './memory-store.mjs';
|
|
23
28
|
import { detectMemoryMode } from './memory-mode.mjs';
|
|
24
29
|
import { sanitizeMemoryText } from './memory-schema.mjs';
|
|
@@ -521,21 +526,6 @@ function shouldFinalizeSession() {
|
|
|
521
526
|
return process.env.OBSIDIAN_NO_AUTO_FINALIZE !== '1';
|
|
522
527
|
}
|
|
523
528
|
|
|
524
|
-
function sharedHandoffFromInput(input = {}, entry = {}) {
|
|
525
|
-
const supplied = input.shared || input.handoff?.shared;
|
|
526
|
-
const shared = supplied && typeof supplied === 'object' && !Array.isArray(supplied)
|
|
527
|
-
? { ...supplied }
|
|
528
|
-
: {};
|
|
529
|
-
const workSessionId = shared.work_session_id
|
|
530
|
-
|| shared.workSessionId
|
|
531
|
-
|| input.work_session_id
|
|
532
|
-
|| input.workSessionId
|
|
533
|
-
|| entry?.work_session_id
|
|
534
|
-
|| '';
|
|
535
|
-
if (!shared.work_session_id && workSessionId) shared.work_session_id = workSessionId;
|
|
536
|
-
return Object.keys(shared).length ? shared : null;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
529
|
export function commitSessionMemory(vaultBase, handoff, { projectOptions = {} } = {}) {
|
|
540
530
|
if (detectMemoryMode(vaultBase).mode === 'legacy') {
|
|
541
531
|
return { status: 'legacy', eventCount: 0, eventIds: [], checkpoint: null };
|
|
@@ -1277,6 +1267,29 @@ export async function main({
|
|
|
1277
1267
|
const tx = parseTranscript(identity.transcriptPath || input.transcript_path || input.transcriptPath);
|
|
1278
1268
|
const requestedTurnId = String(input.turn_id || input.turnId || '');
|
|
1279
1269
|
const sessionId = identity.canonicalConversationId;
|
|
1270
|
+
let handoffEvidenceAuthority;
|
|
1271
|
+
let sharedHandoff;
|
|
1272
|
+
let activeContextChangeSlug;
|
|
1273
|
+
try {
|
|
1274
|
+
handoffEvidenceAuthority = resolveHandoffEvidenceAuthority(vaultBase, {
|
|
1275
|
+
input,
|
|
1276
|
+
sessionId,
|
|
1277
|
+
projectRoot: input.cwd || process.cwd(),
|
|
1278
|
+
});
|
|
1279
|
+
sharedHandoff = buildCausalSharedHandoff({
|
|
1280
|
+
input,
|
|
1281
|
+
entry,
|
|
1282
|
+
authority: handoffEvidenceAuthority,
|
|
1283
|
+
});
|
|
1284
|
+
activeContextChangeSlug = causalChangeSlug(entry, handoffEvidenceAuthority);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
const detail = String(error?.message || 'active context causal indisponível');
|
|
1287
|
+
process.stderr.write(`[wendkeep] Stop causal bloqueado: ${detail}\n`);
|
|
1288
|
+
writeHookOutput({
|
|
1289
|
+
systemMessage: `wendkeep: Stop bloqueado antes de publicar handoff/evidência: ${detail}`,
|
|
1290
|
+
});
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1280
1293
|
const finalizing = shouldFinalizeSession();
|
|
1281
1294
|
const turnIdentity = resolveTurnIdentity(tx, requestedTurnId);
|
|
1282
1295
|
if (!turnIdentity) {
|
|
@@ -1389,11 +1402,10 @@ export async function main({
|
|
|
1389
1402
|
}
|
|
1390
1403
|
const finalSummary = sessionFinalSummary(tx);
|
|
1391
1404
|
const memoryEvidence = collectLifecycleEvidence(vaultBase, {
|
|
1392
|
-
changeSlug:
|
|
1405
|
+
changeSlug: activeContextChangeSlug,
|
|
1393
1406
|
summary: finalSummary,
|
|
1394
1407
|
noteRel: sessionRel,
|
|
1395
1408
|
});
|
|
1396
|
-
const sharedHandoff = sharedHandoffFromInput(input, entry);
|
|
1397
1409
|
memoryHandoff = {
|
|
1398
1410
|
projectId,
|
|
1399
1411
|
identity,
|
|
@@ -1531,9 +1543,11 @@ export async function main({
|
|
|
1531
1543
|
// grafo quando a change fechava antes do turno seguinte. Aqui sobrevive ao reopen e acumula toda
|
|
1532
1544
|
// change que passou pela sessão (upsertListSection deduplica). Fail-quiet: nunca derruba o Stop.
|
|
1533
1545
|
try {
|
|
1534
|
-
const chgLink =
|
|
1535
|
-
? `Change ativa: [[${getLocale(vaultBase).folders.changes}/${
|
|
1536
|
-
:
|
|
1546
|
+
const chgLink = activeContextChangeSlug
|
|
1547
|
+
? `Change ativa: [[${getLocale(vaultBase).folders.changes}/${activeContextChangeSlug}/proposta]]`
|
|
1548
|
+
: handoffEvidenceAuthority.mode === 'legacy'
|
|
1549
|
+
? activeChangeLink(vaultBase)
|
|
1550
|
+
: '';
|
|
1537
1551
|
const wl = (chgLink.match(/\[\[[^\]]+\]\]/) || [])[0];
|
|
1538
1552
|
if (wl) {
|
|
1539
1553
|
mutateSessionNote(sessionPath, (cur) => (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.76.
|
|
3
|
+
"version": "0.76.7",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"precheck": "node --check src/worktree.mjs && node --check src/context.mjs && node --check packages/vault/src/worktree-metadata.mjs",
|
|
45
|
-
"check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-scope.mjs && node --check packages/vault/src/memory-candidate-policy.mjs && node --check packages/vault/src/evidence-recall.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
45
|
+
"check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/active-context-handoff-evidence.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-scope.mjs && node --check packages/vault/src/memory-candidate-policy.mjs && node --check packages/vault/src/evidence-recall.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
46
46
|
"test": "node --test --test-concurrency=2",
|
|
47
47
|
"test:core": "node scripts/run-scope.mjs core",
|
|
48
48
|
"release": "node scripts/release.mjs",
|
package/src/profile.mjs
CHANGED
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
resolveOperatingProfile,
|
|
9
9
|
setOperatingProfile,
|
|
10
10
|
} from './operating-profile.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
sessionTaskOperatingProfile,
|
|
13
|
+
setSessionTaskOperatingProfile,
|
|
14
|
+
} from '../hooks/operating-profile-task-store.mjs';
|
|
15
|
+
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
12
16
|
import { resolve } from 'node:path';
|
|
13
17
|
import { findProjectBinding, resolveProjectVault, updateProjectBinding } from './project-vault.mjs';
|
|
14
18
|
|
|
@@ -163,12 +167,13 @@ function sessionProfile(vaultBase, sessionId, projectResolved) {
|
|
|
163
167
|
return sessionBaseProfile(sessions[sessionId], projectResolved);
|
|
164
168
|
}
|
|
165
169
|
|
|
166
|
-
function sessionProfileStatus(vaultBase, sessionId, projectResolved) {
|
|
170
|
+
function sessionProfileStatus(vaultBase, sessionId, projectResolved, { context = null } = {}) {
|
|
167
171
|
const sessions = readSessionRegistry(vaultBase).sessions || {};
|
|
168
172
|
if (!Object.hasOwn(sessions, sessionId)) throw new Error(`sessão não encontrada: ${sessionId}`);
|
|
169
173
|
const entry = sessions[sessionId];
|
|
170
174
|
const base = sessionBaseProfile(entry, projectResolved);
|
|
171
|
-
const taskLease = evaluateTaskOperatingProfileLease(
|
|
175
|
+
const taskLease = evaluateTaskOperatingProfileLease(
|
|
176
|
+
sessionTaskOperatingProfile(vaultBase, sessionId, { context }), {
|
|
172
177
|
sessionId,
|
|
173
178
|
turnId: entry.last_prompt_turn_id || '',
|
|
174
179
|
turnSequence: entry.last_turn_sequence,
|
|
@@ -183,6 +188,15 @@ function sessionProfileStatus(vaultBase, sessionId, projectResolved) {
|
|
|
183
188
|
};
|
|
184
189
|
}
|
|
185
190
|
|
|
191
|
+
function commandActiveContext(state, sessionId) {
|
|
192
|
+
if (!sessionId || state.resolved.bindingError) return null;
|
|
193
|
+
return resolveCommandActiveContext({
|
|
194
|
+
vaultBase: state.vaultBase,
|
|
195
|
+
projectRoot: state.resolved.projectRoot || process.cwd(),
|
|
196
|
+
sessionId,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
186
200
|
function sessionOutputPayload(effective, sessionId) {
|
|
187
201
|
return {
|
|
188
202
|
profile: effective.profile,
|
|
@@ -213,8 +227,9 @@ export function runProfile(argv = []) {
|
|
|
213
227
|
if (sub === 'status' || sub === 'show') {
|
|
214
228
|
if (args.length > 1) return fail(`${sub} não aceita argumentos posicionais adicionais`);
|
|
215
229
|
try {
|
|
230
|
+
const activeContext = sessionId ? commandActiveContext(state, sessionId) : null;
|
|
216
231
|
const effective = sessionId
|
|
217
|
-
? sessionProfileStatus(state.vaultBase, sessionId, projectResolved)
|
|
232
|
+
? sessionProfileStatus(state.vaultBase, sessionId, projectResolved, { context: activeContext })
|
|
218
233
|
: { profile: projectResolved.profile, source: projectResolved.source, scope: 'project' };
|
|
219
234
|
output({
|
|
220
235
|
...(sessionId ? sessionOutputPayload(effective, sessionId) : {
|
|
@@ -235,11 +250,12 @@ export function runProfile(argv = []) {
|
|
|
235
250
|
if (!reason) return fail('route requer --reason <text>');
|
|
236
251
|
try {
|
|
237
252
|
const base = sessionProfile(state.vaultBase, sessionId, projectResolved);
|
|
253
|
+
const activeContext = commandActiveContext(state, sessionId);
|
|
238
254
|
const lease = setSessionTaskOperatingProfile(
|
|
239
255
|
state.vaultBase,
|
|
240
256
|
sessionId,
|
|
241
257
|
args[1],
|
|
242
|
-
{ reason },
|
|
258
|
+
{ reason, context: activeContext },
|
|
243
259
|
);
|
|
244
260
|
output({
|
|
245
261
|
profile: lease.profile,
|