@awak-app/simy-cli 0.5.15 → 0.5.17
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 +78 -15
- package/package.json +2 -2
- package/src/agent.js +33 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +5 -6
- 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/connection-discovery.js +49 -0
- package/src/console/app.js +2 -2
- package/src/orchestrator/audit.js +47 -5
- package/src/orchestrator/budget.js +1 -1
- package/src/orchestrator/checklist-continuation.js +21 -0
- package/src/orchestrator/completion-contract.js +1 -1
- package/src/orchestrator/contract.js +2 -0
- package/src/{codex-plugins/simy/skills/agentic-delivery-loop/SKILL.md → orchestrator/delivery/WORKFLOW.md} +121 -13
- package/src/{codex-plugins/simy/skills/agentic-delivery-loop → orchestrator/delivery}/references/acceptance-evidence.md +27 -3
- package/src/{codex-plugins/simy/skills/agentic-delivery-loop → orchestrator/delivery}/references/change-boundary-matrix.md +1 -1
- package/src/{codex-plugins/simy/skills/agentic-delivery-loop → orchestrator/delivery}/references/delivery-levels.md +78 -27
- package/src/{codex-plugins/simy/skills/agentic-delivery-loop → orchestrator/delivery}/references/minimum-completion.md +3 -3
- package/src/orchestrator/delivery/references/pr-closure.md +203 -0
- package/src/{codex-plugins/simy/skills/agentic-delivery-loop → orchestrator/delivery}/references/state-and-receipts.md +4 -1
- package/src/orchestrator/delivery/references/ux-specification.md +98 -0
- package/src/{codex-plugins/simy/skills/agentic-delivery-loop → orchestrator/delivery}/scripts/production-contract.mjs +1 -1
- package/src/orchestrator/delivery-level.js +19 -0
- package/src/orchestrator/evidence.js +64 -5
- package/src/orchestrator/independent-audit.js +18 -0
- package/src/orchestrator/index.js +7 -0
- package/src/orchestrator/instruction.js +32 -3
- package/src/orchestrator/loop.js +4 -1
- package/src/orchestrator/minimum-completion.js +2 -2
- package/src/orchestrator/pr-closure.js +26 -0
- package/src/orchestrator/pr-preparation.js +420 -0
- package/src/orchestrator/presentation.js +2 -2
- package/src/orchestrator/result.js +8 -2
- package/src/orchestrator/ux-specification.js +20 -0
- package/src/runner.js +104 -1
- package/src/sqm/agent-skill.js +39 -7
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/agents/openai.yaml +0 -5
- /package/src/{codex-plugins/simy/skills/agentic-delivery-loop → orchestrator/delivery}/references/chrome-user-verification.md +0 -0
- /package/src/{codex-plugins/simy/skills/agentic-delivery-loop → orchestrator/delivery}/references/cross-repository-control.md +0 -0
- /package/src/{codex-plugins/simy/skills/agentic-delivery-loop → orchestrator/delivery}/references/local-sqm-evidence.md +0 -0
- /package/src/{codex-plugins/simy/skills/agentic-delivery-loop → orchestrator/delivery}/references/shared-system-specifications.md +0 -0
- /package/src/{codex-plugins/simy/skills/agentic-delivery-loop → orchestrator/delivery}/scripts/check-acceptance.mjs +0 -0
- /package/src/{codex-plugins/simy/skills/agentic-delivery-loop → orchestrator/delivery}/scripts/check-minimum-completion.mjs +0 -0
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
|
|
@@ -49,6 +56,11 @@ platform installations and refreshes an already enabled SIMY Codex plugin throug
|
|
|
49
56
|
receive a fresh cache. Use a new Codex task to pick up updated skills; running
|
|
50
57
|
tasks are not restarted. Missing or disabled plugins are not enabled automatically.
|
|
51
58
|
Failed refreshes retry on the next check without blocking daemon startup.
|
|
59
|
+
Delivery Loop is a SIMY Web/CLI runtime workflow, not a local Codex skill.
|
|
60
|
+
The plugin ships only SQM and PR review. Updating to this release removes the
|
|
61
|
+
previously bundled Delivery Loop from managed plugin sources and refreshes the
|
|
62
|
+
enabled cache. Older running CLI releases can restore their old bundled skill
|
|
63
|
+
until upgraded; deleting a local skill alone does not change their updater.
|
|
52
64
|
`--no-auto-update` or `SIMY_AUTO_UPDATE=0` disables these background updates too;
|
|
53
65
|
`--no-workflow-prompt` only suppresses the initial installation prompt.
|
|
54
66
|
|
|
@@ -267,7 +279,7 @@ is not copied into the recovery contract.
|
|
|
267
279
|
## Provider token budget
|
|
268
280
|
|
|
269
281
|
Each run accepts a configurable `token_budget` (or the explicit
|
|
270
|
-
`provider_token_budget` alias) from 1 to
|
|
282
|
+
`provider_token_budget` alias) from 1 to 15,000,000 Provider tokens. The default
|
|
271
283
|
is 250,000. Provider tokens are the Codex or Claude Code usage reported for the
|
|
272
284
|
run; they are a safety limit and are not billed as SIMY tokens.
|
|
273
285
|
|
|
@@ -277,6 +289,12 @@ limit. SIMY Web can edit that suggestion and call
|
|
|
277
289
|
`provider_token_budget`. The CLI persists the higher limit and resumes the same
|
|
278
290
|
run. SIMY token consumption remains a separate platform billing record.
|
|
279
291
|
|
|
292
|
+
An in-flight provider call can report usage above the previous limit. Recovery
|
|
293
|
+
keeps that full usage history, including reported cached input; it never resets
|
|
294
|
+
the counter or increases the limit automatically. An explicitly approved higher
|
|
295
|
+
limit must exceed usage already recorded and remain within the finite cap.
|
|
296
|
+
Retry limits and all completion gates still apply after a budget increase.
|
|
297
|
+
|
|
280
298
|
## SQM local checks
|
|
281
299
|
|
|
282
300
|
SQM deliberately has two identity-bound stages:
|
|
@@ -376,8 +394,16 @@ The local PR lifecycle is intentionally separate from deployment:
|
|
|
376
394
|
2. Require explicit acceptance criteria plus an approved design summary for high-risk work.
|
|
377
395
|
3. Run the coding executor and verify its result against local Git evidence.
|
|
378
396
|
4. Run a separate AI audit session without granting it an implementation role.
|
|
379
|
-
5.
|
|
380
|
-
|
|
397
|
+
5. Require eight revision-bound PR-preparation rows: latest base/head, current CI and
|
|
398
|
+
disclosed skips, exact companion revisions, E2E outcomes and zero residual fixtures,
|
|
399
|
+
security/privacy, browser/localization when applicable, final diff scope, and honest
|
|
400
|
+
residual/post-merge status. The CLI refreshes the remote base, requires it to be an
|
|
401
|
+
ancestor of the final head, and compares it with the PR base SHA.
|
|
402
|
+
Four additional conditional safeguards cover official-checklist ownership,
|
|
403
|
+
authoritative deployment-target mapping, live-test data safety, and saved-run/
|
|
404
|
+
checkpoint compatibility without forcing irrelevant work to execute them.
|
|
405
|
+
6. Mark the result `pr_ready_for_review` while GitHub checks or peer approval are pending.
|
|
406
|
+
7. Mark it `merge_ready` only after the observed PR head/base, CI checks, merge state,
|
|
381
407
|
and human approval all pass.
|
|
382
408
|
|
|
383
409
|
The Web UI can refresh review and CI evidence with
|
|
@@ -428,42 +454,79 @@ organization-wide bundle.
|
|
|
428
454
|
|
|
429
455
|
Every CLI coding-loop promotion now requires a current two-layer observation
|
|
430
456
|
matrix: the requested operation and result, plus independently checked final
|
|
431
|
-
postcondition and integration. See the [minimum completion contract](src/
|
|
457
|
+
postcondition and integration. See the [minimum completion contract](src/orchestrator/delivery/references/minimum-completion.md)
|
|
432
458
|
for the executable format. Missing mappings, mismatching observations, changed
|
|
433
459
|
artifacts or stale revisions block promotion even when the executor reports
|
|
434
460
|
success. Independent review checks original-request coverage and the test oracle.
|
|
435
461
|
|
|
436
462
|
Existing v2 run history is retained; resumed/rechecked runs need the new evidence.
|
|
437
|
-
The same checker ships in the
|
|
438
|
-
|
|
463
|
+
The same checker ships under `src/orchestrator/delivery/scripts/` in the CLI
|
|
464
|
+
package, independent of Codex plugin installation. It does not establish deployment
|
|
465
|
+
completion or intercept another host application's final response.
|
|
466
|
+
|
|
467
|
+
### PR closure and user-outcome transfer
|
|
468
|
+
|
|
469
|
+
The Web/CLI executor and independent auditor receive the same
|
|
470
|
+
[PR closure procedure](src/orchestrator/delivery/references/pr-closure.md).
|
|
471
|
+
It requires a revision-bound before/after table, reachable normal-user paths
|
|
472
|
+
(including no-workflow fallback), independent review, explicit authority,
|
|
473
|
+
pre-close freshness checks and post-close remote reconciliation. A file match or
|
|
474
|
+
successor link is not proof of UX inheritance; administrative transfer, approved
|
|
475
|
+
retirement and complete inheritance have distinct decisions. Unresolved yellow/red comparison rows require
|
|
476
|
+
remediation PRs (or exact existing open equivalents), mapped by criterion ID,
|
|
477
|
+
owner and acceptance checks. Uncreated plans or unresolved authority/product
|
|
478
|
+
decisions block transfer completion; creating a PR does not make a row green.
|
|
479
|
+
Closure is not a new delivery completion state. This is runtime instruction and review guidance,
|
|
480
|
+
not a GitHub hook that mechanically intercepts every external close command.
|
|
439
481
|
|
|
440
482
|
### Submit repaired local delivery evidence to SQM
|
|
441
483
|
|
|
442
484
|
The bundled Delivery Loop maintains an external acceptance checklist with separate
|
|
443
485
|
user-journey and technical-invariant results. It refreshes the table after each
|
|
444
486
|
evaluated action and marks evidence pending when the code or runtime context
|
|
445
|
-
changes.
|
|
446
|
-
|
|
487
|
+
changes. A non-green table is a checkpoint, not completion: the executor must
|
|
488
|
+
perform feasible authorized repairs, missing verification and bounded waits,
|
|
489
|
+
then re-observe and update the same criteria until the requested endpoint passes.
|
|
490
|
+
A genuine safety/authority/dependency/budget stop remains explicitly unfinished
|
|
491
|
+
with affected IDs and a concrete resumption condition; no automatic budget increase
|
|
492
|
+
or background monitor is implied. Both executor and independent auditor receive
|
|
493
|
+
this continuation contract. The [acceptance evidence guide](src/orchestrator/delivery/references/acceptance-evidence.md)
|
|
494
|
+
documents the offline validator shipped inside the CLI runtime. Its result establishes
|
|
447
495
|
record completeness and artifact integrity, not overall delivery or deployment.
|
|
448
496
|
At the start of each new run, the loop recommends and confirms one of five
|
|
449
|
-
[delivery levels](src/
|
|
450
|
-
in chat.
|
|
451
|
-
|
|
452
|
-
|
|
497
|
+
[delivery levels](src/orchestrator/delivery/references/delivery-levels.md)
|
|
498
|
+
in chat. Level 5 is highest assurance; level 1 is lightest. Every selection or
|
|
499
|
+
recommendation shows all five levels (5 to 1), their content and verification scope,
|
|
500
|
+
task-specific hour estimates, and the recommended level with its rationale.
|
|
501
|
+
Estimates distinguish execution from external waits and state assumptions and
|
|
502
|
+
uncertainty; there are no fixed durations by level. Risk-based floors preserve
|
|
503
|
+
mandatory assurance. Historical selections retain their numbering version and
|
|
504
|
+
meaning. These are executor/auditor instructions, not a new deterministic CLI
|
|
505
|
+
selection validator; selecting a level grants no additional authority.
|
|
453
506
|
|
|
454
507
|
`simy sqm submit-evidence --file delivery.json --host https://app-dev.simy.one`
|
|
455
508
|
submits an explicit bounded summary through your existing organization-bound device
|
|
456
509
|
session. `--dry-run` previews the exact document without uploading it. See the
|
|
457
|
-
[local evidence contract](src/
|
|
510
|
+
[local evidence contract](src/orchestrator/delivery/references/local-sqm-evidence.md).
|
|
458
511
|
The receipt identifies the source digest and analysis job; it is not a declaration
|
|
459
512
|
that an incident or blocking rule has been published.
|
|
460
513
|
|
|
461
514
|
## Delivery Loop shared specification
|
|
462
515
|
|
|
516
|
+
UX descriptions throughout the Web/CLI lifecycle follow the
|
|
517
|
+
[behavioral-completeness standard](src/orchestrator/delivery/references/ux-specification.md).
|
|
518
|
+
Observable interaction units explicitly cover intent/context, entry, perception,
|
|
519
|
+
control, response, usable outcome, continuation, alternatives/recovery and evidence.
|
|
520
|
+
Independent reconstruction, source coverage, causal continuity, branch closure,
|
|
521
|
+
observability and traceability determine sufficient detail, not word counts or
|
|
522
|
+
product-specific scenarios. Unknowns remain explicit; compact summaries link to
|
|
523
|
+
the complete canonical specification. The same standard is supplied to the actual
|
|
524
|
+
executor and independent auditor; it is not an additional automatic schema gate.
|
|
525
|
+
|
|
463
526
|
The [Delivery Loop system specification](docs/systems/delivery-loop/README.md)
|
|
464
527
|
explains its purpose, As-Is and proposed To-Be through twelve linked views. Keep
|
|
465
528
|
these as shared team documents; each change updates affected views and checks
|
|
466
|
-
cross-view consistency. The
|
|
529
|
+
cross-view consistency. The workflow requires independent design, implementation-to-
|
|
467
530
|
To-Be and PR reviews, plus explicitly opted-in SQM before and after PR creation.
|
|
468
|
-
The [workflow reference](src/
|
|
531
|
+
The [workflow reference](src/orchestrator/delivery/references/shared-system-specifications.md)
|
|
469
532
|
explains baseline updates, historical regression coverage and evidence limits.
|
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.17",
|
|
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/
|
|
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/orchestrator/delivery/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.17";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simy",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
-
"description": "Run SIMY quality
|
|
4
|
+
"description": "Run SIMY quality and pull request review workflows from the current Codex thread.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Specialist Doctors Inc.",
|
|
7
7
|
"url": "https://github.com/SpecialistDoctors-Inc"
|
|
8
8
|
},
|
|
9
9
|
"repository": "https://github.com/SpecialistDoctors-Inc/simy-cli",
|
|
10
10
|
"license": "UNLICENSED",
|
|
11
|
-
"keywords": ["simy", "sqm", "
|
|
11
|
+
"keywords": ["simy", "sqm", "pull-request"],
|
|
12
12
|
"skills": "./skills/",
|
|
13
13
|
"interface": {
|
|
14
14
|
"displayName": "SIMY",
|
|
15
|
-
"shortDescription": "Quality
|
|
16
|
-
"longDescription": "SIMY turns the current Codex thread into a bounded quality check
|
|
15
|
+
"shortDescription": "Quality and PR review workflows for SIMY.",
|
|
16
|
+
"longDescription": "SIMY turns the current Codex thread into a bounded quality check or a read-only pull request review. Delivery Loop runs through SIMY Web and CLI, not a local Codex skill.",
|
|
17
17
|
"developerName": "Specialist Doctors Inc.",
|
|
18
18
|
"category": "Developer Tools",
|
|
19
|
-
"capabilities": ["Quality", "
|
|
19
|
+
"capabilities": ["Quality", "Review"],
|
|
20
20
|
"defaultPrompt": [
|
|
21
21
|
"Run the SIMY SQM Loop for the current thread.",
|
|
22
|
-
"Run the SIMY Delivery Loop for the current thread.",
|
|
23
22
|
"Review the current pull request with SIMY."
|
|
24
23
|
]
|
|
25
24
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/src/console/app.js
CHANGED
|
@@ -96,7 +96,7 @@ export function AgenticLoopConsole({ agent, onQuit = () => {} }) {
|
|
|
96
96
|
|
|
97
97
|
useEffect(() => {
|
|
98
98
|
if (selectedRun?.status === "pr_ready_for_review") {
|
|
99
|
-
setNotice("
|
|
99
|
+
setNotice("Unfinished: resolve the pending gates, then use /recheck to refresh readiness.");
|
|
100
100
|
} else if (
|
|
101
101
|
selectedRun?.status === "waiting_human" ||
|
|
102
102
|
selectedRun?.controlState === "waiting_human"
|
|
@@ -1174,7 +1174,7 @@ function nextActionForRun(run, repositoryMissing = false) {
|
|
|
1174
1174
|
return { tone: "yellow", text: "Press p to resume the local agent." };
|
|
1175
1175
|
}
|
|
1176
1176
|
if (run.status === "pr_ready_for_review") {
|
|
1177
|
-
return { tone: "yellow", text: "
|
|
1177
|
+
return { tone: "yellow", text: "Unfinished: resolve pending gates, then press r to refresh evidence." };
|
|
1178
1178
|
}
|
|
1179
1179
|
if (run.status === "merge_ready") {
|
|
1180
1180
|
return {
|