@awak-app/simy-cli 0.7.37 → 0.7.39
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 +55 -5
- package/package.json +2 -1
- package/src/auto-update.js +25 -13
- package/src/claude-desktop/bridge.js +156 -0
- package/src/claude-desktop/extension.js +84 -0
- package/src/claude-desktop/server.js +86 -0
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +2 -2
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +240 -24
- package/src/codex-plugins/simy/hooks/mention-router.mjs +42 -0
- package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +4 -1
- package/src/codex-plugins/simy/hooks/registered-requirement-checker.mjs +201 -0
- package/src/codex-plugins/simy/hooks/requirement-acceptance.mjs +69 -0
- package/src/codex-plugins/simy/skills/continue/SKILL.md +8 -3
- package/src/delivery-hooks/supervisor-watchdog.js +3 -2
- package/src/desktop-host.js +48 -18
- package/src/index.js +10 -0
- package/src/runner.js +28 -26
- package/src/session-supervision/acceptance-reads.js +11 -2
- package/src/session-supervision/analysis-manager.js +9 -2
- package/src/session-supervision/catalog-refresh.js +16 -0
- package/src/session-supervision/claude-native-agent-contract.js +62 -0
- package/src/session-supervision/claude-outcome-review.js +7 -6
- package/src/session-supervision/claude-sessions.js +137 -76
- package/src/session-supervision/codex-app-server.js +3 -3
- package/src/session-supervision/codex-children.js +2 -2
- package/src/session-supervision/conversation-context.js +58 -26
- package/src/session-supervision/discovery-window.js +8 -2
- package/src/session-supervision/index.js +25 -12
- package/src/session-supervision/purpose-verification.js +12 -3
- package/src/sqm/advisory-guidance.js +1 -0
- package/src/sqm/checkers.js +39 -1
- package/src/sqm/claude-plugin.js +15 -1
- package/src/sqm/claude-simy-agent.md +11 -0
- package/src/sqm/delivery-assessment.js +78 -0
- package/src/sqm/hypothesis-knowledge.js +3 -3
- package/src/sqm/knowledge-recall.js +34 -12
- package/src/sqm/reasoned-assessment.js +27 -12
- package/src/sqm/workflow-onboarding.js +2 -1
- package/src/update-detachment/runtime.js +2 -1
|
@@ -12,6 +12,8 @@ import { recordHandoffStage, compactConsistencyOutbox } from './consistency-hand
|
|
|
12
12
|
import { retainApplicableRules } from './purpose-rule-policy.mjs';
|
|
13
13
|
import { reconcileRequirements, retainControllerEvidence, reconcileFindings, findingText, evidenceLedger, currentPurposeObservation } from './purpose-evidence.mjs';
|
|
14
14
|
import { workspaceIdentity } from './purpose-workspace.mjs';
|
|
15
|
+
import { normalizeRequirementContract, pendingRequirements } from './requirement-acceptance.mjs';
|
|
16
|
+
import { bindRequirementChecker, registeredCheckerCurrent, registeredRequirementObserver, isRegisteredRequirementObserver } from './registered-requirement-checker.mjs';
|
|
15
17
|
import { actionReversibilityInstruction, searchEnabled, failureLimitReached, searchState, recordSearchExperiment, rememberSearchWait, searchNeedsPivot, markSearchPivot, repeatedSearchAction } from './purpose-search.mjs';
|
|
16
18
|
// Standalone by design: this exact module is shipped to both provider plugins.
|
|
17
19
|
import { createHash, randomUUID } from 'node:crypto';
|
|
@@ -40,7 +42,7 @@ const RETRYABLE = new Set(['overloaded', 'server_error', 'rate_limit']);
|
|
|
40
42
|
const APPROVAL_DECISIONS = new Set(['automatic', 'proceed_and_report', 'wait']);
|
|
41
43
|
const LOCAL_ACTIONS = new Set(['read', 'local_edit', 'local_test']);
|
|
42
44
|
const ACTIONS = new Set([...LOCAL_ACTIONS, 'external', 'destructive', 'scope_change', 'unknown']);
|
|
43
|
-
const AUTONOMY_POLICIES = new Set(['strict', 'balanced', 'autonomous']);
|
|
45
|
+
const AUTONOMY_POLICIES = new Set(['strict', 'balanced', 'autonomous', 'local_single_file']);
|
|
44
46
|
const ASSESSMENT_FIELDS = { risk: ['low', 'medium', 'high', 'unknown'], reversibility: ['reversible', 'irreversible', 'unknown'], externalEffect: ['none', 'local', 'external', 'unknown'], blastRadius: ['single_file', 'repository', 'multi_system', 'unknown'], uncertainty: ['low', 'medium', 'high', 'unknown'] };
|
|
45
47
|
export const rootDirectory = (env = process.env) => join(env.SIMY_HOME?.trim() || join(homedir(), '.simy'), 'delivery-hooks');
|
|
46
48
|
// Provider identity remains bound to record.cwd. File acceptance and writer
|
|
@@ -449,7 +451,9 @@ export function missingAcceptanceDiagnosis(managed, missing) {
|
|
|
449
451
|
const names = missing.map(item => item.id).join(', ');
|
|
450
452
|
const scope = managed.decision.action.scope;
|
|
451
453
|
const action = { kind: 'read', scope, summary: `Inspect missing acceptance checks: ${names}` };
|
|
452
|
-
return assessedPlan({ diagnosis:
|
|
454
|
+
return assessedPlan({ diagnosis: missing.some(item => item.check === 'behavior')
|
|
455
|
+
? `Behavior acceptance is incomplete or unresolved: ${names}. Read the observations and distinguish a failed behavior from unavailable verification before any repair.`
|
|
456
|
+
: `Worker claimed completion, but independent acceptance checks still fail: ${names}. Diagnose the discrepancy before any repair.`,
|
|
453
457
|
alternatives: [{ id: 'inspect_missing_acceptance', action,
|
|
454
458
|
assessment: { risk: 'low', reversibility: 'reversible', externalEffect: 'none', blastRadius: 'single_file', uncertainty: 'low', inObjective: true, userChoice: false },
|
|
455
459
|
expectedVerification: `Report actual outcome and unmet conditions for ${names}`, rollback: 'No changes to undo' }],
|
|
@@ -535,7 +539,7 @@ export function evaluateAssessedPlan(plan, policy, scopes, now = Date.now(), { a
|
|
|
535
539
|
const localWrite = action.kind === 'local_edit' || allowLocalTestWrites && action.kind === 'local_test' && facts.externalEffect === 'local';
|
|
536
540
|
const autonomous = policy === 'autonomous' && llmDelegated;
|
|
537
541
|
const external = autonomous && allowReversibleExternal && action.kind === 'external' && facts.externalEffect === 'external';
|
|
538
|
-
let reason = null, waitKind = autonomous ? 'research' : 'human';
|
|
542
|
+
let reason = null, assessmentError = null, waitKind = autonomous ? 'research' : 'human';
|
|
539
543
|
if (!scopes.includes(action.scope)) { reason = 'Action scope is outside the task enrollment.'; waitKind = 'authority'; }
|
|
540
544
|
else if (facts.inObjective !== true) reason = 'Establish objective fit or choose work within the original task.';
|
|
541
545
|
else if (facts.reversibility === 'irreversible') { reason = 'Irreversible consequences require an exact user decision.'; waitKind = 'human'; }
|
|
@@ -545,13 +549,16 @@ export function evaluateAssessedPlan(plan, policy, scopes, now = Date.now(), { a
|
|
|
545
549
|
else if (autonomous && allowReversibleExternal && action.kind === 'external' && facts.externalEffect !== 'external') reason = 'External action and declared effects disagree. Inspect the target and correct the assessment without executing it; API/service mutations have external effects even for a loopback service, while local filesystem work uses a local action.';
|
|
546
550
|
else if ((!LOCAL_ACTIONS.has(action.kind) || facts.externalEffect === 'external') && !external) { reason = 'This action needs existing external authority or a reversible supported alternative.'; waitKind = 'authority'; }
|
|
547
551
|
else if ((localWrite || external) && (!option.rollback || external && /^(?:no changes|none|n\/a)/i.test(option.rollback.trim()))) reason = 'Effectful work needs a concrete restoration plan.';
|
|
548
|
-
else if (localWrite && facts.externalEffect !== 'local') reason = 'Local write has inconsistent effect boundaries.';
|
|
552
|
+
else if (localWrite && facts.externalEffect !== 'local') { reason = 'Local write has inconsistent effect boundaries.'; assessmentError = 'local_write_effect_mismatch'; }
|
|
549
553
|
else if (!localWrite && !external && facts.externalEffect !== 'none') reason = 'Read or test action has unexpected side effects.';
|
|
550
554
|
else if (!autonomous && (facts.blastRadius === 'multi_system' || facts.risk === 'high' || facts.uncertainty === 'high')) reason = 'Selected policy requires a narrower or lower-risk action.';
|
|
551
555
|
else if (policy === 'strict' && (localWrite || facts.risk !== 'low' || facts.uncertainty !== 'low')) reason = 'Strict policy requires low-risk read or local test.';
|
|
552
556
|
else if (policy === 'balanced' && (facts.risk !== 'low' || facts.uncertainty !== 'low' || localWrite && facts.blastRadius !== 'single_file')) reason = 'Balanced policy requires a low-risk, narrow action.';
|
|
553
|
-
|
|
554
|
-
|
|
557
|
+
else if (policy === 'local_single_file' && (action.scope !== 'task' || facts.uncertainty !== 'low' ||
|
|
558
|
+
(action.kind === 'local_edit' ? !['low','medium'].includes(facts.risk) || facts.blastRadius !== 'single_file' || /^(?:no changes|none|n\/a)\b/i.test(option.rollback.trim()) :
|
|
559
|
+
localWrite || facts.risk !== 'low'))) reason = 'Local single-file policy requires known low uncertainty, reversible task-local single-file edits at low or medium risk, or low-risk read-only work.';
|
|
560
|
+
const report = external || autonomous && (facts.risk !== 'low' || facts.uncertainty !== 'low' || facts.userChoice !== false || facts.blastRadius === 'multi_system') || ['autonomous','local_single_file'].includes(policy) && facts.risk === 'medium';
|
|
561
|
+
return { id: digest(JSON.stringify({ plan, policy, llmDelegated, allowReversibleExternal })), outcome: reason ? 'wait' : report ? 'report' : 'proceed', ...(reason ? { waitKind } : {}), ...(assessmentError ? { assessmentError } : {}), ...(external && !reason ? { autonomousExternal: true } : {}), reason: reason || 'AI selected reversible work within the enrolled objective and provider authority.', selectedOptionId: option.id, action, assessment: facts, expectedVerification: option.expectedVerification, rollback: option.rollback, policy, at: now };
|
|
555
562
|
}
|
|
556
563
|
// Recompute from the frozen plan, not an arbitrary checkpoint flag. Provider
|
|
557
564
|
// permissions are checked independently by the adapter at actual dispatch.
|
|
@@ -566,15 +573,19 @@ export function canExecuteAutonomousExternal(managed) {
|
|
|
566
573
|
}
|
|
567
574
|
// Shared by live Stop hooks and cold exact-session recovery. Keep the result
|
|
568
575
|
// contract identical across transports; the host still verifies acceptance.
|
|
569
|
-
|
|
576
|
+
function actionAssessmentInstruction(decision, managed = null) {
|
|
570
577
|
const scopes = managed?.approvalScopes ?? (decision.action?.scope ? [decision.action.scope] : []);
|
|
571
|
-
const
|
|
578
|
+
const policy=managed?.autonomyPolicy || decision.policy;
|
|
579
|
+
return `${policy==='local_single_file' ? 'The host-enrolled local_single_file policy permits only reversible task-local edits to one file at low or medium risk with known low uncertainty. Read-only work stays low risk; external/high/unknown/user-choice effects remain held. Declare exactly one task-relative writeResources target, retain a concrete rollback, and checkpoint the actual result before the next effect. ' : ''}Enrolled action.scope labels: ${JSON.stringify(scopes)}. scope is an exact enrollment label, not a filename or target description. Reuse an enrolled label for work within that scope; do not invent labels. Put concrete file names and affected targets in action.summary. These labels do not expand the original objective or provider permissions. externalEffect classifies all effects, not just network effects: read-only work uses "none"; local file writes use "local"; outside-service effects use "external"; retain "unknown" when facts are missing. local_edit therefore requires externalEffect:"local", even when it makes no network call. Never relabel an external or unknown effect to bypass a hold. ${policy==='balanced' ? 'Balanced policy requires known low risk and uncertainty; a local write must be single_file, reversible and have a concrete rollback. Keep the entire original goal in remaining work, but choose one bounded original-task action rather than collapsing a multi-file goal into one write.' : policy==='strict' ? 'Strict policy permits low-risk read or local test; it does not authorize local edits.' : 'The enrolled policy and actual permission boundaries remain authoritative.'}`;
|
|
580
|
+
}
|
|
581
|
+
export function assessedCheckpointContract(decision, managed = null) {
|
|
582
|
+
const actionFields=actionAssessmentInstruction(decision,managed);
|
|
572
583
|
const observationField = flexibleReasoning(managed) ? 'Plan diagnosis, recommendation and its reason are optional when the selected action is clear. Optional observations may be omitted; do not create evidence files, citations or extra reports. Actual tool results are captured by the controller.' : 'Return the observed result; evidence attachments and extra reports are optional.';
|
|
573
584
|
return `${minimalSufficientChangeInstruction({ turn: 'assessed' })}\nReturn ONLY one JSON object with top-level status:"in_progress|completed|blocked|error" and summary:"what happened". Use the key status, NEVER kind, for an ordinary work checkpoint. Record actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"what actually happened"} for THIS selected action. ${observationField} ${actionReversibilityInstruction} This is provider-reported, not independent verification. When the requested outcome is a conversational answer or correction, put the actual answer text in summary. Saying it was prepared or delivered is insufficient; do not place the answer only in an undeclared JSON field. For an answer-only request, returning that text here is the conversational result: do not invent a separate message-delivery action unless the original user requested a separate destination. This does not verify implementation or external effects. For in_progress/error include nextStep, remainingWork, and plan:{diagnosis,alternatives:[{id,action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification,rollback}],selectedOptionId,recommendation:{optionId,reason},userQuestion?} for the NEXT action; select one assessed option but do not execute it in this turn. Valid action.kind values are exactly read, local_edit, local_test, external, destructive, scope_change, unknown; use local_edit for writing local files. ${actionFields} Risk is low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown. For blastRadius, single_file and repository describe filesystem scope; multi_system is the conservative category for a known non-filesystem boundary, including an API/service or conversation. State the actual affected targets in the plan; this category grants no additional target or permission. Use unknown for missing scope facts, not merely because a known target is not a file. Missing facts are unknown. Successful diagnosis or repair is only an action result, not completion of the original user task. If original work remains, return in_progress with remainingWork and the next assessed plan, preserving the original objective, corrections and acceptance criteria. Do not repeat effects already performed. Report completed only when the original requested outcome is achieved; keep unfinished checks as remaining work. Do not add audit reports, certification, or evidence-submission tasks. Blocked requires blocker set to exactly authorization, missing_information, or external_dependency, plus nextStep. For authorization, include an assessed plan and set approval to an exact copy of the selected plan alternative action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. ${informationWaitInstruction} external_dependency requires waitCondition:{condition,owner,nextCheck} with the observable resumption condition, dependency owner, and bounded recheck instruction, plus nextStep.`;
|
|
574
585
|
|
|
575
586
|
}
|
|
576
|
-
export function understandingCheckpointContract(decision) {
|
|
577
|
-
return `${minimalSufficientChangeInstruction({ turn: 'understanding' })}\n${correctiveFollowupInstruction}\nThis is an UNDERSTANDING turn, not a work turn. Read the existing conversation context and available read-only evidence; ${decision.purpose ? "Authorized read-only API and web research is allowed within existing provider permissions; do not send private material to external services. Do not edit files, write to external services, or start the proposed next action." : "do not edit files, call external services, or start the proposed next action."} Return ONLY JSON {kind:"session_understanding",summary:"...",actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"...",evidence:["..."]},sessionUnderstanding:{state:"clear|ambiguous|context_missing|already_done",objective:"current user goal",constraints:["..."],done:["..."],remaining:["..."],nextAction:{kind:"read|local_edit|local_test|external|destructive|scope_change|unknown",scope:"task",summary:"..."},criteria:[{description:"...",source:"user_grounded|proposed",basisIndex:0,check:{type:"manual|semantic|file_exists|file_contains|file_equals",path:"relative/path",text:"expected text"}}],ambiguities:["..."],question:"one focused user question if unclear",basis:[{source:"user|assistant|artifact",turnRef:"if available",snippet:"short relevant excerpt"}]},plan:{diagnosis:"...",alternatives:[{id:"...",action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification:"...",rollback:"..."}],selectedOptionId:"...",recommendation:{optionId:"...",reason:"..."}}}. For clear, choose one NEXT action but do not execute it. Set sessionUnderstanding.nextAction to an exact copy of the selected plan alternative's action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. Complete every known assessment field: risk low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown; inObjective and userChoice booleans. For blastRadius, single_file and repository describe filesystem scope; multi_system is the conservative category for a known non-filesystem boundary, including an API/service or conversation. State the actual affected targets in the plan; this category grants no additional target or permission. Use unknown for missing scope facts, not merely because a known target is not a file. For a read-only single-file action with no effects, use reversible, none, single_file when those facts are known; unknown facts require AI investigation; reversible preferences are delegated under autonomous policy. Write objective and criteria in the original user's language. Lead objective with the intended user experience (who can do what in which situation), when grounded in the request. Criteria must describe observable outcomes for that experience, not merely code written or tests passing. Keep technical-only requests technical; do not invent new UX obligations. Preserve done and remaining as the recorded stages of the whole requested work, distinguishing reported implementation from verified outcomes. Use only the original user's goal; constraints[] must contain only constraints from the original user, not these SIMY turn instructions. Do not invent new work or acceptance obligations. Include a user-grounded criterion only with a basisIndex pointing to a user message. Use file_equals for a user-requested file whose complete UTF-8 contents must be exact (including whitespace/newlines), file_contains only for a requested substring, and file_exists only for existence. ${decision.search ? "For semantic outcomes use check.type semantic; reserve manual for an explicit requirement for a human decision. Missing verification tooling is work to do, not a manual condition." : "For semantic conditions use semantic; do not create human verification requirements."} Never emit file_equals with null, omitted, guessed, or placeholder text. If exact expected contents are unknown, choose read to learn them and use semantic and obtain the needed observation automatically. Do not invent a file check from repository hints alone. If the original work is already done, choose already_done with remaining:[], nextAction:null, no plan, and no new goal. First recover missing context with authorized reads and infer reversible defaults from the original request. Select read investigation when facts remain unknown. Only if the original goal cannot be grounded at all or irreversible personal consequences require a decision, set one ambiguity and one focused question, nextAction:null, and omit plan. Provider statements about past work and semantic success are not independent verification. If and only if the original user requests ONLY a conversational answer or design (no implementation, files, tests, deployment or external effects), no independent review/audit or human acceptance is requested anywhere in the user context, and the existing answer already satisfies every requested point, you may include sessionUnderstanding.conversationOutcome:{kind:"answer_only",independentReviewRequested:false,requestTurnRefs:["exact user turn refs"],answerTurnRefs:["exact assistant answer turn refs"]}. Include the latest user and latest answer references, and exact user excerpts in the criteria basis. Read and judge the actual answer, including factual errors and unsupported claims; its mere existence or a worker saying done is insufficient. If the answer is wrong or incomplete, use clear with the next read action to formulate the missing/corrected answer, not already_done. Omit conversationOutcome for uncertain scope, missing context or requested effects; a design that describes unimplemented work is complete only when the user asked for design alone. This optional classification ends only an initially uncontracted conversational task as reported_done; it does not waive a frozen acceptance contract or assert independent verification. Optionally include sessionUnderstanding.matchingContext:{deliverables:["concrete requested output"],projectHints:["project name"],repositoryUrls:[],relatedUrls:[],actionIds:[],sourceRefs:[{platform:"slack",externalThreadId:"exact thread",channelExternalId:"exact channel"}]}. Extract only explicit conversation evidence; IDs and sources are retrieval signals, never authenticated identity. Do not infer URLs or identifiers.`;
|
|
587
|
+
export function understandingCheckpointContract(decision, managed = null) {
|
|
588
|
+
return `${minimalSufficientChangeInstruction({ turn: 'understanding' })}\n${correctiveFollowupInstruction}\n${actionAssessmentInstruction(decision,managed)}\nThis is an UNDERSTANDING turn, not a work turn. Read the existing conversation context and available read-only evidence; ${decision.purpose ? "Authorized read-only API and web research is allowed within existing provider permissions; do not send private material to external services. Do not edit files, write to external services, or start the proposed next action." : "do not edit files, call external services, or start the proposed next action."} Return ONLY JSON {kind:"session_understanding",summary:"...",actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"...",evidence:["..."]},sessionUnderstanding:{state:"clear|ambiguous|context_missing|already_done",objective:"current user goal",constraints:["..."],done:["..."],remaining:["..."],nextAction:{kind:"read|local_edit|local_test|external|destructive|scope_change|unknown",scope:"task",summary:"..."},criteria:[{description:"...",source:"user_grounded|proposed",basisIndex:0,check:{type:"manual|semantic|file_exists|file_contains|file_equals",path:"relative/path",text:"expected text"}}],ambiguities:["..."],question:"one focused user question if unclear",basis:[{source:"user|assistant|artifact",turnRef:"if available",snippet:"short relevant excerpt"}]},plan:{diagnosis:"...",alternatives:[{id:"...",action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification:"...",rollback:"..."}],selectedOptionId:"...",recommendation:{optionId:"...",reason:"..."}}}. For clear, choose one NEXT action but do not execute it. Set sessionUnderstanding.nextAction to an exact copy of the selected plan alternative's action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. Complete every known assessment field: risk low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown; inObjective and userChoice booleans. For blastRadius, single_file and repository describe filesystem scope; multi_system is the conservative category for a known non-filesystem boundary, including an API/service or conversation. State the actual affected targets in the plan; this category grants no additional target or permission. Use unknown for missing scope facts, not merely because a known target is not a file. For a read-only single-file action with no effects, use reversible, none, single_file when those facts are known; unknown facts require AI investigation; reversible preferences are delegated under autonomous policy. Write objective and criteria in the original user's language. Lead objective with the intended user experience (who can do what in which situation), when grounded in the request. Criteria must describe observable outcomes for that experience, not merely code written or tests passing. Keep technical-only requests technical; do not invent new UX obligations. Preserve done and remaining as the recorded stages of the whole requested work, distinguishing reported implementation from verified outcomes. Use only the original user's goal; constraints[] must contain only constraints from the original user, not these SIMY turn instructions. Do not invent new work or acceptance obligations. Include a user-grounded criterion only with a basisIndex pointing to a user message. Use file_equals for a user-requested file whose complete UTF-8 contents must be exact (including whitespace/newlines), file_contains only for a requested substring, and file_exists only for existence. ${decision.search ? "For semantic outcomes use check.type semantic; reserve manual for an explicit requirement for a human decision. Missing verification tooling is work to do, not a manual condition." : "For semantic conditions use semantic; do not create human verification requirements."} Never emit file_equals with null, omitted, guessed, or placeholder text. If exact expected contents are unknown, choose read to learn them and use semantic and obtain the needed observation automatically. Do not invent a file check from repository hints alone. If the original work is already done, choose already_done with remaining:[], nextAction:null, no plan, and no new goal. First recover missing context with authorized reads and infer reversible defaults from the original request. Select read investigation when facts remain unknown. Only if the original goal cannot be grounded at all or irreversible personal consequences require a decision, set one ambiguity and one focused question, nextAction:null, and omit plan. Provider statements about past work and semantic success are not independent verification. If and only if the original user requests ONLY a conversational answer or design (no implementation, files, tests, deployment or external effects), no independent review/audit or human acceptance is requested anywhere in the user context, and the existing answer already satisfies every requested point, you may include sessionUnderstanding.conversationOutcome:{kind:"answer_only",independentReviewRequested:false,requestTurnRefs:["exact user turn refs"],answerTurnRefs:["exact assistant answer turn refs"]}. Include the latest user and latest answer references, and exact user excerpts in the criteria basis. Read and judge the actual answer, including factual errors and unsupported claims; its mere existence or a worker saying done is insufficient. If the answer is wrong or incomplete, use clear with the next read action to formulate the missing/corrected answer, not already_done. Omit conversationOutcome for uncertain scope, missing context or requested effects; a design that describes unimplemented work is complete only when the user asked for design alone. This optional classification ends only an initially uncontracted conversational task as reported_done; it does not waive a frozen acceptance contract or assert independent verification. Optionally include sessionUnderstanding.matchingContext:{deliverables:["concrete requested output"],projectHints:["project name"],repositoryUrls:[],relatedUrls:[],actionIds:[],sourceRefs:[{platform:"slack",externalThreadId:"exact thread",channelExternalId:"exact channel"}]}. Extract only explicit conversation evidence; IDs and sources are retrieval signals, never authenticated identity. Do not infer URLs or identifiers.`;
|
|
578
589
|
}
|
|
579
590
|
function boundedContextList(value, maxItems = 8, maxLength = 300) {
|
|
580
591
|
if (!Array.isArray(value) || value.length > maxItems) throw new Error('Session understanding has too many items.');
|
|
@@ -690,11 +701,14 @@ function recordPurposeProgress(run, now, countAttempt = true) {
|
|
|
690
701
|
if (fresh.length) purpose.progressCapacityReached = true;
|
|
691
702
|
}
|
|
692
703
|
}
|
|
704
|
+
function managedAssessmentOptions(managed) {
|
|
705
|
+
return {allowLocalTestWrites:true,llmDelegated:searchEnabled(managed),
|
|
706
|
+
allowReversibleExternal:searchEnabled(managed) && managed.resumeChannel==='codex_app_server'};
|
|
707
|
+
}
|
|
693
708
|
function recordDecision(managed, plan, now) {
|
|
694
709
|
// Classify effectful local tests as writes, with the same scope, policy and
|
|
695
710
|
// rollback checks as local_edit. Native write permissions remain independent.
|
|
696
|
-
const assessmentOptions =
|
|
697
|
-
allowReversibleExternal: searchEnabled(managed) && managed.resumeChannel === 'codex_app_server' };
|
|
711
|
+
const assessmentOptions = managedAssessmentOptions(managed);
|
|
698
712
|
let decision = evaluateAssessedPlan(plan, managed.autonomyPolicy, managed.approvalScopes, now, assessmentOptions);
|
|
699
713
|
const dependencyPending = option => (option?.dependsOn || []).some(id =>
|
|
700
714
|
!managed.purpose?.evidence?.units?.some(unit => unit.id === id && unit.status === 'verified' && unit.purposeRevision === managed.purpose.evidence.purposeRevision));
|
|
@@ -773,6 +787,7 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
773
787
|
if (!spec) return null;
|
|
774
788
|
if (spec !== true && (typeof spec !== 'object' || Array.isArray(spec))) throw new Error('Invalid managed task specification.');
|
|
775
789
|
const config = spec === true ? {} : spec;
|
|
790
|
+
if (Object.hasOwn(config, 'requirementChecker') || Object.hasOwn(config, 'hostRequirementChecker')) throw new Error('Checker registration must come from the owning host, not a managed specification.');
|
|
776
791
|
if (!Array.isArray(config.acceptanceCriteria) || (!config.acceptanceCriteria.length && config.contextBootstrap !== true) || config.acceptanceCriteria.length > 20) throw new Error('Managed tasks require acceptanceCriteria.');
|
|
777
792
|
const criteria = config.acceptanceCriteria.map((item, index) => {
|
|
778
793
|
if (typeof item !== 'object' || !item || !text(item.description, 500)) throw new Error('Each acceptance criterion needs a description.');
|
|
@@ -814,6 +829,7 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
814
829
|
deliveryLevel = { requested: value.requested, effective: value.effective, numberingVersion: value.numberingVersion, selectionRef: text(value.selectionRef, 200) };
|
|
815
830
|
}
|
|
816
831
|
const managed = { ...(config.purpose ? { purpose: structuredClone(config.purpose) } : {}), taskId: randomUUID(), deliveryLevel, thread: { provider, id: session },
|
|
832
|
+
...(config.requirementContract ? { requirementContract: normalizeRequirementContract(config.requirementContract) } : {}),
|
|
817
833
|
independentReviewRequired: config.independentReviewRequired === true,
|
|
818
834
|
actionId: text(config.actionId, 200) || null, actionDueAt, acceptanceCriteria: criteria, verified: {}, remainingWork: text(config.remainingWork, 4000) || null,
|
|
819
835
|
initialAction, proposedAction: initialAction, approvalPolicy, approvalScopes: [...new Set(scopes)].slice(0, 20),
|
|
@@ -842,12 +858,16 @@ function freezeEnrollment(run, now) {
|
|
|
842
858
|
ledger.charter ||= { objective: run.objective, criteria: structuredClone(run.managed.acceptanceCriteria), approvalScopes: [...run.managed.approvalScopes], frozenAt: now };
|
|
843
859
|
}
|
|
844
860
|
run.managed.enrollment = { objective: run.objective, acceptanceDigest: digest(JSON.stringify(run.managed.acceptanceCriteria)),
|
|
861
|
+
...(run.managed.requirementContract ? { requirementContractHash: run.managed.requirementContract.contractHash } : {}),
|
|
862
|
+
...(run.managed.requirementChecker ? { requirementCheckerDigest: run.managed.requirementChecker.digest } : {}),
|
|
845
863
|
...(run.managed.providerSandboxSource ? {nativeAuthorityDigest:digest(JSON.stringify([run.managed.providerSandbox,run.managed.providerSandboxSource]))} : {}),
|
|
846
864
|
deliveryLevel: structuredClone(run.managed.deliveryLevel || null), frozenAt: now };
|
|
847
865
|
}
|
|
848
866
|
export function pendingAcceptance(run) {
|
|
849
867
|
const enrollment = run.managed?.enrollment;
|
|
850
868
|
if (enrollment && (enrollment.objective !== run.objective || enrollment.acceptanceDigest !== digest(JSON.stringify(run.managed.acceptanceCriteria)) ||
|
|
869
|
+
enrollment.requirementContractHash !== run.managed.requirementContract?.contractHash ||
|
|
870
|
+
enrollment.requirementCheckerDigest !== run.managed.requirementChecker?.digest ||
|
|
851
871
|
JSON.stringify(enrollment.deliveryLevel) !== JSON.stringify(run.managed.deliveryLevel || null))) return [{ id: 'enrollment', description: 'Original objective, level and acceptance contract',
|
|
852
872
|
check: 'manual', detail: 'Enrollment changed unexpectedly; stop and inspect before enrolling a revised task' }];
|
|
853
873
|
const feedback = new Map(pendingAcceptanceFeedback(run).map(item => [item.criterionId, item]));
|
|
@@ -860,7 +880,7 @@ export function pendingAcceptance(run) {
|
|
|
860
880
|
if (run.managed?.independentReviewRequired && !searchEnabled(run.managed) && run.managed.liveOutcomeReview?.passed !== true) pending.push({
|
|
861
881
|
id: 'independent-review', description: 'Required independent outcome review', check: 'semantic',
|
|
862
882
|
detail: 'Independent outcome review has not passed; file checks alone cannot complete this task' });
|
|
863
|
-
return pending;
|
|
883
|
+
return [...pending, ...pendingRequirements(run.managed)];
|
|
864
884
|
}
|
|
865
885
|
function acceptancePendingReason(run) {
|
|
866
886
|
return `Acceptance verification pending: ${pendingAcceptance(run).map(item => `${item.id} (${item.description}): ${item.detail}`).join('; ')}`;
|
|
@@ -929,7 +949,7 @@ export function liveOutcomeReviewEligible(record) {
|
|
|
929
949
|
}
|
|
930
950
|
// Checkpoints are rejected while only manual acceptance is pending, so the
|
|
931
951
|
// review instruction applies only where an independent review can follow.
|
|
932
|
-
function reviewInstruction(run) {
|
|
952
|
+
export function reviewInstruction(run) {
|
|
933
953
|
const m = run.managed;
|
|
934
954
|
if (m?.capability !== 'live_hook' || m?.thread?.provider !== 'claude') return '';
|
|
935
955
|
if (run.state === 'verification_pending' && !m.independentReviewRequired && !effectiveAcceptanceCriteria(m).some(c => c.check.type === 'semantic'))
|
|
@@ -937,7 +957,7 @@ function reviewInstruction(run) {
|
|
|
937
957
|
? 'Manual acceptance is pending. The completion report records remaining work, but no additional executable next plan has been accepted. '
|
|
938
958
|
: 'Only manual acceptance remains and the user verifies it. ') +
|
|
939
959
|
'Checkpoints are not accepted until the user verifies or resumes this task; do not resubmit one. ';
|
|
940
|
-
return 'When the requested artifact is ready or repaired, submit a completed work checkpoint to request independent review.
|
|
960
|
+
return 'When the requested artifact is ready or repaired, submit a completed work checkpoint to request independent review. Use status=completed with the actual actionResult even while acceptance is pending; this is not acceptance. If nativeOutcomeReview.prompt is absent, submit that checkpoint first to obtain a fresh review prompt; never reuse a pre-repair prompt. ';
|
|
941
961
|
}
|
|
942
962
|
export function deliveryAcceptanceContext(run) {
|
|
943
963
|
const feedback = manualAcceptanceFeedback(run);
|
|
@@ -991,6 +1011,9 @@ function initialDeliveryRun(objective, nextStep, now, maxFailures) {
|
|
|
991
1011
|
maxFailuresSource: maxFailures === undefined ? 'default' : 'explicit', continuations: 0, failures: 0,
|
|
992
1012
|
revision: 0, consumedRevision: -1, checkpoint: null, reason: 'started', managed: null };
|
|
993
1013
|
}
|
|
1014
|
+
function understandingTurnInstruction(managed, decision) {
|
|
1015
|
+
return `Understand the current user request in this ORIGINAL conversation. ${managed.understandingAnswer ? `The user answered question ${managed.understandingAnswer.questionId}: ${managed.understandingAnswer.text}. ` : ''}${understandingCheckpointContract(decision,managed)}`;
|
|
1016
|
+
}
|
|
994
1017
|
function hookDeliveryContext(record, automatic = false) {
|
|
995
1018
|
const { provider, session, run } = record;
|
|
996
1019
|
const identity = `SIMY connected to this ${provider} conversation: ${session}.`;
|
|
@@ -1000,9 +1023,14 @@ function hookDeliveryContext(record, automatic = false) {
|
|
|
1000
1023
|
(['active', 'reported_done', 'completed'].includes(run.state) ? `${correctiveFollowupInstruction} ` : '') +
|
|
1001
1024
|
(provider === 'codex' && ['completed', 'reported_done'].includes(run.state) ? `Finished run ID: ${run.id}. Its completion does not supervise later user requests. If the user explicitly requests supervision of the current goal, use simy delivery followup --provider codex --session ${session} --previous-run-id ${run.id} --objective CURRENT_GOAL. A different goal archives the old run; the same ordinary reported_done goal retains identity and budgets for read-only reconciliation. A new prompt alone is not consent to run this command. ` : '') +
|
|
1002
1025
|
(run.state === 'active' && !run.managed ? `Before ending the turn, submit an honest JSON checkpoint with simy delivery checkpoint --provider ${provider} --session ${session} --file -. Use status in_progress with summary, remainingWork and nextStep for remaining work; completed with summary, acceptanceMet:true and evidence for finished work; or blocked with summary and blocker missing_information or authorization. The first completed checkpoint requests an outcome check in this conversation; inspect the actual result and submit a fresh checkpoint. If requested work remains, perform the first authorized step instead of ending with self-evaluation or a plan. Completion is provider-reported, not independently verified. ${informationWaitInstruction} ` : '') +
|
|
1026
|
+
// Publish the current read/understanding contract at the real user boundary,
|
|
1027
|
+
// before any work or Stop. Rendering it neither dispatches nor consumes a
|
|
1028
|
+
// decision, and terminal/paused runs never receive an execution instruction.
|
|
1029
|
+
(run.state === 'active' && run.managed?.phase === 'understanding' && run.managed.decision ?
|
|
1030
|
+
`${understandingTurnInstruction(run.managed, run.managed.decision)} Submit this checkpoint through standard input using simy delivery checkpoint --provider ${provider} --session ${session} --file -. A chat JSON answer does not submit a checkpoint. ` : '') +
|
|
1003
1031
|
`Preserve the current task and its permissions, pauses and stops; never restart or reset it automatically.`;
|
|
1004
1032
|
}
|
|
1005
|
-
export async function startDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), executionCwd, upgradeRunId, previousRunId, reconcileRunId, objective, nextStep, maxMinutes, maxContinuations, maxFailures, continuationPolicy, managed, admission,
|
|
1033
|
+
export async function startDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), executionCwd, hostRequirementChecker, upgradeRunId, previousRunId, reconcileRunId, objective, nextStep, maxMinutes, maxContinuations, maxFailures, continuationPolicy, managed, admission,
|
|
1006
1034
|
liveHookAdmission, autoContinueEpoch, followupAutoContinue = false, providerLinkEpoch, inspectProcess = processIdentity, now = Date.now() }) {
|
|
1007
1035
|
const link = await providerLinkState(root, provider);
|
|
1008
1036
|
if (providerLinkEpoch !== undefined && providerLinkEpoch !== link.epoch) throw Object.assign(new Error("SIMY provider connection changed before enrollment."), { code: "provider_link_changed" });
|
|
@@ -1013,6 +1041,11 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
1013
1041
|
if (!(await stat(executionCwd)).isDirectory()) throw new Error('Task execution directory must be a directory.');
|
|
1014
1042
|
}
|
|
1015
1043
|
if (!text(objective) || !text(nextStep)) throw new Error('Objective and concrete next step are required.');
|
|
1044
|
+
const hostRequirements = hostRequirementChecker === undefined ? null : await bindRequirementChecker(hostRequirementChecker,
|
|
1045
|
+
{ root, provider, session, cwd: executionCwd || cwd, objective: text(objective) });
|
|
1046
|
+
if (hostRequirements && (!managed || managed.resumeChannel && managed.resumeChannel !== 'live_hook')) throw new Error('Registered checker requires managed Claude live-hook enrollment.');
|
|
1047
|
+
if (hostRequirements && managed.requirementContract && normalizeRequirementContract(managed.requirementContract).contractHash !== hostRequirements.requirementContract.contractHash)
|
|
1048
|
+
throw new Error('Managed contract differs from the host registration.');
|
|
1016
1049
|
return updateRecord(root, provider, session, async previous => {
|
|
1017
1050
|
if (previous?.providerArchived === true) throw new Error('Restore the original provider conversation before starting Delivery Loop.');
|
|
1018
1051
|
if (autoContinueEpoch && (previous?.autoContinue !== true || previous.autoContinueEpoch !== autoContinueEpoch)) throw new Error('Automatic continuation was disabled before enrollment.');
|
|
@@ -1048,6 +1081,7 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
1048
1081
|
const managedConfig = managed && { ...(managed === true ? {} : managed),
|
|
1049
1082
|
...(continuationPolicy === undefined ? {} : { continuationPolicy }) };
|
|
1050
1083
|
const managedState = managedSpec(managedConfig, session, provider, previous.process, admitted ? admission : null, now);
|
|
1084
|
+
if (hostRequirements) Object.assign(managedState, structuredClone(hostRequirements));
|
|
1051
1085
|
if ((following || reconciling) && (provider !== 'codex' || !admitted || !managedState?.autoRecovery || managedState.capability !== 'codex_app_server' || managedState.initialAction.kind !== 'read' || managedState.contextBootstrap)) throw new Error('Follow-up requires exact-session managed supervision, a concrete goal and read-only reconciliation.');
|
|
1052
1086
|
if (upgrading && (!managedState?.autonomyPolicy || managedState.initialAction.kind !== 'read' || managedState.contextBootstrap)) throw new Error('Upgrade requires read-only reconciliation and frozen acceptance of the original objective.');
|
|
1053
1087
|
if (!managedState && continuationMode(continuationPolicy) === 'progress') throw new Error('Progress continuation requires frozen managed acceptance criteria.');
|
|
@@ -1242,7 +1276,8 @@ export function diagnoseControllerApproval(record, now) {
|
|
|
1242
1276
|
return queued;
|
|
1243
1277
|
}
|
|
1244
1278
|
|
|
1245
|
-
export async function checkpointDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), checkpoint, expectedLease, expectedNativeHandoff, toolEvidence = [], sourceContext, controllerEvidence, now = Date.now() }) {
|
|
1279
|
+
export async function checkpointDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), checkpoint, expectedLease, expectedNativeHandoff, toolEvidence = [], sourceContext, controllerEvidence, signal, now = Date.now() }) {
|
|
1280
|
+
signal?.throwIfAborted();
|
|
1246
1281
|
if (expectedNativeHandoff && checkpoint?.kind) throw new Error('Native handoff requires an ordinary action checkpoint.');
|
|
1247
1282
|
if (checkpoint?.kind === 'action_association') return checkpointActionAssociation({ root, provider, session, cwd, checkpoint, expectedLease, now });
|
|
1248
1283
|
if (checkpoint?.kind === 'session_understanding') return checkpointUnderstanding({ root, provider, session, cwd, checkpoint, expectedLease, sourceContext, controllerEvidence, now });
|
|
@@ -1253,7 +1288,23 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1253
1288
|
if (checkpoint.status === 'error' && !text(checkpoint.nextStep)) throw new Error('A concrete nextStep is required.');
|
|
1254
1289
|
if (checkpoint.status === 'blocked' && !['authorization', 'missing_information', 'external_dependency'].includes(checkpoint.blocker)) throw new Error('Specify authorization, missing_information, or external_dependency.');
|
|
1255
1290
|
const evidence = Array.isArray(checkpoint.evidence) ? checkpoint.evidence.map(v => text(v, 1000)).filter(Boolean).slice(0, 20) : [];
|
|
1291
|
+
// Registered read-only verification runs outside updateRecord's lock. The
|
|
1292
|
+
// ordinary checkpoint still applies the existing stop/authority/action rules.
|
|
1293
|
+
if (checkpoint.status === 'completed') {
|
|
1294
|
+
const snapshot = await readRecord(root, provider, session);
|
|
1295
|
+
const binding = snapshot?.run?.managed?.requirementChecker;
|
|
1296
|
+
const decision = snapshot?.run?.managed?.decision;
|
|
1297
|
+
const result = checkpoint.actionResult;
|
|
1298
|
+
const actionBound = !snapshot?.run?.managed?.autonomyPolicy || result?.decisionId === decision?.id &&
|
|
1299
|
+
['observed', 'failed', 'not_run'].includes(result?.status) && Boolean(text(result.summary, 500));
|
|
1300
|
+
if (binding && actionBound && checkpointDirectoryMatches(snapshot, cwd) && requirementObservationEligible(root, snapshot)) {
|
|
1301
|
+
const observer = registeredRequirementObserver({ root, binding, provider, session, runId: snapshot.run.id });
|
|
1302
|
+
observer.checkVersion = snapshot.run.managed.requirementContract.checkVersion;
|
|
1303
|
+
await observeRequirementAcceptance({ root, provider, session, observer, signal, now });
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1256
1306
|
const settled = await updateRecord(root, provider, session, async record => {
|
|
1307
|
+
signal?.throwIfAborted();
|
|
1257
1308
|
const run = record?.run;
|
|
1258
1309
|
let effectiveStatus = checkpoint.status;
|
|
1259
1310
|
let nextStep = text(checkpoint.nextStep);
|
|
@@ -1363,7 +1414,8 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1363
1414
|
rememberPerspectives(run, run.managed.purpose.lastReview, actionResult, now, perspectiveScope(record));
|
|
1364
1415
|
}
|
|
1365
1416
|
if (run.managed.autonomyPolicy && checkpoint.status === 'completed') {
|
|
1366
|
-
const missing = run.managed.acceptanceCriteria.filter(item => !['manual', 'semantic'].includes(item.check.type) && !Object.hasOwn(run.managed.verified, item.id))
|
|
1417
|
+
const missing = [...run.managed.acceptanceCriteria.filter(item => !['manual', 'semantic'].includes(item.check.type) && !Object.hasOwn(run.managed.verified, item.id)),
|
|
1418
|
+
...pendingRequirements(run.managed)];
|
|
1367
1419
|
if (missing.length) {
|
|
1368
1420
|
effectiveStatus = 'in_progress';
|
|
1369
1421
|
repairPlan = checkpoint.plan && nextStep ? assessedPlan(checkpoint.plan, run.managed) : missingAcceptanceDiagnosis(run.managed, missing);
|
|
@@ -1467,9 +1519,10 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1467
1519
|
run.blockerSource = effectiveStatus === 'blocked' ? 'checkpoint' : null;
|
|
1468
1520
|
run.providerRequest = null;
|
|
1469
1521
|
run.checkpoint = { status: effectiveStatus, summary: text(checkpoint.summary), nextStep,
|
|
1522
|
+
...(effectiveStatus !== checkpoint.status || incompleteResult ? { reportedStatus: checkpoint.status } : {}),
|
|
1470
1523
|
blocker: unsupportedWait ? undefined : checkpoint.blocker,
|
|
1471
1524
|
...(unsupportedWait ? { reportedBlocker: checkpoint.blocker } : checkpoint.blocker === 'missing_information' ? { question: text(checkpoint.question, 1000) } : {}),
|
|
1472
|
-
...(remainingWork ? { remainingWork } : {}),
|
|
1525
|
+
...(remainingWork ? { remainingWork } : {}),
|
|
1473
1526
|
evidence, acceptanceMet: !unsupportedWait && !incompleteResult && checkpoint.acceptanceMet === true };
|
|
1474
1527
|
if (!run.managed && effectiveStatus !== 'completed') delete run.completionCheck;
|
|
1475
1528
|
if (run.managed?.liveOutcomeReview) {
|
|
@@ -1483,6 +1536,9 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1483
1536
|
const semanticReviewRequired = effectiveAcceptanceCriteria(run.managed).some(c => c.check.type === 'semantic');
|
|
1484
1537
|
if (searchEnabled(run.managed) && effectiveStatus === 'completed' && (run.managed.independentReviewRequired || semanticReviewRequired || pendingAcceptance(run).length || purposeCompletionBarrier(run))) {
|
|
1485
1538
|
queuePurposeVerification(run, checkpoint.summary, now);
|
|
1539
|
+
} else if (searchEnabled(run.managed) && effectiveStatus === 'in_progress' && semanticReviewStalled(run) &&
|
|
1540
|
+
!(await reviewedCurrentWorkspace(record, root, searchState(run.managed).review))) {
|
|
1541
|
+
queuePurposeVerification(run, `Repeated read-only checks report no remaining change; independently review the semantic acceptance. Latest result: ${text(checkpoint.summary, 600)}`, now);
|
|
1486
1542
|
} else if (run.managed && effectiveStatus === 'completed') {
|
|
1487
1543
|
const pending = pendingAcceptance(run);
|
|
1488
1544
|
if (pending.length) { run.state = 'verification_pending'; run.reason = acceptancePendingReason(run); }
|
|
@@ -1730,6 +1786,16 @@ async function checkpointUnderstanding({ root, provider, session, cwd, checkpoin
|
|
|
1730
1786
|
if (['clear', 'already_done'].includes(understanding.state) && result.status !== 'observed') throw new Error('Clear understanding requires an observed read result.');
|
|
1731
1787
|
const plan = understanding.state === 'clear' ? assessedPlan(checkpoint.plan, run.managed) : null;
|
|
1732
1788
|
if (plan && JSON.stringify(plan.alternatives.find(item => item.id === plan.selectedOptionId).action) !== JSON.stringify(understanding.nextAction)) throw new Error('Understood next action differs from selected assessed option.');
|
|
1789
|
+
// A deterministic contradictory declaration is invalid input, not evidence
|
|
1790
|
+
// that the human needs to authorize an effect. Reject before committing
|
|
1791
|
+
// understanding; never rewrite none to local or dispatch the write.
|
|
1792
|
+
if (plan && run.managed.contextBootstrap && !searchEnabled(run.managed) &&
|
|
1793
|
+
!run.managed.pendingApproval && !run.managed.executionUncertain && !run.managed.rejectedActionOverflow &&
|
|
1794
|
+
!run.managed.rejectedActionDigests?.includes(digest(JSON.stringify(understanding.nextAction)))) {
|
|
1795
|
+
const assessed=evaluateAssessedPlan(plan,run.managed.autonomyPolicy,run.managed.approvalScopes,now,managedAssessmentOptions(run.managed));
|
|
1796
|
+
if (assessed.assessmentError==='local_write_effect_mismatch')
|
|
1797
|
+
throw new Error('Inconsistent understanding assessment: local_edit requires externalEffect:"local" for actual local writes, not "none". Correct the declaration honestly or choose authorized read investigation; do not execute or relabel external/unknown effects.');
|
|
1798
|
+
}
|
|
1733
1799
|
if (purposeEnabled(run)) recordPurposeReview(run.managed,
|
|
1734
1800
|
validatePurposeReview(checkpoint.purposeReview, plan, run.managed.purpose.ruleIds, searchEnabled(run.managed) ? run.managed.purpose.lastReview?.rules || [] : [], run.managed), now);
|
|
1735
1801
|
if (searchEnabled(run.managed) && expectedLease && sourceContext) run.managed.purpose.originalContext ||= sourceContext;
|
|
@@ -1779,7 +1845,25 @@ async function checkpointUnderstanding({ root, provider, session, cwd, checkpoin
|
|
|
1779
1845
|
run.managed.phase = 'active';
|
|
1780
1846
|
run.state = 'reported_done';
|
|
1781
1847
|
run.reason = 'Original conversation reports no remaining work; independent completion was not verified.';
|
|
1782
|
-
if (
|
|
1848
|
+
if (run.managed.requirementContract) {
|
|
1849
|
+
// An already-enrolled behavior contract is independent of a worker's
|
|
1850
|
+
// interpretation that nothing remains. Acquire its observation through
|
|
1851
|
+
// the existing read/checkpoint path; reported_done would end that path.
|
|
1852
|
+
if (!run.managed.acceptanceCriteria.length) run.managed.acceptanceCriteria = understanding.criteria
|
|
1853
|
+
.filter(item => item.source === 'user_grounded').map(item => ({ id: item.id, description: item.description,
|
|
1854
|
+
check: item.check.type === 'file_equals' ? { type: 'file_equals', exactPath: item.check.path, text: item.check.text } : item.check }));
|
|
1855
|
+
freezeEnrollment(run, now);
|
|
1856
|
+
await verifyAutomaticCriteria(record, now);
|
|
1857
|
+
const missing = pendingRequirements(run.managed);
|
|
1858
|
+
if (missing.length) {
|
|
1859
|
+
const diagnosis = missingAcceptanceDiagnosis(run.managed, missing);
|
|
1860
|
+
run.state = 'active'; recordDecision(run.managed, diagnosis, now);
|
|
1861
|
+
run.nextStep = run.managed.decision.action.summary;
|
|
1862
|
+
run.reason = 'Reported completion still needs the enrolled independent behavior observations.';
|
|
1863
|
+
} else {
|
|
1864
|
+
run.state = 'verification_pending'; run.reason = acceptancePendingReason(run);
|
|
1865
|
+
}
|
|
1866
|
+
} else if (conversationalCompletion) {
|
|
1783
1867
|
// These are inferred expectations, not a user-enrolled verification
|
|
1784
1868
|
// contract. Preserve them in sessionUnderstanding without promoting
|
|
1785
1869
|
// them into new audit obligations or claiming independent acceptance.
|
|
@@ -1820,8 +1904,98 @@ function newVerifiedCriterion(run, id, now) {
|
|
|
1820
1904
|
if (!Object.hasOwn(managed.verified, id)) managed.lastOutcomeAt = now;
|
|
1821
1905
|
managed.lastVerifiedProgressAt = Math.max(managed.lastVerifiedProgressAt ?? run.startedAt, now);
|
|
1822
1906
|
}
|
|
1907
|
+
// Host adapter API, deliberately absent from the worker checkpoint JSON/CLI.
|
|
1908
|
+
// The caller provides a registered read-only observer, not a worker verdict or
|
|
1909
|
+
// a command discovered from task files. This API grants no execution permission.
|
|
1910
|
+
function requirementObservationEligible(root, record) {
|
|
1911
|
+
const managed = record?.run?.managed;
|
|
1912
|
+
return managed?.requirementContract && ['active', 'verification_pending'].includes(record.run.state) &&
|
|
1913
|
+
!record.run.stopReason && !hasPendingUserStop(root, record) && automaticContinuationAllowed(record) &&
|
|
1914
|
+
!managed.pendingApproval && !managed.nativeWait && !managed.executionUncertain && !managed.purpose?.nativeBoundary &&
|
|
1915
|
+
!acceptanceAuthorityHeld(record.run) && !managed.liveWorkspaceLease && !managed.resumeLease && !managed.outcomeAudit?.lease &&
|
|
1916
|
+
!['pending', 'pending_received', 'transferred'].includes(managed.handoff?.status) &&
|
|
1917
|
+
!['understanding', 'needs_clarification'].includes(managed.phase);
|
|
1918
|
+
}
|
|
1919
|
+
export async function observeRequirementAcceptance({ root, provider, session, observer, signal, now = Date.now() }) {
|
|
1920
|
+
signal?.throwIfAborted();
|
|
1921
|
+
if (typeof observer !== 'function') throw new Error('A host behavior observer is required.');
|
|
1922
|
+
const snapshot = await readRecord(root, provider, session);
|
|
1923
|
+
const eligible = record => requirementObservationEligible(root, record);
|
|
1924
|
+
if (!eligible(snapshot)) throw new Error('Behavior observation cannot override a task hold.');
|
|
1925
|
+
if (snapshot.run.managed.requirementChecker && !isRegisteredRequirementObserver(observer)) throw new Error('Enrolled checker requires its registered host execution path.');
|
|
1926
|
+
const contract = normalizeRequirementContract(snapshot.run.managed.requirementContract);
|
|
1927
|
+
if (observer.checkVersion !== contract.checkVersion) throw new Error('Host observer version does not match the enrolled contract.');
|
|
1928
|
+
const link = await providerLinkState(root, provider);
|
|
1929
|
+
if (!link.connected) throw new Error('Provider connection is unavailable.');
|
|
1930
|
+
const controller = new AbortController();
|
|
1931
|
+
const abort = () => controller.abort(signal.reason);
|
|
1932
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
1933
|
+
if (signal?.aborted) abort();
|
|
1934
|
+
let checking = false;
|
|
1935
|
+
const stillCurrent = async () => {
|
|
1936
|
+
if (checking || controller.signal.aborted) return;
|
|
1937
|
+
checking = true;
|
|
1938
|
+
try {
|
|
1939
|
+
const record = await readRecord(root, provider, session), currentLink = await providerLinkState(root, provider);
|
|
1940
|
+
if (!eligible(record) || record.run.id !== snapshot.run.id || record.run.revision !== snapshot.run.revision ||
|
|
1941
|
+
record.run.controlRevision !== snapshot.run.controlRevision || !currentLink.connected || currentLink.epoch !== link.epoch)
|
|
1942
|
+
controller.abort(new Error('Task authority changed during behavior verification.'));
|
|
1943
|
+
} catch { controller.abort(new Error('Task authority is unavailable.')); }
|
|
1944
|
+
finally { checking = false; }
|
|
1945
|
+
};
|
|
1946
|
+
const monitor = setInterval(stillCurrent, 100);
|
|
1947
|
+
let candidateHash, receipt;
|
|
1948
|
+
try {
|
|
1949
|
+
await stillCurrent();
|
|
1950
|
+
controller.signal.throwIfAborted();
|
|
1951
|
+
candidateHash = await workspaceIdentity(taskDirectory(snapshot), root, undefined, { signal: controller.signal });
|
|
1952
|
+
await stillCurrent();
|
|
1953
|
+
controller.signal.throwIfAborted();
|
|
1954
|
+
receipt = await observer({ cwd: taskDirectory(snapshot), contract: structuredClone(contract), candidateHash, signal: controller.signal });
|
|
1955
|
+
}
|
|
1956
|
+
catch { receipt = { schemaVersion: 1, contractHash: contract.contractHash, checkVersion: contract.checkVersion,
|
|
1957
|
+
candidateHash, execution: { status: 'infrastructure_error', assertionCount: 0 }, observations: [] }; }
|
|
1958
|
+
finally { clearInterval(monitor); signal?.removeEventListener('abort', abort); }
|
|
1959
|
+
// Bound storage before retaining output from a verifier transport.
|
|
1960
|
+
try {
|
|
1961
|
+
const serialized = JSON.stringify(receipt);
|
|
1962
|
+
if (!serialized || Buffer.byteLength(serialized) > 48 * 1024) throw new Error('Unusable observation.');
|
|
1963
|
+
receipt = JSON.parse(serialized);
|
|
1964
|
+
} catch { receipt = null; }
|
|
1965
|
+
return updateRecord(root, provider, session, async record => {
|
|
1966
|
+
signal?.throwIfAborted();
|
|
1967
|
+
const currentLink = await providerLinkState(root, provider);
|
|
1968
|
+
if (!eligible(record) || record.run.id !== snapshot.run.id || record.run.revision !== snapshot.run.revision ||
|
|
1969
|
+
record.run.controlRevision !== snapshot.run.controlRevision || !currentLink.connected || currentLink.epoch !== link.epoch ||
|
|
1970
|
+
JSON.stringify(record.run.managed.enrollment) !== JSON.stringify(snapshot.run.managed.enrollment) ||
|
|
1971
|
+
record.run.managed.requirementContract.contractHash !== contract.contractHash) return record;
|
|
1972
|
+
const owner = record.run.managed.liveHookOwner;
|
|
1973
|
+
if (owner && await processIdentity(owner.pid) !== owner.identity) return record;
|
|
1974
|
+
record.run.managed.requirementObservation = { id: randomUUID(), source: 'host_requirement_observer',
|
|
1975
|
+
...(snapshot.run.managed.requirementChecker ? { checkerDigest: snapshot.run.managed.requirementChecker.digest } : {}),
|
|
1976
|
+
candidateHash, stateRoot: root, checkedAt: now, current: false, receipt };
|
|
1977
|
+
await verifyAutomaticCriteria(record, now);
|
|
1978
|
+
if (!pendingAcceptance(record.run).length && record.run.state === 'verification_pending') {
|
|
1979
|
+
record.run.state = 'active'; record.run.reason = 'Current behavior observation passed; awaiting the original Stop boundary.';
|
|
1980
|
+
}
|
|
1981
|
+
return record;
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1823
1985
|
export async function verifyAutomaticCriteria(record, now) {
|
|
1824
1986
|
const managed = record.run.managed;
|
|
1987
|
+
if (managed.requirementObservation) {
|
|
1988
|
+
const observation = managed.requirementObservation;
|
|
1989
|
+
// Refresh against the actual current workspace on every checkpoint/Stop.
|
|
1990
|
+
// A cached worker PASS or a previously matching candidate cannot waive it.
|
|
1991
|
+
observation.current = observation.source === 'host_requirement_observer' &&
|
|
1992
|
+
(!managed.requirementChecker || observation.checkerDigest === managed.requirementChecker.digest &&
|
|
1993
|
+
await registeredCheckerCurrent(observation.stateRoot, managed.requirementChecker)) &&
|
|
1994
|
+
observation.receipt?.candidateHash === observation.candidateHash &&
|
|
1995
|
+
observation.receipt?.contractHash === managed.requirementContract?.contractHash &&
|
|
1996
|
+
observation.receipt?.checkVersion === managed.requirementContract?.checkVersion &&
|
|
1997
|
+
observation.candidateHash === await workspaceIdentity(taskDirectory(record), observation.stateRoot).catch(() => null);
|
|
1998
|
+
}
|
|
1825
1999
|
if (managed.liveOutcomeReview?.passed && managed.liveOutcomeReview.workspaceDigest !== await workspaceIdentity(taskDirectory(record)).catch(() => null)) {
|
|
1826
2000
|
managed.liveOutcomeReview = { ...managed.liveOutcomeReview, passed: false, summary: 'Workspace changed after independent review; inspect the current result.' };
|
|
1827
2001
|
delete managed.nativeOutcomeReview;
|
|
@@ -1834,6 +2008,23 @@ export async function verifyAutomaticCriteria(record, now) {
|
|
|
1834
2008
|
evidence_refs: [`host-check:${managed.taskId}:${criterion.id}:${sha256 || 'failed'}`] };
|
|
1835
2009
|
};
|
|
1836
2010
|
const failed = (criterion, detail) => { delete managed.verified[criterion.id]; observe(criterion, false, detail); };
|
|
2011
|
+
if (managed.requirementContract) {
|
|
2012
|
+
const pending = new Map(pendingRequirements(managed).map(row => [row.id, row]));
|
|
2013
|
+
let requirements = [];
|
|
2014
|
+
try { requirements = normalizeRequirementContract(managed.requirementContract).requirements; }
|
|
2015
|
+
catch { for (const id of Object.keys(managed.verified)) if (id.startsWith('behavior:')) delete managed.verified[id]; }
|
|
2016
|
+
for (const requirement of requirements) {
|
|
2017
|
+
const id = `behavior:${requirement.id}`, row = pending.get(id);
|
|
2018
|
+
if (row) delete managed.verified[id];
|
|
2019
|
+
else {
|
|
2020
|
+
newVerifiedCriterion(record.run, id, now);
|
|
2021
|
+
managed.verified[id] = { source: 'behavior', checkedAt: now, sha256: managed.requirementObservation.candidateHash };
|
|
2022
|
+
}
|
|
2023
|
+
managed.criterionObservations[id] = { criterion_id: id, passed: row ? row.status === 'failed' ? false : null : true,
|
|
2024
|
+
verification_method: 'host_behavior', observed_result: row?.detail || 'All required assertions independently observed.',
|
|
2025
|
+
evidence_refs: managed.requirementObservation ? [`host-behavior:${managed.requirementObservation.id}`] : [] };
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
1837
2028
|
const feedbackPending = new Set(pendingAcceptanceFeedback(record.run).map(item => item.criterionId));
|
|
1838
2029
|
for (const criterion of effectiveAcceptanceCriteria(managed)) {
|
|
1839
2030
|
if (criterion.check.type === 'manual') {
|
|
@@ -1864,6 +2055,29 @@ export async function verifyAutomaticCriteria(record, now) {
|
|
|
1864
2055
|
} catch { failed(criterion, 'Acceptance file could not be independently read'); }
|
|
1865
2056
|
}
|
|
1866
2057
|
}
|
|
2058
|
+
// A semantic criterion is verified only by the independent review, and the
|
|
2059
|
+
// review is queued when the worker reports completed. The worker sees the
|
|
2060
|
+
// criterion as unverified until then, so it can keep re-reading an already
|
|
2061
|
+
// correct result and never report completed: the review never runs and the
|
|
2062
|
+
// task spends its whole budget. Two consecutive observed read-only results
|
|
2063
|
+
// (reads cannot change the workspace) with nothing pending except semantic
|
|
2064
|
+
// criteria mean there is nothing left for the worker to do, so the controller
|
|
2065
|
+
// schedules the review instead. The review still decides, and its findings
|
|
2066
|
+
// become the next repair cycle. A workspace the last review already judged is
|
|
2067
|
+
// not reviewed again; after a repair it is.
|
|
2068
|
+
function semanticReviewStalled(run) {
|
|
2069
|
+
const m = run.managed;
|
|
2070
|
+
if (m.executionUncertain || m.pendingApproval || m.userQuestion || m.dependencyWait || searchState(m).phase === 'verifying') return false;
|
|
2071
|
+
const pending = pendingAcceptance(run);
|
|
2072
|
+
if (!pending.length || pending.some(item => item.check !== 'semantic')) return false;
|
|
2073
|
+
const recent = (m.workHistory || []).filter(item => item.status !== 'session_understanding').slice(-2);
|
|
2074
|
+
return recent.length === 2 && recent.every(item => item.action?.kind === 'read' && item.result?.status === 'observed');
|
|
2075
|
+
}
|
|
2076
|
+
async function reviewedCurrentWorkspace(record, root, review) {
|
|
2077
|
+
if (!review?.workspaceDigest) return false;
|
|
2078
|
+
const current = await workspaceIdentity(taskDirectory(record), root, undefined, { excludedPaths: review.manifest?.source === 'controller_certified_independent_proposal' ? review.manifest.exclusions.map(row => row.path) : [] }).catch(() => null);
|
|
2079
|
+
return current === review.workspaceDigest;
|
|
2080
|
+
}
|
|
1867
2081
|
function purposeCompletionBarrier(run) {
|
|
1868
2082
|
const managed=run.managed;
|
|
1869
2083
|
if(!searchEnabled(managed)) return null;
|
|
@@ -2137,8 +2351,10 @@ const pausesWithoutProgress = managed => managed.continuationPolicy === 'progres
|
|
|
2137
2351
|
function recordManagedOutcomeProgress(run, now, assessmentAvailable = true) {
|
|
2138
2352
|
const managed = run.managed;
|
|
2139
2353
|
if (managed?.continuationPolicy !== 'progress') return;
|
|
2140
|
-
const current = managed.acceptanceCriteria.map(criterion => ({ id: criterion.id, statement: criterion.description }))
|
|
2141
|
-
|
|
2354
|
+
const current = [...managed.acceptanceCriteria.map(criterion => ({ id: criterion.id, statement: criterion.description })),
|
|
2355
|
+
...(managed.requirementContract?.requirements || []).map(row => ({ id: `behavior:${row.id}`, statement: row.description }))];
|
|
2356
|
+
const objectiveDigest = digest(JSON.stringify({ objective: run.objective, criteria: managed.acceptanceCriteria,
|
|
2357
|
+
...(managed.requirementContract ? { requirementContract: managed.requirementContract } : {}) }));
|
|
2142
2358
|
const results = current.flatMap(criterion => {
|
|
2143
2359
|
const observation = managed.verified[criterion.id];
|
|
2144
2360
|
return observation ? [{ criterion_id: criterion.id, passed: true,
|
|
@@ -2286,7 +2502,7 @@ export function decideStop(run, now = Date.now()) {
|
|
|
2286
2502
|
next.reason = 'check_reported_outcome';
|
|
2287
2503
|
}
|
|
2288
2504
|
const selected = next.managed?.autonomyPolicy ? next.managed.decision : null;
|
|
2289
|
-
const instruction = inspectCompletion ? resultFollowThroughInstruction({ completion: true }) : selected && next.managed.phase === 'action_association' ? `${associationContract(next.managed.actionAssociation.job)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping.` : selected && next.managed.phase === 'understanding' ?
|
|
2505
|
+
const instruction = inspectCompletion ? resultFollowThroughInstruction({ completion: true }) : selected && next.managed.phase === 'action_association' ? `${associationContract(next.managed.actionAssociation.job)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping.` : selected && next.managed.phase === 'understanding' ? `${understandingTurnInstruction(next.managed, selected)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping. A shell heredoc is suitable. This updates SIMY metadata only; do not create a checkpoint file in the task workspace.` : selected ? `${initialHook ? `Managed objective: ${next.objective}. Frozen acceptance criteria: ${JSON.stringify(next.managed.acceptanceCriteria)}. ` : ''}${replanHook ? `Replan context: ${next.managed.plan.diagnosis}. ` : ''}Execute only selected ${selected.action.kind} action in scope ${selected.action.scope}: ${selected.action.summary}. Expected verification: ${selected.expectedVerification}. Rollback: ${selected.rollback || 'none needed'}. ${assessedCheckpointContract(selected, next.managed)} Submit that JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping. A shell heredoc is suitable; do not create a task-workspace checkpoint file.` : fresh ? `${minimalSufficientChangeInstruction()}\n${informationWaitInstruction}\n${next.nextStep}` : `${minimalSufficientChangeInstruction()}\n${informationWaitInstruction}\n${resultFollowThroughInstruction()}`;
|
|
2290
2506
|
return { run: next, output: { decision: 'block', reason: `SIMY Delivery Loop (${next.continuations} continuations). ${next.executionCwd ? `Use task execution directory ${JSON.stringify(next.executionCwd)} for task work and acceptance. ` : ''}${deliveryAcceptanceContext(next)} ${instruction}\n${purposeContract(next.managed)}\nStay within the original request and permissions. If one action lacks authorization or information, preserve that dependency and select independent authorized work. Resume this same task; do not start a replacement loop.` } };
|
|
2291
2507
|
}
|
|
2292
2508
|
// A process signature protects against PID reuse. Never treat a stale timer alone
|
|
@@ -2695,7 +2911,7 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
2695
2911
|
if (run.managed && run.checkpoint?.status === 'completed' && run.revision > run.consumedRevision) {
|
|
2696
2912
|
await verifyAutomaticCriteria(record, now);
|
|
2697
2913
|
const pending = pendingAcceptance(run);
|
|
2698
|
-
const missingFiles = pending.filter(item => ['file_exists', 'file_contains', 'file_equals'].includes(item.check) || run.managed.liveOutcomeReview?.passed === false && item.check === 'semantic');
|
|
2914
|
+
const missingFiles = pending.filter(item => ['file_exists', 'file_contains', 'file_equals', 'behavior'].includes(item.check) || run.managed.liveOutcomeReview?.passed === false && item.check === 'semantic');
|
|
2699
2915
|
if (missingFiles.length && run.managed.autonomyPolicy && !run.managed.pendingApproval &&
|
|
2700
2916
|
!run.managed.nativeWait && !run.managed.purpose?.nativeBoundary && !run.managed.executionUncertain) {
|
|
2701
2917
|
// Completion reads at checkpoint time can become stale before Stop.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const MAX_INPUT_BYTES = 256 * 1024;
|
|
5
|
+
|
|
6
|
+
// Detect the address only. Claude, not a keyword classifier, interprets intent.
|
|
7
|
+
export function routeMention(input, pluginRoot) {
|
|
8
|
+
if (input?.hook_event_name !== 'UserPromptSubmit' || typeof input.prompt !== 'string' ||
|
|
9
|
+
!pluginRoot || !/(^|[^A-Za-z0-9_./:@-])(?:@simy(?![A-Za-z0-9_./:@-])|@(?:agent-)?simy:simy(?![A-Za-z0-9_./:@-]))/i.test(input.prompt)) return {};
|
|
10
|
+
const skill = name => JSON.stringify(join(pluginRoot, 'skills', name, 'SKILL.md'));
|
|
11
|
+
return { hookSpecificOutput: {
|
|
12
|
+
hookEventName: 'UserPromptSubmit',
|
|
13
|
+
additionalContext: `SIMY request routing for this submitted message. Parent conversation identity supplied by Claude: ${JSON.stringify(typeof input.session_id === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(input.session_id) ? input.session_id : null)}. If unavailable, obtain the original identity before any control:
|
|
14
|
+
|
|
15
|
+
Interpret the latest user's complete message in its conversation context, including Japanese and English. The @simy address or SIMY agent selection is not itself authorization. A SIMY subagent returns advice only; the parent Claude conversation must perform authorized controls using its own verified identity, never a child session ID. Do not classify intent by isolated keywords.
|
|
16
|
+
For an explicit request to continue or take over the original work, inspect/disable Auto Continue, or stop/wait, read and follow ${skill('continue')}.
|
|
17
|
+
For diagnosis, why work stopped, or repair of a stopped task, read and follow ${skill('diagnose')}. Diagnosis alone does not authorize repair or continuation.
|
|
18
|
+
For an explicit quality check or SQM request, read and follow ${skill('sqm-loop')}.
|
|
19
|
+
For an explicit PR review, read and follow ${skill('pr-review')}. A review alone does not authorize implementing fixes.
|
|
20
|
+
A task stop is distinct from turning Auto Continue off: use the original task's existing controls and clarify if the requested stop is ambiguous. For questions about SIMY, answer the question. Quoted text, examples, code blocks, reported speech, hypotheticals, negation, or a request to wait/stop are not authorization to start work. If a quoted example accompanies a separate genuine request, follow only that request.
|
|
21
|
+
Handle explicitly requested combined intents within each skill's scope. Continuing work does not imply SQM. If the intended action is ambiguous, clarify it instead of enabling work by default.
|
|
22
|
+
Preserve the exact original Claude conversation and its verified identity, permissions, stops, approval waits and opt-outs. Never create another task or provider session to bypass those boundaries. Do not treat a mention, chosen skill or accepted setting as proof that execution succeeded.`,
|
|
23
|
+
} };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function main({ stream = process.stdin, output = process.stdout, env = process.env } = {}) {
|
|
27
|
+
let result = {};
|
|
28
|
+
try {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
let bytes = 0;
|
|
31
|
+
for await (const chunk of stream) {
|
|
32
|
+
const buffer = Buffer.from(chunk);
|
|
33
|
+
bytes += buffer.length;
|
|
34
|
+
if (bytes > MAX_INPUT_BYTES) { output.write('{}\n'); return; }
|
|
35
|
+
chunks.push(buffer);
|
|
36
|
+
}
|
|
37
|
+
result = routeMention(JSON.parse(Buffer.concat(chunks).toString('utf8')), env.CLAUDE_PLUGIN_ROOT);
|
|
38
|
+
} catch { /* A malformed hook input must not block the user's message. */ }
|
|
39
|
+
output.write(JSON.stringify(result) + '\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) await main();
|