wendkeep 0.87.0 → 0.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.en.md +3 -2
  3. package/README.md +3 -2
  4. package/bin/wendkeep.mjs +1 -0
  5. package/docs/en/commands/ecosystem-bridges.md +172 -0
  6. package/docs/en/commands/observer-security.md +154 -0
  7. package/docs/en/commands/observer.md +30 -12
  8. package/docs/en/commands/verify.md +6 -0
  9. package/docs/pt-BR/commands/ecosystem-bridges.md +169 -0
  10. package/docs/pt-BR/commands/observer-security.md +154 -0
  11. package/docs/pt-BR/commands/observer.md +30 -12
  12. package/docs/pt-BR/commands/verify.md +6 -0
  13. package/hooks/observer-publish.mjs +3 -1
  14. package/package.json +2 -1
  15. package/packages/cli/src/index.mjs +10 -1
  16. package/packages/harness/src/sensors-core.mjs +49 -3
  17. package/packages/integrations/src/bridge-config.mjs +139 -0
  18. package/packages/integrations/src/bridge-contract.mjs +316 -0
  19. package/packages/integrations/src/bridge-diagnostics.mjs +45 -0
  20. package/packages/integrations/src/canonical-bridge-authority.mjs +32 -0
  21. package/packages/integrations/src/capabilities.mjs +34 -0
  22. package/packages/integrations/src/ecosystem-bridge.mjs +82 -0
  23. package/packages/integrations/src/index.mjs +6 -0
  24. package/packages/integrations/src/spec-kit-adapter.mjs +259 -0
  25. package/packages/integrations/src/superpowers-adapter.mjs +269 -0
  26. package/packages/mcp/src/executor.mjs +35 -2
  27. package/packages/observer/package.json +16 -0
  28. package/packages/observer/src/audit.mjs +1 -0
  29. package/packages/observer/src/authz.mjs +38 -0
  30. package/packages/observer/src/encryption.mjs +75 -0
  31. package/packages/observer/src/index.mjs +7 -0
  32. package/packages/observer/src/policy.mjs +305 -0
  33. package/packages/observer/src/purge.mjs +100 -0
  34. package/packages/observer/src/redaction.mjs +54 -0
  35. package/packages/observer/src/retention.mjs +39 -0
  36. package/packages/observer/src/token-registry.mjs +122 -0
  37. package/schema/ecosystem-bridge-artifact-manifest-v1.schema.json +30 -0
  38. package/schema/ecosystem-bridge-v1.schema.json +65 -0
  39. package/schema/observer/006-observer-security.sql +64 -0
  40. package/schema/observer-policy-v1.schema.json +63 -0
  41. package/schema/sync-event-v1.schema.json +10 -0
  42. package/schema/wendkeep.evidence-envelope-v2.schema.json +39 -0
  43. package/schema/wendkeep.sensors.schema.json +14 -0
  44. package/src/doctor.mjs +6 -1
  45. package/src/ecosystem-bridge-artifact-collector.mjs +111 -0
  46. package/src/ecosystem-bridge-baseline.mjs +58 -0
  47. package/src/ecosystem-bridge-proof.mjs +97 -0
  48. package/src/ecosystem-bridges.mjs +227 -0
  49. package/src/evidence-envelope.mjs +2 -0
  50. package/src/observer-auth.mjs +8 -0
  51. package/src/observer-privacy.mjs +7 -3
  52. package/src/observer-publish.mjs +31 -0
  53. package/src/observer-server.mjs +179 -20
  54. package/src/observer-sql-migrate.mjs +5 -2
  55. package/src/observer-sql-publish.mjs +114 -39
  56. package/src/observer-sql-store.mjs +299 -45
  57. package/src/observer-transcript-store.mjs +23 -8
  58. package/src/observer.mjs +145 -12
  59. package/src/sync-protocol.mjs +20 -0
  60. package/src/task-contracts.mjs +19 -0
  61. package/src/task.mjs +82 -0
  62. package/src/verify.mjs +9 -0
  63. package/web/observer/app.mjs +107 -31
  64. package/web/observer/index.html +7 -0
  65. package/web/observer/styles.css +5 -0
