xcoder-jarvis-agent 0.0.0-stage → 1.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +224 -2
  3. package/dist/index.d.ts +27 -0
  4. package/dist/index.js +28 -0
  5. package/dist/jarvis/agent.d.ts +57 -0
  6. package/dist/jarvis/agent.js +306 -0
  7. package/dist/jarvis/argv.d.ts +30 -0
  8. package/dist/jarvis/argv.js +138 -0
  9. package/dist/jarvis/budget.d.ts +49 -0
  10. package/dist/jarvis/budget.js +83 -0
  11. package/dist/jarvis/chat/confirm.d.ts +23 -0
  12. package/dist/jarvis/chat/confirm.js +37 -0
  13. package/dist/jarvis/chat/lines.d.ts +41 -0
  14. package/dist/jarvis/chat/lines.js +93 -0
  15. package/dist/jarvis/chat/prompt.d.ts +10 -0
  16. package/dist/jarvis/chat/prompt.js +18 -0
  17. package/dist/jarvis/chat/session.d.ts +38 -0
  18. package/dist/jarvis/chat/session.js +122 -0
  19. package/dist/jarvis/chat/terminator.d.ts +14 -0
  20. package/dist/jarvis/chat/terminator.js +19 -0
  21. package/dist/jarvis/decision.d.ts +64 -0
  22. package/dist/jarvis/decision.js +116 -0
  23. package/dist/jarvis/memory/store.d.ts +88 -0
  24. package/dist/jarvis/memory/store.js +189 -0
  25. package/dist/jarvis/prompt/assemble.d.ts +61 -0
  26. package/dist/jarvis/prompt/assemble.js +118 -0
  27. package/dist/jarvis/prompt/policy.d.ts +22 -0
  28. package/dist/jarvis/prompt/policy.js +80 -0
  29. package/dist/jarvis/prompt/skills.d.ts +69 -0
  30. package/dist/jarvis/prompt/skills.js +257 -0
  31. package/dist/jarvis/react-loop.d.ts +51 -0
  32. package/dist/jarvis/react-loop.js +160 -0
  33. package/dist/jarvis/telemetry/provenance.d.ts +54 -0
  34. package/dist/jarvis/telemetry/provenance.js +56 -0
  35. package/dist/jarvis/test-helpers.d.ts +14 -0
  36. package/dist/jarvis/test-helpers.js +69 -0
  37. package/dist/jarvis/tools/fs-tools.d.ts +28 -0
  38. package/dist/jarvis/tools/fs-tools.js +292 -0
  39. package/dist/jarvis/tools/memory-tools.d.ts +27 -0
  40. package/dist/jarvis/tools/memory-tools.js +83 -0
  41. package/dist/jarvis/tools/registry.d.ts +54 -0
  42. package/dist/jarvis/tools/registry.js +49 -0
  43. package/dist/jarvis/util/confirm.d.ts +28 -0
  44. package/dist/jarvis/util/confirm.js +54 -0
  45. package/dist/jarvis/util/kill.d.ts +10 -0
  46. package/dist/jarvis/util/kill.js +39 -0
  47. package/dist/jarvis/util/text.d.ts +74 -0
  48. package/dist/jarvis/util/text.js +190 -0
  49. package/dist/program.d.ts +2 -0
  50. package/dist/program.js +12 -0
  51. package/package.json +35 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Neural Node Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,225 @@
