@awak-app/simy-cli 0.5.15 → 0.5.16
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 +17 -2
- package/package.json +2 -2
- package/src/agent.js +33 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/README.md +17 -0
- package/src/codex-plugins/simy/hooks/connection-reminder.mjs +184 -0
- package/src/codex-plugins/simy/hooks/hooks.json +27 -0
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/SKILL.md +56 -0
- package/src/connection-discovery.js +49 -0
- package/src/orchestrator/audit.js +47 -5
- package/src/orchestrator/contract.js +2 -0
- package/src/orchestrator/evidence.js +64 -5
- package/src/orchestrator/independent-audit.js +10 -0
- package/src/orchestrator/index.js +7 -0
- package/src/orchestrator/instruction.js +21 -1
- package/src/orchestrator/loop.js +3 -0
- package/src/orchestrator/pr-preparation.js +420 -0
- package/src/orchestrator/result.js +6 -0
- package/src/runner.js +92 -1
- package/src/sqm/agent-skill.js +39 -7
package/README.md
CHANGED
|
@@ -42,6 +42,13 @@ SQM checks apply when you explicitly select `@simy`, invoke the SQM skill, or as
|
|
|
42
42
|
After installation, restart Codex and find and install **SIMY** in **Plugins**
|
|
43
43
|
before using `@simy` in a new task.
|
|
44
44
|
|
|
45
|
+
The plugin also includes a non-blocking connection reminder. At task startup,
|
|
46
|
+
resume, and prompt submission it reads the origin-scoped local discovery record
|
|
47
|
+
and checks the loopback CLI health endpoint. It warns once per disconnected
|
|
48
|
+
period in each Codex session, then rearms after the CLI reconnects. The hook does
|
|
49
|
+
not start or install the CLI, read session tokens, enable SQM, or send email.
|
|
50
|
+
Review and enable the plugin hooks through Codex's `/hooks` command.
|
|
51
|
+
|
|
45
52
|
After that first installation, the daemon checks installed workflows at startup
|
|
46
53
|
and every hour, including after a CLI automatic upgrade. It updates only existing
|
|
47
54
|
platform installations and refreshes an already enabled SIMY Codex plugin through
|
|
@@ -376,8 +383,16 @@ The local PR lifecycle is intentionally separate from deployment:
|
|
|
376
383
|
2. Require explicit acceptance criteria plus an approved design summary for high-risk work.
|
|
377
384
|
3. Run the coding executor and verify its result against local Git evidence.
|
|
378
385
|
4. Run a separate AI audit session without granting it an implementation role.
|
|
379
|
-
5.
|
|
380
|
-
|
|
386
|
+
5. Require eight revision-bound PR-preparation rows: latest base/head, current CI and
|
|
387
|
+
disclosed skips, exact companion revisions, E2E outcomes and zero residual fixtures,
|
|
388
|
+
security/privacy, browser/localization when applicable, final diff scope, and honest
|
|
389
|
+
residual/post-merge status. The CLI refreshes the remote base, requires it to be an
|
|
390
|
+
ancestor of the final head, and compares it with the PR base SHA.
|
|
391
|
+
Four additional conditional safeguards cover official-checklist ownership,
|
|
392
|
+
authoritative deployment-target mapping, live-test data safety, and saved-run/
|
|
393
|
+
checkpoint compatibility without forcing irrelevant work to execute them.
|
|
394
|
+
6. Mark the result `pr_ready_for_review` while GitHub checks or peer approval are pending.
|
|
395
|
+
7. Mark it `merge_ready` only after the observed PR head/base, CI checks, merge state,
|
|
381
396
|
and human approval all pass.
|
|
382
397
|
|
|
383
398
|
The Web UI can refresh review and CI evidence with
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
4
4
|
"description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js && npm run check:sqm",
|
|
41
41
|
"check:sqm": "node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
|
|
42
42
|
"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",
|
|
43
|
-
"check:syntax": "node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.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/minimum-completion.js && node --check ./src/codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-minimum-completion.mjs && 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/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-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",
|
|
43
|
+
"check:syntax": "node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.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/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-minimum-completion.mjs && 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/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-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",
|
|
44
44
|
"check:package": "node ./scripts/check-package-contents.js",
|
|
45
45
|
"build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
|
|
46
46
|
"build:macos-installer": "bash packaging/build-macos-arm64.sh --production",
|
package/src/agent.js
CHANGED
|
@@ -126,6 +126,10 @@ import {
|
|
|
126
126
|
createSqmLocalReviewApi,
|
|
127
127
|
createSqmLocalReviewWorker,
|
|
128
128
|
} from "./sqm/review-worker.js";
|
|
129
|
+
import {
|
|
130
|
+
publishConnectionDiscovery,
|
|
131
|
+
removeConnectionDiscovery,
|
|
132
|
+
} from "./connection-discovery.js";
|
|
129
133
|
|
|
130
134
|
const DEVICE_HEARTBEAT_INTERVAL_MS = 20_000;
|
|
131
135
|
|
|
@@ -510,14 +514,20 @@ export async function startAgent({
|
|
|
510
514
|
res.once("close", releaseUpdateWork);
|
|
511
515
|
}
|
|
512
516
|
if (req.method === "GET" && url.pathname === "/v1/health") {
|
|
517
|
+
const sessionValid = isSessionValid(session, Date.now(), apiOrigin);
|
|
518
|
+
const connection = connectionStatus.snapshot({ authenticated: sessionValid });
|
|
513
519
|
json(res, 200, {
|
|
514
520
|
ok: true,
|
|
515
521
|
cli_version: CLI_VERSION,
|
|
516
522
|
api_contract_version: CLI_API_CONTRACT_VERSION,
|
|
517
523
|
daemon,
|
|
518
524
|
web_origin: apiOrigin,
|
|
519
|
-
session_valid:
|
|
525
|
+
session_valid: sessionValid,
|
|
520
526
|
session_expires_at: session?.expires_at ?? null,
|
|
527
|
+
connection: {
|
|
528
|
+
state: connection.state,
|
|
529
|
+
lastSuccessAt: connection.lastSuccessAt,
|
|
530
|
+
},
|
|
521
531
|
cli_update: updateManager?.snapshot?.() ?? null,
|
|
522
532
|
});
|
|
523
533
|
return;
|
|
@@ -1566,6 +1576,14 @@ export async function startAgent({
|
|
|
1566
1576
|
await new Promise((resolve) => server.listen(requestedPort, "127.0.0.1", resolve));
|
|
1567
1577
|
const address = server.address();
|
|
1568
1578
|
port = typeof address === "object" && address ? address.port : requestedPort;
|
|
1579
|
+
let connectionDiscovery = null;
|
|
1580
|
+
try {
|
|
1581
|
+
connectionDiscovery = await (
|
|
1582
|
+
dependencies.publishConnectionDiscovery || publishConnectionDiscovery
|
|
1583
|
+
)({ webOrigin: apiOrigin, port, sessionRoot });
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
reportConnectionDiscoveryError(error, quiet);
|
|
1586
|
+
}
|
|
1569
1587
|
if (!quiet) {
|
|
1570
1588
|
console.log(`SIMY local agent listening on http://127.0.0.1:${port}`);
|
|
1571
1589
|
console.log(`SIMY Web host: ${apiOrigin}`);
|
|
@@ -1598,6 +1616,13 @@ export async function startAgent({
|
|
|
1598
1616
|
updateManager?.stop?.();
|
|
1599
1617
|
await sqmLocalReviewWorker?.close?.();
|
|
1600
1618
|
await durableLocalTaskWorker?.close?.();
|
|
1619
|
+
try {
|
|
1620
|
+
await (dependencies.removeConnectionDiscovery || removeConnectionDiscovery)(
|
|
1621
|
+
connectionDiscovery,
|
|
1622
|
+
);
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
reportConnectionDiscoveryError(error, quiet);
|
|
1625
|
+
}
|
|
1601
1626
|
registry.close();
|
|
1602
1627
|
localTaskRegistry.close();
|
|
1603
1628
|
})();
|
|
@@ -1850,6 +1875,13 @@ function reportHeartbeatError(error, quiet) {
|
|
|
1850
1875
|
);
|
|
1851
1876
|
}
|
|
1852
1877
|
|
|
1878
|
+
function reportConnectionDiscoveryError(error, quiet) {
|
|
1879
|
+
if (quiet) return;
|
|
1880
|
+
console.error(
|
|
1881
|
+
`SIMY connection reminder metadata could not be updated: ${error instanceof Error ? error.message : String(error)}`,
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1853
1885
|
function reportDurableLocalTaskError(error, quiet) {
|
|
1854
1886
|
if (quiet) return;
|
|
1855
1887
|
console.error(
|
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.5.
|
|
16
|
+
export const CLI_VERSION = "0.5.16";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# SIMY connection reminder
|
|
2
|
+
|
|
3
|
+
The SIMY Codex plugin checks the origin-scoped local CLI connection when a task
|
|
4
|
+
starts or resumes and when a prompt is submitted. The hook only reads the local
|
|
5
|
+
daemon discovery record and its `/v1/health` endpoint. It never starts, installs,
|
|
6
|
+
signs in to, or changes SIMY CLI, and it never sends email.
|
|
7
|
+
|
|
8
|
+
The reminder is non-blocking. It stays silent while the latest cloud heartbeat is
|
|
9
|
+
fresh, warns once for each disconnected period in a Codex session, and rearms
|
|
10
|
+
after a successful probe. State is stored under `PLUGIN_DATA`; the fallback is
|
|
11
|
+
the SIMY home directory. Installing the plugin does not trust its hooks
|
|
12
|
+
automatically: review and enable them through Codex's `/hooks` command.
|
|
13
|
+
|
|
14
|
+
When `SIMY_WEB_ORIGIN` or `SIMY_API_ORIGIN` is set, only that origin is accepted.
|
|
15
|
+
Otherwise the hook checks at most 16 origin-keyed discovery records and accepts
|
|
16
|
+
the first live, authenticated connection with a fresh cloud heartbeat. It never
|
|
17
|
+
scans ports or reads stored session tokens.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
|
|
9
|
+
const HEARTBEAT_FRESHNESS_MS = 90_000;
|
|
10
|
+
const PROBE_TIMEOUT_MS = 750;
|
|
11
|
+
const MAX_INPUT_BYTES = 64 * 1024;
|
|
12
|
+
const MAX_DISCOVERY_RECORDS = 16;
|
|
13
|
+
|
|
14
|
+
export async function runConnectionReminder({
|
|
15
|
+
env = process.env,
|
|
16
|
+
input = {},
|
|
17
|
+
fetchImpl = globalThis.fetch,
|
|
18
|
+
now = () => Date.now(),
|
|
19
|
+
} = {}) {
|
|
20
|
+
const explicitWebOrigin = resolveExplicitWebOrigin(env);
|
|
21
|
+
const sessionRoot = env.SIMY_HOME?.trim() || join(homedir(), ".simy");
|
|
22
|
+
const pluginData = env.PLUGIN_DATA?.trim() || join(sessionRoot, "plugin-data", "simy");
|
|
23
|
+
const sessionId = String(input.session_id || input.sessionId || env.CODEX_THREAD_ID || "unknown");
|
|
24
|
+
const statePath = reminderStatePath(pluginData, explicitWebOrigin || "any-origin", sessionId);
|
|
25
|
+
const connected = explicitWebOrigin
|
|
26
|
+
? await probeConnection({ webOrigin: explicitWebOrigin, sessionRoot, fetchImpl, now })
|
|
27
|
+
: await probeAnyConnection({ sessionRoot, fetchImpl, now });
|
|
28
|
+
const previous = await readState(statePath);
|
|
29
|
+
if (connected) {
|
|
30
|
+
if (previous?.disconnected) await writeState(statePath, { disconnected: false }).catch(() => {});
|
|
31
|
+
return { continue: true };
|
|
32
|
+
}
|
|
33
|
+
if (previous?.disconnected) return { continue: true };
|
|
34
|
+
await writeState(statePath, { disconnected: true }).catch(() => {});
|
|
35
|
+
return { continue: true, systemMessage: disconnectedMessage(env) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function probeConnection({ webOrigin, sessionRoot, fetchImpl, now }) {
|
|
39
|
+
let discovery;
|
|
40
|
+
try {
|
|
41
|
+
discovery = JSON.parse(await readFile(discoveryPath(webOrigin, sessionRoot), "utf8"));
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
discovery?.schema_version !== 1 ||
|
|
47
|
+
discovery?.web_origin !== webOrigin ||
|
|
48
|
+
!Number.isInteger(discovery?.port) ||
|
|
49
|
+
discovery.port < 1 ||
|
|
50
|
+
discovery.port > 65_535
|
|
51
|
+
) return false;
|
|
52
|
+
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
55
|
+
timeout.unref?.();
|
|
56
|
+
try {
|
|
57
|
+
const response = await fetchImpl(`http://127.0.0.1:${discovery.port}/v1/health`, {
|
|
58
|
+
headers: { Origin: webOrigin },
|
|
59
|
+
redirect: "error",
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) return false;
|
|
63
|
+
const health = await response.json();
|
|
64
|
+
const lastSuccess = Date.parse(health?.connection?.lastSuccessAt || "");
|
|
65
|
+
const sampledAt = typeof now === "function" ? now() : now;
|
|
66
|
+
return Boolean(
|
|
67
|
+
health?.ok === true &&
|
|
68
|
+
health?.web_origin === webOrigin &&
|
|
69
|
+
health?.session_valid === true &&
|
|
70
|
+
health?.connection?.state === "online" &&
|
|
71
|
+
Number.isFinite(lastSuccess) &&
|
|
72
|
+
sampledAt - lastSuccess >= 0 &&
|
|
73
|
+
sampledAt - lastSuccess <= HEARTBEAT_FRESHNESS_MS,
|
|
74
|
+
);
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function probeAnyConnection({ sessionRoot, fetchImpl, now }) {
|
|
83
|
+
let entries;
|
|
84
|
+
try {
|
|
85
|
+
entries = (await readdir(join(sessionRoot, "connections")))
|
|
86
|
+
.filter((entry) => entry.endsWith(".json"))
|
|
87
|
+
.sort()
|
|
88
|
+
.slice(0, MAX_DISCOVERY_RECORDS);
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (!entries.length) return false;
|
|
93
|
+
const probes = entries.map(async (entry) => {
|
|
94
|
+
try {
|
|
95
|
+
const record = JSON.parse(await readFile(join(sessionRoot, "connections", entry), "utf8"));
|
|
96
|
+
const webOrigin = normalizeOrigin(record?.web_origin);
|
|
97
|
+
if (!webOrigin || entry !== `${sha256(webOrigin)}.json`) return false;
|
|
98
|
+
return probeConnection({ webOrigin, sessionRoot, fetchImpl, now });
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return (await Promise.all(probes)).some(Boolean);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveExplicitWebOrigin(env) {
|
|
107
|
+
const raw = env.SIMY_WEB_ORIGIN || env.SIMY_API_ORIGIN;
|
|
108
|
+
if (!raw) return null;
|
|
109
|
+
return normalizeOrigin(raw) || "invalid-origin";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeOrigin(raw) {
|
|
113
|
+
try {
|
|
114
|
+
const url = new URL(raw);
|
|
115
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if ((url.pathname && url.pathname !== "/") || url.search || url.hash) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return url.origin;
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function discoveryPath(webOrigin, sessionRoot) {
|
|
128
|
+
return join(sessionRoot, "connections", `${sha256(webOrigin)}.json`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function reminderStatePath(pluginData, webOrigin, sessionId) {
|
|
132
|
+
return join(pluginData, "connection-reminders", `${sha256(`${webOrigin}\0${sessionId}`)}.json`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function readState(target) {
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(await readFile(target, "utf8"));
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function writeState(target, state) {
|
|
144
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
145
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
146
|
+
await writeFile(temporary, `${JSON.stringify(state)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
147
|
+
await rename(temporary, target);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function disconnectedMessage(env) {
|
|
151
|
+
const locale = String(env.LC_ALL || env.LC_MESSAGES || env.LANG || "").toLowerCase();
|
|
152
|
+
return locale.startsWith("ja")
|
|
153
|
+
? "SIMY CLI に接続できません。SIMY CLI を起動し、ログイン状態をご確認ください。作業はこのまま続けられます。"
|
|
154
|
+
: "SIMY CLI is not connected. Start SIMY CLI and check that you are signed in. You can keep working in the meantime.";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function sha256(value) {
|
|
158
|
+
return createHash("sha256").update(value).digest("hex");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function readHookInput(stream = process.stdin) {
|
|
162
|
+
let raw = "";
|
|
163
|
+
for await (const chunk of stream) {
|
|
164
|
+
raw += String(chunk);
|
|
165
|
+
if (Buffer.byteLength(raw) > MAX_INPUT_BYTES) return {};
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const parsed = JSON.parse(raw || "{}");
|
|
169
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
170
|
+
} catch {
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function main() {
|
|
176
|
+
try {
|
|
177
|
+
const result = await runConnectionReminder({ input: await readHookInput() });
|
|
178
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
179
|
+
} catch {
|
|
180
|
+
process.stdout.write(`${JSON.stringify({ continue: true })}\n`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) await main();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup|resume",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"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', 'connection-reminder.mjs')).href); await hook.main();\"",
|
|
10
|
+
"timeout": 3
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"UserPromptSubmit": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"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', 'connection-reminder.mjs')).href); await hook.main();\"",
|
|
21
|
+
"timeout": 3
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -416,6 +416,52 @@ After authority for the required remote changes exists, record reproduction,
|
|
|
416
416
|
tests, evidence, companion revisions, skipped checks, and remaining gates in the
|
|
417
417
|
Draft PR so the exact remote candidate can be inspected. Before Ready or merge,
|
|
418
418
|
pass the reviews and explicitly opted-in SQM gates required by the frozen plan.
|
|
419
|
+
The CLI's structured PR-preparation contract uses these stable rows for every new
|
|
420
|
+
Delivery Loop run: `current_base_and_head`, `current_ci_and_skips`,
|
|
421
|
+
`exact_revision_integration`, `e2e_outcome_and_cleanup`,
|
|
422
|
+
`security_and_privacy`, `ui_and_i18n`, `final_diff_and_scope`, and
|
|
423
|
+
`residual_status_disclosure`. A required row needs a concrete observation,
|
|
424
|
+
controller-verified evidence references, and the current full head SHA. Mark a
|
|
425
|
+
row not applicable only with a boundary reason, an authoritative source,
|
|
426
|
+
verified evidence, and the current head binding; failed, unavailable, stale,
|
|
427
|
+
skipped, or time-limited checks remain non-green. Exact-revision rows identify
|
|
428
|
+
each companion repository and full SHA, cleanup reports a numeric residual count
|
|
429
|
+
of zero, and security/privacy and UI/i18n rows carry structured boundary and
|
|
430
|
+
browser evidence rather than an unsupported prose claim.
|
|
431
|
+
|
|
432
|
+
Four additional safeguards are always present and are required only when their
|
|
433
|
+
boundary applies: `official_checklist_ownership`, `deployment_target_mapping`,
|
|
434
|
+
`live_data_safety`, and `resume_compatibility`. Non-applicability uses the same
|
|
435
|
+
current-head-bound verified-evidence rule as the eight stable rows. For checklist
|
|
436
|
+
or chat rendering, normalize heading levels 1 through 6, leading whitespace, and
|
|
437
|
+
trailing hashes; allow exactly one controller/validator-owned official checklist
|
|
438
|
+
and label model-generated lookalikes unverified. For release work, derive the
|
|
439
|
+
verification environment from an authoritative repository/branch deployment
|
|
440
|
+
topology and record how the exact deployed revision will be observed. For shared
|
|
441
|
+
or live tests, freeze a no-persistence, scoped-fixture, or explicitly authorized
|
|
442
|
+
production-write policy and prove residual test data is zero. When loop state,
|
|
443
|
+
derived views, or checklist rendering changes, verify normal and checkpoint
|
|
444
|
+
resume, classify paid resume as supported or not supported from verified
|
|
445
|
+
authoritative evidence, exercise paid resume when supported, and prove
|
|
446
|
+
derived-view recomputation and preservation of artifact content.
|
|
447
|
+
|
|
448
|
+
Immediately before promotion, fetch the remote base and require it to be an
|
|
449
|
+
ancestor of the final head and identical to the PR's base SHA. After any conflict
|
|
450
|
+
resolution, base/head/companion/manifest change, rerun affected CI, integration,
|
|
451
|
+
E2E, security, privacy, browser, and cleanup checks. Pin all companion SHAs and
|
|
452
|
+
verify the combined path. Inspect scenario outcomes, not only job status, and
|
|
453
|
+
prove cleanup left zero residual fixtures. Document every intentionally skipped
|
|
454
|
+
GitHub check with its reason and impact; never describe a skipped full/live job
|
|
455
|
+
as executed E2E. For changed trust boundaries, check that arbitrary error text,
|
|
456
|
+
email addresses, URLs, identifiers, credentials, and user data cannot leak into
|
|
457
|
+
logs, breadcrumbs, or telemetry. For UI or localization changes, inspect the
|
|
458
|
+
actual locale/browser, overflow, console/network state, and screenshots.
|
|
459
|
+
|
|
460
|
+
Before asking for review, report the exact head and companion SHAs, current base,
|
|
461
|
+
tests and CI, skipped checks, remaining uncertainty, and post-merge migration,
|
|
462
|
+
deployment, scheduler, smoke, monitoring, or publication work. `DIRTY`, `BEHIND`,
|
|
463
|
+
stale exact-SHA evidence, a cleanup failure, or missing required CI blocks Ready.
|
|
464
|
+
Do not let an earlier message or receipt override freshly observed GitHub state.
|
|
419
465
|
Rerun a review or SQM gate only when its bound head,
|
|
420
466
|
base, diff, specification, policy, evidence, environment or SQM bundle changed,
|
|
421
467
|
or when a PR-only check became applicable. Otherwise retain the verified receipt
|
|
@@ -463,10 +509,20 @@ refresh, session return, delayed responses, follow-up lineage, authorization,
|
|
|
463
509
|
and artifact download/open checks when applicable. Capture screenshots and
|
|
464
510
|
video when visible or timing-dependent behavior matters.
|
|
465
511
|
|
|
512
|
+
Resolve that environment from the authoritative deployment topology for the
|
|
513
|
+
repository and branch; never assume a conventional environment name. Before the
|
|
514
|
+
journey, freeze its live-data write policy. Prefer non-persisting test input; when
|
|
515
|
+
a scoped fixture or authorized production write is necessary, bind it to the run,
|
|
516
|
+
reconcile ambiguous effects, clean it up, and independently prove the residual
|
|
517
|
+
count is zero.
|
|
518
|
+
|
|
466
519
|
If this fails, retain `merged` and `deployed_exact_revision`; do not reset
|
|
467
520
|
history or claim `done`. Save the exact revision, environment, input, Run ID,
|
|
468
521
|
timestamps, visible state, network evidence, and logs, then add a boundary
|
|
469
522
|
regression before re-entering implementation.
|
|
523
|
+
After the repair is deployed, repeat the same failing input and journey in the
|
|
524
|
+
same mapped environment against the newly observed exact revision before
|
|
525
|
+
advancing to `live_verified`.
|
|
470
526
|
|
|
471
527
|
After the frozen observation window, evaluate the production outcome and guardrail
|
|
472
528
|
measures for the same experience IDs. A functioning deployment is not successful
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
|
|
6
|
+
import { normalizeWebOrigin } from "./web-origin.js";
|
|
7
|
+
|
|
8
|
+
export function connectionDiscoveryPath(webOrigin, sessionRoot) {
|
|
9
|
+
const origin = normalizeWebOrigin(webOrigin);
|
|
10
|
+
const originHash = createHash("sha256").update(origin).digest("hex");
|
|
11
|
+
return join(sessionRoot || defaultSessionRoot(), "connections", `${originHash}.json`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function publishConnectionDiscovery({ webOrigin, port, sessionRoot }) {
|
|
15
|
+
const origin = normalizeWebOrigin(webOrigin);
|
|
16
|
+
const target = connectionDiscoveryPath(origin, sessionRoot || defaultSessionRoot());
|
|
17
|
+
const instanceId = randomBytes(16).toString("hex");
|
|
18
|
+
const record = {
|
|
19
|
+
schema_version: 1,
|
|
20
|
+
web_origin: origin,
|
|
21
|
+
port,
|
|
22
|
+
pid: process.pid,
|
|
23
|
+
instance_id: instanceId,
|
|
24
|
+
started_at: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
27
|
+
const temporary = `${target}.${process.pid}.${instanceId}.tmp`;
|
|
28
|
+
await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, {
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
mode: 0o600,
|
|
31
|
+
});
|
|
32
|
+
await rename(temporary, target);
|
|
33
|
+
return { path: target, instanceId };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function defaultSessionRoot() {
|
|
37
|
+
return process.env.SIMY_HOME?.trim() || join(homedir(), ".simy");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function removeConnectionDiscovery(discovery) {
|
|
41
|
+
if (!discovery?.path || !discovery.instanceId) return;
|
|
42
|
+
try {
|
|
43
|
+
const current = JSON.parse(await readFile(discovery.path, "utf8"));
|
|
44
|
+
if (current?.instance_id !== discovery.instanceId) return;
|
|
45
|
+
await rm(discovery.path, { force: true });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error?.code !== "ENOENT" && !(error instanceof SyntaxError)) throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
stringArray,
|
|
10
10
|
} from "./shared.js";
|
|
11
11
|
import { evaluateRetryProblemSolving } from "./problem-solving.js";
|
|
12
|
+
import {
|
|
13
|
+
evaluatePrPreparation,
|
|
14
|
+
independentAuditEvidenceIdentity,
|
|
15
|
+
skippedChecksDisclosure,
|
|
16
|
+
} from "./pr-preparation.js";
|
|
12
17
|
import {
|
|
13
18
|
completionEvidenceChecks,
|
|
14
19
|
normalizeVisualReview,
|
|
@@ -132,6 +137,17 @@ export async function auditAttempt(charter, attempt, { previousAttempt = null, s
|
|
|
132
137
|
}
|
|
133
138
|
attempt.retry_problem_solving = retryProblemSolving;
|
|
134
139
|
|
|
140
|
+
const prPreparation = evaluatePrPreparation(charter, attempt);
|
|
141
|
+
for (const preparationCheck of prPreparation.checks) {
|
|
142
|
+
addCheck(
|
|
143
|
+
`pr_preparation_${preparationCheck.id}`,
|
|
144
|
+
preparationCheck.label,
|
|
145
|
+
preparationCheck.passed,
|
|
146
|
+
preparationCheck.detail,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
attempt.pr_preparation_gate = prPreparation;
|
|
150
|
+
|
|
135
151
|
for (const check of [...completionEvidenceChecks(charter, attempt), minimumCompletionCheck(charter, attempt)]) {
|
|
136
152
|
addCheck(
|
|
137
153
|
check.id,
|
|
@@ -146,9 +162,9 @@ export async function auditAttempt(charter, attempt, { previousAttempt = null, s
|
|
|
146
162
|
|
|
147
163
|
if (attempt.independent_audit) {
|
|
148
164
|
addCheck("minimum_review_current", "Independent review covers the current evidence",
|
|
149
|
-
Boolean(attempt.independent_audit.
|
|
150
|
-
attempt.independent_audit.
|
|
151
|
-
"Changed minimum evidence requires a fresh independent review.");
|
|
165
|
+
Boolean(attempt.independent_audit.evidence_identity_sha256 &&
|
|
166
|
+
attempt.independent_audit.evidence_identity_sha256 === independentAuditEvidenceIdentity(attempt)),
|
|
167
|
+
"Changed head, base, CI, skip disclosure, PR preparation, companion revision, or minimum evidence requires a fresh independent review.");
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
const evidenceRequired = charter.expected_evidence.includes("ui_evidence_path");
|
|
@@ -314,12 +330,18 @@ export function evaluatePrReadiness(charter, attempt) {
|
|
|
314
330
|
const local = attempt.observed_evidence?.local || {};
|
|
315
331
|
const independentAudit = attempt.independent_audit;
|
|
316
332
|
const minimum = minimumCompletionCheck(charter, attempt);
|
|
333
|
+
const preparation = evaluatePrPreparation(charter, attempt);
|
|
334
|
+
const skippedDisclosure = skippedChecksDisclosure(
|
|
335
|
+
github.checks,
|
|
336
|
+
attempt.skipped_checks,
|
|
337
|
+
);
|
|
317
338
|
const checks = [
|
|
318
339
|
readinessCheck(minimum.id, minimum.passed, minimum.detail),
|
|
319
340
|
readinessCheck("minimum_independent_binding",
|
|
320
|
-
Boolean(independentAudit?.
|
|
321
|
-
"Independent review must cover the current minimum evidence
|
|
341
|
+
Boolean(independentAudit?.evidence_identity_sha256 && independentAudit.evidence_identity_sha256 === independentAuditEvidenceIdentity(attempt)),
|
|
342
|
+
"Independent review must cover the current head, base, CI, skips, preparation rows, companion revisions, and minimum evidence."),
|
|
322
343
|
readinessCheck("implementation_gate", attempt.audit?.passed === true, "Implementation gate must pass."),
|
|
344
|
+
readinessCheck("pr_preparation_gate", preparation.passed, preparation.detail),
|
|
323
345
|
readinessCheck(
|
|
324
346
|
"independent_ai_audit",
|
|
325
347
|
independentAudit?.passed === true,
|
|
@@ -361,6 +383,21 @@ export function evaluatePrReadiness(charter, attempt) {
|
|
|
361
383
|
github.base_branch === charter.base_branch,
|
|
362
384
|
`Expected ${charter.base_branch}; GitHub reports ${github.base_branch || "missing"}.`,
|
|
363
385
|
),
|
|
386
|
+
readinessCheck(
|
|
387
|
+
"base_ref_refreshed",
|
|
388
|
+
local.base_ref_refreshed === true,
|
|
389
|
+
local.base_ref_error || "The remote base ref was not refreshed before readiness evaluation.",
|
|
390
|
+
),
|
|
391
|
+
readinessCheck(
|
|
392
|
+
"latest_base_integrated",
|
|
393
|
+
local.base_is_ancestor === true,
|
|
394
|
+
`Latest ${charter.base_branch} ${local.base_sha || "unknown"} is not an ancestor of local HEAD ${local.head_sha || "unknown"}.`,
|
|
395
|
+
),
|
|
396
|
+
readinessCheck(
|
|
397
|
+
"github_base_matches_fetched_base",
|
|
398
|
+
Boolean(github.base_sha && local.base_sha && github.base_sha === local.base_sha),
|
|
399
|
+
`GitHub base ${github.base_sha || "unknown"}; fetched base ${local.base_sha || "unknown"}.`,
|
|
400
|
+
),
|
|
364
401
|
readinessCheck(
|
|
365
402
|
"pr_mergeable",
|
|
366
403
|
github.mergeable === "MERGEABLE" && github.merge_state_status === "CLEAN",
|
|
@@ -368,6 +405,11 @@ export function evaluatePrReadiness(charter, attempt) {
|
|
|
368
405
|
),
|
|
369
406
|
readinessCheck("status_checks_present", github.checks_present === true, "No PR status checks found."),
|
|
370
407
|
readinessCheck("status_checks_passing", github.checks_passing === true, "PR checks are pending or failing."),
|
|
408
|
+
readinessCheck(
|
|
409
|
+
"skipped_checks_disclosed",
|
|
410
|
+
skippedDisclosure.passed,
|
|
411
|
+
skippedDisclosure.detail,
|
|
412
|
+
),
|
|
371
413
|
readinessCheck(
|
|
372
414
|
"required_checks_present",
|
|
373
415
|
github.required_checks_present === true,
|
|
@@ -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 { defaultPrPreparationContract } from "./pr-preparation.js";
|
|
13
14
|
import {
|
|
14
15
|
DESKTOP_EXECUTION_TARGET,
|
|
15
16
|
normalizeDesktopExecutionTarget,
|
|
@@ -174,6 +175,7 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
|
|
|
174
175
|
nonGoals: finalNonGoals,
|
|
175
176
|
expectedEvidence,
|
|
176
177
|
}),
|
|
178
|
+
pr_preparation_contract: defaultPrPreparationContract(),
|
|
177
179
|
thread_state: {
|
|
178
180
|
original_request:
|
|
179
181
|
cleanString(charterContext.original_request) || cleanString(request.requirement),
|