@awak-app/simy-cli 0.2.1 → 0.2.2

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 CHANGED
@@ -36,6 +36,14 @@ runs. It builds the requirement charter, runs Codex or Claude Code, audits
36
36
  structured completion evidence, and re-instructs the executor within the
37
37
  configured attempt budget.
38
38
 
39
+ Before Web creates an Agentic Loop, it classifies whether the request actually
40
+ needs a managed PR, verification, evidence, and merge lifecycle. A bounded
41
+ ordinary task is sent to CLI 0.2.2 or newer through the one-time direct executor
42
+ instead. CLI independently recomputes the guardrail, restricts read-only work at
43
+ the Provider command boundary, and permits exactly one Provider invocation. It
44
+ does not create an Agentic Loop ledger, audit session, or retry lifecycle for
45
+ that task.
46
+
39
47
  ## CLI updates
40
48
 
41
49
  The CLI checks the npm registry at startup and once per hour while it is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Local SIMY Agentic Loop executor for Codex and Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,12 +25,13 @@
25
25
  "test:e2e:hypothesis-retries": "node ./scripts/hypothesis-retries-e2e.js",
26
26
  "test:e2e:budgets": "node ./scripts/budget-enforcement-e2e.js",
27
27
  "test:e2e:desktop-executor": "node ./scripts/desktop-executor-e2e.js",
28
+ "test:e2e:pre-guardrail": "node ./scripts/pre-guardrail-e2e.js",
28
29
  "test:e2e:console:real-provider": "node ./scripts/real-provider-console-e2e.js",
29
30
  "test:e2e:console:fullstack": "node ./scripts/fullstack-cli-smoke.js",
30
- "check": "npm run check:auto-update && node --check ./src/desktop-executor.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/fixtures/fake-desktop-provider.js && npm run check:syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
31
+ "check": "npm run check:auto-update && node --check ./src/desktop-executor.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/pre-guardrail-e2e.js && node --check ./scripts/fixtures/fake-desktop-provider.js && npm run check:syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
31
32
  "check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js",
32
33
  "check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js",
33
- "check:syntax": "node --check ./src/index.js && node --check ./src/agent.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/run-registry.js && node --check ./src/backend-executable.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js",
34
+ "check:syntax": "node --check ./src/index.js && node --check ./src/agent.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js",
34
35
  "check:package": "node ./scripts/check-package-contents.js"
35
36
  },
