@awak-app/simy-cli 0.2.1 → 0.2.3
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 +8 -0
- package/package.json +5 -3
- package/src/agent.js +135 -0
- package/src/cli-contract.js +2 -2
- package/src/desktop-executor.js +43 -2
- package/src/direct-executor.js +142 -0
- package/src/execution-guardrail.js +120 -0
- package/src/orchestrator/audit.js +90 -2
- package/src/orchestrator/completion-contract.js +159 -0
- package/src/orchestrator/contract.js +23 -12
- package/src/orchestrator/independent-audit.js +88 -3
- package/src/orchestrator/instruction.js +26 -4
- package/src/orchestrator/loop.js +5 -1
- package/src/orchestrator/result.js +10 -0
- package/src/runner.js +1 -0
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.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Local SIMY Agentic Loop executor for Codex and Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,12 +25,14 @@
|
|
|
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",
|
|
29
|
+
"test:e2e:completion-contract": "node ./scripts/completion-contract-e2e.js",
|
|
28
30
|
"test:e2e:console:real-provider": "node ./scripts/real-provider-console-e2e.js",
|
|
29
31
|
"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",
|
|
32
|
+
"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
33
|
"check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js",
|
|
32
34
|
"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",
|
|
35
|
+
"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/completion-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/completion-contract-fixture.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/completion-contract-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
36
|
"check:package": "node ./scripts/check-package-contents.js"
|
|
35
37
|
},
|
|
36
38
|
"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
|
};
|
package/src/cli-contract.js
CHANGED
package/src/desktop-executor.js
CHANGED
|
@@ -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
|
-
?
|
|
81
|
-
|
|
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
|
+
}
|
|
@@ -8,11 +8,23 @@ import {
|
|
|
8
8
|
stringArray,
|
|
9
9
|
} from "./shared.js";
|
|
10
10
|
import { evaluateRetryProblemSolving } from "./problem-solving.js";
|
|
11
|
+
import {
|
|
12
|
+
completionEvidenceChecks,
|
|
13
|
+
normalizeVisualReview,
|
|
14
|
+
} from "./completion-contract.js";
|
|
11
15
|
|
|
12
16
|
export async function auditAttempt(charter, attempt, { previousAttempt = null } = {}) {
|
|
13
17
|
const checks = [];
|
|
14
|
-
const addCheck = (
|
|
15
|
-
|
|
18
|
+
const addCheck = (
|
|
19
|
+
id,
|
|
20
|
+
label,
|
|
21
|
+
passed,
|
|
22
|
+
detail,
|
|
23
|
+
severity = "blocker",
|
|
24
|
+
repairability = "auto",
|
|
25
|
+
evidenceRefs = [],
|
|
26
|
+
) => {
|
|
27
|
+
checks.push({ id, label, passed, severity, detail, repairability, evidence_refs: evidenceRefs });
|
|
16
28
|
};
|
|
17
29
|
const local = attempt.observed_evidence?.local || {};
|
|
18
30
|
|
|
@@ -119,6 +131,18 @@ export async function auditAttempt(charter, attempt, { previousAttempt = null }
|
|
|
119
131
|
}
|
|
120
132
|
attempt.retry_problem_solving = retryProblemSolving;
|
|
121
133
|
|
|
134
|
+
for (const check of completionEvidenceChecks(charter, attempt)) {
|
|
135
|
+
addCheck(
|
|
136
|
+
check.id,
|
|
137
|
+
check.label,
|
|
138
|
+
check.passed,
|
|
139
|
+
check.detail,
|
|
140
|
+
"blocker",
|
|
141
|
+
"auto",
|
|
142
|
+
check.evidence_refs,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
122
146
|
const evidenceRequired = charter.expected_evidence.includes("ui_evidence_path");
|
|
123
147
|
const evidenceArtifact = await inspectEvidence(charter, attempt.ui_evidence_path);
|
|
124
148
|
if (evidenceRequired || attempt.ui_evidence_path) {
|
|
@@ -142,6 +166,32 @@ export async function auditAttempt(charter, attempt, { previousAttempt = null }
|
|
|
142
166
|
? `${evidenceArtifact.visual_file_count} screenshot or video artifact(s) found.`
|
|
143
167
|
: "UI and browser-extension work requires a real screenshot or video; a path, text file, or empty directory is not sufficient.",
|
|
144
168
|
);
|
|
169
|
+
const visualReview = normalizeVisualReview(attempt.visual_review);
|
|
170
|
+
const visualArtifactReview = await inspectVisualReviewArtifacts(charter, visualReview);
|
|
171
|
+
addCheck(
|
|
172
|
+
"visual_content_inspected",
|
|
173
|
+
"Visual evidence content was inspected against acceptance criteria",
|
|
174
|
+
Boolean(
|
|
175
|
+
visualReview.inspected &&
|
|
176
|
+
visualReview.artifacts.length > 0 &&
|
|
177
|
+
visualReview.acceptance_observations.length > 0,
|
|
178
|
+
),
|
|
179
|
+
visualReview.inspected && visualReview.acceptance_observations.length > 0
|
|
180
|
+
? visualReview.acceptance_observations.join("; ")
|
|
181
|
+
: "UI evidence must be opened and inspected; record the artifact paths and concrete acceptance observations.",
|
|
182
|
+
"blocker",
|
|
183
|
+
"auto",
|
|
184
|
+
visualReview.artifacts,
|
|
185
|
+
);
|
|
186
|
+
addCheck(
|
|
187
|
+
"visual_review_artifacts_verified",
|
|
188
|
+
"Claimed visual-review artifacts are real local media",
|
|
189
|
+
visualArtifactReview.valid,
|
|
190
|
+
visualArtifactReview.summary,
|
|
191
|
+
"blocker",
|
|
192
|
+
"auto",
|
|
193
|
+
visualArtifactReview.verified,
|
|
194
|
+
);
|
|
145
195
|
}
|
|
146
196
|
|
|
147
197
|
const findings = checks
|
|
@@ -409,3 +459,41 @@ async function isVisualEvidenceFile(filePath) {
|
|
|
409
459
|
header.subarray(0, 4).equals(Buffer.from([26, 69, 223, 163]))
|
|
410
460
|
);
|
|
411
461
|
}
|
|
462
|
+
|
|
463
|
+
async function inspectVisualReviewArtifacts(charter, visualReview) {
|
|
464
|
+
const root = charter.ui_evidence_root ? path.resolve(charter.ui_evidence_root) : null;
|
|
465
|
+
const verified = [];
|
|
466
|
+
const invalid = [];
|
|
467
|
+
for (const artifact of visualReview.artifacts) {
|
|
468
|
+
const resolved = path.resolve(artifact);
|
|
469
|
+
const underRoot = Boolean(
|
|
470
|
+
root && (resolved === root || resolved.startsWith(`${root}${path.sep}`)),
|
|
471
|
+
);
|
|
472
|
+
let fileStat = null;
|
|
473
|
+
try {
|
|
474
|
+
fileStat = await stat(resolved);
|
|
475
|
+
} catch {
|
|
476
|
+
// The invalid reason below covers missing files.
|
|
477
|
+
}
|
|
478
|
+
if (
|
|
479
|
+
!underRoot ||
|
|
480
|
+
!fileStat?.isFile() ||
|
|
481
|
+
fileStat.size <= 0 ||
|
|
482
|
+
!(await isVisualEvidenceFile(resolved))
|
|
483
|
+
) {
|
|
484
|
+
invalid.push(artifact);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
verified.push(resolved);
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
valid: visualReview.artifacts.length > 0 && invalid.length === 0,
|
|
491
|
+
verified,
|
|
492
|
+
summary:
|
|
493
|
+
visualReview.artifacts.length === 0
|
|
494
|
+
? "No inspected visual artifact paths were recorded."
|
|
495
|
+
: invalid.length > 0
|
|
496
|
+
? `Missing, non-visual, or out-of-root artifacts: ${invalid.join(", ")}`
|
|
497
|
+
: `${verified.length} inspected visual artifact(s) exist under the required local root with valid media signatures.`,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { cleanString, stringArray } from "./shared.js";
|
|
2
|
+
|
|
3
|
+
export const COMPLETION_CONTRACT_VERSION = 2;
|
|
4
|
+
|
|
5
|
+
export function buildCompletionContract({
|
|
6
|
+
inherited,
|
|
7
|
+
requirement,
|
|
8
|
+
acceptanceCriteria,
|
|
9
|
+
nonGoals,
|
|
10
|
+
expectedEvidence,
|
|
11
|
+
}) {
|
|
12
|
+
const source = record(inherited);
|
|
13
|
+
const minimumScope = stringArray(source.minimum_scope);
|
|
14
|
+
const proofRequired = stringArray(source.proof_required);
|
|
15
|
+
return {
|
|
16
|
+
version: COMPLETION_CONTRACT_VERSION,
|
|
17
|
+
user_outcome: cleanString(source.user_outcome) || cleanString(requirement),
|
|
18
|
+
minimum_scope:
|
|
19
|
+
minimumScope.length > 0 ? minimumScope : stringArray(acceptanceCriteria),
|
|
20
|
+
non_goals: stringArray(nonGoals),
|
|
21
|
+
proof_required: uniqueStrings([
|
|
22
|
+
"criterion_evidence",
|
|
23
|
+
...proofRequired,
|
|
24
|
+
...stringArray(expectedEvidence),
|
|
25
|
+
]),
|
|
26
|
+
deadline: cleanString(source.deadline) || null,
|
|
27
|
+
milestones: stringArray(source.milestones),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function acceptanceCriterionRows(charter) {
|
|
32
|
+
return stringArray(charter?.acceptance_criteria).map((statement, index) => ({
|
|
33
|
+
id: `AC-${index + 1}`,
|
|
34
|
+
statement,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeUncertaintyVerification(value) {
|
|
39
|
+
const source = record(value);
|
|
40
|
+
return {
|
|
41
|
+
top_uncertainty: cleanString(source.top_uncertainty) || null,
|
|
42
|
+
first_verification: cleanString(source.first_verification) || null,
|
|
43
|
+
evidence_checked: uniqueStrings(source.evidence_checked),
|
|
44
|
+
observed_result: cleanString(source.observed_result) || null,
|
|
45
|
+
resolved: source.resolved === true,
|
|
46
|
+
fallback: cleanString(source.fallback) || null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizeCriterionEvidence(value) {
|
|
51
|
+
if (!Array.isArray(value)) return [];
|
|
52
|
+
return value
|
|
53
|
+
.map((item) => {
|
|
54
|
+
const source = record(item);
|
|
55
|
+
const criterionId = cleanString(source.criterion_id);
|
|
56
|
+
if (!criterionId) return null;
|
|
57
|
+
return {
|
|
58
|
+
criterion_id: criterionId,
|
|
59
|
+
passed: source.passed === true,
|
|
60
|
+
verification_method: cleanString(source.verification_method) || null,
|
|
61
|
+
evidence_refs: uniqueStrings(source.evidence_refs),
|
|
62
|
+
observed_result: cleanString(source.observed_result) || null,
|
|
63
|
+
};
|
|
64
|
+
})
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function normalizeVisualReview(value) {
|
|
69
|
+
const source = record(value);
|
|
70
|
+
return {
|
|
71
|
+
inspected: source.inspected === true,
|
|
72
|
+
artifacts: uniqueStrings(source.artifacts),
|
|
73
|
+
acceptance_observations: uniqueStrings(source.acceptance_observations),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function completionEvidenceChecks(charter, attempt) {
|
|
78
|
+
const contract = record(charter?.completion_contract);
|
|
79
|
+
if (contract.version !== COMPLETION_CONTRACT_VERSION) return [];
|
|
80
|
+
|
|
81
|
+
const uncertainty = normalizeUncertaintyVerification(attempt?.uncertainty_verification);
|
|
82
|
+
const criterionEvidence = normalizeCriterionEvidence(attempt?.criterion_evidence);
|
|
83
|
+
const rows = acceptanceCriterionRows(charter);
|
|
84
|
+
const checks = [
|
|
85
|
+
check(
|
|
86
|
+
"top_uncertainty_recorded",
|
|
87
|
+
"Highest uncertainty is explicit",
|
|
88
|
+
Boolean(uncertainty.top_uncertainty),
|
|
89
|
+
uncertainty.top_uncertainty || "No highest uncertainty was recorded.",
|
|
90
|
+
),
|
|
91
|
+
check(
|
|
92
|
+
"first_verification_recorded",
|
|
93
|
+
"First discriminating verification is explicit",
|
|
94
|
+
Boolean(uncertainty.first_verification),
|
|
95
|
+
uncertainty.first_verification || "No first verification was recorded.",
|
|
96
|
+
),
|
|
97
|
+
check(
|
|
98
|
+
"uncertainty_evidence_recorded",
|
|
99
|
+
"Uncertainty verification records concrete evidence",
|
|
100
|
+
uncertainty.evidence_checked.length > 0 && Boolean(uncertainty.observed_result),
|
|
101
|
+
uncertainty.evidence_checked.length > 0 && uncertainty.observed_result
|
|
102
|
+
? `${uncertainty.evidence_checked.length} evidence item(s): ${uncertainty.observed_result}`
|
|
103
|
+
: "The uncertainty check needs evidence_checked and an observed_result.",
|
|
104
|
+
),
|
|
105
|
+
check(
|
|
106
|
+
"top_uncertainty_resolved",
|
|
107
|
+
"Highest uncertainty is resolved before completion",
|
|
108
|
+
uncertainty.resolved,
|
|
109
|
+
uncertainty.resolved
|
|
110
|
+
? "The executor marked the highest uncertainty resolved with observed evidence."
|
|
111
|
+
: uncertainty.fallback || "The highest uncertainty remains unresolved.",
|
|
112
|
+
),
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
for (const row of rows) {
|
|
116
|
+
const matches = criterionEvidence.filter((item) => item.criterion_id === row.id);
|
|
117
|
+
const evidence = matches[0];
|
|
118
|
+
checks.push(
|
|
119
|
+
check(
|
|
120
|
+
`criterion_evidence_${row.id.toLowerCase()}`,
|
|
121
|
+
`${row.id} has passing completion evidence`,
|
|
122
|
+
Boolean(
|
|
123
|
+
matches.length === 1 &&
|
|
124
|
+
evidence?.passed &&
|
|
125
|
+
evidence.verification_method &&
|
|
126
|
+
evidence.evidence_refs.length > 0 &&
|
|
127
|
+
evidence.observed_result,
|
|
128
|
+
),
|
|
129
|
+
matches.length > 1
|
|
130
|
+
? `Expected exactly one evidence result for ${row.id}; recorded ${matches.length}.`
|
|
131
|
+
: evidence
|
|
132
|
+
? `${evidence.verification_method || "method missing"}: ${
|
|
133
|
+
evidence.observed_result || "observed result missing"
|
|
134
|
+
}; evidence ${evidence.evidence_refs.join(", ") || "missing"}.`
|
|
135
|
+
: `No criterion evidence was recorded for ${row.id}: ${row.statement}`,
|
|
136
|
+
evidence?.evidence_refs || [],
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return checks;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function check(id, label, passed, detail, evidenceRefs = []) {
|
|
144
|
+
return {
|
|
145
|
+
id,
|
|
146
|
+
label,
|
|
147
|
+
passed: Boolean(passed),
|
|
148
|
+
detail,
|
|
149
|
+
evidence_refs: evidenceRefs,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function record(value) {
|
|
154
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function uniqueStrings(value) {
|
|
158
|
+
return [...new Set(stringArray(value))];
|
|
159
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "./shared.js";
|
|
10
10
|
import { budgetState, resolveRunBudgets } from "./budget.js";
|
|
11
11
|
import { classifyRisk } from "./risk.js";
|
|
12
|
+
import { buildCompletionContract } from "./completion-contract.js";
|
|
12
13
|
import {
|
|
13
14
|
DESKTOP_EXECUTION_TARGET,
|
|
14
15
|
normalizeDesktopExecutionTarget,
|
|
@@ -63,6 +64,19 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
|
|
|
63
64
|
const inheritedNonGoals = stringArray(charterContext.non_goals);
|
|
64
65
|
const inheritedArtifacts = stringArray(charterContext.artifacts_required);
|
|
65
66
|
const inheritedAssumptions = stringArray(charterContext.assumptions);
|
|
67
|
+
const inheritedCompletionContract = record(charterContext.completion_contract);
|
|
68
|
+
const finalAcceptanceCriteria =
|
|
69
|
+
acceptanceCriteria.length > 0
|
|
70
|
+
? acceptanceCriteria
|
|
71
|
+
: [
|
|
72
|
+
"The requested change is implemented without unrelated scope.",
|
|
73
|
+
"Relevant tests pass and are recorded.",
|
|
74
|
+
`A pull request targets ${baseBranch}.`,
|
|
75
|
+
];
|
|
76
|
+
const finalNonGoals =
|
|
77
|
+
inheritedNonGoals.length > 0
|
|
78
|
+
? inheritedNonGoals
|
|
79
|
+
: ["Unrelated refactors", "Server-side execution of repository code"];
|
|
66
80
|
const designReviewRequired =
|
|
67
81
|
risk.requires_design_review || inheritedDesignReview.required === true;
|
|
68
82
|
const designReviewApproved =
|
|
@@ -101,14 +115,7 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
|
|
|
101
115
|
retry_budget: budgets.retry_budget,
|
|
102
116
|
token_budget: budgets.token_budget,
|
|
103
117
|
ui_evidence_root: cleanString(request.ui_evidence_root),
|
|
104
|
-
acceptance_criteria:
|
|
105
|
-
acceptanceCriteria.length > 0
|
|
106
|
-
? acceptanceCriteria
|
|
107
|
-
: [
|
|
108
|
-
"The requested change is implemented without unrelated scope.",
|
|
109
|
-
"Relevant tests pass and are recorded.",
|
|
110
|
-
`A pull request targets ${baseBranch}.`,
|
|
111
|
-
],
|
|
118
|
+
acceptance_criteria: finalAcceptanceCriteria,
|
|
112
119
|
acceptance_criteria_source: acceptanceCriteriaSource,
|
|
113
120
|
expected_tests: stringArray(request.expected_tests),
|
|
114
121
|
expected_evidence: expectedEvidence,
|
|
@@ -149,14 +156,18 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
|
|
|
149
156
|
accepted_artifact_kinds: browserEvidenceRequired ? ["image", "video"] : [],
|
|
150
157
|
visual_review_required: browserEvidenceRequired,
|
|
151
158
|
},
|
|
159
|
+
completion_contract: buildCompletionContract({
|
|
160
|
+
inherited: inheritedCompletionContract,
|
|
161
|
+
requirement: cleanString(charterContext.user_goal) || cleanString(request.requirement),
|
|
162
|
+
acceptanceCriteria: finalAcceptanceCriteria,
|
|
163
|
+
nonGoals: finalNonGoals,
|
|
164
|
+
expectedEvidence,
|
|
165
|
+
}),
|
|
152
166
|
thread_state: {
|
|
153
167
|
original_request:
|
|
154
168
|
cleanString(charterContext.original_request) || cleanString(request.requirement),
|
|
155
169
|
user_goal: cleanString(charterContext.user_goal) || cleanString(request.requirement),
|
|
156
|
-
non_goals:
|
|
157
|
-
inheritedNonGoals.length > 0
|
|
158
|
-
? inheritedNonGoals
|
|
159
|
-
: ["Unrelated refactors", "Server-side execution of repository code"],
|
|
170
|
+
non_goals: finalNonGoals,
|
|
160
171
|
expected_finish_line:
|
|
161
172
|
cleanString(charterContext.expected_finish_line) ||
|
|
162
173
|
`A verified pull request targeting ${baseBranch}.`,
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { bullets, cleanString, section } from "./shared.js";
|
|
2
|
+
import {
|
|
3
|
+
COMPLETION_CONTRACT_VERSION,
|
|
4
|
+
acceptanceCriterionRows,
|
|
5
|
+
normalizeCriterionEvidence,
|
|
6
|
+
normalizeVisualReview,
|
|
7
|
+
} from "./completion-contract.js";
|
|
2
8
|
|
|
3
9
|
export function buildIndependentAuditInstruction(charter, attempt) {
|
|
4
10
|
const localEvidence = attempt.observed_evidence?.local || {};
|
|
@@ -21,6 +27,8 @@ export function buildIndependentAuditInstruction(charter, attempt) {
|
|
|
21
27
|
[
|
|
22
28
|
`Requirement: ${charter.requirement}`,
|
|
23
29
|
`Acceptance criteria:\n${bullets(charter.acceptance_criteria)}`,
|
|
30
|
+
`User-visible outcome: ${charter.completion_contract?.user_outcome || charter.requirement}`,
|
|
31
|
+
`Minimum scope:\n${bullets(charter.completion_contract?.minimum_scope || charter.acceptance_criteria)}`,
|
|
24
32
|
`Risk level: ${charter.risk.level}`,
|
|
25
33
|
`Risk tags: ${charter.risk.tags.join(", ") || "none"}`,
|
|
26
34
|
`Base branch: ${charter.base_branch}`,
|
|
@@ -33,6 +41,9 @@ export function buildIndependentAuditInstruction(charter, attempt) {
|
|
|
33
41
|
`Local UI evidence path: ${attempt.ui_evidence_path || "not reported"}`,
|
|
34
42
|
`Local evidence verification: ${evidenceArtifact?.summary || "not available"}`,
|
|
35
43
|
`Reported tests:\n${bullets(attempt.tests_run || [])}`,
|
|
44
|
+
`Executor uncertainty verification: ${JSON.stringify(attempt.uncertainty_verification || {})}`,
|
|
45
|
+
`Executor criterion evidence: ${JSON.stringify(attempt.criterion_evidence || [])}`,
|
|
46
|
+
`Executor visual review: ${JSON.stringify(attempt.visual_review || {})}`,
|
|
36
47
|
`Observed local branch: ${localEvidence.branch_name || "unknown"}`,
|
|
37
48
|
`Observed local HEAD: ${localEvidence.head_sha || "unknown"}`,
|
|
38
49
|
`Observed changed files:\n${bullets(localEvidence.changed_files || [])}`,
|
|
@@ -48,6 +59,10 @@ export function buildIndependentAuditInstruction(charter, attempt) {
|
|
|
48
59
|
"Inspect the final diff and related code directly from the repository.",
|
|
49
60
|
"Look for unmet acceptance criteria, regressions, security issues, data and concurrency risks, missing tests, and unrelated changes.",
|
|
50
61
|
"Treat executor claims as untrusted until supported by repository evidence.",
|
|
62
|
+
"For every AC-N criterion, return a criterion_results entry only after directly checking its cited proof and observed outcome.",
|
|
63
|
+
charter.evidence_policy?.browser_required
|
|
64
|
+
? "Open and inspect the actual local screenshots or videos. Return visual_review.inspected=true only after checking their visible content against the acceptance criteria."
|
|
65
|
+
: "Do not require visual evidence when the Charter does not require browser evidence.",
|
|
51
66
|
"Do not modify files, commit, push, or update the pull request.",
|
|
52
67
|
].join("\n"),
|
|
53
68
|
),
|
|
@@ -55,8 +70,10 @@ export function buildIndependentAuditInstruction(charter, attempt) {
|
|
|
55
70
|
"result_contract",
|
|
56
71
|
[
|
|
57
72
|
"Finish with exactly one line beginning SIMY_AUDIT_JSON: followed by one JSON object.",
|
|
58
|
-
"Required keys: passed, summary, findings.",
|
|
73
|
+
"Required keys: passed, summary, findings, criterion_results, visual_review.",
|
|
59
74
|
"Each finding candidate must contain passed, code, severity, target, explanation, and repairability.",
|
|
75
|
+
"criterion_results must contain one entry per AC-N with criterion_id, passed, verification_method, evidence_refs, and observed_result.",
|
|
76
|
+
"visual_review must contain inspected, artifacts, and acceptance_observations.",
|
|
60
77
|
"Only passed=false entries are findings. Omit passing checks, and never report passed=true evidence as a finding.",
|
|
61
78
|
"Use passed=false when a blocker or major finding remains, or when evidence is unavailable.",
|
|
62
79
|
].join("\n"),
|
|
@@ -64,13 +81,19 @@ export function buildIndependentAuditInstruction(charter, attempt) {
|
|
|
64
81
|
].join("\n\n");
|
|
65
82
|
}
|
|
66
83
|
|
|
67
|
-
export function buildIndependentAudit(execution) {
|
|
84
|
+
export function buildIndependentAudit(execution, charter = null, attempt = null) {
|
|
68
85
|
const result = execution?.result && typeof execution.result === "object" ? execution.result : {};
|
|
86
|
+
const completionContractV2 =
|
|
87
|
+
charter?.completion_contract?.version === COMPLETION_CONTRACT_VERSION;
|
|
69
88
|
const valid =
|
|
70
89
|
execution?.exitCode === 0 &&
|
|
71
90
|
!execution?.error &&
|
|
72
91
|
typeof result.passed === "boolean" &&
|
|
73
|
-
Array.isArray(result.findings)
|
|
92
|
+
Array.isArray(result.findings) &&
|
|
93
|
+
(!completionContractV2 ||
|
|
94
|
+
(Array.isArray(result.criterion_results) &&
|
|
95
|
+
result.visual_review &&
|
|
96
|
+
typeof result.visual_review === "object"));
|
|
74
97
|
if (!valid) {
|
|
75
98
|
return {
|
|
76
99
|
passed: false,
|
|
@@ -85,6 +108,8 @@ export function buildIndependentAudit(execution) {
|
|
|
85
108
|
repairability: "manual",
|
|
86
109
|
},
|
|
87
110
|
],
|
|
111
|
+
criterion_results: [],
|
|
112
|
+
visual_review: normalizeVisualReview(null),
|
|
88
113
|
requires_human: true,
|
|
89
114
|
};
|
|
90
115
|
}
|
|
@@ -101,6 +126,64 @@ export function buildIndependentAudit(execution) {
|
|
|
101
126
|
auto_fix_hint: cleanString(item.auto_fix_hint) || null,
|
|
102
127
|
}));
|
|
103
128
|
const findings = evaluations.filter((item) => item.passed === false);
|
|
129
|
+
const criterionResults = normalizeCriterionEvidence(result.criterion_results);
|
|
130
|
+
if (completionContractV2) {
|
|
131
|
+
for (const criterion of acceptanceCriterionRows(charter)) {
|
|
132
|
+
const matches = criterionResults.filter(
|
|
133
|
+
(item) => item.criterion_id === criterion.id,
|
|
134
|
+
);
|
|
135
|
+
const criterionResult = matches[0];
|
|
136
|
+
if (
|
|
137
|
+
matches.length === 1 &&
|
|
138
|
+
criterionResult?.passed &&
|
|
139
|
+
criterionResult.verification_method &&
|
|
140
|
+
criterionResult.evidence_refs.length > 0 &&
|
|
141
|
+
criterionResult.observed_result
|
|
142
|
+
) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
findings.push({
|
|
146
|
+
passed: false,
|
|
147
|
+
code: `INDEPENDENT_${criterion.id.replace("-", "_")}_UNPROVEN`,
|
|
148
|
+
severity: "blocker",
|
|
149
|
+
target: criterion.id,
|
|
150
|
+
explanation: matches.length > 1
|
|
151
|
+
? `The independent result returned ${matches.length} entries for ${criterion.id}; exactly one is required.`
|
|
152
|
+
: criterionResult
|
|
153
|
+
? `The independent result did not positively prove ${criterion.id}: ${criterion.statement}`
|
|
154
|
+
: `The independent result omitted ${criterion.id}: ${criterion.statement}`,
|
|
155
|
+
repairability: "auto",
|
|
156
|
+
auto_fix_hint: "Re-audit this acceptance criterion and cite directly inspected evidence.",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const visualReview = normalizeVisualReview(result.visual_review);
|
|
161
|
+
const executorVisualReview = normalizeVisualReview(attempt?.visual_review);
|
|
162
|
+
const visualArtifactsOverlap = visualReview.artifacts.some((artifact) =>
|
|
163
|
+
executorVisualReview.artifacts.includes(artifact),
|
|
164
|
+
);
|
|
165
|
+
if (
|
|
166
|
+
completionContractV2 &&
|
|
167
|
+
charter.evidence_policy?.browser_required &&
|
|
168
|
+
!(
|
|
169
|
+
visualReview.inspected &&
|
|
170
|
+
visualReview.artifacts.length > 0 &&
|
|
171
|
+
visualReview.acceptance_observations.length > 0 &&
|
|
172
|
+
(!attempt || visualArtifactsOverlap)
|
|
173
|
+
)
|
|
174
|
+
) {
|
|
175
|
+
findings.push({
|
|
176
|
+
passed: false,
|
|
177
|
+
code: "INDEPENDENT_VISUAL_REVIEW_UNPROVEN",
|
|
178
|
+
severity: "blocker",
|
|
179
|
+
target: "visual_review",
|
|
180
|
+
explanation: attempt && !visualArtifactsOverlap
|
|
181
|
+
? "The independent auditor did not inspect any artifact from the executor's verified visual-review set."
|
|
182
|
+
: "The independent auditor did not record inspection of the actual visual evidence content.",
|
|
183
|
+
repairability: "auto",
|
|
184
|
+
auto_fix_hint: "Open the local screenshots or videos and record acceptance observations.",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
104
187
|
if (result.passed === false && findings.length === 0) {
|
|
105
188
|
findings.push({
|
|
106
189
|
passed: false,
|
|
@@ -117,6 +200,8 @@ export function buildIndependentAudit(execution) {
|
|
|
117
200
|
passed: result.passed === true && findings.length === 0,
|
|
118
201
|
summary: cleanString(result.summary) || "Independent audit completed.",
|
|
119
202
|
findings,
|
|
203
|
+
criterion_results: criterionResults,
|
|
204
|
+
visual_review: visualReview,
|
|
120
205
|
requires_human: requiresHuman,
|
|
121
206
|
};
|
|
122
207
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { bullets, looksLikeUiTask, section } from "./shared.js";
|
|
2
2
|
import { problemSolvingResultContract } from "./problem-solving.js";
|
|
3
|
+
import { acceptanceCriterionRows } from "./completion-contract.js";
|
|
3
4
|
|
|
4
5
|
export function buildCodingInstruction(
|
|
5
6
|
charter,
|
|
@@ -31,7 +32,24 @@ export function buildCodingInstruction(
|
|
|
31
32
|
`Local evidence root: ${charter.ui_evidence_root || "not required"}`,
|
|
32
33
|
].join("\n"),
|
|
33
34
|
),
|
|
34
|
-
section(
|
|
35
|
+
section(
|
|
36
|
+
"completion_contract",
|
|
37
|
+
[
|
|
38
|
+
`User-visible outcome: ${charter.completion_contract?.user_outcome || charter.thread_state.user_goal}`,
|
|
39
|
+
`Minimum scope:\n${bullets(charter.completion_contract?.minimum_scope || charter.acceptance_criteria)}`,
|
|
40
|
+
`Non-goals:\n${bullets(charter.completion_contract?.non_goals || charter.thread_state.non_goals)}`,
|
|
41
|
+
`Proof required:\n${bullets(charter.completion_contract?.proof_required || [])}`,
|
|
42
|
+
`Deadline: ${charter.completion_contract?.deadline || "not specified"}`,
|
|
43
|
+
].join("\n"),
|
|
44
|
+
),
|
|
45
|
+
section(
|
|
46
|
+
"acceptance_criteria",
|
|
47
|
+
bullets(
|
|
48
|
+
acceptanceCriterionRows(charter).map(
|
|
49
|
+
(criterion) => `${criterion.id}: ${criterion.statement}`,
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
),
|
|
35
53
|
section("expected_tests", bullets(charter.expected_tests)),
|
|
36
54
|
section("must_not", bullets(charter.must_not)),
|
|
37
55
|
];
|
|
@@ -91,16 +109,17 @@ export function buildCodingInstruction(
|
|
|
91
109
|
"Treat implementation as hypothesis testing: state assumptions, list plausible causes or approaches, inspect discriminating evidence, then choose the smallest strategy supported by that evidence.",
|
|
92
110
|
attemptNumber > 1
|
|
93
111
|
? "This is a retry. Before editing, generate at least eight distinct MECE hypotheses, select a different primary hypothesis or approach, and do not repeat the previous strategy."
|
|
94
|
-
: "For the first attempt,
|
|
112
|
+
: "For the first attempt, identify the single highest uncertainty and run the smallest discriminating verification before editing.",
|
|
95
113
|
attemptNumber > 1
|
|
96
114
|
? "Explain exactly what changed from the previous attempt and why the new evidence supports this strategy."
|
|
97
|
-
: "Record the
|
|
115
|
+
: "Record the uncertainty, first verification, concrete evidence checked, observed result, selected hypothesis, and strategy in the structured result.",
|
|
98
116
|
].join("\n"),
|
|
99
117
|
),
|
|
100
118
|
section(
|
|
101
119
|
"execution_contract",
|
|
102
120
|
[
|
|
103
121
|
"Read the repository instructions and current git state before editing.",
|
|
122
|
+
"Before editing, resolve the highest uncertainty with the first discriminating verification. If it cannot be resolved, stop and report a fallback instead of claiming completion.",
|
|
104
123
|
"Preserve user changes and keep the diff scoped to the requirement.",
|
|
105
124
|
"Run relevant tests, inspect the final diff, commit, push, and create or update a PR.",
|
|
106
125
|
`The PR must target ${charter.base_branch}.`,
|
|
@@ -111,8 +130,11 @@ export function buildCodingInstruction(
|
|
|
111
130
|
"result_contract",
|
|
112
131
|
[
|
|
113
132
|
"Finish with exactly one line beginning SIMY_RESULT_JSON: followed by one JSON object.",
|
|
114
|
-
"Required keys: outcome_kind, summary, branch_name, commit_sha, commit_message_headline, pr_url, pr_number, pr_title, pr_base_branch, tests_run, tests_passed, ui_evidence_path, unrelated_changes_detected, secret_scan_passed, changed_files, residual_risks, problem_solving.",
|
|
133
|
+
"Required keys: outcome_kind, summary, branch_name, commit_sha, commit_message_headline, pr_url, pr_number, pr_title, pr_base_branch, tests_run, tests_passed, ui_evidence_path, unrelated_changes_detected, secret_scan_passed, changed_files, residual_risks, problem_solving, uncertainty_verification, criterion_evidence, visual_review.",
|
|
115
134
|
problemSolvingResultContract(),
|
|
135
|
+
"uncertainty_verification must contain top_uncertainty, first_verification, evidence_checked, observed_result, resolved, and fallback. Only set resolved=true when concrete observed evidence resolves it.",
|
|
136
|
+
"criterion_evidence must contain one entry for every AC-N with criterion_id, passed, verification_method, evidence_refs, and observed_result. Only set passed=true when the evidence directly proves that criterion.",
|
|
137
|
+
"For UI or browser-extension work, visual_review must contain inspected=true, the local screenshot or video paths in artifacts, and concrete acceptance_observations from viewing the actual content. Capturing a file without viewing it does not count.",
|
|
116
138
|
"Use null for unavailable scalar values and [] for unavailable arrays. Do not claim evidence that was not observed.",
|
|
117
139
|
].join("\n"),
|
|
118
140
|
),
|
package/src/orchestrator/loop.js
CHANGED
|
@@ -202,7 +202,11 @@ export async function runCodingLoop({
|
|
|
202
202
|
}),
|
|
203
203
|
);
|
|
204
204
|
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
205
|
-
attempt.independent_audit = buildIndependentAudit(
|
|
205
|
+
attempt.independent_audit = buildIndependentAudit(
|
|
206
|
+
auditExecution,
|
|
207
|
+
snapshot.charter,
|
|
208
|
+
attempt,
|
|
209
|
+
);
|
|
206
210
|
attempt.token_usage = mergeTokenUsage(attempt.token_usage, auditExecution.tokenUsage);
|
|
207
211
|
attempt.budget = refreshBudgetState(snapshot);
|
|
208
212
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { cleanString, stringArray } from "./shared.js";
|
|
2
2
|
import { normalizeProblemSolving } from "./problem-solving.js";
|
|
3
|
+
import {
|
|
4
|
+
normalizeCriterionEvidence,
|
|
5
|
+
normalizeUncertaintyVerification,
|
|
6
|
+
normalizeVisualReview,
|
|
7
|
+
} from "./completion-contract.js";
|
|
3
8
|
|
|
4
9
|
export function buildAttempt({ attemptNumber, charter, instruction, promptInterventions, execution }) {
|
|
5
10
|
const result = execution.result && typeof execution.result === "object" ? execution.result : {};
|
|
@@ -28,6 +33,11 @@ export function buildAttempt({ attemptNumber, charter, instruction, promptInterv
|
|
|
28
33
|
ui_evidence_path: cleanString(result.ui_evidence_path) || null,
|
|
29
34
|
summary,
|
|
30
35
|
problem_solving: normalizeProblemSolving(result.problem_solving),
|
|
36
|
+
uncertainty_verification: normalizeUncertaintyVerification(
|
|
37
|
+
result.uncertainty_verification,
|
|
38
|
+
),
|
|
39
|
+
criterion_evidence: normalizeCriterionEvidence(result.criterion_evidence),
|
|
40
|
+
visual_review: normalizeVisualReview(result.visual_review),
|
|
31
41
|
prompt_interventions: promptInterventions,
|
|
32
42
|
work_log_signals: [],
|
|
33
43
|
work_log_interventions: [],
|
package/src/runner.js
CHANGED
|
@@ -158,6 +158,7 @@ export function restoreRun({ snapshot, session, apiOrigin, localPath = null }) {
|
|
|
158
158
|
risk: charter.risk,
|
|
159
159
|
design_review: charter.design_review,
|
|
160
160
|
evidence_policy: charter.evidence_policy,
|
|
161
|
+
completion_contract: charter.completion_contract,
|
|
161
162
|
prompt_policy_report: charter.prompt_policy_report,
|
|
162
163
|
original_request: charter.thread_state?.original_request,
|
|
163
164
|
user_goal: charter.thread_state?.user_goal,
|