@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 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. Mark the result `pr_ready_for_review` while GitHub checks or peer approval are pending.
380
- 6. Mark it `merge_ready` only after the observed PR head/base, CI checks, merge state,
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.15",
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: isSessionValid(session, Date.now(), apiOrigin),
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(
@@ -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.15";
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.minimum_matrix_sha256 &&
150
- attempt.independent_audit.minimum_matrix_sha256 === attempt.observed_evidence?.minimum_completion?.matrix_sha256),
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?.minimum_matrix_sha256 && independentAudit.minimum_matrix_sha256 === attempt.observed_evidence?.minimum_completion?.matrix_sha256),
321
- "Independent review must cover the current minimum evidence matrix."),
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),