@causyne/agent 1.10.17 → 1.10.18

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.
@@ -31,7 +31,7 @@ are application policies and require adversarial review.
31
31
  | Concern | Source |
32
32
  | --- | --- |
33
33
  | Invocation and local session | `entrypoint.js`, `cliMode.js`, `tui/` |
34
- | Orchestration and budgets | `agentLoop.js`, `agentProfiles.ts`, `subagentJournal.ts`, `modelRouter.js`, `tokenTracker.js` |
34
+ | Orchestration and budgets | `agentLoop.js`, `decisionPolicy.ts`, `implementationPlan.ts`, `agentProfiles.ts`, `subagentJournal.ts`, `modelRouter.js`, `tokenTracker.js` |
35
35
  | Host execution | `tools/registry.js`, `sandbox.js`, `execSafe.js` |
36
36
  | Models | `providers/`, `modelCatalogue.js` |
37
37
  | Context and memory | `memory.js`, `memoryManager.js`, `contextManager.js`, `cortex/` |
@@ -49,6 +49,20 @@ Runtime validation is still required at model, filesystem, subprocess and
49
49
  extension boundaries. Rust is reserved for a measured, narrow native worker;
50
50
  no Rust rewrite is underway.
51
51
 
52
+ ## Planning and durable decisions
53
+
54
+ Complex implementation uses `update_plan` to maintain a validated checklist in
55
+ session state. The TUI keeps the current steps visible beside the input surface;
56
+ plans are not written into the user's project. A mutating tool call on a complex
57
+ task is held until the model has created this plan.
58
+
59
+ `request_decision` handles choices with durable product, architecture, data,
60
+ security, or workflow consequences. It presents two to four concrete options
61
+ and a custom-answer path. This checkpoint is distinct from a tool permission:
62
+ selecting a design never grants filesystem or command access. The checkpoint
63
+ runs in the provider-independent agent loop, so Mode C can remove routine
64
+ approval friction without silently choosing an unresolved architecture.
65
+
52
66
  ## Tool and extension boundaries
53
67
 
54
68
  Tool schemas are assembled once from native tools, explicitly enabled plugins,
@@ -92,16 +106,19 @@ browser OAuth is not implemented yet; an HTTP 401 is surfaced as
92
106
 
93
107
  Causyne's web foundation has two layers. Lightweight TypeScript search and
94
108
  fetch clients work without a browser runtime. Rendered navigation uses a
95
- dedicated in-process Camoufox adapter derived from the locally reviewed Camofox
96
- Browser source. The adapter is deliberately smaller than the reference service:
109
+ dedicated in-process Causyne Browser adapter built from a locally reviewed
110
+ upstream browser engine. Required upstream names and licenses remain in the
111
+ third-party notices. The adapter is deliberately smaller than the reference
112
+ service:
97
113
  it does not import an HTTP server, inbound port, cloud deployment layer, VNC,
98
114
  telemetry, remote WebSocket control, or OpenClaw integration.
99
115
 
100
- Camoufox and Playwright are exact-pinned optional capability packages installed
101
- into the user's private Causyne capability directory only after an explicit
102
- browser capability grant. Causyne does not download a browser binary. Advanced
103
- rendered browsing requires a separately installed Camoufox bundle selected by
104
- `CAUSYNE_CAMOFOX_EXECUTABLE` or discovered from a standard system location.
116
+ The browser engine and Playwright are exact-pinned optional capability packages
117
+ installed into the user's private Causyne capability directory only after an
118
+ explicit browser capability grant. Causyne does not download a browser binary. Advanced
119
+ rendered browsing requires a separately installed supported browser runtime
120
+ selected by `CAUSYNE_BROWSER_EXECUTABLE` or discovered from a standard system
121
+ location.
105
122
  The main npm package therefore stays small and web search remains useful when
106
123
  the optional rendered browser is absent.
107
124
 
@@ -66,12 +66,12 @@ The active source takes a different architecture and distribution path:
66
66
  approval class. Curated assets are size-limited, constrained to catalog URLs,
67
67
  protected from output path traversal, and are not automatically extracted.
68
68
  - Web search uses bounded HTTP providers without requiring a browser binary.
69
- Interactive rendered browsing uses an outbound, in-process Camoufox adapter.
70
- Causyne does not run Camofox Browser's server, inbound port, remote control,
69
+ Interactive rendered browsing uses an outbound, in-process Causyne Browser adapter.
70
+ Causyne does not run an inbound browser-control server, inbound port, remote control,
71
71
  VNC, telemetry, cloud deployment, or OpenClaw integration. Optional launcher
72
72
  packages require an explicit capability grant. Causyne never downloads the
73
- Camoufox browser binary; an existing external installation must be selected
74
- through `CAUSYNE_CAMOFOX_EXECUTABLE` or a standard system location.
73
+ browser runtime; an existing external installation must be selected
74
+ through `CAUSYNE_BROWSER_EXECUTABLE` or a standard system location.
75
75
  - Browser navigation and subresources accept only public HTTP(S) destinations.
76
76
  Hostnames are resolved and checked against private, loopback, link-local,
