delimit-cli 4.1.53 → 4.3.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 +26 -0
- package/README.md +34 -3
- package/bin/delimit-cli.js +150 -2
- package/bin/delimit-setup.js +22 -7
- package/gateway/ai/agent_dispatch.py +79 -0
- package/gateway/ai/daily_digest.py +386 -0
- package/gateway/ai/ledger_manager.py +32 -0
- package/gateway/ai/license_core.py +2 -0
- package/gateway/ai/notify.py +17 -11
- package/gateway/ai/reddit_proxy.py +28 -9
- package/gateway/ai/sensing/__init__.py +35 -0
- package/gateway/ai/sensing/schema.py +107 -0
- package/gateway/ai/sensing/signal_store.py +348 -0
- package/gateway/ai/server.py +419 -6
- package/gateway/ai/supabase_sync.py +308 -0
- package/gateway/ai/work_order.py +216 -0
- package/gateway/ai/workers/__init__.py +32 -0
- package/gateway/ai/workers/base.py +154 -0
- package/gateway/ai/workers/executor.py +861 -0
- package/gateway/ai/workers/outreach_drafter.py +161 -0
- package/gateway/ai/workers/pr_drafter.py +148 -0
- package/lib/ai-sbom-engine.js +154 -0
- package/lib/trust-page-engine.js +179 -0
- package/lib/wrap-engine.js +431 -0
- package/package.json +14 -1
- package/adapters/codex-security.js +0 -64
- package/adapters/codex-skill.js +0 -78
- package/adapters/cursor-rules.js +0 -73
- package/gateway/ai/continuity.py +0 -462
- package/gateway/ai/inbox_daemon_runner.py +0 -217
- package/gateway/ai/loop_engine.py +0 -1303
- package/gateway/ai/social_cache.py +0 -341
- package/gateway/ai/social_daemon.py +0 -483
- package/gateway/ai/tweet_corpus_schema.sql +0 -76
- package/scripts/crosspost_devto.py +0 -304
- package/scripts/demo-v420-clean.sh +0 -267
- package/scripts/demo-v420-deliberation.sh +0 -217
- package/scripts/demo-v420.sh +0 -55
- package/scripts/sync-gateway.sh +0 -112
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.2.0] - 2026-04-21
|
|
4
|
+
|
|
5
|
+
### Added (gateway sync — LED-987 through LED-1008)
|
|
6
|
+
- **Venture tagging (LED-1008)** — work orders, deliberations, and social drafts carry a canonical venture tag propagated through `save_draft` and the Supabase sync writers. `_normalize_venture` maps freeform strings (e.g., "DomainVested", "wire.report", "LT") to the canonical 4-member vocabulary (delimit / domainvested / wirereport / livetube). Unknown values pass through lowercased so they surface instead of dropping.
|
|
7
|
+
- **Warm-window filter for social targets (LED-998)** — 72h window on X scans, 14d on reddit. Env-tunable via `DELIMIT_X_WARM_HOURS` and `DELIMIT_REDDIT_WARM_HOURS`. Fail-open on missing timestamps so parser bugs don't silently drop everything.
|
|
8
|
+
- **X draft salvage (LED-997)** — `_try_trim_twitter_draft` trims LLM output at sentence boundary when it exceeds 3 sentences or 280 chars. Runs before the max-length check so a good-but-long draft becomes a good-and-short one instead of dying in the sanitizer. Proven live: salvaged a 626-char draft to 241 chars on the first post-deploy cycle.
|
|
9
|
+
- **Reversible stale cleanup (LED-990)** — weekly systemd timer demotes blocked items to a cold lane after 30d instead of deleting. Ledger update restores cold → blocked/open with a single status flip. First-run safe with DRY_RUN=1.
|
|
10
|
+
- **Warm-thread PR watcher (LED-989)** — 14-day warm window on outreach follow-ups; skip threads inactive longer than that. MAX_ACTIVE_THREADS=8 cap prevents dog-piling a single repository.
|
|
11
|
+
- **Lemon Squeezy → Supabase reconciler (LED-996)** — trial watcher now polls LS every 6h and upserts subscription_status + role into the Supabase users table. Catches webhook drops without manual SQL intervention.
|
|
12
|
+
- **ACTION_DENYLIST (LED-988)** — executor v2 gains an explicit denylist of prohibited action categories (money, legal/identity, credentials, deploy, contracts) that fires BEFORE the ACTION_SPEC whitelist check. Defense-in-depth against LLM-driven executor drift.
|
|
13
|
+
- **Reddit residential-IP proxy (LED-987)** — scoped service bypasses 429s on reddit34.p.rapidapi.com by routing through a residential IP. Systemd-managed, auto-restart, rate-limited.
|
|
14
|
+
- **propose_pr autonomous build primitive (LED-988)** — executor can propose PRs against an allowlisted repo set (`PROPOSE_PR_ALLOWED_REPOS`) with a fixed branch prefix (`delimit/`) and author (`delimit-bot`). Guarded by denylist + whitelist.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Exit-shim counter undercounting** — previously missed commits outside `SESSION_CWD` and dropped Z-suffixed timestamps; both now captured.
|
|
18
|
+
- **Proprietary path leaks** — sync-gateway.sh EXCLUDE list hardened to keep Jamsons-portfolio-specific files (social.py, social_target.py, inbox_daemon.py, founding_users.py, deliberation.py) out of the npm bundle.
|
|
19
|
+
|
|
20
|
+
### Tests
|
|
21
|
+
- Gateway: 163/163 passing on changed-file tests (social.py, social_target.py, supabase_sync).
|
|
22
|
+
- npm CLI: 134/134 passing (no CLI behavior changes — bundled gateway update).
|
|
23
|
+
- Security audit: 0 real findings across gateway + UI (false positives only — test fixtures and TypeScript `token:` parameter types).
|
|
24
|
+
|
|
25
|
+
### Notes
|
|
26
|
+
- Companion dashboard changes at app.delimit.ai (LED-995 Billing + API Keys wiring, LED-997 Blocked drafts tab, LED-1008 venture chips + filters) shipped via delimit-ui/main.
|
|
27
|
+
- Supabase migration 025 (venture TEXT column on 4 tables) applied separately via Management API.
|
|
28
|
+
|
|
3
29
|
## [4.1.53] - 2026-04-10
|
|
4
30
|
|
|
5
31
|
### Fixed (cycle engine — think→build→deploy)
|
package/README.md
CHANGED
|
@@ -16,8 +16,7 @@ The universal command for the Delimit Swarm. When you say **"Think and Build"**,
|
|
|
16
16
|
Works across any configuration — from a single model on a budget to an enterprise swarm of 4+ models.
|
|
17
17
|
|
|
18
18
|
[](https://www.npmjs.com/package/delimit-cli)
|
|
19
|
-
[](https://delimit.ai)
|
|
19
|
+
[](https://github.com/delimit-ai/delimit-mcp-server)
|
|
21
20
|
[](https://github.com/marketplace/actions/delimit-api-governance)
|
|
22
21
|
[](https://opensource.org/licenses/MIT)
|
|
23
22
|
[](https://glama.ai/mcp/servers/delimit-ai/delimit)
|
|
@@ -70,6 +69,34 @@ npx delimit-cli init # Sets up governance + drift baseline
|
|
|
70
69
|
|
|
71
70
|
---
|
|
72
71
|
|
|
72
|
+
## What's New in v4.3
|
|
73
|
+
|
|
74
|
+
*Gate every AI-assisted invocation. Ship the receipts.*
|
|
75
|
+
|
|
76
|
+
- **`delimit wrap`** — pipe `claude -p`, `cursor`, `aider`, `codex`, or any AI-assisted CLI through a signed governance gate. Snapshots the git diff before/after, runs lint + tests, HMAC-signs an `att_*` attestation, emits a public replay URL. Advisory by default; `--enforce` blocks CI on policy violations; `--max-time <s>` is a kill switch that tags the attestation as a `liability_incident` and prints a cross-model handoff command.
|
|
77
|
+
- **`delimit trust-page`** — renders a directory of attestations into a static HTML trust page + JSON Feed 1.1 feed. Single file, no framework, offline-renderable. Deploy anywhere.
|
|
78
|
+
- **`delimit ai-sbom`** — aggregates attestations into a CycloneDX 1.6 bill-of-materials with AI-specific fields (detected models per vendor, tool-call surface, policy gate counts). Pipe straight into procurement.
|
|
79
|
+
- **Cross-model by construction** — `wrap` is agnostic to the producer. Same attestation schema whether the pipe upstream is Claude Code, Cursor, Aider, Codex, or Gemini CLI. Switch producers without losing the audit chain.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Gate any AI-assisted CLI
|
|
83
|
+
delimit wrap -- claude -p "add tests for payments"
|
|
84
|
+
# → att_7d556843c84fb881 signed, replay: https://delimit.ai/att/att_7d556843c84fb881
|
|
85
|
+
|
|
86
|
+
# Kill switch + handoff after 60s wall-clock
|
|
87
|
+
delimit wrap --max-time 60 -- cursor edit "refactor auth middleware"
|
|
88
|
+
# → if killed: kind=liability_incident
|
|
89
|
+
# → suggested: delimit wrap -- claude -p "refactor auth middleware"
|
|
90
|
+
|
|
91
|
+
# Render accumulated attestations as a public trust page
|
|
92
|
+
delimit trust-page -o ./trust
|
|
93
|
+
# → ./trust/index.html (+ feed.json)
|
|
94
|
+
|
|
95
|
+
# Build a CycloneDX-AI bill of materials
|
|
96
|
+
delimit ai-sbom -o ./ai-sbom.json
|
|
97
|
+
# → components: 4 models detected, 187 gates run
|
|
98
|
+
```
|
|
99
|
+
|
|
73
100
|
## What's New in v4.20
|
|
74
101
|
|
|
75
102
|
*The highest state of AI governance.*
|
|
@@ -121,7 +148,7 @@ delimit deliberate "Should we build rate limiting in-house or use a managed serv
|
|
|
121
148
|
- **Cross-Model Audit** -- 3 lenses (security, correctness, governance) with deterministic synthesis
|
|
122
149
|
- **4-model deliberation** -- Claude + Grok + Gemini + Codex debate until consensus
|
|
123
150
|
- **Universal Swarm Triggers** -- "Think and Build", "Keep building", "Ask Delimit"
|
|
124
|
-
- **
|
|
151
|
+
- **Full governance toolkit** -- lint, diff, policy, evidence, drift, attestation, and swarm orchestration exposed as MCP tools and CLI subcommands
|
|
125
152
|
|
|
126
153
|
---
|
|
127
154
|
|
|
@@ -213,6 +240,10 @@ npx delimit-cli models --status # Show current model config
|
|
|
213
240
|
npx delimit-cli status # Compact dashboard of your Delimit setup
|
|
214
241
|
npx delimit-cli doctor # Check setup health
|
|
215
242
|
npx delimit-cli uninstall --dry-run # Preview removal
|
|
243
|
+
npx delimit-cli wrap -- claude -p "..." # Gate any AI-assisted CLI + signed attestation (v4.3)
|
|
244
|
+
npx delimit-cli wrap --max-time 60 -- codex "..."# With kill switch + handoff on timeout
|
|
245
|
+
npx delimit-cli trust-page -o ./trust # Render attestations into a static trust page
|
|
246
|
+
npx delimit-cli ai-sbom -o ./ai-sbom.json # Build a CycloneDX-AI bill of materials
|
|
216
247
|
```
|
|
217
248
|
|
|
218
249
|
### What the MCP toolkit adds
|
package/bin/delimit-cli.js
CHANGED
|
@@ -5768,8 +5768,7 @@ program
|
|
|
5768
5768
|
// Try to run deliberation directly via the gateway
|
|
5769
5769
|
const HOME = process.env.HOME || require('os').homedir();
|
|
5770
5770
|
const gatewayScript = path.join(HOME, '.delimit', 'server', 'ai', 'deliberation.py');
|
|
5771
|
-
const
|
|
5772
|
-
const scriptPath = fs.existsSync(gatewayScript) ? gatewayScript : fs.existsSync(gatewayAlt) ? gatewayAlt : null;
|
|
5771
|
+
const scriptPath = fs.existsSync(gatewayScript) ? gatewayScript : null;
|
|
5773
5772
|
|
|
5774
5773
|
if (scriptPath) {
|
|
5775
5774
|
console.log(chalk.dim('Running multi-model deliberation...\n'));
|
|
@@ -6114,6 +6113,155 @@ program
|
|
|
6114
6113
|
console.log(require('../package.json').version);
|
|
6115
6114
|
});
|
|
6116
6115
|
|
|
6116
|
+
// LED-1048: delimit wrap — gate any AI-assisted CLI with signed attestation + replay
|
|
6117
|
+
// Surface 1 CLI-pipe extension. Cross-model-agnostic (claude -p, cursor, aider, codex, ...).
|
|
6118
|
+
// Advisory by default; opt-in --enforce to block on policy violations.
|
|
6119
|
+
program
|
|
6120
|
+
.command('wrap')
|
|
6121
|
+
.description('Gate an AI-assisted CLI invocation with signed attestation (advisory-first)')
|
|
6122
|
+
.argument('<cmd...>', 'The command to wrap (e.g. `claude -p "add tests"`)')
|
|
6123
|
+
.option('--enforce', 'Block exit on policy violation (default: advisory)', false)
|
|
6124
|
+
.option('--deliberate', 'Also run multi-model deliberation (advisory)', false)
|
|
6125
|
+
.option('--no-attest', 'Skip attestation emission (dry run)')
|
|
6126
|
+
.option('--max-time <seconds>', 'Kill switch: SIGKILL the wrapped command after N seconds (liability_incident attestation + handoff)', parseInt)
|
|
6127
|
+
.option('--json', 'Output result as JSON', false)
|
|
6128
|
+
.action(async (cmdParts, options) => {
|
|
6129
|
+
const { runWrap, replayUrl } = require('../lib/wrap-engine');
|
|
6130
|
+
try {
|
|
6131
|
+
const result = await runWrap(cmdParts, {
|
|
6132
|
+
enforce: !!options.enforce,
|
|
6133
|
+
deliberate: !!options.deliberate,
|
|
6134
|
+
attest: options.attest !== false,
|
|
6135
|
+
maxTimeSeconds: options.maxTime || 0,
|
|
6136
|
+
cwd: process.cwd(),
|
|
6137
|
+
});
|
|
6138
|
+
|
|
6139
|
+
if (result.error === 'quota_exceeded') {
|
|
6140
|
+
console.error(chalk.red(`\n [wrap] ${result.message}\n`));
|
|
6141
|
+
process.exit(1);
|
|
6142
|
+
return;
|
|
6143
|
+
}
|
|
6144
|
+
|
|
6145
|
+
if (options.json) {
|
|
6146
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6147
|
+
process.exit(result.exit);
|
|
6148
|
+
return;
|
|
6149
|
+
}
|
|
6150
|
+
|
|
6151
|
+
console.log();
|
|
6152
|
+
const banner = result.kind === 'liability_incident'
|
|
6153
|
+
? chalk.bold.red(` delimit wrap — ${result.attestation_id || '(no attestation)'} [liability_incident]`)
|
|
6154
|
+
: chalk.bold.cyan(` delimit wrap — ${result.attestation_id || '(no attestation)'}`);
|
|
6155
|
+
console.log(banner);
|
|
6156
|
+
console.log(chalk.gray(` wrapped exit: ${result.wrapped_exit} mode: ${result.advisory ? 'advisory' : 'enforce'} tier: ${result.tier || 'n/a'}`));
|
|
6157
|
+
if (result.killed_by_timeout) {
|
|
6158
|
+
console.log(chalk.yellow(` ⚠ kill switch fired — wrapped command exceeded --max-time`));
|
|
6159
|
+
}
|
|
6160
|
+
if (result.gates.length) {
|
|
6161
|
+
console.log();
|
|
6162
|
+
for (const g of result.gates) {
|
|
6163
|
+
const tag = g.exit === undefined
|
|
6164
|
+
? chalk.gray('·')
|
|
6165
|
+
: (g.exit === 0 ? chalk.green('✓') : chalk.red('✗'));
|
|
6166
|
+
const name = g.name + (g.runner ? ':' + g.runner : '') + (g.spec ? ':' + path.basename(g.spec) : '');
|
|
6167
|
+
const note = g.result ? ` (${g.result})` : '';
|
|
6168
|
+
console.log(` ${tag} ${name}${note}`);
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
if (result.violations.length) {
|
|
6172
|
+
console.log();
|
|
6173
|
+
console.log(chalk.yellow(` ${result.violations.length} violation(s):`));
|
|
6174
|
+
for (const v of result.violations) console.log(chalk.yellow(` - ${v}`));
|
|
6175
|
+
if (result.advisory) console.log(chalk.gray(' advisory mode — wrap exit unaffected. Re-run with --enforce to block.'));
|
|
6176
|
+
}
|
|
6177
|
+
if (result.attestation_id) {
|
|
6178
|
+
console.log();
|
|
6179
|
+
console.log(chalk.gray(` attestation: ${result.attestation_path || '(not saved)'}`));
|
|
6180
|
+
console.log(chalk.gray(` replay: ${result.replay_url}`));
|
|
6181
|
+
}
|
|
6182
|
+
if (result.handoff_suggestion) {
|
|
6183
|
+
console.log();
|
|
6184
|
+
console.log(chalk.bold(' cross-model handoff suggestion:'));
|
|
6185
|
+
console.log(` ${chalk.cyan(result.handoff_suggestion.suggested_command)}`);
|
|
6186
|
+
console.log(chalk.gray(` (alternates: ${result.handoff_suggestion.alternates.join(', ')})`));
|
|
6187
|
+
}
|
|
6188
|
+
console.log();
|
|
6189
|
+
process.exit(result.exit);
|
|
6190
|
+
} catch (e) {
|
|
6191
|
+
console.error(chalk.red(`\n [wrap] ${e.message || e}\n`));
|
|
6192
|
+
process.exit(1);
|
|
6193
|
+
}
|
|
6194
|
+
});
|
|
6195
|
+
|
|
6196
|
+
// LED-1018 Venture #6 MVP: trust-page + ai-sbom subcommands
|
|
6197
|
+
// Render aggregated attestations (from `delimit wrap`) into a public static
|
|
6198
|
+
// trust page (HTML + JSON Feed) and a CycloneDX-AI bill of materials.
|
|
6199
|
+
program
|
|
6200
|
+
.command('trust-page')
|
|
6201
|
+
.description('Render attestations into a public trust page (static HTML + JSON feed)')
|
|
6202
|
+
.option('-d, --dir <path>', 'Attestation directory', path.join(os.homedir(), '.delimit', 'attestations'))
|
|
6203
|
+
.option('-o, --out <path>', 'Output directory', './trust-page')
|
|
6204
|
+
.option('-t, --title <title>', 'Trust page title', 'Trust Page')
|
|
6205
|
+
.option('--json', 'Output result as JSON', false)
|
|
6206
|
+
.action(async (options) => {
|
|
6207
|
+
const { renderTrustPage } = require('../lib/trust-page-engine');
|
|
6208
|
+
try {
|
|
6209
|
+
const result = renderTrustPage(options.dir, options.out, options.title);
|
|
6210
|
+
if (options.json) {
|
|
6211
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6212
|
+
return;
|
|
6213
|
+
}
|
|
6214
|
+
console.log();
|
|
6215
|
+
console.log(chalk.bold.cyan(` delimit trust-page`));
|
|
6216
|
+
console.log(chalk.gray(` source: ${options.dir}`));
|
|
6217
|
+
console.log(chalk.gray(` output: ${result.outDir}/`));
|
|
6218
|
+
console.log(chalk.gray(` attestations rendered: ${result.count}`));
|
|
6219
|
+
console.log(chalk.gray(` feed items: ${result.feed_items} html bytes: ${result.html_bytes}`));
|
|
6220
|
+
console.log();
|
|
6221
|
+
console.log(chalk.bold(` Open: ${path.resolve(result.outDir)}/index.html`));
|
|
6222
|
+
console.log();
|
|
6223
|
+
} catch (e) {
|
|
6224
|
+
console.error(chalk.red(`\n [trust-page] ${e.message || e}\n`));
|
|
6225
|
+
process.exit(1);
|
|
6226
|
+
}
|
|
6227
|
+
});
|
|
6228
|
+
|
|
6229
|
+
program
|
|
6230
|
+
.command('ai-sbom')
|
|
6231
|
+
.description('Build a CycloneDX-AI bill of materials from attestations')
|
|
6232
|
+
.option('-d, --dir <path>', 'Attestation directory', path.join(os.homedir(), '.delimit', 'attestations'))
|
|
6233
|
+
.option('-o, --out <path>', 'Output file', './ai-sbom.json')
|
|
6234
|
+
.option('-n, --name <name>', 'BOM subject name', 'ai-sbom')
|
|
6235
|
+
.option('-v, --package-version <v>', 'BOM subject version', '1.0.0')
|
|
6236
|
+
.option('--json', 'Print the SBOM to stdout instead of writing to file', false)
|
|
6237
|
+
.action(async (options) => {
|
|
6238
|
+
const { buildAISBOM } = require('../lib/ai-sbom-engine');
|
|
6239
|
+
try {
|
|
6240
|
+
const { sbom, aggregate, attestation_count } = buildAISBOM(options.dir, {
|
|
6241
|
+
name: options.name,
|
|
6242
|
+
version: options.packageVersion,
|
|
6243
|
+
});
|
|
6244
|
+
if (options.json) {
|
|
6245
|
+
console.log(JSON.stringify(sbom, null, 2));
|
|
6246
|
+
return;
|
|
6247
|
+
}
|
|
6248
|
+
fs.writeFileSync(options.out, JSON.stringify(sbom, null, 2));
|
|
6249
|
+
console.log();
|
|
6250
|
+
console.log(chalk.bold.cyan(` delimit ai-sbom`));
|
|
6251
|
+
console.log(chalk.gray(` source: ${options.dir}`));
|
|
6252
|
+
console.log(chalk.gray(` output: ${path.resolve(options.out)}`));
|
|
6253
|
+
console.log(chalk.gray(` attestations scanned: ${attestation_count}`));
|
|
6254
|
+
console.log(chalk.gray(` models detected: ${aggregate.models.length}`));
|
|
6255
|
+
console.log(chalk.gray(` tool-call surface: ${aggregate.tool_calls.length}`));
|
|
6256
|
+
console.log(chalk.gray(` total gates run: ${aggregate.total_gates_run}`));
|
|
6257
|
+
console.log(chalk.gray(` total violations: ${aggregate.total_violations}`));
|
|
6258
|
+
console.log();
|
|
6259
|
+
} catch (e) {
|
|
6260
|
+
console.error(chalk.red(`\n [ai-sbom] ${e.message || e}\n`));
|
|
6261
|
+
process.exit(1);
|
|
6262
|
+
}
|
|
6263
|
+
});
|
|
6264
|
+
|
|
6117
6265
|
// Hide legacy/internal commands from --help
|
|
6118
6266
|
['install', 'mode', 'status', 'policy', 'auth', 'audit',
|
|
6119
6267
|
'explain-decision', 'uninstall', 'proxy', 'hook'].forEach(name => {
|
package/bin/delimit-setup.js
CHANGED
|
@@ -780,15 +780,30 @@ delimit_exit_screen() {
|
|
|
780
780
|
else
|
|
781
781
|
DURATION="\${ELAPSED}s"
|
|
782
782
|
fi
|
|
783
|
-
# Count git commits made during session
|
|
783
|
+
# Count git commits made during session. SESSION_CWD is captured at shim
|
|
784
|
+
# launch; commits made in other repos during the session would be missed.
|
|
785
|
+
# Scan a best-effort set of known roots plus the launch cwd.
|
|
784
786
|
COMMITS=0
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
787
|
+
# Customer-facing: scan launch cwd, its parent, and common project roots.
|
|
788
|
+
# If an org or solo dev keeps multiple repos in \$HOME or \$HOME/code, commits
|
|
789
|
+
# there during a session get counted.
|
|
790
|
+
REPO_ROOTS="\$SESSION_CWD"
|
|
791
|
+
for parent in "\$SESSION_CWD/.." "\$HOME" "\$HOME/code" "\$HOME/src" "\$HOME/projects"; do
|
|
792
|
+
[ -d "\$parent" ] || continue
|
|
793
|
+
for d in "\$parent"/*/.git; do
|
|
794
|
+
[ -d "\$d" ] && REPO_ROOTS="\$REPO_ROOTS \$(dirname \$d)"
|
|
795
|
+
done
|
|
796
|
+
done
|
|
797
|
+
for r in \$REPO_ROOTS; do
|
|
798
|
+
[ -d "\$r/.git" ] || continue
|
|
799
|
+
C=\$(git -C "\$r" log --after="@\$SESSION_START" --format="%H" 2>/dev/null | wc -l | tr -d ' ')
|
|
800
|
+
COMMITS=\$((COMMITS + C))
|
|
801
|
+
done
|
|
802
|
+
# Count ledger items created during session (by timestamp).
|
|
803
|
+
# Ledger JSON is written with ": " (space) between key and value, and ISO
|
|
804
|
+
# timestamps end with "Z" (UTC), so the regex must tolerate both.
|
|
789
805
|
LEDGER_DIR="\$DELIMIT_HOME/ledger"
|
|
790
806
|
LEDGER_ITEMS=0
|
|
791
|
-
# Convert epoch SESSION_START to ISO prefix for string comparison
|
|
792
807
|
SESSION_ISO=\$(date -u -d "@\$SESSION_START" +%Y-%m-%dT%H:%M:%S 2>/dev/null || date -u -r "\$SESSION_START" +%Y-%m-%dT%H:%M:%S 2>/dev/null || echo "")
|
|
793
808
|
if [ -d "\$LEDGER_DIR" ] && [ -n "\$SESSION_ISO" ]; then
|
|
794
809
|
for lf in "\$LEDGER_DIR"/*.jsonl; do
|
|
@@ -796,7 +811,7 @@ delimit_exit_screen() {
|
|
|
796
811
|
COUNT=\$(awk -v start="\$SESSION_ISO" '
|
|
797
812
|
BEGIN { n=0 }
|
|
798
813
|
{
|
|
799
|
-
if (match(\$0, /"created_at":"([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2})"/, arr)) {
|
|
814
|
+
if (match(\$0, /"created_at"[[:space:]]*:[[:space:]]*"([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2})Z?"/, arr)) {
|
|
800
815
|
if (arr[1] >= start) n++
|
|
801
816
|
}
|
|
802
817
|
}
|
|
@@ -18,11 +18,38 @@ from typing import Any, Dict, List, Optional
|
|
|
18
18
|
AGENTS_DIR = Path.home() / ".delimit" / "agents"
|
|
19
19
|
TASKS_FILE = AGENTS_DIR / "tasks.json"
|
|
20
20
|
AUDIT_FILE = AGENTS_DIR / "audit.jsonl"
|
|
21
|
+
PAUSE_FILE = Path.home() / ".delimit" / "pause_dispatch"
|
|
21
22
|
|
|
22
23
|
VALID_PRIORITIES = {"P0", "P1", "P2"}
|
|
23
24
|
VALID_ASSIGNEES = {"claude", "codex", "gemini", "any"}
|
|
24
25
|
VALID_STATUSES = {"dispatched", "in_progress", "done", "handed_off", "failed"}
|
|
25
26
|
|
|
27
|
+
# LED-876: auto-pause when dead-letter queue depth (stuck 'dispatched' tasks)
|
|
28
|
+
# hits this threshold. Prevents runaway dispatch when no workers are pulling.
|
|
29
|
+
DLQ_AUTO_PAUSE_THRESHOLD = 20
|
|
30
|
+
|
|
31
|
+
# LED-878: router table — resolves assignee='any' to a specific model at
|
|
32
|
+
# dispatch time based on task_type. This eliminates the dead-letter 'any'
|
|
33
|
+
# bucket without requiring a worker process to exist yet. The mapping is
|
|
34
|
+
# deliberately conservative: if the task type is unknown, fall through to
|
|
35
|
+
# gemini (cheapest, highest throughput) rather than pile onto claude.
|
|
36
|
+
TASK_TYPE_ROUTER = {
|
|
37
|
+
# Outreach and social work — Gemini Flash is fast and cheap
|
|
38
|
+
"outreach": "gemini",
|
|
39
|
+
"social": "gemini",
|
|
40
|
+
"content": "gemini",
|
|
41
|
+
"sensor": "gemini",
|
|
42
|
+
# Engineering — Claude / Codex for code, Claude for governance
|
|
43
|
+
"fix": "claude",
|
|
44
|
+
"feat": "claude",
|
|
45
|
+
"refactor": "claude",
|
|
46
|
+
"test": "codex",
|
|
47
|
+
"research": "gemini",
|
|
48
|
+
"strategy": "gemini",
|
|
49
|
+
"deliberation": "claude",
|
|
50
|
+
}
|
|
51
|
+
ROUTER_DEFAULT_ASSIGNEE = "gemini"
|
|
52
|
+
|
|
26
53
|
|
|
27
54
|
def _ensure_dir():
|
|
28
55
|
"""Create the agents directory if it doesn't exist."""
|
|
@@ -74,10 +101,62 @@ def dispatch_task(
|
|
|
74
101
|
if not title or not title.strip():
|
|
75
102
|
return {"error": "title is required"}
|
|
76
103
|
|
|
104
|
+
# LED-876: reject ghost "[VENTURE] Engage: on x" titles with empty author
|
|
105
|
+
# slot. The social_target fix drops these at the scanner, but keep this as
|
|
106
|
+
# a belt-and-suspenders check since agent_dispatch has other callers too.
|
|
107
|
+
stripped = title.strip()
|
|
108
|
+
if "Engage: on " in stripped or "Engage: on " in stripped:
|
|
109
|
+
return {"error": f"rejected ghost engage task with empty author: {stripped!r}"}
|
|
110
|
+
|
|
111
|
+
# LED-876: manual kill switch. Touch ~/.delimit/pause_dispatch to halt all
|
|
112
|
+
# dispatches instantly without touching loop_config. Remove the file to
|
|
113
|
+
# resume. Kept deliberately simple so it works from any shell.
|
|
114
|
+
if PAUSE_FILE.exists():
|
|
115
|
+
_append_audit({
|
|
116
|
+
"action": "dispatch_rejected_paused",
|
|
117
|
+
"title": stripped,
|
|
118
|
+
"reason": str(PAUSE_FILE),
|
|
119
|
+
})
|
|
120
|
+
return {"error": f"dispatch paused: {PAUSE_FILE} exists"}
|
|
121
|
+
|
|
122
|
+
# LED-876: automatic circuit breaker. If the DLQ (count of 'dispatched'
|
|
123
|
+
# tasks that never moved to in_progress/done/failed) exceeds the threshold,
|
|
124
|
+
# auto-create the pause file and reject. This stops the cycle from growing
|
|
125
|
+
# the queue unboundedly when workers aren't consuming.
|
|
126
|
+
existing_tasks = _load_tasks()
|
|
127
|
+
dlq_depth = sum(1 for t in existing_tasks.values() if t.get("status") == "dispatched")
|
|
128
|
+
if dlq_depth >= DLQ_AUTO_PAUSE_THRESHOLD:
|
|
129
|
+
PAUSE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
PAUSE_FILE.write_text(
|
|
131
|
+
f"auto-paused at {time.strftime('%Y-%m-%dT%H:%M:%SZ')} "
|
|
132
|
+
f"(dlq_depth={dlq_depth} >= {DLQ_AUTO_PAUSE_THRESHOLD})\n"
|
|
133
|
+
)
|
|
134
|
+
_append_audit({
|
|
135
|
+
"action": "dispatch_auto_paused",
|
|
136
|
+
"dlq_depth": dlq_depth,
|
|
137
|
+
"threshold": DLQ_AUTO_PAUSE_THRESHOLD,
|
|
138
|
+
})
|
|
139
|
+
return {
|
|
140
|
+
"error": (
|
|
141
|
+
f"auto-paused: DLQ depth {dlq_depth} >= {DLQ_AUTO_PAUSE_THRESHOLD}. "
|
|
142
|
+
f"Clear stuck tasks then delete {PAUSE_FILE} to resume."
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
77
146
|
assignee = assignee.lower().strip() if assignee else "any"
|
|
78
147
|
if assignee not in VALID_ASSIGNEES:
|
|
79
148
|
return {"error": f"assignee must be one of: {', '.join(sorted(VALID_ASSIGNEES))}"}
|
|
80
149
|
|
|
150
|
+
# LED-878: resolve 'any' to a specific model via the router table so
|
|
151
|
+
# tasks never land in a bucket no worker pulls from. The mapping uses
|
|
152
|
+
# task_type as the primary key; if unknown, falls through to the
|
|
153
|
+
# default (gemini — cheapest + highest throughput).
|
|
154
|
+
if assignee == "any":
|
|
155
|
+
tt = (task_type or "").lower().strip()
|
|
156
|
+
routed = TASK_TYPE_ROUTER.get(tt, ROUTER_DEFAULT_ASSIGNEE)
|
|
157
|
+
if routed in VALID_ASSIGNEES and routed != "any":
|
|
158
|
+
assignee = routed
|
|
159
|
+
|
|
81
160
|
priority = priority.upper().strip() if priority else "P1"
|
|
82
161
|
if priority not in VALID_PRIORITIES:
|
|
83
162
|
return {"error": f"priority must be one of: {', '.join(sorted(VALID_PRIORITIES))}"}
|