@awak-app/simy-cli 0.7.15 → 0.7.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 +225 -98
- package/package.json +5 -3
- package/src/agent.js +129 -7
- package/src/backend-executable.js +7 -6
- package/src/bounded-local-task-subtask-pool.js +32 -6
- package/src/bounded-local-task-subtasks-contract.js +1 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +2 -2
- package/src/codex-plugins/simy/hooks/README.md +8 -3
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +302 -78
- package/src/codex-plugins/simy/hooks/hooks.json +1 -1
- package/src/codex-plugins/simy/hooks/loop-kernel/conformance.json +136 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/index.mjs +42 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/outcome-review.mjs +31 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/pin.json +6 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/policy.json +141 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/task-pool.mjs +34 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/work-frontier.mjs +132 -0
- package/src/codex-plugins/simy/hooks/loop-kernel.mjs +6 -0
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +16 -5
- package/src/codex-plugins/simy/hooks/purpose-completion-contract.mjs +10 -0
- package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +12 -11
- package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +28 -38
- package/src/codex-plugins/simy/hooks/purpose-rule-policy.mjs +1 -2
- package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +5 -63
- package/src/codex-plugins/simy/hooks/purpose-search.mjs +21 -4
- package/src/codex-plugins/simy/hooks/result-cycle.mjs +166 -0
- package/src/codex-plugins/simy/hooks/scene-reasoning.mjs +98 -12
- package/src/codex-plugins/simy/hooks/user-experience.mjs +26 -0
- package/src/codex-plugins/simy/hooks/work-frontier.mjs +2 -0
- package/src/codex-plugins/simy/skills/diagnose/SKILL.md +10 -4
- package/src/coding-run-recovery.js +4 -2
- package/src/delivery-hooks/claude-workflow.md +10 -7
- package/src/delivery-hooks/command.js +76 -10
- package/src/delivery-hooks/consistency-monitor.js +20 -4
- package/src/delivery-hooks/consult.js +60 -0
- package/src/delivery-hooks/diagnosis.js +2 -2
- package/src/delivery-hooks/legacy-hook-routing.js +46 -8
- package/src/delivery-hooks/local-runtime.js +81 -1
- package/src/delivery-hooks/local-supervisor.js +13 -4
- package/src/delivery-hooks/repair-continuation.js +80 -0
- package/src/delivery-hooks/repair.js +164 -0
- package/src/delivery-hooks/supervisor-handover.js +84 -0
- package/src/delivery-hooks/supervisor-owner.js +3 -2
- package/src/delivery-hooks/supervisor-service.js +38 -6
- package/src/delivery-hooks/supervisor-watchdog.js +15 -4
- package/src/delivery-hooks/supervisor.js +151 -18
- package/src/desktop-access.js +2 -1
- package/src/desktop-host.js +35 -2
- package/src/durable-local-task-worker.js +372 -58
- package/src/execution-capability-contract.js +15 -1
- package/src/index.js +3 -3
- package/src/local-photos-read.swift +33 -0
- package/src/local-photos.js +47 -0
- package/src/local-task-attachment-store.js +31 -0
- package/src/local-task-destination.js +174 -0
- package/src/local-task.js +119 -7
- package/src/native-delegation-contract.js +68 -0
- package/src/native-provider-identity.js +60 -0
- package/src/orchestrator/audit.js +24 -323
- package/src/orchestrator/budget.js +22 -11
- package/src/orchestrator/charter-validation.js +3 -41
- package/src/orchestrator/checklist-continuation.js +2 -14
- package/src/orchestrator/completion-contract.js +4 -49
- package/src/orchestrator/contract.js +5 -9
- package/src/orchestrator/delivery/WORKFLOW.md +30 -710
- package/src/orchestrator/delivery/references/acceptance-evidence.md +4 -216
- package/src/orchestrator/delivery/references/change-boundary-matrix.md +4 -103
- package/src/orchestrator/delivery/references/chrome-user-verification.md +5 -133
- package/src/orchestrator/delivery/references/cross-repository-control.md +4 -74
- package/src/orchestrator/delivery/references/delivery-levels.md +3 -117
- package/src/orchestrator/delivery/references/local-sqm-evidence.md +1 -1
- package/src/orchestrator/delivery/references/minimum-completion.md +4 -144
- package/src/orchestrator/delivery/references/pr-closure.md +4 -200
- package/src/orchestrator/delivery/references/shared-system-specifications.md +4 -161
- package/src/orchestrator/delivery/references/state-and-receipts.md +4 -245
- package/src/orchestrator/delivery/references/state-model.md +4 -185
- package/src/orchestrator/delivery/references/ux-specification.md +4 -95
- package/src/orchestrator/delivery-level.js +2 -10
- package/src/orchestrator/evidence.js +2 -3
- package/src/orchestrator/independent-audit.js +15 -164
- package/src/orchestrator/instruction.js +19 -254
- package/src/orchestrator/loop.js +35 -95
- package/src/orchestrator/outcome-monitor.js +21 -0
- package/src/orchestrator/pr-closure.js +2 -16
- package/src/orchestrator/recovery.js +32 -2
- package/src/orchestrator/ux-specification.js +3 -12
- package/src/provider-delegation.js +336 -0
- package/src/runner.js +5 -2
- package/src/session-supervision/acceptance-reads.js +34 -0
- package/src/session-supervision/artifact-observer.js +138 -0
- package/src/session-supervision/browser-cdp.js +59 -0
- package/src/session-supervision/browser-recovery.js +97 -0
- package/src/session-supervision/codex-app-server.js +61 -22
- package/src/session-supervision/completion-artifacts.js +44 -0
- package/src/session-supervision/consistency-auditor.js +8 -11
- package/src/session-supervision/continuation-analysis.js +47 -28
- package/src/session-supervision/final-response-recovery.js +5 -0
- package/src/session-supervision/index.js +22 -5
- package/src/session-supervision/interaction-proxy.js +250 -0
- package/src/session-supervision/interaction-task.js +189 -0
- package/src/session-supervision/native-interaction.js +102 -0
- package/src/session-supervision/provider-failure.js +40 -0
- package/src/session-supervision/purpose-instruction.js +14 -14
- package/src/session-supervision/purpose-recovery.js +32 -12
- package/src/session-supervision/purpose-verification.js +50 -216
- package/src/session-supervision/recovery-frontier.js +55 -0
- package/src/session-supervision/stop-notifications.js +8 -8
- package/src/sqm/agent-skill.js +1 -1
- package/src/sqm/checkers.js +12 -6
- package/src/sqm/claude-plugin-update.js +22 -8
- package/src/sqm/claude-plugin.js +6 -0
- package/src/sqm/local-tests.js +2 -1
- package/src/sqm/plugin-cache.js +67 -0
- package/src/sqm/proof.js +25 -0
- package/src/sqm/report.js +1 -0
- package/src/sqm/workflow-onboarding.js +3 -39
- package/src/sqm/workflow-update.js +17 -6
- package/src/startup-preflight.js +2 -20
- package/src/team-status-fence-store.js +32 -0
- package/src/team-threads.js +457 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { evidenceLedger } from '../codex-plugins/simy/hooks/purpose-evidence.mjs';
|
|
2
|
+
import { reconcileWorkFrontier, revalidateWorkProposal } from '../codex-plugins/simy/hooks/work-frontier.mjs';
|
|
3
|
+
import { searchEnabled } from '../codex-plugins/simy/hooks/purpose-search.mjs';
|
|
4
|
+
import { taskDirectory } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
5
|
+
|
|
6
|
+
// Enroll the controller's canonical workspace, never a worker-provided lock ID.
|
|
7
|
+
// Read-only diagnosis/verification is the path that resolves observed effects;
|
|
8
|
+
// it must remain possible while the frontier prevents another writer.
|
|
9
|
+
export function recoveryWorkProposal(record) {
|
|
10
|
+
const managed = record.run.managed;
|
|
11
|
+
if (!searchEnabled(managed)) return null;
|
|
12
|
+
const ledger = evidenceLedger(managed);
|
|
13
|
+
const workspace = `workspace:${taskDirectory(record)}`;
|
|
14
|
+
try {
|
|
15
|
+
for (const unit of ledger.units) {
|
|
16
|
+
// Repair pre-migration understanding reads whose checkpoint settled but
|
|
17
|
+
// whose ledger occupancy was never released. Never settle an active or
|
|
18
|
+
// unknown write, or infer acceptance from the read's completion.
|
|
19
|
+
const attempt = managed.executionAttempt;
|
|
20
|
+
const liveAttempt = attempt?.decisionId === unit.id && attempt.phase !== 'checkpointed';
|
|
21
|
+
if (unit.action?.kind === 'read' && unit.status === 'dispatched' && !liveAttempt &&
|
|
22
|
+
managed.workHistory?.some(row => row.decisionId === unit.id && row.status === 'session_understanding' && row.result?.status === 'observed')) {
|
|
23
|
+
unit.status = 'observed';
|
|
24
|
+
}
|
|
25
|
+
if (!unit.action || ['verified', 'superseded', 'superseded_by_user_revision'].includes(unit.status)) continue;
|
|
26
|
+
unit.owner ||= managed.taskId;
|
|
27
|
+
if (unit.id.startsWith('wait:')) unit.waitReason ||= 'required_operation_observation';
|
|
28
|
+
unit.readResources = [...new Set((unit.readResources || []).map(id => id === 'task' ? workspace : id))];
|
|
29
|
+
unit.writeResources = [...new Set((unit.writeResources || []).map(id => id === 'task' ? workspace : id))];
|
|
30
|
+
if (unit.action.kind !== 'read' && !unit.writeResources.includes(workspace)) unit.writeResources.push(workspace);
|
|
31
|
+
}
|
|
32
|
+
// Missing PR readback stays blocked. This boundary never invents CI evidence.
|
|
33
|
+
const proposal = reconcileWorkFrontier(managed, { stopped: ['paused', 'stopped', 'cancelled'].includes(record.run.state) });
|
|
34
|
+
delete managed.frontierIssue;
|
|
35
|
+
return proposal;
|
|
36
|
+
} catch {
|
|
37
|
+
managed.frontierIssue = 'Existing work-unit ledger needs reconciliation before dispatch.';
|
|
38
|
+
return { blocked: true };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function admitRecoveryWork(record, proposal, leaseId, now) {
|
|
43
|
+
if (!proposal) return;
|
|
44
|
+
const managed = record.run.managed;
|
|
45
|
+
const unit = managed.purpose.evidence.units.find(row => row.id === managed.decision.id);
|
|
46
|
+
if (!unit && managed.decision.action.kind === 'read' && ['understanding', 'needs_clarification'].includes(managed.phase)) return;
|
|
47
|
+
if (!unit) throw new Error('Selected work unit is missing.');
|
|
48
|
+
const current = revalidateWorkProposal(managed, proposal, unit.id, {
|
|
49
|
+
stopped: ['paused', 'stopped', 'cancelled'].includes(record.run.state),
|
|
50
|
+
});
|
|
51
|
+
if (!current.eligible) {
|
|
52
|
+
throw new Error(`Work proposal needs reconciliation: ${current.reason || 'not eligible'}`);
|
|
53
|
+
}
|
|
54
|
+
unit.status = 'dispatched'; unit.dispatchedAt = now; unit.attemptId = leaseId;
|
|
55
|
+
}
|
|
@@ -4,14 +4,15 @@ import { deliveryStop } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
|
4
4
|
// Auto-continue works while nobody is watching, so a person must learn when it
|
|
5
5
|
// stops. Map a stopped run to a notification kind; the native window owns copy.
|
|
6
6
|
export function stopNotificationKind(run, now = Date.now()) {
|
|
7
|
-
if (!run?.managed?.autoContinueEpoch) return null;
|
|
7
|
+
if (!run?.managed?.autoContinueEpoch && !run?.managed?.repairRequest) return null;
|
|
8
8
|
// The clock keeps aging while paused; the persisted human decision takes
|
|
9
9
|
// precedence over a threshold that was reached only after that decision.
|
|
10
10
|
if (run.state === 'paused') return run.reason === 'Paused by user.' ? null : deliveryStop(run, now) ? 'paused' : null;
|
|
11
|
-
if (run.state === '
|
|
11
|
+
if (run.state === 'auto_resuming' && !run.managed.resumeLease?.owner) return 'needs_decision';
|
|
12
|
+
if (run.state === 'blocked' || run.state === 'verification_pending' || run.state === 'reported_done') return 'needs_decision';
|
|
12
13
|
if (run.state === 'recovery_required' && run.managed.autoRecovery !== true) return 'needs_decision';
|
|
13
14
|
if (run.state === 'failed' || run.state === 'budget_exhausted') return 'failed';
|
|
14
|
-
if (run.state === 'completed'
|
|
15
|
+
if (run.state === 'completed') return 'finished';
|
|
15
16
|
return null;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -24,19 +25,18 @@ export function createStopNotifier({ emit, now = Date.now } = {}) {
|
|
|
24
25
|
let baseline = true;
|
|
25
26
|
return {
|
|
26
27
|
observe(records) {
|
|
27
|
-
const present = new Set();
|
|
28
28
|
for (const record of records) {
|
|
29
29
|
const run = record?.run, kind = stopNotificationKind(run, now());
|
|
30
30
|
const key = JSON.stringify([record.provider, record.session, run?.managed?.taskId]);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const id = createHash('sha256').update(JSON.stringify([record.provider, record.session, run.managed.taskId, run.state, run.revision, run.reason])).digest('hex').slice(0, 32);
|
|
31
|
+
if (!kind) continue;
|
|
32
|
+
const id = createHash('sha256').update(JSON.stringify([record.provider, record.session, run.managed.taskId, run.id, kind, run.blocker, run.reason])).digest('hex').slice(0, 32);
|
|
34
33
|
if (seen.get(key) === id) continue;
|
|
35
34
|
seen.set(key, id);
|
|
36
35
|
if (!baseline) emit({ type: 'notify', id, kind, provider: record.provider, session: record.session,
|
|
37
36
|
objective: String(run.objective || '').slice(0, 200) });
|
|
38
37
|
}
|
|
39
|
-
|
|
38
|
+
// Temporary provider absence or a diagnostic pass is not a new stop.
|
|
39
|
+
// Retain fingerprints until a materially different outcome is observed.
|
|
40
40
|
baseline = false;
|
|
41
41
|
},
|
|
42
42
|
};
|
package/src/sqm/agent-skill.js
CHANGED
|
@@ -40,7 +40,7 @@ Prefer the fixed local \`$HOME/.simy/bin/simy\` (or \`$SIMY_HOME/bin/simy\`) whe
|
|
|
40
40
|
3. Read \`proof.json\` and require \`outcome.status\` to be \`passed\`.
|
|
41
41
|
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.
|
|
42
42
|
5. Verify the proof's repository, base, HEAD and working-tree/diff identity and its bound signature with SQM tooling. Use separate artifact paths outside the diff, and rerun after bound inputs change.
|
|
43
|
-
6. Include proof paths, outcome reasons, matched modules, evaluated rules and scenario statuses from \`executions\` in the PR evidence, separating blocking and advisory results. A pass with \`no_applicable_rules\`, zero scenario evaluations or advisory-only checks does not establish feature or historical incident coverage. Scenario statuses are deterministic evaluations, not browser/provider/deployed execution evidence. Retain relevant uncovered incidents and their concrete regression result or gap; do not promote incomplete delivery acceptance.
|
|
43
|
+
6. Include proof paths, outcome reasons, matched modules, evaluated rules and scenario statuses from \`executions\` in the PR evidence, separating blocking and advisory results. A pass with \`no_applicable_rules\`, zero scenario evaluations or advisory-only checks does not establish feature or historical incident coverage. Scenario statuses are deterministic evaluations, not browser/provider/deployed execution evidence. Retain relevant uncovered incidents and their concrete regression result or gap; do not promote incomplete delivery acceptance. Zero coverage alone is not a Delivery Loop missing_information blocker: investigate rule applicability and available verification, and continue authorized repair or testing. Keep requested SQM acceptance unmet until it is actually satisfied; alternative tests do not substitute for it. Use an information wait only for a specific question that requires the user's answer.
|
|
44
44
|
|
|
45
45
|
Do not replace SQM with an LLM review, and do not claim the check passed without reading the generated proof.
|
|
46
46
|
Use existing authorization for minimal related repair and recheck; do not request another continuation after ordinary blockers are resolved. LLM judgment is the base, relevant HC-01–HC-102 lenses are supplemental, and explicit stops, irreversible actions and real permissions remain boundaries.
|
package/src/sqm/checkers.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isAdvisoryGuidance, advisoryGuidanceDescriptions } from './advisory-guidance.js';
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { lstat, readFile } from "node:fs/promises";
|
|
3
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
import { lintKnowledgeModule } from "./reliability.js";
|
|
@@ -307,7 +307,9 @@ function activeExceptionPatterns(module) {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
function selectPaths(paths, globs) {
|
|
310
|
-
|
|
310
|
+
const selected = paths.filter((file) => matchesAny(globs, file));
|
|
311
|
+
if (selected.length > MAX_FILES_PER_PATTERN) throw new Error("SQM inspection scope exceeds file limit; coverage is incomplete.");
|
|
312
|
+
return selected;
|
|
311
313
|
}
|
|
312
314
|
|
|
313
315
|
function matchesAny(globs, file) {
|
|
@@ -365,12 +367,16 @@ async function readText(root, relative) {
|
|
|
365
367
|
const resolved = path.resolve(root, relative);
|
|
366
368
|
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) throw new Error("SQM path escaped the repository root.");
|
|
367
369
|
try {
|
|
370
|
+
const actualRoot = await realpath(root);
|
|
371
|
+
const actualFile = await realpath(resolved);
|
|
372
|
+
if (!actualFile.startsWith(`${actualRoot}${path.sep}`)) throw new Error("SQM path escaped the repository root.");
|
|
368
373
|
const fileStat = await lstat(resolved);
|
|
369
|
-
if (!fileStat.isFile() || fileStat.isSymbolicLink())
|
|
370
|
-
|
|
371
|
-
|
|
374
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink()) throw new Error("Not a regular file.");
|
|
375
|
+
if (fileStat.size > MAX_FILE_BYTES) throw new Error("Unreadable text scope.");
|
|
376
|
+
const buffer = await readFile(actualFile);
|
|
377
|
+
if (buffer.length > MAX_FILE_BYTES || buffer.includes(0)) throw new Error("Unreadable text scope.");
|
|
372
378
|
return buffer.toString("utf8");
|
|
373
379
|
} catch {
|
|
374
|
-
|
|
380
|
+
throw new Error(`SQM could not inspect ${relative}; coverage is incomplete.`);
|
|
375
381
|
}
|
|
376
382
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { readFile, realpath } from "node:fs/promises";
|
|
1
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { CLAUDE_MARKETPLACE, CLAUDE_PLUGIN_SELECTOR } from "./claude-plugin.js";
|
|
4
|
+
import { pluginCacheMatches, repairPluginCache } from "./plugin-cache.js";
|
|
4
5
|
|
|
5
6
|
export async function refreshClaudePlugin({ status, home, claudeHome, resolveExecutable, run }) {
|
|
6
7
|
if (!status.claude_plugin?.installed) return { outcome: "not_enabled" };
|
|
@@ -19,21 +20,34 @@ export async function refreshClaudePlugin({ status, home, claudeHome, resolveExe
|
|
|
19
20
|
await realpath(marketplace.path) !== await realpath(status.claude_plugin.marketplace_root)) {
|
|
20
21
|
throw new Error("Enabled SIMY Claude plugin points to a different marketplace; refresh skipped.");
|
|
21
22
|
}
|
|
22
|
-
const
|
|
23
|
-
if (!entry?.installPath || entry.version !== status.claude_plugin.version) return
|
|
23
|
+
const cacheLocation = async (entry) => {
|
|
24
|
+
if (!entry?.installPath || entry.version !== status.claude_plugin.version) return null;
|
|
24
25
|
const cacheRoot = await realpath(path.join(claudeHome, "plugins/cache")).catch(() => null);
|
|
25
|
-
if (!cacheRoot) return
|
|
26
|
+
if (!cacheRoot) return null;
|
|
26
27
|
const installedPath = await realpath(entry.installPath).catch(() => null);
|
|
27
28
|
const relative = installedPath && path.relative(cacheRoot, installedPath);
|
|
28
|
-
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return
|
|
29
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return null;
|
|
30
|
+
return installedPath;
|
|
31
|
+
};
|
|
32
|
+
const cacheCurrent = async (entry) => {
|
|
33
|
+
const installedPath = await cacheLocation(entry);
|
|
34
|
+
if (!installedPath) return false;
|
|
29
35
|
try {
|
|
30
|
-
return JSON.parse(await readFile(path.join(installedPath, ".claude-plugin/plugin.json"), "utf8")).version === status.claude_plugin.version
|
|
31
|
-
|
|
36
|
+
return JSON.parse(await readFile(path.join(installedPath, ".claude-plugin/plugin.json"), "utf8")).version === status.claude_plugin.version &&
|
|
37
|
+
await pluginCacheMatches(status.claude_plugin.path, installedPath);
|
|
38
|
+
} catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code) || error instanceof SyntaxError) return false; throw error; }
|
|
32
39
|
};
|
|
33
40
|
if (await cacheCurrent(installed)) return { outcome: "current" };
|
|
34
41
|
await run(executable, ["plugin", "marketplace", "update", CLAUDE_MARKETPLACE], options);
|
|
35
42
|
await run(executable, ["plugin", "update", CLAUDE_PLUGIN_SELECTOR, "--scope", "user"], options);
|
|
36
|
-
|
|
43
|
+
let refreshed = (await list()).find((entry) => entry.id === CLAUDE_PLUGIN_SELECTOR && entry.scope === "user");
|
|
44
|
+
if (refreshed?.enabled && !await cacheCurrent(refreshed)) {
|
|
45
|
+
const location = await cacheLocation(refreshed);
|
|
46
|
+
if (location && !(await lstat(refreshed.installPath)).isSymbolicLink()) {
|
|
47
|
+
await repairPluginCache(status.claude_plugin.path, location);
|
|
48
|
+
refreshed = (await list()).find((entry) => entry.id === CLAUDE_PLUGIN_SELECTOR && entry.scope === "user");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
37
51
|
if (!refreshed?.enabled || !await cacheCurrent(refreshed)) {
|
|
38
52
|
throw new Error("Claude Code did not confirm the updated SIMY plugin cache; will retry.");
|
|
39
53
|
}
|
package/src/sqm/claude-plugin.js
CHANGED
|
@@ -25,6 +25,9 @@ export async function claudePluginFiles() {
|
|
|
25
25
|
// Claude workflow without shipping it back into Codex's skill inventory.
|
|
26
26
|
files.set("skills/delivery-loop/SKILL.md", await readFile(
|
|
27
27
|
new URL("../delivery-hooks/claude-workflow.md", import.meta.url), "utf8"));
|
|
28
|
+
for (const name of ["loop-kernel.mjs", "loop-kernel/index.mjs", "loop-kernel/policy.json", "loop-kernel/pin.json", "loop-kernel/outcome-review.mjs", "loop-kernel/work-frontier.mjs", "loop-kernel/task-pool.mjs"]) {
|
|
29
|
+
files.set(`hooks/${name}`, await readFile(path.join(SOURCE, "hooks", name), "utf8"));
|
|
30
|
+
}
|
|
28
31
|
files.set("hooks/delivery-loop.mjs", await readFile(path.join(SOURCE, "hooks/delivery-loop.mjs"), "utf8"));
|
|
29
32
|
files.set("hooks/recovery-incident.mjs", await readFile(path.join(SOURCE, "hooks/recovery-incident.mjs"), "utf8"));
|
|
30
33
|
files.set("hooks/minimal-sufficient.mjs", await readFile(path.join(SOURCE, "hooks/minimal-sufficient.mjs"), "utf8"));
|
|
@@ -38,7 +41,10 @@ export async function claudePluginFiles() {
|
|
|
38
41
|
files.set("hooks/purpose-search.mjs", await readFile(path.join(SOURCE, "hooks/purpose-search.mjs"), "utf8"));
|
|
39
42
|
files.set("hooks/completion-binding.mjs", await readFile(path.join(SOURCE, "hooks/completion-binding.mjs"), "utf8"));
|
|
40
43
|
files.set("hooks/consistency-handoff.mjs", await readFile(path.join(SOURCE, "hooks/consistency-handoff.mjs"), "utf8"));
|
|
44
|
+
files.set("hooks/purpose-completion-contract.mjs", await readFile(path.join(SOURCE, "hooks/purpose-completion-contract.mjs"), "utf8"));
|
|
41
45
|
files.set("hooks/purpose-evidence.mjs", await readFile(path.join(SOURCE, "hooks/purpose-evidence.mjs"), "utf8"));
|
|
46
|
+
files.set("hooks/user-experience.mjs", await readFile(path.join(SOURCE, "hooks/user-experience.mjs"), "utf8"));
|
|
47
|
+
files.set("hooks/result-cycle.mjs", await readFile(path.join(SOURCE, "hooks/result-cycle.mjs"), "utf8"));
|
|
42
48
|
files.set("hooks/scene-reasoning.mjs", await readFile(path.join(SOURCE, "hooks/scene-reasoning.mjs"), "utf8"));
|
|
43
49
|
files.set("hooks/purpose-cycle.mjs", await readFile(path.join(SOURCE, "hooks/purpose-cycle.mjs"), "utf8"));
|
|
44
50
|
files.set("hooks/purpose-rule-policy.mjs", await readFile(path.join(SOURCE, "hooks/purpose-rule-policy.mjs"), "utf8"));
|
package/src/sqm/local-tests.js
CHANGED
|
@@ -8,7 +8,8 @@ import path from 'node:path';
|
|
|
8
8
|
// This catalog is shipped with the CLI; a repository cannot supply shell commands.
|
|
9
9
|
const profiles = {
|
|
10
10
|
'delivery-loop': [
|
|
11
|
-
{ id: '
|
|
11
|
+
{ id: 'stop-investigation-regressions', files: ['test/result-cycle.test.js', 'test/delivery-result-followthrough.test.js', 'test/corrective-followup.test.js', 'test/stop-investigation-regression.test.js', 'test/acceptance-reads.test.js', 'test/browser-recovery.test.js', 'test/browser-cdp.test.js', 'test/interaction-task.test.js', 'test/native-interaction.test.js'] },
|
|
12
|
+
{ id: 'autonomous-recovery', files: ['test/autonomous-recovery.test.js', 'test/scene-reasoning.test.js', 'test/supervisor-watchdog.test.js', 'test/supervisor-handover.test.js', 'test/work-frontier.test.js', 'test/legacy-hook-routing.test.js', 'test/procedure-free-review.test.js'] },
|
|
12
13
|
{ id: 'perspective-memory', files: ['test/perspective-memory.test.js'] },
|
|
13
14
|
{ id: 'optional-reasoning', files: ['test/purpose-selective-rules.test.js', 'test/purpose-evidence.test.js', 'test/purpose-rule-review.test.js'] },
|
|
14
15
|
// This connected controller group now contains over 200 cases. Keep a
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// A matching manifest version does not prove that the cached hooks can run.
|
|
6
|
+
// Compare every shipped file; provider-owned cache metadata may coexist.
|
|
7
|
+
export async function pluginCacheMatches(source, cache) {
|
|
8
|
+
async function visit(sourceDirectory, cacheDirectory) {
|
|
9
|
+
const target = await lstat(cacheDirectory);
|
|
10
|
+
if (!target.isDirectory() || target.isSymbolicLink()) return false;
|
|
11
|
+
for (const entry of await readdir(sourceDirectory, { withFileTypes: true })) {
|
|
12
|
+
const from = path.join(sourceDirectory, entry.name);
|
|
13
|
+
const to = path.join(cacheDirectory, entry.name);
|
|
14
|
+
if (entry.isDirectory()) {
|
|
15
|
+
if (!await visit(from, to)) return false;
|
|
16
|
+
} else {
|
|
17
|
+
if (!entry.isFile()) return false;
|
|
18
|
+
const cached = await lstat(to);
|
|
19
|
+
if (!cached.isFile() || cached.isSymbolicLink()) return false;
|
|
20
|
+
if (!(await readFile(from)).equals(await readFile(to))) return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
try { return await visit(source, cache); }
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (['ENOENT', 'ENOTDIR'].includes(error.code)) return false;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Some providers skip recopying a cache whose version already matches. Restore
|
|
33
|
+
// only the shipped files of an already enabled, source-verified cache. Never
|
|
34
|
+
// follow links or remove provider-owned metadata. Each file replacement is atomic.
|
|
35
|
+
export async function repairPluginCache(source, cache) {
|
|
36
|
+
const directory = async target => {
|
|
37
|
+
const info = await lstat(target);
|
|
38
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('Plugin cache repair requires real directories.');
|
|
39
|
+
};
|
|
40
|
+
await directory(source);
|
|
41
|
+
await directory(cache);
|
|
42
|
+
async function visit(from, to) {
|
|
43
|
+
await directory(from);
|
|
44
|
+
await directory(to);
|
|
45
|
+
for (const entry of await readdir(from, { withFileTypes: true })) {
|
|
46
|
+
const original = path.join(from, entry.name), target = path.join(to, entry.name);
|
|
47
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() && !entry.isFile()) throw new Error('Plugin source contains an unsupported file.');
|
|
48
|
+
let existing;
|
|
49
|
+
try { existing = await lstat(target); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
50
|
+
if (existing?.isSymbolicLink()) throw new Error('Plugin cache repair refuses symbolic links.');
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
if (!existing) await mkdir(target);
|
|
53
|
+
await visit(original, target);
|
|
54
|
+
} else {
|
|
55
|
+
if (existing && !existing.isFile()) throw new Error('Plugin cache repair requires regular files.');
|
|
56
|
+
const contents = await readFile(original);
|
|
57
|
+
if (existing && contents.equals(await readFile(target))) continue;
|
|
58
|
+
const temporary = path.join(to, `.simy-repair-${randomUUID()}`);
|
|
59
|
+
try {
|
|
60
|
+
await writeFile(temporary, contents, { flag: 'wx', mode: (await lstat(original)).mode & 0o777 });
|
|
61
|
+
await rename(temporary, target);
|
|
62
|
+
} finally { await rm(temporary, { force: true }); }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await visit(source, cache);
|
|
67
|
+
}
|
package/src/sqm/proof.js
CHANGED
|
@@ -75,6 +75,7 @@ export function buildProof({ organizationId, repository, bundle, minCheck, evalu
|
|
|
75
75
|
enforcement: 'blocking', rule_results: [], scenarios: [], state_models: [], stressors: [],
|
|
76
76
|
local_tests: localTests,
|
|
77
77
|
}] : [])],
|
|
78
|
+
inspection_scope: sqmInspectionScope(evaluation.modules),
|
|
78
79
|
findings,
|
|
79
80
|
outcome: {
|
|
80
81
|
status,
|
|
@@ -159,6 +160,30 @@ function taggedDigest(value) {
|
|
|
159
160
|
return `sha256:${createHash("sha256").update(canonicalize(value), "utf8").digest("hex")}`;
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
// This scope is part of the signed proof. Static scenarios never become UI or
|
|
164
|
+
// human acceptance evidence, even when the legacy enforcement outcome is passed.
|
|
165
|
+
export function sqmInspectionScope(modules) {
|
|
166
|
+
const checks = modules.flatMap(module => (module.rule_results || []).map(rule => ({
|
|
167
|
+
module_id: module.module_id, rule_id: rule.rule_id, status: rule.status,
|
|
168
|
+
})));
|
|
169
|
+
return { evidence_kind: "static_check", checks, checked_count: checks.length,
|
|
170
|
+
status: checks.length === 0 ? "unconfirmed" : checks.every(check => check.status === "passed") ? "verified" : "unmet",
|
|
171
|
+
scenario_evaluations: modules.reduce((count, module) => count + (module.scenarios || []).length, 0),
|
|
172
|
+
scenario_generation: "not_observed", real_ui_test: "not_observed", human_acceptance: "not_observed" };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function verifyProofScope(proof, evidence, { repository, requiredChecks, trustedKeyId } = {}) {
|
|
176
|
+
const verified = verifySignedEvidence(proof, evidence);
|
|
177
|
+
if (!trustedKeyId || verified.key_id !== trustedKeyId) throw new Error("SQM signer is not the enrolled trusted key.");
|
|
178
|
+
if (!repository?.head_commit || !sameRepositoryIdentity(proof.repository_identity, repositoryIdentity(repository))) throw new Error("SQM proof revision is stale.");
|
|
179
|
+
const scope = sqmInspectionScope(proof.executions);
|
|
180
|
+
if (canonicalize(scope) !== canonicalize(proof.inspection_scope || null)) throw new Error("SQM inspection scope is missing or inconsistent.");
|
|
181
|
+
if (!Array.isArray(requiredChecks) || !requiredChecks.length || scope.status !== "verified" ||
|
|
182
|
+
requiredChecks.some(required => !scope.checks.some(check => check.module_id === required.module_id && check.rule_id === required.rule_id && check.status === "passed"))) throw new Error("SQM required inspection coverage is incomplete.");
|
|
183
|
+
if (proof.outcome.status !== "passed") throw new Error("SQM enforcement did not pass.");
|
|
184
|
+
return { ...verified, evidence_kind: "static_check", checked_count: scope.checked_count };
|
|
185
|
+
}
|
|
186
|
+
|
|
162
187
|
export function localTestsFromProof(proof) {
|
|
163
188
|
return proof?.executions?.find(row => row.kind === 'local_test_profile')?.local_tests || null;
|
|
164
189
|
}
|
package/src/sqm/report.js
CHANGED
|
@@ -25,6 +25,7 @@ export function formatSqmCoverage(proof) {
|
|
|
25
25
|
const semantic = rules.filter(rule => rule.assessment).map(rule => rule.assessment);
|
|
26
26
|
if (semantic.length) lines.push(`Semantic assessments: ${semantic.filter(result => result.executed).length} executed · ${semantic.filter(result => result.outcome === 'scored').length} scored · ${semantic.filter(result => result.outcome !== 'scored').length} inconclusive. Local model judgments are advisory and do not establish independent acceptance.`);
|
|
27
27
|
lines.push("Scenario evaluations are deterministic checks, not browser/provider/deployed journey evidence. Record incident coverage and delivery acceptance separately.");
|
|
28
|
+
if (proof.inspection_scope) lines.push(`Artifact/UI/human acceptance: not established by this static proof; inspection status=${proof.inspection_scope.status}.`);
|
|
28
29
|
return lines.join("\n");
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
|
-
|
|
3
1
|
import { inspectSqmAgentSkill, installSqmAgentSkill } from "./agent-skill.js";
|
|
4
2
|
|
|
5
3
|
const INSTALL_FLAG = "--install-workflows";
|
|
@@ -9,11 +7,8 @@ export async function prepareDaemonWorkflows({
|
|
|
9
7
|
argv,
|
|
10
8
|
daemon,
|
|
11
9
|
daemonChild = process.env.SIMY_DAEMON_CHILD === "1",
|
|
12
|
-
stdinIsTTY = process.stdin.isTTY === true,
|
|
13
|
-
stdoutIsTTY = process.stdout.isTTY === true,
|
|
14
10
|
inspect = inspectSqmAgentSkill,
|
|
15
11
|
install = installSqmAgentSkill,
|
|
16
|
-
confirm = confirmWorkflowInstallation,
|
|
17
12
|
logger = console.log,
|
|
18
13
|
errorLogger = console.error,
|
|
19
14
|
} = {}) {
|
|
@@ -42,23 +37,12 @@ export async function prepareDaemonWorkflows({
|
|
|
42
37
|
return { outcome: "update_pending", status };
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"SIMY Codex/Claude workflows are missing or outdated. Re-run in a terminal with --install-workflows to install them.",
|
|
48
|
-
);
|
|
49
|
-
return { outcome: "confirmation_unavailable", status };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const accepted = installRequested || (await confirm({ status }));
|
|
53
|
-
if (!accepted) {
|
|
54
|
-
logger("SIMY workflow installation skipped. The daemon will still start.");
|
|
55
|
-
return { outcome: "declined", status };
|
|
56
|
-
}
|
|
57
|
-
|
|
40
|
+
// Source preparation is CLI startup work, including unattended service starts.
|
|
41
|
+
// Provider plugin enablement and hook trust remain provider-owned controls.
|
|
58
42
|
try {
|
|
59
43
|
const installed = await install();
|
|
60
44
|
logger(
|
|
61
|
-
"SIMY workflows installed. In Codex, restart, then find and install SIMY in Plugins
|
|
45
|
+
"SIMY workflows installed. In Codex, restart, then find and install SIMY in Plugins. In Claude Code, add the SIMY marketplace and install simy@simy-local before using /simy:sqm-loop or /simy:pr-review. For Delivery Loop, review and enable SIMY hooks in /hooks. Delivery Loop then starts automatically on user prompts, without per-task commands. Plugin installation does not grant hook trust.",
|
|
62
46
|
);
|
|
63
47
|
if (installed.claude_plugin?.marketplace_root) {
|
|
64
48
|
logger(`Claude Code: claude plugin marketplace add ${JSON.stringify(installed.claude_plugin.marketplace_root)}`);
|
|
@@ -71,26 +55,6 @@ export async function prepareDaemonWorkflows({
|
|
|
71
55
|
}
|
|
72
56
|
}
|
|
73
57
|
|
|
74
|
-
export async function confirmWorkflowInstallation({
|
|
75
|
-
input = process.stdin,
|
|
76
|
-
output = process.stdout,
|
|
77
|
-
} = {}) {
|
|
78
|
-
output.write(
|
|
79
|
-
[
|
|
80
|
-
"SIMY can install or update its Codex and Claude Code plugin sources and opt-in SQM skills.",
|
|
81
|
-
"Existing unrelated plugins and hooks will be preserved.",
|
|
82
|
-
"",
|
|
83
|
-
].join("\n"),
|
|
84
|
-
);
|
|
85
|
-
const prompt = createInterface({ input, output });
|
|
86
|
-
try {
|
|
87
|
-
const answer = await prompt.question("Install SIMY workflows now? [y/N] ");
|
|
88
|
-
return /^(y|yes)$/i.test(answer.trim());
|
|
89
|
-
} finally {
|
|
90
|
-
prompt.close();
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
58
|
function message(error) {
|
|
95
59
|
return error instanceof Error ? error.message : String(error);
|
|
96
60
|
}
|
|
@@ -7,6 +7,7 @@ import { pinnedWorkflowSource } from "../delivery-hooks/local-runtime.js";
|
|
|
7
7
|
|
|
8
8
|
import { runProviderCommand } from "./provider-command.js";
|
|
9
9
|
import { refreshClaudePlugin } from "./claude-plugin-update.js";
|
|
10
|
+
import { pluginCacheMatches } from "./plugin-cache.js";
|
|
10
11
|
|
|
11
12
|
import { resolveBackendExecutable } from "../backend-executable.js";
|
|
12
13
|
import { inspectSqmAgentSkill, installSqmAgentSkill, retireLegacySqmGate } from "./agent-skill.js";
|
|
@@ -14,13 +15,18 @@ import { inspectSqmAgentSkill, installSqmAgentSkill, retireLegacySqmGate } from
|
|
|
14
15
|
|
|
15
16
|
const CHECK_INTERVAL_MS = 60 * 60 * 1000;
|
|
16
17
|
|
|
17
|
-
export function startWorkflowUpdates({ enabled, sync = syncInstalledWorkflows, intervalMs = CHECK_INTERVAL_MS,
|
|
18
|
+
export function startWorkflowUpdates({ enabled, installMissing = true, sync = syncInstalledWorkflows, intervalMs = CHECK_INTERVAL_MS,
|
|
18
19
|
errorLogger = console.error } = {}) {
|
|
19
20
|
let pending;
|
|
20
21
|
let stopped = !enabled;
|
|
21
22
|
const check = () => {
|
|
22
23
|
if (stopped || pending) return;
|
|
23
|
-
pending = Promise.resolve().then(sync).
|
|
24
|
+
pending = Promise.resolve().then(() => sync({ installMissing })).then((result) => {
|
|
25
|
+
if (result?.refreshPending) {
|
|
26
|
+
const details = result.refreshFailures?.map(({ provider, message }) => `${provider}: ${message}`).join('; ');
|
|
27
|
+
errorLogger(`SIMY workflow sources are prepared, but plugin cache refresh is pending; will retry later.${details ? ` ${details}` : ''}`);
|
|
28
|
+
}
|
|
29
|
+
}).catch((error) => {
|
|
24
30
|
errorLogger(`SIMY workflow update failed; will retry later: ${error.message}`);
|
|
25
31
|
}).finally(() => { pending = null; });
|
|
26
32
|
};
|
|
@@ -79,16 +85,20 @@ export async function syncInstalledWorkflows({ home = homedir(), installMissing
|
|
|
79
85
|
if (platforms.length) status = await install({ home, platforms });
|
|
80
86
|
const results = {};
|
|
81
87
|
const failures = [];
|
|
88
|
+
const refreshFailures = [];
|
|
82
89
|
for (const [provider, refresh] of [
|
|
83
90
|
["codex", () => refreshCodexPlugin({ status, home, codexHome, resolveExecutable, run })],
|
|
84
91
|
["claude", () => refreshClaudePlugin({ status, home, claudeHome, resolveExecutable, run })],
|
|
85
92
|
]) {
|
|
86
93
|
try { results[provider] = await refresh(); }
|
|
87
|
-
catch (error) {
|
|
94
|
+
catch (error) {
|
|
95
|
+
failures.push(error);
|
|
96
|
+
refreshFailures.push({ provider, message: error.message });
|
|
97
|
+
}
|
|
88
98
|
}
|
|
89
99
|
// Explicit desktop setup prepares both sources even when a provider CLI is
|
|
90
100
|
// absent. Cache refresh remains retryable; no provider is enabled here.
|
|
91
|
-
if (failures.length && installMissing) return { outcome: "prepared", refreshPending: true };
|
|
101
|
+
if (failures.length && installMissing) return { outcome: "prepared", refreshPending: true, refreshFailures, providers: results };
|
|
92
102
|
if (failures.length) throw new AggregateError(failures, failures.map((error) => error.message).join("; "));
|
|
93
103
|
if (results.claude.outcome === "updated") return { ...results.claude, providers: results };
|
|
94
104
|
if (results.codex.outcome !== "not_enabled") return results.codex;
|
|
@@ -123,8 +133,9 @@ async function refreshCodexPlugin({ status, home, codexHome, resolveExecutable,
|
|
|
123
133
|
const cacheManifest = path.join(codexHome, "plugins", "cache", marketplace.name,
|
|
124
134
|
"simy", status.codex_plugin.version, ".codex-plugin", "plugin.json");
|
|
125
135
|
const cacheCurrent = async () => {
|
|
126
|
-
try { return JSON.parse(await readFile(cacheManifest, "utf8")).version === status.codex_plugin.version
|
|
127
|
-
|
|
136
|
+
try { return JSON.parse(await readFile(cacheManifest, "utf8")).version === status.codex_plugin.version &&
|
|
137
|
+
await pluginCacheMatches(status.codex_plugin.path, path.dirname(path.dirname(cacheManifest))); }
|
|
138
|
+
catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code) || error instanceof SyntaxError) return false; throw error; }
|
|
128
139
|
};
|
|
129
140
|
if (installed.version === status.codex_plugin.version && await cacheCurrent()) return { outcome: "current" };
|
|
130
141
|
await run(executable, ["plugin", "add", selector, "--json"], options);
|
package/src/startup-preflight.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createCodingLoopSnapshot } from "./orchestrator/contract.js";
|
|
2
|
-
import { validateCharter } from "./orchestrator/charter-validation.js";
|
|
3
1
|
|
|
4
2
|
// Reuse the execution charter rather than inventing a second approval policy.
|
|
5
3
|
// These checks inspect local metadata only; model access requires real dispatch.
|
|
@@ -9,28 +7,12 @@ export function startupRequirementChecks(request = {}) {
|
|
|
9
7
|
action: { id: "review_model_access", type: "inspect_provider", label: "Review provider and model settings" } }];
|
|
10
8
|
if (!String(request.requirement || "").trim()) {
|
|
11
9
|
checks.push({ key: "task_requirements", status: "unknown", blocking: false,
|
|
12
|
-
summary: "Environment check only. Supply the task requirement to
|
|
10
|
+
summary: "Environment check only. Supply the task requirement to identify the requested outcome.",
|
|
13
11
|
action: { id: "provide_requirement", type: "edit_requirements", label: "Provide task requirements" } });
|
|
14
12
|
return checks;
|
|
15
13
|
}
|
|
16
|
-
const { charter } = createCodingLoopSnapshot({ runId: "preflight", request });
|
|
17
|
-
const findings = validateCharter(charter);
|
|
18
|
-
for (const [key, code, passed, actionId, label] of [
|
|
19
|
-
["acceptance_criteria", "EXPLICIT_ACCEPTANCE_CRITERIA_REQUIRED", "Acceptance criteria satisfy the task risk policy.", "provide_acceptance_criteria", "Provide explicit acceptance criteria"],
|
|
20
|
-
["design_review", "DESIGN_REVIEW_REQUIRED", "Design approval satisfies the task risk policy.", "approve_design", "Provide design review and approval evidence"],
|
|
21
|
-
["required_checks", "REQUIRED_CI_CHECKS_MISSING", "Required checks satisfy the task risk policy.", "provide_required_checks", "Provide CI and security check names"],
|
|
22
|
-
]) {
|
|
23
|
-
const finding = findings.find(item => item.code === code);
|
|
24
|
-
checks.push({ key, status: finding ? "failed" : "passed", blocking: Boolean(finding),
|
|
25
|
-
summary: finding?.explanation || passed,
|
|
26
|
-
...(finding ? { code, action: { id: actionId, type: "edit_requirements", label } } : {}) });
|
|
27
|
-
}
|
|
28
|
-
checks.push({ key: "assurance_selection", status: "unknown", blocking: false,
|
|
29
|
-
summary: "The executor must verify the five-level comparison and user selection in existing decision evidence before material implementation.",
|
|
30
|
-
action: { id: "review_assurance_selection", type: "review_decision_evidence", label: "Review assurance level and selection evidence" } });
|
|
31
14
|
checks.push({ key: "external_dependencies", status: "unknown", blocking: false,
|
|
32
|
-
summary: "
|
|
33
|
-
action: { id: "confirm_external_dependencies", type: "edit_requirements", label: "Confirm external inputs and permissions" } });
|
|
15
|
+
summary: "Check external inputs and permissions when the requested action needs them. Local readiness does not certify Cloud entitlement." });
|
|
34
16
|
return checks;
|
|
35
17
|
}
|
|
36
18
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
// Only an opaque device/account/org/thread digest and a pending OFF version
|
|
5
|
+
// persist. Conversation text and provider identifiers never enter this store.
|
|
6
|
+
export function createTeamStatusFenceStore(root) {
|
|
7
|
+
const directory = join(root, 'team-status-revocations');
|
|
8
|
+
const path = key => {
|
|
9
|
+
if (!/^[a-f0-9]{64}$/.test(key)) throw new Error('Invalid revocation scope');
|
|
10
|
+
return join(directory, `${key}.json`);
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
async read(key) {
|
|
14
|
+
let raw;
|
|
15
|
+
try { raw = await readFile(path(key), 'utf8'); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
16
|
+
const value = JSON.parse(raw);
|
|
17
|
+
if (value?.schemaVersion !== 1 || !Number.isSafeInteger(value.expectedVersion) || value.expectedVersion < 0) throw new Error('Invalid revocation fence');
|
|
18
|
+
return { expectedVersion: value.expectedVersion };
|
|
19
|
+
},
|
|
20
|
+
async write(key, expectedVersion) {
|
|
21
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
22
|
+
const destination = path(key), temporary = `${destination}.${randomUUID()}.tmp`;
|
|
23
|
+
try {
|
|
24
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
25
|
+
try { await handle.writeFile(JSON.stringify({schemaVersion:1,expectedVersion})); await handle.sync(); } finally { await handle.close(); }
|
|
26
|
+
await rename(temporary, destination);
|
|
27
|
+
if (process.platform !== 'win32') { const parent = await open(directory, 'r'); try { await parent.sync(); } finally { await parent.close(); } }
|
|
28
|
+
} finally { await rm(temporary, {force:true}); }
|
|
29
|
+
},
|
|
30
|
+
async clear(key) { await rm(path(key), {force:true}); },
|
|
31
|
+
};
|
|
32
|
+
}
|