package/src/observer.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  import { homedir } from 'node:os';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
2
4
  import { isAbsolute, resolve } from 'node:path';
3
5
  import { readObserverIndexSource } from './observer-store.mjs';
4
6
  import { buildProjectSnapshot } from './observer-snapshot.mjs';
@@ -6,15 +8,23 @@ import { compareMemoryParity } from './observer-memory-publish.mjs';
6
8
  import { publishObserverSql } from './observer-sql-publish.mjs';
7
9
  import { migrateObserverData } from './observer-sql-migrate.mjs';
8
10
  import { startObserverServer } from './observer-server.mjs';
9
- import { ensureObserverDatabase, migrateObserverDatabase, listSqlProjects, registerSqlProject, upsertSqlProjectSnapshot, OBSERVER_SQL_FILE, OBSERVER_SQL_SCHEMA_VERSION } from './observer-sql-store.mjs';
11
+ import { ensureObserverDatabase, listSqlProjects, readSqlProject, registerSqlProject, upsertSqlProjectSnapshot, OBSERVER_SQL_FILE, OBSERVER_SQL_SCHEMA_VERSION } from './observer-sql-store.mjs';
10
12
  import { resolveProjectVault } from '../packages/vault/src/project-vault.mjs';
11
13
  import { observerAuthHeaders, resolveObserverToken } from './observer-auth.mjs';
14
+ import { recordObserverAudit } from '../packages/observer/src/authz.mjs';
15
+ import { readObserverPolicy, saveObserverPolicy } from '../packages/observer/src/policy.mjs';
16
+ import { purgeObserverData } from '../packages/observer/src/purge.mjs';
17
+ import { runObserverRetention } from '../packages/observer/src/retention.mjs';
18
+ import { registerObserverToken, revokeObserverToken, rotateObserverToken } from '../packages/observer/src/token-registry.mjs';
19
+ import { observerEncryptionFromEnvironment } from '../packages/observer/src/encryption.mjs';
12
20
 