77
77
  multicast, reserved, local, internal, and metadata ranges. Non-browser
@@ -27,6 +27,9 @@ No dependency in the reviewed main lockfile was marked with an install script.
27
27
  Optional capability dependencies are installed only after approval into a
28
28
  separate local environment and are not covered by the main lockfile review.
29
29
 
30
+ The product-facing browser capability is **Causyne Browser**. Upstream names
31
+ below are used only for required attribution and exact dependency identifiers.
32
+
30
33
  The optional browser capability installs `camoufox-js` 0.11.5 under MPL-2.0
31
34
  and `playwright-core` 1.58.0 under Apache-2.0. They remain separate packages in
32
35
  the user's local capability directory and are not copied into Causyne Agent's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@causyne/agent",
3
- "version": "1.10.17",
3
+ "version": "1.10.18",
4
4
  "description": "Agentic AI for software, tools, and computer workflows",
5
5
  "main": "bin/causyne.js",
6
6
  "type": "module",
package/src/agentLoop.js CHANGED
@@ -20,6 +20,7 @@ import { analyzeTask, buildTaskBrief, packsFromShape } from './taskShape.js';
20
20
  import { lookupPlan, recordPlan, recordMiss, renderPlanHint, toStep } from './planCache.js';
21
21
  import { routedConfig } from './modelRouter.js';
22
22
  import { assessStep, isThrashing } from './uncertainty.js';
23
+ import { assessDecisionRequirement, isMutatingToolCall, requiresImplementationPlan } from './decisionPolicy.js';
23
24
  import { VFETracker } from './vfeTracker.js';
