@awak-app/simy-cli 0.5.6 → 0.5.8
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 +24 -1
- package/package.json +1 -1
- package/src/agent.js +1 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/SKILL.md +16 -0
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/local-sqm-evidence.md +34 -0
- package/src/index.js +1 -1
- package/src/sqm/agent-skill.js +61 -75
- package/src/sqm/bundle-store.js +9 -4
- package/src/sqm/command.js +3 -0
- package/src/sqm/index.js +1 -1
- package/src/sqm/local-evidence.js +28 -0
- package/src/sqm/proof.js +1 -0
- package/src/sqm/validation.js +15 -1
- package/src/sqm/workflow-onboarding.js +1 -1
- package/src/sqm/workflow-update.js +3 -2
package/README.md
CHANGED
|
@@ -11,9 +11,12 @@ simy --daemon
|
|
|
11
11
|
|
|
12
12
|
When an interactive `simy --daemon` starts without managed SIMY workflows,
|
|
13
13
|
the CLI asks once before installing the Codex plugin
|
|
14
|
-
marketplace
|
|
14
|
+
marketplace and Codex/Claude opt-in SQM skills. Existing unrelated plugins
|
|
15
15
|
and hooks are preserved. Use `--install-workflows` for explicit non-interactive
|
|
16
16
|
installation or `--no-workflow-prompt` to start the daemon without asking.
|
|
17
|
+
|
|
18
|
+
SQM checks apply when you explicitly select `@simy`, invoke the SQM skill, or ask to use SQM for the current task. Ordinary PR creation and `git push` do not opt in. Upgrading and restarting the daemon migrates the legacy global PR gate and shipped personal skills; unrelated hooks are preserved. Start a new coding session to reload skill instructions.
|
|
19
|
+
|
|
17
20
|
After installation, restart Codex and find and install **SIMY** in **Plugins**
|
|
18
21
|
before using `@simy` in a new task.
|
|
19
22
|
|
|
@@ -380,3 +383,23 @@ Publishing is handled by GitHub Actions in `.github/workflows/publish.yml`.
|
|
|
380
383
|
`@awak-app/simy-cli`.
|
|
381
384
|
- The package check inspects the final npm tarball contents and fails if any
|
|
382
385
|
source map file would be published.
|
|
386
|
+
|
|
387
|
+
### Repository system scopes
|
|
388
|
+
|
|
389
|
+
Version 0.5.7 supports signed `system_scope` snapshots from Quality Monitor.
|
|
390
|
+
Each check pins the repository's connected component and graph revision. A new
|
|
391
|
+
check revalidates scoped bundles online; a stale or unavailable scope cannot fall
|
|
392
|
+
back to an older cached component. Minimum and full check proofs retain the
|
|
393
|
+
original snapshot. A scope revision change requires a fresh minimum check.
|
|
394
|
+
Deploy this CLI and the web capability proxy before enabling the backend scope
|
|
395
|
+
requirement. Older clients receive an explicit upgrade error, never an expanded
|
|
396
|
+
organization-wide bundle.
|
|
397
|
+
|
|
398
|
+
### Submit repaired local delivery evidence to SQM
|
|
399
|
+
|
|
400
|
+
`simy sqm submit-evidence --file delivery.json --host https://app-dev.simy.one`
|
|
401
|
+
submits an explicit bounded summary through your existing organization-bound device
|
|
402
|
+
session. `--dry-run` previews the exact document without uploading it. See the
|
|
403
|
+
[local evidence contract](src/codex-plugins/simy/skills/agentic-delivery-loop/references/local-sqm-evidence.md).
|
|
404
|
+
The receipt identifies the source digest and analysis job; it is not a declaration
|
|
405
|
+
that an incident or blocking rule has been published.
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -498,7 +498,7 @@ export async function startAgent({
|
|
|
498
498
|
const body = await readJson(req);
|
|
499
499
|
if (body.confirmed !== true) {
|
|
500
500
|
json(res, 400, {
|
|
501
|
-
error: "Explicit confirmation is required before installing the SIMY plugin
|
|
501
|
+
error: "Explicit confirmation is required before installing the SIMY plugin and opt-in agent skills.",
|
|
502
502
|
});
|
|
503
503
|
return;
|
|
504
504
|
}
|
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.8";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -128,6 +128,22 @@ history or claim `done`. Save the exact revision, environment, input, Run ID,
|
|
|
128
128
|
timestamps, visible state, network evidence, and logs, then add a boundary
|
|
129
129
|
regression before re-entering implementation.
|
|
130
130
|
|
|
131
|
+
### Feed repaired delivery failures back into SQM
|
|
132
|
+
|
|
133
|
+
When an observed failure was repaired, prepare one bounded `delivery.json` record
|
|
134
|
+
per failure using [the local evidence contract](references/local-sqm-evidence.md).
|
|
135
|
+
Record actual impact, distinct failing/fixed commit SHAs, and verification results.
|
|
136
|
+
Keep the same evidence_id when retrying the same submission. Include known PR URLs
|
|
137
|
+
so SQM can retrieve related evidence; a link alone does not establish identity.
|
|
138
|
+
|
|
139
|
+
When the user's delivery scope authorizes sharing these summaries with their SQM
|
|
140
|
+
organization, submit using `simy sqm submit-evidence --file delivery.json --host
|
|
141
|
+
<the authorized origin>`. Otherwise retain the draft locally. Never attach whole
|
|
142
|
+
transcripts, credentials, unrelated files, or automatically read paths named in
|
|
143
|
+
logs. Record the returned source digest and job ID in the delivery receipt.
|
|
144
|
+
Submission is not analysis completion or a published safeguard. Resource locking
|
|
145
|
+
and validation ordering still require Delivery Loop execution controls.
|
|
146
|
+
|
|
131
147
|
### 8. Clean up and issue the receipt
|
|
132
148
|
|
|
133
149
|
Remove scoped fixtures and prove the residual count is zero. Deliver a compact
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Local delivery evidence
|
|
2
|
+
|
|
3
|
+
This is a new SQM source, alongside GitHub and Slack. It is not an incident declaration
|
|
4
|
+
and cannot set review status, severity, organization, or rule lifecycle.
|
|
5
|
+
|
|
6
|
+
```json
|
|
7
|
+
{
|
|
8
|
+
"schema_version": "1.0.0",
|
|
9
|
+
"evidence_id": "checkout-provider-failure-1",
|
|
10
|
+
"run_id": "delivery-run-42",
|
|
11
|
+
"repository": "owner/repository",
|
|
12
|
+
"title": "Checkout provider was missing",
|
|
13
|
+
"occurred_at": "2026-09-07T00:00:00Z",
|
|
14
|
+
"base_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
15
|
+
"head_sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
16
|
+
"failure": "What actually failed, with a bounded error excerpt.",
|
|
17
|
+
"impact": "What was prevented or affected; distinguish local from production.",
|
|
18
|
+
"fix": "What changed to repair the same failure.",
|
|
19
|
+
"verification": "What ran at head_sha and its observed result.",
|
|
20
|
+
"pr_urls": [],
|
|
21
|
+
"changed_paths": ["src/checkout.ts"],
|
|
22
|
+
"diff": "A relevant, sanitized diff excerpt (optional, at most 12000 characters)."
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Replace sample SHAs and statements with observed evidence. Summaries are limited to
|
|
27
|
+
64 KiB total. `--dry-run` prints exactly the submitted document without making a
|
|
28
|
+
request. `--host` selects an existing authenticated SIMY session; use the explicit
|
|
29
|
+
dev origin when testing dev. SQM analysis must already be enabled for the organization.
|
|
30
|
+
|
|
31
|
+
An identical record from the same user returns the same job. Changed evidence
|
|
32
|
+
creates a new digest/job and retains the earlier revision. Keep the returned job ID
|
|
33
|
+
for monitoring in SQM. Candidate checks still need independent review/evaluation;
|
|
34
|
+
local success never proves deployment or production impact.
|
package/src/index.js
CHANGED
|
@@ -103,7 +103,7 @@ Options:
|
|
|
103
103
|
--no-open Do not open the browser when authorization is required
|
|
104
104
|
--port <port> Bind a specific localhost port
|
|
105
105
|
--host <url> Connect to a SIMY Web origin (default: ${DEFAULT_WEB_ORIGIN})
|
|
106
|
-
--install-workflows Install or update SIMY Codex/Claude
|
|
106
|
+
--install-workflows Install or update SIMY Codex/Claude opt-in SQM skills
|
|
107
107
|
--no-workflow-prompt Skip the daemon startup workflow-installation prompt
|
|
108
108
|
--no-auto-update Disable update checks and automatic installation
|
|
109
109
|
|
package/src/sqm/agent-skill.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
export const SQM_AGENT_SKILL_ID = "simy-sqm-pr-check";
|
|
8
|
-
export const SQM_AGENT_SKILL_VERSION = "
|
|
8
|
+
export const SQM_AGENT_SKILL_VERSION = "3.0.0";
|
|
9
9
|
export const SIMY_CODEX_PLUGIN_ID = "simy";
|
|
10
10
|
export const SIMY_CODEX_PLUGIN_VERSION = "0.1.0";
|
|
11
11
|
|
|
@@ -15,41 +15,34 @@ const BUNDLED_CODEX_PLUGIN_ROOT = fileURLToPath(
|
|
|
15
15
|
const MANAGED_PLUGIN_AUTHOR = "Specialist Doctors Inc.";
|
|
16
16
|
const MANAGED_MARKETPLACE_SOURCE = `./plugins/${SIMY_CODEX_PLUGIN_ID}`;
|
|
17
17
|
|
|
18
|
+
const LEGACY_SKILL_DIGEST = "c9ab46e855280196dbc93fd36ba849418e0c4e500dd6e4b7b03b2296e838df92";
|
|
19
|
+
|
|
18
20
|
const SKILL_CONTENT = `---
|
|
19
21
|
name: ${SQM_AGENT_SKILL_ID}
|
|
20
|
-
description: Run SIMY Software Quality Monitor checks
|
|
22
|
+
description: Run SIMY Software Quality Monitor checks for PR work when the user explicitly invokes @simy, this skill, or asks to use SQM. Ordinary PR creation or git push alone does not activate this skill.
|
|
21
23
|
---
|
|
22
24
|
|
|
23
25
|
# SIMY SQM PR check
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
Apply this workflow only when the user has explicitly selected \`@simy\`, invoked this skill, or requested SQM for the current task. A quoted mention, repository organization, installed plugin, or ordinary request to create a PR is not opt-in. Honor a later request to stop using SQM.
|
|
28
|
+
|
|
29
|
+
Without opt-in, continue the requested work without running SQM or making it a prerequisite. You may briefly offer SQM if useful; do not delay the work waiting for an optional answer. Follow any independent repository checks required by the project.
|
|
30
|
+
|
|
31
|
+
When opted in, before creating or proposing a pull request:
|
|
26
32
|
|
|
27
33
|
1. Run \`simy sqm min-check\` from the repository root.
|
|
28
34
|
2. Run \`simy sqm check --proof proof.json --signed-evidence proof.json.signed.json\`.
|
|
29
35
|
3. Read \`proof.json\` and require \`outcome.status\` to be \`passed\`.
|
|
30
|
-
4. If either command fails, SQM is unavailable, or the proof is not passed,
|
|
36
|
+
4. If either command fails, SQM is unavailable, or the proof is not passed, report the exact blocker. Do not claim SQM passed or silently bypass the requested check.
|
|
31
37
|
5. Include the proof path and SQM outcome in the PR body when the check passes.
|
|
32
38
|
|
|
33
39
|
Do not replace SQM with an LLM review, and do not claim the check passed without reading the generated proof.
|
|
34
40
|
`;
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
for await (const chunk of process.stdin) raw += chunk;
|
|
41
|
-
let payload = {};
|
|
42
|
-
try { payload = raw ? JSON.parse(raw) : {}; } catch { process.exit(0); }
|
|
43
|
-
const command = String(payload?.tool_input?.command || payload?.tool_input?.cmd || "");
|
|
44
|
-
if (!/(^|[;&|]\\s*)gh\\s+pr\\s+create(?:\\s|$)/.test(command)) process.exit(0);
|
|
45
|
-
|
|
46
|
-
for (const args of [["sqm", "min-check"], ["sqm", "check"]]) {
|
|
47
|
-
const result = spawnSync("simy", args, { cwd: process.cwd(), stdio: "inherit" });
|
|
48
|
-
if (result.error || result.status !== 0) {
|
|
49
|
-
console.error(result.error ? \`SIMY SQM PR gate failed: \${result.error.message}\` : \`SIMY SQM PR gate blocked the PR (exit \${result.status}).\`);
|
|
50
|
-
process.exit(2);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
42
|
+
// Cached hook registrations in running sessions must also stop blocking PRs.
|
|
43
|
+
const RETIRED_HOOK_SCRIPT = `#!/usr/bin/env node
|
|
44
|
+
// Global SQM gate retired; the skill applies only after explicit task opt-in.
|
|
45
|
+
process.exit(0);
|
|
53
46
|
`;
|
|
54
47
|
|
|
55
48
|
const TARGETS = {
|
|
@@ -65,8 +58,6 @@ export async function inspectSqmAgentSkill({ home = homedir() } = {}) {
|
|
|
65
58
|
const digest = sqmAgentSkillDigest();
|
|
66
59
|
const codexPlugin = await inspectCodexPlugin({ home });
|
|
67
60
|
const hookPath = managedHookPath(home);
|
|
68
|
-
const installedHookDigest = await fileDigest(hookPath);
|
|
69
|
-
const hookMatches = installedHookDigest === digestText(HOOK_SCRIPT);
|
|
70
61
|
const platforms = {};
|
|
71
62
|
for (const [platform, segments] of Object.entries(TARGETS)) {
|
|
72
63
|
const target = path.join(home, ...segments);
|
|
@@ -77,14 +68,12 @@ export async function inspectSqmAgentSkill({ home = homedir() } = {}) {
|
|
|
77
68
|
platforms[platform] = {
|
|
78
69
|
path: target,
|
|
79
70
|
hook_path: platformHookConfigPath(platform, home),
|
|
80
|
-
installed: skillMatches &&
|
|
71
|
+
installed: skillMatches && !hookConfigured && platformExtraMatches,
|
|
81
72
|
state:
|
|
82
73
|
installedDigest === null ||
|
|
83
|
-
!hookConfigured ||
|
|
84
|
-
installedHookDigest === null ||
|
|
85
74
|
(platform === "codex" && codexPlugin.state === "missing")
|
|
86
75
|
? "missing"
|
|
87
|
-
: skillMatches &&
|
|
76
|
+
: skillMatches && !hookConfigured && platformExtraMatches
|
|
88
77
|
? "installed"
|
|
89
78
|
: "outdated",
|
|
90
79
|
...(platform === "codex" ? { plugin: codexPlugin } : {}),
|
|
@@ -94,7 +83,7 @@ export async function inspectSqmAgentSkill({ home = homedir() } = {}) {
|
|
|
94
83
|
skill_id: SQM_AGENT_SKILL_ID,
|
|
95
84
|
version: SQM_AGENT_SKILL_VERSION,
|
|
96
85
|
digest: `sha256:${digest}`,
|
|
97
|
-
enforcement: "
|
|
86
|
+
enforcement: "explicit_opt_in",
|
|
98
87
|
codex_plugin: codexPlugin,
|
|
99
88
|
installed: Object.values(platforms).every((entry) => entry.installed),
|
|
100
89
|
platforms,
|
|
@@ -106,12 +95,10 @@ export async function installSqmAgentSkill({ home = homedir(), platforms = Objec
|
|
|
106
95
|
if (!requested.length || requested.some((platform) => !Object.hasOwn(TARGETS, platform))) {
|
|
107
96
|
throw new Error("platforms must contain codex and/or claude");
|
|
108
97
|
}
|
|
109
|
-
const hookPath = managedHookPath(home);
|
|
110
98
|
if (requested.includes("codex")) await installCodexPlugin({ home });
|
|
111
|
-
await
|
|
99
|
+
await retireLegacySqmGate({ home });
|
|
112
100
|
for (const platform of requested) {
|
|
113
101
|
await atomicText(path.join(home, ...TARGETS[platform]), SKILL_CONTENT, 0o600);
|
|
114
|
-
await installManagedHook(platform, home, hookPath);
|
|
115
102
|
}
|
|
116
103
|
return inspectSqmAgentSkill({ home });
|
|
117
104
|
}
|
|
@@ -252,60 +239,59 @@ function platformHookConfigPath(platform, home) {
|
|
|
252
239
|
: path.join(home, ".claude", "settings.json");
|
|
253
240
|
}
|
|
254
241
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
242
|
+
// Only migrate the exact shipped legacy skill. Custom user instructions are retained.
|
|
243
|
+
export async function retireLegacySqmGate({ home = homedir() } = {}) {
|
|
244
|
+
const hookPath = managedHookPath(home);
|
|
245
|
+
let changed = false;
|
|
246
|
+
for (const [platform, segments] of Object.entries(TARGETS)) {
|
|
247
|
+
const target = path.join(home, ...segments);
|
|
248
|
+
if (await fileDigest(target) === LEGACY_SKILL_DIGEST) {
|
|
249
|
+
await atomicText(target, SKILL_CONTENT, 0o600);
|
|
250
|
+
changed = true;
|
|
251
|
+
}
|
|
252
|
+
changed = await removeManagedHook(platform, home, hookPath) || changed;
|
|
253
|
+
}
|
|
254
|
+
const hookDigest = await fileDigest(hookPath);
|
|
255
|
+
if (hookDigest !== null && hookDigest !== digestText(RETIRED_HOOK_SCRIPT)) {
|
|
256
|
+
await atomicText(hookPath, RETIRED_HOOK_SCRIPT, 0o700);
|
|
257
|
+
changed = true;
|
|
258
|
+
}
|
|
259
|
+
return changed;
|
|
266
260
|
}
|
|
267
261
|
|
|
268
|
-
async function
|
|
262
|
+
async function removeManagedHook(platform, home, hookPath) {
|
|
269
263
|
const target = platformHookConfigPath(platform, home);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
264
|
+
const config = await readJsonFile(target, { missing: null, invalid: "throw" });
|
|
265
|
+
if (!config?.hooks) return false;
|
|
266
|
+
let changed = false;
|
|
267
|
+
for (const [event, groups] of Object.entries(config.hooks)) {
|
|
268
|
+
if (!Array.isArray(groups)) continue;
|
|
269
|
+
const kept = [];
|
|
270
|
+
for (const group of groups) {
|
|
271
|
+
if (!Array.isArray(group?.hooks)) { kept.push(group); continue; }
|
|
272
|
+
const remaining = group.hooks.filter((hook) => !isManagedHook(hook, hookPath));
|
|
273
|
+
if (remaining.length === group.hooks.length) kept.push(group);
|
|
274
|
+
else {
|
|
275
|
+
changed = true;
|
|
276
|
+
if (remaining.length) kept.push({ ...group, hooks: remaining });
|
|
277
|
+
}
|
|
276
278
|
}
|
|
279
|
+
if (kept.length) config.hooks[event] = kept;
|
|
280
|
+
else if (groups.length) delete config.hooks[event];
|
|
277
281
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
: {};
|
|
281
|
-
const existing = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
|
|
282
|
-
hooks.PreToolUse = [
|
|
283
|
-
...existing.filter((group) => !groupContainsManagedHook(group)),
|
|
284
|
-
managedHookGroup(hookPath),
|
|
285
|
-
];
|
|
286
|
-
await atomicText(target, `${JSON.stringify({ ...config, hooks }, null, 2)}\n`, 0o600);
|
|
282
|
+
if (changed) await atomicText(target, `${JSON.stringify(config, null, 2)}\n`, 0o600);
|
|
283
|
+
return changed;
|
|
287
284
|
}
|
|
288
285
|
|
|
289
286
|
async function hasManagedHook(platform, home, hookPath) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
config.hooks.PreToolUse.some((group) => groupContainsManagedHook(group, hookPath));
|
|
294
|
-
} catch (error) {
|
|
295
|
-
if (error?.code === "ENOENT" || error instanceof SyntaxError) return false;
|
|
296
|
-
throw error;
|
|
297
|
-
}
|
|
287
|
+
const config = await readJsonFile(platformHookConfigPath(platform, home), { missing: null, invalid: "throw" });
|
|
288
|
+
return Object.values(config?.hooks || {}).some((groups) => Array.isArray(groups) &&
|
|
289
|
+
groups.some((group) => Array.isArray(group?.hooks) && group.hooks.some((hook) => isManagedHook(hook, hookPath))));
|
|
298
290
|
}
|
|
299
291
|
|
|
300
|
-
function
|
|
301
|
-
|
|
302
|
-
return
|
|
303
|
-
const command = typeof hook?.command === "string" ? hook.command : "";
|
|
304
|
-
return expectedPath
|
|
305
|
-
? command === `node ${JSON.stringify(expectedPath)}`
|
|
306
|
-
: command.includes("/.simy/integrations/sqm-agent/pre-pr-hook.mjs") ||
|
|
307
|
-
command.includes("\\.simy\\integrations\\sqm-agent\\pre-pr-hook.mjs");
|
|
308
|
-
});
|
|
292
|
+
function isManagedHook(hook, expectedPath) {
|
|
293
|
+
const command = typeof hook?.command === "string" ? hook.command : "";
|
|
294
|
+
return command === `node ${JSON.stringify(expectedPath)}`;
|
|
309
295
|
}
|
|
310
296
|
|
|
311
297
|
async function fileDigest(target) {
|
package/src/sqm/bundle-store.js
CHANGED
|
@@ -41,13 +41,13 @@ export async function loadKnowledgeBundle({
|
|
|
41
41
|
}
|
|
42
42
|
const cacheIdentity = normalizedCacheIdentity({ endpoint, accountId, organizationId, repository });
|
|
43
43
|
const location = cacheLocation(cacheRoot, cacheIdentity);
|
|
44
|
-
const validation = { cliVersion, publicKeys, organizationId };
|
|
44
|
+
const validation = { cliVersion, publicKeys, organizationId, repository };
|
|
45
45
|
const cached = await readVerifiedCache(location, validation, cacheIdentity);
|
|
46
46
|
const fresh =
|
|
47
47
|
cached &&
|
|
48
48
|
now - cached.fetchedAt < ttlMs &&
|
|
49
49
|
Date.parse(cached.bundle.expires_at) > now;
|
|
50
|
-
if (!offline && endpoint && (!fresh || refresh)) {
|
|
50
|
+
if (!offline && endpoint && (!fresh || refresh || cached?.bundle.system_scope)) {
|
|
51
51
|
try {
|
|
52
52
|
const fetched = await fetchBundle({ endpoint, repository, cliVersion, token, organizationId, etag: cached?.etag, fetchImpl });
|
|
53
53
|
if (fetched.notModified) {
|
|
@@ -56,6 +56,7 @@ export async function loadKnowledgeBundle({
|
|
|
56
56
|
await writeMetadata(location, { ...cached.metadata, fetched_at: new Date(now).toISOString(), etag: fetched.etag || cached.etag });
|
|
57
57
|
return result(cached.bundle, [], "cache_revalidated");
|
|
58
58
|
}
|
|
59
|
+
if (cached?.bundle.system_scope && !fetched.bundle?.system_scope) throw nonFallbackError("SQM Cloud attempted to remove the pinned system scope contract.");
|
|
59
60
|
const discoveredTrust = signingTrustForFetch({
|
|
60
61
|
bundle: fetched.bundle,
|
|
61
62
|
endpoint,
|
|
@@ -81,11 +82,12 @@ export async function loadKnowledgeBundle({
|
|
|
81
82
|
return result(bundle, [], "cloud");
|
|
82
83
|
} catch (error) {
|
|
83
84
|
if (error?.code === "incompatible_cli") throw error;
|
|
84
|
-
if (error?.fallbackAllowed !== false && cached && Date.parse(cached.bundle.expires_at) > now) return result(cached.bundle, [`SQM Cloud unavailable; using last verified bundle: ${message(error)}`], "stale_cache");
|
|
85
|
+
if (error?.fallbackAllowed !== false && cached && !cached.bundle.system_scope && Date.parse(cached.bundle.expires_at) > now) return result(cached.bundle, [`SQM Cloud unavailable; using last verified bundle: ${message(error)}`], "stale_cache");
|
|
85
86
|
return result(null, [`SQM knowledge could not be used; shadow checks were skipped: ${message(error)}`], "unavailable");
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
if (cached) {
|
|
90
|
+
if (cached.bundle.system_scope) return result(null, ["SQM system scope must be revalidated online before a new check."], "unavailable");
|
|
89
91
|
if (Date.parse(cached.bundle.expires_at) <= now) return result(null, ["The cached SQM knowledge bundle is expired; shadow checks were skipped."], "unavailable");
|
|
90
92
|
const warnings = [];
|
|
91
93
|
if (offline) warnings.push("SQM is offline; using the last verified knowledge bundle.");
|
|
@@ -96,10 +98,12 @@ export async function loadKnowledgeBundle({
|
|
|
96
98
|
return result(null, [`SQM has no verified cache (${reason}); shadow checks were skipped.`], "unavailable");
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
export function verifyAndValidate(bundle, { cliVersion, publicKeys, organizationId } = {}) {
|
|
101
|
+
export function verifyAndValidate(bundle, { cliVersion, publicKeys, organizationId, repository } = {}) {
|
|
100
102
|
try { validateKnowledgeBundle(bundle, { cliVersion }); } catch (error) { throw nonFallbackError(message(error), error); }
|
|
101
103
|
if (organizationId && bundle.organization_id !== organizationId) throw nonFallbackError(`SQM bundle organization mismatch: expected ${organizationId}.`);
|
|
104
|
+
if (bundle.system_scope && repository && bundle.system_scope.repository !== repository.toLowerCase()) throw nonFallbackError("SQM bundle repository system scope does not match this repository.");
|
|
102
105
|
for (const module of bundle.modules) {
|
|
106
|
+
if (bundle.system_scope && module.scope.visibility === "organization" && !module.scope.repositories.some((repo) => bundle.system_scope.repositories.includes(repo.toLowerCase()))) throw nonFallbackError(`SQM module ${module.id} is outside the repository system.`);
|
|
103
107
|
if (module.scope.visibility === "organization" && module.scope.organization_id !== bundle.organization_id) throw nonFallbackError(`SQM module ${module.id} is scoped to a different organization.`);
|
|
104
108
|
}
|
|
105
109
|
const keyId = bundle.integrity.key_id;
|
|
@@ -166,6 +170,7 @@ async function fetchBundle({ endpoint, repository, cliVersion, token, organizati
|
|
|
166
170
|
const url = new URL(endpoint);
|
|
167
171
|
url.searchParams.set("repository", repository);
|
|
168
172
|
url.searchParams.set("cli_version", cliVersion);
|
|
173
|
+
url.searchParams.set("scope_version", "1");
|
|
169
174
|
const headers = { Accept: "application/json" };
|
|
170
175
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
171
176
|
if (organizationId) headers["x-simy-org-id"] = organizationId;
|
package/src/sqm/command.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import { isSessionValid, readSession } from "../session-store.js";
|
|
6
6
|
import { resolveWebOrigin } from "../web-origin.js";
|
|
7
|
+
import { submitLocalEvidence } from "./local-evidence.js";
|
|
7
8
|
import { uploadRunEvidence } from "./evidence-client.js";
|
|
8
9
|
import { evaluateKnowledgeBundle } from "./checkers.js";
|
|
9
10
|
import { runSqmCheck, runSqmMinCheck } from "./index.js";
|
|
@@ -15,6 +16,7 @@ export async function runSqmCommand(argv, { cliVersion }) {
|
|
|
15
16
|
printHelp();
|
|
16
17
|
return 0;
|
|
17
18
|
}
|
|
19
|
+
if (argv[0] === "submit-evidence") return await submitLocalEvidence(argv);
|
|
18
20
|
if (argv[0] === "lint") return await runLintCommand(argv);
|
|
19
21
|
if (argv[0] === "replay") return await runReplayCommand(argv);
|
|
20
22
|
if (!["min-check", "check"].includes(argv[0])) throw new Error("Usage: simy sqm <min-check|check|lint|replay> [options]");
|
|
@@ -95,6 +97,7 @@ function printHelp() {
|
|
|
95
97
|
console.log(`Usage:
|
|
96
98
|
simy sqm min-check [options]
|
|
97
99
|
simy sqm check [options]
|
|
100
|
+
simy sqm submit-evidence --file <delivery.json> [--host <origin>] [--dry-run]
|
|
98
101
|
simy sqm lint --bundle <path>
|
|
99
102
|
simy sqm replay --bundle <path> --dataset <path>
|
|
100
103
|
|
package/src/sqm/index.js
CHANGED
|
@@ -132,7 +132,7 @@ function toLegacyCheck(minCheck, loaded) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
function bundleIdentity(bundle) {
|
|
135
|
-
return { id: bundle.bundle_id, version: bundle.version, digest: bundle.integrity.digest, signing_key_id: bundle.integrity.key_id };
|
|
135
|
+
return { id: bundle.bundle_id, version: bundle.version, digest: bundle.integrity.digest, signing_key_id: bundle.integrity.key_id, ...(bundle.system_scope ? { system_scope: bundle.system_scope } : {}) };
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
function unavailable(warning, repository = null) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { isSessionValid, readSession } from "../session-store.js";
|
|
3
|
+
import { resolveWebOrigin } from "../web-origin.js";
|
|
4
|
+
import { webApiUrl } from "../web-api.js";
|
|
5
|
+
|
|
6
|
+
/** Submission is explicit; no transcript scanning or implicit file attachment. */
|
|
7
|
+
export async function submitLocalEvidence(argv, { fetchImpl = globalThis.fetch, readSessionImpl = readSession } = {}) {
|
|
8
|
+
const option = (name) => { const i = argv.indexOf(name); return i < 0 ? undefined : argv[i + 1]; };
|
|
9
|
+
const file = option("--file");
|
|
10
|
+
if (!file || file.startsWith("--")) throw new Error("Usage: simy sqm submit-evidence --file <delivery.json> [--host <origin>] [--dry-run]");
|
|
11
|
+
const raw = await readFile(file);
|
|
12
|
+
if (raw.length > 64 * 1024) throw new Error("Local delivery evidence exceeds 64 KiB; submit a bounded summary.");
|
|
13
|
+
const evidence = JSON.parse(raw.toString("utf8"));
|
|
14
|
+
if (!evidence || Array.isArray(evidence) || evidence.schema_version !== "1.0.0") throw new Error("Expected local delivery evidence schema_version 1.0.0");
|
|
15
|
+
if (argv.includes("--dry-run")) { console.log(JSON.stringify({ submitted: false, evidence }, null, 2)); return 0; }
|
|
16
|
+
const origin = resolveWebOrigin(option("--host"));
|
|
17
|
+
const session = await readSessionImpl(origin);
|
|
18
|
+
if (!isSessionValid(session, Date.now(), origin)) throw new Error(`Sign in first: simy --host ${origin}`);
|
|
19
|
+
const response = await fetchImpl(webApiUrl("sqm/local-evidence", { webOrigin: origin, apiBaseUrl: session.api_base_url }), {
|
|
20
|
+
method: "POST", headers: { authorization: `Bearer ${session.token}`, "content-type": "application/json" },
|
|
21
|
+
body: JSON.stringify(evidence), signal: AbortSignal.timeout(30_000),
|
|
22
|
+
});
|
|
23
|
+
const result = await response.json().catch(() => null);
|
|
24
|
+
if (!response.ok) throw new Error(`SQM submission failed (${response.status}): ${result?.error?.message || result?.error || "Unavailable"}`);
|
|
25
|
+
if (!result?.source_id || !Array.isArray(result.jobs) || !result.jobs.length) throw new Error("SQM returned no analysis job receipt");
|
|
26
|
+
console.log(JSON.stringify(result, null, 2));
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
package/src/sqm/proof.js
CHANGED
|
@@ -57,6 +57,7 @@ export function buildProof({ organizationId, repository, bundle, minCheck, evalu
|
|
|
57
57
|
version: bundle.version,
|
|
58
58
|
digest: bundle.integrity.digest,
|
|
59
59
|
signing_key_id: bundle.integrity.key_id,
|
|
60
|
+
...(bundle.system_scope ? { system_scope: bundle.system_scope } : {}),
|
|
60
61
|
},
|
|
61
62
|
min_check: {
|
|
62
63
|
result_digest: minCheck.result_digest,
|
package/src/sqm/validation.js
CHANGED
|
@@ -17,13 +17,27 @@ const SCENARIO_REQUIREMENTS = new Set(["local", "requires_human", "requires_post
|
|
|
17
17
|
export function validateKnowledgeBundle(bundle, { cliVersion }) {
|
|
18
18
|
const errors = [];
|
|
19
19
|
object(bundle, "$", errors);
|
|
20
|
-
exactKeys(bundle, ["schema_version", "bundle_id", "version", "organization_id", "generated_at", "expires_at", "modules", "integrity"], "$", errors);
|
|
20
|
+
exactKeys(bundle, ["schema_version", "bundle_id", "version", "organization_id", "generated_at", "expires_at", "modules", "integrity", "system_scope"], "$", errors);
|
|
21
21
|
equal(bundle?.schema_version, "1.0.0", "$.schema_version", errors);
|
|
22
22
|
stringPattern(bundle?.bundle_id, /^[a-z][a-z0-9._-]{2,127}$/, "$.bundle_id", errors);
|
|
23
23
|
stringPattern(bundle?.version, /^[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$/, "$.version", errors);
|
|
24
24
|
nullableString(bundle?.organization_id, 200, "$.organization_id", errors);
|
|
25
25
|
dateTime(bundle?.generated_at, "$.generated_at", errors);
|
|
26
26
|
dateTime(bundle?.expires_at, "$.expires_at", errors);
|
|
27
|
+
if (bundle?.system_scope !== undefined) {
|
|
28
|
+
const scope = bundle.system_scope;
|
|
29
|
+
object(scope, "$.system_scope", errors);
|
|
30
|
+
exactKeys(scope, ["organization_id", "repository", "graph_revision", "repositories"], "$.system_scope", errors);
|
|
31
|
+
equal(scope?.organization_id, bundle.organization_id, "$.system_scope.organization_id", errors);
|
|
32
|
+
stringPattern(scope?.repository, REPOSITORY, "$.system_scope.repository", errors);
|
|
33
|
+
if (!Number.isSafeInteger(scope?.graph_revision) || scope.graph_revision < 0) errors.push("$.system_scope.graph_revision must be a nonnegative integer");
|
|
34
|
+
if (!Array.isArray(scope?.repositories) || !scope.repositories.length || !scope.repositories.includes(scope.repository)) {
|
|
35
|
+
errors.push("$.system_scope.repositories must contain the target repository");
|
|
36
|
+
} else {
|
|
37
|
+
for (const repo of scope.repositories) stringPattern(repo, REPOSITORY, "$.system_scope.repositories[]", errors);
|
|
38
|
+
if (new Set(scope.repositories).size !== scope.repositories.length) errors.push("$.system_scope.repositories must be unique");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
27
41
|
if (!Array.isArray(bundle?.modules)) errors.push("$.modules must be an array");
|
|
28
42
|
for (const [index, module] of (bundle?.modules || []).entries()) {
|
|
29
43
|
validateModule(module, `$.modules[${index}]`, errors, cliVersion);
|
|
@@ -73,7 +73,7 @@ export async function confirmWorkflowInstallation({
|
|
|
73
73
|
} = {}) {
|
|
74
74
|
output.write(
|
|
75
75
|
[
|
|
76
|
-
"SIMY can install or update its Codex plugin marketplace, Codex/Claude SQM skills
|
|
76
|
+
"SIMY can install or update its Codex plugin marketplace, Codex/Claude opt-in SQM skills.",
|
|
77
77
|
"Existing unrelated plugins and hooks will be preserved.",
|
|
78
78
|
"",
|
|
79
79
|
].join("\n"),
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
|
|
7
7
|
import { resolveBackendExecutable } from "../backend-executable.js";
|
|
8
|
-
import { inspectSqmAgentSkill, installSqmAgentSkill } from "./agent-skill.js";
|
|
8
|
+
import { inspectSqmAgentSkill, installSqmAgentSkill, retireLegacySqmGate } from "./agent-skill.js";
|
|
9
9
|
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
11
|
const CHECK_INTERVAL_MS = 60 * 60 * 1000;
|
|
@@ -47,11 +47,12 @@ export async function syncInstalledWorkflows({ home = homedir(),
|
|
|
47
47
|
throw error;
|
|
48
48
|
}
|
|
49
49
|
try {
|
|
50
|
+
const migrated = await retireLegacySqmGate({ home });
|
|
50
51
|
let status = await inspect({ home });
|
|
51
52
|
const platforms = Object.entries(status.platforms)
|
|
52
53
|
.filter(([, entry]) => entry.state === "outdated").map(([platform]) => platform);
|
|
53
54
|
if (platforms.length) status = await install({ home, platforms });
|
|
54
|
-
if (!status.platforms.codex.installed) return { outcome: platforms.length ? "updated" : "unchanged" };
|
|
55
|
+
if (!status.platforms.codex.installed) return { outcome: platforms.length || migrated ? "updated" : "unchanged" };
|
|
55
56
|
|
|
56
57
|
const marketplace = JSON.parse(await readFile(status.codex_plugin.marketplace_path, "utf8"));
|
|
57
58
|
if (typeof marketplace.name !== "string" || !/^[A-Za-z0-9_-]+$/.test(marketplace.name)) {
|