scoutline 0.23.0 → 0.24.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/README.md +49 -1
- package/dist/capabilities/investigation.d.ts +134 -0
- package/dist/capabilities/investigation.d.ts.map +1 -0
- package/dist/capabilities/investigation.js +278 -0
- package/dist/capabilities/investigation.js.map +1 -0
- package/dist/commands/archive.d.ts +18 -1
- package/dist/commands/archive.d.ts.map +1 -1
- package/dist/commands/archive.js +23 -12
- package/dist/commands/archive.js.map +1 -1
- package/dist/commands/investigate.d.ts +232 -0
- package/dist/commands/investigate.d.ts.map +1 -0
- package/dist/commands/investigate.js +838 -0
- package/dist/commands/investigate.js.map +1 -0
- package/dist/commands/search.d.ts +16 -0
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +10 -2
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/vision.d.ts.map +1 -1
- package/dist/commands/vision.js +15 -2
- package/dist/commands/vision.js.map +1 -1
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +379 -10
- package/dist/index.js.map +1 -1
- package/dist/lib/context-file.d.ts +21 -0
- package/dist/lib/context-file.d.ts.map +1 -1
- package/dist/lib/context-file.js +84 -6
- package/dist/lib/context-file.js.map +1 -1
- package/dist/lib/equals-form.d.ts +34 -0
- package/dist/lib/equals-form.d.ts.map +1 -0
- package/dist/lib/equals-form.js +49 -0
- package/dist/lib/equals-form.js.map +1 -0
- package/dist/lib/investigate-claims.d.ts +98 -0
- package/dist/lib/investigate-claims.d.ts.map +1 -0
- package/dist/lib/investigate-claims.js +166 -0
- package/dist/lib/investigate-claims.js.map +1 -0
- package/dist/lib/investigate-extract.d.ts +58 -0
- package/dist/lib/investigate-extract.d.ts.map +1 -0
- package/dist/lib/investigate-extract.js +126 -0
- package/dist/lib/investigate-extract.js.map +1 -0
- package/dist/lib/investigate-planner.d.ts +60 -0
- package/dist/lib/investigate-planner.d.ts.map +1 -0
- package/dist/lib/investigate-planner.js +125 -0
- package/dist/lib/investigate-planner.js.map +1 -0
- package/dist/lib/journal.d.ts.map +1 -1
- package/dist/lib/journal.js +10 -9
- package/dist/lib/journal.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +4 -1
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/types.d.ts +45 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/providers/zai/adapter.d.ts.map +1 -1
- package/dist/providers/zai/adapter.js +245 -3
- package/dist/providers/zai/adapter.js.map +1 -1
- package/dist/providers/zai/layout-parsing.d.ts +76 -0
- package/dist/providers/zai/layout-parsing.d.ts.map +1 -0
- package/dist/providers/zai/layout-parsing.js +151 -0
- package/dist/providers/zai/layout-parsing.js.map +1 -0
- package/dist/providers/zai/media.d.ts +19 -0
- package/dist/providers/zai/media.d.ts.map +1 -1
- package/dist/providers/zai/media.js +61 -0
- package/dist/providers/zai/media.js.map +1 -1
- package/package.json +1 -1
- package/skills/scoutline/SKILL.md +71 -1
package/dist/index.js
CHANGED
|
@@ -17,9 +17,11 @@ import { CONFIG_HELP, configGetCommand, configSetCommand, configUnsetCommand, }
|
|
|
17
17
|
import { usageCommand, USAGE_HELP, DEFAULT_USAGE_WINDOW_DAYS, MAX_USAGE_WINDOW_DAYS, } from "./commands/usage.js";
|
|
18
18
|
import { historyCommand, historyClearCommand, historyRecallCommand, historyExportCommand, HISTORY_HELP, HISTORY_NOTE_HELP, HISTORY_RECALL_HELP, HISTORY_CLEAR_HELP, HISTORY_EXPORT_HELP, } from "./commands/history.js";
|
|
19
19
|
import { handleFetch } from "./commands/fetch.js";
|
|
20
|
-
import { handleArchive,
|
|
20
|
+
import { handleArchive, parseArchiveTokens } from "./commands/archive.js";
|
|
21
21
|
import { handleWatch } from "./commands/watch.js";
|
|
22
22
|
import { handleScience } from "./commands/science.js";
|
|
23
|
+
import { investigate, INVESTIGATE_HELP, } from "./commands/investigate.js";
|
|
24
|
+
import { ZaiApiClient } from "./lib/api-client.js";
|
|
23
25
|
import { buildProviderCacheKey, cacheStats, clearAllCaches, parsePruneDuration, pruneCaches, } from "./lib/cache.js";
|
|
24
26
|
import { parseBatchManifest } from "./lib/batch-manifest.js";
|
|
25
27
|
import { assignBatchProviders } from "./lib/batch-assign.js";
|
|
@@ -33,7 +35,8 @@ import { buildJournalEntry, buildNoteEntry, buildSearchSkeleton, buildReadSkelet
|
|
|
33
35
|
import { buildJournalRepeatMarker } from "./lib/journal.js";
|
|
34
36
|
import { applyBudget } from "./lib/output-budget.js";
|
|
35
37
|
import { persistCompaction } from "./lib/output-budget-persistence.js";
|
|
36
|
-
import { FileError, ValidationError, UnsupportedCapabilityError, CommandOptionUnsupportedError, getErrorExitCode, } from "./lib/errors.js";
|
|
38
|
+
import { ApiError, FileError, ValidationError, UnsupportedCapabilityError, CommandOptionUnsupportedError, getErrorExitCode, } from "./lib/errors.js";
|
|
39
|
+
import { normalizeEqualsFormFlags } from "./lib/equals-form.js";
|
|
37
40
|
import * as os from "node:os";
|
|
38
41
|
import * as path from "node:path";
|
|
39
42
|
import * as fs from "node:fs/promises";
|
|
@@ -50,6 +53,7 @@ import { createUsageLedgerSink, readUsageLedger, resolveUsageLedgerPath, } from
|
|
|
50
53
|
import { classifyCredentialState, formatEnvOnlyHint, isCommandHelpInvocation, isDryRunBatchInvocation, OBSERVATIONAL_COMMANDS, } from "./lib/trigger-detection.js";
|
|
51
54
|
import { resolveProviderId, resolveEffectiveProvider, parseProviderId, } from "./providers/selection.js";
|
|
52
55
|
import { BUILT_IN_PROVIDER_DESCRIPTORS } from "./providers/registry.js";
|
|
56
|
+
import { createZaiDescriptor } from "./providers/zai/adapter.js";
|
|
53
57
|
import { PROVIDER_IDS } from "./providers/types.js";
|
|
54
58
|
import { SHARED_PROVIDER_FLAG_IDS, SHARED_PROVIDER_IDS } from "./providers/catalog.js";
|
|
55
59
|
import { executeWithFallback } from "./lib/provider-fallback.js";
|
|
@@ -101,6 +105,10 @@ Commands:
|
|
|
101
105
|
DOI, PMID, arXiv ids; keyless scholarly suppliers:
|
|
102
106
|
openalex, arxiv, crossref, pubmed, europepmc)
|
|
103
107
|
code Execute TypeScript tool chains (Code Mode, Z.AI)
|
|
108
|
+
investigate Local investigation pipeline: plan sub-queries, fan out
|
|
109
|
+
search, read top sources, extract passages into an
|
|
110
|
+
EvidencePack (search+reader supplier union; the pack is data,
|
|
111
|
+
text modes fall back to JSON)
|
|
104
112
|
init Interactive onboarding wizard (writes ~/.scoutline/config.json)
|
|
105
113
|
config Manage ~/.scoutline/config.json keys (get / set / unset,
|
|
106
114
|
credential-free)
|
|
@@ -166,6 +174,7 @@ Help:
|
|
|
166
174
|
scoutline archive --help
|
|
167
175
|
scoutline watch --help
|
|
168
176
|
scoutline science --help
|
|
177
|
+
scoutline investigate --help
|
|
169
178
|
scoutline init --help
|
|
170
179
|
`.trim();
|
|
171
180
|
function parseArgs(args) {
|
|
@@ -237,6 +246,8 @@ export function rejectFlagPair(flags, name, makeError) {
|
|
|
237
246
|
throw makeError();
|
|
238
247
|
}
|
|
239
248
|
}
|
|
249
|
+
// #263: re-exported for the parser-level tests.
|
|
250
|
+
export { normalizeEqualsFormFlags };
|
|
240
251
|
/**
|
|
241
252
|
* Collect every occurrence of a long `--<name>` flag in argv order,
|
|
242
253
|
* mirroring parseArgs' value-consumption rule (the next argument is the
|
|
@@ -464,6 +475,25 @@ export const STRICT_FLAG_ALLOWLIST = {
|
|
|
464
475
|
"max-chars",
|
|
465
476
|
"no-journal",
|
|
466
477
|
]),
|
|
478
|
+
// investigate-pipeline T6: the documented control surface only
|
|
479
|
+
// (PRD AC-1). The rejected trio (depth/arms/budget-tokens) and
|
|
480
|
+
// --context-stdin are deliberately ABSENT — under strict flags they
|
|
481
|
+
// reject here, and the lenient default rejects them inside
|
|
482
|
+
// handleInvestigate (the rejection IS the feature).
|
|
483
|
+
investigate: new Set([
|
|
484
|
+
"help",
|
|
485
|
+
"h",
|
|
486
|
+
"provider",
|
|
487
|
+
"context",
|
|
488
|
+
"sources",
|
|
489
|
+
"max-chars",
|
|
490
|
+
"no-cache",
|
|
491
|
+
"no-journal",
|
|
492
|
+
"synthesize",
|
|
493
|
+
// investigate-verify lane (DESIGN D5): claim-corroboration mode
|
|
494
|
+
// flag. Pair-rejected with --context below (verify owns planning).
|
|
495
|
+
"verify",
|
|
496
|
+
]),
|
|
467
497
|
};
|
|
468
498
|
/**
|
|
469
499
|
* The strict-flag gate (#241): scan a command's argv (the tokens AFTER
|
|
@@ -554,6 +584,7 @@ const SAVE_CAPABLE_COMMANDS = new Set([
|
|
|
554
584
|
"research",
|
|
555
585
|
"repo",
|
|
556
586
|
"vision",
|
|
587
|
+
"investigate",
|
|
557
588
|
]);
|
|
558
589
|
/**
|
|
559
590
|
* ADR-0007 D5 — every command the dispatcher routes (below): the
|
|
@@ -588,6 +619,7 @@ export const DISPATCHED_COMMANDS = new Set([
|
|
|
588
619
|
"archive",
|
|
589
620
|
"watch",
|
|
590
621
|
"science",
|
|
622
|
+
"investigate",
|
|
591
623
|
]);
|
|
592
624
|
/**
|
|
593
625
|
* ADR-0007 D5 — commands WITHOUT an Output Budget ladder. They reject
|
|
@@ -632,6 +664,11 @@ export const ACCEPT_NO_JOURNAL_COMMANDS = new Set([
|
|
|
632
664
|
// Science verticals (T7): the science noun journals (skeleton entries,
|
|
633
665
|
// PRD AC-5c), so its --no-journal per-call escape must exist too.
|
|
634
666
|
"science",
|
|
667
|
+
// investigate-pipeline T6 (PRD AC-10): the run journals its
|
|
668
|
+
// UNDERLYING search/read ops exactly as running them standalone
|
|
669
|
+
// would (umbrella capability "search" — see the journalWiring map),
|
|
670
|
+
// so its --no-journal per-call escape exists on the same seam.
|
|
671
|
+
"investigate",
|
|
635
672
|
]);
|
|
636
673
|
/**
|
|
637
674
|
* Review N10 (fix-round F-6) — the dispatch surface, extracted from
|
|
@@ -1018,7 +1055,19 @@ async function handleVision(args, outputMode, deps) {
|
|
|
1018
1055
|
// descriptor ID is the *attempted* provider (not the
|
|
1019
1056
|
// registry-derived effective provider) so fallback attempts
|
|
1020
1057
|
// record the actual descriptor that invoked transport.
|
|
1021
|
-
|
|
1058
|
+
// glm-ocr lane (ADR-0014 D7): extract-text on zai counts
|
|
1059
|
+
// usage-ledger attempts at the ADAPTER (the OCR cache is
|
|
1060
|
+
// adapter-internal; an 1113+fallback run is two attempts
|
|
1061
|
+
// inside one executor invoke; a cache hit is zero). The
|
|
1062
|
+
// adapter's seam receives the sink through the zai
|
|
1063
|
+
// descriptor's layoutParsingConsume dep (threaded at
|
|
1064
|
+
// registry construction below); the executor emission is
|
|
1065
|
+
// suppressed here so attempts are counted exactly once.
|
|
1066
|
+
...((deps.consume !== undefined &&
|
|
1067
|
+
!(descriptor.zaiOcrLedgerSeam === true &&
|
|
1068
|
+
operation === "extract-text"))
|
|
1069
|
+
? { consume: deps.consume }
|
|
1070
|
+
: {}),
|
|
1022
1071
|
...(deps.consume !== undefined ? { provider: descriptor.id } : {}),
|
|
1023
1072
|
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
1024
1073
|
};
|
|
@@ -3541,6 +3590,264 @@ async function handleHistoryExport(args, outputMode, deps) {
|
|
|
3541
3590
|
...(since !== undefined ? { since } : {}),
|
|
3542
3591
|
}), outputMode, now, deps.secrets);
|
|
3543
3592
|
}
|
|
3593
|
+
// ---------------------------------------------------------------------------
|
|
3594
|
+
// investigate-pipeline T6 (ADR-0013): the CLI wiring for the local
|
|
3595
|
+
// investigation pipeline. Mirrors handleSearch's ORDER — valueless-flag
|
|
3596
|
+
// guards BEFORE the help-gate, count-class validation BEFORE provider
|
|
3597
|
+
// resolution — and hands the parsed controls to commands/investigate.ts
|
|
3598
|
+
// (T4/T5), which owns the plan → fan-out → merge → read → pack
|
|
3599
|
+
// orchestration. The command is NOT routed through executeWithFallback:
|
|
3600
|
+
// it owns its provider grid through resolveFanoutPlan tiers (D3).
|
|
3601
|
+
// ---------------------------------------------------------------------------
|
|
3602
|
+
/**
|
|
3603
|
+
* Strict positive-integer parse for `--sources` (the parseAndValidateCount
|
|
3604
|
+
* class — not the lax parseInt idiom). Parse-level, before the help-gate
|
|
3605
|
+
* is bypassed and before provider resolution, so a bad value is
|
|
3606
|
+
* VALIDATION_ERROR regardless of credentials or provider state.
|
|
3607
|
+
*/
|
|
3608
|
+
export function parseAndValidateSources(raw) {
|
|
3609
|
+
if (raw === undefined || raw === "")
|
|
3610
|
+
return undefined;
|
|
3611
|
+
if (raw === true) {
|
|
3612
|
+
throw new ValidationError("--sources requires a value.", "Use a positive integer (e.g. --sources 5).");
|
|
3613
|
+
}
|
|
3614
|
+
const str = typeof raw === "string" ? raw : String(raw);
|
|
3615
|
+
if (!/^\d+$/.test(str)) {
|
|
3616
|
+
throw new ValidationError(`Invalid --sources value "${str}": must be a positive integer`, "Use a positive integer (e.g. --sources 5).");
|
|
3617
|
+
}
|
|
3618
|
+
const parsed = Number(str);
|
|
3619
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
3620
|
+
throw new ValidationError(`Invalid --sources value "${str}": must be a positive safe integer`, "Use a positive integer (e.g. --sources 5).");
|
|
3621
|
+
}
|
|
3622
|
+
return parsed;
|
|
3623
|
+
}
|
|
3624
|
+
async function handleInvestigate(args, outputMode, deps) {
|
|
3625
|
+
const { flags, positional } = parseArgs(args);
|
|
3626
|
+
// Valueless --context guard BEFORE the help-gate (the handleSearch
|
|
3627
|
+
// D1 placement pin): parseArgs records `true` for a valueless flag,
|
|
3628
|
+
// so without this check `investigate --context` would short-circuit
|
|
3629
|
+
// to HELP + exit 0 with the malformed flag silently swallowed.
|
|
3630
|
+
if (flags.context === true) {
|
|
3631
|
+
throw new ValidationError("--context requires a value.", "Pass a file path: --context <path>.");
|
|
3632
|
+
}
|
|
3633
|
+
// PRD AC-1 pins the accepted controls exactly; --context-stdin is NOT
|
|
3634
|
+
// investigate's (search-only spelling — the question's pipes and
|
|
3635
|
+
// --context cover the sub-query sources). Rejected explicitly, never
|
|
3636
|
+
// accepted-and-dropped; before the help-gate like every other
|
|
3637
|
+
// valueless/malformed-flag guard here.
|
|
3638
|
+
if (flags["context-stdin"] !== undefined) {
|
|
3639
|
+
throw new ValidationError("investigate has no --context-stdin flag.", "Pipe sub-queries with | in the question, or pass --context <path> (see `scoutline investigate --help`).");
|
|
3640
|
+
}
|
|
3641
|
+
// investigate-verify lane (DESIGN D5, PRD AC-1): --verify owns
|
|
3642
|
+
// planning — the claims ARE the grid — so a --context file is a
|
|
3643
|
+
// mode conflict. Both spellings of --context carry; the valueless
|
|
3644
|
+
// --context guard above already fired for the true case.
|
|
3645
|
+
if (flags.verify !== undefined && flags.context !== undefined) {
|
|
3646
|
+
throw new ValidationError("--verify and --context cannot be combined: --verify splits the statement into claims (verify owns planning).", "Drop --context, or run without --verify to plan from the notes file.");
|
|
3647
|
+
}
|
|
3648
|
+
// Valueless --verify guard: parseArgs would swallow a following
|
|
3649
|
+
// token as the value. (The `--verify=` equals-form check is GONE:
|
|
3650
|
+
// #263 normalizes `--verify=foo` to `--verify foo` upstream, and this
|
|
3651
|
+
// existing boolean guard rejects it with the same error.)
|
|
3652
|
+
if (flags.verify !== undefined && flags.verify !== true) {
|
|
3653
|
+
throw new ValidationError("--verify is a boolean flag and takes no value", "Pass the bare --verify to enable claim-corroboration mode, or omit it.");
|
|
3654
|
+
}
|
|
3655
|
+
const verify = flags.verify === true;
|
|
3656
|
+
// PRD AC-1 / ADR-0013 rejected extensions: --depth, --arms, and
|
|
3657
|
+
// --budget-tokens DO NOT EXIST (the parser rejecting them IS the
|
|
3658
|
+
// feature — no accept-and-drop). rejectFlagPair guards BOTH
|
|
3659
|
+
// spellings (#242); before the help-gate for the same reason as
|
|
3660
|
+
// --context above.
|
|
3661
|
+
rejectFlagPair(flags, "depth", () => new ValidationError("investigate has no --depth flag; planning is deterministic (pipes > --context > template).", "Structure the question with | sub-queries or --context <path> instead."));
|
|
3662
|
+
rejectFlagPair(flags, "arms", () => new ValidationError("investigate has no --arms flag; the arm set IS the provider pin.", "Use --provider <tavily,exa|all> to fan out, or a single --provider id for one arm."));
|
|
3663
|
+
rejectFlagPair(flags, "budget-tokens", () => new ValidationError("investigate has no --budget-tokens flag; --max-chars is the budget.", "Use --max-chars <n> (characters, not tokens)."));
|
|
3664
|
+
// T7 (PRD AC-7): --synthesize is VALUELESS. parseArgs assigns the
|
|
3665
|
+
// next non-dash token as a flag value, so `--synthesize foo` (and the
|
|
3666
|
+
// #263-normalized `--synthesize=foo`) rejects here, before the
|
|
3667
|
+
// help-gate, like every other malformed-flag guard in this handler.
|
|
3668
|
+
if (flags.synthesize !== undefined && flags.synthesize !== true) {
|
|
3669
|
+
throw new ValidationError("--synthesize is a boolean flag and takes no value", "Pass the bare --synthesize to enable it, or omit it.");
|
|
3670
|
+
}
|
|
3671
|
+
const synthesize = flags.synthesize === true;
|
|
3672
|
+
if (flags.help || flags.h || positional.length === 0) {
|
|
3673
|
+
deps.invocation.writeStdout(INVESTIGATE_HELP);
|
|
3674
|
+
return 0;
|
|
3675
|
+
}
|
|
3676
|
+
// Z.AI-only (PRD AC-7, the Code Mode precedent): the escape hatch
|
|
3677
|
+
// is Z.AI chat regardless of the provider pin. The handler is the
|
|
3678
|
+
// seam where the RAW pin is visible — the command cannot see it —
|
|
3679
|
+
// so the notice fires here, before any billable work.
|
|
3680
|
+
if (synthesize && deps.provider !== undefined && deps.provider.trim().length > 0) {
|
|
3681
|
+
const pinned = deps.provider.trim().toLowerCase();
|
|
3682
|
+
if (pinned !== "zai") {
|
|
3683
|
+
deps.invocation.writeStderr(`investigate: --synthesize is Z.AI-only; ignoring the --provider ${pinned} pin for the brief (the pack still uses it).`);
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
// The synthesis dep (T7): a Z.AI chat completion through the
|
|
3687
|
+
// in-repo /chat/completions surface. Built HERE, at the handler
|
|
3688
|
+
// seam, exactly like every other capability — the command holds no
|
|
3689
|
+
// transport. `visionComplete` is the only chat-completions method
|
|
3690
|
+
// on ZaiApiClient; the transport is fetch-per-call (no client to
|
|
3691
|
+
// close), and nothing is constructed when the flag is absent.
|
|
3692
|
+
const synthesizeDep = !synthesize
|
|
3693
|
+
? undefined
|
|
3694
|
+
: deps.synthesize ??
|
|
3695
|
+
(async (prompt) => {
|
|
3696
|
+
const client = new ZaiApiClient(undefined, deps.env);
|
|
3697
|
+
const response = await client.visionComplete([
|
|
3698
|
+
{
|
|
3699
|
+
role: "system",
|
|
3700
|
+
content: "You are given an evidence pack assembled from web sources: a question, " +
|
|
3701
|
+
"its planned sub-queries, and verbatim passage quotes extracted from the " +
|
|
3702
|
+
"read sources. Write a short, direct brief answering the question using " +
|
|
3703
|
+
"ONLY the quoted evidence. Do not invent facts, do not add citations that " +
|
|
3704
|
+
"are not in the quotes, and say plainly when the evidence is insufficient.",
|
|
3705
|
+
},
|
|
3706
|
+
{
|
|
3707
|
+
role: "user",
|
|
3708
|
+
// Deterministic serialization: fixed key order, compact JSON.
|
|
3709
|
+
content: JSON.stringify({
|
|
3710
|
+
question: prompt.question,
|
|
3711
|
+
subQueries: [...prompt.subQueries],
|
|
3712
|
+
quotes: [...prompt.quotes],
|
|
3713
|
+
}),
|
|
3714
|
+
},
|
|
3715
|
+
]);
|
|
3716
|
+
const content = response.choices?.[0]?.message?.content;
|
|
3717
|
+
if (typeof content !== "string") {
|
|
3718
|
+
throw new ApiError("synthesis returned no brief text", 502);
|
|
3719
|
+
}
|
|
3720
|
+
return content;
|
|
3721
|
+
});
|
|
3722
|
+
// Count-class validation BEFORE provider resolution (Fixup D order):
|
|
3723
|
+
// a bad --sources is VALIDATION_ERROR even with no credentials, and
|
|
3724
|
+
// --max-chars rides the strict parseMaxCharsFlag (parseBriefMaxChars
|
|
3725
|
+
// class). Both are re-validated inside the command's trust boundary.
|
|
3726
|
+
const sources = parseAndValidateSources(flags.sources);
|
|
3727
|
+
const maxChars = parseMaxCharsFlag(flags);
|
|
3728
|
+
const question = positional.join(" ");
|
|
3729
|
+
// Resolve the fan-out activation plan once HERE (pure — the same
|
|
3730
|
+
// inputs handleSearch passes) so the journal hook can stamp per-arm
|
|
3731
|
+
// serving cells BEFORE any arm runs; the command resolves its own
|
|
3732
|
+
// plan identically from the same inputs (the tiers are deterministic,
|
|
3733
|
+
// so the two resolutions agree by construction).
|
|
3734
|
+
const fanoutPlan = resolveFanoutPlan({
|
|
3735
|
+
explicitProviderRaw: deps.provider,
|
|
3736
|
+
env: deps.env,
|
|
3737
|
+
configFanout: deps.configFanout === true,
|
|
3738
|
+
...(deps.routing !== undefined ? { routing: deps.routing } : {}),
|
|
3739
|
+
descriptors: deps.providerDescriptors,
|
|
3740
|
+
});
|
|
3741
|
+
// Save wiring (D7): the pack is a first-class saveable result. The
|
|
3742
|
+
// provider routing mirrors the in-code vocabulary — fan-out records
|
|
3743
|
+
// the arm list with no single effective; single records the resolved
|
|
3744
|
+
// arm (the resolver's single-mode arm, not a quota-ranked pick — the
|
|
3745
|
+
// command runs exactly that arm). Args carry the
|
|
3746
|
+
// provider-influencing allow-list only.
|
|
3747
|
+
const investigateSaveArgs = {
|
|
3748
|
+
...(deps.provider !== undefined ? { provider: deps.provider } : {}),
|
|
3749
|
+
...(sources !== undefined ? { sources } : {}),
|
|
3750
|
+
...(flags["no-cache"] === true ? { "no-cache": true } : {}),
|
|
3751
|
+
...(synthesize ? { synthesize: true } : {}),
|
|
3752
|
+
...(verify ? { verify: true } : {}),
|
|
3753
|
+
};
|
|
3754
|
+
const investigateProviderRouting = fanoutPlan.mode === "fanout"
|
|
3755
|
+
? {
|
|
3756
|
+
mode: "fanout",
|
|
3757
|
+
...(deps.provider !== undefined ? { requested: deps.provider } : {}),
|
|
3758
|
+
arms: fanoutPlan.arms.map((arm) => String(arm)),
|
|
3759
|
+
}
|
|
3760
|
+
: {
|
|
3761
|
+
mode: "single",
|
|
3762
|
+
...(deps.provider !== undefined ? { requested: deps.provider } : {}),
|
|
3763
|
+
effective: String(fanoutPlan.arms[0]),
|
|
3764
|
+
};
|
|
3765
|
+
const save = createSaveArtifactHook(deps, {
|
|
3766
|
+
command: "investigate",
|
|
3767
|
+
outputMode,
|
|
3768
|
+
args: investigateSaveArgs,
|
|
3769
|
+
provider: investigateProviderRouting,
|
|
3770
|
+
});
|
|
3771
|
+
// Journal wiring (PRD AC-10): investigate journals its UNDERLYING
|
|
3772
|
+
// search/read ops exactly as running them standalone would. The
|
|
3773
|
+
// umbrella capability is "search" — JournalableCapability is closed
|
|
3774
|
+
// over {search, read, research, science} and lib/journal.ts is a
|
|
3775
|
+
// read-only seam for this lane; a literal "investigate" entry fails
|
|
3776
|
+
// asJournalEntry validation and would poison the whole log
|
|
3777
|
+
// (fail-open drop + corruption notice). Entry shape: query = the
|
|
3778
|
+
// question, skeleton rows = the pack sources (url/title) — fed via
|
|
3779
|
+
// the resultRows thunk read after dispatch resolves. Fan-out runs
|
|
3780
|
+
// stamp per-arm cells on the shared capture first (the arm-race fix),
|
|
3781
|
+
// exactly like handleSearch.
|
|
3782
|
+
let journalRows;
|
|
3783
|
+
if (fanoutPlan.mode === "fanout" && deps.journal !== undefined) {
|
|
3784
|
+
installFanoutArmCells(deps.journal.capture, fanoutPlan.arms);
|
|
3785
|
+
}
|
|
3786
|
+
const journal = deps.journal === undefined
|
|
3787
|
+
? undefined
|
|
3788
|
+
: createJournalHook(deps, {
|
|
3789
|
+
journal: deps.journal,
|
|
3790
|
+
query: question,
|
|
3791
|
+
resultRows: () => journalRows,
|
|
3792
|
+
fanoutRouting: fanoutPlan.mode === "fanout"
|
|
3793
|
+
? {
|
|
3794
|
+
mode: "fanout",
|
|
3795
|
+
...(deps.provider !== undefined ? { requested: deps.provider } : {}),
|
|
3796
|
+
arms: fanoutPlan.arms,
|
|
3797
|
+
}
|
|
3798
|
+
: undefined,
|
|
3799
|
+
});
|
|
3800
|
+
return invokeCommand(deps.invocation, async (context) => {
|
|
3801
|
+
const result = await investigate(question, {
|
|
3802
|
+
...(deps.provider !== undefined ? { provider: deps.provider } : {}),
|
|
3803
|
+
...(typeof flags.context === "string" ? { contextFile: flags.context } : {}),
|
|
3804
|
+
...(sources !== undefined ? { sources } : {}),
|
|
3805
|
+
...(maxChars !== undefined ? { maxChars } : {}),
|
|
3806
|
+
noCache: flags["no-cache"] === true,
|
|
3807
|
+
noJournal: collectLongFlagValues(args, "no-journal").length > 0,
|
|
3808
|
+
...(synthesize ? { synthesize: true } : {}),
|
|
3809
|
+
...(verify ? { verify: true } : {}),
|
|
3810
|
+
}, {
|
|
3811
|
+
descriptors: deps.providerDescriptors,
|
|
3812
|
+
env: deps.env,
|
|
3813
|
+
configFanout: deps.configFanout === true,
|
|
3814
|
+
...(deps.routing !== undefined ? { routing: deps.routing } : {}),
|
|
3815
|
+
// One shared cache for the search arms AND the reads — the
|
|
3816
|
+
// production defaultCache aliases every capability triple to
|
|
3817
|
+
// the same root, and the command's warm-cacheHit accounting
|
|
3818
|
+
// (search + reader partitions) assumes the one store.
|
|
3819
|
+
cache: deps.searchCache,
|
|
3820
|
+
sleep: deps.searchSleep,
|
|
3821
|
+
random: deps.searchRandom,
|
|
3822
|
+
...(deps.consume !== undefined ? { consume: deps.consume } : {}),
|
|
3823
|
+
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
3824
|
+
fusionMode: deps.fusionMode ?? "rrf",
|
|
3825
|
+
...(deps.now !== undefined ? { nowWall: () => new Date(deps.now()) } : {}),
|
|
3826
|
+
loadContextText: async (filePath) => (await readContextSource({ file: filePath }, {
|
|
3827
|
+
readFile: (f) => fs.readFile(f),
|
|
3828
|
+
readStdin: () => {
|
|
3829
|
+
throw new Error("stdin context is not investigate's flag");
|
|
3830
|
+
},
|
|
3831
|
+
})).text,
|
|
3832
|
+
readerCapabilityFor: (descriptor) => descriptor.create({ env: deps.env }).reader,
|
|
3833
|
+
...(deps.secrets !== undefined ? { secrets: deps.secrets } : {}),
|
|
3834
|
+
...(synthesizeDep !== undefined ? { synthesize: synthesizeDep } : {}),
|
|
3835
|
+
}, context);
|
|
3836
|
+
// Feed the journal skeleton from the pack BEFORE the result
|
|
3837
|
+
// leaves the behavior — url/title identities of the read
|
|
3838
|
+
// sources, the same row shape search journals.
|
|
3839
|
+
if (result.kind === "data") {
|
|
3840
|
+
const pack = result.data;
|
|
3841
|
+
if (Array.isArray(pack.sources)) {
|
|
3842
|
+
journalRows = pack.sources.map((source) => ({
|
|
3843
|
+
url: typeof source.url === "string" ? source.url : undefined,
|
|
3844
|
+
title: typeof source.title === "string" ? source.title : undefined,
|
|
3845
|
+
}));
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
return result;
|
|
3849
|
+
}, outputMode, deps.now, deps.secrets, save, journal);
|
|
3850
|
+
}
|
|
3544
3851
|
export { handleFetch, fetchCommand, executeFetch, FETCH_HELP } from "./commands/fetch.js";
|
|
3545
3852
|
export { handleArchive, archiveCdxCommand, archiveGetCommand, ARCHIVE_HELP, } from "./commands/archive.js";
|
|
3546
3853
|
async function handleQuota(args, outputMode, deps) {
|
|
@@ -4103,7 +4410,34 @@ export async function main(args, dependencies) {
|
|
|
4103
4410
|
const depsConfig = dependencies.config;
|
|
4104
4411
|
const loadScoutlineConfig = dependencies.loadScoutlineConfig ??
|
|
4105
4412
|
(depsConfig !== undefined ? async () => depsConfig : undefined);
|
|
4413
|
+
// glm-ocr lane (ADR-0014 D7): in full production mode (no injected
|
|
4414
|
+
// descriptor list), rebuild the zai entry with the shared consumption
|
|
4415
|
+
// sink threaded into its layoutParsingConsume seam so the extract-text
|
|
4416
|
+
// OCR arm counts ledger attempts at the adapter (cache hits = zero
|
|
4417
|
+
// rows; 1113+fallback = two rows). The sink binding exists BELOW this
|
|
4418
|
+
// point (main body order), so this is a lazy rebuild: the array holds
|
|
4419
|
+
// a descriptor whose create() closes over a sink REFERENCE resolved
|
|
4420
|
+
// at first vision dispatch. Tests injecting providerDescriptors keep
|
|
4421
|
+
// full control (their descriptor carries their own seams).
|
|
4106
4422
|
const providerDescriptors = dependencies.providerDescriptors ?? BUILT_IN_PROVIDER_DESCRIPTORS;
|
|
4423
|
+
const productionZaiLedgerDescriptors = dependencies.providerDescriptors
|
|
4424
|
+
? providerDescriptors
|
|
4425
|
+
: providerDescriptors.map((descriptor) => descriptor.id === "zai"
|
|
4426
|
+
? Object.assign(createZaiDescriptor({
|
|
4427
|
+
notice: (line) => process.stderr.write(`${line}\n`),
|
|
4428
|
+
layoutParsingConsume: {
|
|
4429
|
+
record: (event) => {
|
|
4430
|
+
if (zaiLedgerSink === undefined)
|
|
4431
|
+
return Promise.resolve();
|
|
4432
|
+
return zaiLedgerSink.record(event);
|
|
4433
|
+
},
|
|
4434
|
+
},
|
|
4435
|
+
}),
|
|
4436
|
+
// Marker: this descriptor's zai adapter owns extract-text
|
|
4437
|
+
// ledger rows through its seam (checked in handleVision
|
|
4438
|
+
// to suppress the executor emission exactly once).
|
|
4439
|
+
{ zaiOcrLedgerSeam: true })
|
|
4440
|
+
: descriptor);
|
|
4107
4441
|
// Resolve configured Provider credentials from the INJECTED env (B3) so
|
|
4108
4442
|
// redaction follows the same environment the handlers see — a secret
|
|
4109
4443
|
// that exists only in MainDependencies.env is still redacted from output.
|
|
@@ -4116,12 +4450,20 @@ export async function main(args, dependencies) {
|
|
|
4116
4450
|
// envelope in the output mode the argv requested (re-derived from the
|
|
4117
4451
|
// raw argv via `bestEffortOutputMode` because the partially parsed
|
|
4118
4452
|
// result never escapes the throw) and the error's exit code.
|
|
4453
|
+
// #263: central `--flag=value` ≡ `--flag value` normalization. Runs
|
|
4454
|
+
// before EVERY argv consumer below (extractGlobalOptions, the
|
|
4455
|
+
// strict-flag gate, help/version checks, command handlers' parseArgs)
|
|
4456
|
+
// so all token walks observe space-form tokens only. Pure and
|
|
4457
|
+
// total-length-preserving plus at most the split tokens; the only
|
|
4458
|
+
// behavior change is where a `--flag=value` used to be a garbage
|
|
4459
|
+
// boolean key.
|
|
4460
|
+
const normalizedArgs = normalizeEqualsFormFlags(args);
|
|
4119
4461
|
let extracted;
|
|
4120
4462
|
try {
|
|
4121
|
-
extracted = extractGlobalOptions(
|
|
4463
|
+
extracted = extractGlobalOptions(normalizedArgs);
|
|
4122
4464
|
}
|
|
4123
4465
|
catch (error) {
|
|
4124
|
-
invocation.writeStderr(formatErrorOutput(error, bestEffortOutputMode(
|
|
4466
|
+
invocation.writeStderr(formatErrorOutput(error, bestEffortOutputMode(normalizedArgs, invocation), envSecrets));
|
|
4125
4467
|
return getErrorExitCode(error);
|
|
4126
4468
|
}
|
|
4127
4469
|
const { outputFormat, forcePretty, forceRaw, provider, noFallback, isolated, rest } = extracted;
|
|
@@ -4329,6 +4671,9 @@ export async function main(args, dependencies) {
|
|
|
4329
4671
|
// like the quota sink). The composite isolates each side — one
|
|
4330
4672
|
// sink's failure becomes one redacted warning and never blocks or
|
|
4331
4673
|
// fails the other.
|
|
4674
|
+
// glm-ocr lane: late-bound sink reference for the rebuilt zai
|
|
4675
|
+
// descriptor (assigned immediately after `consume` below).
|
|
4676
|
+
let zaiLedgerSink;
|
|
4332
4677
|
const consume = dependencies.consume ??
|
|
4333
4678
|
// ADR-0006 §5: --isolated runs skip local state persistence
|
|
4334
4679
|
// entirely — no usage-ledger writes, no quota-snapshot writes — so
|
|
@@ -4346,6 +4691,7 @@ export async function main(args, dependencies) {
|
|
|
4346
4691
|
filePath: resolveUsageLedgerPath(resolveConfigRootPure(env, { homedir: os.homedir() })),
|
|
4347
4692
|
}))
|
|
4348
4693
|
: undefined);
|
|
4694
|
+
zaiLedgerSink = consume;
|
|
4349
4695
|
// PB-T4: quota snapshot for selection. Declared here so
|
|
4350
4696
|
// `buildHandlerDeps` closes over the binding; assigned AFTER the
|
|
4351
4697
|
// PB-T1 pre-command refresh so observational commands' fresh data is
|
|
@@ -4362,11 +4708,12 @@ export async function main(args, dependencies) {
|
|
|
4362
4708
|
// env/secrets/fallbackEnabled depend on whether config has been loaded.
|
|
4363
4709
|
const buildHandlerDeps = (credEnv, credSecrets, credFallback, credRouting = undefined, credFanout = undefined, credFusion = undefined) => ({
|
|
4364
4710
|
invocation,
|
|
4711
|
+
synthesize: dependencies.synthesize,
|
|
4365
4712
|
env: credEnv,
|
|
4366
4713
|
secrets: credSecrets,
|
|
4367
4714
|
now,
|
|
4368
4715
|
provider,
|
|
4369
|
-
providerDescriptors,
|
|
4716
|
+
providerDescriptors: productionZaiLedgerDescriptors,
|
|
4370
4717
|
fallbackEnabled: credFallback,
|
|
4371
4718
|
routing: credRouting,
|
|
4372
4719
|
configFanout: credFanout,
|
|
@@ -4598,7 +4945,7 @@ export async function main(args, dependencies) {
|
|
|
4598
4945
|
// the human table was never the flag's contract.
|
|
4599
4946
|
// Determine the subcommand through the parser (not positional
|
|
4600
4947
|
// assumption): options may precede `get`/`cdx`.
|
|
4601
|
-
const archiveSubcommand =
|
|
4948
|
+
const archiveSubcommand = parseArchiveTokens(commandArgs).subcommand;
|
|
4602
4949
|
const archiveOutputMode = forceRaw && outputFormat === undefined && archiveSubcommand === "get"
|
|
4603
4950
|
? "compact"
|
|
4604
4951
|
: outputMode;
|
|
@@ -5016,12 +5363,27 @@ export async function main(args, dependencies) {
|
|
|
5016
5363
|
config.journal !== false
|
|
5017
5364
|
? {}
|
|
5018
5365
|
: undefined;
|
|
5366
|
+
// M1 (glm-ocr review): journal/save chains build from the ledger-
|
|
5367
|
+
// rebuilt list so --save vision runs keep the adapter-owned
|
|
5368
|
+
// extract-text seam (marker survives the capture spread).
|
|
5019
5369
|
const journalingDescriptors = journalCapture === undefined
|
|
5020
|
-
?
|
|
5021
|
-
: captureServingDescriptors(
|
|
5370
|
+
? productionZaiLedgerDescriptors
|
|
5371
|
+
: captureServingDescriptors(productionZaiLedgerDescriptors, journalCapture);
|
|
5022
5372
|
const journalWiring = journalCapture === undefined
|
|
5023
5373
|
? undefined
|
|
5024
|
-
: {
|
|
5374
|
+
: {
|
|
5375
|
+
// investigate-pipeline T6: the run journals its UNDERLYING
|
|
5376
|
+
// search/read ops under the umbrella capability "search" —
|
|
5377
|
+
// JournalableCapability is closed over
|
|
5378
|
+
// {search,read,research,science} (lib/journal.ts is a
|
|
5379
|
+
// read-only seam for this lane) and a literal "investigate"
|
|
5380
|
+
// entry would fail asJournalEntry validation and poison the
|
|
5381
|
+
// whole log (fail-open drop + corruption notice). The
|
|
5382
|
+
// underlying ops ARE search/read; running them standalone
|
|
5383
|
+
// journals exactly so (orchestrator ruling 2026-09-20).
|
|
5384
|
+
capability: (command === "investigate" ? "search" : command),
|
|
5385
|
+
capture: journalCapture,
|
|
5386
|
+
};
|
|
5025
5387
|
const saveWiring = saveRequest === undefined || isHelpInvocation
|
|
5026
5388
|
? undefined
|
|
5027
5389
|
: buildSaveWiring(saveRequest, journalingDescriptors, journalCapture);
|
|
@@ -5126,6 +5488,13 @@ export async function main(args, dependencies) {
|
|
|
5126
5488
|
commandRecognized = true;
|
|
5127
5489
|
exitCode = await handleQuota(commandArgs, outputMode, handlerDepsWithSelection);
|
|
5128
5490
|
break;
|
|
5491
|
+
case "investigate":
|
|
5492
|
+
commandRecognized = true;
|
|
5493
|
+
// PRD AC-9: --isolated is ACCEPTED (never rejected — investigate
|
|
5494
|
+
// has no stateful dir; resumability is pure cache replay, and the
|
|
5495
|
+
// isolated/<pid> cache segment is exactly the right namespace).
|
|
5496
|
+
exitCode = await handleInvestigate(commandArgs, outputMode, handlerDepsWithSave);
|
|
5497
|
+
break;
|
|
5129
5498
|
case "code":
|
|
5130
5499
|
commandRecognized = true;
|
|
5131
5500
|
exitCode = await handleCode(commandArgs, outputMode, handlerDepsWithSelection);
|