24
25
  import {
25
26
  routePacks,
@@ -75,11 +76,15 @@ Specifically, you excel at:
75
76
  - **No Chitchat**: Tools are for actions, text output is *only* for communication. Do not add explanatory comments within tool calls or code blocks.
76
77
  - **File Paths**: ALWAYS output the absolute path of any file you create, modify, or generate so the user knows exactly where it is.
77
78
 
78
- # Task Management & Planning (TodoWrite)
79
- - ALWAYS use the TodoWrite tool to track and manage tasks before starting implementation. Create atomic, verb-led, clear outcome items.
79
+ # Task Management & Planning
80
+ - Use update_plan to create a visible implementation checklist before complex implementation. Create atomic, verb-led, verifiable steps and keep exactly one step in progress.
80
81
  - Check off completed TODOs immediately before reporting progress. Do not batch up multiple task completions.
81
82
  - Reconcile and close the todo list when all goal tasks are done. If the user asks you to implement, do not output a separate text-based plan. Just build and display the todo list.
82
83
 
84
+ # Durable Decisions
85
+ - Use request_decision when multiple materially different architecture, product, data, security, or workflow directions are valid. Give 2-4 concrete options with consequences; the UI adds a custom-answer choice.
86
+ - A decision is separate from permission. Do not ask for routine implementation details that can be resolved from the repository. Even in Mode C, pause for an unresolved durable architecture decision before changing the system.
87
+
83
88
  # Maximize Parallel Tool Calls (Speed is Key)
84
89
  - CRITICAL INSTRUCTION: For maximum efficiency, whenever you perform multiple operations, invoke all relevant tools concurrently (in parallel) rather than sequentially.
85
90
  - Searching for different patterns (imports, usage, definitions) MUST happen in parallel. Reading multiple files MUST happen in parallel.
@@ -344,6 +349,12 @@ export class AgentLoop {
344
349
  this.circuitBreakerUnlocked = false;
345
350
  this.stopRequested = false;
346
351
  this.pendingApprovals = new Map(); // Maps toolCallId -> resolve function
352
+ this.waitForDecision = async () => null;
353
+ this._decisionAnswers = new Map();
354
+ this._decisionRequirement = { required: false, kind: 'architecture', reason: '' };
355
+ this._decisionResolved = true;
356
+ this._implementationPlan = [];
357
+ this._implementationPlanRequired = false;
347
358
  // A delegated subagent can tighten these two ceilings. They are applied
348
359
  // after every config refresh so run() cannot accidentally replace a
349
360
  // worker's declared bounds with the parent tier defaults.
@@ -700,6 +711,58 @@ export class AgentLoop {
700
711
  }
701
712
  }
702
713
 
714
+ updateImplementationPlan(update) {
715
+ this._implementationPlan = update.plan.map(item => ({ ...item }));
716
+ this.emitEvent({
717
+ type: 'agent_plan',
718
+ explanation: update.explanation || '',
719
+ plan: this._implementationPlan.map(item => ({ status: item.status, text: item.step, id: item.id }))
720
+ });
721
+ auditLog.log({
722
+ event: 'implementation_plan_updated', taskId: this.taskId,
723
+ steps: this._implementationPlan.length,
724
+ completed: this._implementationPlan.filter(item => item.status === 'completed').length
725
+ });
726
+ }
727
+
728
+ async requestDecision(request) {
729
+ const fingerprint = createHash('sha256')
730
+ .update(JSON.stringify({ kind: request.kind, question: request.question, options: request.options.map(option => option.id) }))
731
+ .digest('hex');
732
+ const remembered = this._decisionAnswers.get(fingerprint);
733
+ if (remembered) return remembered;
734
+
735
+ this.emitEvent({ type: 'decision_request', request });
736
+ auditLog.log({ event: 'decision_requested', taskId: this.taskId, kind: request.kind, optionCount: request.options.length });
737
+ const answer = await this.waitForDecision(request);
738
+ if (!answer || typeof answer.label !== 'string' || !answer.label.trim()) {
739
+ this.emitEvent({ type: 'decision_result', request, answer: null });
740
+ return null;
741
+ }
742
+ const normalized = {
743
+ requestId: request.id,
744
+ optionId: typeof answer.optionId === 'string' ? answer.optionId : null,
745
+ label: answer.label.trim(),
746
+ custom: answer.custom === true
747
+ };
748
+ this._decisionAnswers.set(fingerprint, normalized);
749
+ if (request.kind === 'architecture') this._decisionResolved = true;
750
+ this.emitEvent({ type: 'decision_result', request, answer: normalized });
751
+ auditLog.log({ event: 'decision_answered', taskId: this.taskId, kind: request.kind, custom: normalized.custom });
752
+ return normalized;
753
+ }
754
+
755
+ mutationCheckpoint(functionName, args = {}) {
756
+ if (!isMutatingToolCall(functionName, args)) return null;
757
+ if (this._decisionRequirement.required && !this._decisionResolved) {
758
+ return 'ARCHITECTURE_DECISION_REQUIRED: Before changing the system, call request_decision with kind "architecture", 2-4 concrete options, their consequences, and a clear reason. Do not ask about routine details; this checkpoint exists because the current request crosses a durable architecture boundary.';
759
+ }
760
+ if (this._implementationPlanRequired && this._implementationPlan.length === 0) {
761
+ return 'IMPLEMENTATION_PLAN_REQUIRED: Call update_plan with atomic, verifiable steps and exactly one in_progress step before mutating this complex task.';
762
+ }
763
+ return null;
764
+ }
765
+
703
766
  updateTierLimits() {
704
767
  this.config = loadConfig();
705
768
  if (typeof this.initializeProvider === 'function') {
@@ -816,10 +879,26 @@ export class AgentLoop {
816
879
  this._lessonSimilarity = 0;
817
880
  this._lastToolResult = null;
818
881
  this._paradigmShiftPending = false;
882
+ this._implementationPlan = [];
883
+ this._decisionAnswers = new Map();
884
+ this._decisionRequirement = assessDecisionRequirement(this.userPrompt || '');
885
+ this._decisionResolved = !this._decisionRequirement.required;
886
+ this._implementationPlanRequired = false;
819
887
  if (this.vfeTracker) this.vfeTracker.reset();
820
888
 
821
889
  if (this.userPrompt) {
822
890
  this.taskShape = analyzeTask(this.userPrompt, this.workspaceContext);
891
+ this._implementationPlanRequired = requiresImplementationPlan(
892
+ this.userPrompt,
893
+ this.taskShape?.archetype || '',
894
+ this.taskShape?.confidence || ''
895
+ );
896
+ // request_decision is comparatively large and most tasks never need it.
897
+ // On providers without server-side schema deferral, expose it from the
898
+ // first model call only when the deterministic task policy found an
899
+ // unresolved durable choice. This keeps the resident prompt within its
900
+ // measured budget without making the checkpoint undiscoverable.
901
+ if (this._decisionRequirement.required) this.unlockedTools.add('request_decision');
823
902
 
824
903
  // Scored routing, with the regex router as a safety net so a pack can
825
904
  // never be missed just because it scored below threshold.
@@ -1077,9 +1156,8 @@ export class AgentLoop {
1077
1156
  // `tool_search_tool_regex`) keep the whole core in context for free —
1078
1157
  // the API holds the schemas server-side and expands them on demand,
1079
1158
  // with no discovery round trip. Every OTHER provider was serialising
1080
- // all 24 core schemas into every request: 3,341 tokens to advertise
1081
- // tools a typical turn never calls. There, ship the resident 8 (1,059
1082
- // tokens) and let `tool_search` — the same mechanism that already
1159
+ // all 24 core schemas into every request. There, ship the small resident
1160
+ // set and let `tool_search` — the same mechanism that already
1083
1161
  // unlocks packs — fetch the rest.
1084
1162
  const residentOnly = this.llm.supportsToolSearch?.() !== true;
1085
1163
  let dynamicTools = await getDynamicTools(this.config.autonomyMode, this.llm.supportsVision, {
@@ -1408,18 +1486,6 @@ export class AgentLoop {
1408
1486
  this.messages.push(assistantMsg);
1409
1487
 
1410
1488
  if (response.content) {
1411
- // V5: Parse TodoWrite checklist for visual plan display
1412
- const planRegex = /-\s*\[([ xX\/])\]\s*(.*)/g;
1413
- const planMatch = [...response.content.matchAll(planRegex)];
1414
- if (planMatch.length > 0) {
1415
- const planTasks = planMatch.map(m => {
1416
- const marker = m[1].toLowerCase();
1417
- const status = marker === 'x' ? 'completed' : (marker === '/' ? 'in_progress' : 'pending');
1418
- return { status, text: m[2].trim() };
1419
- });
1420
- this.emitEvent({ type: 'agent_plan', plan: planTasks });
1421
- }
1422
-
1423
1489
  this.emitEvent({ type: 'agent_message', content: response.content });
1424
1490
  }
1425
1491
 
@@ -1549,7 +1615,7 @@ export class AgentLoop {
1549
1615
  let webGuardMsg = '';
1550
1616
 
1551
1617
  if (!browserReady && !browserDeclined) {
1552
- webGuardMsg = `[ANTI-SLOP CHECK: You are building a Web/UI project and must inspect it before finishing, but the dedicated browser capability is not enabled yet. Call \`manage_local_dependencies\` with action "grant_capability" and group "browser" to request the Causyne Camoufox runtime. If the user declines, read the final compiled HTML/CSS instead. This check fires once.]`;
1618
+ webGuardMsg = `[ANTI-SLOP CHECK: You are building a Web/UI project and must inspect it before finishing, but the dedicated browser capability is not enabled yet. Call \`manage_local_dependencies\` with action "grant_capability" and group "browser" to request the Causyne Browser runtime. If the user declines, read the final compiled HTML/CSS instead. This check fires once.]`;
1553
1619
  } else if (hasVision && !visionReady && !visionDeclined) {
1554
1620
  webGuardMsg = `[ANTI-SLOP CHECK: The browser is available, but desktop screen capture is not enabled. Call \`manage_local_dependencies\` with action "grant_capability" and group "vision" to request it, or use \`browser_act\` snapshots and its workspace-scoped screenshot action. This check fires once.]`;
1555
1621
  } else if (hasVision && visionReady && !this.webVerificationState.screenshotTaken) {
@@ -1803,6 +1869,20 @@ export class AgentLoop {
1803
1869
  continue;
1804
1870
  }
1805
1871
 
1872
+ const checkpointError = this.mutationCheckpoint(functionName, args);
1873
+ if (checkpointError) {
1874
+ this.messages.push({
1875
+ role: 'tool',
1876
+ tool_call_id: toolCall.id,
1877
+ name: functionName,
1878
+ content: JSON.stringify({
1879
+ success: false,
1880
+ error: checkpointError
1881
+ })
1882
+ });
1883
+ continue;
1884
+ }
1885
+
1806
1886
  // A policy hook runs after the built-in sandbox but before any user
1807
1887
  // approval prompt, so a deterministic denial never asks the user to
1808
1888
  // approve work that will be rejected anyway.
@@ -2266,7 +2346,7 @@ export class AgentLoop {
2266
2346
  //
2267
2347
  // Only payloads worth the trade are folded. A small `{"success":true}`
2268
2348
  // ack costs less than the pointer that would replace it, and every
2269
- // TodoWrite would otherwise collide with every other TodoWrite.
2349
+ // Plan updates would otherwise collide with unrelated plan updates.
2270
2350
  const OBSERVATION_FOLD_FLOOR = 600;
2271
2351
  if (resultString.length >= OBSERVATION_FOLD_FLOOR) {
2272
2352
  const digest = createHash('sha256').update(resultString).digest('hex');
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  function assertExecutable(candidate) {
5
5
  const stat = statSync(candidate);
6
6
  if (!stat.isFile() && !stat.isSymbolicLink()) {
7
- throw new Error(`Camoufox executable is not a file: ${candidate}`);
7
+ throw new Error(`Causyne Browser executable is not a file: ${candidate}`);
8
8
  }
9
9
  if (process.platform !== 'win32')
10
10
  accessSync(candidate, constants.X_OK);
@@ -61,33 +61,28 @@ function resourceDirectoryFor(executablePath) {
61
61
  }
62
62
  return path.dirname(executablePath);
63
63
  }
64
- export function resolveCamoufoxRuntime() {
65
- const configured = process.env.CAUSYNE_CAMOFOX_EXECUTABLE?.trim();
66
- const configuredHome = process.env.CAMOUFOX_INSTALL_DIR?.trim();
67
- const candidates = configured
68
- ? [path.resolve(configured)]
69
- : configuredHome
70
- ? [expectedExecutable(path.resolve(configuredHome))]
71
- : candidateExecutables();
64
+ export function resolveBrowserRuntime() {
65
+ const configured = process.env.CAUSYNE_BROWSER_EXECUTABLE?.trim();
66
+ const candidates = configured ? [path.resolve(configured)] : candidateExecutables();
72
67
  const executablePath = candidates.find(existsSync);
73
68
  if (!executablePath) {
74
- throw new Error('Camoufox is not installed. Causyne does not download browser binaries automatically. ' +
75
- 'Install a trusted Camoufox build separately, then set CAUSYNE_CAMOFOX_EXECUTABLE to its executable path.');
69
+ throw new Error('Causyne Browser runtime is not installed. Causyne does not download browser binaries automatically. ' +
70
+ 'Install a supported browser runtime separately, then set CAUSYNE_BROWSER_EXECUTABLE to its executable path.');
76
71
  }
77
72
  assertExecutable(executablePath);
78
73
  const resolvedExecutable = realpathSync(executablePath);
79
74
  const installDirectory = findInstallDirectory(resolvedExecutable);
80
75
  if (!installDirectory) {
81
- throw new Error(`Camoufox at ${resolvedExecutable} is not inside a complete distribution. ` +
82
- 'Keep the distribution version.json beside its platform bundle and point CAUSYNE_CAMOFOX_EXECUTABLE at that bundle.');
76
+ throw new Error(`Causyne Browser runtime at ${resolvedExecutable} is not a complete distribution. ` +
77
+ 'Keep version.json beside its platform bundle and point CAUSYNE_BROWSER_EXECUTABLE at that bundle.');
83
78
  }
84
79
  const resourceDirectory = resourceDirectoryFor(resolvedExecutable);
85
80
  if (!existsSync(path.join(resourceDirectory, 'properties.json'))) {
86
- throw new Error(`Camoufox properties.json was not found in ${resourceDirectory}.`);
81
+ throw new Error(`Causyne Browser runtime metadata was not found in ${resourceDirectory}.`);
87
82
  }
88
83
  return {
89
84
  installDirectory,
90
85
  resourceDirectory,
91
- source: configured || configuredHome ? 'environment' : 'system'
86
+ source: configured ? 'environment' : 'system'
92
87
  };
93
88
  }
@@ -6,7 +6,7 @@ import path from 'node:path';
6
6
  // discovery at commit 2c0036a8b18bf9cd983376751ff4d52dfd9add0f. Causyne
7
7
  // keeps this integration layer in TypeScript and never downloads a browser.
8
8
 
9
- export interface PreparedCamoufoxRuntime {
9
+ export interface PreparedBrowserRuntime {
10
10
  installDirectory: string;
11
11
  resourceDirectory: string;
12
12
  source: 'environment' | 'system';
@@ -15,7 +15,7 @@ export interface PreparedCamoufoxRuntime {
15
15
  function assertExecutable(candidate: string): void {
16
16
  const stat = statSync(candidate);
17
17
  if (!stat.isFile() && !stat.isSymbolicLink()) {
18
- throw new Error(`Camoufox executable is not a file: ${candidate}`);
18
+ throw new Error(`Causyne Browser executable is not a file: ${candidate}`);
19
19
  }
20
20
  if (process.platform !== 'win32') accessSync(candidate, constants.X_OK);
21
21
  }
@@ -74,19 +74,14 @@ function resourceDirectoryFor(executablePath: string): string {
74
74
  return path.dirname(executablePath);
75
75
  }
76
76
 
77
- export function resolveCamoufoxRuntime(): PreparedCamoufoxRuntime {
78
- const configured = process.env.CAUSYNE_CAMOFOX_EXECUTABLE?.trim();
79
- const configuredHome = process.env.CAMOUFOX_INSTALL_DIR?.trim();
80
- const candidates = configured
81
- ? [path.resolve(configured)]
82
- : configuredHome
83
- ? [expectedExecutable(path.resolve(configuredHome))]
84
- : candidateExecutables();
77
+ export function resolveBrowserRuntime(): PreparedBrowserRuntime {
78
+ const configured = process.env.CAUSYNE_BROWSER_EXECUTABLE?.trim();
79
+ const candidates = configured ? [path.resolve(configured)] : candidateExecutables();
85
80
  const executablePath = candidates.find(existsSync);
86
81
  if (!executablePath) {
87
82
  throw new Error(
88
- 'Camoufox is not installed. Causyne does not download browser binaries automatically. ' +
89
- 'Install a trusted Camoufox build separately, then set CAUSYNE_CAMOFOX_EXECUTABLE to its executable path.'
83
+ 'Causyne Browser runtime is not installed. Causyne does not download browser binaries automatically. ' +
84
+ 'Install a supported browser runtime separately, then set CAUSYNE_BROWSER_EXECUTABLE to its executable path.'
90
85
  );
91
86
  }
92
87
 
@@ -95,18 +90,18 @@ export function resolveCamoufoxRuntime(): PreparedCamoufoxRuntime {
95
90
  const installDirectory = findInstallDirectory(resolvedExecutable);
96
91
  if (!installDirectory) {
97
92
  throw new Error(
98
- `Camoufox at ${resolvedExecutable} is not inside a complete distribution. ` +
99
- 'Keep the distribution version.json beside its platform bundle and point CAUSYNE_CAMOFOX_EXECUTABLE at that bundle.'
93
+ `Causyne Browser runtime at ${resolvedExecutable} is not a complete distribution. ` +
94
+ 'Keep version.json beside its platform bundle and point CAUSYNE_BROWSER_EXECUTABLE at that bundle.'
100
95
  );
101
96
  }
102
97
  const resourceDirectory = resourceDirectoryFor(resolvedExecutable);
103
98
  if (!existsSync(path.join(resourceDirectory, 'properties.json'))) {
104
- throw new Error(`Camoufox properties.json was not found in ${resourceDirectory}.`);
99
+ throw new Error(`Causyne Browser runtime metadata was not found in ${resourceDirectory}.`);
105
100
  }
106
101
 
107
102
  return {
108
103
  installDirectory,
109
104
  resourceDirectory,
110
- source: configured || configuredHome ? 'environment' : 'system'
105
+ source: configured ? 'environment' : 'system'
111
106
  };
112
107
  }
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { capabilityDirectory, ensureCapability, loadCapabilityPackage } from '../provisioning/capabilityProvisioner.js';
5
- import { resolveCamoufoxRuntime } from './camofoxRuntime.js';
5
+ import { resolveBrowserRuntime } from './browserRuntime.js';
6
6
  import { assertPublicWebUrl } from './urlPolicy.js';
7
7
  import { windowSnapshot } from './snapshotWindow.js';
8
8
  const NAVIGATION_TIMEOUT_MS = 30_000;
@@ -113,7 +113,7 @@ const SEARCH_EXTRACT_SCRIPT = `(() => {
113
113
  export class CausyneBrowser {
114
114
  workspace;
115
115
  ensureBrowserCapability;
116
- loadCamoufox;
116
+ loadBrowserEngine;
117
117
  resolveRuntime;
118
118
  profileDirectory;
119
119
  visible;
@@ -129,8 +129,8 @@ export class CausyneBrowser {
129
129
  constructor(options = {}) {
130
130
  this.workspace = path.resolve(options.workspace ?? process.cwd());
131
131
  this.ensureBrowserCapability = options.ensureBrowserCapability ?? (() => ensureCapability('browser'));
132
- this.loadCamoufox = options.loadCamoufox ?? (async () => await loadCapabilityPackage('camoufox-js'));
133
- this.resolveRuntime = options.resolveRuntime ?? resolveCamoufoxRuntime;
132
+ this.loadBrowserEngine = options.loadBrowserEngine ?? (async () => await loadCapabilityPackage('camoufox-js'));
133
+ this.resolveRuntime = options.resolveRuntime ?? resolveBrowserRuntime;
134
134
  this.profileDirectory = path.resolve(options.profileDirectory ?? path.join(capabilityDirectory(), 'browser-profile'));
135
135
  this.visible = options.visible ?? process.env.CAUSYNE_BROWSER_VISIBLE === '1';
136
136
  this.validateUrl = options.validateUrl ?? assertPublicWebUrl;
@@ -215,9 +215,9 @@ export class CausyneBrowser {
215
215
  const runtime = this.resolveRuntime();
216
216
  process.env.CAMOUFOX_INSTALL_DIR = runtime.installDirectory;
217
217
  await fs.mkdir(this.profileDirectory, { recursive: true, mode: 0o700 });
218
- const module = await this.loadCamoufox();
218
+ const module = await this.loadBrowserEngine();
219
219
  if (!module || typeof module.Camoufox !== 'function')
220
- throw new Error('The Camoufox capability has no usable launcher');
220
+ throw new Error('Causyne Browser has no usable launcher');
221
221
  this.session = await module.Camoufox({
222
222
  headless: !this.visible,
223
223
  user_data_dir: this.profileDirectory,
@@ -7,7 +7,7 @@ import {
7
7
  ensureCapability,
8
8
  loadCapabilityPackage
9
9
  } from '../provisioning/capabilityProvisioner.js';
10
- import { resolveCamoufoxRuntime, type PreparedCamoufoxRuntime } from './camofoxRuntime.js';
10
+ import { resolveBrowserRuntime, type PreparedBrowserRuntime } from './browserRuntime.js';
11
11
  import { assertPublicWebUrl } from './urlPolicy.js';
12
12
  import { windowSnapshot, type SnapshotWindow } from './snapshotWindow.js';
13
13
 
@@ -76,7 +76,7 @@ interface BrowserSession {
76
76
  on?(event: 'page', handler: (page: BrowserPage) => void): void;
77
77
  }
78
78
 
79
- interface CamoufoxModule {
79
+ interface BrowserEngineModule {
80
80
  Camoufox(options: Record<string, unknown>): Promise<BrowserSession>;
81
81
  }
82
82
 
@@ -132,8 +132,8 @@ export interface BrowserActionResult {
132
132
  interface RuntimeOptions {
133
133
  workspace?: string;
134
134
  ensureBrowserCapability?: () => Promise<{ ok: boolean; reason?: string; requiresConsent?: boolean }>;
135
- loadCamoufox?: () => Promise<CamoufoxModule>;
136
- resolveRuntime?: () => PreparedCamoufoxRuntime;
135
+ loadBrowserEngine?: () => Promise<BrowserEngineModule>;
136
+ resolveRuntime?: () => PreparedBrowserRuntime;
137
137
  profileDirectory?: string;
138
138
  visible?: boolean;
139
139
  validateUrl?: (value: string) => Promise<URL>;
@@ -240,7 +240,7 @@ const SEARCH_EXTRACT_SCRIPT = `(() => {
240
240
  export class CausyneBrowser {
241
241
  private readonly workspace: string;
242
242
  private readonly ensureBrowserCapability: NonNullable<RuntimeOptions['ensureBrowserCapability']>;
243
- private readonly loadCamoufox: NonNullable<RuntimeOptions['loadCamoufox']>;
243
+ private readonly loadBrowserEngine: NonNullable<RuntimeOptions['loadBrowserEngine']>;
244
244
  private readonly resolveRuntime: NonNullable<RuntimeOptions['resolveRuntime']>;
245
245
  private readonly profileDirectory: string;
246
246
  private readonly visible: boolean;
@@ -257,8 +257,8 @@ export class CausyneBrowser {
257
257
  constructor(options: RuntimeOptions = {}) {
258
258
  this.workspace = path.resolve(options.workspace ?? process.cwd());
259
259
  this.ensureBrowserCapability = options.ensureBrowserCapability ?? (() => ensureCapability('browser'));
260
- this.loadCamoufox = options.loadCamoufox ?? (async () => await loadCapabilityPackage('camoufox-js') as CamoufoxModule);
261
- this.resolveRuntime = options.resolveRuntime ?? resolveCamoufoxRuntime;
260
+ this.loadBrowserEngine = options.loadBrowserEngine ?? (async () => await loadCapabilityPackage('camoufox-js') as BrowserEngineModule);
261
+ this.resolveRuntime = options.resolveRuntime ?? resolveBrowserRuntime;
262
262
  this.profileDirectory = path.resolve(options.profileDirectory ?? path.join(capabilityDirectory(), 'browser-profile'));
263
263
  this.visible = options.visible ?? process.env.CAUSYNE_BROWSER_VISIBLE === '1';
264
264
  this.validateUrl = options.validateUrl ?? assertPublicWebUrl;
@@ -340,8 +340,8 @@ export class CausyneBrowser {
340
340
  const runtime = this.resolveRuntime();
341
341
  process.env.CAMOUFOX_INSTALL_DIR = runtime.installDirectory;
342
342
  await fs.mkdir(this.profileDirectory, { recursive: true, mode: 0o700 });
343
- const module = await this.loadCamoufox();
344
- if (!module || typeof module.Camoufox !== 'function') throw new Error('The Camoufox capability has no usable launcher');
343
+ const module = await this.loadBrowserEngine();
344
+ if (!module || typeof module.Camoufox !== 'function') throw new Error('Causyne Browser has no usable launcher');
345
345
  this.session = await module.Camoufox({
346
346
  headless: !this.visible,
347
347
  user_data_dir: this.profileDirectory,
package/src/cliMode.js CHANGED
@@ -12,7 +12,7 @@ import { getPackageVersion } from './version.js';
12
12
  import { resolveCharPool } from './contextManager.js';
13
13
  import { PRIMARY_ACTIONS, homeStatusLine, homeHint, moreLayer, configureInPrompt, mcpPicker, PROVIDERS } from './tui/home.js';
14
14
  import { SelectLayer } from './tui/dialogs.js';
15
- import { askSelect, askText, askConfirm } from './tui/ask.js';
15
+ import { askSelect, askText, askConfirm, askDecision } from './tui/ask.js';
16
16
  import { renderReport } from './reporting.js';
17
17
  import { grantAlways } from './approvalGrants.js';
18
18
  import { auditLog } from './auditLog.js';
@@ -151,6 +151,15 @@ function makeApprovalHandler(conn, dialogs = null) {
151
151
  };
152
152
  }
153
153
 
154
+ /** Keep implementation choices distinct from tool permission decisions. */
155
+ function makeDecisionHandler(conn, dialogs = null) {
156
+ return async (request) => {
157
+ conn.stopSpinner();
158
+ if (dialogs && typeof dialogs.decide === 'function') return await dialogs.decide(request);
159
+ return await askDecision(request);
160
+ };
161
+ }
162
+
154
163
  /** Muted one-liner for menu feedback that is not success and not an error. */
155
164
  function dimNotice(text) { return paint('muted', text); }
156
165
 
@@ -263,6 +272,7 @@ export async function runTask(task) {
263
272
 
264
273
  // Local human-in-the-loop, showing WHAT is being approved (see L6).
265
274
  loop.waitForApproval = makeApprovalHandler(conn);
275
+ loop.waitForDecision = makeDecisionHandler(conn);
266
276
 
267
277
  await loop.run();
268
278
  console.log('\n\x1b[32m[DONE]\x1b[0m Task completed.\n');
@@ -322,6 +332,7 @@ export async function chatMode({ home = false } = {}) {
322
332
  });
323
333
 
324
334
  loop.waitForApproval = makeApprovalHandler(conn);
335
+ loop.waitForDecision = makeDecisionHandler(conn);
325
336
 
326
337
  // The prompt owns the keyboard so the chat can offer history, completion and
327
338
  // typing while a turn runs. It is skipped when stdin is not a terminal (piped
@@ -378,6 +389,7 @@ export async function chatMode({ home = false } = {}) {
378
389
  // prompt. The panel shows the risk, the arguments and a scrollable diff in
379
390
  // one frame, and dismissing it denies.
380
391
  loop.waitForApproval = makeApprovalHandler(conn, prompt);
392
+ loop.waitForDecision = makeDecisionHandler(conn, prompt);
381
393
  // From here the modal asks the question, so the renderer must stop
382
394
  // printing the full request into the transcript as well — see
383
395
  // ChatRenderer.onApprovalRequest().
@@ -0,0 +1,68 @@
1
+ const ARCHITECTURE_TERMS = [
2
+ 'architecture', 'arsitektur',
3
+ 'framework', 'runtime', 'database', 'storage', 'authentication', 'authorization',
4
+ 'deployment', 'hosting', 'protocol', 'plugin system', 'sandbox', 'license',
5
+ 'typescript', 'javascript', 'python', 'rust', 'kotlin', 'swift'
6
+ ];
7
+ const CHANGE_TERMS = [
8
+ 'change', 'replace', 'switch', 'choose', 'decide', 'redesign', 'restructure',
9
+ 'rewrite', 'rebuild', 'migration', 'migrate',
10
+ 'ubah', 'ganti', 'pilih', 'tentukan', 'rombak', 'beralih', 'migrasi'
11
+ ];
12
+ const EXPLICIT_CHOICE_PATTERNS = [
13
+ /\b(?:i|we)\s+(?:choose|chose|selected?|decided? (?:on|to use))\s+[a-z0-9@._+-]+/i,
14
+ /\b(?:must use|will use|switch to|rewrite (?:to|in)|migrate to)\s+[a-z0-9@._+-]+/i,
15
+ /\b(?:saya pilih|aku pilih|kita pilih|harus memakai|beralih ke|ubah ke|ganti ke)\s+[a-z0-9@._+-]+/i,
16
+ /^(?:please\s+)?(?:use|choose|gunakan|pilih)\s+[a-z0-9@._+-]+/i,
17
+ /\b(?:do not|don't|jangan)\s+(?:use|gunakan|pakai)\s+[a-z0-9@._+-]+/i
18
+ ];
19
+ const MUTATING_TOOLS = new Set([
20
+ 'fs_write', 'fs_edit', 'MultiEdit', 'fs_delete', 'fs_move', 'sysRun', 'sysRunBg',
21
+ 'gitOp', 'manage_local_dependencies', 'plugin_manage', 'tool_forge', 'create_skill'
22
+ ]);
23
+ function containsAny(text, terms) {
24
+ return terms.some(term => text.includes(term));
25
+ }
26
+ export function assessDecisionRequirement(prompt) {
27
+ const raw = String(prompt ?? '').trim();
28
+ const text = raw.toLowerCase();
29
+ const architectural = containsAny(text, ARCHITECTURE_TERMS);
30
+ const change = containsAny(text, CHANGE_TERMS);
31
+ const alternatives = /\b(?:or|versus|vs\.?|atau|dibanding(?:kan)? dengan)\b/i.test(raw);
32
+ const asksForChoice = /\b(?:which|what should|should we|mana|apakah sebaiknya|bagaimana sebaiknya)\b/i.test(raw);
33
+ const delegatesBroadRedesign = /\b(?:redesign|restructure|rombak)\s+(?:the\s+)?(?:architecture|system|runtime|database|arsitektur|sistem)\b/i.test(raw);
34
+ const explicit = EXPLICIT_CHOICE_PATTERNS.some(pattern => pattern.test(raw));
35
+ if (!architectural || explicit || (!alternatives && !asksForChoice && !(change && delegatesBroadRedesign))) {
36
+ return { required: false, kind: 'architecture', reason: '' };
37
+ }
38
+ return {
39
+ required: true,
40
+ kind: 'architecture',
41
+ reason: 'The request reaches a durable architecture boundary with more than one plausible implementation.'
42
+ };
43
+ }
44
+ export function requiresImplementationPlan(prompt, archetype = '', confidence = '') {
45
+ const text = String(prompt ?? '').toLowerCase();
46
+ if (archetype === 'build_app' || archetype === 'integrate')
47
+ return true;
48
+ if (confidence === 'high' && /\b(?:implement|build|create|migrate|rewrite|refactor|integrate|buat|bangun|ubah)\b/i.test(text)) {
49
+ return true;
50
+ }
51
+ return containsAny(text, ARCHITECTURE_TERMS) && containsAny(text, CHANGE_TERMS);
52
+ }
53
+ export function isMutatingToolCall(toolName, args = {}) {
54
+ if (MUTATING_TOOLS.has(toolName)) {
55
+ if (toolName === 'sysRun' || toolName === 'sysRunBg') {
56
+ const command = String(args.command ?? '');
57
+ return !/^\s*(?:pwd|ls|find|rg|grep|cat|sed|head|tail|git\s+(?:status|diff|log|show)|npm\s+(?:view|audit|test)|node\s+--check)\b/i.test(command);
58
+ }
59
+ if (toolName === 'gitOp') {
60
+ const action = String(args.action ?? args.command ?? args.operation ?? '');
61
+ return !/^(?:status|diff|log|show|branch|remote)$/i.test(action.trim());
62
+ }
63
+ return true;
64
+ }
65
+ if (toolName.startsWith('plugin__') || toolName.startsWith('mcp__'))
66
+ return false;
67
+ return false;
68
+ }