@awak-app/simy-cli 0.5.29 → 0.5.31
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 +15 -5
- package/package.json +3 -2
- package/src/agent.js +148 -3
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +15 -6
- package/src/codex-plugins/simy/hooks/README.md +15 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +196 -0
- package/src/codex-plugins/simy/hooks/hooks.json +53 -0
- package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +28 -0
- package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +3 -1
- package/src/coding-run-store.js +69 -0
- package/src/delivery-hooks/command.js +31 -0
- package/src/delivery-hooks/desktop.js +40 -0
- package/src/desktop-host.js +11 -1
- package/src/index.js +7 -0
- package/src/local-task.js +55 -0
- package/src/orchestrator/audit.js +3 -0
- package/src/orchestrator/checklist-continuation.js +3 -3
- package/src/orchestrator/completion-contract.js +7 -0
- package/src/orchestrator/contract.js +6 -4
- package/src/orchestrator/delivery/WORKFLOW.md +93 -6
- package/src/orchestrator/delivery/references/acceptance-evidence.md +9 -5
- package/src/orchestrator/delivery/references/change-boundary-matrix.md +20 -1
- package/src/orchestrator/delivery/references/desktop-monitor-triggers.md +35 -0
- package/src/orchestrator/delivery/references/shared-system-specifications.md +3 -0
- package/src/orchestrator/delivery/references/state-and-receipts.md +19 -0
- package/src/orchestrator/execution-io.js +15 -0
- package/src/orchestrator/independent-audit.js +14 -1
- package/src/orchestrator/instruction.js +3 -0
- package/src/orchestrator/loop.js +71 -0
- package/src/orchestrator/outcome-monitor.js +123 -0
- package/src/provider-stream.js +3 -1
- package/src/runner.js +162 -20
- package/src/sqm/agent-skill.js +3 -2
- package/src/sqm/claude-plugin.js +6 -1
- package/src/sqm/command.js +13 -0
- package/src/sqm/report.js +23 -0
- package/src/sqm/workflow-onboarding.js +1 -1
- package/src/workflow-observation-sender.js +208 -0
- package/src/workflow-reporting.js +35 -0
- package/src/workflow-reporting.md +137 -0
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
"timeout": 3
|
|
11
11
|
}
|
|
12
12
|
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "node --input-type=module -e \"import { pathToFileURL } from 'node:url'; import { join } from 'node:path'; const hook = await import(pathToFileURL(join(process.env.PLUGIN_ROOT, 'hooks', 'delivery-loop.mjs')).href); await hook.main('codex');\"",
|
|
19
|
+
"timeout": 3
|
|
20
|
+
}
|
|
21
|
+
]
|
|
13
22
|
}
|
|
14
23
|
],
|
|
15
24
|
"UserPromptSubmit": [
|
|
@@ -22,6 +31,50 @@
|
|
|
22
31
|
}
|
|
23
32
|
]
|
|
24
33
|
}
|
|
34
|
+
],
|
|
35
|
+
"Stop": [
|
|
36
|
+
{
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "node --input-type=module -e \"import { pathToFileURL } from 'node:url'; import { join } from 'node:path'; const hook = await import(pathToFileURL(join(process.env.PLUGIN_ROOT, 'hooks', 'delivery-loop.mjs')).href); await hook.main('codex');\"",
|
|
41
|
+
"timeout": 3
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"SessionEnd": [
|
|
47
|
+
{
|
|
48
|
+
"hooks": [
|
|
49
|
+
{
|
|
50
|
+
"type": "command",
|
|
51
|
+
"command": "node --input-type=module -e \"import { pathToFileURL } from 'node:url'; import { join } from 'node:path'; const hook = await import(pathToFileURL(join(process.env.PLUGIN_ROOT, 'hooks', 'delivery-loop.mjs')).href); await hook.main('codex');\"",
|
|
52
|
+
"timeout": 3
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"Interrupt": [
|
|
58
|
+
{
|
|
59
|
+
"hooks": [
|
|
60
|
+
{
|
|
61
|
+
"type": "command",
|
|
62
|
+
"command": "node --input-type=module -e \"import { pathToFileURL } from 'node:url'; import { join } from 'node:path'; const hook = await import(pathToFileURL(join(process.env.PLUGIN_ROOT, 'hooks', 'delivery-loop.mjs')).href); await hook.main('codex');\"",
|
|
63
|
+
"timeout": 3
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"PermissionRequest": [
|
|
69
|
+
{
|
|
70
|
+
"hooks": [
|
|
71
|
+
{
|
|
72
|
+
"type": "command",
|
|
73
|
+
"command": "node --input-type=module -e \"import { pathToFileURL } from 'node:url'; import { join } from 'node:path'; const hook = await import(pathToFileURL(join(process.env.PLUGIN_ROOT, 'hooks', 'delivery-loop.mjs')).href); await hook.main('codex');\"",
|
|
74
|
+
"timeout": 3
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
25
78
|
]
|
|
26
79
|
}
|
|
27
80
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: delivery-loop
|
|
3
|
+
description: Continue an explicitly requested SIMY Delivery Loop in the current provider conversation using user-reviewed hooks, bounded checkpoints, and human blocker handling.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SIMY Delivery Loop
|
|
7
|
+
|
|
8
|
+
Use only when the user explicitly requests this skill or SIMY Delivery Loop for the current task. Installing this plugin does not opt a task in. Do not run SQM unless separately requested.
|
|
9
|
+
|
|
10
|
+
1. Require the SIMY SessionStart hook context with the exact provider and session ID. If absent, explain how to install the SIMY plugin, review and enable its hooks with `/hooks`, and start a new conversation. Codex settings can be opened at `codex://settings`; a hooks subpage deep link is not guaranteed. Never change hook trust or bypass permissions yourself.
|
|
11
|
+
2. Use `simy delivery start --provider <provider> --session <exact-session-id> --objective '<user outcome and acceptance criteria>' --next-step '<concrete next action>'`. Default limits: 6 automatic continuations, 3 consecutive failures/missing checkpoints, 30 minutes. Set different finite limits only if the user requests them. Use the current working directory. Never invent a session ID, start another loop to reset limits, or resume a stopped loop without a user request.
|
|
12
|
+
3. Work in the current conversation with its normal permissions. Before ending each turn, write a JSON checkpoint and run `simy delivery checkpoint --provider <provider> --session <id> --file <file>` from the same directory. Keep checkpoints outside the tracked source diff.
|
|
13
|
+
|
|
14
|
+
Checkpoint shapes:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{"status":"in_progress","summary":"Tests identified one remaining failure","nextStep":"Fix the failing validation test and rerun the targeted suite"}
|
|
18
|
+
```
|
|
19
|
+
```json
|
|
20
|
+
{"status":"blocked","blocker":"authorization","summary":"Deployment requires the user's approval"}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For missing information use `blocker: "missing_information"`. For a failed attempt use `status: "error"` and a concrete `nextStep`; repeated errors stop the loop. Do not reclassify blocked work as in_progress to force continuation. Never assume a provider error means permission to switch accounts, credentials, or bypass approvals.
|
|
24
|
+
|
|
25
|
+
For completed work use `{"status":"completed","summary":"Actual result","acceptanceMet":true,"evidence":["Exact checks and observed results"]}` only after checking the user's acceptance criteria. Evidence is a report, not independent certification: preserve failed checks and deployment gaps. The hook must not infer success from optimistic closing text.
|
|
26
|
+
|
|
27
|
+
4. The Stop hook evaluates checkpoints and supplies the next instruction within the original budgets. Missing checkpoints consume the failure budget. A human interrupt stops the loop. Approval/input events block it. Provider errors/exits may require reopening the existing conversation; these hooks cannot restart a dead provider process.
|
|
28
|
+
5. After the user explicitly resolves the blocker and requests continuation, `simy delivery resume --provider <provider> --session <id>` retains all budgets. `simy delivery stop` permanently ends this loop. `simy delivery status` shows observed hook activity and loop states; old observations do not prove hooks remain enabled today.
|
|
@@ -14,7 +14,9 @@ Treat the current Codex thread, its attachments, and the active repository as th
|
|
|
14
14
|
3. Run `simy sqm min-check` from the repository root.
|
|
15
15
|
4. Run `simy sqm check --proof proof.json --signed-evidence proof.json.signed.json`.
|
|
16
16
|
5. Read `proof.json` and require `outcome.status` to be `passed` before reporting success or proposing a pull request.
|
|
17
|
-
6.
|
|
17
|
+
6. Verify the proof's repository, base, HEAD and working-tree/diff identity against the candidate, and verify its bound signature with SQM tooling. Use separate artifact paths outside the diff; do not edit between min-check and check or reuse stale proof after a candidate change.
|
|
18
|
+
7. Report outcome reasons, matched modules, evaluated rules and scenario statuses from `executions`, separating blocking and advisory results. `passed` with `no_applicable_rules`, zero scenario evaluations, or advisory-only checks is not feature or incident coverage. Scenario statuses are deterministic evaluations, not evidence that a browser, provider or deployed journey ran. Record relevant uncovered review findings and their concrete regression evidence or remaining gap.
|
|
19
|
+
8. Report the checked repository/revision, proof paths, outcome, blockers, and any evidence gap. Keep delivery acceptance pending for unverified applicable paths even when SQM passes.
|
|
18
20
|
|
|
19
21
|
If implementation is required before the check can pass, obtain or confirm the user's change authorization and make the smallest relevant repair. A request to run SQM alone does not authorize unrelated edits, a PR, merge, deployment, or external messages.
|
|
20
22
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { redactExecutionValue } from "./orchestrator/execution-io.js";
|
|
7
|
+
|
|
8
|
+
const RUN_ID = /^[A-Za-z0-9_-]{1,160}$/;
|
|
9
|
+
const writes = new Map();
|
|
10
|
+
|
|
11
|
+
export function codingRunsRoot(sessionRoot) {
|
|
12
|
+
return path.resolve(sessionRoot || path.join(homedir(), ".simy"), "coding-runs");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function saveCodingRunSnapshot(root, snapshot) {
|
|
16
|
+
const id = String(snapshot?.id || "");
|
|
17
|
+
if (!RUN_ID.test(id)) throw new Error("Invalid coding run ID for durable storage.");
|
|
18
|
+
// Capture at enqueue time: later mutations must not rewrite a queued version.
|
|
19
|
+
snapshot = redactExecutionValue(snapshot);
|
|
20
|
+
const directory = path.resolve(root);
|
|
21
|
+
const key = path.join(directory, id);
|
|
22
|
+
const previous = writes.get(key) || Promise.resolve();
|
|
23
|
+
const current = previous.catch(() => undefined).then(async () => {
|
|
24
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
25
|
+
const body = JSON.stringify(snapshot);
|
|
26
|
+
const document = JSON.stringify({
|
|
27
|
+
version: 1,
|
|
28
|
+
sha256: createHash("sha256").update(body).digest("hex"),
|
|
29
|
+
snapshot,
|
|
30
|
+
});
|
|
31
|
+
const target = `${key}.json`;
|
|
32
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
33
|
+
await writeFile(temporary, `${document}\n`, { mode: 0o600 });
|
|
34
|
+
await rename(temporary, target);
|
|
35
|
+
});
|
|
36
|
+
writes.set(key, current);
|
|
37
|
+
try { await current; } finally {
|
|
38
|
+
if (writes.get(key) === current) writes.delete(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function loadCodingRunSnapshots(root) {
|
|
43
|
+
let files;
|
|
44
|
+
try {
|
|
45
|
+
files = await readdir(root, { withFileTypes: true });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error?.code === "ENOENT") return { snapshots: [], errors: [] };
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const snapshots = [];
|
|
51
|
+
const errors = [];
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
if (!file.isFile() || !file.name.endsWith(".json")) continue;
|
|
54
|
+
const id = file.name.slice(0, -5);
|
|
55
|
+
if (!RUN_ID.test(id)) continue;
|
|
56
|
+
try {
|
|
57
|
+
const document = JSON.parse(await readFile(path.join(root, file.name), "utf8"));
|
|
58
|
+
const body = JSON.stringify(document.snapshot);
|
|
59
|
+
const hash = createHash("sha256").update(body).digest("hex");
|
|
60
|
+
if (document.version !== 1 || document.sha256 !== hash || document.snapshot?.id !== id) {
|
|
61
|
+
throw new Error("Coding run record failed integrity check.");
|
|
62
|
+
}
|
|
63
|
+
snapshots.push(document.snapshot);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
errors.push({ id, message: error instanceof Error ? error.message : String(error) });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { snapshots, errors };
|
|
69
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { startDelivery, checkpointDelivery, controlDelivery, listRecords, rootDirectory } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
|
+
|
|
4
|
+
export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = process.cwd(), output = console.log } = {}) {
|
|
5
|
+
const [action, ...args] = argv;
|
|
6
|
+
if (!action || action === '--help') {
|
|
7
|
+
output('simy delivery start --provider codex|claude --session ID --objective TEXT --next-step TEXT [--max-continuations 6] [--max-failures 3] [--max-minutes 30]\nsimy delivery checkpoint --provider codex|claude --session ID --file checkpoint.json\nsimy delivery status\nsimy delivery stop|resume --provider codex|claude --session ID\nEnable and review SIMY hooks in your provider first. Starting a loop opts in only this session. Resume requires the user to resolve the blocker; original budgets are retained.');
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
const options = {};
|
|
11
|
+
const allowed = new Set(['provider', 'session', 'objective', 'next-step', 'max-continuations', 'max-failures', 'max-minutes', 'file']);
|
|
12
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
13
|
+
const key = args[i]?.replace(/^--/, '');
|
|
14
|
+
if (!args[i]?.startsWith('--') || !allowed.has(key) || args[i + 1] === undefined || Object.hasOwn(options, key)) throw new Error('Invalid delivery option. Use simy delivery --help.');
|
|
15
|
+
options[key] = args[i + 1];
|
|
16
|
+
}
|
|
17
|
+
const base = { root, cwd, provider: options.provider, session: options.session };
|
|
18
|
+
let result;
|
|
19
|
+
if (action === 'status') result = (await listRecords(root)).filter(r => (!options.provider || r.provider === options.provider) && (!options.session || r.session === options.session));
|
|
20
|
+
else if (action === 'start') result = await startDelivery({ ...base, objective: options.objective, nextStep: options['next-step'],
|
|
21
|
+
...Object.fromEntries([['max-continuations', 'maxContinuations'], ['max-failures', 'maxFailures'], ['max-minutes', 'maxMinutes']].filter(([key]) => options[key] !== undefined).map(([key, field]) => [field, Number(options[key])])) });
|
|
22
|
+
else if (action === 'checkpoint') {
|
|
23
|
+
if (!options.file) throw new Error('--file is required.');
|
|
24
|
+
const data = await readFile(options.file, 'utf8');
|
|
25
|
+
if (Buffer.byteLength(data) > 64 * 1024) throw new Error('Checkpoint exceeds 64 KiB.');
|
|
26
|
+
result = await checkpointDelivery({ ...base, checkpoint: JSON.parse(data) });
|
|
27
|
+
} else if (['stop', 'resume'].includes(action)) result = await controlDelivery({ ...base, action });
|
|
28
|
+
else throw new Error('Unknown delivery command.');
|
|
29
|
+
output(JSON.stringify(result, null, 2));
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { reconcileRecords, rootDirectory } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
4
|
+
|
|
5
|
+
export const HOOK_SETUP = Object.freeze({
|
|
6
|
+
codex: { settingsUrl: 'codex://settings', docsUrl: 'https://learn.chatgpt.com/docs/hooks', command: '/hooks' },
|
|
7
|
+
claude: { settingsUrl: null, docsUrl: 'https://code.claude.com/docs/en/hooks-guide', command: '/hooks' },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export async function deliveryHookStatus({ root = rootDirectory(), now = Date.now() } = {}) {
|
|
11
|
+
const records = await reconcileRecords({ root, now });
|
|
12
|
+
return {
|
|
13
|
+
providers: Object.fromEntries(['codex', 'claude'].map(provider => {
|
|
14
|
+
const latest = records.filter(r => r.provider === provider).sort((a, b) => b.observedAt - a.observedAt)[0];
|
|
15
|
+
return [provider, { ...HOOK_SETUP[provider], state: latest ? 'observed' : 'not_observed', lastObservedAt: latest?.observedAt || null }];
|
|
16
|
+
})),
|
|
17
|
+
runs: records.filter(r => r.run).sort((a, b) => b.run.updatedAt - a.run.updatedAt).slice(0, 30).map(r => ({
|
|
18
|
+
provider: r.provider, session: r.session, objective: r.run.objective, state: r.run.state, reason: r.run.reason,
|
|
19
|
+
continuations: r.run.continuations, maxContinuations: r.run.maxContinuations, deadline: r.run.deadline,
|
|
20
|
+
needsAttention: ['blocked', 'failed', 'recovery_required', 'budget_exhausted'].includes(r.run.state), updatedAt: r.run.updatedAt,
|
|
21
|
+
})),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Wait for the OS opener to exit. A successful spawn alone does not prove that
|
|
26
|
+
// a custom URL scheme has a registered handler.
|
|
27
|
+
export async function openHookSetup(provider, { platform = process.platform, run = promisify(execFile) } = {}) {
|
|
28
|
+
const setup = HOOK_SETUP[provider];
|
|
29
|
+
if (!setup) throw new Error("Unsupported provider.");
|
|
30
|
+
const targets = platform === "darwin" && setup.settingsUrl ? [setup.settingsUrl, setup.docsUrl] : [setup.docsUrl];
|
|
31
|
+
for (const url of targets) {
|
|
32
|
+
try {
|
|
33
|
+
const command = platform === "darwin" ? "open" : platform === "win32" ? "rundll32" : "xdg-open";
|
|
34
|
+
const args = platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
35
|
+
await run(command, args, { timeout: 5000, maxBuffer: 4096 });
|
|
36
|
+
return { opened: true, url };
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
throw new Error("Could not open hook setup. Use /hooks inside your provider.");
|
|
40
|
+
}
|
package/src/desktop-host.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { deliveryHookStatus, openHookSetup } from "./delivery-hooks/desktop.js";
|
|
2
|
+
import { controlDelivery } from "./codex-plugins/simy/hooks/delivery-loop.mjs";
|
|
1
3
|
import { createWindowsDesktopUpdater } from "./windows-desktop-updater.js";
|
|
2
4
|
import { spawn, execFile } from "node:child_process";
|
|
3
5
|
import { promisify } from "node:util";
|
|
@@ -42,7 +44,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
42
44
|
const publish = () => {
|
|
43
45
|
if (closed || relay || !agent) return Promise.resolve();
|
|
44
46
|
if (publishing) return publishing;
|
|
45
|
-
publishing = Promise.resolve(agent.getDesktopStatus()).then((status) => {
|
|
47
|
+
publishing = Promise.resolve(agent.getDesktopStatus()).then(async (status) => {
|
|
48
|
+
status.deliveryHooks = await (dependencies.deliveryHookStatus || deliveryHookStatus)({ root: join(root, "delivery-hooks") }).catch(() => ({ error: "Delivery hook status unavailable", providers: {}, runs: [] }));
|
|
46
49
|
if (!closed && !relay) emit({ type: "status", status });
|
|
47
50
|
}).finally(() => { publishing = null; });
|
|
48
51
|
return publishing;
|
|
@@ -178,6 +181,13 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
178
181
|
if (login) await login(root);
|
|
179
182
|
else await openProviderLogin(provider, root);
|
|
180
183
|
}
|
|
184
|
+
else if (action === "codex_hooks" || action === "claude_hooks") {
|
|
185
|
+
const provider = action === "codex_hooks" ? "codex" : "claude";
|
|
186
|
+
await (dependencies.openHookSettings || openHookSetup)(provider);
|
|
187
|
+
} else if (action === "delivery_stop" || action === "delivery_resume") {
|
|
188
|
+
await controlDelivery({ root: join(root, "delivery-hooks"), provider: command.provider, session: command.session,
|
|
189
|
+
action: action === "delivery_stop" ? "stop" : "resume" });
|
|
190
|
+
}
|
|
181
191
|
else if (action === "quit") {
|
|
182
192
|
const status = await agent.getDesktopStatus();
|
|
183
193
|
if (status.tasks.activeCount > 0 && command.force !== true) throw new Error("Tasks are still running. Confirm before quitting.");
|
package/src/index.js
CHANGED
|
@@ -70,6 +70,13 @@ if (!configuredWebOrigin) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
async function runCommand(installMode) {
|
|
73
|
+
if (dispatchArgv[0] === "delivery") {
|
|
74
|
+
try {
|
|
75
|
+
const { runDeliveryCommand } = await import("./delivery-hooks/command.js");
|
|
76
|
+
process.exitCode = await runDeliveryCommand(dispatchArgv.slice(1));
|
|
77
|
+
} catch (error) { console.error(error.message); process.exitCode = 2; }
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
73
80
|
if (dispatchArgv[0] === "sqm") {
|
|
74
81
|
try {
|
|
75
82
|
const { runSqmCommand } = await import("./sqm/command.js");
|
package/src/local-task.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildWorkflowReportingInstruction } from "./workflow-reporting.js";
|
|
1
2
|
import { execFile } from "node:child_process";
|
|
2
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
4
|
import { EventEmitter, once } from "node:events";
|
|
@@ -37,6 +38,10 @@ import {
|
|
|
37
38
|
} from "./local-task-scenario-packs.js";
|
|
38
39
|
import { createProviderStreamDecoder } from "./provider-stream.js";
|
|
39
40
|
import { signalExecutorProcess } from "./runner.js";
|
|
41
|
+
import {
|
|
42
|
+
createWorkflowObservationSender,
|
|
43
|
+
normalizeWorkflowObservationBinding,
|
|
44
|
+
} from "./workflow-observation-sender.js";
|
|
40
45
|
|
|
41
46
|
export const DEFAULT_LOCAL_TASK_TOKEN_BUDGET = 100_000;
|
|
42
47
|
export const DEFAULT_LOCAL_TASK_TIMEOUT_MS = 10 * 60_000;
|
|
@@ -250,6 +255,7 @@ export function createLocalTask({
|
|
|
250
255
|
timeoutMs = DEFAULT_LOCAL_TASK_TIMEOUT_MS,
|
|
251
256
|
idempotencyKeyHash = null,
|
|
252
257
|
requestFingerprint = null,
|
|
258
|
+
workflowObservationBinding = null,
|
|
253
259
|
}) {
|
|
254
260
|
validateTaskId(id);
|
|
255
261
|
const now = new Date().toISOString();
|
|
@@ -258,6 +264,10 @@ export function createLocalTask({
|
|
|
258
264
|
const normalizedScenarioPack = normalizeLocalTaskScenarioPack(scenarioPack);
|
|
259
265
|
const scenarioExpectedOutputs =
|
|
260
266
|
localTaskScenarioExpectedOutputs(normalizedScenarioPack);
|
|
267
|
+
const normalizedWorkflowObservationBinding = normalizeWorkflowObservationBinding(
|
|
268
|
+
workflowObservationBinding,
|
|
269
|
+
{ provider: backend },
|
|
270
|
+
);
|
|
261
271
|
return {
|
|
262
272
|
id,
|
|
263
273
|
kind: "local_task",
|
|
@@ -295,6 +305,17 @@ export function createLocalTask({
|
|
|
295
305
|
provider_token_budget: tokenBudget,
|
|
296
306
|
provider_tokens_used: 0,
|
|
297
307
|
token_usage: { records: [] },
|
|
308
|
+
...(normalizedWorkflowObservationBinding
|
|
309
|
+
? {
|
|
310
|
+
workflow_observation_binding: normalizedWorkflowObservationBinding,
|
|
311
|
+
workflow_observation: {
|
|
312
|
+
status: "pending",
|
|
313
|
+
workflow_id: normalizedWorkflowObservationBinding.workflow_id,
|
|
314
|
+
workflow_run_id: normalizedWorkflowObservationBinding.workflow_run_id,
|
|
315
|
+
development_outcome: "unverified",
|
|
316
|
+
},
|
|
317
|
+
}
|
|
318
|
+
: {}),
|
|
298
319
|
timeout_ms: normalizedTimeout,
|
|
299
320
|
created_at: now,
|
|
300
321
|
updated_at: now,
|
|
@@ -326,6 +347,7 @@ export function createLocalTaskRequestFingerprint({
|
|
|
326
347
|
timeoutMs,
|
|
327
348
|
attachmentManifest,
|
|
328
349
|
scenarioPack,
|
|
350
|
+
workflowObservationBinding = null,
|
|
329
351
|
}) {
|
|
330
352
|
const normalizedModel = normalizeLocalTaskModel(model);
|
|
331
353
|
const manifest = Array.isArray(attachmentManifest)
|
|
@@ -338,6 +360,10 @@ export function createLocalTaskRequestFingerprint({
|
|
|
338
360
|
sha256: String(item.sha256 || "").toLowerCase(),
|
|
339
361
|
}))
|
|
340
362
|
: [];
|
|
363
|
+
const normalizedWorkflowObservationBinding = normalizeWorkflowObservationBinding(
|
|
364
|
+
workflowObservationBinding,
|
|
365
|
+
{ provider: backend },
|
|
366
|
+
);
|
|
341
367
|
const payload = {
|
|
342
368
|
instruction: String(instruction || "").trim(),
|
|
343
369
|
backend: String(backend || ""),
|
|
@@ -351,6 +377,9 @@ export function createLocalTaskRequestFingerprint({
|
|
|
351
377
|
timeout_ms: normalizeTimeoutMs(timeoutMs),
|
|
352
378
|
attachment_manifest: manifest,
|
|
353
379
|
scenario_pack: normalizeLocalTaskScenarioPack(scenarioPack),
|
|
380
|
+
...(normalizedWorkflowObservationBinding
|
|
381
|
+
? { workflow_observation_binding: normalizedWorkflowObservationBinding }
|
|
382
|
+
: {}),
|
|
354
383
|
};
|
|
355
384
|
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
356
385
|
}
|
|
@@ -362,6 +391,9 @@ export async function startLocalTask(
|
|
|
362
391
|
environment = process.env,
|
|
363
392
|
persist = persistLocalTask,
|
|
364
393
|
beforeProviderInvocation = null,
|
|
394
|
+
session = null,
|
|
395
|
+
apiOrigin = null,
|
|
396
|
+
observationFetch = globalThis.fetch,
|
|
365
397
|
} = {},
|
|
366
398
|
) {
|
|
367
399
|
if (!task || task.status !== "queued") throw new Error("Local Task must be queued.");
|
|
@@ -441,6 +473,14 @@ export async function startLocalTask(
|
|
|
441
473
|
// synchronous spawn boundary so a stopped task can never start Provider work.
|
|
442
474
|
if (task.stop_requested || task.status !== "running") return task;
|
|
443
475
|
|
|
476
|
+
const observationSender = createWorkflowObservationSender({
|
|
477
|
+
binding: task.workflow_observation_binding,
|
|
478
|
+
backend: task.backend,
|
|
479
|
+
session,
|
|
480
|
+
apiOrigin,
|
|
481
|
+
fetchImpl: observationFetch,
|
|
482
|
+
});
|
|
483
|
+
|
|
444
484
|
const runtimeTempBase = process.platform === "darwin" ? "/tmp" : tmpdir();
|
|
445
485
|
const runtimeTempRoot = await mkdtemp(
|
|
446
486
|
path.join(runtimeTempBase, `simy-local-task-${task.id}-`),
|
|
@@ -512,6 +552,7 @@ export async function startLocalTask(
|
|
|
512
552
|
stream: "stdout",
|
|
513
553
|
onLine,
|
|
514
554
|
onUsage,
|
|
555
|
+
onEvent: (event) => observationSender?.observe(event),
|
|
515
556
|
onTerminalError: (message) => {
|
|
516
557
|
task.error ||= message;
|
|
517
558
|
task.reason_code ||= "provider_execution_failed";
|
|
@@ -559,6 +600,9 @@ export async function startLocalTask(
|
|
|
559
600
|
(total, item) => total + providerTokenCount(item),
|
|
560
601
|
0,
|
|
561
602
|
);
|
|
603
|
+
if (observationSender) {
|
|
604
|
+
task.workflow_observation = await observationSender.flush();
|
|
605
|
+
}
|
|
562
606
|
|
|
563
607
|
let artifacts = [];
|
|
564
608
|
let scenarioAcceptance = null;
|
|
@@ -708,6 +752,9 @@ export function localTaskSnapshot(task, { includeEvents = true, includeLocalPath
|
|
|
708
752
|
provider_token_budget: task.provider_token_budget,
|
|
709
753
|
provider_tokens_used: task.provider_tokens_used,
|
|
710
754
|
token_usage: structuredClone(task.token_usage),
|
|
755
|
+
...(task.workflow_observation
|
|
756
|
+
? { workflow_observation: structuredClone(task.workflow_observation) }
|
|
757
|
+
: {}),
|
|
711
758
|
timeout_ms: task.timeout_ms,
|
|
712
759
|
created_at: task.created_at,
|
|
713
760
|
updated_at: task.updated_at,
|
|
@@ -723,6 +770,7 @@ export function localTaskSnapshot(task, { includeEvents = true, includeLocalPath
|
|
|
723
770
|
outputs_root_expected: task.outputs_root_expected,
|
|
724
771
|
idempotency_key_hash: task.idempotency_key_hash || null,
|
|
725
772
|
request_fingerprint: task.request_fingerprint || null,
|
|
773
|
+
workflow_observation_binding: task.workflow_observation_binding || null,
|
|
726
774
|
}
|
|
727
775
|
: {}),
|
|
728
776
|
};
|
|
@@ -912,6 +960,10 @@ export async function localTaskArtifact(task, artifactId) {
|
|
|
912
960
|
}
|
|
913
961
|
|
|
914
962
|
function restoreLocalTask(snapshot) {
|
|
963
|
+
const workflowObservationBinding = normalizeWorkflowObservationBinding(
|
|
964
|
+
snapshot.workflow_observation_binding,
|
|
965
|
+
{ provider: snapshot.backend },
|
|
966
|
+
);
|
|
915
967
|
return {
|
|
916
968
|
...snapshot,
|
|
917
969
|
outputs_root_expected:
|
|
@@ -925,6 +977,8 @@ function restoreLocalTask(snapshot) {
|
|
|
925
977
|
scenario_acceptance: snapshot.scenario_acceptance || null,
|
|
926
978
|
warnings: Array.isArray(snapshot.warnings) ? snapshot.warnings : [],
|
|
927
979
|
token_usage: snapshot.token_usage || { records: [] },
|
|
980
|
+
workflow_observation_binding: workflowObservationBinding,
|
|
981
|
+
workflow_observation: snapshot.workflow_observation || null,
|
|
928
982
|
child: null,
|
|
929
983
|
operation_promise: null,
|
|
930
984
|
stop_requested: false,
|
|
@@ -957,6 +1011,7 @@ function localTaskProviderInstruction(task) {
|
|
|
957
1011
|
: null;
|
|
958
1012
|
return [
|
|
959
1013
|
"You are the local executor for a SIMY Local Task, not an Agentic Loop.",
|
|
1014
|
+
buildWorkflowReportingInstruction(),
|
|
960
1015
|
"Complete this bounded task once. Do not create Git branches, commits, pull requests, audits, or automatic retries.",
|
|
961
1016
|
`Permission mode: ${task.permission_mode}.`,
|
|
962
1017
|
`Task workspace: ${task.workspace_path}`,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./pr-preparation.js";
|
|
17
17
|
import {
|
|
18
18
|
completionEvidenceChecks,
|
|
19
|
+
hasUserOutcomeProof,
|
|
19
20
|
normalizeVisualReview,
|
|
20
21
|
} from "./completion-contract.js";
|
|
21
22
|
|
|
@@ -340,6 +341,8 @@ export function evaluatePrReadiness(charter, attempt) {
|
|
|
340
341
|
readinessCheck("minimum_independent_binding",
|
|
341
342
|
Boolean(independentAudit?.evidence_identity_sha256 && independentAudit.evidence_identity_sha256 === independentAuditEvidenceIdentity(attempt)),
|
|
342
343
|
"Independent review must cover the current head, base, CI, skips, preparation rows, companion revisions, and minimum evidence."),
|
|
344
|
+
readinessCheck("user_outcome_proven", hasUserOutcomeProof(independentAudit?.coverage_review),
|
|
345
|
+
"Independent review must observe the original user outcome and cite its evidence."),
|
|
343
346
|
readinessCheck("implementation_gate", attempt.audit?.passed === true, "Implementation gate must pass."),
|
|
344
347
|
readinessCheck("pr_preparation_gate", preparation.passed, preparation.detail),
|
|
345
348
|
readinessCheck(
|
|
@@ -8,12 +8,12 @@ export const ACCEPTANCE_EVIDENCE_PATH = fileURLToPath(
|
|
|
8
8
|
export function buildChecklistContinuationInstruction({ auditor = false } = {}) {
|
|
9
9
|
return section("checklist_continuation_contract", [
|
|
10
10
|
`The table is a progress checkpoint, not the deliverable or a stopping condition. Read: ${ACCEPTANCE_EVIDENCE_PATH}`,
|
|
11
|
-
"
|
|
12
|
-
"After
|
|
11
|
+
"Maintain the complete 依頼チェックリスト with columns #, ご依頼, 現状, 判定 in the evidence record after chartering, each evaluated attempt, and in the final receipt. User-facing reporting follows workflow_reporting_contract; show the full table only when requested or when it is the most useful requested answer, never as mandatory commentary. Preserve stable criterion IDs, all rows and both applicable evidence layers. ✅ requires current valid passing evidence for every applicable layer; ❌ means an observed failure; 🟡 means pending, missing, stale, blocked or unverifiable evidence. Retain counts and highest proven state in that record.",
|
|
12
|
+
"After observing any non-green row, classify its cause and immediately take the next safe authorized action: implement missing behavior, repair a failure, exercise an untested path, collect missing evidence, or rerun stale verification. Re-observe the same criterion against the current candidate, run the validator, update the table, and repeat until every applicable requested outcome and gate for the authorized endpoint is verified. A next-action list without performing feasible actions is not completion. Never require another continue request merely because a table was shown.",
|
|
13
13
|
"For CI or other in-progress dependencies, use the available bounded wait/recheck or approved monitoring mechanism and maintain progress communication. Unchanged external state is not itself a blocker. Do not start duplicate jobs or busy-poll. Recheck current revision/results, repair authorized failures, and keep waiting only within the existing time, attempt and cost limits. If waiting cannot continue, report the concrete limit and explicit resumption condition; do not silently abandon it or promise an unscheduled follow-up.",
|
|
14
14
|
"If a row genuinely requires new authority, credentials, a user/product decision, an unavailable dependency or an exhausted safety budget, retain it as non-green and record affected IDs, evidence of the blocker, safe alternatives attempted, exact missing input, owner and resumption trigger. Continue independent in-scope work. Stop with blocked, best_available, failed or cancelled as appropriate, explicitly unfinished, not verified_complete. Never raise budgets, bypass a stop, or expand authority to force green.",
|
|
15
15
|
"Do not end successfully while a safe authorized repair, verification or bounded wait remains. All-green applies to the frozen authorized endpoint: PR-only work does not authorize merge/deploy, but a PR URL, an improvement PR, an issue, a narrower test or a future plan does not satisfy an unmet outcome. Never drop/merge away rows, weaken criteria, relabel applicable checks as not_applicable, recolor unknowns, or copy new digests onto old evidence to force green. Do not change the requested endpoint to match the work already completed.",
|
|
16
|
-
"Before the final response, independently reconcile the original request with all criterion IDs and current evidence, check remaining endpoint gates, and
|
|
16
|
+
"Before the final response, independently reconcile the original request with all criterion IDs and current evidence, check remaining endpoint gates, and preserve the full table and explicit disposition in the evidence record. Distinguish a partial artifact from verified completion and disclose material remaining outcomes in the requested response format; keep yellow/red counts in the record. A pr_ready_for_review or blocked controller state is not proof of overall completion. These instructions strengthen continuation and reporting; they do not add a background scheduler or remove controller limits.",
|
|
17
17
|
auditor
|
|
18
18
|
? "Remain read-only. Check that the executor performed feasible follow-up actions after non-green tables. Report premature stopping, omitted rows, unsupported green claims, or a blocker without evidence as findings, alongside exact criterion IDs and the necessary next action. Do not approve from a table's colors or the executor's completion wording."
|
|
19
19
|
: "Execute the continuation within this attempt instead of merely returning a yellow table or delegating feasible remaining work to the requester. Return control honestly at a genuine boundary; never fabricate completion evidence.",
|
|
@@ -4,6 +4,13 @@ import { cleanString, stringArray } from "./shared.js";
|
|
|
4
4
|
export const COMPLETION_CONTRACT_VERSION = 2;
|
|
5
5
|
const criterionIdPattern = /^[A-Za-z][A-Za-z0-9]*(?:[-_.][A-Za-z0-9]+)*$/;
|
|
6
6
|
|
|
7
|
+
export function hasUserOutcomeProof(coverage) {
|
|
8
|
+
const proof = coverage?.user_outcome;
|
|
9
|
+
return proof?.passed === true && Boolean(cleanString(proof.verification_method)) &&
|
|
10
|
+
Boolean(cleanString(proof.observed_result)) && Array.isArray(proof.evidence_refs) &&
|
|
11
|
+
proof.evidence_refs.some((ref) => Boolean(cleanString(ref)));
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
export function buildCompletionContract({
|
|
8
15
|
inherited,
|
|
9
16
|
requirement,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { budgetState, resolveRunBudgets } from "./budget.js";
|
|
11
11
|
import { classifyRisk } from "./risk.js";
|
|
12
12
|
import { buildCompletionContract } from "./completion-contract.js";
|
|
13
|
+
import { freezeObjective } from "./outcome-monitor.js";
|
|
13
14
|
import { defaultPrPreparationContract } from "./pr-preparation.js";
|
|
14
15
|
import {
|
|
15
16
|
DESKTOP_EXECUTION_TARGET,
|
|
@@ -56,11 +57,11 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
|
|
|
56
57
|
cleanString(charterContext.acceptance_criteria_source) === "explicit"
|
|
57
58
|
? "explicit"
|
|
58
59
|
: acceptanceCriteria.length > 0 &&
|
|
59
|
-
cleanString(charterContext.acceptance_criteria_source) === "
|
|
60
|
-
? "
|
|
60
|
+
cleanString(charterContext.acceptance_criteria_source) === "request"
|
|
61
|
+
? "request"
|
|
61
62
|
: acceptanceCriteria.length > 0
|
|
62
63
|
? "explicit"
|
|
63
|
-
: "
|
|
64
|
+
: "request";
|
|
64
65
|
const inheritedRequiredChecks = stringArray(charterContext.required_checks);
|
|
65
66
|
const inheritedNonGoals = stringArray(charterContext.non_goals);
|
|
66
67
|
const inheritedArtifacts = stringArray(charterContext.artifacts_required);
|
|
@@ -71,7 +72,7 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
|
|
|
71
72
|
acceptanceCriteria.length > 0
|
|
72
73
|
? acceptanceCriteria
|
|
73
74
|
: [
|
|
74
|
-
|
|
75
|
+
cleanString(request.requirement),
|
|
75
76
|
"Relevant tests pass and are recorded.",
|
|
76
77
|
`A pull request targets ${baseBranch}.`,
|
|
77
78
|
];
|
|
@@ -216,6 +217,7 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
|
|
|
216
217
|
updated_at: now,
|
|
217
218
|
};
|
|
218
219
|
snapshot.budget = budgetState(snapshot);
|
|
220
|
+
freezeObjective(snapshot.charter);
|
|
219
221
|
return snapshot;
|
|
220
222
|
}
|
|
221
223
|
|