13
21
  export const OBSERVER_HELP = `wendkeep observer — Observer local multi-projeto
14
22
 
15
23
  Uso:
16
24
  wendkeep observer serve [--data-dir P] [--host 127.0.0.1] [--port 8787]
17
- [--allow-non-loopback] [--token TOKEN]
25
+ [--allow-non-loopback] [--require-loopback-auth] [--require-encryption] [--token TOKEN]
26
+ [--bootstrap-token-id ID] [--bootstrap-role ROLE]
27
+ [--bootstrap-projects P1,P2] [--bootstrap-scopes S1,S2] --bootstrap-expires-at ISO
18
28
  wendkeep observer register --project P [--vault V] [--data-dir D] [--json]
19
29
  wendkeep observer publish --project P [--vault V] [--data-dir D] [--json]
20
30
  wendkeep observer reconcile --project P [--vault V] [--data-dir D] [--url U]
@@ -22,6 +32,14 @@ Uso:
22
32
  wendkeep observer memory import --project P [--vault V] [--url U] [--token TOKEN]
23
33
  [--capture-level metadata|messages|full-transcript] [--json]
24
34
  wendkeep observer status [--data-dir D] [--json]
35
+ wendkeep observer security token create --project-id P --role R --scopes S
36
+ --token-env ENV --expires-at ISO [--token-id ID] [--reason TEXT] [--json]
37
+ wendkeep observer security token rotate --project-id P --token-id ID --token-env ENV
38
+ --expires-at ISO [--new-token-id ID] [--reason TEXT] [--json]
39
+ wendkeep observer security token revoke --project-id P --token-id ID [--reason TEXT] [--json]
40
+ wendkeep observer security policy set --project-id P --file policy.json [--json]
41
+ wendkeep observer security purge --project-id P --before ISO --classes C [--dry-run] [--operation-id ID] [--json]
42
+ wendkeep observer security retention run --project-id P [--dry-run] [--operation-id ID] [--observed-at ISO] [--json]
25
43
 
26
44
  O Observer local pode manter snapshots operacionais e uma cópia completa da memória em volume
27
45
  Docker. O comando memory import faz a primeira migração de um vault para o container.
@@ -34,6 +52,10 @@ function optionValue(argv, name) {
34
52
  return argv.find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1) || '';
35
53
  }
36
54
 
55
+ function csv(value) {
56
+ return String(value || '').split(',').map((item) => item.trim()).filter(Boolean);
57
+ }
58
+
37
59
  function dataDir(argv) {
38
60
  return resolve(optionValue(argv, '--data-dir')
39
61
  || process.env.WENDKEEP_OBSERVER_DATA_DIR
@@ -62,23 +84,22 @@ function summary(index) {
62
84
  };
63
85
  }
64
86
 
65
- function databaseSummary(dir) {
66
- const db = ensureObserverDatabase(dir);
87
+ function databaseSummary(dir, security) {
88
+ const db = ensureObserverDatabase(dir, { security });
67
89
  try {
68
- const migrations = migrateObserverDatabase(db);
69
90
  return {
70
91
  engine: 'sqlite',
71
92
  file: OBSERVER_SQL_FILE,
72
93
  schema_version: OBSERVER_SQL_SCHEMA_VERSION,
73
- migrations: migrations.applied.length,
94
+ migrations: Number(db.prepare('SELECT COUNT(*) AS count FROM schema_migrations').get().count),
74
95
  projects: listSqlProjects(db).length,
75
96
  ready: true,
76
97
  };
77
98
  } finally { db.close(); }
78
99
  }
79
100
 
80
- function sqlProjectsSummary(dir) {
81
- const db = ensureObserverDatabase(dir);
101
+ function sqlProjectsSummary(dir, security) {
102
+ const db = ensureObserverDatabase(dir, { security });
82
103
  try {
83
104
  return listSqlProjects(db).map((project) => ({
84
105
  projectId: project.project_id,
@@ -99,21 +120,133 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
99
120
  }
100
121
  const dir = dataDir(argv);
101
122
  const token = resolveObserverToken(optionValue(argv, '--token'));
123
+ const databaseSecurity = {
124
+ encryption: observerEncryptionFromEnvironment({
125
+ required: argv.includes('--require-encryption') || process.env.WENDKEEP_OBSERVER_REQUIRE_ENCRYPTION === '1',
126
+ }),
127
+ };
128
+
129
+ if (sub === 'security') {
130
+ const domain = argv[1] || '';
131
+ const action = argv[2] || '';
132
+ const projectId = optionValue(argv, '--project-id');
133
+ if (!projectId) throw new Error('observer security: --project-id é obrigatório.');
134
+ const db = ensureObserverDatabase(dir, { security: databaseSecurity });
135
+ try {
136
+ readSqlProject(db, projectId);
137
+ if (domain === 'token' && action === 'create') {
138
+ const envName = optionValue(argv, '--token-env');
139
+ const rawToken = envName ? String(process.env[envName] || '') : '';
140
+ if (!envName || !rawToken) throw new Error('observer security token create: --token-env deve apontar para um segredo não vazio.');
141
+ const created = registerObserverToken(db, {
142
+ tokenId: optionValue(argv, '--token-id') || randomBytes(12).toString('hex'),
143
+ token: rawToken,
144
+ role: optionValue(argv, '--role'),
145
+ projectIds: [projectId],
146
+ scopes: optionValue(argv, '--scopes').split(',').map((item) => item.trim()).filter(Boolean),
147
+ expiresAt: optionValue(argv, '--expires-at'),
148
+ });
149
+ recordObserverAudit(db, {
150
+ projectId, tokenId: created.token_id, capability: 'security:recovery', outcome: 'created',
151
+ metadata: { route: 'offline-cli', method: 'LOCAL', reason: optionValue(argv, '--reason') || 'token create' },
152
+ });
153
+ print(created, asJson, write);
154
+ return 0;
155
+ }
156
+ if (domain === 'token' && action === 'revoke') {
157
+ const tokenId = optionValue(argv, '--token-id');
158
+ const revoked = revokeObserverToken(db, { tokenId });
159
+ recordObserverAudit(db, {
160
+ projectId, tokenId, capability: 'security:recovery', outcome: revoked.revoked ? 'revoked' : 'not-found',
161
+ metadata: { route: 'offline-cli', method: 'LOCAL', reason: optionValue(argv, '--reason') || 'token revoke' },
162
+ });
163
+ print(revoked, asJson, write);
164
+ return revoked.revoked ? 0 : 1;
165
+ }
166
+ if (domain === 'token' && action === 'rotate') {
167
+ const envName = optionValue(argv, '--token-env');
168
+ const rawToken = envName ? String(process.env[envName] || '') : '';
169
+ if (!envName || !rawToken) throw new Error('observer security token rotate: --token-env deve apontar para um segredo não vazio.');
170
+ const rotated = rotateObserverToken(db, {
171
+ tokenId: optionValue(argv, '--token-id'),
172
+ newTokenId: optionValue(argv, '--new-token-id') || randomBytes(12).toString('hex'),
173
+ newToken: rawToken,
174
+ expiresAt: optionValue(argv, '--expires-at'),
175
+ });
176
+ recordObserverAudit(db, {
177
+ projectId, tokenId: rotated.token_id, capability: 'security:recovery', outcome: 'rotated',
178
+ metadata: { route: 'offline-cli', method: 'LOCAL', reason: optionValue(argv, '--reason') || 'token rotate' },
179
+ });
180
+ print(rotated, asJson, write);
181
+ return 0;
182
+ }
183
+ if (domain === 'policy' && action === 'set') {
184
+ const path = optionValue(argv, '--file');
185
+ if (!path) throw new Error('observer security policy set: --file é obrigatório.');
186
+ const policy = saveObserverPolicy(db, projectId, JSON.parse(readFileSync(resolve(path), 'utf8')));
187
+ print({ schema_version: 1, project_id: projectId, policy }, asJson, write);
188
+ return 0;
189
+ }
190
+ if (domain === 'policy' && action === 'show') {
191
+ print({ schema_version: 1, project_id: projectId, policy: readObserverPolicy(db, projectId) }, asJson, write);
192
+ return 0;
193
+ }
194
+ if (domain === 'purge') {
195
+ const result = purgeObserverData(db, {
196
+ projectId,
197
+ before: optionValue(argv, '--before'),
198
+ classes: optionValue(argv, '--classes').split(',').map((item) => item.trim()).filter(Boolean),
199
+ operationId: optionValue(argv, '--operation-id'),
200
+ dryRun: argv.includes('--dry-run'),
201
+ });
202
+ print(result, asJson, write);
203
+ return 0;
204
+ }
205
+ if (domain === 'retention' && action === 'run') {
206
+ const policy = readObserverPolicy(db, projectId);
207
+ const observedAt = optionValue(argv, '--observed-at');
208
+ const result = runObserverRetention(db, {
209
+ projectId,
210
+ policy: policy.retention,
211
+ clock: () => observedAt ? new Date(observedAt) : new Date(),
212
+ operationId: optionValue(argv, '--operation-id'),
213
+ dryRun: argv.includes('--dry-run'),
214
+ });
215
+ print(result, asJson, write);
216
+ return 0;
217
+ }
218
+ throw new Error(`observer security: operação desconhecida: ${domain} ${action}`.trim());
219
+ } finally { db.close(); }
220
+ }
102
221
 
103
222
  if (sub === 'status') {
104
223
  const legacy = summary(readObserverIndexSource(dir));
105
- print({ ...legacy, projects: sqlProjectsSummary(dir), legacy_projects: legacy.projects, database: databaseSummary(dir) }, asJson, write);
224
+ print({ ...legacy, projects: sqlProjectsSummary(dir, databaseSecurity), legacy_projects: legacy.projects, database: databaseSummary(dir, databaseSecurity) }, asJson, write);
106
225
  return 0;
107
226
  }
108
227
 
109
228
  if (sub === 'serve') {
110
229
  const host = optionValue(argv, '--host') || '127.0.0.1';
230
+ const encryption = databaseSecurity.encryption;
231
+ const secureMode = argv.includes('--require-loopback-auth') || Boolean(encryption);
111
232
  const server = await startObserverServer({
112
233
  dataDir: dir,
113
234
  host,
114
235
  port: Number(optionValue(argv, '--port') || 8787),
115
236
  allowNonLoopback: argv.includes('--allow-non-loopback'),
116
237
  token,
238
+ bootstrap: {
239
+ tokenId: optionValue(argv, '--bootstrap-token-id') || process.env.WENDKEEP_OBSERVER_BOOTSTRAP_TOKEN_ID || '',
240
+ role: optionValue(argv, '--bootstrap-role') || process.env.WENDKEEP_OBSERVER_BOOTSTRAP_ROLE || 'admin',
241
+ projectIds: csv(optionValue(argv, '--bootstrap-projects') || process.env.WENDKEEP_OBSERVER_BOOTSTRAP_PROJECTS),
242
+ scopes: csv(optionValue(argv, '--bootstrap-scopes') || process.env.WENDKEEP_OBSERVER_BOOTSTRAP_SCOPES || '*'),
243
+ expiresAt: optionValue(argv, '--bootstrap-expires-at') || process.env.WENDKEEP_OBSERVER_BOOTSTRAP_EXPIRES_AT || '',
244
+ },
245
+ security: {
246
+ enabled: secureMode,
247
+ requireLoopbackAuth: argv.includes('--require-loopback-auth'),
248
+ encryption,
249
+ },
117
250
  });
118
251
  const address = server.address();
119
252
  process.stdout.write(`wendkeep observer listening: http://${address.address}:${address.port}\n`);
