talos-code 0.1.0 → 0.2.1
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 +112 -0
- package/README.md +9 -4
- package/dist/args.js +4 -1
- package/dist/commands/provider-cli.js +146 -87
- package/dist/config/types.js +1 -1
- package/dist/i18n/en/approval.js +3 -0
- package/dist/i18n/en/credentials.js +10 -0
- package/dist/i18n/en/errors.js +21 -0
- package/dist/i18n/en/firstrun.js +12 -0
- package/dist/i18n/en/screen.js +38 -0
- package/dist/i18n/error-view.js +1 -1
- package/dist/main.js +2 -1
- package/dist/provider/openrouter-login.js +172 -0
- package/dist/runtime/context-archive.js +69 -0
- package/dist/runtime/repo.js +11 -0
- package/dist/runtime/supervisor.js +5 -2
- package/dist/runtime/talos-composition.js +9 -3
- package/dist/tui/agent-view.js +51 -0
- package/dist/tui/app.js +364 -27
- package/dist/tui/catalog-service.js +2 -1
- package/dist/tui/components/status-indicator.js +10 -4
- package/dist/tui/components/terminal-shell.js +10 -1
- package/dist/tui/descendant-approvals.js +104 -0
- package/dist/tui/launch-state.js +9 -0
- package/dist/tui/live-activity.js +13 -2
- package/dist/tui/overlays/agent-tree.js +7 -3
- package/dist/tui/overlays/approval-dialog.js +3 -1
- package/dist/tui/overlays/model-picker.js +5 -0
- package/dist/tui/overlays/provider-picker.js +26 -0
- package/dist/tui/project-trust-prompt.js +5 -3
- package/dist/tui/session-controller.js +25 -12
- package/dist/tui/setup-wizard.js +46 -0
- package/dist/tui/slash-commands.js +3 -1
- package/dist/tui/theme-catalog.js +3 -1
- package/dist/version.js +1 -1
- package/dist/workspace/checkpoint-store.js +32 -1
- package/dist/workspace/checkpoint.js +40 -12
- package/package.json +3 -1
- package/vendor/harness-ui/src/kernel/talosHarness.mjs +149 -2
- package/vendor/manifest.json +2 -2
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { resolveWorkspaceIdentity } from "../security/workspace-identity.js";
|
|
7
7
|
export const CHECKPOINT_LIMITS = { maxEntries: 50_000, maxStoredBytes: 256 * 1024 * 1024, maxStoredFileBytes: 64 * 1024 * 1024 };
|
|
8
|
+
const RACY_MARGIN_MS = 2_000, BLOB_SYNC_BATCH = 16;
|
|
8
9
|
function coded(code, message = code, details = {}) { return Object.assign(new Error(message === code ? code : `${code}: ${message}`), { code, details }); }
|
|
9
10
|
function sha256(data) { return createHash('sha256').update(data).digest('hex'); }
|
|
10
11
|
function slash(value) { return value.split(path.sep).join('/'); }
|
|
@@ -59,15 +60,15 @@ catch (error) {
|
|
|
59
60
|
if (process.platform !== 'win32')
|
|
60
61
|
throw error;
|
|
61
62
|
} }
|
|
62
|
-
async function storeBlob(blobRoot, data) {
|
|
63
|
+
async function storeBlob(blobRoot, data, budget) {
|
|
63
64
|
const hash = sha256(data), target = path.join(blobRoot, hash);
|
|
64
|
-
|
|
65
|
+
if (budget.blobs.has(hash))
|
|
66
|
+
return hash;
|
|
65
67
|
const temp = path.join(blobRoot, `.${hash}.${process.pid}.tmp`);
|
|
66
68
|
try {
|
|
67
69
|
const handle = await open(temp, 'wx', 0o600);
|
|
68
70
|
try {
|
|
69
71
|
await handle.writeFile(data);
|
|
70
|
-
await handle.sync();
|
|
71
72
|
}
|
|
72
73
|
finally {
|
|
73
74
|
await handle.close();
|
|
@@ -76,14 +77,35 @@ async function storeBlob(blobRoot, data) {
|
|
|
76
77
|
await rm(temp, { force: true });
|
|
77
78
|
return;
|
|
78
79
|
} throw error; });
|
|
79
|
-
await durableDir(blobRoot);
|
|
80
80
|
}
|
|
81
81
|
catch (error) {
|
|
82
82
|
if (error?.code !== 'EEXIST')
|
|
83
83
|
throw error;
|
|
84
84
|
}
|
|
85
|
+
budget.blobs.add(hash);
|
|
86
|
+
budget.pending.push(target);
|
|
85
87
|
return hash;
|
|
86
88
|
}
|
|
89
|
+
/* Every blob written by this capture is durable before the capture returns (and so before its record is written); a sync
|
|
90
|
+
error is never swallowed (gate E8/E9). */
|
|
91
|
+
async function syncNewBlobs(blobRoot, budget) {
|
|
92
|
+
if (budget.pending.length === 0)
|
|
93
|
+
return;
|
|
94
|
+
for (let i = 0; i < budget.pending.length; i += BLOB_SYNC_BATCH)
|
|
95
|
+
await Promise.all(budget.pending.slice(i, i + BLOB_SYNC_BATCH).map(durableFile));
|
|
96
|
+
budget.pending = [];
|
|
97
|
+
await durableDir(blobRoot);
|
|
98
|
+
}
|
|
99
|
+
async function assertFileContained(workspace, absolute, budget) {
|
|
100
|
+
const dir = path.dirname(absolute);
|
|
101
|
+
let canonical = budget.dirs.get(dir);
|
|
102
|
+
if (canonical === undefined) {
|
|
103
|
+
canonical = await realpath(dir);
|
|
104
|
+
budget.dirs.set(dir, canonical);
|
|
105
|
+
}
|
|
106
|
+
if (!inside(workspace.canonicalRoot, canonical))
|
|
107
|
+
throw coded('CHECKPOINT_PATH_ESCAPE', `Checkpoint path escapes workspace: ${absolute}`);
|
|
108
|
+
}
|
|
87
109
|
function consumeFileBudget(budget, size) {
|
|
88
110
|
if (size > budget.limits.maxStoredFileBytes)
|
|
89
111
|
throw coded('CHECKPOINT_FILE_TOO_LARGE');
|
|
@@ -178,10 +200,13 @@ async function captureActualFile(workspace, absolute, relative, blobRoot, budget
|
|
|
178
200
|
}
|
|
179
201
|
if (!stat.isFile())
|
|
180
202
|
throw coded('CHECKPOINT_SPECIAL_FILE_UNSUPPORTED', `Unsupported workspace entry: ${relative}`);
|
|
181
|
-
await
|
|
203
|
+
await assertFileContained(workspace, absolute, budget);
|
|
182
204
|
consumeFileBudget(budget, stat.size);
|
|
183
|
-
const
|
|
184
|
-
const
|
|
205
|
+
const seen = budget.cache?.previous.get(relative);
|
|
206
|
+
const unchanged = Boolean(seen && seen.size === stat.size && seen.mtimeMs === stat.mtimeMs && seen.ctimeMs === stat.ctimeMs
|
|
207
|
+
&& stat.mtimeMs < seen.verifiedAtMs - RACY_MARGIN_MS && stat.ctimeMs < seen.verifiedAtMs - RACY_MARGIN_MS && budget.blobs.has(seen.ref));
|
|
208
|
+
const ref = unchanged ? seen.ref : await storeBlob(blobRoot, await readFile(absolute), budget);
|
|
209
|
+
budget.cache?.next.set(relative, { size: stat.size, mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs, ref, verifiedAtMs: budget.startedAtMs });
|
|
185
210
|
const executable = process.platform === 'win32' ? indexExecutable : Boolean(stat.mode & 0o111);
|
|
186
211
|
return { kind: 'file', source: 'blob', ref, size: stat.size, mode: stat.mode & 0o777, executable };
|
|
187
212
|
}
|
|
@@ -249,7 +274,7 @@ async function captureGit(workspace, blobRoot, budget) {
|
|
|
249
274
|
continue;
|
|
250
275
|
}
|
|
251
276
|
consumeEntry(budget);
|
|
252
|
-
await
|
|
277
|
+
await assertFileContained(workspace, absolute, budget);
|
|
253
278
|
entries[relative] = { kind: 'file', source: 'git', ref: oid, size: stat.size, mode: stat.mode & 0o777, executable: mode === '100755', gitPath };
|
|
254
279
|
}
|
|
255
280
|
for (const gitPath of nulList(untrackedRaw)) {
|
|
@@ -302,7 +327,7 @@ async function captureNonGit(workspace, blobRoot, budget) {
|
|
|
302
327
|
}
|
|
303
328
|
if (!stat.isFile())
|
|
304
329
|
throw coded('CHECKPOINT_SPECIAL_FILE_UNSUPPORTED', `Unsupported workspace entry: ${relative}`);
|
|
305
|
-
await
|
|
330
|
+
await assertFileContained(workspace, absolute, budget);
|
|
306
331
|
consumeFileBudget(budget, stat.size);
|
|
307
332
|
}
|
|
308
333
|
}
|
|
@@ -328,14 +353,17 @@ async function captureNonGit(workspace, blobRoot, budget) {
|
|
|
328
353
|
await walk(workspace.canonicalRoot, '');
|
|
329
354
|
return { capturedAt: new Date().toISOString(), workspace: { canonicalRoot: workspace.canonicalRoot, identityHash: workspace.identityHash, gitRoot: null }, entries, git: null, coverage: { tracked: false, untrackedNonIgnored: true, ignoredUntracked: true, gitMetadataRestored: false }, exclusions: ['Filesystem metadata other than file content, executable bit and symlink target is outside rollback coverage.', 'Empty-directory existence is outside rollback coverage.'] };
|
|
330
355
|
}
|
|
331
|
-
export async function captureWorkspaceSnapshot({ projectRoot, blobRoot, limits = CHECKPOINT_LIMITS }) {
|
|
356
|
+
export async function captureWorkspaceSnapshot({ projectRoot, blobRoot, limits = CHECKPOINT_LIMITS, statCache }) {
|
|
357
|
+
const startedAtMs = Date.now();
|
|
332
358
|
const workspace = await resolveWorkspaceIdentity({ projectRoot });
|
|
333
359
|
await ensurePrivateDir(blobRoot);
|
|
334
360
|
const canonicalBlobRoot = await realpath(blobRoot);
|
|
335
361
|
if (inside(workspace.canonicalRoot, canonicalBlobRoot))
|
|
336
362
|
throw coded('CHECKPOINT_STORE_INSIDE_WORKSPACE');
|
|
337
|
-
const budget = { entries: 0, storedBytes: 0, limits };
|
|
338
|
-
|
|
363
|
+
const budget = { entries: 0, storedBytes: 0, limits, startedAtMs, blobs: new Set(await readdir(canonicalBlobRoot)), pending: [], dirs: new Map(), cache: statCache ?? null };
|
|
364
|
+
const snapshot = workspace.gitRoot ? await captureGit(workspace, canonicalBlobRoot, budget) : await captureNonGit(workspace, canonicalBlobRoot, budget);
|
|
365
|
+
await syncNewBlobs(canonicalBlobRoot, budget);
|
|
366
|
+
return snapshot;
|
|
339
367
|
}
|
|
340
368
|
export function checkpointMutationAllowed(mode) { return mode !== 'plan'; }
|
|
341
369
|
export function extractWorkspaceWriteEvidence(raw) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talos-code",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "TALOS in the terminal: a coding agent with per-tool permissions, file checkpoints and resumable sessions, on the model you choose.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coding-agent",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"@microsoft/mxc-sdk": "0.8.0",
|
|
35
35
|
"@modelcontextprotocol/client": "2.0.0",
|
|
36
36
|
"@napi-rs/keyring": "1.3.0",
|
|
37
|
+
"@vscode/ripgrep": "1.18.0",
|
|
37
38
|
"ai": "7.0.93",
|
|
38
39
|
"diff": "9.0.0",
|
|
39
40
|
"docx": "9.5.1",
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"dist/",
|
|
62
63
|
"vendor/",
|
|
63
64
|
"README.md",
|
|
65
|
+
"CHANGELOG.md",
|
|
64
66
|
"LICENSE",
|
|
65
67
|
"THIRD_PARTY_NOTICES.md"
|
|
66
68
|
]
|
|
@@ -3662,6 +3662,123 @@ async function tuttiIPercorsi(disco, { filtro = null } = {}) {
|
|
|
3662
3662
|
return { percorsi, dimensioni, stato }
|
|
3663
3663
|
}
|
|
3664
3664
|
|
|
3665
|
+
/**
|
|
3666
|
+
* ⭐⭐⭐ P18 fase 3 (owner 26/09/2026: «TALOS più veloce di tutti»; decisioni: ripgrep DENTRO il pacchetto,
|
|
3667
|
+
* righe trovate con un tetto) — `cerca` nel contenuto con ripgrep.
|
|
3668
|
+
*
|
|
3669
|
+
* Misurato sul banco ermetico (`cli/scripts/bench-harness.ts`, 26/09): quattro `cerca` su un albero di 6.000 file
|
|
3670
|
+
* costavano **9,3 s** (una lettura JS alla volta, fino a `MAX_FILE_LETTI`), e su questo monorepo (6.405 file visibili)
|
|
3671
|
+
* la ricerca si fermava a 5.000 letture: **incompleta**, e una parola assente risultava «non c'e'». Pi fa `rg --json`
|
|
3672
|
+
* (`coding-agent/src/core/tools/grep.ts:119-168`), Claude Code ha ripgrep incluso (`USE_BUILTIN_RIPGREP`), Codex lo usa
|
|
3673
|
+
* dalla shell. ⇒ Stessa semantica di prima (sottostringa, maiuscole indifferenti, `.gitignore` anche fuori da un repo,
|
|
3674
|
+
* `.git`/`node_modules` sempre fuori, i binari saltati), ma ogni risultato porta **riga e testo** (`percorso:riga:testo`):
|
|
3675
|
+
* il modello non deve riaprire il file per sapere dove.
|
|
3676
|
+
*
|
|
3677
|
+
* Il binario: `TALOS_RG_PATH` (la CLI lo mette da `@vscode/ripgrep`, che e' nel suo pacchetto, senza rete), altrimenti
|
|
3678
|
+
* `@vscode/ripgrep` se risolvibile da qui. ⛔ Assente, rotto o in timeout ⇒ `null`, e `cercaNelProgetto` prosegue con la
|
|
3679
|
+
* camminata JS di sempre: nessuna installazione perde la ricerca.
|
|
3680
|
+
*/
|
|
3681
|
+
const MAX_CERCA_INSIEME = 8
|
|
3682
|
+
const MAX_RIGHE_RG = 100
|
|
3683
|
+
const MAX_RIGHE_PER_FILE = 5
|
|
3684
|
+
const MAX_COLONNE_RG = 200
|
|
3685
|
+
const MAX_BYTE_RG = 4_000_000
|
|
3686
|
+
const TEMPO_MAX_RG_MS = 20_000
|
|
3687
|
+
let ripgrepRisolto = null
|
|
3688
|
+
function percorsoRipgrep() {
|
|
3689
|
+
const daAmbiente = process.env.TALOS_RG_PATH
|
|
3690
|
+
if (daAmbiente) return Promise.resolve(existsSync(daAmbiente) ? daAmbiente : null)
|
|
3691
|
+
ripgrepRisolto ??= import('@vscode/ripgrep').then(
|
|
3692
|
+
(m) => (typeof m?.rgPath === 'string' && existsSync(m.rgPath) ? m.rgPath : null),
|
|
3693
|
+
() => null)
|
|
3694
|
+
return ripgrepRisolto
|
|
3695
|
+
}
|
|
3696
|
+
function eseguiRipgrep(rg, argomenti, cartella, segnale) {
|
|
3697
|
+
return new Promise((fine) => {
|
|
3698
|
+
let figlio
|
|
3699
|
+
try { figlio = spawn(rg, argomenti, { cwd: cartella, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }) }
|
|
3700
|
+
catch { fine(null); return }
|
|
3701
|
+
const pezzi = []
|
|
3702
|
+
let byte = 0, tagliato = false
|
|
3703
|
+
const orologio = setTimeout(() => { tagliato = true; figlio.kill() }, TEMPO_MAX_RG_MS)
|
|
3704
|
+
/* Lo Stop ferma anche la ricerca IN VOLO (vincolo del desktop, 26/09): si uccide rg e si dice «fermata». */
|
|
3705
|
+
let fermato = false
|
|
3706
|
+
const alloStop = () => { fermato = true; figlio.kill() }
|
|
3707
|
+
if (segnale?.aborted) alloStop(); else segnale?.addEventListener?.('abort', alloStop, { once: true })
|
|
3708
|
+
figlio.stdout.on('data', (pezzo) => {
|
|
3709
|
+
if (tagliato) return
|
|
3710
|
+
byte += pezzo.length
|
|
3711
|
+
pezzi.push(pezzo)
|
|
3712
|
+
if (byte >= MAX_BYTE_RG) { tagliato = true; figlio.kill() }
|
|
3713
|
+
})
|
|
3714
|
+
figlio.on('error', () => { clearTimeout(orologio); fine(null) })
|
|
3715
|
+
figlio.on('close', (codice) => {
|
|
3716
|
+
clearTimeout(orologio)
|
|
3717
|
+
segnale?.removeEventListener?.('abort', alloStop)
|
|
3718
|
+
if (fermato) { fine({ testo: '', tagliato: false, scaduto: false, fermato: true }); return }
|
|
3719
|
+
const testo = Buffer.concat(pezzi).toString('utf8')
|
|
3720
|
+
/* 0 = trovato, 1 = niente; 2 = errori (un file illeggibile) con o senza risultati. Un 2 senza uscita,
|
|
3721
|
+
o un processo ucciso senza niente, non e' «non c'e'»: e' un guasto, e decide il ripiego JS. */
|
|
3722
|
+
if (codice === 0 || codice === 1 || testo) fine({ testo, tagliato: tagliato && byte >= MAX_BYTE_RG, scaduto: tagliato && byte < MAX_BYTE_RG })
|
|
3723
|
+
else fine(null)
|
|
3724
|
+
})
|
|
3725
|
+
})
|
|
3726
|
+
}
|
|
3727
|
+
const percorsoDaRg = (p) => p.replace(/\\/gu, '/').replace(/^\.\//u, '')
|
|
3728
|
+
async function cercaConRipgrep(radice, testo, chiaveNome, segnale) {
|
|
3729
|
+
const rg = await percorsoRipgrep()
|
|
3730
|
+
if (!rg) return null
|
|
3731
|
+
const comuni = ['--no-config', '--hidden', '--no-require-git', '--glob', '!.git', '--glob', '!node_modules']
|
|
3732
|
+
/* Il ripiego delle cartelle potate vale, come prima, solo quando non c'e' un `.gitignore` da cui leggere. */
|
|
3733
|
+
if (!existsSync(join(radice, '.gitignore'))) for (const c of POTATE_SENZA_GITIGNORE) comuni.push('--glob', `!${c}`)
|
|
3734
|
+
const ricerca = await eseguiRipgrep(rg, [...comuni, '--line-number', '--with-filename', '--no-heading', '--color', 'never',
|
|
3735
|
+
'--fixed-strings', '--ignore-case', '--max-count', String(MAX_RIGHE_PER_FILE), '--max-columns', String(MAX_COLONNE_RG),
|
|
3736
|
+
'--max-columns-preview', '--max-filesize', String(MAX_BYTE_FILE), '-e', testo, '--', '.'], radice, segnale)
|
|
3737
|
+
if (ricerca === null) return null
|
|
3738
|
+
if (ricerca.fermato) return 'stopped: the search was interrupted.'
|
|
3739
|
+
/* rg lavora in parallelo e non promette un ordine: si ordina qui, cosi' la stessa domanda da' la stessa risposta. */
|
|
3740
|
+
const perFile = new Map()
|
|
3741
|
+
for (const riga of ricerca.testo.split(/\r?\n/u)) {
|
|
3742
|
+
const m = /^(.+?):(\d+):(.*)$/u.exec(riga)
|
|
3743
|
+
if (!m) continue
|
|
3744
|
+
const percorso = percorsoDaRg(m[1])
|
|
3745
|
+
if (!perFile.has(percorso)) perFile.set(percorso, [])
|
|
3746
|
+
perFile.get(percorso).push(`${percorso}:${m[2]}:${m[3].trim()}`)
|
|
3747
|
+
}
|
|
3748
|
+
let perNome = []
|
|
3749
|
+
if (chiaveNome) {
|
|
3750
|
+
const elenco = await eseguiRipgrep(rg, [...comuni, '--files', '.'], radice, segnale)
|
|
3751
|
+
if (elenco === null) return null
|
|
3752
|
+
if (elenco.fermato) return 'stopped: the search was interrupted.'
|
|
3753
|
+
perNome = elenco.testo.split(/\r?\n/u).filter(Boolean).map(percorsoDaRg).filter((p) => p.toLowerCase().includes(chiaveNome)).sort()
|
|
3754
|
+
}
|
|
3755
|
+
const avvisi = []
|
|
3756
|
+
if (ricerca.scaduto) avvisi.push(`⚠ incomplete scan: the search took longer than ${TEMPO_MAX_RG_MS / 1000} s and was stopped. Search inside a subfolder.`)
|
|
3757
|
+
if (ricerca.tagliato) avvisi.push('⚠ incomplete scan: too much output — the matches below are a part of them. Narrow the search.')
|
|
3758
|
+
const coda = avvisi.length > 0 ? `\n${avvisi.join('\n')}` : ''
|
|
3759
|
+
const file = [...perFile.keys()].sort()
|
|
3760
|
+
if (file.length === 0 && perNome.length === 0) {
|
|
3761
|
+
return `no file matches "${testo}" (searched with ripgrep; .gitignore respected, binaries skipped). Try a shorter or different "testo".${coda}`
|
|
3762
|
+
}
|
|
3763
|
+
const righe = perNome.slice(0, MAX_RISULTATI)
|
|
3764
|
+
let righeTesto = 0, fileMostrati = 0
|
|
3765
|
+
for (const f of file) {
|
|
3766
|
+
if (fileMostrati >= MAX_RISULTATI || righeTesto >= MAX_RIGHE_RG) break
|
|
3767
|
+
const trovate = perFile.get(f).slice(0, MAX_RIGHE_RG - righeTesto)
|
|
3768
|
+
righe.push(...trovate)
|
|
3769
|
+
righeTesto += trovate.length
|
|
3770
|
+
fileMostrati += 1
|
|
3771
|
+
}
|
|
3772
|
+
const fileTagliati = file.length - fileMostrati
|
|
3773
|
+
const nomiTagliati = perNome.length - Math.min(perNome.length, MAX_RISULTATI)
|
|
3774
|
+
return righe.join('\n')
|
|
3775
|
+
+ (nomiTagliati > 0 ? `\n… and ${nomiTagliati} more paths matching "${chiaveNome}" not shown — narrow the search.` : '')
|
|
3776
|
+
// ⛔ Il taglio si DICHIARA, come nella camminata JS: «40 file» non deve leggersi come «sono 40».
|
|
3777
|
+
+ (fileTagliati > 0 ? `\n… and ${fileTagliati} more files with matches not shown — narrow the search.` : '')
|
|
3778
|
+
+ (file.some((f) => perFile.get(f).length >= MAX_RIGHE_PER_FILE) ? `\n(at most ${MAX_RIGHE_PER_FILE} matching lines per file are shown)` : '')
|
|
3779
|
+
+ coda
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3665
3782
|
/**
|
|
3666
3783
|
* ⭐⭐⭐ CERCA — l'attrezzo che toglie la cecita', senza comprare l'inventario.
|
|
3667
3784
|
*
|
|
@@ -3685,10 +3802,16 @@ async function tuttiIPercorsi(disco, { filtro = null } = {}) {
|
|
|
3685
3802
|
* @param {{radice?: string}} [opzioni] — la cartella VERA, per leggere il `.gitignore`.
|
|
3686
3803
|
* Assente (test, ponte del telefono) ⇒ si usa il ripiego `POTATE_SENZA_GITIGNORE`.
|
|
3687
3804
|
*/
|
|
3688
|
-
export async function cercaNelProgetto(disco, { testo, nome }, { radice } = {}) {
|
|
3805
|
+
export async function cercaNelProgetto(disco, { testo, nome }, { radice, segnale } = {}) {
|
|
3689
3806
|
const chiaveTesto = String(testo ?? '').trim().toLowerCase()
|
|
3690
3807
|
const chiaveNome = String(nome ?? '').trim().toLowerCase()
|
|
3691
3808
|
if (!chiaveTesto && !chiaveNome) return 'give at least one of "testo" or "nome".'
|
|
3809
|
+
/* P18 fase 3: con la cartella vera e un ripgrep disponibile, la ricerca nel contenuto la fa `rg`.
|
|
3810
|
+
`null` = rg assente o fallito: si prosegue con la camminata JS qui sotto, invariata. */
|
|
3811
|
+
if (chiaveTesto && radice) {
|
|
3812
|
+
const conRg = await cercaConRipgrep(radice, String(testo).trim(), chiaveNome, segnale)
|
|
3813
|
+
if (conRg !== null) return conRg
|
|
3814
|
+
}
|
|
3692
3815
|
|
|
3693
3816
|
const filtro = await filtroDelProgetto(radice)
|
|
3694
3817
|
const { percorsi, dimensioni, stato } = await tuttiIPercorsi(disco, { filtro })
|
|
@@ -8136,6 +8259,30 @@ export async function talosLavora({
|
|
|
8136
8259
|
break
|
|
8137
8260
|
}
|
|
8138
8261
|
|
|
8262
|
+
/*
|
|
8263
|
+
* ⭐⭐ P18 fase 2 (owner 26/09/2026, patch approvata dalla lane desktop) — le `cerca` della STESSA risposta
|
|
8264
|
+
* partono INSIEME, fino a `MAX_CERCA_INSIEME` (Hermes 8, `_MAX_TOOL_WORKERS`; Claude Code 10, i blocchi
|
|
8265
|
+
* «concurrency-safe» dello `StreamingToolExecutor`). Il ciclo qui sotto resta quello di sempre: hook,
|
|
8266
|
+
* approvazioni, eventi `tool-inizio`/`tool-esito` e risultati nella storia, tutto nell'ordine delle chiamate;
|
|
8267
|
+
* il ramo `cerca` aspetta la SUA ricerca già partita invece di avviarla.
|
|
8268
|
+
* ⛔ Solo `cerca`: e' pura (legge dentro la cartella, non chiede approvazioni) e un rifiuto del pre-hook su una
|
|
8269
|
+
* lettura e' gia' ignorato (vedi FASE A qui sotto). `leggi` no: un percorso fuori dal progetto chiede
|
|
8270
|
+
* l'approvazione, e quella va chiesta in ordine, prima di leggere.
|
|
8271
|
+
* ⛔ Lo Stop ferma anche quelle in volo: il segnale arriva fino a rg, che viene ucciso.
|
|
8272
|
+
* Misura (banco ermetico, 4 `cerca` su 6.000 file): 8.648 ms prima di P18, 377 con ripgrep in fila.
|
|
8273
|
+
*/
|
|
8274
|
+
const ricercheAvviate = new Map()
|
|
8275
|
+
const daCercare = chiamate.filter((c) => c.function?.name === 'cerca')
|
|
8276
|
+
if (daCercare.length > 1 && !segnaleStop?.aborted) {
|
|
8277
|
+
for (const c of daCercare.slice(0, MAX_CERCA_INSIEME)) {
|
|
8278
|
+
let argomentiCerca = {}
|
|
8279
|
+
try { argomentiCerca = JSON.parse(c.function?.arguments || '{}') } catch { /* vuoto: il ramo dira' cosa manca */ }
|
|
8280
|
+
const avviata = cercaNelProgetto(disco, argomentiCerca, { radice: cartella, segnale: segnaleStop })
|
|
8281
|
+
avviata.catch(() => {})
|
|
8282
|
+
ricercheAvviate.set(c, avviata)
|
|
8283
|
+
}
|
|
8284
|
+
}
|
|
8285
|
+
|
|
8139
8286
|
let fermatoDentroIlGiro = false
|
|
8140
8287
|
for (const c of chiamate) {
|
|
8141
8288
|
/*
|
|
@@ -8232,7 +8379,7 @@ export async function talosLavora({
|
|
|
8232
8379
|
* potare. Senza, `cerca` ricade sulla lista fissa — che e' il ripiego per il ponte
|
|
8233
8380
|
* del telefono e per i test, non il caso normale.
|
|
8234
8381
|
*/
|
|
8235
|
-
esito = await cercaNelProgetto(disco, argomenti, { radice: cartella })
|
|
8382
|
+
esito = await (ricercheAvviate.get(c) ?? cercaNelProgetto(disco, argomenti, { radice: cartella, segnale: segnaleStop }))
|
|
8236
8383
|
}
|
|
8237
8384
|
else if (nome === 'leggi') {
|
|
8238
8385
|
/*
|
package/vendor/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "talos.cli.vendor.v1",
|
|
3
|
-
"sourceCommit": "
|
|
3
|
+
"sourceCommit": "9a10705c95b865e72c431f8d8ad7873bcdd430b5",
|
|
4
4
|
"files": 163,
|
|
5
5
|
"sha256": {
|
|
6
6
|
"context-engine/package.json": "cd416c2ef02241365eca721ce1bed7a702cff34bb804d79e974f9f36731ced8a",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"harness-ui/src/image-generator.mjs": "8762739d578f81d1b881d24338518480e59fcae5b8a3ff6a071947652bec8008",
|
|
74
74
|
"harness-ui/src/istruzioni-di-progetto.mjs": "a0c2c9dec9d119799e3bc02b1aee99ffa996d2c141ba679345f7309495a29cff",
|
|
75
75
|
"harness-ui/src/kernel/dist/kernelPerIlBanco.js": "ff75a3e93b6c6d6d838331016c5eb2d1e6acfea85ffc0609d2f531f07b202a54",
|
|
76
|
-
"harness-ui/src/kernel/talosHarness.mjs": "
|
|
76
|
+
"harness-ui/src/kernel/talosHarness.mjs": "c03a8d1e155a8f4f31793e687f7b26e5ee625ff3a3dcfc737dde6f8cb0fca32a",
|
|
77
77
|
"harness-ui/src/library-policy-store.mjs": "b0295e20d7348cc13cb23a8ad2a0b577fe275b4778b94e102ceeb4fb1bb4c70b",
|
|
78
78
|
"harness-ui/src/library-store.mjs": "b7a0333da5783361536509f9108e1293b1e8c22bddfb02fd5581a4e1ed4bcd4f",
|
|
79
79
|
"harness-ui/src/llama-server-supervisor.mjs": "d59cc22a0e7875b4deeb4a4eac1daa55c3f524db7f5f9bd1f5e58f2c4be261e3",
|