delimit-cli 4.5.0 → 4.5.2
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 +108 -0
- package/README.md +2 -2
- package/adapters/cursor-rules.js +17 -4
- package/bin/delimit-cli.js +109 -24
- package/gateway/ai/content_engine.py +3 -4
- package/gateway/ai/inbox_classifier.py +215 -0
- package/gateway/ai/integrations/opensage_wrapper.py +4 -1
- package/gateway/ai/ledger_manager.py +218 -38
- package/gateway/ai/license.py +26 -0
- package/gateway/ai/notify.py +68 -3
- package/gateway/ai/reddit_proxy.py +93 -15
- package/gateway/ai/reddit_scanner.py +36 -18
- package/gateway/ai/server.py +128 -6
- package/gateway/ai/social_capability/__init__.py +6 -0
- package/gateway/ai/social_capability/capability_validator.py +273 -0
- package/gateway/ai/social_capability/current_capabilities.yaml +95 -0
- package/gateway/ai/social_queue.py +307 -0
- package/gateway/ai/supabase_sync.py +14 -2
- package/gateway/ai/swarm.py +29 -11
- package/gateway/ai/tui.py +6 -2
- package/gateway/ai/x_ranker.py +276 -0
- package/lib/attest-mcp.js +487 -0
- package/lib/attest-telemetry.js +48 -0
- package/lib/delimit-home.js +35 -0
- package/lib/delimit-template.js +14 -0
- package/lib/managed-section.js +92 -0
- package/lib/trust-page-engine.js +6 -2
- package/lib/wrap-engine.js +21 -4
- package/package.json +8 -2
- package/scripts/postinstall.js +89 -40
- package/gateway/ai/content_grounding/__init__.py +0 -98
- package/gateway/ai/content_grounding/build.py +0 -350
- package/gateway/ai/content_grounding/consume.py +0 -280
- package/gateway/ai/content_grounding/features.py +0 -218
- package/gateway/ai/content_grounding/fixtures/fail/01_missing_evidence.json +0 -9
- package/gateway/ai/content_grounding/fixtures/fail/02_unknown_evidence_prefix.json +0 -9
- package/gateway/ai/content_grounding/fixtures/fail/03_banned_comparative.json +0 -17
- package/gateway/ai/content_grounding/fixtures/fail/04_banned_adoption.json +0 -17
- package/gateway/ai/content_grounding/fixtures/fail/05_aggregate_no_numeric.json +0 -17
- package/gateway/ai/content_grounding/fixtures/fail/06_unversioned_inference_rule.json +0 -18
- package/gateway/ai/content_grounding/fixtures/pass/01_feature_shipped.json +0 -18
- package/gateway/ai/content_grounding/fixtures/pass/02_aggregate_claim.json +0 -23
- package/gateway/ai/content_grounding/fixtures/pass/03_attestation.json +0 -16
- package/gateway/ai/content_grounding/schemas/claim.schema.json +0 -40
- package/gateway/ai/content_grounding/schemas/event.schema.json +0 -23
- package/gateway/ai/content_grounding/schemas.py +0 -276
- package/gateway/ai/content_grounding/telemetry.py +0 -221
- package/gateway/ai/inbox_drafts/__init__.py +0 -61
- package/gateway/ai/inbox_drafts/registry.py +0 -412
- package/gateway/ai/inbox_drafts/schema.py +0 -374
- package/gateway/ai/inbox_executor.py +0 -565
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,114 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [4.5.2] - 2026-05-02
|
|
5
|
+
|
|
6
|
+
### Hardened — `postinstall.js` never-block-install guard (LED-1188)
|
|
7
|
+
|
|
8
|
+
`scripts/postinstall.js` is now wrapped in a top-level guard that ensures no
|
|
9
|
+
postinstall failure mode can ever block `npm install delimit-cli`. Per the
|
|
10
|
+
customer-protection rule, npm publish is a production deploy and a postinstall
|
|
11
|
+
crash on a paying Pro user's machine is a customer-facing incident regardless
|
|
12
|
+
of root cause.
|
|
13
|
+
|
|
14
|
+
Failure modes now hardened:
|
|
15
|
+
- **EROFS / EACCES / EPERM / EPIPE on stdout** — every banner write is wrapped
|
|
16
|
+
in a try/catch (some sandbox installers redirect stdout to a read-only pipe).
|
|
17
|
+
- **Network unreachable / DNS fail / TLS error / proxy reject** — the
|
|
18
|
+
telemetry HTTPS request silent-fails at every layer (`req.on('error')`,
|
|
19
|
+
`req.on('timeout')`, outer try/catch).
|
|
20
|
+
- **Missing or unreadable `package.json`** — graceful no-op; lets the install
|
|
21
|
+
complete so `delimit doctor` can diagnose the partial-install state.
|
|
22
|
+
- **`uncaughtException` propagation** — outermost guard swallows any
|
|
23
|
+
synchronous throw from the IIFE so the npm process always sees exit 0.
|
|
24
|
+
- **Idempotency** — re-running install is a no-op; the postinstall does not
|
|
25
|
+
write to `~/.delimit/` (that's `bin/delimit-setup.js`).
|
|
26
|
+
- **`DELIMIT_NO_TELEMETRY=1|true|yes`** kill switch honored (case-insensitive).
|
|
27
|
+
|
|
28
|
+
Regression coverage: `tests/postinstall-hardening.test.js` (6 tests) locks the
|
|
29
|
+
contract.
|
|
30
|
+
|
|
31
|
+
### Added — `lib/delimit-home.js` env-var unification (LED-1188)
|
|
32
|
+
|
|
33
|
+
Single source of truth for resolving the Delimit private-state directory. Replaces
|
|
34
|
+
~37 hardcoded `path.join(os.homedir(), '.delimit')` sites across the CLI surface.
|
|
35
|
+
|
|
36
|
+
Resolution order:
|
|
37
|
+
1. `$DELIMIT_HOME` (preferred — explicit, easy to reason about)
|
|
38
|
+
2. `$DELIMIT_NAMESPACE_ROOT` (gateway-compat fallback)
|
|
39
|
+
3. `<homedir>/.delimit` (default)
|
|
40
|
+
|
|
41
|
+
Both helpers re-resolve on every call so tests can mutate `process.env` between
|
|
42
|
+
calls without module-cache invalidation. Public API:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
const { delimitHome, homeSubpath } = require('delimit-cli/lib/delimit-home');
|
|
46
|
+
const ledger = homeSubpath('ledger'); // shorthand for path.join(delimitHome(), 'ledger')
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Regression coverage: `tests/delimit-home.test.js` (9 tests).
|
|
50
|
+
|
|
51
|
+
### Carry — STR-656 `delimit attest mcp` v1 (PRs #73, #74, #76, #77)
|
|
52
|
+
|
|
53
|
+
The four mcp-server PRs already merged into main are carried to npm in this
|
|
54
|
+
release. No customer-facing change beyond what those PRs documented:
|
|
55
|
+
|
|
56
|
+
- **#73** — `delimit attest mcp` panel-verdict behavior locks (Q1–Q6):
|
|
57
|
+
live MCP-protocol-conformance probe, 3-tier exit codes, `--output` /
|
|
58
|
+
`--no-write` flags, EROFS soft-fail on the preview JSON write, telemetry
|
|
59
|
+
counter with `DELIMIT_NO_TELEMETRY` kill switch, top-level runtime guard.
|
|
60
|
+
`--write` is now deprecated alias for `--output` (will be removed in v4.7).
|
|
61
|
+
- **#74** — `delimit setup` template + `package.json` files-array gate
|
|
62
|
+
`gateway/ai/self_repair/` and `self_repair_daemon.py` as gateway-only.
|
|
63
|
+
- **#76** — Template reframe of the operating-model rule per swarm-executor
|
|
64
|
+
panel verdict.
|
|
65
|
+
- **#77** — `package.json` files-array gate `gateway/ai/corp_dashboard.py`
|
|
66
|
+
as gateway-only.
|
|
67
|
+
|
|
68
|
+
Regression coverage: `tests/attest-mcp.test.js` (10 tests, now in the npm
|
|
69
|
+
test script — was previously orphaned).
|
|
70
|
+
|
|
71
|
+
### Bundle — proprietary gating extended
|
|
72
|
+
|
|
73
|
+
`package.json` files-array now excludes three additional gateway-internal
|
|
74
|
+
modules from the npm bundle (per the npm-bundle proprietary-gating rule —
|
|
75
|
+
these are not customer-shipping CLI surfaces):
|
|
76
|
+
|
|
77
|
+
- `!gateway/ai/content_grounding/` — LED-1084 grounding layer
|
|
78
|
+
- `!gateway/ai/inbox_drafts/` — LED-1129 SQLite draft registry
|
|
79
|
+
- `!gateway/ai/inbox_executor.py` — LED-1134 inbox executor
|
|
80
|
+
|
|
81
|
+
These remain gateway-only; they are imported only by gateway daemons and have
|
|
82
|
+
no public CLI surface.
|
|
83
|
+
|
|
84
|
+
### Backward compatibility
|
|
85
|
+
|
|
86
|
+
- No MCP tool signature changes
|
|
87
|
+
- No CLI command renamed or removed
|
|
88
|
+
- No storage format change
|
|
89
|
+
- All previously-passing tests still pass; 25 new tests added (171 → 196)
|
|
90
|
+
|
|
91
|
+
## [4.5.1] - 2026-04-28
|
|
92
|
+
|
|
93
|
+
### Security — attestation `canonicalize()` strengthened (LED-1180)
|
|
94
|
+
|
|
95
|
+
The `canonicalize()` helper used to derive attestation IDs and HMAC signatures was passing `Object.keys(bundle).sort()` as the second argument to `JSON.stringify`. JSON.stringify treats that argument as a property **allowlist**, not a sort order, and the allowlist contained only top-level keys — so nested objects serialised as `{}` and the HMAC committed only to the bundle's top-level shape.
|
|
96
|
+
|
|
97
|
+
Practical effect: a bundle with `{governance: {violations: ["safe"]}}` and one with `{governance: {violations: ["malicious"]}}` produced **identical signatures**. Tampering nested fields was undetectable through signature verification.
|
|
98
|
+
|
|
99
|
+
**This release replaces canonicalize with a proper recursive sorted-key serializer.** Old (v4.3 – v4.5.0) attestations remain readable but verify with the new canonicalize and will report `signature_mismatch`. New attestations produced by v4.5.1+ commit to the full content of the bundle.
|
|
100
|
+
|
|
101
|
+
- `lib/wrap-engine.js` — fixed `canonicalize()`, exported it for reuse
|
|
102
|
+
- `lib/trust-page-engine.js` — verifier now imports the corrected canonicalize
|
|
103
|
+
- `tests/v43-wrap-engine.test.js` — added LED-1180 regression: tampering a nested field MUST change the signature; if it doesn't, canonicalize is silently dropping nested keys
|
|
104
|
+
- `tests/v43-trust-page-engine.test.js` — test fixtures sign with the corrected canonicalize
|
|
105
|
+
|
|
106
|
+
If you have a corpus of v4.5.0 or earlier attestations and need them re-signed under the new primitive, the migration tool is on the LED-1180 follow-up. For most users, attestations are short-lived merge-decision artifacts and re-signing is unnecessary.
|
|
107
|
+
|
|
108
|
+
### Other
|
|
109
|
+
|
|
110
|
+
- (Internal) LED-1175 + LED-1177 MVP shipped in `delimit-private`: signed deliberation attestations + Scanner Input v0 schema. Public docs: [delimit.ai/docs/scanner-input](https://delimit.ai/docs/scanner-input), [delimit.ai/docs/vs-bugcrawl](https://delimit.ai/docs/vs-bugcrawl). No customer-facing CLI changes in 4.5.1.
|
|
111
|
+
|
|
4
112
|
## [4.5.0] - 2026-04-27
|
|
5
113
|
|
|
6
114
|
### Added — Ledger hygiene toolkit (LED-1145, 7 PRs)
|
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
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
|
+
[](https://github.com/marketplace/actions/delimit-api-governance)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
11
|
[](https://glama.ai/mcp/servers/delimit-ai/delimit)
|
|
12
12
|
|
package/adapters/cursor-rules.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
+
const { upsertManagedSection } = require('../lib/managed-section');
|
|
13
14
|
|
|
14
15
|
// LED-213: Import canonical template for cross-model parity
|
|
15
16
|
const { getDelimitSection } = require('../lib/delimit-template');
|
|
@@ -26,14 +27,26 @@ const CURSORRULES_FILE = path.join(HOME, '.cursorrules');
|
|
|
26
27
|
function installRules(version) {
|
|
27
28
|
const rules = getDelimitRules(version);
|
|
28
29
|
|
|
29
|
-
// Install to .cursor/rules/delimit.md (new location, Cursor 0.45+)
|
|
30
|
+
// Install to .cursor/rules/delimit.md (new location, Cursor 0.45+).
|
|
31
|
+
// LED-1180 follow-up: use upsertManagedSection so user-customized
|
|
32
|
+
// content above/below the delimit:start/end markers is preserved.
|
|
33
|
+
// The previous implementation did fs.writeFileSync(rulesFile, rules)
|
|
34
|
+
// — full overwrite — which clobbered any user customizations on every
|
|
35
|
+
// `delimit-cli setup`.
|
|
36
|
+
let action = 'unchanged';
|
|
37
|
+
let rulesFile = null;
|
|
30
38
|
if (fs.existsSync(CURSOR_DIR)) {
|
|
31
39
|
fs.mkdirSync(CURSOR_RULES_DIR, { recursive: true });
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
rulesFile = path.join(CURSOR_RULES_DIR, 'delimit.md');
|
|
41
|
+
const result = upsertManagedSection(rulesFile, rules, version);
|
|
42
|
+
action = result.action;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
installed: true,
|
|
47
|
+
action,
|
|
48
|
+
paths: [CURSORRULES_FILE, path.join(CURSOR_RULES_DIR, 'delimit.md')],
|
|
49
|
+
};
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
/**
|
package/bin/delimit-cli.js
CHANGED
|
@@ -11,6 +11,7 @@ const inquirer = require('inquirer');
|
|
|
11
11
|
const DelimitAuthSetup = require('../lib/auth-setup');
|
|
12
12
|
const DelimitHooksInstaller = require('../lib/hooks-installer');
|
|
13
13
|
const crossModelHooks = require('../lib/cross-model-hooks');
|
|
14
|
+
const { delimitHome, homeSubpath } = require('../lib/delimit-home');
|
|
14
15
|
const {
|
|
15
16
|
resolveContinuityContext,
|
|
16
17
|
formatContinuityReport,
|
|
@@ -58,8 +59,8 @@ function normalizeNaturalLanguageArgs(argv) {
|
|
|
58
59
|
const raw = argv.slice(2);
|
|
59
60
|
if (raw.length === 0) {
|
|
60
61
|
// First-run detection: if no ~/.delimit exists, show welcome flow
|
|
61
|
-
const
|
|
62
|
-
if (!fs.existsSync(
|
|
62
|
+
const home = delimitHome();
|
|
63
|
+
if (!fs.existsSync(home) || !fs.existsSync(path.join(home, 'server'))) {
|
|
63
64
|
return ['scan']; // lowest friction entry point for new users
|
|
64
65
|
}
|
|
65
66
|
return resolveRepoRoot(process.cwd()) ? ['session', '--inspect'] : ['session', '--all'];
|
|
@@ -1093,8 +1094,7 @@ program
|
|
|
1093
1094
|
.option('--dry-run', 'Preview what would be removed without making changes')
|
|
1094
1095
|
.action(async (options) => {
|
|
1095
1096
|
const dryRun = options.dryRun;
|
|
1096
|
-
const
|
|
1097
|
-
const backupDir = path.join(HOME, '.delimit', 'backups', `uninstall-${Date.now()}`);
|
|
1097
|
+
const backupDir = homeSubpath('backups', `uninstall-${Date.now()}`);
|
|
1098
1098
|
const changes = [];
|
|
1099
1099
|
|
|
1100
1100
|
if (dryRun) {
|
|
@@ -1349,8 +1349,7 @@ program
|
|
|
1349
1349
|
|
|
1350
1350
|
// Helper function for installation
|
|
1351
1351
|
async function installDelimit(mode, scope, hooksType = 'all') {
|
|
1352
|
-
const
|
|
1353
|
-
const DELIMIT_HOME = path.join(HOME, '.delimit');
|
|
1352
|
+
const DELIMIT_HOME = delimitHome();
|
|
1354
1353
|
|
|
1355
1354
|
// Create directories
|
|
1356
1355
|
['bin', 'hooks', 'shims', 'config', 'audit', 'credentials'].forEach(dir => {
|
|
@@ -2554,7 +2553,7 @@ program
|
|
|
2554
2553
|
.action(async () => {
|
|
2555
2554
|
console.log(chalk.bold('\n Delimit — Resume Work\n'));
|
|
2556
2555
|
|
|
2557
|
-
const DELIMIT_HOME =
|
|
2556
|
+
const DELIMIT_HOME = delimitHome();
|
|
2558
2557
|
|
|
2559
2558
|
// 1. Last session handoff
|
|
2560
2559
|
const sessionsDir = path.join(DELIMIT_HOME, 'sessions');
|
|
@@ -3285,10 +3284,10 @@ program
|
|
|
3285
3284
|
.option('--format <fmt>', 'Output format: md, json, html', 'md')
|
|
3286
3285
|
.option('--output <file>', 'Write report to file instead of stdout')
|
|
3287
3286
|
.action(async (options) => {
|
|
3288
|
-
const
|
|
3289
|
-
const evidenceDir = path.join(
|
|
3290
|
-
const ledgerDir = path.join(
|
|
3291
|
-
const memoryDir = path.join(
|
|
3287
|
+
const home = delimitHome();
|
|
3288
|
+
const evidenceDir = path.join(home, 'evidence');
|
|
3289
|
+
const ledgerDir = path.join(home, 'ledger');
|
|
3290
|
+
const memoryDir = path.join(home, 'memory');
|
|
3292
3291
|
|
|
3293
3292
|
// Parse duration into milliseconds
|
|
3294
3293
|
function parseDuration(dur) {
|
|
@@ -4906,6 +4905,94 @@ program
|
|
|
4906
4905
|
}
|
|
4907
4906
|
});
|
|
4908
4907
|
|
|
4908
|
+
// STR-656 — `delimit attest mcp` local-preview command (no public attestation,
|
|
4909
|
+
// no badge, no publish). Per the methodology gate (STR-657) the public signed-
|
|
4910
|
+
// attestation surface is locked until: 30d methodology visibility +
|
|
4911
|
+
// 14d CLI shipped + 5+ merge-gate pilot reference accounts + incident-
|
|
4912
|
+
// response process documented. This command exists so maintainers can
|
|
4913
|
+
// run the methodology checks locally and see the preview report shape.
|
|
4914
|
+
program
|
|
4915
|
+
.command('attest <kind>')
|
|
4916
|
+
.description('Run a Delimit attestation methodology check locally (preview only). kind: mcp')
|
|
4917
|
+
.option('--path <dir>', 'Path to repo (default: cwd)')
|
|
4918
|
+
.option('--json', 'Emit machine-readable JSON instead of the text preview')
|
|
4919
|
+
.option('--output <file>', 'Write the preview JSON to a file (default: .delimit/attestation-preview.json)')
|
|
4920
|
+
.option('--write <file>', '(deprecated) alias for --output')
|
|
4921
|
+
.option('--no-write', 'Do not write the preview JSON to disk')
|
|
4922
|
+
.action(async (kind, opts) => {
|
|
4923
|
+
const { recordTelemetry } = require('../lib/attest-telemetry');
|
|
4924
|
+
// --write is the deprecated alias for --output. Emit a one-line
|
|
4925
|
+
// notice so users migrate before we remove it. (Panel verdict on
|
|
4926
|
+
// STR-656 pre-push gate, 2026-04-30: retire the alias in v4.7.)
|
|
4927
|
+
if (opts.write && typeof opts.write === 'string' && !opts.output) {
|
|
4928
|
+
console.error(chalk.yellow(
|
|
4929
|
+
' [deprecation] --write is deprecated; use --output instead. (will be removed in v4.7)'
|
|
4930
|
+
));
|
|
4931
|
+
}
|
|
4932
|
+
if (kind !== 'mcp') {
|
|
4933
|
+
console.log(chalk.yellow(` Unknown attestation kind: ${kind}`));
|
|
4934
|
+
console.log(chalk.gray(' Supported kinds (v1): mcp'));
|
|
4935
|
+
console.log(chalk.gray(' pr-review and release attestations land in a follow-up.'));
|
|
4936
|
+
recordTelemetry({ kind, outcome: 'unknown_kind' });
|
|
4937
|
+
process.exitCode = 2;
|
|
4938
|
+
return;
|
|
4939
|
+
}
|
|
4940
|
+
const { runAttestMcp, renderPreview } = require('../lib/attest-mcp');
|
|
4941
|
+
let report;
|
|
4942
|
+
try {
|
|
4943
|
+
report = await runAttestMcp({ path: opts.path });
|
|
4944
|
+
} catch (e) {
|
|
4945
|
+
console.log(chalk.red(` attest mcp crashed: ${e.message}`));
|
|
4946
|
+
recordTelemetry({ kind: 'mcp', outcome: 'runner_crash', error: e.message });
|
|
4947
|
+
process.exitCode = 2;
|
|
4948
|
+
return;
|
|
4949
|
+
}
|
|
4950
|
+
if (report.error) {
|
|
4951
|
+
console.log(chalk.red(` ${report.error}`));
|
|
4952
|
+
recordTelemetry({ kind: 'mcp', outcome: 'runner_error', error: report.error });
|
|
4953
|
+
process.exitCode = 2;
|
|
4954
|
+
return;
|
|
4955
|
+
}
|
|
4956
|
+
if (opts.json) {
|
|
4957
|
+
console.log(JSON.stringify(report, null, 2));
|
|
4958
|
+
} else {
|
|
4959
|
+
console.log(renderPreview(report));
|
|
4960
|
+
}
|
|
4961
|
+
// Write the JSON report unless --no-write was given. --output wins,
|
|
4962
|
+
// --write is the deprecated alias kept for ≥1 minor cycle.
|
|
4963
|
+
if (opts.write !== false) {
|
|
4964
|
+
const outPath = opts.output || opts.write ||
|
|
4965
|
+
path.join(report.repo.path, '.delimit', 'attestation-preview.json');
|
|
4966
|
+
try {
|
|
4967
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
4968
|
+
fs.writeFileSync(outPath, JSON.stringify(report, null, 2) + '\n');
|
|
4969
|
+
if (!opts.json) console.log(chalk.gray(` Preview JSON: ${outPath}\n`));
|
|
4970
|
+
} catch (e) {
|
|
4971
|
+
// Soft-fail: a read-only filesystem (EROFS) or permissions
|
|
4972
|
+
// issue must NOT elevate the exit code — the report itself
|
|
4973
|
+
// is on stdout already.
|
|
4974
|
+
if (!opts.json) {
|
|
4975
|
+
console.log(chalk.yellow(` could not write preview JSON (${e.code || 'IO'}): ${e.message}`));
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
// 3-tier exit codes (panel verdict on STR-656 scaffold review):
|
|
4980
|
+
// 0 — pass + skip (no policy failure, no tool error)
|
|
4981
|
+
// 1 — at least one check returned fail (policy violation)
|
|
4982
|
+
// 2 — at least one check returned error (tool unavailable / unreadable input)
|
|
4983
|
+
// Skips do NOT raise the exit code; they are evidence states, not failures.
|
|
4984
|
+
const anyFail = report.checks.some((c) => c.status === 'fail');
|
|
4985
|
+
const anyError = report.checks.some((c) => c.status === 'error');
|
|
4986
|
+
const exitCode = anyFail ? 1 : (anyError ? 2 : 0);
|
|
4987
|
+
recordTelemetry({
|
|
4988
|
+
kind: 'mcp',
|
|
4989
|
+
outcome: ['pass', 'fail', 'error'][exitCode],
|
|
4990
|
+
methodology_version: report.methodology_version,
|
|
4991
|
+
check_summary: report.checks.map((c) => `${c.id}:${c.status}`).join(','),
|
|
4992
|
+
});
|
|
4993
|
+
process.exitCode = exitCode;
|
|
4994
|
+
});
|
|
4995
|
+
|
|
4909
4996
|
// CI command — generate GitHub Action workflow
|
|
4910
4997
|
program
|
|
4911
4998
|
.command('ci')
|
|
@@ -5477,8 +5564,7 @@ program
|
|
|
5477
5564
|
.command('activate <key>')
|
|
5478
5565
|
.description('Activate a Delimit Pro license key')
|
|
5479
5566
|
.action(async (key) => {
|
|
5480
|
-
const
|
|
5481
|
-
const licenseDir = path.join(os.homedir(), '.delimit');
|
|
5567
|
+
const licenseDir = delimitHome();
|
|
5482
5568
|
const licensePath = path.join(licenseDir, 'license.json');
|
|
5483
5569
|
|
|
5484
5570
|
if (!key || key.length < 10) {
|
|
@@ -5766,8 +5852,7 @@ program
|
|
|
5766
5852
|
console.log(`Question: ${chalk.bold(question)}\n`);
|
|
5767
5853
|
|
|
5768
5854
|
// Try to run deliberation directly via the gateway
|
|
5769
|
-
const
|
|
5770
|
-
const gatewayScript = path.join(HOME, '.delimit', 'server', 'ai', 'deliberation.py');
|
|
5855
|
+
const gatewayScript = homeSubpath('server', 'ai', 'deliberation.py');
|
|
5771
5856
|
const scriptPath = fs.existsSync(gatewayScript) ? gatewayScript : null;
|
|
5772
5857
|
|
|
5773
5858
|
if (scriptPath) {
|
|
@@ -5809,7 +5894,7 @@ if result.get('summary'):
|
|
|
5809
5894
|
}
|
|
5810
5895
|
|
|
5811
5896
|
// Save pending deliberation to file for reference
|
|
5812
|
-
const deliberationDir =
|
|
5897
|
+
const deliberationDir = homeSubpath('deliberation');
|
|
5813
5898
|
fs.mkdirSync(deliberationDir, { recursive: true });
|
|
5814
5899
|
const pending = {
|
|
5815
5900
|
question,
|
|
@@ -5848,8 +5933,8 @@ if result.get('summary'):
|
|
|
5848
5933
|
// Models command: BYOK deliberation key management wizard
|
|
5849
5934
|
// ---------------------------------------------------------------------------
|
|
5850
5935
|
|
|
5851
|
-
const MODELS_CONFIG_PATH =
|
|
5852
|
-
const DELIBERATION_USAGE_PATH =
|
|
5936
|
+
const MODELS_CONFIG_PATH = homeSubpath('models.json');
|
|
5937
|
+
const DELIBERATION_USAGE_PATH = homeSubpath('deliberation_usage.json');
|
|
5853
5938
|
|
|
5854
5939
|
const DEFAULT_MODELS = {
|
|
5855
5940
|
grok: { enabled: false, api_key: '', model: 'grok-4-0709', name: 'Grok 4' },
|
|
@@ -6199,7 +6284,7 @@ program
|
|
|
6199
6284
|
program
|
|
6200
6285
|
.command('trust-page')
|
|
6201
6286
|
.description('Render attestations into a public trust page (static HTML + JSON feed)')
|
|
6202
|
-
.option('-d, --dir <path>', 'Attestation directory',
|
|
6287
|
+
.option('-d, --dir <path>', 'Attestation directory', homeSubpath('attestations'))
|
|
6203
6288
|
.option('-o, --out <path>', 'Output directory', './trust-page')
|
|
6204
6289
|
.option('-t, --title <title>', 'Trust page title', 'Trust Page')
|
|
6205
6290
|
.option('--json', 'Output result as JSON', false)
|
|
@@ -6229,7 +6314,7 @@ program
|
|
|
6229
6314
|
program
|
|
6230
6315
|
.command('ai-sbom')
|
|
6231
6316
|
.description('Build a CycloneDX-AI bill of materials from attestations')
|
|
6232
|
-
.option('-d, --dir <path>', 'Attestation directory',
|
|
6317
|
+
.option('-d, --dir <path>', 'Attestation directory', homeSubpath('attestations'))
|
|
6233
6318
|
.option('-o, --out <path>', 'Output file', './ai-sbom.json')
|
|
6234
6319
|
.option('-n, --name <name>', 'BOM subject name', 'ai-sbom')
|
|
6235
6320
|
.option('-v, --package-version <v>', 'BOM subject version', '1.0.0')
|
|
@@ -6292,7 +6377,7 @@ program
|
|
|
6292
6377
|
console.log("\nUse " + chalk.cyan("delimit vault list") + " to see configured secrets.");
|
|
6293
6378
|
} else if (action === "list") {
|
|
6294
6379
|
console.log(chalk.bold("Configured Secrets:"));
|
|
6295
|
-
const secretsDir =
|
|
6380
|
+
const secretsDir = homeSubpath('secrets');
|
|
6296
6381
|
if (fs.existsSync(secretsDir)) {
|
|
6297
6382
|
const files = fs.readdirSync(secretsDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
|
|
6298
6383
|
if (files.length === 0) {
|
|
@@ -6314,7 +6399,7 @@ program
|
|
|
6314
6399
|
console.log(chalk.dim(" Example: delimit vault set OPENAI_API_KEY"));
|
|
6315
6400
|
process.exit(1);
|
|
6316
6401
|
}
|
|
6317
|
-
const secretsDir =
|
|
6402
|
+
const secretsDir = homeSubpath('secrets');
|
|
6318
6403
|
fs.mkdirSync(secretsDir, { recursive: true });
|
|
6319
6404
|
const filePath = path.join(secretsDir, `${name}.json`);
|
|
6320
6405
|
const existing = fs.existsSync(filePath);
|
|
@@ -6338,7 +6423,7 @@ program
|
|
|
6338
6423
|
console.log(chalk.red("Usage: delimit vault reveal <NAME>"));
|
|
6339
6424
|
process.exit(1);
|
|
6340
6425
|
}
|
|
6341
|
-
const secretsDir =
|
|
6426
|
+
const secretsDir = homeSubpath('secrets');
|
|
6342
6427
|
const filePath = path.join(secretsDir, `${name}.json`);
|
|
6343
6428
|
if (!fs.existsSync(filePath)) {
|
|
6344
6429
|
console.log(chalk.red(` Secret "${name}" not found.`));
|
|
@@ -6399,7 +6484,7 @@ program
|
|
|
6399
6484
|
// Memory commands: remember, recall, forget
|
|
6400
6485
|
// ---------------------------------------------------------------------------
|
|
6401
6486
|
|
|
6402
|
-
const MEMORY_DIR =
|
|
6487
|
+
const MEMORY_DIR = homeSubpath('memory');
|
|
6403
6488
|
const MEMORY_FILE = path.join(MEMORY_DIR, 'memories.jsonl');
|
|
6404
6489
|
|
|
6405
6490
|
const KNOWN_TECH_TERMS = new Set([
|
|
@@ -988,11 +988,10 @@ def post_next_tweet() -> Dict[str, Any]:
|
|
|
988
988
|
Checks the day-typed tweet schedule first. Falls back to the flat queue
|
|
989
989
|
if no scheduled tweet is available for today.
|
|
990
990
|
"""
|
|
991
|
-
from ai.social import post_tweet,
|
|
991
|
+
from ai.social import post_tweet, should_post_now
|
|
992
992
|
|
|
993
|
-
if not
|
|
994
|
-
|
|
995
|
-
return {"status": "skipped", "reason": f"Daily posting limit reached ({daily_limit}/day)"}
|
|
993
|
+
if not should_post_now():
|
|
994
|
+
return {"status": "skipped", "reason": "Rate cap hit (2/hr or 24/day)"}
|
|
996
995
|
|
|
997
996
|
# --- Try day-typed schedule first ---
|
|
998
997
|
scheduled = get_scheduled_tweet()
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Inbox-reply keyword classifier — extracted from inbox_daemon.py for
|
|
2
|
+
LED-2059 live-reload.
|
|
3
|
+
|
|
4
|
+
The inbox daemon is a long-running thread inside the gateway process.
|
|
5
|
+
Edits to keyword lists or detector regex landed on disk but didn't take
|
|
6
|
+
effect until the gateway restarted, which is why the founder's "ship the
|
|
7
|
+
symphony thread" reply (2026-04-28 incident) didn't auto-execute even
|
|
8
|
+
though the LED-820 fix was already on disk.
|
|
9
|
+
|
|
10
|
+
This module is reloaded by ``inbox_daemon.poll_once()`` at the start of
|
|
11
|
+
each poll via ``importlib.reload``. Code changes here pick up within one
|
|
12
|
+
poll interval (default 300s) without a gateway restart.
|
|
13
|
+
|
|
14
|
+
Three classifier signals layered by escalating intent:
|
|
15
|
+
- ``detect_approval_keywords`` — soft "approved" / "lgtm". Sets the draft
|
|
16
|
+
to ``approved`` status; the founder still posts manually.
|
|
17
|
+
- ``detect_explicit_post_keywords`` — strong "ship it" / "post 812" /
|
|
18
|
+
"autopost". The daemon is allowed to call ``auto_post_draft`` with a
|
|
19
|
+
per-call DELIMIT_ENABLE_X_AUTOPOST bypass.
|
|
20
|
+
- ``detect_cancel_keywords`` — "cancel" / "hold" / "drop it". Marks the
|
|
21
|
+
draft cancelled and skips any future processing for that id.
|
|
22
|
+
|
|
23
|
+
All three detectors strip quoted Gmail / Outlook history before scanning
|
|
24
|
+
so a quoted prior email containing one of the keywords doesn't trigger
|
|
25
|
+
the wrong branch (the LED-817 incident).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
from typing import Iterable
|
|
32
|
+
|
|
33
|
+
# ── Keyword lists ────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
# LED-817 (P0): word-boundary regex matching to prevent substring false
|
|
36
|
+
# positives from quoted Gmail history (e.g. "Reply 'hold' → I hold" in
|
|
37
|
+
# a quoted prior email tripping the cancel branch on an "approved" reply).
|
|
38
|
+
#
|
|
39
|
+
# LED-820 (P1) tier split: APPROVAL_KEYWORDS is the SOFT signal (mark
|
|
40
|
+
# approved, email founder for manual post — same as before). EXPLICIT_POST
|
|
41
|
+
# is the STRONG signal — caller authorized auto-execution of the draft's
|
|
42
|
+
# action right now, no second click required.
|
|
43
|
+
|
|
44
|
+
APPROVAL_KEYWORDS: list[str] = [
|
|
45
|
+
"approved",
|
|
46
|
+
"approve",
|
|
47
|
+
"yes",
|
|
48
|
+
"go ahead",
|
|
49
|
+
"lgtm",
|
|
50
|
+
"looks good",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Explicit-post keywords — strong signal. Founder authorized auto-execution
|
|
54
|
+
# of the draft's action (post the tweet, comment the issue) WITHOUT a
|
|
55
|
+
# second click. Only triggers when both (a) one of these phrases is in the
|
|
56
|
+
# unquoted reply body AND (b) the draft has a registered draft_id match.
|
|
57
|
+
EXPLICIT_POST_KEYWORDS: list[str] = [
|
|
58
|
+
"post it",
|
|
59
|
+
"ship it",
|
|
60
|
+
"post 8", # "post 812", "post 800", etc. — LED-id-prefixed posts
|
|
61
|
+
"post led", # "post LED-812"
|
|
62
|
+
"publish it",
|
|
63
|
+
"send it",
|
|
64
|
+
"go post",
|
|
65
|
+
"post via api",
|
|
66
|
+
"autopost",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
CANCEL_KEYWORDS: list[str] = [
|
|
70
|
+
"cancel",
|
|
71
|
+
"stop",
|
|
72
|
+
"abort",
|
|
73
|
+
"don't post",
|
|
74
|
+
"do not post",
|
|
75
|
+
"hold",
|
|
76
|
+
"skip",
|
|
77
|
+
"drop it",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ── Regex compilation ────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
def _compile_keyword_regex(keywords: Iterable[str]) -> re.Pattern[str]:
|
|
84
|
+
"""LED-817: build a strict word-boundary regex. Stricter than ``\\b``
|
|
85
|
+
because hyphens count as word boundaries in Python — ``\\bstop\\b``
|
|
86
|
+
matches the 'stop' in 'non-stop', re-introducing the substring bug
|
|
87
|
+
we're trying to fix. Use ``(?<![\\w-])`` / ``(?![\\w-])`` to treat
|
|
88
|
+
hyphens as internal so 'non-stop' doesn't trigger 'stop' but
|
|
89
|
+
'please cancel.' still triggers 'cancel'.
|
|
90
|
+
"""
|
|
91
|
+
parts: list[str] = []
|
|
92
|
+
for kw in keywords:
|
|
93
|
+
if " " in kw or "'" in kw:
|
|
94
|
+
# Multi-word phrase — exact escape, internal whitespace
|
|
95
|
+
# already provides separation.
|
|
96
|
+
parts.append(re.escape(kw))
|
|
97
|
+
else:
|
|
98
|
+
parts.append(rf"(?<![\w-]){re.escape(kw)}(?![\w-])")
|
|
99
|
+
return re.compile("(" + "|".join(parts) + ")", re.IGNORECASE)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
_APPROVAL_RE = _compile_keyword_regex(APPROVAL_KEYWORDS)
|
|
103
|
+
_CANCEL_RE = _compile_keyword_regex(CANCEL_KEYWORDS)
|
|
104
|
+
_EXPLICIT_POST_RE = _compile_keyword_regex(EXPLICIT_POST_KEYWORDS)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ── Quoted-email stripping ──────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
# LED-817 (P0): strip quoted email content before keyword scanning.
|
|
110
|
+
# Gmail (and most clients) preserve quoted history below the reply.
|
|
111
|
+
# Without stripping, a substring like "hold" from a previously-quoted
|
|
112
|
+
# email of mine triggered cancel on an "approved" reply. Detect quote
|
|
113
|
+
# markers and cut everything from the first marker onward.
|
|
114
|
+
_QUOTE_MARKERS: tuple[re.Pattern[str], ...] = (
|
|
115
|
+
re.compile(r"^On\s+.+?\s+wrote:\s*$", re.MULTILINE), # Gmail
|
|
116
|
+
re.compile(r"^-{2,}\s*Original Message\s*-{2,}", re.MULTILINE | re.IGNORECASE),
|
|
117
|
+
re.compile(r"^-{2,}\s*Forwarded message", re.MULTILINE | re.IGNORECASE),
|
|
118
|
+
re.compile(r"^From:\s*.+?\s*<", re.MULTILINE), # Outlook
|
|
119
|
+
re.compile(r"^Sent from my", re.MULTILINE), # Mobile sig
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Lines starting with ">" are quoted in plaintext email
|
|
123
|
+
_QUOTED_LINE_PREFIX_RE = re.compile(r"^[\s]*>", re.MULTILINE)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _strip_quoted_content(text: str) -> str:
|
|
127
|
+
"""Remove quoted email history so keyword scans only see the new reply.
|
|
128
|
+
|
|
129
|
+
Cuts at the first quote marker found anywhere in the body, then drops
|
|
130
|
+
any remaining lines that start with '>'. The intent is conservative:
|
|
131
|
+
if a marker is ambiguous, we keep the text. False negatives (failing
|
|
132
|
+
to strip) cause the same false-positive bug we're fixing, so the
|
|
133
|
+
detection has to favor cutting too aggressively rather than too little.
|
|
134
|
+
"""
|
|
135
|
+
if not text:
|
|
136
|
+
return ""
|
|
137
|
+
|
|
138
|
+
# Find the earliest position of any quote marker
|
|
139
|
+
earliest = len(text)
|
|
140
|
+
for pattern in _QUOTE_MARKERS:
|
|
141
|
+
match = pattern.search(text)
|
|
142
|
+
if match and match.start() < earliest:
|
|
143
|
+
earliest = match.start()
|
|
144
|
+
|
|
145
|
+
head = text[:earliest]
|
|
146
|
+
|
|
147
|
+
# Drop ">"-prefixed lines from the head (in case Gmail used ">" without
|
|
148
|
+
# a "On X wrote:" header, or the user manually quoted).
|
|
149
|
+
cleaned_lines = [
|
|
150
|
+
line for line in head.splitlines()
|
|
151
|
+
if not _QUOTED_LINE_PREFIX_RE.match(line)
|
|
152
|
+
]
|
|
153
|
+
return "\n".join(cleaned_lines).strip()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── Public detectors ────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
def detect_approval_keywords(text: str) -> bool:
|
|
159
|
+
"""Soft-signal approval. Returns True if ``text`` (after stripping
|
|
160
|
+
quoted history) contains an approval keyword on a word boundary.
|
|
161
|
+
|
|
162
|
+
Guards against feedback loops:
|
|
163
|
+
- Ignores emails FROM the daemon itself (contain "post this manually")
|
|
164
|
+
- Ignores pathological one-word spam ("test" / "hello" / "approve me")
|
|
165
|
+
- Otherwise relies on the upstream draft_id match to filter
|
|
166
|
+
"""
|
|
167
|
+
if not text:
|
|
168
|
+
return False
|
|
169
|
+
body = _strip_quoted_content(text).lower().strip()
|
|
170
|
+
if not body:
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
# Block feedback loop: daemon's own confirmation emails
|
|
174
|
+
if "post this manually" in body or "has been approved" in body:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
# LED-817 (P0): the previous junk-block dropped bare "approved" /
|
|
178
|
+
# "approve" replies under the assumption they were spam. With the
|
|
179
|
+
# upstream `draft_id and detect_approval_keywords` gate at the
|
|
180
|
+
# callsite, a bare "approved" can only fire when the reply is in a
|
|
181
|
+
# signed-draft thread — i.e. founder-intent. Keep the spam guard
|
|
182
|
+
# only for the truly pathological cases.
|
|
183
|
+
if body in ("test", "hello", "approve me"):
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
return bool(_APPROVAL_RE.search(body))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def detect_cancel_keywords(text: str) -> bool:
|
|
190
|
+
"""LED-817: word-boundary regex against the unquoted reply only,
|
|
191
|
+
no longer trips on 'hold' inside quoted history."""
|
|
192
|
+
if not text:
|
|
193
|
+
return False
|
|
194
|
+
body = _strip_quoted_content(text).lower().strip()
|
|
195
|
+
if not body:
|
|
196
|
+
return False
|
|
197
|
+
return bool(_CANCEL_RE.search(body))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def detect_explicit_post_keywords(text: str) -> bool:
|
|
201
|
+
"""LED-820 (P1): strong-signal trigger that authorizes the daemon to
|
|
202
|
+
actually execute the draft's action (post the tweet, comment the
|
|
203
|
+
issue) instead of merely marking it approved.
|
|
204
|
+
|
|
205
|
+
Returns True only when the unquoted reply body contains an unambiguous
|
|
206
|
+
posting directive ("post it" / "ship it" / "post 812" / etc).
|
|
207
|
+
Generic approvals like "approved" / "lgtm" do NOT auto-execute — the
|
|
208
|
+
founder must explicitly direct the post.
|
|
209
|
+
"""
|
|
210
|
+
if not text:
|
|
211
|
+
return False
|
|
212
|
+
body = _strip_quoted_content(text).lower().strip()
|
|
213
|
+
if not body:
|
|
214
|
+
return False
|
|
215
|
+
return bool(_EXPLICIT_POST_RE.search(body))
|
|
@@ -32,7 +32,10 @@ from typing import Any, Callable, Dict, List, Optional
|
|
|
32
32
|
|
|
33
33
|
logger = logging.getLogger("delimit.integrations.opensage")
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
# LED-1188: env-var-aware home resolution (DELIMIT_HOME / DELIMIT_NAMESPACE_ROOT).
|
|
36
|
+
from ..continuity import get_namespace_root # noqa: E402
|
|
37
|
+
|
|
38
|
+
DELIMIT_HOME = get_namespace_root()
|
|
36
39
|
AUDIT_DIR = DELIMIT_HOME / "audit"
|
|
37
40
|
POLICY_FILE = DELIMIT_HOME / "enforcement_mode"
|
|
38
41
|
|