@@ -179,7 +312,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
179
312
  const snapshot = buildProjectSnapshot({ vaultBase: vault, projectRoot: root });
180
313
  const url = optionValue(argv, '--url') || process.env.WENDKEEP_OBSERVER_URL || '';
181
314
  if (!url) {
182
- const db = ensureObserverDatabase(dir);
315
+ const db = ensureObserverDatabase(dir, { security: databaseSecurity });
183
316
  let migration;
184
317
  try {
185
318
  migration = migrateObserverData({
@@ -231,7 +364,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
231
364
  const snapshot = buildProjectSnapshot({ vaultBase: vault, projectRoot: root });
232
365
 
233
366
  if (sub === 'register') {
234
- const db = ensureObserverDatabase(dir);
367
+ const db = ensureObserverDatabase(dir, { security: databaseSecurity });
235
368
  let sql;
236
369
  try {
237
370
  sql = registerSqlProject(db, {
@@ -245,7 +378,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
245
378
  return 0;
246
379
  }
247
380
 
248
- const db = ensureObserverDatabase(dir);
381
+ const db = ensureObserverDatabase(dir, { security: databaseSecurity });
249
382
  let migration;
250
383
  try {
251
384
  migration = migrateObserverData({
@@ -73,9 +73,25 @@ function validatePrivatePayload(payload) {
73
73
  }
74
74
  }
75
75
 
76
+ function normalizePolicyRef(value) {
77
+ if (value == null) return null;
78
+ const keys = Object.keys(value).sort();
79
+ if (keys.join(',') !== 'hash,policy_id,version'
80
+ || !HASH_PATTERN.test(String(value.hash || ''))
81
+ || !Number.isSafeInteger(Number(value.version)) || Number(value.version) < 1) {
82
+ throw syncError('WENDKEEP_SYNC_POLICY_REF_INVALID', 'policy_ref must contain only policy_id, version and sha256 hash');
83
+ }
84
+ return {
85
+ policy_id: requiredText(value.policy_id, 'policy_ref.policy_id', 160),
86
+ version: Number(value.version),
87
+ hash: String(value.hash),
88
+ };
89
+ }
90
+
76
91
  export function createSyncEvent({
77
92
  projectId, recordKey, revision, baseRevision, payload = null, causalParentIds = [],
78
93
  actorId, deviceId, leaseId = '', observedAt, operation = 'put', privacy = 'shared',
94
+ policyRef = null,
79
95
  } = {}) {
80
96
  const project_id = requiredText(projectId, 'project_id', 160);
81
97
  const record_key = requiredText(recordKey, 'record_key', 2048);
@@ -108,6 +124,7 @@ export function createSyncEvent({
108
124
  observed_at: timestamp.toISOString(),
109
125
  operation,
110
126
  privacy,
127
+ ...(policyRef ? { policy_ref: normalizePolicyRef(policyRef) } : {}),
111
128
  payload: operation === 'tombstone' ? null : structuredClone(payload),
112
129
  };
113
130
  return { ...draft, event_id: syncSha256(draft).slice(7) };
@@ -137,6 +154,7 @@ export function validateSyncEvent(event, { projectId = '' } = {}) {
137
154
  observedAt: event.observed_at,
138
155
  operation: event.operation,
139
156
  privacy: event.privacy,
157
+ policyRef: event.policy_ref || null,
140
158
  });
141
159
  return event;
142
160
  }
@@ -153,6 +171,7 @@ function eventCandidate(event) {
153
171
  observed_at: event.observed_at,
154
172
  operation: event.operation,
155
173
  privacy: event.privacy,
174
+ ...(event.policy_ref ? { policy_ref: structuredClone(event.policy_ref) } : {}),
156
175
  payload: structuredClone(event.payload),
157
176
  };
158
177
  }
@@ -168,6 +187,7 @@ function recordFromEvent(event, { conflicted = false } = {}) {
168
187
  observed_at: event.observed_at,
169
188
  operation: event.operation,
170
189
  privacy: event.privacy,
190
+ ...(event.policy_ref ? { policy_ref: structuredClone(event.policy_ref) } : {}),
171
191
  payload: structuredClone(event.payload),
172
192
  tombstone: event.operation === 'tombstone',
173
193
  conflicted,
@@ -43,6 +43,25 @@ function uniqueStrings(values) {
43
43
  return [...new Set(list.map((value) => String(value).trim()).filter(Boolean))];
44
44
  }
45
45
 
46
+ export function normalizeExternalArtifactEvidence(input = {}) {
47
+ const source = String(input.source || '').trim();
48
+ const externalId = String(input.external_id || '').trim();
49
+ const kind = String(input.kind || '').trim();
50
+ const sha256Value = String(input.sha256 || '').trim();
51
+ if (!source || !externalId || !['artifact', 'review', 'commit'].includes(kind)
52
+ || !/^[a-f0-9]{64}$/.test(sha256Value)) {
53
+ throw Object.assign(new Error('external artifact evidence is incomplete'), { code: 'TASK_EXTERNAL_ARTIFACT_INVALID' });
54
+ }
55
+ return {
56
+ schema_version: 1,
57
+ source,
58
+ external_id: externalId,
59
+ kind,
60
+ sha256: sha256Value,
61
+ authority: 'reported',
62
+ };
63
+ }
64
+
46
65
  function bindingFrom(input) {
47
66
  return {
48
67
  project_id: String(input.projectId || ''),
package/src/task.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import * as bridgeFs from 'node:fs';
1
2
  import { isAbsolute, resolve } from 'node:path';
2
3
  import { activeChange } from '../hooks/change-core.mjs';
3
4
  import { resolveActiveContext } from '../hooks/active-context-store.mjs';
@@ -6,6 +7,19 @@ import { buildTaskContractSnapshot, evaluateTaskContracts } from './task-contrac
6
7
  import { claimTaskLease, releaseTaskLease } from './task-leases.mjs';
7
8
  import { findProjectRoot } from '../packages/harness/src/sensors-core.mjs';
8
9
  import { resolveHookOperatingProfile } from '../hooks/operating-profile-runtime.mjs';
10
+ import { buildSuperpowersDispatch } from '../packages/integrations/src/superpowers-adapter.mjs';
11
+ import { importSpecKitProjection } from '../packages/integrations/src/spec-kit-adapter.mjs';
12
+ import { validateBridgeProjection } from '../packages/integrations/src/bridge-contract.mjs';
13
+ import { issueCanonicalDispatchAuthority } from '../packages/integrations/src/canonical-bridge-authority.mjs';
14
+
15
+ export function buildExternalTaskDispatch({ adapter = '', taskContract, ...options } = {}) {
16
+ if (String(adapter) !== 'superpowers') {
17
+ throw Object.assign(new Error(`unsupported task adapter: ${adapter || '(missing)'}`), {
18
+ code: 'TASK_ADAPTER_UNSUPPORTED',
19
+ });
20
+ }
21
+ return buildSuperpowersDispatch({ taskContract, ...options });
22
+ }
9
23
 
10
24
  const HELP = `wendkeep task <list|show|evaluate|claim|release> [task-id]
11
25
 
@@ -56,6 +70,74 @@ function commandState(argv) {
56
70
  };
57
71
  }
58
72
 
73
+ function resolveCanonicalTaskAuthority(argv = [], taskIdOverride = '') {
74
+ const state = commandState(argv);
75
+ const snapshot = buildTaskContractSnapshot(state);
76
+ const taskId = String(taskIdOverride || opt(argv, '--task-id') || '').trim();
77
+ const contract = snapshot.contracts.find((item) => item.task_id === taskId);
78
+ if (!contract) {
79
+ throw Object.assign(new Error(`task not found: ${taskId || '(missing id)'}`), { code: 'TASK_NOT_FOUND' });
80
+ }
81
+ return {
82
+ authority: 'wendkeep-canonical',
83
+ task_contract: structuredClone(contract),
84
+ active_context: structuredClone(snapshot.binding),
85
+ };
86
+ }
87
+
88
+ export function buildCanonicalExternalTaskDispatch({
89
+ adapter = '', authorityArgv = [], taskId = '', submittedTaskContract = null,
90
+ projectRoot = '', config, specKitProjection = null, baselineProjection = null, ...options
91
+ } = {}) {
92
+ if (adapter !== 'superpowers') return buildExternalTaskDispatch({ adapter, taskContract: null, ...options });
93
+ if (specKitProjection) {
94
+ const supplied = validateBridgeProjection(specKitProjection);
95
+ if (!supplied.valid) {
96
+ return {
97
+ schema_version: 1, adapter: 'superpowers', active: true, ok: false,
98
+ diagnostics: [{
99
+ schema_version: 1, code: 'BRIDGE_PROJECTION_INVALID', adapter: 'spec-kit', blocking: true,
100
+ message: 'submitted Spec Kit projection is not sealed by its complete decision state',
101
+ }],
102
+ };
103
+ }
104
+ }
105
+ if (config?.adapters?.['spec-kit']?.enabled) {
106
+ if (!specKitProjection || !baselineProjection) {
107
+ return {
108
+ schema_version: 1, adapter: 'superpowers', active: true, ok: false,
109
+ diagnostics: [{
110
+ schema_version: 1, code: 'BRIDGE_BASELINE_MISSING', adapter: 'spec-kit', blocking: true,
111
+ message: 'active Spec Kit dispatch requires its canonical baseline and submitted projection',
112
+ }],
113
+ };
114
+ }
115
+ const baseline = validateBridgeProjection(baselineProjection);
116
+ if (!baseline.valid || baselineProjection.projection_id !== specKitProjection.projection_id) {
117
+ return {
118
+ schema_version: 1, adapter: 'superpowers', active: true, ok: false,
119
+ diagnostics: [{
120
+ schema_version: 1, code: 'BRIDGE_BASELINE_STALE', adapter: 'spec-kit', blocking: true,
121
+ message: 'submitted projection does not match the canonical Spec Kit baseline',
122
+ }],
123
+ };
124
+ }
125
+ }
126
+ const canonical = resolveCanonicalTaskAuthority(authorityArgv, taskId);
127
+ const canonicalReceipt = issueCanonicalDispatchAuthority(canonical);
128
+ const liveProjection = specKitProjection
129
+ ? importSpecKitProjection({ projectRoot, config, previousProjection: baselineProjection || specKitProjection, fs: bridgeFs })
130
+ : null;
131
+ return buildExternalTaskDispatch({
132
+ adapter,
133
+ taskContract: submittedTaskContract || canonical.task_contract,
134
+ canonicalAuthority: canonicalReceipt,
135
+ specKitProjection: liveProjection,
136
+ config,
137
+ ...options,
138
+ });
139
+ }
140
+
59
141
  function write(value, json, line = '') {
60
142
  if (json) process.stdout.write(`${JSON.stringify(value)}\n`);
61
143
  else process.stdout.write(`${line || String(value)}\n`);
package/src/verify.mjs CHANGED
@@ -25,6 +25,7 @@ import { resolveCommandActiveContext } from './active-context-runtime.mjs';
25
25
  import { resolveHookOperatingProfile } from '../hooks/operating-profile-runtime.mjs';
26
26
  import { evaluateTddAttestation } from './tdd-attestation.mjs';
27
27
  import { readTddAttestationStore } from './tdd-attestation-store.mjs';
28
+ import { collectBridgeArtifactEvidence } from './ecosystem-bridge-artifact-collector.mjs';
28
29
  import { writeVaultFileAtomic } from '../packages/vault/src/vault-path-safety.mjs';
29
30
  import { evaluateHostCoverage } from '../packages/integrations/src/capabilities.mjs';
30
31
  import { evidenceCheckoutBinding } from '../packages/vault/src/evidence-envelope.mjs';
@@ -151,6 +152,13 @@ export function runVerify(argv) {
151
152
  process.stderr.write(`wendkeep verify: ${error.code || 'WENDKEEP_EVIDENCE_BINDING_FAILED'}: ${error.message}\n`);
152
153
  process.exit(2);
153
154
  }
155
+ let externalArtifacts;
156
+ try {
157
+ externalArtifacts = collectBridgeArtifactEvidence({ projectRoot, tasks, sensors: evidence });
158
+ } catch (error) {
159
+ process.stderr.write(`wendkeep verify: ${error.code || 'BRIDGE_ARTIFACT_COLLECTION_FAILED'}: ${error.message}\n`);
160
+ process.exit(2);
161
+ }
154
162
  const envelope = buildEvidenceEnvelope({
155
163
  identity,
156
164
  changeSlug: slug,
@@ -159,6 +167,7 @@ export function runVerify(argv) {
159
167
  effectiveSpecSha256: `sha256:${effective.hash}`,
160
168
  sensorConfigSha256: sensorConfigSha256(sensors, ids),
161
169
  sensors: evidence,
170
+ externalArtifacts,
162
171
  tddAttestations,
163
172
  startedAt,
164
173
  finishedAt: new Date().toISOString(),