delimit-cli 4.5.12 → 4.6.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.
- package/CHANGELOG.md +45 -0
- package/README.md +9 -8
- package/bin/delimit-cli.js +162 -1
- package/bin/delimit-setup.js +46 -6
- package/gateway/ai/_compile_status.py +154 -0
- package/gateway/ai/agent_dispatch.py +36 -0
- package/gateway/ai/backends/tools_infra.py +150 -10
- package/gateway/ai/daemon.py +10 -0
- package/gateway/ai/daily_digest.py +1 -2
- package/gateway/ai/delimit_daemon.py +67 -0
- package/gateway/ai/dispatch_gate.py +399 -0
- package/gateway/ai/hot_reload.py +1 -2
- package/gateway/ai/led193_daemon/executor.py +9 -0
- package/gateway/ai/ledger_manager.py +9 -0
- package/gateway/ai/license_core.cpython-310-x86_64-linux-gnu.so +0 -0
- package/gateway/ai/license_core.pyi +17 -19
- package/gateway/ai/notify.py +39 -0
- package/gateway/ai/outreach_substantive.py +676 -0
- package/gateway/ai/reaper.py +70 -0
- package/gateway/ai/reddit_scanner.py +10 -5
- package/gateway/ai/sensing/schema.py +1 -1
- package/gateway/ai/sensing/signal_store.py +0 -1
- package/gateway/ai/server.py +5171 -1462
- package/gateway/ai/social_capability/fit_floor.py +114 -12
- package/gateway/ai/tdqs_lint.py +611 -0
- package/gateway/ai/usage_allowlist.py +198 -0
- package/gateway/ai/workers/base.py +2 -2
- package/gateway/ai/workers/executor.py +32 -3
- package/gateway/ai/workers/outreach_drafter.py +0 -1
- package/gateway/ai/workers/pr_drafter.py +0 -1
- package/gateway/ai/x_ranker.py +12 -2
- package/gateway/core/json_schema_diff.py +25 -1
- package/lib/auth-signin.js +136 -0
- package/lib/auth-signout.js +169 -0
- package/lib/delimit-template.js +11 -0
- package/lib/migration-2092-banner.js +213 -0
- package/package.json +3 -3
- package/server.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [4.6.0] - 2026-05-15
|
|
5
|
+
|
|
6
|
+
### Added — Codex CLI + Gemini CLI auto-trigger directives (LED-1399)
|
|
7
|
+
|
|
8
|
+
`delimit setup` now installs governance directives at the **verified-effective**
|
|
9
|
+
locations for Codex CLI and Gemini CLI, closing the gap where users of those
|
|
10
|
+
CLIs got the delimit MCP server wired but **not** the auto-trigger behaviors
|
|
11
|
+
(`delimit_test_smoke` after edits, `delimit_repo_diagnose` before commit, etc.)
|
|
12
|
+
that Claude Code users already get.
|
|
13
|
+
|
|
14
|
+
**What changed:**
|
|
15
|
+
- **Codex CLI**: writes `~/AGENTS.md` (verified against the Codex binary spec —
|
|
16
|
+
Codex auto-loads `AGENTS.md` "from CWD up to the root"). The previous
|
|
17
|
+
`~/.codex/instructions.md` write was dead code; Codex never read that path.
|
|
18
|
+
- **Gemini CLI**: writes `~/.gemini/GEMINI.md` (verified against the gemini-cli
|
|
19
|
+
bundle: `return ["GEMINI.md"]` is the discovery list; the global tier is
|
|
20
|
+
`~/.gemini/GEMINI.md`).
|
|
21
|
+
- Both gated on the respective CLI being installed (directory exists OR
|
|
22
|
+
`which` finds the binary). Both use the existing managed-section markers
|
|
23
|
+
(`<!-- delimit:start -->` / `<!-- delimit:end -->`) so user content is
|
|
24
|
+
preserved on upgrade per the LED-1257 customer-protection rule.
|
|
25
|
+
|
|
26
|
+
**Backwards-compat:** existing `~/.codex/instructions.md` files from prior
|
|
27
|
+
installs are NOT removed (harmless dead config; deleting could clobber user
|
|
28
|
+
customizations around our managed section).
|
|
29
|
+
|
|
30
|
+
### Other changes
|
|
31
|
+
|
|
32
|
+
- **server.json merge-gate framing** (LED-2178): formal merge-gate framing in
|
|
33
|
+
the public server.json descriptor.
|
|
34
|
+
- **Memory Rules in `delimit init` template** (STR-143): the init-generated
|
|
35
|
+
CLAUDE.md now includes the canonical Memory Rules section.
|
|
36
|
+
- Documentation refreshes: cross-agent-handoff worked example surfaced on README,
|
|
37
|
+
test-count badge bumped, misleading version stamps removed.
|
|
38
|
+
|
|
39
|
+
### Known issue (pre-existing, fix tracked)
|
|
40
|
+
|
|
41
|
+
- **`delimit attest mcp` exit codes** (LED-1403): on tool error (e.g. no
|
|
42
|
+
lockfile → npm audit unavailable) and unknown attestation kind, the CLI
|
|
43
|
+
currently returns exit 1 instead of the expected exit 2. CI/CD pipelines
|
|
44
|
+
that gate on tier-2 (treating "tool unavailable" as a hard error vs.
|
|
45
|
+
"fail" which is a soft check) should pin this expectation. Tracked for
|
|
46
|
+
fix in a follow-up release.
|
|
47
|
+
|
|
48
|
+
|
|
4
49
|
## [4.5.2] - 2026-05-02
|
|
5
50
|
|
|
6
51
|
### Hardened — `postinstall.js` never-block-install guard (LED-1188)
|
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
Wrap any AI coding assistant (Claude Code, Codex, Cursor, Gemini CLI) with a governance chain that runs your gates, records what changed, and signs a replayable receipt for every merge.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/delimit-cli)
|
|
8
|
-
[](https://github.com/delimit-ai/delimit-mcp-server)
|
|
9
9
|
[](https://github.com/marketplace/actions/delimit-api-governance)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
|
-
[](https://glama.ai/mcp/servers/delimit-ai/delimit-mcp-server/score)
|
|
12
12
|
|
|
13
13
|
```console
|
|
14
14
|
$ delimit wrap -- claude "fix the flaky test in tests/api.spec.ts"
|
|
@@ -32,8 +32,9 @@ Every wrapped run emits a `delimit.attestation.v1` bundle: repo head before/afte
|
|
|
32
32
|
|
|
33
33
|
## See it in action
|
|
34
34
|
|
|
35
|
-
Worked
|
|
35
|
+
Worked examples, real artifacts, every claim verifiable:
|
|
36
36
|
|
|
37
|
+
- **[Cross-agent handoff: one artifact, four CLIs](https://delimit.ai/reports/cross-agent-handoff)**: structured session handoff between Claude Code, Codex, Cursor, and Gemini CLI through a single JSON record at `~/.delimit/sessions/`. Persistent context across sessions, same artifact across four CLIs.
|
|
37
38
|
- **[cal.com v2 API attestation](https://delimit.ai/reports/cal-com-v2-attestation)**: full diff, signed verdict, replayable bundle. Runs the same chain you get on day one.
|
|
38
39
|
|
|
39
40
|
For the schema and signing methodology behind every report, see **[delimit.ai/methodology/mcp-attestation](https://delimit.ai/methodology/mcp-attestation)**.
|
|
@@ -86,7 +87,7 @@ npx delimit-cli init # Sets up governance + drift baseline
|
|
|
86
87
|
|
|
87
88
|
---
|
|
88
89
|
|
|
89
|
-
## What's New
|
|
90
|
+
## What's New
|
|
90
91
|
|
|
91
92
|
*Gate every AI-assisted invocation. Ship the receipts.*
|
|
92
93
|
|
|
@@ -114,9 +115,9 @@ delimit ai-sbom -o ./ai-sbom.json
|
|
|
114
115
|
# → components: 4 models detected, 187 gates run
|
|
115
116
|
```
|
|
116
117
|
|
|
117
|
-
##
|
|
118
|
+
## Earlier releases
|
|
118
119
|
|
|
119
|
-
*The highest state of AI governance.*
|
|
120
|
+
*The highest state of AI governance — earlier features still active.*
|
|
120
121
|
|
|
121
122
|
- **`delimit doctor`** -- 14 prescriptive diagnostics. Every failure prints the exact command to fix it. `--ci` for pipelines, `--fix` for auto-repair.
|
|
122
123
|
- **`delimit simulate`** -- policy dry-run. See what would be blocked before you commit. The `terraform plan` for API governance.
|
|
@@ -257,7 +258,7 @@ npx delimit-cli models --status # Show current model config
|
|
|
257
258
|
npx delimit-cli status # Compact dashboard of your Delimit setup
|
|
258
259
|
npx delimit-cli doctor # Check setup health
|
|
259
260
|
npx delimit-cli uninstall --dry-run # Preview removal
|
|
260
|
-
npx delimit-cli wrap -- claude -p "..." # Gate any AI-assisted CLI + signed attestation
|
|
261
|
+
npx delimit-cli wrap -- claude -p "..." # Gate any AI-assisted CLI + signed attestation
|
|
261
262
|
npx delimit-cli wrap --max-time 60 -- codex "..."# With kill switch + handoff on timeout
|
|
262
263
|
npx delimit-cli trust-page -o ./trust # Render attestations into a static trust page
|
|
263
264
|
npx delimit-cli ai-sbom -o ./ai-sbom.json # Build a CycloneDX-AI bill of materials
|
|
@@ -419,4 +420,4 @@ If you spot another code path that could phone home without disclosure, file an
|
|
|
419
420
|
- [npm](https://www.npmjs.com/package/delimit-cli) -- CLI package
|
|
420
421
|
- [Pricing](https://delimit.ai/pricing) -- free tier + Pro
|
|
421
422
|
|
|
422
|
-
MIT License
|
|
423
|
+
MIT License
|
package/bin/delimit-cli.js
CHANGED
|
@@ -69,7 +69,7 @@ function normalizeNaturalLanguageArgs(argv) {
|
|
|
69
69
|
const explicitCommands = new Set([
|
|
70
70
|
'install', 'mode', 'status', 'session', 'build', 'ask', 'policy', 'auth', 'audit',
|
|
71
71
|
'explain-decision', 'uninstall', 'proxy', 'hook', 'version', 'vault', 'deliberate',
|
|
72
|
-
'remember', 'recall', 'forget', 'report'
|
|
72
|
+
'remember', 'recall', 'forget', 'report', 'signin', 'signout', 'activate'
|
|
73
73
|
]);
|
|
74
74
|
if (explicitCommands.has((raw[0] || '').toLowerCase())) {
|
|
75
75
|
return raw;
|
|
@@ -5653,6 +5653,140 @@ program
|
|
|
5653
5653
|
} catch {}
|
|
5654
5654
|
});
|
|
5655
5655
|
|
|
5656
|
+
// ---------------------------------------------------------------------------
|
|
5657
|
+
// LED-2100: `delimit signin` — capture delimit.ai OAuth bearer token so the
|
|
5658
|
+
// hosted-deliberation tier (LED-2092) can authenticate from the CLI. The
|
|
5659
|
+
// token is written to ~/.delimit/auth.json (mode 0600) where the gateway
|
|
5660
|
+
// reads it via deliberation.py::_read_oauth_token. Existing keys in
|
|
5661
|
+
// auth.json (e.g. from `delimit auth`) are preserved.
|
|
5662
|
+
//
|
|
5663
|
+
// Surface contract (do not change without coordinating with the gateway):
|
|
5664
|
+
// - Writes `delimit_token` and `access_token` (same value, dual-key for
|
|
5665
|
+
// compatibility with the gateway resolver's preferred-then-fallback read)
|
|
5666
|
+
// - Writes `signed_in_at` (ISO8601) and `email` (when supplied)
|
|
5667
|
+
// - Distinct from `delimit activate <key>` which writes license.json for
|
|
5668
|
+
// the Pro license-key flow. Sign-in is OAuth-bearer-token only.
|
|
5669
|
+
//
|
|
5670
|
+
// Default flow is paste-token (works in headless / SSH sessions). The
|
|
5671
|
+
// browser-callback flow is a follow-up once delimit.ai/account/cli ships
|
|
5672
|
+
// the cli-aware redirect endpoint.
|
|
5673
|
+
// ---------------------------------------------------------------------------
|
|
5674
|
+
program
|
|
5675
|
+
.command('signin')
|
|
5676
|
+
.description('Sign in to delimit.ai to enable hosted multi-model deliberation')
|
|
5677
|
+
.option('--token <token>', 'Provide the bearer token directly (skip prompt)')
|
|
5678
|
+
.option('--email <email>', 'Associate the token with an email address')
|
|
5679
|
+
.option('--status', 'Print current sign-in status without changes')
|
|
5680
|
+
.action(async (options) => {
|
|
5681
|
+
const { writeAuthToken, readCurrentToken, authFilePath } = require('../lib/auth-signin');
|
|
5682
|
+
|
|
5683
|
+
if (options.status) {
|
|
5684
|
+
const token = readCurrentToken();
|
|
5685
|
+
if (!token) {
|
|
5686
|
+
console.log(chalk.yellow(' Not signed in.'));
|
|
5687
|
+
console.log(chalk.dim(` Run: delimit signin`));
|
|
5688
|
+
process.exit(1);
|
|
5689
|
+
}
|
|
5690
|
+
console.log(chalk.green(' Signed in.'));
|
|
5691
|
+
console.log(chalk.dim(` Auth file: ${authFilePath()}`));
|
|
5692
|
+
return;
|
|
5693
|
+
}
|
|
5694
|
+
|
|
5695
|
+
let token = (options.token || process.env.DELIMIT_AUTH_TOKEN || '').trim();
|
|
5696
|
+
let email = (options.email || '').trim();
|
|
5697
|
+
|
|
5698
|
+
if (!token) {
|
|
5699
|
+
console.log(chalk.blue.bold('\n Delimit sign-in\n'));
|
|
5700
|
+
console.log(' 1. Open: ' + chalk.cyan('https://delimit.ai/account/cli'));
|
|
5701
|
+
console.log(' 2. Sign in and copy your CLI token');
|
|
5702
|
+
console.log(' 3. Paste it below\n');
|
|
5703
|
+
|
|
5704
|
+
const answers = await inquirer.prompt([
|
|
5705
|
+
{
|
|
5706
|
+
type: 'password',
|
|
5707
|
+
name: 'token',
|
|
5708
|
+
message: 'Paste token:',
|
|
5709
|
+
mask: '*',
|
|
5710
|
+
validate: (input) => {
|
|
5711
|
+
const v = (input || '').trim();
|
|
5712
|
+
if (!v) return 'Token cannot be empty.';
|
|
5713
|
+
if (v.length < 16) return 'Token looks too short — copy the full string from delimit.ai/account/cli.';
|
|
5714
|
+
return true;
|
|
5715
|
+
},
|
|
5716
|
+
},
|
|
5717
|
+
{
|
|
5718
|
+
type: 'input',
|
|
5719
|
+
name: 'email',
|
|
5720
|
+
message: 'Email (optional, for display only):',
|
|
5721
|
+
default: '',
|
|
5722
|
+
},
|
|
5723
|
+
]);
|
|
5724
|
+
token = (answers.token || '').trim();
|
|
5725
|
+
email = email || (answers.email || '').trim();
|
|
5726
|
+
}
|
|
5727
|
+
|
|
5728
|
+
try {
|
|
5729
|
+
const result = writeAuthToken({ token, email });
|
|
5730
|
+
console.log('');
|
|
5731
|
+
if (email) {
|
|
5732
|
+
console.log(chalk.green(` Signed in as ${email}. Hosted deliberation enabled.`));
|
|
5733
|
+
} else {
|
|
5734
|
+
console.log(chalk.green(' Signed in. Hosted deliberation enabled.'));
|
|
5735
|
+
}
|
|
5736
|
+
console.log(chalk.dim(` Auth file: ${result.path} (mode 0600)`));
|
|
5737
|
+
if (result.merged) {
|
|
5738
|
+
console.log(chalk.dim(' Existing auth.json keys preserved.'));
|
|
5739
|
+
}
|
|
5740
|
+
console.log(chalk.dim(' To sign out: delimit signout'));
|
|
5741
|
+
console.log('');
|
|
5742
|
+
} catch (err) {
|
|
5743
|
+
console.error(chalk.red(` Sign-in failed: ${err.message}`));
|
|
5744
|
+
process.exit(1);
|
|
5745
|
+
}
|
|
5746
|
+
});
|
|
5747
|
+
|
|
5748
|
+
// ---------------------------------------------------------------------------
|
|
5749
|
+
// LED-2106: `delimit signout` — remove the OAuth bearer token written by
|
|
5750
|
+
// `delimit signin` (LED-2100) without clobbering the legacy bookkeeping
|
|
5751
|
+
// keys (configured / timestamp / tools) written by `delimit auth`. Today
|
|
5752
|
+
// users sign out via `rm ~/.delimit/auth.json` which wipes everything;
|
|
5753
|
+
// this command scrubs only the OAuth-related keys.
|
|
5754
|
+
//
|
|
5755
|
+
// Surface contract:
|
|
5756
|
+
// - Removes ONLY: delimit_token, access_token, signed_in_at, email
|
|
5757
|
+
// - Preserves: every other key (e.g. configured, timestamp, tools)
|
|
5758
|
+
// - Deletes auth.json entirely if it has no remaining keys after scrub
|
|
5759
|
+
// - Idempotent: safe to run when not signed in (prints "Not signed in.")
|
|
5760
|
+
// ---------------------------------------------------------------------------
|
|
5761
|
+
program
|
|
5762
|
+
.command('signout')
|
|
5763
|
+
.description('Sign out of delimit.ai (removes the hosted-deliberation token)')
|
|
5764
|
+
.action(async () => {
|
|
5765
|
+
const { removeAuthToken } = require('../lib/auth-signout');
|
|
5766
|
+
try {
|
|
5767
|
+
const result = removeAuthToken();
|
|
5768
|
+
if (!result.changed) {
|
|
5769
|
+
console.log(chalk.yellow(' Not signed in.'));
|
|
5770
|
+
return;
|
|
5771
|
+
}
|
|
5772
|
+
console.log('');
|
|
5773
|
+
if (result.email) {
|
|
5774
|
+
console.log(chalk.green(` Signed out (${result.email}).`));
|
|
5775
|
+
} else {
|
|
5776
|
+
console.log(chalk.green(' Signed out.'));
|
|
5777
|
+
}
|
|
5778
|
+
if (result.deleted) {
|
|
5779
|
+
console.log(chalk.dim(` Removed: ${result.path}`));
|
|
5780
|
+
} else {
|
|
5781
|
+
console.log(chalk.dim(` Auth file: ${result.path} (other keys preserved)`));
|
|
5782
|
+
}
|
|
5783
|
+
console.log('');
|
|
5784
|
+
} catch (err) {
|
|
5785
|
+
console.error(chalk.red(` Sign-out failed: ${err.message}`));
|
|
5786
|
+
process.exit(1);
|
|
5787
|
+
}
|
|
5788
|
+
});
|
|
5789
|
+
|
|
5656
5790
|
// ---------------------------------------------------------------------------
|
|
5657
5791
|
// LED-187: Export governance config as shareable JSON
|
|
5658
5792
|
// ---------------------------------------------------------------------------
|
|
@@ -5842,6 +5976,21 @@ program
|
|
|
5842
5976
|
.option('--mode <mode>', 'Deliberation mode: quick | dialogue | debate', 'dialogue')
|
|
5843
5977
|
.option('--question <q>', 'Question to deliberate (alternative to positional arg)')
|
|
5844
5978
|
.action(async (questionParts, options) => {
|
|
5979
|
+
// LED-2095: one-time migration banner for Free + BYOK users to
|
|
5980
|
+
// surface the LED-2092 / LED-2093 ephemeral-Free-tier change.
|
|
5981
|
+
// This fires on the first deliberation-adjacent call after the
|
|
5982
|
+
// upgrade and never again on this machine.
|
|
5983
|
+
try {
|
|
5984
|
+
const { maybeShowMigrationBanner } = require('../lib/migration-2092-banner');
|
|
5985
|
+
const banner = maybeShowMigrationBanner();
|
|
5986
|
+
if (banner.shown) {
|
|
5987
|
+
console.log(chalk.yellow(banner.text));
|
|
5988
|
+
}
|
|
5989
|
+
} catch {
|
|
5990
|
+
// Banner is purely informational; never block deliberation
|
|
5991
|
+
// because of a flag-file write or models.json read error.
|
|
5992
|
+
}
|
|
5993
|
+
|
|
5845
5994
|
const question = options.question || (questionParts.length > 0 ? questionParts.join(' ') : null);
|
|
5846
5995
|
|
|
5847
5996
|
if (options.list) {
|
|
@@ -6058,6 +6207,18 @@ program
|
|
|
6058
6207
|
.description('Configure deliberation model API keys (BYOK)')
|
|
6059
6208
|
.option('--status', 'Show current model configuration (non-interactive)')
|
|
6060
6209
|
.action(async (options) => {
|
|
6210
|
+
// LED-2095: deliberation-adjacent surface — also a valid trigger
|
|
6211
|
+
// point for the one-time migration banner. See lib/migration-2092-banner.js.
|
|
6212
|
+
try {
|
|
6213
|
+
const { maybeShowMigrationBanner } = require('../lib/migration-2092-banner');
|
|
6214
|
+
const banner = maybeShowMigrationBanner();
|
|
6215
|
+
if (banner.shown) {
|
|
6216
|
+
console.log(chalk.yellow(banner.text));
|
|
6217
|
+
}
|
|
6218
|
+
} catch {
|
|
6219
|
+
// Banner is informational; never block the command on banner errors.
|
|
6220
|
+
}
|
|
6221
|
+
|
|
6061
6222
|
const config = loadModelsConfig();
|
|
6062
6223
|
|
|
6063
6224
|
// --status: non-interactive output
|
package/bin/delimit-setup.js
CHANGED
|
@@ -633,12 +633,52 @@ Run full governance compliance checks. Verify security, policy compliance, evide
|
|
|
633
633
|
log(` ${dim(' CLAUDE.md already up to date')}`);
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
// Codex instructions
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
636
|
+
// Codex instructions: AGENTS.md is the file Codex CLI auto-loads
|
|
637
|
+
// ("from CWD up to the root", per Codex binary spec). The previous
|
|
638
|
+
// ~/.codex/instructions.md write was dead code — Codex never read it.
|
|
639
|
+
// LED-1399: install ~/AGENTS.md (parallels ~/CLAUDE.md) when codex CLI
|
|
640
|
+
// is present so user-home-rooted sessions pick up governance triggers.
|
|
641
|
+
const codexHome = path.join(os.homedir(), '.codex');
|
|
642
|
+
let hasCodex = fs.existsSync(codexHome);
|
|
643
|
+
if (!hasCodex) {
|
|
644
|
+
try { execSync('which codex 2>/dev/null', { stdio: 'pipe' }); hasCodex = true; } catch {}
|
|
645
|
+
}
|
|
646
|
+
if (hasCodex) {
|
|
647
|
+
const agentsMd = path.join(os.homedir(), 'AGENTS.md');
|
|
648
|
+
const agentsResult = upsertDelimitSection(agentsMd);
|
|
649
|
+
if (agentsResult.action === 'created') {
|
|
650
|
+
await logp(` ${green('✓')} Created ${agentsMd} (Codex CLI auto-loads from CWD up to home)`);
|
|
651
|
+
} else if (agentsResult.action === 'updated') {
|
|
652
|
+
await logp(` ${green('✓')} Updated Delimit section in ${agentsMd}`);
|
|
653
|
+
} else if (agentsResult.action === 'appended') {
|
|
654
|
+
await logp(` ${green('✓')} Appended Delimit section to ${agentsMd} (user content preserved)`);
|
|
655
|
+
} else {
|
|
656
|
+
log(` ${dim(' AGENTS.md already up to date')}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Gemini CLI: GEMINI.md is the auto-loaded instruction file
|
|
661
|
+
// (~/.gemini/GEMINI.md is the user-global tier per Gemini CLI bundle).
|
|
662
|
+
// LED-1399: install when Gemini CLI is present so governance triggers
|
|
663
|
+
// fire across-projects without per-repo setup.
|
|
664
|
+
const geminiHome = path.join(os.homedir(), '.gemini');
|
|
665
|
+
let hasGemini = fs.existsSync(geminiHome);
|
|
666
|
+
if (!hasGemini) {
|
|
667
|
+
try { execSync('which gemini 2>/dev/null', { stdio: 'pipe' }); hasGemini = true; } catch {}
|
|
668
|
+
}
|
|
669
|
+
if (hasGemini) {
|
|
670
|
+
// Ensure ~/.gemini exists before writing the user-global GEMINI.md.
|
|
671
|
+
try { fs.mkdirSync(geminiHome, { recursive: true, mode: 0o755 }); } catch {}
|
|
672
|
+
const geminiMd = path.join(geminiHome, 'GEMINI.md');
|
|
673
|
+
const geminiResult = upsertDelimitSection(geminiMd);
|
|
674
|
+
if (geminiResult.action === 'created') {
|
|
675
|
+
await logp(` ${green('✓')} Created ${geminiMd} (Gemini CLI user-global instructions)`);
|
|
676
|
+
} else if (geminiResult.action === 'updated') {
|
|
677
|
+
await logp(` ${green('✓')} Updated Delimit section in ${geminiMd}`);
|
|
678
|
+
} else if (geminiResult.action === 'appended') {
|
|
679
|
+
await logp(` ${green('✓')} Appended Delimit section to ${geminiMd} (user content preserved)`);
|
|
680
|
+
} else {
|
|
681
|
+
log(` ${dim(' GEMINI.md already up to date')}`);
|
|
642
682
|
}
|
|
643
683
|
}
|
|
644
684
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""LED-2087 Phase 1a — proprietary-module compilation status helper.
|
|
2
|
+
|
|
3
|
+
Customers can run on the Python source fallback (slower; no compiled-
|
|
4
|
+
attestation parity) when the platform-appropriate ``.so`` /``.pyd``
|
|
5
|
+
isn't shipped in their bundle. The 3 proprietary modules
|
|
6
|
+
(``license_core``, ``deliberation``, ``governance``) each get this
|
|
7
|
+
treatment via the LED-1259 warn-and-fallback path for ``license_core``
|
|
8
|
+
and (post-LED-2087-phase-1a) the same introspection here for the
|
|
9
|
+
other two.
|
|
10
|
+
|
|
11
|
+
This module is intentionally minimal-surface:
|
|
12
|
+
- No modification of the proprietary source modules
|
|
13
|
+
- No new MCP tool added to the customer-facing surface
|
|
14
|
+
- One INFO-level log line at gateway startup (silent on the happy
|
|
15
|
+
Linux x86_64 / py3.10 path where all three are native-loaded)
|
|
16
|
+
- Importable status helper for tests + future replay tooling
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import importlib
|
|
21
|
+
import logging
|
|
22
|
+
from typing import Dict, Iterable, List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("delimit.ai._compile_status")
|
|
25
|
+
|
|
26
|
+
# Modules audited by this helper. Each tuple is (import_name, friendly_label).
|
|
27
|
+
# ai.license_core already has the LED-1259 warn-and-fallback path in
|
|
28
|
+
# ai/license.py — we still introspect it here for the consolidated
|
|
29
|
+
# startup report.
|
|
30
|
+
PROPRIETARY_MODULES: Tuple[Tuple[str, str], ...] = (
|
|
31
|
+
("ai.license_core", "license_core"),
|
|
32
|
+
("ai.deliberation", "deliberation"),
|
|
33
|
+
("ai.governance", "governance"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Extensions Python uses for native compiled modules across platforms.
|
|
37
|
+
# .so — Linux + macOS
|
|
38
|
+
# .pyd — Windows
|
|
39
|
+
# .dylib — macOS dynamic libs (Python typically uses .so on macOS too,
|
|
40
|
+
# but include for defensive coverage)
|
|
41
|
+
_NATIVE_EXTS: Tuple[str, ...] = (".so", ".pyd", ".dylib")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_native_compiled(import_name: str) -> Optional[bool]:
|
|
45
|
+
"""Return True if the named module loaded from a native binary
|
|
46
|
+
(``.so`` / ``.pyd`` / ``.dylib``), False if it loaded from
|
|
47
|
+
``.py`` source, or None if the module isn't importable at all.
|
|
48
|
+
|
|
49
|
+
Pure introspection — no side effects beyond the import call itself.
|
|
50
|
+
The import is harmless: if the module is already imported (almost
|
|
51
|
+
always true at gateway startup) ``importlib`` returns the cached
|
|
52
|
+
module without re-executing.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
mod = importlib.import_module(import_name)
|
|
56
|
+
except ImportError:
|
|
57
|
+
return None
|
|
58
|
+
path = getattr(mod, "__file__", "") or ""
|
|
59
|
+
if not path:
|
|
60
|
+
# Some build pipelines produce modules without ``__file__``
|
|
61
|
+
# (e.g. frozen modules). Conservatively treat as "unknown but
|
|
62
|
+
# importable" — surface as None so callers don't assume either
|
|
63
|
+
# native or source state.
|
|
64
|
+
return None
|
|
65
|
+
for ext in _NATIVE_EXTS:
|
|
66
|
+
if path.endswith(ext):
|
|
67
|
+
return True
|
|
68
|
+
if path.endswith(".py"):
|
|
69
|
+
return False
|
|
70
|
+
# Unknown extension (e.g. .pyc) — conservatively treat as not-native.
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def compilation_status_report(
|
|
75
|
+
modules: Iterable[Tuple[str, str]] = PROPRIETARY_MODULES,
|
|
76
|
+
) -> Dict[str, str]:
|
|
77
|
+
"""Return ``{friendly_label: status}`` for each module.
|
|
78
|
+
|
|
79
|
+
Status values:
|
|
80
|
+
- ``"native"`` — loaded from .so / .pyd / .dylib
|
|
81
|
+
- ``"source"`` — loaded from .py (fallback path)
|
|
82
|
+
- ``"missing"`` — module not importable at all
|
|
83
|
+
- ``"unknown"`` — importable but ``__file__`` unrecognized
|
|
84
|
+
|
|
85
|
+
Used by the startup logger + tests + future status-query tooling.
|
|
86
|
+
Deterministic given the same set of imported modules + the same
|
|
87
|
+
platform (no clock-dependent state).
|
|
88
|
+
"""
|
|
89
|
+
report: Dict[str, str] = {}
|
|
90
|
+
for import_name, label in modules:
|
|
91
|
+
native = is_native_compiled(import_name)
|
|
92
|
+
if native is True:
|
|
93
|
+
report[label] = "native"
|
|
94
|
+
elif native is False:
|
|
95
|
+
# Distinguish source-known-extension from unknown.
|
|
96
|
+
try:
|
|
97
|
+
mod = importlib.import_module(import_name)
|
|
98
|
+
path = getattr(mod, "__file__", "") or ""
|
|
99
|
+
report[label] = "source" if path.endswith(".py") else "unknown"
|
|
100
|
+
except ImportError:
|
|
101
|
+
# Should not happen post-is_native_compiled returning False,
|
|
102
|
+
# but cover the race anyway.
|
|
103
|
+
report[label] = "missing"
|
|
104
|
+
else:
|
|
105
|
+
report[label] = "missing"
|
|
106
|
+
return report
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def log_compilation_status_on_startup(
|
|
110
|
+
modules: Iterable[Tuple[str, str]] = PROPRIETARY_MODULES,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Emit one log line summarizing the proprietary-module load state.
|
|
113
|
+
|
|
114
|
+
Silent on the happy path (Linux x86_64 / Python 3.10 dev box where
|
|
115
|
+
all three modules are native-loaded — actually emits INFO, but
|
|
116
|
+
the message clearly says "all native" so ops can scan past).
|
|
117
|
+
|
|
118
|
+
Calls this exactly once at gateway server startup. Idempotent: if
|
|
119
|
+
a future caller invokes it twice, both emissions show the same
|
|
120
|
+
state because is_native_compiled is pure.
|
|
121
|
+
"""
|
|
122
|
+
report = compilation_status_report(modules)
|
|
123
|
+
native = [label for label, status in report.items() if status == "native"]
|
|
124
|
+
source = [label for label, status in report.items() if status == "source"]
|
|
125
|
+
missing = [label for label, status in report.items() if status == "missing"]
|
|
126
|
+
unknown = [label for label, status in report.items() if status == "unknown"]
|
|
127
|
+
|
|
128
|
+
if not source and not missing and not unknown:
|
|
129
|
+
logger.info(
|
|
130
|
+
"[LED-2087] proprietary modules native-loaded: %s",
|
|
131
|
+
", ".join(native) if native else "(none)",
|
|
132
|
+
)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# Customer-facing: ANY non-native module gets surfaced clearly so the
|
|
136
|
+
# operator knows performance / attestation parity is degraded for
|
|
137
|
+
# those specific modules. This is the LED-1259 warn-and-fallback
|
|
138
|
+
# pattern extended to deliberation + governance.
|
|
139
|
+
fragments: List[str] = []
|
|
140
|
+
if native:
|
|
141
|
+
fragments.append(f"native={','.join(native)}")
|
|
142
|
+
if source:
|
|
143
|
+
fragments.append(f"source-fallback={','.join(source)}")
|
|
144
|
+
if missing:
|
|
145
|
+
fragments.append(f"missing={','.join(missing)}")
|
|
146
|
+
if unknown:
|
|
147
|
+
fragments.append(f"unknown={','.join(unknown)}")
|
|
148
|
+
logger.warning(
|
|
149
|
+
"[LED-2087] proprietary-module load state — %s. Source-fallback "
|
|
150
|
+
"and missing modules run Python source path (slower; no compiled-"
|
|
151
|
+
"attestation parity). Cross-platform binaries land per the "
|
|
152
|
+
"LED-2087 Phase 1 build matrix.",
|
|
153
|
+
" ".join(fragments),
|
|
154
|
+
)
|
|
@@ -161,6 +161,42 @@ def dispatch_task(
|
|
|
161
161
|
if priority not in VALID_PRIORITIES:
|
|
162
162
|
return {"error": f"priority must be one of: {', '.join(sorted(VALID_PRIORITIES))}"}
|
|
163
163
|
|
|
164
|
+
# LED-1279: anti-duplicate gate. If the title/description/context tags an
|
|
165
|
+
# LED that's already been shipped (i.e. there's a commit on main mentioning
|
|
166
|
+
# the LED with date >= LED.created_at), refuse the dispatch and auto-close
|
|
167
|
+
# the LED. Yesterday's AGT-65A61AD5 wasted three subagent cycles on
|
|
168
|
+
# LED-1208/9/10, all of which had been shipped in commit 014fb5c on
|
|
169
|
+
# 2026-05-03. This gate prevents that class of duplicate.
|
|
170
|
+
try:
|
|
171
|
+
from ai.dispatch_gate import evaluate_dispatch, extract_led_id, lookup_led_created_at
|
|
172
|
+
|
|
173
|
+
led_id_for_gate = extract_led_id(title, description, context)
|
|
174
|
+
if led_id_for_gate:
|
|
175
|
+
led_created_at = lookup_led_created_at(led_id_for_gate)
|
|
176
|
+
refusal = evaluate_dispatch(
|
|
177
|
+
title=title,
|
|
178
|
+
description=description,
|
|
179
|
+
context=context,
|
|
180
|
+
led_created_at=led_created_at,
|
|
181
|
+
)
|
|
182
|
+
if refusal is not None:
|
|
183
|
+
_append_audit({
|
|
184
|
+
"action": "dispatch_refused_shipped",
|
|
185
|
+
"title": stripped,
|
|
186
|
+
"led_id": refusal.get("led_id"),
|
|
187
|
+
"shipped_in": refusal.get("shipped_in", {}).get("short_sha"),
|
|
188
|
+
"shipped_repo": refusal.get("shipped_in", {}).get("repo"),
|
|
189
|
+
})
|
|
190
|
+
return refusal
|
|
191
|
+
except Exception as e: # pragma: no cover — gate must never crash dispatch
|
|
192
|
+
# If the gate itself blows up, log it and proceed — losing a dispatch
|
|
193
|
+
# to a gate bug is a worse failure mode than the duplicate it would
|
|
194
|
+
# have caught.
|
|
195
|
+
_append_audit({
|
|
196
|
+
"action": "dispatch_gate_error",
|
|
197
|
+
"error": str(e)[:200],
|
|
198
|
+
})
|
|
199
|
+
|
|
164
200
|
tasks = _load_tasks()
|
|
165
201
|
|
|
166
202
|
normalized_external_key = external_key.strip()
|