instar 1.3.310 → 1.3.311
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +32 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +17 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/BootSelfKnowledge.d.ts +121 -0
- package/dist/core/BootSelfKnowledge.d.ts.map +1 -0
- package/dist/core/BootSelfKnowledge.js +285 -0
- package/dist/core/BootSelfKnowledge.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +104 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/types.d.ts +26 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +8 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +135 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +72 -64
- package/src/scaffold/templates.ts +8 -0
- package/src/templates/scripts/secret-get.mjs +143 -0
- package/upgrades/1.3.311.md +20 -0
- package/upgrades/side-effects/session-boot-self-knowledge.md +78 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* secret-get.mjs — read ONE secret value from the agent's encrypted SecretStore
|
|
4
|
+
* vault and stream it to stdout for piping. The retrieval affordance promised by
|
|
5
|
+
* the Session Boot Self-Knowledge block (docs/specs/session-boot-self-knowledge.md):
|
|
6
|
+
* a session that boots knowing `github_token` is in the vault uses THIS script to
|
|
7
|
+
* fetch it — never `node -e` ad-hoc reads, never asking the user to re-send it.
|
|
8
|
+
*
|
|
9
|
+
* Containment contract (sibling of secret-drop-retrieve.mjs, same rules):
|
|
10
|
+
* - The VALUE goes to stdout ONLY — single write, no trailing newline — so it
|
|
11
|
+
* can pipe straight into the consuming command without ever being echoed
|
|
12
|
+
* into a terminal, chat, or transcript.
|
|
13
|
+
* - ALL diagnostics go to stderr and are limited to key NAMES, lengths, and
|
|
14
|
+
* error categories — never values.
|
|
15
|
+
* - On ANY error, stdout receives ZERO value bytes (stderr-only, non-zero exit).
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node .instar/scripts/secret-get.mjs <keyPath>
|
|
19
|
+
* → prints the value to stdout. Pipe it: `... github_token | gh auth login --with-token`
|
|
20
|
+
*
|
|
21
|
+
* node .instar/scripts/secret-get.mjs --names
|
|
22
|
+
* → prints vault key paths + value lengths to stderr; nothing to stdout.
|
|
23
|
+
*
|
|
24
|
+
* node .instar/scripts/secret-get.mjs <keyPath> --run -- <cmd...>
|
|
25
|
+
* → runs <cmd> with the value piped to its stdin (atomic handoff — the value
|
|
26
|
+
* never touches a shell variable or the transcript). Exits with <cmd>'s code.
|
|
27
|
+
*
|
|
28
|
+
* Vault access:
|
|
29
|
+
* Loads the instar SecretStore implementation from the local install and reads
|
|
30
|
+
* the vault at `.instar/secrets/config.secrets.enc` relative to cwd (run from
|
|
31
|
+
* the agent home). Keychain-backed master key by default — the same read path
|
|
32
|
+
* the server uses.
|
|
33
|
+
*
|
|
34
|
+
* Exit codes:
|
|
35
|
+
* 0 — value printed (or --run command succeeded, or --names listed)
|
|
36
|
+
* 1 — key not found, vault absent/undecryptable, or --run command failed
|
|
37
|
+
* 2 — usage error (missing args, cannot resolve the instar dist)
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import * as fs from 'node:fs';
|
|
41
|
+
import * as path from 'node:path';
|
|
42
|
+
import { spawnSync } from 'node:child_process';
|
|
43
|
+
import { createRequire } from 'node:module';
|
|
44
|
+
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
|
|
47
|
+
// `<keyPath> --run -- <cmd...>`: everything after the first `--` (which must
|
|
48
|
+
// follow `--run`) is the command to receive the value on stdin.
|
|
49
|
+
const runIdx = args.indexOf('--run');
|
|
50
|
+
const dashIdx = args.indexOf('--');
|
|
51
|
+
let runCmd = null;
|
|
52
|
+
if (runIdx !== -1) {
|
|
53
|
+
if (dashIdx === -1 || dashIdx < runIdx || dashIdx === args.length - 1) {
|
|
54
|
+
process.stderr.write('usage: secret-get.mjs <keyPath> --run -- <cmd...>\n');
|
|
55
|
+
process.exit(2);
|
|
56
|
+
}
|
|
57
|
+
runCmd = args.slice(dashIdx + 1);
|
|
58
|
+
}
|
|
59
|
+
const positional = (dashIdx === -1 ? args : args.slice(0, dashIdx)).filter((a) => !a.startsWith('--'));
|
|
60
|
+
const namesOnly = args.includes('--names');
|
|
61
|
+
const keyPath = positional[0];
|
|
62
|
+
|
|
63
|
+
if (!namesOnly && !keyPath) {
|
|
64
|
+
process.stderr.write('usage: secret-get.mjs <keyPath> | secret-get.mjs --names\n');
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Resolve the instar dist: deployed agents run from the shadow-install; the dev
|
|
69
|
+
// checkout falls back to its own dist. Never print resolution paths on success.
|
|
70
|
+
const require = createRequire(import.meta.url);
|
|
71
|
+
const candidates = [
|
|
72
|
+
path.resolve('.instar/shadow-install/node_modules/instar/dist/core/SecretStore.js'),
|
|
73
|
+
path.resolve('dist/core/SecretStore.js'),
|
|
74
|
+
];
|
|
75
|
+
let SecretStore = null;
|
|
76
|
+
for (const c of candidates) {
|
|
77
|
+
if (fs.existsSync(c)) {
|
|
78
|
+
try {
|
|
79
|
+
({ SecretStore } = require(c));
|
|
80
|
+
break;
|
|
81
|
+
} catch {
|
|
82
|
+
// try the next candidate; details are not value-bearing but stay off stdout
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!SecretStore) {
|
|
87
|
+
process.stderr.write('secret-get: cannot resolve the instar SecretStore module (run from the agent home)\n');
|
|
88
|
+
process.exit(2);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const stateDir = path.resolve('.instar');
|
|
92
|
+
if (!fs.existsSync(path.join(stateDir, 'secrets', 'config.secrets.enc'))) {
|
|
93
|
+
process.stderr.write('secret-get: no vault on this machine (.instar/secrets/config.secrets.enc absent)\n');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let secrets;
|
|
98
|
+
try {
|
|
99
|
+
secrets = new SecretStore({ stateDir }).read();
|
|
100
|
+
} catch {
|
|
101
|
+
process.stderr.write(
|
|
102
|
+
'secret-get: vault exists but could not be decrypted (master-key mismatch?). ' +
|
|
103
|
+
'Do NOT repair/rotate/delete — surface to the operator.\n',
|
|
104
|
+
);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Flatten to dot-notation leaves — names + lengths only, mirroring the vault's
|
|
109
|
+
// own get/set addressing. Values never leave this function except via stdout.
|
|
110
|
+
function leaves(obj, prefix = '') {
|
|
111
|
+
const out = [];
|
|
112
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
113
|
+
const p = prefix ? `${prefix}.${k}` : k;
|
|
114
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) out.push(...leaves(v, p));
|
|
115
|
+
else out.push([p, v]);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
const entries = leaves(secrets);
|
|
120
|
+
|
|
121
|
+
if (namesOnly) {
|
|
122
|
+
for (const [name, value] of entries) {
|
|
123
|
+
process.stderr.write(`${name} (${String(value ?? '').length} chars)\n`);
|
|
124
|
+
}
|
|
125
|
+
if (entries.length === 0) process.stderr.write('(vault is empty)\n');
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const hit = entries.find(([name]) => name === keyPath);
|
|
130
|
+
if (!hit) {
|
|
131
|
+
process.stderr.write(`secret-get: no key "${keyPath}" in the vault. Known keys:\n`);
|
|
132
|
+
for (const [name] of entries) process.stderr.write(` ${name}\n`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
const value = typeof hit[1] === 'string' ? hit[1] : JSON.stringify(hit[1]);
|
|
136
|
+
|
|
137
|
+
if (runCmd) {
|
|
138
|
+
const r = spawnSync(runCmd[0], runCmd.slice(1), { input: value, stdio: ['pipe', 'inherit', 'inherit'] });
|
|
139
|
+
process.exit(r.status ?? 1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
process.stdout.write(value);
|
|
143
|
+
process.exit(0);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
New `src/core/BootSelfKnowledge.ts` (block builder: sanitization, depth-2 collapse, alphabetical + capped + byte-bounded rendering, mtime+size-keyed module cache); three routes in `routes.ts`; one fetch block in `getSessionStartHook()`; `selfKnowledge` config surface (defaults backfilled by `migrateConfig`, `enabled` left unset for the developmentAgent gate); CLAUDE.md template section + `migrateClaudeMd` parity; `secret-get.mjs` shipped via `migrateScripts` + init. Spec: `docs/specs/session-boot-self-knowledge.md` (converged, 3 iterations, cross-model codex-cli:gpt-5.5).
|
|
9
|
+
|
|
10
|
+
## What to Tell Your User
|
|
11
|
+
|
|
12
|
+
Nothing user-visible yet — the feature ships dark on the fleet (live on the development agent for the bake). When the fleet flip lands it gets its own note: "your agent now remembers what credentials it holds across sessions — it won't ask you to re-send a key it already has."
|
|
13
|
+
|
|
14
|
+
## Summary of New Capabilities
|
|
15
|
+
|
|
16
|
+
- `GET /self-knowledge/session-context` (Bearer; `enabled ?? !!developmentAgent`): a bounded, sanitized `<session-self-knowledge>` block with the agent's vault secret NAMES (never values; same `secretKeyPaths()` derivation as `/secrets/sync-status`, depth-capped) + self-asserted operational facts. `?full=1` bypasses the display caps. A vault that exists but won't decrypt is reported honestly as DECRYPT-FAILED with hands-off guidance — never as an empty vault.
|
|
17
|
+
- `POST/DELETE /self-knowledge/facts`: agent-driven writer for durable per-machine operational facts (auto-stamped `{fact, updatedAt, machine}`; duplicate/cap/ambiguity guarded; atomic temp+rename config write).
|
|
18
|
+
- `.instar/scripts/secret-get.mjs` (always-overwrite installed): hardened vault retrieval — value streams to stdout for piping straight into the consuming command, names/diagnostics to stderr, zero value bytes on any error path.
|
|
19
|
+
- The session-start hook injects the block at every boot (fail-open: dark/unreachable/version-skew → silent skip), placed after the org-intent and preferences blocks.
|
|
20
|
+
- Structural test guard: `MasterKeyManager` forces the file key under vitest — no test can ever read or overwrite the machine-global OS keychain master key again (the 2026-06-05 bifurcated-key incident class is closed).
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Side-Effects Review — Session Boot Self-Knowledge
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `session-boot-self-knowledge`
|
|
4
|
+
**Date:** `2026-06-05`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `spec-converge multi-reviewer panel (3 rounds: security/adversarial/integration/scalability/lessons-aware internal + Standards-Conformance Gate + codex-cli:gpt-5.5 external each round)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds the boot self-knowledge block: a bounded, sanitized `<session-self-knowledge>` context block (vault secret NAMES — never values — plus self-asserted operational facts) built server-side by the new `src/core/BootSelfKnowledge.ts`, served by `GET /self-knowledge/session-context` (with `?full=1`), written by `POST/DELETE /self-knowledge/facts`, injected by a new fetch block in `getSessionStartHook()`, retrieved-from by the new hardened `secret-get.mjs` script, and configured by the new `InstarConfig.selfKnowledge` surface (defaults via `ConfigDefaults` + `migrateConfig`; CLAUDE.md template + `migrateClaudeMd`; script via `migrateScripts` + init). Includes the structural `MasterKeyManager` VITEST constructor guard. Files: `src/core/BootSelfKnowledge.ts` (new), `src/core/SecretStore.ts`, `src/server/routes.ts`, `src/core/PostUpdateMigrator.ts`, `src/core/types.ts`, `src/config/ConfigDefaults.ts`, `src/scaffold/templates.ts`, `src/commands/init.ts`, `src/templates/scripts/secret-get.mjs` (new), plus 4 test files, the spec + ELI16 + convergence report, and the release fragment. Spec: `docs/specs/session-boot-self-knowledge.md` (converged + approved).
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
This change adds NO decision point with blocking authority — it is a pure signal producer (read-only context injection; per `docs/signal-vs-authority.md`). Conditionals it adds are availability switches and writer-input validation, not behavior gates:
|
|
15
|
+
|
|
16
|
+
- `GET /self-knowledge/session-context` enabled-resolution (`enabled ?? !!developmentAgent`) — add — availability switch (graduated rollout), not a behavior gate.
|
|
17
|
+
- Facts writer validation (400 empty/oversize; 409 duplicate/cap/ambiguous/expect-mismatch) — add — input validation on an agent-driven write surface.
|
|
18
|
+
- Pass-through: SecretStore read path (read-only), session-start hook (additive fetch block, fail-open), config write path (new atomic helper for one array).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Over-block
|
|
23
|
+
|
|
24
|
+
The enabled-resolution 503s the route on fleet agents (flag unset, `developmentAgent` false) — by design (graduated rollout), not an over-block: the hook fail-opens silently. The facts writer rejects: empty/oversize facts (legitimate long facts >256 chars must be split — accepted cost, keeps the boot block bounded), exact duplicates, adds past the 50-fact cap, ambiguous `match` deletes, and stale `index+expect` deletes. Each 4xx carries an actionable message. No legitimate session-context READ is ever rejected beyond auth + the availability switch. No other block/allow surface.
|
|
25
|
+
|
|
26
|
+
## 2. Under-block
|
|
27
|
+
|
|
28
|
+
- A fact that is misleading-but-validly-shaped (≤256 chars, unique) is stored and injected every boot — mitigated by the self-asserted/unverified labeling, per-index render for one-call removal, and the per-serve audit line; residual risk accepted for v1 and explicitly watched during the bake (spec §Threat model).
|
|
29
|
+
- The last-writer-wins window between the facts writer and the pre-existing NON-atomic config writers (PATCH /config, telemetry) remains — bounded to the handler's microseconds by re-read-before-write, pinned by the interleaving migration test; accepted and documented.
|
|
30
|
+
- Names already written into past transcripts are not retroactively scrubbed if the feature is later disabled.
|
|
31
|
+
|
|
32
|
+
## 3. Level-of-abstraction fit
|
|
33
|
+
|
|
34
|
+
Right layer. The names derivation stays in `secretKeyPaths()` (shared with `/secrets/sync-status` — no logic fork); the block-building/presentation is a new module rather than overloading sync-status (which 503s when secret-sync is dark — wrong availability semantics for a boot surface) or the SelfKnowledgeTree (LLM search over AGENT.md — different system, noted in code comments on both). The hook injection rides the existing org-intent/preferences pattern rather than inventing a new injection mechanism. Rejected alternatives (pull surfaces: /capabilities, MCP resources, memory files) are analyzed in the spec — the failure class is "agent doesn't know to look," which only push-at-boot removes.
|
|
35
|
+
|
|
36
|
+
## 4. Signal vs authority compliance
|
|
37
|
+
|
|
38
|
+
Compliant — pure signal producer. The block is wrapped in an envelope that explicitly subordinates it to org-intent constraints, safety rules, and user instructions. The guidance line is signal-shaped ("retrieve rather than re-ask, unless you have evidence it is invalid") not absolute. A deterministic block on credential re-asks was considered and rejected as brittle-authority (spec §Why guidance stays a signal); the designed escalation if the bake shows non-compliance is a smart-gate signal feed, not a regex block. The VITEST keychain guard is a test-environment safety rail, not a runtime decision point.
|
|
39
|
+
|
|
40
|
+
## 5. Interactions
|
|
41
|
+
|
|
42
|
+
- Coexists with the org-intent and preferences injections: placed after both in the hook (authoritative contract first); envelope states precedence. No shadowing — different routes, different envelopes.
|
|
43
|
+
- `/self-knowledge/*` namespace shared with the SelfKnowledgeTree routes (search/validate/health) — no path collision; comments mark which system serves which path.
|
|
44
|
+
- The names cache keys on the vault file path + (mtimeMs,size); secret-sync writes go through the same in-process server, so a peer-pushed secret invalidates the cache on its atomic write. No double-fire: the route is the only consumer.
|
|
45
|
+
- `migrateConfig`'s recursive add-missing merge interacts with operator-set `selfKnowledge` values — partial-override case pinned by test.
|
|
46
|
+
- The VITEST guard interacts with every existing test that constructs SecretStores — it can only make them SAFER (file-key instead of keychain); tests that explicitly pass `forceFileKey: true` (e.g. SecretMigrator's) are unchanged.
|
|
47
|
+
|
|
48
|
+
## 6. External surfaces
|
|
49
|
+
|
|
50
|
+
- Vault key NAMES become visible in: the Bearer-gated route response, the agent's session context, and therefore on-disk session transcripts (which can travel further than vaults — debug bundles, provider retention; spec §Threat model). This is the feature's one genuinely new exposure and the reason it ships dark-fleet with the live flip as an explicit follow-up decision.
|
|
51
|
+
- No cross-agent or cross-machine surface changes: facts are per-machine (config doesn't sync); names reflect the local vault (which secret-sync may populate). Nothing here changes timing-sensitive behavior visible to other systems; the hook fetch is fail-open with `--max-time 4 --connect-timeout 1`.
|
|
52
|
+
|
|
53
|
+
## 7. Rollback cost
|
|
54
|
+
|
|
55
|
+
Low. Per-agent: `selfKnowledge.sessionContext.enabled: false` (route 503s, hook silently skips — no restart needed; the flag is fresh-read). Fleet: revert the PR — no data formats change; the only state this feature writes is the `operationalFacts` array via explicit calls, which survives or is hand-removable. The VITEST guard's rollback is one constructor line. Names already in transcripts are the only non-revertible residue (documented).
|
|
56
|
+
|
|
57
|
+
## Deferred / follow-ups (all tracked)
|
|
58
|
+
|
|
59
|
+
- Live-fleet flip (`enabled: true` in ConfigDefaults) — rides PR #800's merge or explicit approver direction (spec §Availability Resolution rule; the approver was asked directly in the approval request).
|
|
60
|
+
- Session-start hook's pre-existing uncapped sibling curls — framework-issues ledger `session-start-hook-uncapped-curls`.
|
|
61
|
+
- `/secrets/sync-status` rendering decrypt-failure as an empty vault — framework-issues ledger `sync-status-decrypt-fail-reads-empty`.
|
|
62
|
+
- Per-agent keychain accounts + key-id header + dual-key read fallback — pre-existing commitment from the 2026-06-05 incident (CMT lineage in topic 13481), unchanged by this PR.
|
|
63
|
+
|
|
64
|
+
## Post-review fix round (fresh-eyes code review, 2026-06-05)
|
|
65
|
+
|
|
66
|
+
The independent code review of the feature commit found ONE real bug, fixed before PR: the names cache (keyed on the vault file) cached the `decrypt-failed` outcome — but a decrypt failure is almost always a MASTER-KEY problem (a separate file the cache key cannot see), so a recovered key kept serving the stale hands-off warning until a restart. Fix: only the healthy outcome is cached; a failed state is re-tried on every read (cheap relative to lying about recovery). Plus hardening: backticks are stripped from rendered names/facts (a hostile name can no longer break the inline-code span). Two regression tests added (decrypt-recovery-heals-without-restart; backtick-inertness).
|
|
67
|
+
|
|
68
|
+
## CI-fix round (post-PR, 2026-06-05)
|
|
69
|
+
|
|
70
|
+
Three CI failures owned per Zero-Failure: (1) `ConversationStore.test.ts` time-bomb — the test anchored retirement at fixed `2026-05-30`, and its 25h-stale entries crossed the store's 7-day expiry on 2026-06-05 (main's last CI run squeaked under the boundary by an hour; this PR's run detonated it) — re-anchored at real now, semantics unchanged; (2) no-silent-fallbacks ratchet — the four new BootSelfKnowledge catch blocks annotated `@silent-fallback-ok` with per-catch justifications (never a baseline bump); (3) docs-coverage route floor — the three new routes documented in the site API reference + a new features page (which also satisfies the route ratchet for the existing tree routes' namespace).
|
|
71
|
+
|
|
72
|
+
## Compaction-parity round (approver design review, 2026-06-05)
|
|
73
|
+
|
|
74
|
+
Justin's review surfaced the long-session gap: the block injected at session start survives compaction only if the summary carries it. Fixed in-PR: the compaction-recovery hook now carries the same fail-open fetch (re-injection after every compaction — refreshed, not merely preserved), with a Phase-3 e2e running the real compact-hook block against a live server. Collateral: org-intent + preferences share the boot-only gap (filed as `session-context-injectors-lack-compaction-parity`); a "Compaction Parity" constitution amendment is proposed separately. His scale concern is answered by the existing hard 2KB byte-cap (pointer-not-payload design); the AGGREGATE boot-budget concern across all injectors filed as `boot-context-aggregate-budget`.
|
|
75
|
+
|
|
76
|
+
## Post-merge-conflict round (2026-06-05 AM)
|
|
77
|
+
|
|
78
|
+
Rebase onto the post-#848 main + CI surfaced the feature-delivery-completeness registry: the new CLAUDE.md section is now tracked in `featureSections` AND mirrored to the framework-shadow markers (`migrateFrameworkShadowCapabilities`) — Codex/Gemini agents learn the capability too (the Secret Drop lesson: an unshadowed capability gets improvised around). Local pre-push had skipped the smoke ("CI is the authority"), which is why CI caught it.
|