wendkeep 0.76.4 → 0.76.6
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 +22 -0
- package/README.en.md +13 -3
- package/README.md +13 -3
- package/docs/en/commands/operating-profiles.md +19 -4
- package/docs/pt-BR/commands/operating-profiles.md +19 -4
- package/hooks/active-context-store.mjs +43 -1
- package/hooks/change-context.mjs +11 -2
- package/hooks/change-nag.mjs +7 -3
- package/hooks/change-warn.mjs +12 -3
- package/hooks/operating-profile-runtime.mjs +22 -1
- package/hooks/operating-profile-task-store.mjs +122 -18
- package/package.json +1 -1
- package/src/delivery.mjs +134 -22
- package/src/profile.mjs +21 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ 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.6] — 2026-08-22
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Task lease causal.** `operating_profile_task` passa a pertencer ao `active_contexts` da
|
|
12
|
+
worktree/work session; rotas temporárias irmãs não se sobrescrevem nem ampliam autorização.
|
|
13
|
+
- **CLI e hooks escopados.** `profile route/status`, os hooks de operating profile e o consumo no
|
|
14
|
+
Stop resolvem a lease do chamador e preservam o sibling byte a byte por revision/CAS.
|
|
15
|
+
- **Fallback legado fail-closed.** Vault sem registry contextual conserva a lease na sessão;
|
|
16
|
+
depois da inicialização, uma lease global não é copiada nem aplicada sem identidade provada.
|
|
17
|
+
|
|
18
|
+
## [0.76.5] — 2026-08-22
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Delivery causal.** `delivery_id` passa a pertencer ao `active_contexts` da worktree/work session;
|
|
23
|
+
start, status, finish e abandon isolam deliveries concorrentes e rejeitam IDs de outro contexto.
|
|
24
|
+
- **Hooks escopados.** `change-context` e `change-warn` só reconhecem a autorização de delivery do
|
|
25
|
+
chamador causal, sem herdar permissões de sessões irmãs.
|
|
26
|
+
- **Compatibilidade conservadora.** `CURRENT_DELIVERY` vira projeção somente para um contexto
|
|
27
|
+
inequívoco; falha de bind remove o estado recém-criado sem publicar ponteiro parcial.
|
|
28
|
+
|
|
7
29
|
## [0.76.4] — 2026-08-22
|
|
8
30
|
|
|
9
31
|
### Added
|
package/README.en.md
CHANGED
|
@@ -338,10 +338,20 @@ Work kind, profile, contract impact, and operational risk are independent dimens
|
|
|
338
338
|
spec, or ADR. If delivery requires a code/config edit, it pauses and work returns to
|
|
339
339
|
`implementation`:
|
|
340
340
|
|
|
341
|
+
In multi-context Vaults, `active_contexts[].delivery_id` is authoritative and `--session <id>`
|
|
342
|
+
selects the caller explicitly. `CURRENT_DELIVERY` is only a derived projection when there is one
|
|
343
|
+
single, unambiguous context; hooks consult only the causal delivery, and an ID from another context
|
|
344
|
+
fails with `WENDKEEP_DELIVERY_CONTEXT_MISMATCH`.
|
|
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
|
+
|
|
341
351
|
```bash
|
|
342
|
-
npx wendkeep delivery start release-0-74-0 --allow git:merge --allow git:push --allow publish --source-change <slug> --source-commit <sha>
|
|
343
|
-
npx wendkeep delivery status release-0-74-0
|
|
344
|
-
npx wendkeep delivery finish release-0-74-0 --target main --ci-url <url> --version 0.74.0 --npm-integrity <sha512> --release-url <url>
|
|
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>
|
|
353
|
+
npx wendkeep delivery status release-0-74-0 --session <id>
|
|
354
|
+
npx wendkeep delivery finish release-0-74-0 --target main --ci-url <url> --version 0.74.0 --npm-integrity <sha512> --release-url <url> --session <id>
|
|
345
355
|
```
|
|
346
356
|
|
|
347
357
|
If the harness does not record a lease, a small fix remains under the configured profile —
|
package/README.md
CHANGED
|
@@ -338,10 +338,20 @@ Work kind, profile, contract impact, and operational risk are independent dimens
|
|
|
338
338
|
spec, or ADR. If delivery requires a code/config edit, it pauses and work returns to
|
|
339
339
|
`implementation`:
|
|
340
340
|
|
|
341
|
+
In multi-context Vaults, `active_contexts[].delivery_id` is authoritative and `--session <id>`
|
|
342
|
+
selects the caller explicitly. `CURRENT_DELIVERY` is only a derived projection when there is one
|
|
343
|
+
single, unambiguous context; hooks consult only the causal delivery, and an ID from another context
|
|
344
|
+
fails with `WENDKEEP_DELIVERY_CONTEXT_MISMATCH`.
|
|
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
|
+
|
|
341
351
|
```bash
|
|
342
|
-
npx wendkeep delivery start release-0-74-0 --allow git:merge --allow git:push --allow publish --source-change <slug> --source-commit <sha>
|
|
343
|
-
npx wendkeep delivery status release-0-74-0
|
|
344
|
-
npx wendkeep delivery finish release-0-74-0 --target main --ci-url <url> --version 0.74.0 --npm-integrity <sha512> --release-url <url>
|
|
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>
|
|
353
|
+
npx wendkeep delivery status release-0-74-0 --session <id>
|
|
354
|
+
npx wendkeep delivery finish release-0-74-0 --target main --ci-url <url> --version 0.74.0 --npm-integrity <sha512> --release-url <url> --session <id>
|
|
345
355
|
```
|
|
346
356
|
|
|
347
357
|
If the harness does not record a lease, a small fix remains under the configured profile —
|
|
@@ -57,10 +57,10 @@ npx wendkeep flow status [<id>]
|
|
|
57
57
|
npx wendkeep flow show <id> [--session <id>]
|
|
58
58
|
npx wendkeep flow finish <id> [--session <id>]
|
|
59
59
|
npx wendkeep flow promote <id> [--change-slug <slug>] [--session <id>]
|
|
60
|
-
npx wendkeep delivery start [id] --allow <capability> [--source-change <slug>] [--source-commit <sha>]
|
|
61
|
-
npx wendkeep delivery status [id]
|
|
62
|
-
npx wendkeep delivery finish [id] [--target <ref>] [--ci-url <url>] [--version <x.y.z>] [--npm-integrity <sha512>] [--release-url <url>]
|
|
63
|
-
npx wendkeep delivery abandon [id] --reason <text>
|
|
60
|
+
npx wendkeep delivery start [id] --allow <capability> [--source-change <slug>] [--source-commit <sha>] [--session <id>]
|
|
61
|
+
npx wendkeep delivery status [id] [--session <id>]
|
|
62
|
+
npx wendkeep delivery finish [id] [--target <ref>] [--ci-url <url>] [--version <x.y.z>] [--npm-integrity <sha512>] [--release-url <url>] [--session <id>]
|
|
63
|
+
npx wendkeep delivery abandon [id] --reason <text> [--session <id>]
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Every FLOW subcommand also accepts `--project <path>`, `--vault <path>`, and `--json`. When
|
|
@@ -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
|
|
@@ -178,6 +184,15 @@ ownership to the native LLM harness.
|
|
|
178
184
|
and for `publish` requires CI, version, npm integrity, and GitHub Release evidence. Receipts are
|
|
179
185
|
append-only in `.brain/runtime/delivery-receipts.jsonl`. If code/config must change, delivery
|
|
180
186
|
stops with `WENDKEEP_DELIVERY_IMPLEMENTATION_REQUIRED` and work returns to implementation.
|
|
187
|
+
- In a multi-context Vault, `active_contexts[].delivery_id` is authoritative. `--session <id>`
|
|
188
|
+
selects the work session explicitly; without it, only one unambiguous active context for the
|
|
189
|
+
worktree may be used. Implicit status, finish, and abandon resolve that binding, not a global pointer.
|
|
190
|
+
- `CURRENT_DELIVERY` is only a derived projection: it contains the ID for one single, unambiguous
|
|
191
|
+
active context with delivery and stays empty with zero or multiple contexts.
|
|
192
|
+
`WENDKEEP_DELIVERY_CONTEXT_MISMATCH` means the explicit ID belongs to another context; neither
|
|
193
|
+
context is mutated.
|
|
194
|
+
- The `change-context` and `change-warn` hooks consult only the caller's causal delivery; one
|
|
195
|
+
session's authorization is neither injected into nor allowed to suppress warnings in another.
|
|
181
196
|
- Exit `0` means a successful query or transition; exit `1` means a policy/red-sensor block; exit
|
|
182
197
|
`2` means invalid profile, session, flow, or arguments, with no partial mutation.
|
|
183
198
|
|
|
@@ -57,10 +57,10 @@ npx wendkeep flow status [<id>]
|
|
|
57
57
|
npx wendkeep flow show <id> [--session <id>]
|
|
58
58
|
npx wendkeep flow finish <id> [--session <id>]
|
|
59
59
|
npx wendkeep flow promote <id> [--change-slug <slug>] [--session <id>]
|
|
60
|
-
npx wendkeep delivery start [id] --allow <capability> [--source-change <slug>] [--source-commit <sha>]
|
|
61
|
-
npx wendkeep delivery status [id]
|
|
62
|
-
npx wendkeep delivery finish [id] [--target <ref>] [--ci-url <url>] [--version <x.y.z>] [--npm-integrity <sha512>] [--release-url <url>]
|
|
63
|
-
npx wendkeep delivery abandon [id] --reason <texto>
|
|
60
|
+
npx wendkeep delivery start [id] --allow <capability> [--source-change <slug>] [--source-commit <sha>] [--session <id>]
|
|
61
|
+
npx wendkeep delivery status [id] [--session <id>]
|
|
62
|
+
npx wendkeep delivery finish [id] [--target <ref>] [--ci-url <url>] [--version <x.y.z>] [--npm-integrity <sha512>] [--release-url <url>] [--session <id>]
|
|
63
|
+
npx wendkeep delivery abandon [id] --reason <texto> [--session <id>]
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Todos os subcomandos FLOW também aceitam `--project <path>`, `--vault <path>` e `--json`.
|
|
@@ -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
|
|
@@ -177,6 +183,15 @@ harness nativo da LLM.
|
|
|
177
183
|
para capability `publish`, exige CI, versão, integridade npm e GitHub Release. O receipt é
|
|
178
184
|
append-only em `.brain/runtime/delivery-receipts.jsonl`. Se código/config precisar mudar, a
|
|
179
185
|
delivery para com `WENDKEEP_DELIVERY_IMPLEMENTATION_REQUIRED` e o trabalho volta a implementation.
|
|
186
|
+
- Em Vault multi-contexto, `active_contexts[].delivery_id` é a autoridade. `--session <id>` escolhe
|
|
187
|
+
explicitamente a work session; sem ela, somente um active context inequívoco da worktree pode ser
|
|
188
|
+
usado. Status, finish e abandon implícitos consultam esse binding, não um ponteiro global.
|
|
189
|
+
- `CURRENT_DELIVERY` é somente uma projeção derivada: contém o ID quando existe um único contexto
|
|
190
|
+
ativo inequívoco com delivery e fica vazio com zero ou múltiplos contextos.
|
|
191
|
+
`WENDKEEP_DELIVERY_CONTEXT_MISMATCH` indica que o ID explícito pertence a outro contexto; nenhum
|
|
192
|
+
dos dois é mutado.
|
|
193
|
+
- Os hooks `change-context` e `change-warn` consultam somente o delivery causal do chamador; a
|
|
194
|
+
autorização de uma sessão não é injetada nem suprime aviso em outra.
|
|
180
195
|
- Exit `0` indica consulta ou transição concluída; exit `1` indica política/sensor vermelho; exit
|
|
181
196
|
`2` indica perfil, sessão, flow ou argumentos inválidos, sem mutação parcial.
|
|
182
197
|
|
|
@@ -6,8 +6,10 @@ import { mkdirVaultPath, writeVaultFileSync } from './vault-path-safety.mjs';
|
|
|
6
6
|
|
|
7
7
|
export const ACTIVE_CONTEXTS_SCHEMA_VERSION = 1;
|
|
8
8
|
const POINTER = '.brain/CURRENT_CHANGE.md';
|
|
9
|
+
const DELIVERY_POINTER = '.brain/runtime/CURRENT_DELIVERY';
|
|
9
10
|
const ID_PATTERN = /^[A-Za-z0-9._-]{1,160}$/;
|
|
10
11
|
const SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$/;
|
|
12
|
+
const DELIVERY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,100}$/;
|
|
11
13
|
|
|
12
14
|
function contextError(code, message) {
|
|
13
15
|
const error = new Error(message);
|
|
@@ -86,6 +88,22 @@ export function projectLegacyActiveChange(vaultBase, registry = readSessionRegis
|
|
|
86
88
|
return slug;
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
export function projectLegacyActiveDelivery(vaultBase, registry = readSessionRegistry(vaultBase)) {
|
|
92
|
+
const active = activeContexts(registry);
|
|
93
|
+
const id = active.length === 1 ? optionalText(active[0][1]?.delivery_id, 101) : '';
|
|
94
|
+
mkdirVaultPath(vaultBase, join(vaultBase, '.brain', 'runtime'), {
|
|
95
|
+
label: 'runtime da projeção legada de delivery',
|
|
96
|
+
});
|
|
97
|
+
writeVaultFileSync(
|
|
98
|
+
vaultBase,
|
|
99
|
+
join(vaultBase, DELIVERY_POINTER),
|
|
100
|
+
id ? `${id}\n` : '',
|
|
101
|
+
'utf8',
|
|
102
|
+
{ label: 'projeção legada CURRENT_DELIVERY' },
|
|
103
|
+
);
|
|
104
|
+
return id;
|
|
105
|
+
}
|
|
106
|
+
|
|
89
107
|
export function resolveActiveContext(vaultBase, query = {}) {
|
|
90
108
|
const normalized = normalizeIdentity(query, { requireWorkSession: false });
|
|
91
109
|
const registry = readSessionRegistry(vaultBase);
|
|
@@ -121,6 +139,7 @@ export function mutateActiveContext(vaultBase, identity, updater, {
|
|
|
121
139
|
expectedRevision,
|
|
122
140
|
now = new Date().toISOString(),
|
|
123
141
|
mutateRegistry = mutateSessionRegistry,
|
|
142
|
+
projectLegacy = true,
|
|
124
143
|
} = {}) {
|
|
125
144
|
const normalized = normalizeIdentity(identity);
|
|
126
145
|
const key = activeContextKey(normalized);
|
|
@@ -173,7 +192,10 @@ export function mutateActiveContext(vaultBase, identity, updater, {
|
|
|
173
192
|
registry.active_contexts = { ...contexts, [key]: next };
|
|
174
193
|
return { key, context: structuredClone(next), registryRevision: registry.active_contexts_revision };
|
|
175
194
|
});
|
|
176
|
-
|
|
195
|
+
if (projectLegacy) {
|
|
196
|
+
projectLegacyActiveChange(vaultBase);
|
|
197
|
+
projectLegacyActiveDelivery(vaultBase);
|
|
198
|
+
}
|
|
177
199
|
return result;
|
|
178
200
|
}
|
|
179
201
|
|
|
@@ -197,6 +219,26 @@ export function clearActiveContextChange(vaultBase, identity, options = {}) {
|
|
|
197
219
|
}), options);
|
|
198
220
|
}
|
|
199
221
|
|
|
222
|
+
export function setActiveContextDelivery(vaultBase, identity, deliveryId, options = {}) {
|
|
223
|
+
const normalizedId = String(deliveryId || '').trim();
|
|
224
|
+
if (!DELIVERY_PATTERN.test(normalizedId)) {
|
|
225
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_DELIVERY_INVALID', 'delivery_id inválido ou ausente');
|
|
226
|
+
}
|
|
227
|
+
return mutateActiveContext(vaultBase, identity, (context) => ({
|
|
228
|
+
...context,
|
|
229
|
+
delivery_id: normalizedId,
|
|
230
|
+
state: 'active',
|
|
231
|
+
}), options);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function clearActiveContextDelivery(vaultBase, identity, options = {}) {
|
|
235
|
+
return mutateActiveContext(vaultBase, identity, (context) => ({
|
|
236
|
+
...context,
|
|
237
|
+
delivery_id: '',
|
|
238
|
+
state: 'active',
|
|
239
|
+
}), options);
|
|
240
|
+
}
|
|
241
|
+
|
|
200
242
|
function legacyPointer(vaultBase) {
|
|
201
243
|
try {
|
|
202
244
|
return readFileSync(join(vaultBase, POINTER), 'utf8').match(/^change:\s*(.+)$/m)?.[1]?.trim() || '';
|
package/hooks/change-context.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
resolveHookOperatingProfile,
|
|
16
16
|
} from './operating-profile-runtime.mjs';
|
|
17
17
|
import { activeDelivery } from '../src/delivery.mjs';
|
|
18
|
+
import { resolveCommandActiveContext } from '../src/active-context-runtime.mjs';
|
|
18
19
|
|
|
19
20
|
// Conservador de propósito: verbos de tarefa comuns (pt+en) + tamanho mínimo. Falso-negativo
|
|
20
21
|
// custa só o nudge; falso-positivo em pergunta curta viraria ruído.
|
|
@@ -26,11 +27,13 @@ export function looksLikeTask(prompt) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
// Retorna { context, hash? } quando há algo a injetar; null = silêncio.
|
|
29
|
-
export function buildChangePing(vaultBase, sessionId, prompt = '', changeSlug = '', {
|
|
30
|
+
export function buildChangePing(vaultBase, sessionId, prompt = '', changeSlug = '', {
|
|
31
|
+
profile = 'GOVERN', context = null,
|
|
32
|
+
} = {}) {
|
|
30
33
|
const policy = hookProfilePolicy(profile);
|
|
31
34
|
if (!policy.harness) return null;
|
|
32
35
|
const sentinelId = profileSentinelId(sessionId, profile);
|
|
33
|
-
const delivery = activeDelivery(vaultBase);
|
|
36
|
+
const delivery = activeDelivery(vaultBase, { context });
|
|
34
37
|
if (delivery) {
|
|
35
38
|
const hash = `delivery:${delivery.id}`;
|
|
36
39
|
if (readSentinel(vaultBase, 'ctx', sentinelId) === hash) return null;
|
|
@@ -68,10 +71,16 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
68
71
|
const vaultBase = runtime.vaultBase;
|
|
69
72
|
const { identity, entry } = runtime;
|
|
70
73
|
const sid = identity.state === 'resolved' ? identity.canonicalConversationId : (input.session_id || input.sessionId || '');
|
|
74
|
+
const commandContext = runtime.bindingError ? null : resolveCommandActiveContext({
|
|
75
|
+
vaultBase,
|
|
76
|
+
projectRoot: input.cwd || process.cwd(),
|
|
77
|
+
sessionId: sid,
|
|
78
|
+
});
|
|
71
79
|
const ping = runtime.bindingError
|
|
72
80
|
? { context: profileRuntimeError(runtime.bindingError) }
|
|
73
81
|
: buildChangePing(vaultBase, sid, input.prompt || '', entry?.change_slug || '', {
|
|
74
82
|
profile: runtime.profile,
|
|
83
|
+
context: commandContext,
|
|
75
84
|
});
|
|
76
85
|
if (!ping) { writeHookOutput({}); }
|
|
77
86
|
else writeHookOutput({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: ping.context } });
|
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 || {});
|
package/hooks/change-warn.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
resolveHookOperatingProfile,
|
|
14
14
|
} from './operating-profile-runtime.mjs';
|
|
15
15
|
import { activeDelivery } from '../src/delivery.mjs';
|
|
16
|
+
import { resolveCommandActiveContext } from '../src/active-context-runtime.mjs';
|
|
16
17
|
|
|
17
18
|
const CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|prisma|sql|go|rs|java|cs)$/i;
|
|
18
19
|
|
|
@@ -28,12 +29,13 @@ export function warnDecision(filePath, {
|
|
|
28
29
|
cwd = '.',
|
|
29
30
|
sessionId = '',
|
|
30
31
|
profile = 'GOVERN',
|
|
32
|
+
context = null,
|
|
31
33
|
} = {}) {
|
|
32
34
|
const policy = hookProfilePolicy(profile);
|
|
33
35
|
if (!policy.requiresChange) return null;
|
|
34
36
|
if (!filePath || !isCodeFile(filePath)) return null;
|
|
35
|
-
if (activeChange(vaultBase)) return null;
|
|
36
|
-
if (activeDelivery(vaultBase)) return null;
|
|
37
|
+
if (activeChange(vaultBase, { context })) return null;
|
|
38
|
+
if (activeDelivery(vaultBase, { context })) return null;
|
|
37
39
|
const abs = norm(isAbsolute(filePath) ? filePath : resolve(cwd, filePath));
|
|
38
40
|
// Dentro do vault (NTFS é case-insensitive) ou em dirs de config de agente: não é código do projeto.
|
|
39
41
|
if (abs.toLowerCase().startsWith(`${norm(resolve(vaultBase)).toLowerCase()}/`)) return null;
|
|
@@ -52,13 +54,20 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
52
54
|
try {
|
|
53
55
|
const input = readHookInput();
|
|
54
56
|
const runtime = resolveHookOperatingProfile({ input });
|
|
57
|
+
const sid = input.session_id || input.sessionId || '';
|
|
58
|
+
const commandContext = runtime.bindingError ? null : resolveCommandActiveContext({
|
|
59
|
+
vaultBase: runtime.vaultBase,
|
|
60
|
+
projectRoot: input.cwd || process.cwd(),
|
|
61
|
+
sessionId: sid,
|
|
62
|
+
});
|
|
55
63
|
const warn = runtime.bindingError
|
|
56
64
|
? profileRuntimeError(runtime.bindingError)
|
|
57
65
|
: warnDecision(input.tool_input?.file_path, {
|
|
58
66
|
vaultBase: runtime.vaultBase,
|
|
59
67
|
cwd: input.cwd || '.',
|
|
60
|
-
sessionId:
|
|
68
|
+
sessionId: sid,
|
|
61
69
|
profile: runtime.profile,
|
|
70
|
+
context: commandContext,
|
|
62
71
|
});
|
|
63
72
|
if (!warn) { writeHookOutput({}); }
|
|
64
73
|
else writeHookOutput({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: warn } });
|
|
@@ -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 {
|
|
@@ -152,9 +154,26 @@ export function resolveHookOperatingProfile({
|
|
|
152
154
|
const entry = identity.state === 'resolved'
|
|
153
155
|
? readSessionRegistry(vaultResolution.base).sessions?.[identity.canonicalConversationId] || null
|
|
154
156
|
: null;
|
|
157
|
+
let activeContext = null;
|
|
158
|
+
let contextError = null;
|
|
159
|
+
if (!bindingError && identity.state === 'resolved') {
|
|
160
|
+
try {
|
|
161
|
+
activeContext = resolveCommandActiveContext({
|
|
162
|
+
vaultBase: vaultResolution.base,
|
|
163
|
+
projectRoot: input?.cwd || vaultResolution.projectRoot || process.cwd(),
|
|
164
|
+
sessionId: identity.canonicalConversationId,
|
|
165
|
+
});
|
|
166
|
+
} catch (error) {
|
|
167
|
+
contextError = bindingDiagnostic(error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
155
170
|
const base = sessionOverride(entry) || project;
|
|
156
171
|
const taskLease = evaluateTaskOperatingProfileLease(
|
|
157
|
-
|
|
172
|
+
contextError ? null : sessionTaskOperatingProfile(
|
|
173
|
+
vaultResolution.base,
|
|
174
|
+
identity.canonicalConversationId || '',
|
|
175
|
+
{ context: activeContext },
|
|
176
|
+
),
|
|
158
177
|
taskLeaseContext(entry, input, identity.canonicalConversationId || ''),
|
|
159
178
|
);
|
|
160
179
|
const selected = taskLease.state === 'active'
|
|
@@ -176,6 +195,8 @@ export function resolveHookOperatingProfile({
|
|
|
176
195
|
baseProfile: base.profile,
|
|
177
196
|
baseSource: base.source,
|
|
178
197
|
taskLease,
|
|
198
|
+
activeContext,
|
|
199
|
+
...(contextError ? { contextError } : {}),
|
|
179
200
|
resolution: vaultResolution,
|
|
180
201
|
...(bindingError ? { bindingError } : {}),
|
|
181
202
|
};
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.76.
|
|
3
|
+
"version": "0.76.6",
|
|
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": [
|
package/src/delivery.mjs
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
3
3
|
import { basename, join, resolve } from 'node:path';
|
|
4
4
|
import {
|
|
5
5
|
assertVaultPathSafe, mkdirVaultPath, writeVaultFileSync,
|
|
6
6
|
} from '../hooks/vault-path-safety.mjs';
|
|
7
7
|
import { resolveProjectVault } from './project-vault.mjs';
|
|
8
8
|
import { createWorkRoute } from './work-kind.mjs';
|
|
9
|
+
import {
|
|
10
|
+
activeContextKey,
|
|
11
|
+
clearActiveContextDelivery,
|
|
12
|
+
resolveActiveContext,
|
|
13
|
+
setActiveContextDelivery,
|
|
14
|
+
} from '../hooks/active-context-store.mjs';
|
|
15
|
+
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
9
16
|
|
|
10
17
|
export const DELIVERY_HELP = `wendkeep delivery <subcommand>
|
|
11
18
|
|
|
@@ -15,7 +22,7 @@ export const DELIVERY_HELP = `wendkeep delivery <subcommand>
|
|
|
15
22
|
[--npm-integrity <sha512-...>] [--release-url <url>]
|
|
16
23
|
abandon [id] --reason <text>
|
|
17
24
|
|
|
18
|
-
Common options: --project <path> --vault <path> --json
|
|
25
|
+
Common options: --project <path> --vault <path> --session <id> --json
|
|
19
26
|
Delivery authorizes operational risk and creates an append-only receipt. It never creates a change,
|
|
20
27
|
spec, or ADR. If code/config must change, abandon or pause delivery and resume an implementation.
|
|
21
28
|
`;
|
|
@@ -26,7 +33,7 @@ export const DELIVERY_CAPABILITIES = Object.freeze([
|
|
|
26
33
|
|
|
27
34
|
const VALUE_OPTIONS = new Set([
|
|
28
35
|
'--project', '--vault', '--allow', '--source-change', '--source-commit', '--target',
|
|
29
|
-
'--ci-url', '--version', '--npm-integrity', '--release-url', '--reason',
|
|
36
|
+
'--ci-url', '--version', '--npm-integrity', '--release-url', '--reason', '--session',
|
|
30
37
|
]);
|
|
31
38
|
|
|
32
39
|
function parseArgv(argv) {
|
|
@@ -95,11 +102,49 @@ function readPointer(vaultBase) {
|
|
|
95
102
|
try { return safeId(readFileSync(pointer, 'utf8').trim()); } catch { return ''; }
|
|
96
103
|
}
|
|
97
104
|
|
|
98
|
-
|
|
99
|
-
const
|
|
105
|
+
function deliveryContextError(message) {
|
|
106
|
+
const error = new Error(message);
|
|
107
|
+
error.code = 'WENDKEEP_DELIVERY_CONTEXT_MISMATCH';
|
|
108
|
+
return error;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function deliveryContextBusy(id) {
|
|
112
|
+
const error = new Error(`active context já possui delivery ativo: ${id}`);
|
|
113
|
+
error.code = 'WENDKEEP_DELIVERY_CONTEXT_BUSY';
|
|
114
|
+
return error;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function contextualBinding(vaultBase, context, expectedId = '') {
|
|
118
|
+
const binding = resolveActiveContext(vaultBase, context);
|
|
119
|
+
const id = String(binding.delivery_id || '').trim();
|
|
120
|
+
if (expectedId && id !== expectedId) {
|
|
121
|
+
throw deliveryContextError(`delivery ${expectedId} não pertence ao active context chamador`);
|
|
122
|
+
}
|
|
123
|
+
return { binding, id, key: activeContextKey(context) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertStateContext(state, binding) {
|
|
127
|
+
if (!binding) return;
|
|
128
|
+
if (state.context_key !== binding.key) {
|
|
129
|
+
throw deliveryContextError(`delivery ${state.id} pertence a outro active context`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function activeDelivery(vaultBase, { context = null } = {}) {
|
|
134
|
+
let binding = null;
|
|
135
|
+
let id = readPointer(vaultBase);
|
|
136
|
+
if (context) {
|
|
137
|
+
try { binding = contextualBinding(vaultBase, context); }
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error?.code === 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND') return null;
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
id = binding.id;
|
|
143
|
+
}
|
|
100
144
|
if (!id) return null;
|
|
101
145
|
try {
|
|
102
146
|
const state = readState(vaultBase, id);
|
|
147
|
+
assertStateContext(state, binding);
|
|
103
148
|
return state.state === 'active' ? state : null;
|
|
104
149
|
} catch {
|
|
105
150
|
return null;
|
|
@@ -156,8 +201,12 @@ function context(parsed) {
|
|
|
156
201
|
return { projectRoot, repoRoot, vaultBase: resolved.base };
|
|
157
202
|
}
|
|
158
203
|
|
|
159
|
-
function currentId(parsed, vaultBase) {
|
|
160
|
-
|
|
204
|
+
function currentId(parsed, vaultBase, commandContext = null) {
|
|
205
|
+
const explicit = parsed.positionals[1];
|
|
206
|
+
if (explicit) return safeId(explicit);
|
|
207
|
+
return safeId(commandContext
|
|
208
|
+
? activeDelivery(vaultBase, { context: commandContext })?.id
|
|
209
|
+
: readPointer(vaultBase));
|
|
161
210
|
}
|
|
162
211
|
|
|
163
212
|
function ensureClean(repoRoot) {
|
|
@@ -169,11 +218,30 @@ function ensureClean(repoRoot) {
|
|
|
169
218
|
}
|
|
170
219
|
}
|
|
171
220
|
|
|
172
|
-
export function startDelivery({
|
|
221
|
+
export function startDelivery({
|
|
222
|
+
vaultBase,
|
|
223
|
+
repoRoot,
|
|
224
|
+
id,
|
|
225
|
+
capabilities,
|
|
226
|
+
sourceChange = '',
|
|
227
|
+
sourceCommit = '',
|
|
228
|
+
context = null,
|
|
229
|
+
bindDelivery = setActiveContextDelivery,
|
|
230
|
+
removeState = rmSync,
|
|
231
|
+
now = new Date(),
|
|
232
|
+
}) {
|
|
173
233
|
ensureClean(repoRoot);
|
|
174
234
|
const deliveryId = safeId(id || generatedId(now));
|
|
175
235
|
const paths = deliveryPaths(vaultBase, deliveryId);
|
|
176
236
|
if (existsSync(paths.state)) throw new Error(`delivery já existe: ${deliveryId}`);
|
|
237
|
+
if (context) {
|
|
238
|
+
try {
|
|
239
|
+
const current = contextualBinding(vaultBase, context);
|
|
240
|
+
if (current.id) throw deliveryContextBusy(current.id);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error?.code !== 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND') throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
177
245
|
const commit = sourceCommit || git(repoRoot, ['rev-parse', 'HEAD']);
|
|
178
246
|
git(repoRoot, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
179
247
|
const requestedCapabilities = [...new Set((capabilities || []).map((item) => String(item).trim()).filter(Boolean))];
|
|
@@ -195,16 +263,29 @@ export function startDelivery({ vaultBase, repoRoot, id, capabilities, sourceCha
|
|
|
195
263
|
worktree: repoRoot,
|
|
196
264
|
branch: git(repoRoot, ['branch', '--show-current'], true),
|
|
197
265
|
source_commit: commit,
|
|
266
|
+
...(context ? { context_key: activeContextKey(context) } : {}),
|
|
198
267
|
started_at: now.toISOString(),
|
|
199
268
|
};
|
|
200
269
|
writeState(vaultBase, state);
|
|
201
|
-
|
|
270
|
+
if (context) {
|
|
271
|
+
try { bindDelivery(vaultBase, context, deliveryId); }
|
|
272
|
+
catch (error) {
|
|
273
|
+
try { removeState(paths.state, { force: true }); } catch { /* rollback best-effort */ }
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
setPointer(vaultBase, deliveryId);
|
|
278
|
+
}
|
|
202
279
|
return state;
|
|
203
280
|
}
|
|
204
281
|
|
|
205
|
-
export function finishDelivery({
|
|
282
|
+
export function finishDelivery({
|
|
283
|
+
vaultBase, repoRoot, id, target = 'HEAD', evidence = {}, context = null, now = new Date(),
|
|
284
|
+
}) {
|
|
206
285
|
ensureClean(repoRoot);
|
|
207
286
|
const state = readState(vaultBase, safeId(id));
|
|
287
|
+
const binding = context ? contextualBinding(vaultBase, context, state.id) : null;
|
|
288
|
+
assertStateContext(state, binding);
|
|
208
289
|
if (state.state !== 'active') throw new Error(`delivery ${id} não está ativa`);
|
|
209
290
|
const targetCommit = git(repoRoot, ['rev-parse', `${target}^{commit}`]);
|
|
210
291
|
git(repoRoot, ['merge-base', '--is-ancestor', state.source_commit, targetCommit]);
|
|
@@ -228,29 +309,49 @@ export function finishDelivery({ vaultBase, repoRoot, id, target = 'HEAD', evide
|
|
|
228
309
|
work_kind: 'delivery',
|
|
229
310
|
source_change: state.route.source_change || '',
|
|
230
311
|
source_commit: state.source_commit,
|
|
312
|
+
...(state.context_key ? { context_key: state.context_key } : {}),
|
|
231
313
|
target,
|
|
232
314
|
target_commit: targetCommit,
|
|
233
315
|
capabilities,
|
|
234
316
|
evidence,
|
|
235
317
|
finished_at: now.toISOString(),
|
|
236
318
|
};
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
319
|
+
if (context) clearActiveContextDelivery(vaultBase, context, { expectedRevision: binding.binding.revision });
|
|
320
|
+
try {
|
|
321
|
+
appendReceipt(vaultBase, receipt);
|
|
322
|
+
writeState(vaultBase, { ...state, state: 'completed', target, target_commit: targetCommit, finished_at: receipt.finished_at, receipt });
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (context) {
|
|
325
|
+
try { setActiveContextDelivery(vaultBase, context, state.id); } catch { /* rollback best-effort */ }
|
|
326
|
+
}
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
if (!context && readPointer(vaultBase) === state.id) setPointer(vaultBase);
|
|
240
330
|
return receipt;
|
|
241
331
|
}
|
|
242
332
|
|
|
243
|
-
export function abandonDelivery({ vaultBase, id, reason, now = new Date() }) {
|
|
333
|
+
export function abandonDelivery({ vaultBase, id, reason, context = null, now = new Date() }) {
|
|
244
334
|
const state = readState(vaultBase, safeId(id));
|
|
335
|
+
const binding = context ? contextualBinding(vaultBase, context, state.id) : null;
|
|
336
|
+
assertStateContext(state, binding);
|
|
245
337
|
if (state.state !== 'active') throw new Error(`delivery ${id} não está ativa`);
|
|
246
338
|
if (!String(reason || '').trim()) throw new Error('delivery abandon requer --reason <text>');
|
|
247
339
|
const receipt = {
|
|
248
340
|
schema_version: 1, delivery_id: state.id, outcome: 'abandoned',
|
|
341
|
+
...(state.context_key ? { context_key: state.context_key } : {}),
|
|
249
342
|
reason: String(reason).trim(), abandoned_at: now.toISOString(),
|
|
250
343
|
};
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
344
|
+
if (context) clearActiveContextDelivery(vaultBase, context, { expectedRevision: binding.binding.revision });
|
|
345
|
+
try {
|
|
346
|
+
appendReceipt(vaultBase, receipt);
|
|
347
|
+
writeState(vaultBase, { ...state, state: 'abandoned', reason: receipt.reason, abandoned_at: receipt.abandoned_at });
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (context) {
|
|
350
|
+
try { setActiveContextDelivery(vaultBase, context, state.id); } catch { /* rollback best-effort */ }
|
|
351
|
+
}
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
if (!context && readPointer(vaultBase) === state.id) setPointer(vaultBase);
|
|
254
355
|
return receipt;
|
|
255
356
|
}
|
|
256
357
|
|
|
@@ -263,23 +364,33 @@ export function runDelivery(argv = []) {
|
|
|
263
364
|
try {
|
|
264
365
|
const parsed = parseArgv(argv);
|
|
265
366
|
const sub = parsed.positionals[0] || 'status';
|
|
266
|
-
const { repoRoot, vaultBase } = context(parsed);
|
|
367
|
+
const { projectRoot, repoRoot, vaultBase } = context(parsed);
|
|
368
|
+
const commandContext = resolveCommandActiveContext({
|
|
369
|
+
vaultBase,
|
|
370
|
+
projectRoot,
|
|
371
|
+
sessionId: parsed.value('--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '',
|
|
372
|
+
});
|
|
267
373
|
if (sub === 'start') {
|
|
268
374
|
const state = startDelivery({
|
|
269
375
|
vaultBase, repoRoot, id: parsed.positionals[1], capabilities: parsed.all('--allow'),
|
|
270
376
|
sourceChange: parsed.value('--source-change'), sourceCommit: parsed.value('--source-commit'),
|
|
377
|
+
context: commandContext,
|
|
271
378
|
});
|
|
272
379
|
emit(state, parsed.json);
|
|
273
380
|
return 0;
|
|
274
381
|
}
|
|
275
382
|
if (sub === 'status') {
|
|
276
|
-
const
|
|
383
|
+
const id = currentId(parsed, vaultBase, commandContext);
|
|
384
|
+
const binding = commandContext ? contextualBinding(vaultBase, commandContext, id) : null;
|
|
385
|
+
const state = readState(vaultBase, id);
|
|
386
|
+
assertStateContext(state, binding);
|
|
277
387
|
emit(state, parsed.json);
|
|
278
388
|
return 0;
|
|
279
389
|
}
|
|
280
390
|
if (sub === 'finish') {
|
|
281
391
|
const receipt = finishDelivery({
|
|
282
|
-
vaultBase, repoRoot, id: currentId(parsed, vaultBase), target: parsed.value('--target') || 'HEAD',
|
|
392
|
+
vaultBase, repoRoot, id: currentId(parsed, vaultBase, commandContext), target: parsed.value('--target') || 'HEAD',
|
|
393
|
+
context: commandContext,
|
|
283
394
|
evidence: {
|
|
284
395
|
ci_url: parsed.value('--ci-url'), version: parsed.value('--version'),
|
|
285
396
|
npm_integrity: parsed.value('--npm-integrity'), release_url: parsed.value('--release-url'),
|
|
@@ -290,14 +401,15 @@ export function runDelivery(argv = []) {
|
|
|
290
401
|
}
|
|
291
402
|
if (sub === 'abandon') {
|
|
292
403
|
const receipt = abandonDelivery({
|
|
293
|
-
vaultBase, id: currentId(parsed, vaultBase), reason: parsed.value('--reason'),
|
|
404
|
+
vaultBase, id: currentId(parsed, vaultBase, commandContext), reason: parsed.value('--reason'),
|
|
405
|
+
context: commandContext,
|
|
294
406
|
});
|
|
295
407
|
emit(receipt, parsed.json);
|
|
296
408
|
return 0;
|
|
297
409
|
}
|
|
298
410
|
throw new Error(`subcomando desconhecido: ${sub}`);
|
|
299
411
|
} catch (error) {
|
|
300
|
-
process.stderr.write(`wendkeep delivery: ${error.message}\n`);
|
|
412
|
+
process.stderr.write(`wendkeep delivery: ${error.code ? `${error.code}: ` : ''}${error.message}\n`);
|
|
301
413
|
return 2;
|
|
302
414
|
}
|
|
303
415
|
}
|
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,
|