36
37
  "engines": {
package/src/agent.js CHANGED
@@ -70,6 +70,16 @@ import {
70
70
  CLI_VERSION,
71
71
  withCliContract,
72
72
  } from "./cli-contract.js";
73
+ import {
74
+ createDirectTask,
75
+ directTaskSnapshot,
76
+ DirectTaskRegistry,
77
+ startDirectTask,
78
+ } from "./direct-executor.js";
79
+ import {
80
+ classifyExecutionRequirement,
81
+ EXECUTION_GUARDRAIL_POLICY_VERSION,
82
+ } from "./execution-guardrail.js";
73
83
 
74
84
  const DEVICE_HEARTBEAT_INTERVAL_MS = 20_000;
75
85
 
@@ -84,6 +94,7 @@ export async function startAgent({
84
94
  } = {}) {
85
95
  const apiOrigin = resolveWebOrigin(webOrigin);
86
96
  const registry = new LocalRunRegistry();
97
+ const directRegistry = new DirectTaskRegistry();
87
98
  const authNonce = randomBytes(16).toString("base64url");
88
99
  let session = await readSession(apiOrigin, sessionRoot);
89
100
  if (session && sessionRequiresWebAuthorization(session, apiOrigin)) session = null;
@@ -377,6 +388,112 @@ export async function startAgent({
377
388
  void synchronizeAuthorizedSession();
378
389
  return;
379
390
  }
391
+ const directExecutionMatch = url.pathname.match(/^\/v1\/direct-execution\/([^/]+)$/);
392
+ if (req.method === "GET" && directExecutionMatch) {
393
+ if (!isSessionValid(session, Date.now(), apiOrigin)) {
394
+ json(res, 401, { error: "simy session expired; run simy again" });
395
+ return;
396
+ }
397
+ const task = directRegistry.get(decodeURIComponent(directExecutionMatch[1]));
398
+ if (!task) {
399
+ json(res, 404, { error: "direct task not found" });
400
+ return;
401
+ }
402
+ json(res, 200, { task: directTaskSnapshot(task) });
403
+ return;
404
+ }
405
+ if (req.method === "POST" && url.pathname === "/v1/direct-execution/start") {
406
+ if (!acceptsNewWork(updateManager)) {
407
+ json(res, 503, updateInProgressResponse(updateManager));
408
+ return;
409
+ }
410
+ if (!isSessionValid(session, Date.now(), apiOrigin)) {
411
+ json(res, 401, { error: "simy session expired; run simy again" });
412
+ return;
413
+ }
414
+ const body = await readJson(req);
415
+ const instruction = String(body.instruction || "").trim();
416
+ if (!instruction) {
417
+ json(res, 400, { error: "instruction is required" });
418
+ return;
419
+ }
420
+ if (body.backend !== "codex" && body.backend !== "claude") {
421
+ json(res, 400, { error: "backend must be codex or claude" });
422
+ return;
423
+ }
424
+ const guardrail = classifyExecutionRequirement(instruction);
425
+ if (
426
+ guardrail.decision !== "direct_executor" ||
427
+ body.guardrail?.policy_version !== EXECUTION_GUARDRAIL_POLICY_VERSION ||
428
+ body.guardrail?.decision !== "direct_executor"
429
+ ) {
430
+ json(res, 409, {
431
+ error: "This request is not authorized for one-time direct execution.",
432
+ code: "DIRECT_EXECUTION_GUARD_REQUIRED",
433
+ guardrail,
434
+ });
435
+ return;
436
+ }
437
+ if (
438
+ body.operation !== guardrail.operation ||
439
+ body.permission_mode !== guardrail.permission_mode ||
440
+ body.max_provider_invocations !== 1
441
+ ) {
442
+ json(res, 409, {
443
+ error: "Direct execution permissions do not match the pre-guardrail decision.",
444
+ code: "DIRECT_EXECUTION_GUARD_MISMATCH",
445
+ guardrail,
446
+ });
447
+ return;
448
+ }
449
+ const inspection = normalizeBackendInspection(body.backend, availableCapabilities);
450
+ if (!inspection.compatible) {
451
+ const summary = backendPreflightSummary(body.backend, inspection);
452
+ json(res, 409, {
453
+ error: summary,
454
+ code: `desktop_executor_${inspection.status}`,
455
+ });
456
+ return;
457
+ }
458
+ const repository = String(body.repository || "").trim() || null;
459
+ if (guardrail.repository_required && !repository) {
460
+ json(res, 400, { error: "repository is required for this direct task" });
461
+ return;
462
+ }
463
+ const indexedRepository = repository ? await ensureRepositoryIndexed(repository) : null;
464
+ if (repository && !indexedRepository?.local_path) {
465
+ json(res, 409, {
466
+ error: `${repository} is not authorized on this SIMY CLI.`,
467
+ code: "repository_not_authorized",
468
+ });
469
+ return;
470
+ }
471
+ const repositoryPath = indexedRepository?.local_path || workspace.localPath;
472
+ if (!repositoryPath) {
473
+ json(res, 409, {
474
+ error: "No local workspace is available for this direct task.",
475
+ code: "local_workspace_unavailable",
476
+ });
477
+ return;
478
+ }
479
+ const task = createDirectTask({
480
+ instruction,
481
+ backend: body.backend,
482
+ operation: guardrail.operation,
483
+ permissionMode: guardrail.permission_mode,
484
+ repository,
485
+ repositoryPath,
486
+ });
487
+ directRegistry.create(task);
488
+ const runDirectTask = dependencies.runDirectTask || startDirectTask;
489
+ void Promise.resolve(runDirectTask(task)).catch((error) => {
490
+ task.status = "failed";
491
+ task.error = error instanceof Error ? error.message : String(error);
492
+ task.updated_at = new Date().toISOString();
493
+ });
494
+ json(res, 202, { ok: true, task: directTaskSnapshot(task) });
495
+ return;
496
+ }
380
497
  if (req.method === "POST" && agenticLoopPath === "/v1/agentic-loop/start") {
381
498
  if (!acceptsNewWork(updateManager)) {
382
499
  json(res, 503, updateInProgressResponse(updateManager));
@@ -387,6 +504,22 @@ export async function startAgent({
387
504
  return;
388
505
  }
389
506
  const { body, attachments } = await readCodingLoopStart(req);
507
+ const guardrailSource = String(body.source_message || "").trim();
508
+ if (guardrailSource) {
509
+ const guardrail = classifyExecutionRequirement(guardrailSource);
510
+ if (
511
+ guardrail.decision !== "agentic_loop" ||
512
+ body.guardrail?.policy_version !== EXECUTION_GUARDRAIL_POLICY_VERSION ||
513
+ body.guardrail?.decision !== "agentic_loop"
514
+ ) {
515
+ json(res, 409, {
516
+ error: "This task does not qualify for an Agentic Loop.",
517
+ code: "AGENTIC_LOOP_NOT_REQUIRED",
518
+ guardrail,
519
+ });
520
+ return;
521
+ }
522
+ }
390
523
  const verified = await verifyLaunchChallenge({
391
524
  apiOrigin,
392
525
  apiBaseUrl: session.api_base_url,
@@ -791,6 +924,7 @@ export async function startAgent({
791
924
  server,
792
925
  port,
793
926
  registry,
927
+ directRegistry,
794
928
  webOrigin: apiOrigin,
795
929
  get loginUrl() {
796
930
  return loginUrl?.toString() ?? null;
@@ -1077,6 +1211,7 @@ async function capabilities() {
1077
1211
  repository_scan_approval: true,
1078
1212
  executor_version_preflight: true,
1079
1213
  desktop_executor: true,
1214
+ direct_execution: true,
1080
1215
  },
1081
1216
  session_ttl_hours: 48,
1082
1217
  };
@@ -1,4 +1,4 @@
1
- export const CLI_VERSION = "0.2.1";
1
+ export const CLI_VERSION = "0.2.2";
2
2
  export const CLI_API_CONTRACT_VERSION = 2;
3
3
 
4
4
  export function withCliContract(capabilities = {}, installMode = null) {
@@ -49,11 +49,18 @@ export async function resolveDesktopExecutorCommand({
49
49
  backend,
50
50
  instruction,
51
51
  repositoryPath,
52
+ executionKind = "agentic_loop",
53
+ permissionMode = "workspace_write",
52
54
  environment = process.env,
53
55
  spawnImpl = spawn,
54
56
  inspectionOptions = {},
55
57
  } = {}) {
56
58
  const provider = normalizeBackend(backend);
59
+ const directExecution = executionKind === "direct";
60
+ if (executionKind !== "agentic_loop" && !directExecution) {
61
+ throw new Error("Desktop execution kind must be agentic_loop or direct.");
62
+ }
63
+ const normalizedPermissionMode = normalizePermissionMode(permissionMode);
57
64
  const target = normalizeDesktopExecutionTarget(DESKTOP_EXECUTION_TARGET);
58
65
  const override =
59
66
  provider === "claude" ? environment.SIMY_CLAUDE_COMMAND : environment.SIMY_CODEX_COMMAND;
@@ -62,6 +69,8 @@ export async function resolveDesktopExecutorCommand({
62
69
  ...shellCommand(override, repositoryPath, instruction, spawnImpl),
63
70
  backend: provider,
64
71
  execution_target: target,
72
+ execution_kind: executionKind,
73
+ permission_mode: normalizedPermissionMode,
65
74
  verification: "explicit_command_override",
66
75
  };
67
76
  }
@@ -77,12 +86,24 @@ export async function resolveDesktopExecutorCommand({
77
86
  bin: inspection.executable,
78
87
  args:
79
88
  provider === "claude"
80
- ? claudeBackendArgs(instruction)
81
- : ["exec", "--json", instruction],
89
+ ? directExecution
90
+ ? claudeDirectBackendArgs(instruction, normalizedPermissionMode)
91
+ : claudeBackendArgs(instruction)
92
+ : directExecution
93
+ ? [
94
+ "exec",
95
+ "--json",
96
+ "--sandbox",
97
+ normalizedPermissionMode === "read_only" ? "read-only" : "workspace-write",
98
+ instruction,
99
+ ]
100
+ : ["exec", "--json", instruction],
82
101
  env: {},
83
102
  spawn: spawnImpl,
84
103
  backend: provider,
85
104
  execution_target: target,
105
+ execution_kind: executionKind,
106
+ permission_mode: normalizedPermissionMode,
86
107
  verification: "compatible_version",
87
108
  installed_version: inspection.installed_version,
88
109
  };
@@ -102,11 +123,30 @@ export function claudeBackendArgs(instruction) {
102
123
  ];
103
124
  }
104
125
 
126
+ export function claudeDirectBackendArgs(instruction, permissionMode = "read_only") {
127
+ const normalized = normalizePermissionMode(permissionMode);
128
+ return [
129
+ "-p",
130
+ instruction,
131
+ "--output-format",
132
+ "stream-json",
133
+ "--verbose",
134
+ ...(normalized === "read_only"
135
+ ? ["--permission-mode", "plan"]
136
+ : ["--dangerously-skip-permissions"]),
137
+ ];
138
+ }
139
+
105
140
  function normalizeBackend(backend) {
106
141
  if (backend === "codex" || backend === "claude") return backend;
107
142
  throw new Error("Desktop executor backend must be codex or claude.");
108
143
  }
109
144
 
145
+ function normalizePermissionMode(value) {
146
+ if (value === "read_only" || value === "workspace_write") return value;
147
+ throw new Error("Desktop permission mode must be read_only or workspace_write.");
148
+ }
149
+
110
150
  function desktopExecutorUnavailableError(inspection) {
111
151
  const provider = inspection.provider_label;
112
152
  let message;
@@ -131,6 +171,7 @@ function shellCommand(command, cwd, instruction, spawnImpl) {
131
171
  env: {
132
172
  SIMY_AGENTIC_LOOP_REQUIREMENT: instruction,
133
173
  SIMY_CODING_LOOP_REQUIREMENT: instruction,
174
+ SIMY_DIRECT_EXECUTION_INSTRUCTION: instruction,
134
175
  },
135
176
  spawn: spawnImpl,
136
177
  };
@@ -0,0 +1,142 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import { resolveDesktopExecutorCommand } from "./desktop-executor.js";
4
+ import { createProviderStreamDecoder } from "./provider-stream.js";
5
+
6
+ const MAX_OUTPUT_LINES = 400;
7
+
8
+ export class DirectTaskRegistry {
9
+ #tasks = new Map();
10
+
11
+ create(task) {
12
+ if (this.#tasks.has(task.id)) throw new Error(`Direct task ${task.id} already exists.`);
13
+ this.#tasks.set(task.id, task);
14
+ return task;
15
+ }
16
+
17
+ get(taskId) {
18
+ return this.#tasks.get(taskId) ?? null;
19
+ }
20
+ }
21
+
22
+ export function createDirectTask({
23
+ instruction,
24
+ backend,
25
+ operation,
26
+ permissionMode,
27
+ repository = null,
28
+ repositoryPath,
29
+ }) {
30
+ const now = new Date().toISOString();
31
+ return {
32
+ id: `direct_task_${randomUUID()}`,
33
+ status: "queued",
34
+ instruction,
35
+ backend,
36
+ operation,
37
+ permission_mode: permissionMode,
38
+ repository,
39
+ repository_path: repositoryPath,
40
+ invocation_count: 0,
41
+ output: [],
42
+ result: null,
43
+ error: null,
44
+ token_usage: { records: [] },
45
+ created_at: now,
46
+ updated_at: now,
47
+ child: null,
48
+ };
49
+ }
50
+
51
+ export async function startDirectTask(
52
+ task,
53
+ { resolveCommand = resolveDesktopExecutorCommand, environment = process.env } = {},
54
+ ) {
55
+ if (!task || task.status !== "queued") throw new Error("Direct task must be queued.");
56
+ if (task.invocation_count >= 1) throw new Error("Direct tasks allow only one Provider invocation.");
57
+ const command = await resolveCommand({
58
+ backend: task.backend,
59
+ instruction: task.instruction,
60
+ repositoryPath: task.repository_path,
61
+ executionKind: "direct",
62
+ permissionMode: task.permission_mode,
63
+ environment,
64
+ });
65
+ task.status = "running";
66
+ task.invocation_count = 1;
67
+ task.updated_at = new Date().toISOString();
68
+
69
+ return new Promise((resolve) => {
70
+ let settled = false;
71
+ const rawStdout = [];
72
+ const tokenRecords = [];
73
+ const child = command.spawn(command.bin, command.args, {
74
+ cwd: task.repository_path,
75
+ env: { ...process.env, ...command.env },
76
+ stdio: ["ignore", "pipe", "pipe"],
77
+ detached: process.platform !== "win32",
78
+ });
79
+ task.child = child;
80
+ const onLine = (line) => {
81
+ const normalized = String(line || "").trim();
82
+ if (!normalized) return;
83
+ task.output.push(normalized);
84
+ if (task.output.length > MAX_OUTPUT_LINES) {
85
+ task.output.splice(0, task.output.length - MAX_OUTPUT_LINES);
86
+ }
87
+ task.updated_at = new Date().toISOString();
88
+ };
89
+ const stdoutDecoder = createProviderStreamDecoder({
90
+ backend: task.backend,
91
+ stream: "stdout",
92
+ onLine,
93
+ onUsage: (usage) => tokenRecords.push({ ...usage, phase: "direct", backend: task.backend }),
94
+ });
95
+ const stderrDecoder = createProviderStreamDecoder({
96
+ backend: task.backend,
97
+ stream: "stderr",
98
+ onLine,
99
+ });
100
+ child.stdout?.on("data", (chunk) => {
101
+ const text = chunk.toString("utf8");
102
+ rawStdout.push(text);
103
+ stdoutDecoder.push(text);
104
+ });
105
+ child.stderr?.on("data", (chunk) => stderrDecoder.push(chunk.toString("utf8")));
106
+
107
+ const finish = ({ exitCode = null, error = null } = {}) => {
108
+ if (settled) return;
109
+ settled = true;
110
+ stdoutDecoder.flush();
111
+ stderrDecoder.flush();
112
+ task.child = null;
113
+ task.token_usage = { records: tokenRecords };
114
+ task.status = error || exitCode !== 0 ? "failed" : "succeeded";
115
+ task.error = error || (exitCode !== 0 ? `Provider exited with code ${exitCode}.` : null);
116
+ task.result = task.output.at(-1) || rawStdout.join("").trim() || null;
117
+ task.updated_at = new Date().toISOString();
118
+ resolve(task);
119
+ };
120
+ child.on("error", (error) => finish({ error: error.message }));
121
+ child.on("close", (code) => finish({ exitCode: code }));
122
+ });
123
+ }
124
+
125
+ export function directTaskSnapshot(task) {
126
+ return {
127
+ id: task.id,
128
+ status: task.status,
129
+ instruction: task.instruction,
130
+ backend: task.backend,
131
+ operation: task.operation,
132
+ permission_mode: task.permission_mode,
133
+ repository: task.repository,
134
+ invocation_count: task.invocation_count,
135
+ output: [...task.output],
136
+ result: task.result,
137
+ error: task.error,
138
+ token_usage: structuredClone(task.token_usage),
139
+ created_at: task.created_at,
140
+ updated_at: task.updated_at,
141
+ };
142
+ }
@@ -0,0 +1,120 @@
1
+ export const EXECUTION_GUARDRAIL_POLICY_VERSION = "2026-07-21.1";
2
+
3
+ const OBSERVE_PATTERN =
4
+ /\b(monitor|watch|observe|track|status|progress|keep an eye on)\b|監視|見守|進捗|状況を?(?:見|確認)|监控|監控|观察|觀察|进度|進度|状态|狀態/i;
5
+ const READ_PATTERN =
6
+ /\b(explain|summari[sz]e|inspect|review|look at|read|check|diagnose|investigate)\b|説明|要約|確認|調査|レビュー|查看|解释|說明|总结|總結|检查|檢查|调查|調查|诊断|診斷/i;
7
+ const CHANGE_PATTERN =
8
+ /\b(implement|fix|build|create|change|update|refactor|add|remove|rename|ship|write|edit|merge|release|publish)\b|実装|修正|改善|変更|追加|削除|作成|開発|マージ|公開|发布|發佈|实现|實現|修复|修復|修改|改掉|改一下|新增|删除|刪除|重构|重構|合并|合併/i;
9
+ const OBJECT_PATTERN =
10
+ /\b(code|file|readme|api|ui|ux|feature|bug|pr|pull request|repository|repo|branch|test|migration|endpoint|component|page|cli|backend|extension)\b|コード|ファイル|機能|不具合|画面|ページ|テスト|移行|组件|組件|页面|頁面|代码|代碼|文件|功能|错误|錯誤|仓库|倉庫|分支/i;
11
+ const VAGUE_PATTERN = /^(?:fix|do|handle|continue|change)?\s*(?:it|this|that)?[.!。!]?$/i;
12
+ const VAGUE_CJK_PATTERN = /^(?:これ|それ|あれ|やって|直して|続けて|这个|這個|那个|那個|处理|處理|修一下)[。!]?$/i;
13
+ const UNSUPPORTED_PATTERN =
14
+ /\b(?:steal|exfiltrate|bypass authentication|disable audit)\b|認証を迂回|監査を無効|窃取凭证|繞過認證/i;
15
+
16
+ const MANAGED_SIGNAL_PATTERNS = [
17
+ ["pr_merge_release", /\b(?:pull request|pr|merge|release|publish|deploy)\b|PR|マージ|リリース|デプロイ|合并|合併|发布|發佈|部署/i],
18
+ ["verification", /\b(?:test|e2e|integration test|until green|ci)\b|テスト|検証|通过测试|通過測試|直到.*通过|直到.*通過/i],
19
+ ["browser_evidence", /\b(?:browser|screenshots?|visual evidence|evidence)\b|ブラウザ|スクリーンショット|証跡|截图|截圖|浏览器|瀏覽器|证据|證據/i],
20
+ ["cross_repository", /\b(?:cross[- ]repo|multiple repos|multi[- ]repo)\b|複数(?:の)?リポジトリ|跨仓库|跨倉庫|多个仓库|多個倉庫/i],
21
+ ["high_risk_change", /\b(?:migration|authentication|authorization|security|rollback)\b|移行|認証|認可|セキュリティ|ロールバック|迁移|遷移|认证|認證|授权|授權|安全|回滚|回滾/i],
22
+ ["diagnose_repair_reverify", /\bdiagnos\w*\b[\s\S]*\bfix\w*\b[\s\S]*\b(?:verify|retest)\b|調査[\s\S]*修正[\s\S]*再検証|诊断[\s\S]*修复[\s\S]*复验|診斷[\s\S]*修復[\s\S]*複驗/i],
23
+ ];
24
+
25
+ export function classifyExecutionRequirement(sourceMessage) {
26
+ const source = String(sourceMessage || "")
27
+ .replace(/^\s*\/(?:agentic-loop|coding-loop)\b\s*/i, "")
28
+ .trim();
29
+ if (!source || VAGUE_PATTERN.test(source) || VAGUE_CJK_PATTERN.test(source)) {
30
+ const vagueChangeRequested = CHANGE_PATTERN.test(source);
31
+ return guardrail("needs_clarification", {
32
+ change_requested: vagueChangeRequested,
33
+ operation: vagueChangeRequested ? "execute" : "read",
34
+ permission_mode: vagueChangeRequested ? "workspace_write" : "read_only",
35
+ reason_code: "goal_not_clear",
36
+ });
37
+ }
38
+ if (UNSUPPORTED_PATTERN.test(source)) {
39
+ return guardrail("unsupported", {
40
+ operation: "execute",
41
+ permission_mode: "workspace_write",
42
+ reason_code: "prohibited_operation",
43
+ });
44
+ }
45
+ const changeRequested = CHANGE_PATTERN.test(source);
46
+ const observeRequested = OBSERVE_PATTERN.test(source);
47
+ const readRequested = READ_PATTERN.test(source);
48
+ const hasObject = OBJECT_PATTERN.test(source) || source.length >= 24;
49
+ const agenticSignals = MANAGED_SIGNAL_PATTERNS.filter(([, pattern]) => pattern.test(source)).map(
50
+ ([signal]) => signal,
51
+ );
52
+ if (changeRequested && hasObject && agenticSignals.length >= 2) {
53
+ return guardrail("agentic_loop", {
54
+ change_requested: true,
55
+ managed_lifecycle_required: true,
56
+ repository_required: true,
57
+ operation: "execute",
58
+ permission_mode: "workspace_write",
59
+ agentic_signals: agenticSignals,
60
+ reason_code: "managed_change_lifecycle_required",
61
+ });
62
+ }
63
+ if (observeRequested && !changeRequested) {
64
+ return guardrail("direct_executor", {
65
+ repository_required: /\b(?:repo|repository|pr|branch)\b|リポジトリ|仓库|倉庫|分支/i.test(source),
66
+ operation: "observe",
67
+ permission_mode: "read_only",
68
+ direct_signals: ["bounded_observation", "no_persistent_change"],
69
+ reason_code: "observe_existing_provider_session",
70
+ });
71
+ }
72
+ if (changeRequested && hasObject) {
73
+ return guardrail("direct_executor", {
74
+ change_requested: true,
75
+ repository_required: true,
76
+ operation: "execute",
77
+ permission_mode: "workspace_write",
78
+ agentic_signals: agenticSignals,
79
+ direct_signals: ["single_bounded_change", "managed_lifecycle_not_required"],
80
+ reason_code: "bounded_change_direct_execution",
81
+ });
82
+ }
83
+ if (changeRequested) {
84
+ return guardrail("needs_clarification", {
85
+ change_requested: true,
86
+ operation: "execute",
87
+ permission_mode: "workspace_write",
88
+ reason_code: "change_target_missing",
89
+ });
90
+ }
91
+ if (readRequested || source.length >= 8) {
92
+ return guardrail("direct_executor", {
93
+ repository_required: /\b(?:repo|repository|pr|file|code)\b|コード|ファイル|仓库|倉庫|文件|代码|代碼/i.test(source),
94
+ operation: "read",
95
+ permission_mode: "read_only",
96
+ direct_signals: ["single_read_task", "no_managed_lifecycle"],
97
+ reason_code: "bounded_read_direct_execution",
98
+ });
99
+ }
100
+ return guardrail("needs_clarification", {
101
+ operation: "read",
102
+ permission_mode: "read_only",
103
+ reason_code: "action_missing",
104
+ });
105
+ }
106
+
107
+ function guardrail(decision, overrides) {
108
+ return {
109
+ policy_version: EXECUTION_GUARDRAIL_POLICY_VERSION,
110
+ decision,
111
+ change_requested: false,
112
+ managed_lifecycle_required: false,
113
+ repository_required: false,
114
+ operation: "read",
115
+ permission_mode: "read_only",
116
+ agentic_signals: [],
117
+ direct_signals: [],
118
+ ...overrides,
119
+ };
120
+ }