1
- # Temporary Holding Version
1
+ <!-- xcoder:version 3 | xcoder:task task-482677 | xcoder:updated 2026-11-05T03:54:36.038Z | xcoder:subtask code-st-729fd8 -->
2
+ # @xcoder/jarvis-agent
2
3
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
4
+ Jarvis is a ReAct agent that runs as a local CLI inside a developer's
5
+ workspace. It reasons in a bounded loop, reads the workspace through a small
6
+ allowlist of read-only tools, remembers short notes across sessions, and
7
+ assembles its system prompt at runtime from versioned policy plus skill packs.
8
+
9
+ It is built on `xcoder-dna` and follows the same workspace conventions: ESM,
10
+ TypeScript, npm workspaces, vitest.
11
+
12
+ ## Install and run
13
+
14
+ ```bash
15
+ npm run build -w @xcoder/jarvis-agent
16
+ node packages/jarvis-agent/dist/program.js "summarise how the llm client picks a transport"
17
+ ```
18
+
19
+ Once published, the binaries are `jarvis`.
20
+
21
+ ### Flags
22
+
23
+ | Flag | Meaning | Default |
24
+ |---|---|---|
25
+ | `--skill NAME` | load only this skill pack (repeatable) | all discovered packs |
26
+ | `--max-iterations N` | reasoning steps, clamped to 1..8 | 8 |
27
+ | `--chat` | multi-turn mode: read turns from stdin until `exit`/`quit`/`bye` or EOF | off |
28
+ | `--max-turns N` | chat turns, clamped to 1..200 | 50 |
29
+ | `--no-memory` | skip loading and writing memory | memory on |
30
+ | `--session ID` | session label | generated |
31
+ | `--user ID` | memory scope label | OS user |
32
+ | `--json` | emit one line of JSON | off |
33
+ | `--help` | usage, exit 0 | |
34
+
35
+ ### Exit codes
36
+
37
+ | Code | Meaning |
38
+ |---|---|
39
+ | 0 | answered |
40
+ | 1 | error (bad flags, no task, model failure) |
41
+ | 2 | iteration limit reached |
42
+ | 3 | budget exceeded (in chat: also `--max-turns` reached) |
43
+ | 4 | aborted by the kill switch |
44
+
45
+ Code 2 is always a per-turn outcome in chat mode: a turn that hits its
46
+ iteration limit is reported and the conversation continues.
47
+
48
+ ## Chat mode
49
+
50
+ ```bash
51
+ node packages/jarvis-agent/dist/program.js --chat
52
+ ```
53
+
54
+ Each line you type is one turn. The session ends when you type `exit`, `quit`
55
+ or `bye` on a line by itself (case-insensitive, surrounding spaces ignored) or
56
+ send EOF (Ctrl-D / Ctrl-Z). A line that merely *contains* one of those words —
57
+ `exit now`, `how do I quit a program?` — is an ordinary turn.
58
+
59
+ - **Continuity comes from memory, not from a growing transcript.** Every turn
60
+ re-reads the same memory session and re-assembles the prompt from it, so the
61
+ agent can recall earlier turns up to the memory window limit (~20 entries).
62
+ A very long chat therefore forgets its distant past; use `--session` to name
63
+ a conversation you want to return to.
64
+ - **Each turn is one ordinary run.** It gets its own budget, its own `runId` and
65
+ exactly one provenance record, and the shared `session` label ties the turns
66
+ together. A turn that fails is reported and the loop continues.
67
+ - **Blank lines are inert** — no model call, no memory write, no provenance.
68
+ - **Lines over 8 KiB are rejected before the model sees them**, and only the
69
+ byte count is echoed, never the text.
70
+ - **Ctrl-C and the kill switch still work.** `XCODER_JARVIS_KILL=1` or a
71
+ `.xcoder/jarvis/STOP` file is re-checked at every turn boundary and exits 4.
72
+ - **`--json` emits one JSON object per executed turn** on stdout, so the stream
73
+ stays machine-readable; the session banner goes to stderr only.
74
+ - **`--chat` plus a positional task is an error**, not a seed: a silently
75
+ discarded task looks like a model bug.
76
+
77
+ A tool that needs confirmation prompts on stdin through the same reader the
78
+ loop uses, so a prompt can never swallow a turn. Typing a terminator at a
79
+ confirmation prompt denies that tool and ends the session at the next turn
80
+ boundary — quitting is never blocked by a pending approval.
81
+
82
+ ## How it is built
83
+
84
+ ```
85
+ argv -> runJarvisAgent
86
+ |-- discoverSkills -> skill packs (data, not instructions)
87
+ |-- assembleSystemPrompt -> trusted policy + delimited data blocks
88
+ |-- runReactLoop
89
+ | |-- createLlmClient (xcoder-dna)
90
+ | |-- parseDecision (zod)
91
+ | `-- ToolRegistry (allowlist enforced in code)
92
+ |-- MemoryStore (scoped JSONL, redacted at write)
93
+ `-- writeProvenance (.xcoder/jarvis/runs/[runId].jsonl)
94
+ ```
95
+
96
+ The ReAct loop is owned in-package rather than reusing
97
+ `GenesisReActOrchestrator`, because that orchestrator's tool registry is
98
+ internal and cannot be constrained to Jarvis's allowlist — reusing it would
99
+ mean shipping an agent whose tool surface we do not control.
100
+
101
+ ### Tools
102
+
103
+ Only six tools exist, and the list is enforced in code:
104
+
105
+ | Tool | Risk | Notes |
106
+ |---|---|---|
107
+ | `read_file` | read | workspace-confined, symlink-resolved, secret-path denylist, 256 KiB cap |
108
+ | `glob` | read | `*`, `**`, `?`, `{a,b}` only; 200 results |
109
+ | `grep` | read | regex, 200 chars max; 100 matches; binary skipped |
110
+ | `search_workspace` | read | path substring search |
111
+ | `remember` | write | through `MemoryStore`, redacted before write |
112
+ | `recall` | read | current scope's window |
113
+
114
+ Deliberately absent: shell/exec, file write or delete, network fetch, git
115
+ push, and any SQL. There is no irreversible tool in this build, so nothing
116
+ needs human approval to run. The confirmation seam exists anyway and fails
117
+ closed (deny) when stdin is not a TTY, so adding an irreversible tool later
118
+ cannot silently skip approval.
119
+
120
+ ## Security model
121
+
122
+ Jarvis is designed against the OWASP LLM Top 10. The controls that matter:
123
+
124
+ **Prompt injection (LLM01).** The instruction region is a versioned constant,
125
+ `POLICY`, in `src/jarvis/prompt/policy.ts`. Its sha256 is recorded in every
126
+ run's provenance. Skill packs, memory entries and tool observations are
127
+ treated as untrusted data: they are normalised (zero-width and bidi controls
128
+ stripped), placed in `<untrusted-skill>` / `<untrusted-memory>` /
129
+ `<untrusted-observation>` blocks, and any literal closing delimiter inside
130
+ them is entity-escaped so it cannot end its own block.
131
+
132
+ **Policy is enforced in code, not prose (PRM-09).** A prompt instruction is
133
+ not a security boundary. The tool allowlist lives in `ToolRegistry.get()`,
134
+ which returns `undefined` for any name that is not both registered and
135
+ allowed. Iteration, token and wall-clock caps live in `RunBudget`. Model
136
+ output is validated by a zod discriminated union before it can reach any
137
+ tool.
138
+
139
+ **Insecure output handling (LLM02).** Parsed decisions are the only thing
140
+ that leaves `parseDecision`; raw model text is never passed to a shell, SQL,
141
+ eval, HTML or a file path. There is no `eval` or `Function` constructor in the
142
+ package.
143
+
144
+ **Sensitive information (LLM06).** `read_file` refuses a denylist of
145
+ secret-bearing paths (dotenv files, PEM/key files, ssh identities, npmrc,
146
+ credential files). Anything read is run through `redactSecrets` before it
147
+ enters the transcript, and memory values are redacted before they are
148
+ written to disk.
149
+
150
+ **Excessive agency (LLM08).** At most 8 iterations, one request in flight, a
151
+ read-mostly tool surface, and a kill switch.
152
+
153
+ **Cost and DoS (LLM10).** `RunBudget` caps input/output tokens, iterations and
154
+ wall-clock time. A run over any cap returns exit 3 with no partial answer.
155
+
156
+ ### Kill switch
157
+
158
+ Either signal aborts the run, checked before every LLM call and every tool
159
+ dispatch, and failing closed:
160
+
161
+ ```bash
162
+ export XCODER_JARVIS_KILL=1 # environment
163
+ touch .xcoder/jarvis/STOP # file in the workspace
164
+ ```
165
+
166
+ An aborted run writes no memory entry and still records provenance with
167
+ outcome `aborted`.
168
+
169
+ ### What is logged
170
+
171
+ `.xcoder/jarvis/runs/[runId].jsonl` — one record per run, mode 0600 where the
172
+ platform supports it. The schema holds identifiers, hashes and counts only:
173
+ no prompt text, no model replies, no file contents, no tokens. The one
174
+ free-text field is `error`, which is redacted and capped at 500 characters.
175
+ Records older than 90 days are pruned off the hot path. Set `JARVIS_DEBUG=1`
176
+ to print a stack trace to stderr on an unexpected error (never to the run
177
+ log).
178
+
179
+ ## Memory
180
+
181
+ Memory is append-only JSONL at
182
+ `.xcoder/jarvis/memory/[workspaceId]/[userId].jsonl`, scoped by a hash of the
183
+ resolved workspace path and a sanitised user label. Compaction is
184
+ deterministic and never calls a model: it keeps the newest entry per key,
185
+ then fills remaining capacity newest-first. A torn final line is dropped on
186
+ read rather than throwing, so a killed process cannot corrupt the store.
187
+
188
+ > **Accepted risk — scope is a label, not authentication.** `--user` selects
189
+ > which memory file to use; it does not authenticate anyone. Anyone who can
190
+ > run the CLI can read their own memory file, and anyone with filesystem
191
+ > access can read the others. Real isolation requires separate OS accounts,
192
+ > which is out of scope for a local CLI. Owner: platform team.
193
+
194
+ > **File modes are best-effort on Windows.** The store requests mode 0700/0600
195
+ > but Windows ignores POSIX permission bits; the protections there come from
196
+ the user profile directory's ACLs.
197
+
198
+ ## Skill packs
199
+
200
+ Packs are `SKILL.md` files with YAML frontmatter (`name`, `role`,
201
+ `description`, `triggers`, `version`, `requires_tools`, `composes_with`).
202
+ Discovery searches, first match wins: `$XCODER_SKILLS_DIR`, then
203
+ `.xcoder/skills` in the workspace, then `~/.xcoder/skills` (seeded from the
204
+ shipped packs on first run), then the bundled `xcoder-dna` packs.
205
+
206
+ The frontmatter parser is a line-based YAML subset — no generic YAML
207
+ dependency and no code execution. A pack that fails to parse is skipped and
208
+ recorded in provenance; it never aborts a run. Unknown frontmatter keys are
209
+ dropped rather than reaching the prompt.
210
+
211
+ Because user-level packs are writable by the user, treat pack text as
212
+ untrusted: review a pack before enabling it, exactly as you would review a
213
+ script you were about to run.
214
+
215
+ ## Development
216
+
217
+ ```bash
218
+ npm run typecheck -w @xcoder/jarvis-agent
219
+ npm test # full workspace suite
220
+ npx vitest run packages/jarvis-agent # just this package
221
+ npm run check:boundaries # dependency rules
222
+ ```
223
+
224
+ Tests live in `packages/*/src/**/*.test.ts` — the workspace vitest config only
225
+ discovers that pattern.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Public barrel for `@xcoder/jarvis-agent`.
3
+ *
4
+ * The CLI is the primary consumer, but everything the agent does is
5
+ * importable so a test (or a future orchestrator) can drive the loop, the
6
+ * memory store and the prompt assembler directly without spawning a process.
7
+ */
8
+ export { JARVIS_AGENT_NAME, EXIT, runJarvisAgent, type JarvisDeps, type JarvisResult, } from "./jarvis/agent.js";
9
+ export { MAX_ITERATIONS, runReactLoop, type LlmCompleter, type ReactLoopInput, type ReactLoopOutput, } from "./jarvis/react-loop.js";
10
+ export { ActionDecision, FinalDecision, MAX_ARGS, MAX_ANSWER, MAX_THOUGHT, ReactDecision, ReactParseError, TOOL_NAME_RE, extractJsonObject, isValidToolName, parseDecision, type ReactDecision as ReactDecisionValue, } from "./jarvis/decision.js";
11
+ export { BudgetExceeded, DEFAULT_CAPS, RunBudget, clampIterations, type BudgetCaps, type BudgetSnapshot } from "./jarvis/budget.js";
12
+ export { ArgvError, MAX_TURNS_CEILING, MAX_TURNS_DEFAULT, USAGE, parseArgv, type JarvisRunRequest, } from "./jarvis/argv.js";
13
+ export { MAX_LINE_BYTES, createArrayLineSource, createStdinLineSource, type LineResult, type LineSource, } from "./jarvis/chat/lines.js";
14
+ export { TERMINATORS, isIgnorable, isTerminator, type TerminatorWord } from "./jarvis/chat/terminator.js";
15
+ export { createChatConfirm, type ChatConfirm } from "./jarvis/chat/confirm.js";
16
+ export { CHAT_DIRECTIVE } from "./jarvis/chat/prompt.js";
17
+ export { runChatSession, runChatSessionDetailed, turnArgv, type ChatResult, type StopReason, } from "./jarvis/chat/session.js";
18
+ export { DEFAULT_MAX_BYTES, DEFAULT_MAX_ENTRIES, DEFAULT_WINDOW_ENTRIES, MemoryStore, resolveUserId, workspaceScopeId, type MemoryEntry, type MemoryKind, type MemoryScope, type MemorySource, } from "./jarvis/memory/store.js";
19
+ export { assembleSystemPrompt, wrapObservation, JARVIS_ROLE, UNTRUSTED_MEMORY_TAG, UNTRUSTED_OBSERVATION_TAG, UNTRUSTED_SKILL_TAG, type AssembledPrompt, type PromptProvenance, } from "./jarvis/prompt/assemble.js";
20
+ export { POLICY_VERSION, POLICY, REFUSAL_RULES, renderPolicy } from "./jarvis/prompt/policy.js";
21
+ export { DEFAULT_MAX_PACKS, DEFAULT_MAX_PACK_BYTES, DEFAULT_MAX_TOTAL_BYTES, discoverSkills, parseFrontmatter, type DiscoverSkillsOptions, type DiscoverSkillsResult, type SkillPack, type SkippedSkill, } from "./jarvis/prompt/skills.js";
22
+ export { ALLOWED_TOOLS, DEFAULT_LIMITS, createToolRegistry, type AllowedToolName, type ToolContext, type ToolDescriptor, type ToolLimits, type ToolObservation, type ToolRegistry, } from "./jarvis/tools/registry.js";
23
+ export { FS_TOOLS, globToRegExp, isSecretPath } from "./jarvis/tools/fs-tools.js";
24
+ export { MEMORY_TOOLS } from "./jarvis/tools/memory-tools.js";
25
+ export { RETENTION_DAYS, pruneProvenance, writeProvenance, type JarvisOutcome, type ProvenanceRecord, } from "./jarvis/telemetry/provenance.js";
26
+ export { createConfirm, denyWhenNotTty, requireConfirmation, type ConfirmFn, type ConfirmRequest, type ToolRisk } from "./jarvis/util/confirm.js";
27
+ export { createKillProbe, readKillState, stopFilePath, type KillState } from "./jarvis/util/kill.js";
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ // xcoder:version 2 | xcoder:task task-482677 | xcoder:updated 2026-11-05T03:49:01.166Z | xcoder:subtask code-st-729fd8
2
+ /**
3
+ * Public barrel for `@xcoder/jarvis-agent`.
4
+ *
5
+ * The CLI is the primary consumer, but everything the agent does is
6
+ * importable so a test (or a future orchestrator) can drive the loop, the
7
+ * memory store and the prompt assembler directly without spawning a process.
8
+ */
9
+ export { JARVIS_AGENT_NAME, EXIT, runJarvisAgent, } from "./jarvis/agent.js";
10
+ export { MAX_ITERATIONS, runReactLoop, } from "./jarvis/react-loop.js";
11
+ export { ActionDecision, FinalDecision, MAX_ARGS, MAX_ANSWER, MAX_THOUGHT, ReactDecision, ReactParseError, TOOL_NAME_RE, extractJsonObject, isValidToolName, parseDecision, } from "./jarvis/decision.js";
12
+ export { BudgetExceeded, DEFAULT_CAPS, RunBudget, clampIterations } from "./jarvis/budget.js";
13
+ export { ArgvError, MAX_TURNS_CEILING, MAX_TURNS_DEFAULT, USAGE, parseArgv, } from "./jarvis/argv.js";
14
+ export { MAX_LINE_BYTES, createArrayLineSource, createStdinLineSource, } from "./jarvis/chat/lines.js";
15
+ export { TERMINATORS, isIgnorable, isTerminator } from "./jarvis/chat/terminator.js";
16
+ export { createChatConfirm } from "./jarvis/chat/confirm.js";
17
+ export { CHAT_DIRECTIVE } from "./jarvis/chat/prompt.js";
18
+ export { runChatSession, runChatSessionDetailed, turnArgv, } from "./jarvis/chat/session.js";
19
+ export { DEFAULT_MAX_BYTES, DEFAULT_MAX_ENTRIES, DEFAULT_WINDOW_ENTRIES, MemoryStore, resolveUserId, workspaceScopeId, } from "./jarvis/memory/store.js";
20
+ export { assembleSystemPrompt, wrapObservation, JARVIS_ROLE, UNTRUSTED_MEMORY_TAG, UNTRUSTED_OBSERVATION_TAG, UNTRUSTED_SKILL_TAG, } from "./jarvis/prompt/assemble.js";
21
+ export { POLICY_VERSION, POLICY, REFUSAL_RULES, renderPolicy } from "./jarvis/prompt/policy.js";
22
+ export { DEFAULT_MAX_PACKS, DEFAULT_MAX_PACK_BYTES, DEFAULT_MAX_TOTAL_BYTES, discoverSkills, parseFrontmatter, } from "./jarvis/prompt/skills.js";
23
+ export { ALLOWED_TOOLS, DEFAULT_LIMITS, createToolRegistry, } from "./jarvis/tools/registry.js";
24
+ export { FS_TOOLS, globToRegExp, isSecretPath } from "./jarvis/tools/fs-tools.js";
25
+ export { MEMORY_TOOLS } from "./jarvis/tools/memory-tools.js";
26
+ export { RETENTION_DAYS, pruneProvenance, writeProvenance, } from "./jarvis/telemetry/provenance.js";
27
+ export { createConfirm, denyWhenNotTty, requireConfirmation } from "./jarvis/util/confirm.js";
28
+ export { createKillProbe, readKillState, stopFilePath } from "./jarvis/util/kill.js";
@@ -0,0 +1,57 @@
1
+ import type { LineSource } from "./chat/lines.js";
2
+ import { type LlmCompleter } from "./react-loop.js";
3
+ import { type JarvisOutcome } from "./telemetry/provenance.js";
4
+ import { type ConfirmFn } from "./util/confirm.js";
5
+ import { type ToolDescriptor } from "./tools/registry.js";
6
+ export declare const JARVIS_AGENT_NAME: "jarvis-agent";
7
+ /** Exit codes are the CLI contract (design §2.3, tradeoff T7). */
8
+ export declare const EXIT: {
9
+ readonly answered: 0;
10
+ readonly error: 1;
11
+ readonly iteration_limit: 2;
12
+ readonly budget_exceeded: 3;
13
+ readonly aborted: 4;
14
+ };
15
+ export interface JarvisDeps {
16
+ createClient?: () => LlmCompleter;
17
+ now?: () => Date;
18
+ workspaceRoot?: string;
19
+ memoryRoot?: string;
20
+ runLogRoot?: string;
21
+ tools?: readonly ToolDescriptor<any>[];
22
+ confirm?: ConfirmFn;
23
+ stdout?: NodeJS.WritableStream;
24
+ stderr?: NodeJS.WritableStream;
25
+ /** Injected for tests, so skill discovery never reads the real home dir. */
26
+ env?: NodeJS.ProcessEnv;
27
+ /** Chat seam: replaces process.stdin. Production builds a LineSource from it. */
28
+ stdin?: NodeJS.ReadableStream & {
29
+ isTTY?: boolean;
30
+ };
31
+ /** Chat seam: an explicit LineSource, so loop tests need no process or TTY. */
32
+ lines?: LineSource;
33
+ /** Chat seam: overrides TTY detection for the confirmation gate. */
34
+ isInteractive?: boolean;
35
+ /** Set by the chat loop: record this turn's user line before the run (T4). */
36
+ chatTurn?: boolean;
37
+ }
38
+ export interface JarvisResult {
39
+ outcome: JarvisOutcome;
40
+ answer?: string;
41
+ exitCode: number;
42
+ iterations: number;
43
+ runId: string;
44
+ session: string;
45
+ skills: string[];
46
+ provenancePath?: string;
47
+ error?: string;
48
+ }
49
+ /**
50
+ * Runs one Jarvis invocation and returns a process exit code.
51
+ *
52
+ * Never throws for expected conditions: a bad flag, a missing key, a model
53
+ * failure, a tripped kill switch and an exhausted budget all become a number
54
+ * plus a redacted message (T7). Provenance is written in a `finally` so an
55
+ * aborted run still leaves an audit record.
56
+ */
57
+ export declare function runJarvisAgent(argv: readonly string[], deps?: JarvisDeps): Promise<number>;
@@ -0,0 +1,306 @@
1
+ // xcoder:version 13 | xcoder:task task-482677 | xcoder:updated 2026-11-05T03:48:53.483Z | xcoder:subtask code-st-729fd8
2
+ import fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { createLlmClient, requestScope, shortId, XCODER_VERSION } from "xcoder-dna";
5
+ import { ArgvError, parseArgv, USAGE } from "./argv.js";
6
+ import { BudgetExceeded, clampIterations, DEFAULT_CAPS, RunBudget } from "./budget.js";
7
+ import { CHAT_DIRECTIVE } from "./chat/prompt.js";
8
+ import { runChatSession } from "./chat/session.js";
9
+ import { MemoryStore, resolveUserId, workspaceScopeId } from "./memory/store.js";
10
+ import { assembleSystemPrompt } from "./prompt/assemble.js";
11
+ import { POLICY_VERSION, renderPolicy } from "./prompt/policy.js";
12
+ import { discoverSkills } from "./prompt/skills.js";
13
+ import { MAX_ITERATIONS, runReactLoop } from "./react-loop.js";
14
+ import { writeProvenance, pruneProvenance } from "./telemetry/provenance.js";
15
+ import { createConfirm } from "./util/confirm.js";
16
+ import { sha256 } from "./util/text.js";
17
+ import { createKillProbe, readKillState } from "./util/kill.js";
18
+ import { DEFAULT_LIMITS, createToolRegistry } from "./tools/registry.js";
19
+ import { FS_TOOLS } from "./tools/fs-tools.js";
20
+ import { MEMORY_TOOLS } from "./tools/memory-tools.js";
21
+ export const JARVIS_AGENT_NAME = "jarvis-agent";
22
+ /** Exit codes are the CLI contract (design §2.3, tradeoff T7). */
23
+ export const EXIT = {
24
+ answered: 0,
25
+ error: 1,
26
+ iteration_limit: 2,
27
+ budget_exceeded: 3,
28
+ aborted: 4,
29
+ };
30
+ function emit(stream, text) {
31
+ try {
32
+ stream.write(text);
33
+ }
34
+ catch {
35
+ /* a closed stream must not fail the run */
36
+ }
37
+ }
38
+ /** Resolve a scope id from the real path, falling back to the raw path. */
39
+ async function scopeIdFor(workspaceRoot) {
40
+ try {
41
+ return workspaceScopeId(await fsp.realpath(workspaceRoot));
42
+ }
43
+ catch {
44
+ return workspaceScopeId(path.resolve(workspaceRoot));
45
+ }
46
+ }
47
+ /**
48
+ * Runs one Jarvis invocation and returns a process exit code.
49
+ *
50
+ * Never throws for expected conditions: a bad flag, a missing key, a model
51
+ * failure, a tripped kill switch and an exhausted budget all become a number
52
+ * plus a redacted message (T7). Provenance is written in a `finally` so an
53
+ * aborted run still leaves an audit record.
54
+ */
55
+ export async function runJarvisAgent(argv, deps = {}) {
56
+ const stdout = deps.stdout ?? process.stdout;
57
+ const stderr = deps.stderr ?? process.stderr;
58
+ const now = deps.now ?? (() => new Date());
59
+ let request;
60
+ try {
61
+ request = parseArgv(argv);
62
+ }
63
+ catch (err) {
64
+ const message = err instanceof ArgvError ? err.message : "invalid arguments";
65
+ emit(stderr, `jarvis: ${message}\n\n${USAGE}\n`);
66
+ return EXIT.error;
67
+ }
68
+ if (request.help) {
69
+ emit(stdout, `${USAGE}\n`);
70
+ return EXIT.answered;
71
+ }
72
+ // Chat runs many turns over one stdin/stdout pair; it owns its own loop.
73
+ if (request.chat) {
74
+ return await runChatSession(request, deps);
75
+ }
76
+ if (request.task.length === 0) {
77
+ emit(stderr, `jarvis: no task given\n\n${USAGE}\n`);
78
+ return EXIT.error;
79
+ }
80
+ const workspaceRoot = path.resolve(deps.workspaceRoot ?? process.cwd());
81
+ const memoryRoot = deps.memoryRoot ?? path.join(workspaceRoot, ".xcoder", "jarvis", "memory");
82
+ const runLogRoot = deps.runLogRoot ?? path.join(workspaceRoot, ".xcoder", "jarvis", "runs");
83
+ const env = deps.env ?? process.env;
84
+ const session = request.session ?? shortId("jarvis");
85
+ const runId = shortId("run");
86
+ const userId = resolveUserId(request.user);
87
+ const workspaceId = await scopeIdFor(workspaceRoot);
88
+ const kill = createKillProbe(workspaceRoot);
89
+ const confirm = deps.confirm ?? createConfirm({ stdin: process.stdin, stdout: stderr });
90
+ const memory = new MemoryStore({
91
+ rootDir: memoryRoot,
92
+ scope: { workspaceId, userId },
93
+ now,
94
+ });
95
+ const toolContext = {
96
+ workspaceRoot,
97
+ memory,
98
+ session,
99
+ confirm,
100
+ limits: DEFAULT_LIMITS,
101
+ };
102
+ const tools = deps.tools ?? [...FS_TOOLS, ...MEMORY_TOOLS];
103
+ const registry = createToolRegistry({ tools });
104
+ // Fail closed before doing any work at all.
105
+ const initialKill = readKillState(workspaceRoot);
106
+ if (initialKill.killed) {
107
+ const result = {
108
+ outcome: "aborted",
109
+ exitCode: EXIT.aborted,
110
+ iterations: 0,
111
+ runId,
112
+ session,
113
+ skills: [],
114
+ error: `kill switch tripped (${initialKill.reason})`,
115
+ };
116
+ emit(stderr, `jarvis: aborted — kill switch tripped (${initialKill.reason})\n`);
117
+ await safeWriteProvenance(runLogRoot, result, {
118
+ workspaceId,
119
+ userId,
120
+ policyVersion: POLICY_VERSION,
121
+ policySha256: sha256(renderPolicy(registry.describeForPrompt())),
122
+ skills: [],
123
+ skippedSkills: [],
124
+ toolAllowlist: registry.allowlist(),
125
+ toolsUsed: [],
126
+ memoryWrites: 0,
127
+ redactions: 0,
128
+ now,
129
+ });
130
+ return EXIT.aborted;
131
+ }
132
+ // ---- skills ----
133
+ const discovered = await discoverSkills({
134
+ workspaceRoot,
135
+ allow: request.skills.length > 0 ? request.skills : undefined,
136
+ env,
137
+ });
138
+ if (request.skills.length > 0 && discovered.packs.length === 0) {
139
+ emit(stderr, `jarvis: none of the requested skill packs were found (${request.skills.join(", ")})\n`);
140
+ return EXIT.error;
141
+ }
142
+ // In chat mode the user's line is recorded BEFORE the turn runs, so a failed
143
+ // turn still leaves a trace (tradeoff T4). It is stored as user-sourced data.
144
+ if (deps.chatTurn && request.memory) {
145
+ try {
146
+ await memory.append({
147
+ session,
148
+ kind: "task",
149
+ value: request.task,
150
+ source: "user",
151
+ });
152
+ }
153
+ catch {
154
+ /* a read-only memory dir must not fail the turn */
155
+ }
156
+ }
157
+ // ---- memory window (re-read every turn: this IS the continuity mechanism) ----
158
+ let window = [];
159
+ if (request.memory) {
160
+ try {
161
+ window = await memory.window();
162
+ }
163
+ catch {
164
+ window = [];
165
+ }
166
+ }
167
+ // ---- prompt ----
168
+ // The directive is the TRUSTED instruction section (PRM-02). A chat turn must
169
+ // never splice the user's text there, so it passes a constant; user text only
170
+ // ever arrives as transcript DATA (AC15).
171
+ const directive = deps.chatTurn ? CHAT_DIRECTIVE : request.task;
172
+ const assembled = assembleSystemPrompt({
173
+ agentName: JARVIS_AGENT_NAME,
174
+ directive,
175
+ skills: discovered.packs,
176
+ window,
177
+ toolAllowlist: registry.allowlist(),
178
+ toolManifest: registry.describeForPrompt(),
179
+ skippedSkills: discovered.skipped,
180
+ now: now(),
181
+ });
182
+ // ---- client ----
183
+ let client;
184
+ try {
185
+ client = deps.createClient ? deps.createClient() : createLlmClient("orchestrator");
186
+ }
187
+ catch (err) {
188
+ const message = err instanceof Error ? err.message.slice(0, 300) : "model client could not start";
189
+ emit(stderr, `jarvis: ${message}\n`);
190
+ return EXIT.error;
191
+ }
192
+ const budget = new RunBudget({ ...DEFAULT_CAPS, maxIterations: clampIterations(request.maxIterations, MAX_ITERATIONS) }, now);
193
+ const toolsUsed = [];
194
+ let outcome = "error";
195
+ let answer;
196
+ let iterations = 0;
197
+ let errorMessage;
198
+ let memoryWrites = 0;
199
+ try {
200
+ // Install a recordUsage sink so dna-side usage events reach provenance.
201
+ const loopResult = await requestScope.run({ recordUsage: () => undefined }, () => runReactLoop({
202
+ task: request.task,
203
+ systemPrompt: assembled,
204
+ client,
205
+ registry,
206
+ toolContext,
207
+ maxIterations: clampIterations(request.maxIterations, MAX_ITERATIONS),
208
+ budget,
209
+ isKilled: kill,
210
+ onToolUsed: (name, observation) => {
211
+ toolsUsed.push({ name, ok: observation.ok, bytes: observation.bytes });
212
+ if (name === "remember" && observation.ok)
213
+ memoryWrites += 1;
214
+ },
215
+ }));
216
+ outcome = loopResult.outcome;
217
+ answer = loopResult.answer;
218
+ iterations = loopResult.iterations;
219
+ errorMessage = loopResult.error;
220
+ }
221
+ catch (err) {
222
+ if (err instanceof BudgetExceeded) {
223
+ outcome = "budget_exceeded";
224
+ errorMessage = err.message;
225
+ }
226
+ else {
227
+ outcome = "error";
228
+ errorMessage = err instanceof Error ? err.message.slice(0, 300) : "run failed";
229
+ if (process.env["JARVIS_DEBUG"] === "1" && err instanceof Error && err.stack) {
230
+ emit(stderr, `${err.stack}\n`);
231
+ }
232
+ }
233
+ }
234
+ const exitCode = EXIT[outcome];
235
+ const result = {
236
+ outcome,
237
+ ...(answer !== undefined ? { answer } : {}),
238
+ exitCode,
239
+ iterations,
240
+ runId,
241
+ session,
242
+ skills: discovered.packs.map((p) => p.name),
243
+ ...(errorMessage !== undefined ? { error: errorMessage } : {}),
244
+ };
245
+ const usage = budget.snapshot();
246
+ const provenancePath = await safeWriteProvenance(runLogRoot, result, {
247
+ workspaceId,
248
+ userId,
249
+ policyVersion: assembled.provenance.policyVersion,
250
+ policySha256: assembled.provenance.policySha256,
251
+ skills: assembled.provenance.skills.map((s) => ({ name: s.name, sha256: s.sha256 })),
252
+ skippedSkills: assembled.provenance.skippedSkills,
253
+ toolAllowlist: registry.allowlist(),
254
+ toolsUsed,
255
+ memoryWrites,
256
+ redactions: memory.redactions(),
257
+ usage: { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens },
258
+ outcome,
259
+ iterations,
260
+ error: errorMessage,
261
+ now,
262
+ });
263
+ // Retention pruning is housekeeping: off the hot path, never fatal.
264
+ void pruneProvenance(runLogRoot, now()).catch(() => undefined);
265
+ // ---- report ----
266
+ if (request.json) {
267
+ emit(stdout, JSON.stringify({ ...result, provenancePath }) + "\n");
268
+ }
269
+ else if (outcome === "answered") {
270
+ emit(stdout, `${answer ?? ""}\n`);
271
+ }
272
+ else {
273
+ emit(stderr, `jarvis: ${outcome}${errorMessage ? ` — ${errorMessage}` : ""}\n`);
274
+ }
275
+ return exitCode;
276
+ }
277
+ async function safeWriteProvenance(dir, result, extra) {
278
+ try {
279
+ return await writeProvenance(dir, {
280
+ v: 1,
281
+ runId: result.runId,
282
+ ts: extra.now().toISOString(),
283
+ agent: JARVIS_AGENT_NAME,
284
+ version: XCODER_VERSION,
285
+ workspaceId: extra.workspaceId,
286
+ userId: extra.userId,
287
+ session: result.session,
288
+ policyVersion: extra.policyVersion,
289
+ policySha256: extra.policySha256,
290
+ skills: extra.skills,
291
+ skippedSkills: extra.skippedSkills,
292
+ toolAllowlist: extra.toolAllowlist,
293
+ toolsUsed: extra.toolsUsed,
294
+ iterations: extra.iterations ?? result.iterations,
295
+ outcome: extra.outcome ?? result.outcome,
296
+ usage: extra.usage ?? { inputTokens: 0, outputTokens: 0 },
297
+ memoryWrites: extra.memoryWrites,
298
+ redactions: extra.redactions,
299
+ ...(extra.error !== undefined ? { error: extra.error } : {}),
300
+ });
301
+ }
302
+ catch {
303
+ // A read-only workspace must not turn a good answer into a failure.
304
+ return undefined;
305
+ }
306
+ }