opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.10
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/LICENSE +21 -0
- package/README.md +318 -2
- package/index.js +1836 -0
- package/package.json +70 -4
- package/skills/codebase-design/SKILL.md +114 -0
- package/skills/design-contract/SKILL.md +27 -0
- package/skills/design-critique/SKILL.md +19 -0
- package/skills/design-system/SKILL.md +17 -0
- package/skills/design-system/references/image-analysis.md +14 -0
- package/skills/design-system/references/tokens.md +32 -0
- package/skills/design-system/references/ui-rules.md +14 -0
- package/skills/diagnosing-bugs/SKILL.md +138 -0
- package/skills/frontend-design/SKILL.md +36 -0
- package/skills/handoff/SKILL.md +16 -0
- package/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/skills/linter-negative/SKILL.md +74 -0
- package/skills/no-slop-ui/SKILL.md +26 -0
- package/skills/prototype/SKILL.md +26 -0
- package/skills/registry.json +44087 -0
- package/skills/tastemaker/SKILL.md +8 -0
- package/skills/tastemaker/references/tastemaker.md +14 -0
- package/skills/tdd/SKILL.md +38 -0
- package/skills/visual-review/SKILL.md +103 -0
- package/skills/visual-review/references/review-gates.md +42 -0
- package/skills/visual-review/references/surface-archetypes.md +37 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/writing-for-agents/SKILL.md +81 -0
- package/src/cognition/cognition-store.js +251 -0
- package/src/cognition/cognitive-state.js +126 -0
- package/src/cognition/hypothesis-manager.js +113 -0
- package/src/cognition/observation-engine.js +112 -0
- package/src/cognition/observation-provenance.js +268 -0
- package/src/context/active-context-boundary.js +68 -0
- package/src/context/assembly.js +494 -0
- package/src/context/assessment-rules.js +55 -0
- package/src/context/capsule-staleness.js +267 -0
- package/src/context/capsules.js +2 -0
- package/src/context/compare-array.js +98 -0
- package/src/context/compare-object.js +89 -0
- package/src/context/compare-values.js +42 -0
- package/src/context/context-assembly-contract.js +71 -0
- package/src/context/context-budget-manager.js +232 -0
- package/src/context/context-builder.js +49 -0
- package/src/context/context-cache.js +55 -0
- package/src/context/context-capture.js +243 -0
- package/src/context/context-classification.js +199 -0
- package/src/context/context-compaction.js +179 -0
- package/src/context/context-decision-audit.js +267 -0
- package/src/context/context-demotion.js +173 -0
- package/src/context/context-dictionary.js +106 -0
- package/src/context/context-drift-detection.js +225 -0
- package/src/context/context-evaluation.js +374 -0
- package/src/context/context-event-ingress.js +100 -0
- package/src/context/context-event-normalization.js +224 -0
- package/src/context/context-feedback-loop.js +216 -0
- package/src/context/context-freshness.js +21 -0
- package/src/context/context-graph-builder.js +71 -0
- package/src/context/context-graph.js +451 -0
- package/src/context/context-history-relevance.js +321 -0
- package/src/context/context-interception.js +145 -0
- package/src/context/context-invalidation-events.js +154 -0
- package/src/context/context-lifecycle.js +170 -0
- package/src/context/context-loop-prevention.js +347 -0
- package/src/context/context-manager.js +340 -0
- package/src/context/context-memory-layers.js +265 -0
- package/src/context/context-minimality.js +166 -0
- package/src/context/context-normalization.js +130 -0
- package/src/context/context-optimization-metrics.js +434 -0
- package/src/context/context-output-policy.js +135 -0
- package/src/context/context-pack-authority.js +157 -0
- package/src/context/context-promotion.js +172 -0
- package/src/context/context-query-contract.js +101 -0
- package/src/context/context-ranking.js +164 -0
- package/src/context/context-rate-distortion.js +261 -0
- package/src/context/context-recovery.js +209 -0
- package/src/context/context-retrieval-integration.js +60 -0
- package/src/context/context-retrieval.js +93 -0
- package/src/context/context-router.js +457 -0
- package/src/context/context-routing-evaluation.js +331 -0
- package/src/context/context-routing-observability.js +296 -0
- package/src/context/context-routing-shadow.js +237 -0
- package/src/context/context-scope.js +181 -0
- package/src/context/context-snapshot.js +226 -0
- package/src/context/context-source-registry.js +95 -0
- package/src/context/context-sufficiency-gate.js +120 -0
- package/src/context/context-sufficiency-oracle.js +167 -0
- package/src/context/context-telemetry.js +176 -0
- package/src/context/context.js +633 -0
- package/src/context/dependency-closure.js +246 -0
- package/src/context/page-fault-tracker.js +275 -0
- package/src/context/prompt-engine.js +74 -0
- package/src/context/runtime-context-graph.js +260 -0
- package/src/context/selection.js +2 -0
- package/src/context/session.js +2 -0
- package/src/context/sufficiency-contract.js +300 -0
- package/src/context/tokenizer.js +17 -0
- package/src/context-benchmark-router.mjs +125 -0
- package/src/evidence/evidence-engine.js +161 -0
- package/src/evidence/evidence-factory.js +3 -0
- package/src/evidence/evidence-freshness.js +463 -0
- package/src/evidence/evidence-gap.js +76 -0
- package/src/evidence/evidence-lineage.js +525 -0
- package/src/evidence/evidence.js +136 -0
- package/src/evidence/freshness-validation.js +187 -0
- package/src/evidence/lineage.js +4 -0
- package/src/execution/action-evaluation.js +179 -0
- package/src/execution/circuit-breaker.js +70 -0
- package/src/execution/claim-interception.js +63 -0
- package/src/execution/execution-engine.js +258 -0
- package/src/execution/execution-intent.js +69 -0
- package/src/execution/execution-state.js +89 -0
- package/src/execution/formatting.js +60 -0
- package/src/execution/l1-sanitize.js +16 -0
- package/src/execution/l2-resource-guards.js +19 -0
- package/src/execution/l3-invariant-checker.js +5 -0
- package/src/execution/l4-containment.js +6 -0
- package/src/execution/runtime-guards.js +106 -0
- package/src/execution/strategy-identity.js +162 -0
- package/src/execution/tool-interceptor.js +7 -0
- package/src/execution/tool-result-classifier.js +63 -0
- package/src/integration/canary-deploy.js +143 -0
- package/src/integration/collector.js +59 -0
- package/src/integration/error-recovery.js +6 -0
- package/src/integration/event-bus.js +66 -0
- package/src/integration/health-checker.js +57 -0
- package/src/integration/logger.js +39 -0
- package/src/integration/message-handler.js +611 -0
- package/src/integration/opencode-db.js +153 -0
- package/src/integration/orchestration.js +419 -0
- package/src/integration/part-provenance.js +65 -0
- package/src/integration/rate-limiter.js +55 -0
- package/src/integration/release-gate.js +336 -0
- package/src/integration/rollback-manager.js +164 -0
- package/src/integration/router-adapter.js +269 -0
- package/src/integration/telemetry.js +45 -0
- package/src/integration/update-checker.js +10 -0
- package/src/persistence/event-sourcing.js +75 -0
- package/src/persistence/memory.js +501 -0
- package/src/persistence/migration-runner.js +16 -0
- package/src/persistence/persistence-manager.js +39 -0
- package/src/persistence/transaction-log.js +157 -0
- package/src/policy/action-capabilities.js +116 -0
- package/src/policy/action-policy.js +4 -0
- package/src/policy/assessment-engine.js +104 -0
- package/src/policy/assumption-tracking.js +66 -0
- package/src/policy/budget-enforcer.js +7 -0
- package/src/policy/config-validator.js +102 -0
- package/src/policy/execution-assessment.js +142 -0
- package/src/policy/failure-classification.js +5 -0
- package/src/policy/feature-flags.js +65 -0
- package/src/policy/governance-enforcement.js +118 -0
- package/src/policy/locale.js +15 -0
- package/src/policy/multi-signer.js +151 -0
- package/src/policy/policy-state-machine.js +97 -0
- package/src/policy/ponytail-constraint.js +38 -0
- package/src/policy/project-inspector.js +93 -0
- package/src/policy/query-builder.js +14 -0
- package/src/policy/request-classifier.js +79 -0
- package/src/policy/risk-engine.js +266 -0
- package/src/policy/scope-enforcement.js +140 -0
- package/src/policy/secret-scanner.js +68 -0
- package/src/policy/skill-routing.js +104 -0
- package/src/policy/strategy-capabilities.js +211 -0
- package/src/policy/strategy.js +6 -0
- package/src/policy/uncertainty.js +117 -0
- package/src/shared/container.js +44 -0
- package/src/shared/decision-types.js +24 -0
- package/src/shared/simple-test.js +160 -0
- package/src/shared/wam-events.js +52 -0
- package/src/skills/engine.js +1705 -0
- package/src/skills/skill-registry.js +19 -0
- package/src/skills/skill-routing.js +340 -0
- package/src/state/lifecycle-manager.js +113 -0
- package/src/state/operational-state.js +198 -0
- package/src/state/projections.js +47 -0
- package/src/state/replay-engine.js +5 -0
- package/src/state/requirement-ownership.js +20 -0
- package/src/state/requirement-state.js +152 -0
- package/src/state/run-event-identity.js +172 -0
- package/src/state/state-engine.js +19 -0
- package/src/state/state-machine.js +119 -0
- package/src/state/state-schema.js +318 -0
- package/src/state/state-store.js +335 -0
- package/src/state/state-transition.js +84 -0
- package/src/state/task-dependencies.js +287 -0
- package/src/state/task-execution.js +217 -0
- package/src/state/task-lifecycle.js +59 -0
- package/src/state/task-runs.js +361 -0
- package/src/state/task-state-file.js +86 -0
- package/src/state/task-state.js +41 -0
- package/src/state/task-status.js +54 -0
- package/src/state/task-store.js +48 -0
- package/src/state/wam-state.js +148 -0
- package/src/verification/completion-gate-simple.js +8 -0
- package/src/verification/completion-gate.js +183 -0
- package/src/verification/contract.js +9 -0
- package/src/verification/contradiction-resolution.js +255 -0
- package/src/verification/diagnostic-engine.js +659 -0
- package/src/verification/false-completion-prevention.js +100 -0
- package/src/verification/hook-validator.js +6 -0
- package/src/verification/requirements.js +6 -0
- package/src/verification/verification-context.js +162 -0
- package/src/verification/verification-lifecycle.js +66 -0
- package/src/verification/verification-model.js +89 -0
- package/src/verification/verification-persistence.js +129 -0
- package/src/verification/verification-policy.js +127 -0
- package/src/verification/verification.js +415 -0
package/index.js
ADDED
|
@@ -0,0 +1,1836 @@
|
|
|
1
|
+
import { analyze, getTaskState, persistTaskState, routeSkillsV2, loadSkillOnDemand, cavemanify, estimateTokens, buildAssumptions, escalateAssumptions, formatBacklog, findDuplicateTask } from "./src/skills/engine.js";
|
|
2
|
+
import { startExperiment, noteSuccess, noteFailure } from "./src/execution/execution-engine.js";
|
|
3
|
+
import { migrateLegacyCognition } from "./src/cognition/cognition-store.js";
|
|
4
|
+
import { handleMessage } from "./src/integration/message-handler.js";
|
|
5
|
+
import { injectWamParts } from "./src/integration/part-provenance.js";
|
|
6
|
+
|
|
7
|
+
import { initMemory, updateProjectMemo, summarizeOperationalContext, updateContext, getOperationalContext, updateTaskMemory, addRecentChange, recordDecision, getDecision, updateLiveContext, compactDecisions } from "./src/persistence/memory.js";
|
|
8
|
+
import { getSessionId, listCapsules, getCapsule, promoteCapsule, selectContext, retrieveContext, closeSession, resolveWamRoot, migrateLegacyCapsules } from "./src/context/context.js";
|
|
9
|
+
import { assembleContext } from "./src/context/assembly.js";
|
|
10
|
+
import { evaluateRequirement as evaluateRequirementChecks, verifyRequirement } from "./src/verification/verification.js";
|
|
11
|
+
import { ContextDecisionTracer } from "./src/context/context-decision-audit.js";
|
|
12
|
+
import { guardAction } from "./src/execution/runtime-guards.js";
|
|
13
|
+
import { WamPolicyBlock, evaluateAction } from "./src/policy/risk-engine.js";
|
|
14
|
+
import { enforceGovernance } from "./src/policy/governance-enforcement.js";
|
|
15
|
+
import { classifyByCapabilities, buildCandidate } from "./src/policy/strategy-capabilities.js";
|
|
16
|
+
import { getStatusReport } from "./src/execution/execution-state.js";
|
|
17
|
+
import { createSnapshot, checkContinuation, rebuildScope } from "./src/context/context-snapshot.js";
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
|
|
21
|
+
const sessionExecutions = new Map();
|
|
22
|
+
|
|
23
|
+
const SAFE_READ_TOOLS = new Set(["read", "read_file", "list_directory", "list_files", "get_file"]);
|
|
24
|
+
|
|
25
|
+
function isSafeReadTool(tool) {
|
|
26
|
+
const normalized = String(tool || "").toLowerCase();
|
|
27
|
+
if (SAFE_READ_TOOLS instanceof Set) return SAFE_READ_TOOLS.has(normalized);
|
|
28
|
+
if (SAFE_READ_TOOLS?.length) return SAFE_READ_TOOLS.some((t) => t?.toLowerCase() === normalized);
|
|
29
|
+
return ["read", "read_file", "list_directory", "list_files", "get_file"].includes(normalized);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function inferExpectedObservation(tool, args, requirement) {
|
|
33
|
+
const operation = String(tool || "").toLowerCase();
|
|
34
|
+
const outcomeType = inferOutcomeType(operation, args);
|
|
35
|
+
|
|
36
|
+
if (requirement) {
|
|
37
|
+
return {
|
|
38
|
+
requirementId: requirement.id,
|
|
39
|
+
outcomeType,
|
|
40
|
+
expected: {
|
|
41
|
+
status: "success",
|
|
42
|
+
exitCode: 0,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (operation.includes("read") || operation.includes("list") || operation.includes("inspect")) {
|
|
48
|
+
return { outcomeType, expected: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (operation.includes("write") || operation.includes("edit") || operation.includes("apply") || operation.includes("run") || operation.includes("test")) {
|
|
52
|
+
return {
|
|
53
|
+
outcomeType,
|
|
54
|
+
expected: {
|
|
55
|
+
status: "success",
|
|
56
|
+
exitCode: 0,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { outcomeType, expected: null };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function inferOutcomeType(operation, args) {
|
|
65
|
+
if (operation.includes("run") || operation.includes("exec") || operation.includes("bash")) return "command_execution";
|
|
66
|
+
if (operation.includes("test")) return "test_execution";
|
|
67
|
+
if (operation.includes("write") || operation.includes("edit") || operation.includes("apply")) return "file_modification";
|
|
68
|
+
if (operation.includes("read") || operation.includes("list") || operation.includes("inspect")) return "file_inspection";
|
|
69
|
+
if (operation.includes("search") || operation.includes("grep")) return "search";
|
|
70
|
+
return "unknown";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function bridgeExecution({ taskId, taskRoot, state, tool, args, callID, sessionID }) {
|
|
74
|
+
if (!callID) {
|
|
75
|
+
callID = `${sessionID || getSessionId()}__exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
76
|
+
}
|
|
77
|
+
const resolvedTaskRoot = taskRoot;
|
|
78
|
+
const resolvedTaskId = taskId;
|
|
79
|
+
const resolvedTool = tool;
|
|
80
|
+
const resolvedArgs = args || {};
|
|
81
|
+
const resolvedState = state || {};
|
|
82
|
+
const requirements = Array.isArray(resolvedState.requirements) ? resolvedState.requirements : [];
|
|
83
|
+
const requirement = requirements.find((item) => item?.status !== "done" && item?.status !== "verified") || requirements[0] || null;
|
|
84
|
+
const contractApproved = resolvedState.contract?.status === "APPROVED";
|
|
85
|
+
const phase = resolvedState.phase;
|
|
86
|
+
|
|
87
|
+
if (!taskId || !taskRoot || !tool || (!contractApproved && !["IMPLEMENTING", "VERIFYING"].includes(phase)) || isSafeReadTool(tool)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const requirementText = requirement?.title || requirement?.description || requirement?.text || null;
|
|
92
|
+
const hypothesisStatement = requirementText ? `${requirement.id || "req"}: ${requirementText}` : `${tool} ${JSON.stringify(args)}`;
|
|
93
|
+
try {
|
|
94
|
+
const result = await startExperiment(taskRoot, taskId, {
|
|
95
|
+
statement: hypothesisStatement,
|
|
96
|
+
tool,
|
|
97
|
+
args,
|
|
98
|
+
expectedObservation: inferExpectedObservation(tool, args, requirement),
|
|
99
|
+
confidence: 0.5,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!result?.ok || !result.hypothesis || !result.experiment) {
|
|
103
|
+
console.log(`[wait-a-minute] No se pudo iniciar el experimento para ${tool}: ${result?.guard?.reason || "sin resultado"}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const key = callID || `${sessionID}:${tool}`;
|
|
108
|
+
const mapping = {
|
|
109
|
+
hypothesisId: result.hypothesis.id,
|
|
110
|
+
experimentId: result.experiment.id,
|
|
111
|
+
requirementId: requirement?.id || null,
|
|
112
|
+
};
|
|
113
|
+
sessionExecutions.set(key, mapping);
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
callID: key,
|
|
117
|
+
hypothesisId: result.hypothesis.id,
|
|
118
|
+
experimentId: result.experiment.id,
|
|
119
|
+
requirementId: requirement?.id || null,
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.log(`[wait-a-minute] Falló el puente de ejecución para ${tool}: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Wait a Minute plugin for OpenCode.
|
|
128
|
+
*
|
|
129
|
+
* Adds deterministic control, task-state management, context enrichment, and
|
|
130
|
+
* token optimization to OpenCode agents by correlating tasks, skills, context,
|
|
131
|
+
* evidence, and verified state to determine what should happen next.
|
|
132
|
+
*
|
|
133
|
+
* Intercepts the prompt via chat.message hook before skill resolution and
|
|
134
|
+
* agent execution. Classifies the request, inspects the project, detects
|
|
135
|
+
* assumptions, and selects relevant skills.
|
|
136
|
+
*
|
|
137
|
+
* The analysis results are stored in the session and can be accessed by the
|
|
138
|
+
* main agent before proceeding with implementation.
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
const DEFAULT_CONFIG = {
|
|
142
|
+
tierCaps: { fast: 8, medium: 5, heavy: 3 },
|
|
143
|
+
activePreset: "omni",
|
|
144
|
+
silent: false,
|
|
145
|
+
budgetTokens: 32000,
|
|
146
|
+
contextBudget: 4000,
|
|
147
|
+
tierPrompts: {},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// opencode 1.18.25: chat.message output = { message, parts } (sin .system).
|
|
151
|
+
// Cada part debe incluir un id único (formato prt_...) o el mensaje falla al guardar
|
|
152
|
+
// (SchemaError: Missing key at ["part"]["id"]), bloqueando la sesión.
|
|
153
|
+
function genPartId() {
|
|
154
|
+
let s = "prt_";
|
|
155
|
+
while (s.length < 16) s += Math.floor(Math.random() * 16).toString(16);
|
|
156
|
+
return s;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Inyecta texto visible al frente del mensaje de usuario; fallback a .system (API vieja).
|
|
160
|
+
function emitTextPart(output, text, meta = {}) {
|
|
161
|
+
if (!output) return;
|
|
162
|
+
const part = {
|
|
163
|
+
id: genPartId(),
|
|
164
|
+
type: "text",
|
|
165
|
+
text,
|
|
166
|
+
...(meta.sessionID ? { sessionID: meta.sessionID } : {}),
|
|
167
|
+
...(meta.messageID ? { messageID: meta.messageID } : {}),
|
|
168
|
+
};
|
|
169
|
+
injectWamParts(output, [part], { messageID: meta.messageID, phase: "emit", idFactory: genPartId, position: "prepend" });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// -- v1-enforcement: estado durable, contrato y progreso --------
|
|
173
|
+
|
|
174
|
+
function truncate(text, max = 80) {
|
|
175
|
+
if (!text || text.length <= max) return text || "";
|
|
176
|
+
return text.slice(0, max - 3) + "...";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function nextActionFrom(state) {
|
|
180
|
+
const pending = (state?.requirements || []).find((r) => !["done", "verified"].includes(r.status));
|
|
181
|
+
if (pending) return `Implementar ${truncate(pending.title)} (${pending.id} ${pending.status})`;
|
|
182
|
+
const unverified = (state?.requirements || []).find((r) => r.status === "done");
|
|
183
|
+
if (unverified) return `Verificar ${truncate(unverified.title)} — /wam progress ${unverified.id} verified <evidencia>`;
|
|
184
|
+
if (state?.requirements?.length) return "Verificar requisitos completos antes de DONE";
|
|
185
|
+
return "Continuar tarea";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const BLOCKED_TOOLS = new Set(["write", "edit", "bash", "task", "todowrite", "pty_spawn", "pty_write", "pty_kill"]);
|
|
189
|
+
|
|
190
|
+
// git de solo lectura (status/diff/log/...) es investigación read-only:
|
|
191
|
+
// nunca requiere run-loop ni delegación — permitido aun en ASKING.
|
|
192
|
+
const READONLY_GIT_RE = /^\s*git\s+(status|diff|log|show|ls-files|branch|stash\s+list|remote\s+-v|rev-parse|ls-remote)\b/;
|
|
193
|
+
|
|
194
|
+
function bashCommandOf(input) {
|
|
195
|
+
const c = input?.args?.command ?? input?.params?.command ?? input?.input?.command ?? "";
|
|
196
|
+
if (typeof c === "string" && c.trim()) return c;
|
|
197
|
+
try {
|
|
198
|
+
for (const v of Object.values(input?.args ?? {})) {
|
|
199
|
+
if (typeof v === "string" && /^\s*git\s+/m.test(v)) return v;
|
|
200
|
+
}
|
|
201
|
+
} catch {}
|
|
202
|
+
return "";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const ACTIVE_FILE = (root) => path.join(root || process.cwd(), ".wam", "active-task");
|
|
206
|
+
|
|
207
|
+
function readActiveTaskId(root) {
|
|
208
|
+
try {
|
|
209
|
+
const v = fs.readFileSync(ACTIVE_FILE(root), "utf-8").trim();
|
|
210
|
+
if (!v) return null;
|
|
211
|
+
try {
|
|
212
|
+
const j = JSON.parse(v);
|
|
213
|
+
if (j && typeof j === "object" && j.id) return j.id;
|
|
214
|
+
} catch {}
|
|
215
|
+
return v;
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readActiveTaskRecord(root) {
|
|
222
|
+
try {
|
|
223
|
+
const raw = fs.readFileSync(ACTIVE_FILE(root), "utf-8").trim();
|
|
224
|
+
if (!raw) return null;
|
|
225
|
+
try {
|
|
226
|
+
const j = JSON.parse(raw);
|
|
227
|
+
if (j && j.id && typeof j.ts === "number") return j;
|
|
228
|
+
} catch {}
|
|
229
|
+
return { id: raw, ts: 0 };
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
function writeActiveTaskId(id, root, options = {}) {
|
|
238
|
+
// Ownership: include sessionId + projectRoot + ts for session isolation.
|
|
239
|
+
// Legacy format {id,ts} remains readable; new format adds ownership fields.
|
|
240
|
+
const payload = {
|
|
241
|
+
id,
|
|
242
|
+
ts: Date.now(),
|
|
243
|
+
sessionId: options.sessionId || null,
|
|
244
|
+
projectRoot: options.projectRoot || root || process.cwd(),
|
|
245
|
+
};
|
|
246
|
+
try {
|
|
247
|
+
fs.mkdirSync(path.dirname(ACTIVE_FILE(root)), { recursive: true });
|
|
248
|
+
fs.writeFileSync(ACTIVE_FILE(root), JSON.stringify(payload));
|
|
249
|
+
} catch (err) {
|
|
250
|
+
// Critical write: surface error to caller for handling — never silent
|
|
251
|
+
console.error(`[WAM] writeActiveTaskId failed for ${id} at ${root}:`, err.message);
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function listTaskIds(root) {
|
|
257
|
+
const dir = path.join(root || process.cwd(), ".wam", "tasks");
|
|
258
|
+
try {
|
|
259
|
+
return fs
|
|
260
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
261
|
+
.filter((d) => d.isDirectory())
|
|
262
|
+
.map((d) => d.name);
|
|
263
|
+
} catch {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// -- Persistencia por sesión: N sesiones pueden trabajar la MISMA carpeta sin
|
|
269
|
+
// colisionar en default-task / task-context.md / active-task. ----------------
|
|
270
|
+
const GENERIC_TASK = /^(default-task|task|general|)$/;
|
|
271
|
+
|
|
272
|
+
function readActiveTaskIdFresh(root, options = {}) {
|
|
273
|
+
const rec = readActiveTaskRecord(root);
|
|
274
|
+
if (!rec) return null;
|
|
275
|
+
const ttlMs = options.ttlMs || 60000;
|
|
276
|
+
if (!rec.ts) return null;
|
|
277
|
+
if (Date.now() - rec.ts > ttlMs) return null;
|
|
278
|
+
// Validate ownership: if record has sessionId, it must match caller session;
|
|
279
|
+
// if record has projectRoot, it must match caller's root. This prevents
|
|
280
|
+
// session A from silently adopting session B's active-task.
|
|
281
|
+
if (options.sessionId && rec.sessionId && rec.sessionId !== options.sessionId) return null;
|
|
282
|
+
if (options.projectRoot && rec.projectRoot && rec.projectRoot !== root) return null;
|
|
283
|
+
return rec.id;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* taskId efectivo de WAM para un mensaje:
|
|
288
|
+
* - input.taskId explícito y específico (conversación de opencode) → se usa tal cual
|
|
289
|
+
* - taskId conocido en sessionTasks (cache de chat.message) → ese
|
|
290
|
+
* - active-task reciente (<=60s, escrito por /wam task switch o /wam resume) →
|
|
291
|
+
* ese (intención explícita reciente del usuario)
|
|
292
|
+
* - sin taskId ni sesión conocida → namespace por sesión: ses-<sessionID>
|
|
293
|
+
*
|
|
294
|
+
* active-task global NO se usa como fallback automático: solo /wam task switch
|
|
295
|
+
* o /wam resume lo consultan vía readActiveTaskIdFresh() con TTL de 60s. Esto
|
|
296
|
+
* evita que sesiones nuevas adopten estado de tareas previas en la misma
|
|
297
|
+
* carpeta; tras 60s sin actividad el active-task se considera obsoleto.
|
|
298
|
+
*/
|
|
299
|
+
function effectiveTaskId(input, sessionTasks, wamRoot) {
|
|
300
|
+
if (input?.taskId && !GENERIC_TASK.test(input.taskId)) return input.taskId;
|
|
301
|
+
const cached = sessionTasks?.get?.(input?.sessionID);
|
|
302
|
+
if (cached) return cached;
|
|
303
|
+
const fresh = readActiveTaskIdFresh(wamRoot);
|
|
304
|
+
if (fresh) return fresh;
|
|
305
|
+
if (input?.sessionID) return `ses-${input.sessionID.slice(-10)}`;
|
|
306
|
+
return "default-task";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function taskDir(root, taskId) {
|
|
310
|
+
return path.join(root || process.cwd(), ".wam", "tasks", taskId);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function liveFileFor(root, taskId) {
|
|
314
|
+
return path.join(taskDir(root, taskId), "context.md");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function readLiveContext(root, taskId) {
|
|
318
|
+
const perSession = liveFileFor(root, taskId);
|
|
319
|
+
try {
|
|
320
|
+
if (fs.existsSync(perSession)) return fs.readFileSync(perSession, "utf-8").trim();
|
|
321
|
+
} catch {}
|
|
322
|
+
return "";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Live context por sesión: live snapshot en .wam/tasks/<taskId>/context.md.
|
|
327
|
+
* Aislado por task — dos sesiones sobre la misma carpeta NO se pisan el N2.
|
|
328
|
+
*/
|
|
329
|
+
function persistLiveContext(taskId, state, root) {
|
|
330
|
+
updateLiveContext(taskId, state, root);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// -- operational memory (memory.js): contexto inicial acelerador (spec §13) --
|
|
334
|
+
|
|
335
|
+
function projectState(analysis) {
|
|
336
|
+
return {
|
|
337
|
+
contract: {
|
|
338
|
+
status: "PROPOSED",
|
|
339
|
+
rigor: analysis.completionContract?.rigor || "NORMAL",
|
|
340
|
+
requirements: analysis.completionContract?.requirements || [],
|
|
341
|
+
verification: analysis.completionContract?.verification || [],
|
|
342
|
+
constraints: analysis.completionContract?.constraints || [],
|
|
343
|
+
unknowns: analysis.completionContract?.unknowns || [],
|
|
344
|
+
assumptions: Array.isArray(analysis.assumptions)
|
|
345
|
+
? analysis.assumptions
|
|
346
|
+
: buildAssumptions(analysis.assumed || []),
|
|
347
|
+
},
|
|
348
|
+
questions: [],
|
|
349
|
+
requirements: (analysis.completionContract?.requirements || []).map((title, i) => ({
|
|
350
|
+
id: `req-${i + 1}`,
|
|
351
|
+
title,
|
|
352
|
+
status: "pending",
|
|
353
|
+
evidence: [],
|
|
354
|
+
})),
|
|
355
|
+
persistentPolicies: (analysis.persistentPolicies || []).map((p) => p.policy),
|
|
356
|
+
activeGates: (analysis.persistentPolicies || []).flatMap((p) => p.gates || []),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
function applyCompletionGate(state, promptText, taskId, waitAMinute, persistTaskState, nextActionFrom, root) {
|
|
362
|
+
const gate = waitAMinute.evaluateCompletionGate(state, promptText);
|
|
363
|
+
if (gate.blocked) {
|
|
364
|
+
state.phase = gate.verifying ? "VERIFYING" : "IMPLEMENTING";
|
|
365
|
+
state.nextAction = nextActionFrom(state);
|
|
366
|
+
persistTaskState(taskId, state, root);
|
|
367
|
+
} else if (gate.allDone) {
|
|
368
|
+
state.phase = "DONE";
|
|
369
|
+
state.nextAction = "Tarea completa — contrato verificado";
|
|
370
|
+
persistTaskState(taskId, state, root);
|
|
371
|
+
}
|
|
372
|
+
return gate;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Compresión automática de tarea (caveman-summary.md). Se genera en DONE
|
|
377
|
+
* (memoria comprimida para continuidad) y manualmente via /wam compress.
|
|
378
|
+
*/
|
|
379
|
+
function writeCavemanSummary(taskId, state, root, extra = "") {
|
|
380
|
+
const reqs = state?.requirements || [];
|
|
381
|
+
const pend = reqs.filter((r) => r.status !== "done" && r.status !== "verified").length;
|
|
382
|
+
const base = [
|
|
383
|
+
`task ${taskId} — ${state?.phase || "?"} / ${state?.contract?.status || "?"}`,
|
|
384
|
+
`req: ${pend}/${reqs.length} pend | next: ${state?.nextAction || "—"}`,
|
|
385
|
+
];
|
|
386
|
+
const cav = cavemanify([...base, extra].filter(Boolean).join("\n"));
|
|
387
|
+
try {
|
|
388
|
+
const dir = path.join(root || process.cwd(), ".wam", "tasks", taskId);
|
|
389
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
390
|
+
fs.writeFileSync(path.join(dir, "caveman-summary.md"), cav + "\n");
|
|
391
|
+
} catch {}
|
|
392
|
+
return cav;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
function domainHint(text = "") {
|
|
397
|
+
const t = text.toLowerCase();
|
|
398
|
+
const hints = [];
|
|
399
|
+
if (/\bfrontend\b|\bfe\b|front-?end|\bui\b|button|componente|vista|pantalla|react|web\b/.test(t)) hints.push("frontend");
|
|
400
|
+
if (/backend|back-?end|\bapi\b|server|endpoint|base de datos|\bdb\b|database|nest|express|postgres|mongo/.test(t)) hints.push("backend");
|
|
401
|
+
if (/scrap|crawl|parser|puppeteer|playwright/.test(t)) hints.push("scraper");
|
|
402
|
+
if (/test|e2e|spec|coverage|unitario/.test(t)) hints.push("tests");
|
|
403
|
+
if (/security|seguridad|auth|token|jwt|credencial/.test(t)) hints.push("security");
|
|
404
|
+
return hints.length ? hints.join("+") : "";
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Directiva de delegación para reqs pendientes de un contrato APPROVED:
|
|
409
|
+
* cada req → Task en paralelo con agente libre (el que el entorno tenga).
|
|
410
|
+
*/
|
|
411
|
+
function delegationLines(state) {
|
|
412
|
+
const reqs = (state?.requirements || []).filter((r) => r.status !== "done" && r.status !== "verified");
|
|
413
|
+
if (!reqs.length || state?.contract?.status !== "APPROVED") return [];
|
|
414
|
+
const lines = [
|
|
415
|
+
"[wam delegation] Reqs pendientes → delegar en PARALELO via Task (agente: el apropiado según config del entorno). Mutación directa de archivos desde la sesión principal está BLOQUEADA — solo un subagente ejecuta write/edit.",
|
|
416
|
+
];
|
|
417
|
+
const visible = reqs.slice(0, MAX_VISIBLE_REQS);
|
|
418
|
+
const overflow = reqs.length - MAX_VISIBLE_REQS;
|
|
419
|
+
for (const r of visible) {
|
|
420
|
+
const hint = domainHint(r.title);
|
|
421
|
+
lines.push(` ${r.id} → Task(parallel) "${truncate(r.title, 120)}"${hint ? ` [contexto: ${hint}]` : ""}`);
|
|
422
|
+
}
|
|
423
|
+
if (overflow > 0) {
|
|
424
|
+
lines.push(` ...(+${overflow} requisitos adicionales consolidados)`);
|
|
425
|
+
}
|
|
426
|
+
return lines;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const MAX_VISIBLE_REQS = 8;
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Strategy Continuity: classify whether an action is covered by an active
|
|
433
|
+
* approved strategy. Used by tool.execute.before to avoid asking for
|
|
434
|
+
* per-step approval for actions that are ordinary execution of an
|
|
435
|
+
* already-approved strategy.
|
|
436
|
+
*
|
|
437
|
+
* Returns:
|
|
438
|
+
* { covered: true, reason } → action proceeds without re-approval
|
|
439
|
+
* { covered: false, reason } → action requires explicit user decision
|
|
440
|
+
* { covered: null, reason } → no active strategy, fall through to default logic
|
|
441
|
+
*/
|
|
442
|
+
function classifyActionAgainstStrategy(action, tool, args, approvedStrategy) {
|
|
443
|
+
if (!approvedStrategy || approvedStrategy.status !== "ACTIVE") return { covered: null, reason: "No active approved strategy" }; // Self-contained capability matching (extracted by autonomy-behavior-suite.test.mjs via `new Function(...)`, so this body must not reference external symbols and must avoid inner `\n}` that would prematurely terminate the regex match).
|
|
444
|
+
const actionStr = String(action || "").trim();
|
|
445
|
+
const toolStr = String(tool || "").trim();
|
|
446
|
+
const argv = (args && Array.isArray(args.argv)) ? args.argv : (args && typeof args.command === "string" ? args.command.split(/\s+/) : []);
|
|
447
|
+
const executable = toolStr === "bash" ? String(args && args.command || "").split(/\s+/)[0] : toolStr;
|
|
448
|
+
const candidate = { action: actionStr, tool: toolStr, executable: args };
|
|
449
|
+
const toCapability = (s) => { const str = String(s || "").trim(); const parts = str.split(/\s+/).filter(Boolean); if (parts.length === 0) return null; if (parts.length === 1) return { capability: "action.execute", action: parts[0] }; return { capability: "command.execute", executable: parts[0], argsPattern: parts.slice(1) }; };
|
|
450
|
+
const allowedCaps = (approvedStrategy.allowedActions || []).map(toCapability).filter(Boolean);
|
|
451
|
+
const prohibitedCaps = (approvedStrategy.prohibitedActions || []).map(toCapability).filter(Boolean);
|
|
452
|
+
// Canonical structured classifier (./src/policy/strategy-capabilities.js). In normal
|
|
453
|
+
// module scope these are imported; when this function is extracted via `new Function`
|
|
454
|
+
// (autonomy-behavior-suite.test.mjs) they are absent, so the `typeof` guards fall
|
|
455
|
+
// through to the equivalent inline matcher below (kept self-contained on purpose).
|
|
456
|
+
if (typeof classifyByCapabilities === "function" && typeof buildCandidate === "function") {
|
|
457
|
+
const builtCandidate = buildCandidate({ action: actionStr, tool: toolStr, args: args || {} });
|
|
458
|
+
const verdict = classifyByCapabilities(builtCandidate, { allowed: allowedCaps, prohibited: prohibitedCaps });
|
|
459
|
+
if (verdict && verdict.allowed === true) return { covered: true, reason: "Acción cubierta por strategy activa" };
|
|
460
|
+
if (allowedCaps.length === 0) return { covered: false, reason: "Acción no cubierta por capabilities estructuradas de la strategy" };
|
|
461
|
+
return { covered: false, reason: (verdict && verdict.reason) || "Acción no cubierta por strategy activa" };
|
|
462
|
+
}
|
|
463
|
+
const matchCapability = (cap, cand) => { if (!cap) return false; if (cap.capability === "action.execute") return cap.action === cand.action || cap.action === cand.tool; if (cap.capability === "command.execute") { if (cap.executable && cap.executable !== cand.executable) return false; if (Array.isArray(cap.argsPattern)) { for (let i = 0; i < cap.argsPattern.length; i++) { if (cap.argsPattern[i] !== cand.args[i]) return false; } } return true; } return false; };
|
|
464
|
+
const isProhibited = prohibitedCaps.some((cap) => matchCapability(cap, candidate));
|
|
465
|
+
const isAllowed = allowedCaps.some((cap) => matchCapability(cap, candidate));
|
|
466
|
+
if (isProhibited) return { covered: false, reason: "Acción prohibida explícitamente en strategy" };
|
|
467
|
+
if (allowedCaps.length === 0) return { covered: false, reason: "Acción no cubierta por capabilities estructuradas de la strategy" };
|
|
468
|
+
if (isAllowed) return { covered: true, reason: "Acción cubierta por strategy activa" };
|
|
469
|
+
return { covered: false, reason: "Acción no cubierta por strategy activa" };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// (Legacy substring-matching path removed. Strategy continuity is now evaluated
|
|
473
|
+
// against structured capabilities in ./src/policy/strategy-capabilities.js so that
|
|
474
|
+
// "npm test" cannot accidentally cover "npm publish" or "npm install <pkg>".)
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Evidence-Guided Recovery: classify a failure so the agent must diagnose
|
|
478
|
+
* before proposing a strategic change.
|
|
479
|
+
*
|
|
480
|
+
* Returns a category hint. Provisional, MUST NOT be treated as fact.
|
|
481
|
+
*/
|
|
482
|
+
function classifyFailure(result, expected, observed) {
|
|
483
|
+
const obs = String(observed || "").toLowerCase();
|
|
484
|
+
const res = String(result || "").toLowerCase();
|
|
485
|
+
|
|
486
|
+
if (/timeout|timed out/.test(obs)) return { category: "TIMING", priority: 2 };
|
|
487
|
+
if (/403|401|307|blocked|captcha|perimeterx|big-ip/.test(obs)) return { category: "ANTI_BOT", priority: 8 };
|
|
488
|
+
if (/enotfound|econnrefused|network/.test(obs)) return { category: "NETWORK", priority: 1 };
|
|
489
|
+
if (/0 products|empty|no results|not found/.test(obs)) return { category: "ZERO_RESULT", priority: 3 };
|
|
490
|
+
if (/selector|element not found|null/.test(obs)) return { category: "SELECTOR", priority: 4 };
|
|
491
|
+
if (/chromium|browser|executable/.test(obs)) return { category: "BROWSER_RUNTIME", priority: 5 };
|
|
492
|
+
if (/dependency|module not found|cannot find/.test(obs)) return { category: "DEPENDENCY", priority: 1 };
|
|
493
|
+
if (/permission denied|eacces/.test(obs)) return { category: "ENVIRONMENT", priority: 2 };
|
|
494
|
+
if (/parse|json|syntax/.test(obs)) return { category: "PARSER", priority: 4 };
|
|
495
|
+
|
|
496
|
+
return { category: "UNKNOWN", priority: 10 };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Reference Evidence Priority: load known-good references for the current
|
|
501
|
+
* task domain. Used in context assembly to anchor the agent to proven
|
|
502
|
+
* implementations instead of letting it invent alternatives.
|
|
503
|
+
*/
|
|
504
|
+
function loadReferenceEvidence(taskRoot, intent) {
|
|
505
|
+
const refsFile = path.join(taskRoot || "", ".wam", "references.json");
|
|
506
|
+
if (!fs.existsSync(refsFile)) return [];
|
|
507
|
+
try {
|
|
508
|
+
const refs = JSON.parse(fs.readFileSync(refsFile, "utf-8"));
|
|
509
|
+
const goal = String(intent?.goal || intent?.classification || "").toLowerCase();
|
|
510
|
+
return (refs.references || []).filter((r) => {
|
|
511
|
+
const targets = (r.targets || []).map((t) => t.toLowerCase());
|
|
512
|
+
return targets.some((t) => goal.includes(t)) || goal.length === 0;
|
|
513
|
+
});
|
|
514
|
+
} catch {
|
|
515
|
+
return [];
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function prepareSystemInject(analysis, state, cfg, projectDirectory, waitAMinute, taskId) {
|
|
520
|
+
// WAM injects objective + remaining requirements as advisory state.
|
|
521
|
+
// The agent implements EXACTLY what the user asked — no strategy deviation,
|
|
522
|
+
// no "decide your own approach". Only ambiguity triggers clarification.
|
|
523
|
+
const inject = [];
|
|
524
|
+
const reqs = state.requirements || [];
|
|
525
|
+
const pend = reqs.filter((r) => r.status !== "done");
|
|
526
|
+
const objective = state.contract?.objective || analysis.intent?.goal || null;
|
|
527
|
+
|
|
528
|
+
// Silence for trivial/fast: no inject noise.
|
|
529
|
+
if (analysis.intent?.classification === "trivial" || analysis.strategy === "FAST") {
|
|
530
|
+
return inject;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Once: contract display, then contractDisplayed flips. After that the agent
|
|
534
|
+
// already has the objective in working memory — no need to repeat.
|
|
535
|
+
if (state.phase === "PROPOSED" && !state.contractDisplayed) {
|
|
536
|
+
const visible = reqs.slice(0, MAX_VISIBLE_REQS);
|
|
537
|
+
const overflow = reqs.length - MAX_VISIBLE_REQS;
|
|
538
|
+
inject.push(
|
|
539
|
+
`Objective: ${objective || "pending"}`,
|
|
540
|
+
`Remaining (${pend.length}/${reqs.length}):`,
|
|
541
|
+
...visible.map((r) => ` - ${truncate(r.title, 120)}`),
|
|
542
|
+
...(overflow > 0 ? [` ...(+${overflow} more)`] : [])
|
|
543
|
+
);
|
|
544
|
+
state.contractDisplayed = true;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return inject;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Plugin factory — loaded by OpenCode when registered in opencode.jsonc.
|
|
552
|
+
* Autocontained: no external deps beyond ./engine.js.
|
|
553
|
+
*/
|
|
554
|
+
const WaitAMinutePlugin = async (pluginInput) => {
|
|
555
|
+
let cfg = { ...DEFAULT_CONFIG };
|
|
556
|
+
|
|
557
|
+
// Per-plugin-instance session store
|
|
558
|
+
const sessionStore = new Map();
|
|
559
|
+
|
|
560
|
+
// Project directory (opencode 1.18.25: plugin input, not ctx)
|
|
561
|
+
const projectDirectory = pluginInput?.directory || process.cwd();
|
|
562
|
+
|
|
563
|
+
// Track if plugin is bypassed
|
|
564
|
+
let bypassed = false;
|
|
565
|
+
|
|
566
|
+
// Multi-repo: root real de cada sesión. La factory del plugin V1 es GLOBAL
|
|
567
|
+
// al server (pluginInput.directory = cwd donde se lanzó opencode), pero cada
|
|
568
|
+
// sesión tiene su propio directorio de trabajo (location.directory) — lo
|
|
569
|
+
// consultamos vía client.session.get para no depender del cwd del proceso.
|
|
570
|
+
const sessionRoots = new Map();
|
|
571
|
+
const sessionParents = new Map(); // sessionID → parentID (subagentes Task tienen parent)
|
|
572
|
+
const sessionResolved = new Set(); // sessionIDs cuyo parentID se resolvió (distingue main de subagente)
|
|
573
|
+
const processedMessages = new Map(); // messageID+kind → processed (chat.message idempotency)
|
|
574
|
+
const sessionTasks = new Map(); // sessionID → taskId activo visto en chat.message
|
|
575
|
+
const client = pluginInput?.client;
|
|
576
|
+
|
|
577
|
+
async function resolveSessionBase(sessionID) {
|
|
578
|
+
if (sessionRoots.has(sessionID)) return sessionRoots.get(sessionID);
|
|
579
|
+
let base = projectDirectory;
|
|
580
|
+
try {
|
|
581
|
+
const info = client?.session?.get && sessionID ? await client.session.get({ sessionID }) : null;
|
|
582
|
+
if (info?.location?.directory) base = info.location.directory;
|
|
583
|
+
const parent = info?.parentID || info?.parent_id || info?.parent?.id;
|
|
584
|
+
if (parent) sessionParents.set(sessionID, parent);
|
|
585
|
+
if (info) sessionResolved.add(sessionID);
|
|
586
|
+
} catch {
|
|
587
|
+
// fallback: cwd del server (sesión no resuelta → fail-open, no se gatea)
|
|
588
|
+
}
|
|
589
|
+
sessionRoots.set(sessionID, base);
|
|
590
|
+
return base;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// .wam root para esta sesión+mensaje: repo git objetivo de la sesión real.
|
|
594
|
+
const wamRootFor = async (sessionID, promptText) => {
|
|
595
|
+
const base = await resolveSessionBase(sessionID);
|
|
596
|
+
return resolveWamRoot(promptText, base);
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// Al iniciar/retomar cualquier sesión la memoria .wam debe existir.
|
|
600
|
+
// initMemory es idempotente: crea .wam/context si falta, no fabrica nada.
|
|
601
|
+
const ensureWamMemory = async (sessionID, promptText) => {
|
|
602
|
+
const root = await wamRootFor(sessionID, promptText);
|
|
603
|
+
try {
|
|
604
|
+
initMemory(root);
|
|
605
|
+
} catch {}
|
|
606
|
+
return root;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Runs after tool.execute.before — persists success/failure to the
|
|
611
|
+
* cognition store via noteSuccess/noteFailure, reading experiment IDs
|
|
612
|
+
* from the sessionExecutions map (or input._wam* overrides) instead of
|
|
613
|
+
* input.hypothesisId which is never set by tool.execute.before.
|
|
614
|
+
*/
|
|
615
|
+
async function postToolExecution(input, output) {
|
|
616
|
+
const toolName = input?.tool || "";
|
|
617
|
+
try {
|
|
618
|
+
if (bypassed) return;
|
|
619
|
+
const sid = input?.sessionID;
|
|
620
|
+
const taskRoot = await resolveSessionBase(sid);
|
|
621
|
+
const taskId = sessionTasks.get(sid) || readActiveTaskIdFresh(taskRoot) || "default-task";
|
|
622
|
+
const wamRoot = taskRoot;
|
|
623
|
+
const key = input._wamCallID || input?.callID || `${sid}:${toolName}`;
|
|
624
|
+
const mapping = sessionExecutions.get(key) || {};
|
|
625
|
+
|
|
626
|
+
if (input?.tool && output?.error) {
|
|
627
|
+
try {
|
|
628
|
+
await noteFailure(wamRoot, taskId, {
|
|
629
|
+
hypothesisId: input._wamHypothesisId || mapping.hypothesisId || input.hypothesisId,
|
|
630
|
+
experimentId: input._wamExperimentId || mapping.experimentId || input.experimentId,
|
|
631
|
+
requirementId: input._wamRequirementId || mapping.requirementId || input.requirementId,
|
|
632
|
+
reason: output.error || "tool execution failed",
|
|
633
|
+
actual: output.actual,
|
|
634
|
+
unexpected: output.unexpected,
|
|
635
|
+
provenance: `agent-tool-${toolName}-failure`,
|
|
636
|
+
});
|
|
637
|
+
} catch (e) {
|
|
638
|
+
console.log(`[wait-a-minute] postToolExecution noteFailure error:`, e.message);
|
|
639
|
+
}
|
|
640
|
+
} else if (input?.tool) {
|
|
641
|
+
try {
|
|
642
|
+
await noteSuccess(wamRoot, taskId, {
|
|
643
|
+
hypothesisId: input._wamHypothesisId || mapping.hypothesisId || input.hypothesisId,
|
|
644
|
+
experimentId: input._wamExperimentId || mapping.experimentId || input.experimentId,
|
|
645
|
+
requirementId: input._wamRequirementId || mapping.requirementId || input.requirementId,
|
|
646
|
+
result: output.result,
|
|
647
|
+
actual: output.actual,
|
|
648
|
+
unexpected: output.unexpected,
|
|
649
|
+
provenance: `agent-tool-${toolName}-success`,
|
|
650
|
+
});
|
|
651
|
+
} catch (e) {
|
|
652
|
+
console.log(`[wait-a-minute] postToolExecution noteSuccess error:`, e.message);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
} catch (err) {
|
|
656
|
+
console.error("[wait-a-minute] postToolExecution error:", err);
|
|
657
|
+
} finally {
|
|
658
|
+
const cleanupKey = input._wamCallID || input?.callID || `${input?.sessionID}:${toolName}`;
|
|
659
|
+
if (cleanupKey && cleanupKey !== `${input?.sessionID}:${toolName}`) {
|
|
660
|
+
sessionExecutions.delete(cleanupKey);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
// -------------------------------------------------------------------------
|
|
667
|
+
// opencode 1.18.25 plugin API: factory RETURNS the hooks object
|
|
668
|
+
// -------------------------------------------------------------------------
|
|
669
|
+
return {
|
|
670
|
+
// /wam commands are registered via opencode.jsonc "command" entries on startup,
|
|
671
|
+
// NOT via a plugin config hook (removed: plugin config hook mutated config and
|
|
672
|
+
// is incompatible with opencode 1.18.26+, causing `N.config` TypeError).
|
|
673
|
+
|
|
674
|
+
// Chat message hook — Persistence & Progress Gate.
|
|
675
|
+
// Slim adapter: delegates message processing to src/integration/message-handler.js.
|
|
676
|
+
"chat.message": (input, output) =>
|
|
677
|
+
handleMessage(input, output, {
|
|
678
|
+
bypassed,
|
|
679
|
+
sessionTasks,
|
|
680
|
+
sessionStore,
|
|
681
|
+
cfg,
|
|
682
|
+
resolveSessionBase,
|
|
683
|
+
wamRootFor,
|
|
684
|
+
ensureWamMemory,
|
|
685
|
+
effectiveTaskId,
|
|
686
|
+
genPartId,
|
|
687
|
+
emitTextPart,
|
|
688
|
+
processedMessages,
|
|
689
|
+
readActiveTaskIdFresh,
|
|
690
|
+
writeActiveTaskId,
|
|
691
|
+
waitAMinute,
|
|
692
|
+
migrateLegacyCognition,
|
|
693
|
+
noteSuccess,
|
|
694
|
+
noteFailure,
|
|
695
|
+
getTaskState,
|
|
696
|
+
persistTaskState,
|
|
697
|
+
findDuplicateTask,
|
|
698
|
+
escalateAssumptions,
|
|
699
|
+
buildAssumptions,
|
|
700
|
+
initMemory,
|
|
701
|
+
updateProjectMemo,
|
|
702
|
+
updateLiveContext,
|
|
703
|
+
readLiveContext,
|
|
704
|
+
persistLiveContext,
|
|
705
|
+
assembleContext,
|
|
706
|
+
createSnapshot,
|
|
707
|
+
checkContinuation,
|
|
708
|
+
rebuildScope,
|
|
709
|
+
getDecision,
|
|
710
|
+
recordDecision,
|
|
711
|
+
compactDecisions,
|
|
712
|
+
writeCavemanSummary,
|
|
713
|
+
truncate,
|
|
714
|
+
delegationLines,
|
|
715
|
+
updateTaskMemory,
|
|
716
|
+
addRecentChange,
|
|
717
|
+
closeSession,
|
|
718
|
+
getSessionId,
|
|
719
|
+
classifyAskingMessage,
|
|
720
|
+
}),
|
|
721
|
+
|
|
722
|
+
// Handle /wam CLI (opencode 1.18.25: commands arrive via command.execute.before)
|
|
723
|
+
"command.execute.before": async (input, output) => {
|
|
724
|
+
if (input.command !== "wam") return;
|
|
725
|
+
output.parts = output.parts || [];
|
|
726
|
+
const sid = input.sessionID;
|
|
727
|
+
const root = await resolveSessionBase(sid);
|
|
728
|
+
const taskKey = sessionTasks.get(sid) || readActiveTaskIdFresh(root) || (sid ? `ses-${sid.slice(-10)}` : "default-task");
|
|
729
|
+
const argsArr = Array.isArray(input.arguments)
|
|
730
|
+
? input.arguments
|
|
731
|
+
: (typeof input.arguments === "string" ? input.arguments.split(/\s+/) : []);
|
|
732
|
+
output.parts.push({
|
|
733
|
+
id: genPartId(),
|
|
734
|
+
type: "text",
|
|
735
|
+
text: await wamCli(argsArr, cfg, root, taskKey),
|
|
736
|
+
});
|
|
737
|
+
},
|
|
738
|
+
|
|
739
|
+
// Enforce Clarification Gate (spec change 4): en ASKING se bloquean las
|
|
740
|
+
// herramientas mutantes — la investigación read-only sigue permitida.
|
|
741
|
+
// Además captura el root real de la sesión desde los args de los tools
|
|
742
|
+
// (openmode web multi-proyecto: el cwd de la sesión llega por aquí, no
|
|
743
|
+
// por pluginInput.directory que es el cwd del proceso server).
|
|
744
|
+
"tool.execute.before": async (input, output) => {
|
|
745
|
+
let sid;
|
|
746
|
+
let taskRoot;
|
|
747
|
+
let taskId;
|
|
748
|
+
let st;
|
|
749
|
+
try {
|
|
750
|
+
if (bypassed) return;
|
|
751
|
+
sid = input?.sessionID;
|
|
752
|
+
taskRoot = await resolveSessionBase(sid);
|
|
753
|
+
taskId = sessionTasks.get(sid) || readActiveTaskIdFresh(taskRoot) || (sid ? `ses-${sid.slice(-10)}` : "default-task");
|
|
754
|
+
st = getTaskState(taskId, taskRoot);
|
|
755
|
+
// Fallback: try the active task from disk if first lookup failed
|
|
756
|
+
if (!st && !sid) {
|
|
757
|
+
const activeId = readActiveTaskIdFresh(taskRoot);
|
|
758
|
+
if (activeId && activeId !== taskId) {
|
|
759
|
+
taskId = activeId;
|
|
760
|
+
st = getTaskState(taskId, taskRoot);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const tool = input?.tool || "";
|
|
764
|
+
if (process.env.WAM_DEBUG_TE) {
|
|
765
|
+
console.log(`[WAM-DEBUG-TE] sid=${sid} taskId=${taskId} phase=${st?.phase} tool=${tool}`);
|
|
766
|
+
}
|
|
767
|
+
if (process.env.WAM_DEBUG_TE) {
|
|
768
|
+
console.log(`[WAM-DEBUG-TE-CHECK] phase=${st?.phase} tool=${tool} inBlocked=${BLOCKED_TOOLS.has(tool)} riskCheckWillPass`);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
// Strategy Continuity: si hay estrategia aprobada, verificar si la acción
|
|
773
|
+
// está cubierta antes de proceder con la lógica de ASKING.
|
|
774
|
+
if (st?.approvedStrategy && st.approvedStrategy.status === "ACTIVE") {
|
|
775
|
+
const stratCheck = classifyActionAgainstStrategy(
|
|
776
|
+
input?.description || tool,
|
|
777
|
+
tool,
|
|
778
|
+
output?.args || input?.args || input?.parameters || {},
|
|
779
|
+
st.approvedStrategy
|
|
780
|
+
);
|
|
781
|
+
if (stratCheck.covered === true) {
|
|
782
|
+
// La acción está explícitamente cubierta por la estrategia aprobada.
|
|
783
|
+
// Loggear para observabilidad pero NO bloquear.
|
|
784
|
+
try {
|
|
785
|
+
sessionStore.set(`wam-strategy-hit-${tool}`, { reason: stratCheck.reason, at: Date.now() });
|
|
786
|
+
} catch {}
|
|
787
|
+
} else if (stratCheck.covered === false) {
|
|
788
|
+
// Acción prohibida explícitamente por la estrategia → bloquear.
|
|
789
|
+
const directive = `[wait-a-minute] STRATEGY VIOLATION: ${stratCheck.reason}. La estrategia aprobada "${st.approvedStrategy.strategy}" no autoriza esta acción.`;
|
|
790
|
+
input.output = directive;
|
|
791
|
+
throw new WamPolicyBlock(directive, {
|
|
792
|
+
tool,
|
|
793
|
+
reason: stratCheck.reason,
|
|
794
|
+
level: "BLOCKED",
|
|
795
|
+
source: "approved-strategy-continuity",
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
// covered === null → no decidir aquí; fall through to risk/asking logic
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Action Risk Envelope: evaluar riesgo de la herramienta antes de cualquier
|
|
802
|
+
// otra lógica. Bloquea BLOCKED con WamPolicyBlock (no genérico Error) para
|
|
803
|
+
// evitar que catch genérico lo trague.
|
|
804
|
+
const risk = evaluateAction(tool, input.args || input.parameters || {}, taskRoot);
|
|
805
|
+
if (risk.level === "BLOCKED") {
|
|
806
|
+
const directive = `[wait-a-minute] RISK BLOCK (${tool}): ${risk.reason || "acción fuera del envelope de riesgo"}. Requiere autorización explícita del usuario.`;
|
|
807
|
+
input.output = directive;
|
|
808
|
+
throw new WamPolicyBlock(directive, { tool, reason: risk.reason, level: risk.level });
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Governance Enforcement: block mutating tools when contract is not APPROVED.
|
|
812
|
+
// This prevents user-explicit-execute bypass without proper contract approval.
|
|
813
|
+
// Defer to the Clarification Gate (ENFORCED BLOCK) when in ASKING so the
|
|
814
|
+
// user is directed to answer the blocking unknown, not approve a contract.
|
|
815
|
+
// Sub-sessions (delegated workers, identified by parentID) are NEVER blocked
|
|
816
|
+
// here: they execute the main session's approved plan via Task delegation,
|
|
817
|
+
// not their own contracts. Blocking them breaks flujo sin fricción.
|
|
818
|
+
// Governance Enforcement centralizado (src/policy/governance-enforcement.js).
|
|
819
|
+
// Exento: subagentes (parentID), sesiones no resueltas (fail-open),
|
|
820
|
+
// override explícito (WAM_GOVERNANCE=off), fase terminal, ASKING, y
|
|
821
|
+
// cambios triviales declarados. Solo se gatea la sesión main con un
|
|
822
|
+
// contrato tracked y no aprobado.
|
|
823
|
+
try {
|
|
824
|
+
enforceGovernance(tool, st, {
|
|
825
|
+
isSubagent: sessionParents.has(sid) || !sessionResolved.has(sid),
|
|
826
|
+
declaredFiles: st?.contract?.files || st?.contract?.declaredFiles || [],
|
|
827
|
+
});
|
|
828
|
+
} catch (err) {
|
|
829
|
+
if (err?.wamPolicyBlock) input.output = err.message;
|
|
830
|
+
throw err;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (st?.phase !== "ASKING") return;
|
|
834
|
+
// git read-only (status/diff/log) es investigación: permitido aun en ASKING.
|
|
835
|
+
// git mutante (commit/push) sigue bloqueado hasta responder la pregunta.
|
|
836
|
+
if (tool === "bash" && READONLY_GIT_RE.test(bashCommandOf(input))) return;
|
|
837
|
+
if (BLOCKED_TOOLS.has(tool)) {
|
|
838
|
+
if (process.env.WAM_DEBUG_TE) console.log(`[WAM-DEBUG-TE] BLOCKING ${tool} in ASKING`);
|
|
839
|
+
const u = (st.contract?.unknowns || []).find((x) => x.status === "blocking");
|
|
840
|
+
const question = u ? `${u.id}: ${u.question}` : "pregunta bloqueante pendiente";
|
|
841
|
+
const directive = `[wait-a-minute] ENFORCED BLOCK — tarea en ASKING (${question}). Herramienta ${tool} bloqueada. Responder: /wam answer ${u?.id || "U1"} <respuesta>`;
|
|
842
|
+
input.output = directive;
|
|
843
|
+
throw new Error(directive);
|
|
844
|
+
}
|
|
845
|
+
} catch (err) {
|
|
846
|
+
// WamPolicyBlock MUST be re-thrown — never swallowed by generic catch.
|
|
847
|
+
if (err?.wamPolicyBlock === true) throw err;
|
|
848
|
+
if (typeof err?.message === "string" && err.message.includes("ENFORCED BLOCK")) throw err;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
try {
|
|
852
|
+
const resolvedCallID = input?.callID || `${sid || getSessionId()}__exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
853
|
+
const bridgeResult = await bridgeExecution({
|
|
854
|
+
taskId,
|
|
855
|
+
taskRoot,
|
|
856
|
+
state: st,
|
|
857
|
+
tool: input?.tool || "",
|
|
858
|
+
args: input?.args || input?.parameters || {},
|
|
859
|
+
callID: resolvedCallID,
|
|
860
|
+
sessionID: sid,
|
|
861
|
+
});
|
|
862
|
+
if (bridgeResult?.ok) {
|
|
863
|
+
input._wamCallID = bridgeResult.callID;
|
|
864
|
+
input._wamHypothesisId = bridgeResult.hypothesisId;
|
|
865
|
+
input._wamExperimentId = bridgeResult.experimentId;
|
|
866
|
+
input._wamRequirementId = bridgeResult.requirementId;
|
|
867
|
+
}
|
|
868
|
+
} catch (bridgeError) {
|
|
869
|
+
console.log("[wait-a-minute] Bridge execution non-blocking error:", bridgeError.message);
|
|
870
|
+
}
|
|
871
|
+
},
|
|
872
|
+
|
|
873
|
+
"tool.execute.after": async (input, output) => {
|
|
874
|
+
try {
|
|
875
|
+
await postToolExecution(input, output);
|
|
876
|
+
} catch (e) {
|
|
877
|
+
console.log(`[wait-a-minute] tool.execute.after non-blocking error:`, e.message);
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Clasifica un mensaje recibido durante ASKING (spec clarification-gate):
|
|
885
|
+
* "answer" (respuesta natural), "implementation" (intento de implementar →
|
|
886
|
+
* interceptar, no consumir) o "new-intent" (cambio de tarea → nuevo pre-flight).
|
|
887
|
+
*/
|
|
888
|
+
function classifyAskingMessage(text = "") {
|
|
889
|
+
const lower = (text || "").toLowerCase().trim();
|
|
890
|
+
const doneClaims =
|
|
891
|
+
/(^|\s)(done|finish|finished|complete|completed|terminate|terminated|listo|termin[eé]|complet[ao]|finalizad[oa])\b|(task|tarea)\s+(complete|complet(a|ada|o)|terminad(a|o))|declare.*done/i;
|
|
892
|
+
if (doneClaims.test(lower)) return "blocked-message";
|
|
893
|
+
if (/\b(olvida|olvídate|no quiero|mejor no|en realidad|nada que ver|cambia.*idea|descartar)\b/.test(lower)) return "new-intent";
|
|
894
|
+
if (/\b(implementa|implementar|agrega|agregar|haz|hacer|refactoriza|refactorizar|migra|migrar|crea|crear|elimina|eliminar|arregla|arreglar|fix|configura|configurar|escribe|instala|instalar|construye|build)\b/.test(lower)) return "blocked-message";
|
|
895
|
+
return "answer";
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* /wam CLI — opencode 1.18.25 entrega comandos vía command.execute.before,
|
|
900
|
+
* no vía ctx.command. Lógica extraída del handler antiguo.
|
|
901
|
+
*/
|
|
902
|
+
async function wamCli(args, cfg = {}, root = process.cwd(), taskId = readActiveTaskId(root) || "default-task") {
|
|
903
|
+
const [sub, action, ...rest] = args || [];
|
|
904
|
+
// taskId de la sesión del comando (namespaced ses-<id> si genérico)
|
|
905
|
+
|
|
906
|
+
if (sub === "skills") {
|
|
907
|
+
const skills = waitAMinute.getRegistry();
|
|
908
|
+
const list = Object.values(skills);
|
|
909
|
+
if (action === "list") {
|
|
910
|
+
return list.map(s => `${s.id} [${s.status}]`).join("\n");
|
|
911
|
+
}
|
|
912
|
+
if (action === "search") {
|
|
913
|
+
const q = rest.join(" ").toLowerCase();
|
|
914
|
+
return list
|
|
915
|
+
.filter(s => (s.name || "").toLowerCase().includes(q) || (s.description || "").toLowerCase().includes(q))
|
|
916
|
+
.slice(0, 20)
|
|
917
|
+
.map(s => s.id)
|
|
918
|
+
.join("\n");
|
|
919
|
+
}
|
|
920
|
+
if (action === "inspect") {
|
|
921
|
+
const id = rest.join(" ");
|
|
922
|
+
const s = skills[id];
|
|
923
|
+
if (!s) return `Skill ${id} no encontrada`;
|
|
924
|
+
return [
|
|
925
|
+
`${s.id} [${s.status}]`,
|
|
926
|
+
`name: ${s.name}`,
|
|
927
|
+
`description: ${s.description}`,
|
|
928
|
+
`risk: ${s.risk}`,
|
|
929
|
+
`source: ${s.source?.repository || s.source?.kind || "local"} (${s.source?.path || "—"})`,
|
|
930
|
+
`--- content (max 1500 chars) ---`,
|
|
931
|
+
`${(s.content || "(sin contenido)").slice(0, 1500)}`,
|
|
932
|
+
].join("\n");
|
|
933
|
+
}
|
|
934
|
+
if (action === "explain") {
|
|
935
|
+
const prompt = rest.join(" ");
|
|
936
|
+
if (!prompt) return "Uso: /wam skills explain <prompt>";
|
|
937
|
+
const sel = routeSkillsV2(prompt, {}, waitAMinute.getRegistry(), "STANDARD");
|
|
938
|
+
return sel.selected.length
|
|
939
|
+
? sel.selected.map(s => `${s.name}: ${s.reason}`).join("\n")
|
|
940
|
+
: "Ninguna skill seleccionada para ese prompt";
|
|
941
|
+
}
|
|
942
|
+
return "Uso: /wam skills <list|search|inspect|explain> [args]";
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (sub === "contract") {
|
|
946
|
+
if (action === "approve") return JSON.stringify(waitAMinute.approveContract(taskId, root));
|
|
947
|
+
if (action === "reject") return JSON.stringify(waitAMinute.rejectContract(taskId, root));
|
|
948
|
+
if (action === "edit") {
|
|
949
|
+
try {
|
|
950
|
+
return JSON.stringify(waitAMinute.editContract(taskId, JSON.parse(rest.join(" ")), root));
|
|
951
|
+
} catch {
|
|
952
|
+
return "Error: JSON inválido para /wam contract edit";
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return "Uso: /wam contract <approve|reject|edit <json>>";
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (sub === "resume") {
|
|
959
|
+
return JSON.stringify(waitAMinute.resumeTask(action || taskId, root));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (sub === "strategy") {
|
|
963
|
+
if (action === "set") {
|
|
964
|
+
const scope = rest.join(" ").trim();
|
|
965
|
+
if (!scope) return "Uso: /wam strategy set <scope...>";
|
|
966
|
+
const st = getTaskState(taskId, root);
|
|
967
|
+
if (!st) return "Sin tarea activa";
|
|
968
|
+
if (st.contract?.status !== "APPROVED") {
|
|
969
|
+
return `Contrato no aprobado (status=${st.contract?.status || "?"})`;
|
|
970
|
+
}
|
|
971
|
+
if (!st.approvedStrategy || st.approvedStrategy.status !== "ACTIVE") {
|
|
972
|
+
return "Sin estrategia aprobada activa";
|
|
973
|
+
}
|
|
974
|
+
st.approvedStrategy = {
|
|
975
|
+
...st.approvedStrategy,
|
|
976
|
+
strategy: scope.slice(0, 100),
|
|
977
|
+
scope: scope.slice(0, 100),
|
|
978
|
+
};
|
|
979
|
+
persistTaskState(taskId, st, root);
|
|
980
|
+
return `Estrategia actualizada: "${st.approvedStrategy.strategy}"`;
|
|
981
|
+
}
|
|
982
|
+
return "Uso: /wam strategy set <scope...>";
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (sub === "progress") {
|
|
986
|
+
const [reqId, op, ...evidence] = [action, ...rest];
|
|
987
|
+
if (!reqId) {
|
|
988
|
+
const st = getTaskState(taskId, root);
|
|
989
|
+
if (!st) return "Sin estado de tarea (persiste tras el primer mensaje)";
|
|
990
|
+
return st.requirements
|
|
991
|
+
.map(r => `${r.id} [${r.status}] ${r.title}${r.evidence?.length ? " | evidence: " + r.evidence.join("; ") : ""}`)
|
|
992
|
+
.join("\n");
|
|
993
|
+
}
|
|
994
|
+
if (op === "done") return JSON.stringify(await waitAMinute.markRequirement(taskId, reqId, "done", evidence.join(" "), root));
|
|
995
|
+
if (op === "verified") return JSON.stringify(await waitAMinute.markRequirement(taskId, reqId, "verified", evidence.join(" "), root));
|
|
996
|
+
if (op === "pending") return JSON.stringify(await waitAMinute.markRequirement(taskId, reqId, "pending", "", root));
|
|
997
|
+
return "Uso: /wam progress | /wam progress <id> done <evidencia> | /wam progress <id> verified <evidencia> | /wam progress <id> pending";
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (sub === "answer") {
|
|
1001
|
+
const qid = action;
|
|
1002
|
+
const answer = rest.join(" ").trim();
|
|
1003
|
+
if (!qid || !answer) return "Uso: /wam answer <questionId> <respuesta>";
|
|
1004
|
+
return JSON.stringify(waitAMinute.answerQuestion(taskId, qid, answer, root));
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (sub === "assumptions") {
|
|
1008
|
+
const st = getTaskState(taskId, root);
|
|
1009
|
+
if (!st) return "Sin estado de tarea";
|
|
1010
|
+
const list = st.contract?.assumptions || [];
|
|
1011
|
+
if (!list.length) return "Sin asunciones registradas";
|
|
1012
|
+
return list
|
|
1013
|
+
.map((a) => `${a.id} [${a.classification}/${a.status}] ${a.statement}${a.resolvedBy ? ` (resuelta: ${a.resolvedBy})` : ""}`)
|
|
1014
|
+
.join("\n");
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (sub === "resolve") {
|
|
1018
|
+
const aid = action;
|
|
1019
|
+
const evidence = rest.join(" ").trim();
|
|
1020
|
+
if (!aid || !evidence) return "Uso: /wam resolve <assumptionId> <evidencia>";
|
|
1021
|
+
return JSON.stringify(waitAMinute.resolveAssumption(taskId, aid, evidence, root));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (sub === "status") {
|
|
1025
|
+
const st = getTaskState(taskId, root);
|
|
1026
|
+
if (!st) return "Sin estado de tarea";
|
|
1027
|
+
const experiments = st.experiments || [];
|
|
1028
|
+
const failures = experiments.filter(e => e.status === "failed");
|
|
1029
|
+
const pendingUnknowns = (st.contract?.unknowns || []).filter(u => u.status === "blocking");
|
|
1030
|
+
const pendingAuth = pendingUnknowns.length > 0 ? pendingUnknowns[0] : null;
|
|
1031
|
+
const verifiedReqs = (st.requirements || []).filter(r => r.status === "verified");
|
|
1032
|
+
const totalReqs = (st.requirements || []).length;
|
|
1033
|
+
const verification = { status: `${verifiedReqs.length}/${totalReqs} verified` };
|
|
1034
|
+
const completionEvidence = (st.requirements || [])
|
|
1035
|
+
.filter(r => r.evidence && r.evidence.length > 0)
|
|
1036
|
+
.map(r => ({ req: r.id, evidence: r.evidence.join("; ") }));
|
|
1037
|
+
return getStatusReport(st, { experiments, failures, pendingAuth, verification, completionEvidence });
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (sub === "backlog") {
|
|
1041
|
+
const st = getTaskState(taskId, root);
|
|
1042
|
+
if (!st) return "Sin estado de tarea";
|
|
1043
|
+
const formatted = formatBacklog(st);
|
|
1044
|
+
if (!formatted) return "Sin requerimientos en backlog (todos están en el contract activo)";
|
|
1045
|
+
return formatted;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (sub === "compress") {
|
|
1049
|
+
const id = rest.join(" ") || action || taskId;
|
|
1050
|
+
const st = getTaskState(id, root);
|
|
1051
|
+
if (!st) return "Sin estado de tarea";
|
|
1052
|
+
const budget = cfg.budgetTokens || DEFAULT_CONFIG.budgetTokens;
|
|
1053
|
+
const cav = writeCavemanSummary(id, st, root);
|
|
1054
|
+
const tokens = estimateTokens(cav);
|
|
1055
|
+
const headroom = Math.max(0, budget - tokens);
|
|
1056
|
+
return `${cav}\n[tokens ${tokens} | headroom ${headroom}/${budget}]`;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (sub === "task") {
|
|
1060
|
+
if (action === "list") {
|
|
1061
|
+
const ids = listTaskIds(root);
|
|
1062
|
+
if (!ids.length) return "Sin tareas persistidas (.wam/tasks)";
|
|
1063
|
+
const active = readActiveTaskId(root);
|
|
1064
|
+
return ids
|
|
1065
|
+
.map((id) => {
|
|
1066
|
+
const st = getTaskState(id, root);
|
|
1067
|
+
return `${id}${id === active ? " *activa" : ""} [${st?.phase || "?"}] ${st?.contract?.status || ""}`;
|
|
1068
|
+
})
|
|
1069
|
+
.join("\n");
|
|
1070
|
+
}
|
|
1071
|
+
if (action === "switch") {
|
|
1072
|
+
const id = rest.join(" ");
|
|
1073
|
+
if (!id) return "Uso: /wam task switch <taskId>";
|
|
1074
|
+
writeActiveTaskId(id);
|
|
1075
|
+
const st = getTaskState(id);
|
|
1076
|
+
return st
|
|
1077
|
+
? `Tarea activa: ${id} [${st.phase}] — ${st.nextAction || ""}`
|
|
1078
|
+
: `Tarea activa: ${id} (sin estado persistido — enviar primer mensaje)`;
|
|
1079
|
+
}
|
|
1080
|
+
return "Uso: /wam task <list|switch <id>>";
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (sub === "ctx") {
|
|
1084
|
+
if (action === "list") {
|
|
1085
|
+
const caps = listCapsules(root);
|
|
1086
|
+
if (!caps.length) return "Sin cápsulas (usa /wam ctx add o extracción en DONE)";
|
|
1087
|
+
return caps.map((c) => `${c.context_id} [${c.level} ${c.lifecycle}] ${c.provenance} ${(c.purpose || "").slice(0, 60)}`).join("\n");
|
|
1088
|
+
}
|
|
1089
|
+
if (action === "get") {
|
|
1090
|
+
const q = rest.join(" ");
|
|
1091
|
+
if (!q) return "Uso: /wam ctx get <query>";
|
|
1092
|
+
const r = retrieveContext(q, { root });
|
|
1093
|
+
if (!r.ok) return r.message;
|
|
1094
|
+
return r.capsules.map((h) => `[${h.capsule.level}] ${h.capsule.context_id} (rel ${h.relevance.toFixed(2)}) ${(h.capsule.purpose || "").slice(0, 80)}`).join("\n");
|
|
1095
|
+
}
|
|
1096
|
+
if (action === "show") {
|
|
1097
|
+
const id = rest.join(" ");
|
|
1098
|
+
const c = getCapsule(id, root);
|
|
1099
|
+
if (!c) return `Cápsula ${id} no existe`;
|
|
1100
|
+
return [
|
|
1101
|
+
`${c.context_id} [${c.level} ${c.lifecycle}]`,
|
|
1102
|
+
`provenance: ${c.provenance} | importance: ${c.importance}/10 | confidence: ${c.confidence}`,
|
|
1103
|
+
`purpose: ${c.purpose}`,
|
|
1104
|
+
`scope: ${c.scope}`,
|
|
1105
|
+
`deps: ${(c.dependencies || []).join(", ") || "—"} | supersedes: ${c.supersedes || "—"}`,
|
|
1106
|
+
`--- content (max 600 chars) ---`,
|
|
1107
|
+
`${(c.content || "(sin contenido)").slice(0, 600)}`,
|
|
1108
|
+
].join("\n");
|
|
1109
|
+
}
|
|
1110
|
+
if (action === "promote") {
|
|
1111
|
+
const [id, target, approved] = rest;
|
|
1112
|
+
if (!id || !target) return "Uso: /wam ctx promote <id> <L2|L1> [approved]";
|
|
1113
|
+
const r = promoteCapsule(id, target, { approvedBy: approved === "approved" ? "user" : "", root });
|
|
1114
|
+
if (!r.ok) return `Promoción rechazada: ${r.reason}`;
|
|
1115
|
+
return `Promovida ${id} → ${target} [${r.capsule.provenance}]`;
|
|
1116
|
+
}
|
|
1117
|
+
if (action === "session") {
|
|
1118
|
+
const sid = getSessionId(root);
|
|
1119
|
+
const caps = listCapsules(process.cwd(), { sessionId: sid });
|
|
1120
|
+
return `session: ${sid}\ncapsules de esta sesión: ${caps.length}\nL1 base: ${listCapsules(process.cwd(), { level: "L1", lifecycle: "active" }).length} cápsula(s)`;
|
|
1121
|
+
}
|
|
1122
|
+
if (action === "migrate") {
|
|
1123
|
+
const dryRun = rest.includes("--dry-run");
|
|
1124
|
+
const r = migrateLegacyCapsules(root, { dryRun });
|
|
1125
|
+
if (!r.migrated.length) return "Sin cápsulas legacy (todas tienen session_id)";
|
|
1126
|
+
const tag = r.dryRun ? "[DRY-RUN] " : "";
|
|
1127
|
+
return `${tag}migradas ${r.migrated.length} cápsula(s) a session_id="legacy":\n${r.migrated.map((m) => ` ${m.context_id || m.file}`).join("\n")}`;
|
|
1128
|
+
}
|
|
1129
|
+
if (sub === "decision") {
|
|
1130
|
+
const decisionPoint = action;
|
|
1131
|
+
if (!decisionPoint) {
|
|
1132
|
+
return "Error: decision point required. Usage: /wam decision <point> rationale=\"...\" alternatives=\"...\" evidence=\"...\"";
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Parse key-value pairs from rest
|
|
1136
|
+
const params = {};
|
|
1137
|
+
for (const item of rest) {
|
|
1138
|
+
const match = item.match(/^([^=]+)=(.*)$/);
|
|
1139
|
+
if (match) {
|
|
1140
|
+
let key = match[1].trim();
|
|
1141
|
+
let value = match[2].trim();
|
|
1142
|
+
|
|
1143
|
+
// Remove surrounding quotes if present
|
|
1144
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
1145
|
+
value = value.slice(1, -1);
|
|
1146
|
+
} else if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
|
|
1147
|
+
value = value.slice(1, -1);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
params[key] = value;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const rationale = params.rationale;
|
|
1155
|
+
const alternativesStr = params.alternatives;
|
|
1156
|
+
const evidenceStr = params.evidence;
|
|
1157
|
+
|
|
1158
|
+
if (!rationale || !alternativesStr || !evidenceStr) {
|
|
1159
|
+
return "Error: rationale, alternatives, and evidence are required. Usage: /wam decision <point> rationale=\"...\" alternatives=\"...\" evidence=\"...\"";
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
let alternatives, evidence;
|
|
1163
|
+
try {
|
|
1164
|
+
alternatives = JSON.parse(alternativesStr);
|
|
1165
|
+
evidence = JSON.parse(evidenceStr);
|
|
1166
|
+
|
|
1167
|
+
// Validate that they are arrays
|
|
1168
|
+
if (!Array.isArray(alternatives) || !Array.isArray(evidence)) {
|
|
1169
|
+
return "Error: alternatives and evidence must be JSON arrays.";
|
|
1170
|
+
}
|
|
1171
|
+
} catch (e) {
|
|
1172
|
+
return `Error: Invalid JSON in alternatives or evidence: ${e.message}`;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Create tracer and log the decision
|
|
1176
|
+
const tracer = new ContextDecisionTracer(`decision-${Date.now()}`, root);
|
|
1177
|
+
tracer.logTechnicalDecision(decisionPoint, rationale, alternatives, evidence);
|
|
1178
|
+
|
|
1179
|
+
return `Decision "${decisionPoint}" logged successfully.`;
|
|
1180
|
+
}
|
|
1181
|
+
return "Uso: /wam ctx <list|get <q>|show <id>|promote <id> <L2|L1> [approved]|session|migrate [--dry-run]>";
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return "Uso: /wam <skills|contract|progress|task|compress|ctx|answer|assumptions|resolve|decision>";
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Wait a Minute — deterministic task-state management and context reconstruction API.
|
|
1189
|
+
*/
|
|
1190
|
+
const waitAMinute = {
|
|
1191
|
+
name: "wait-a-minute",
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Obtiene el registry actual.
|
|
1195
|
+
*/
|
|
1196
|
+
getRegistry: function() {
|
|
1197
|
+
return this.loadBundledRegistry();
|
|
1198
|
+
},
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Analiza el prompt del usuario y retorna el contexto de pre-flight.
|
|
1202
|
+
*
|
|
1203
|
+
* @param {Object} options
|
|
1204
|
+
* @param {string} options.prompt - El prompt del usuario
|
|
1205
|
+
* @param {string} [options.projectPath] - Ruta del proyecto (usa cwd por defecto)
|
|
1206
|
+
* @param {Object} [options.config] - Configuración
|
|
1207
|
+
* @param {Object} [options.tierCaps] - Caps por tier
|
|
1208
|
+
* @returns {Object} - Resultado del análisis de pre-flight
|
|
1209
|
+
*/
|
|
1210
|
+
analyze: async function({
|
|
1211
|
+
prompt,
|
|
1212
|
+
projectPath,
|
|
1213
|
+
config,
|
|
1214
|
+
tierCaps,
|
|
1215
|
+
activePreset,
|
|
1216
|
+
activeMode,
|
|
1217
|
+
} = {}) {
|
|
1218
|
+
if (!prompt || typeof prompt !== "string") {
|
|
1219
|
+
return {
|
|
1220
|
+
intent: { classification: "trivial", ambiguity: "low", confidence: 100 },
|
|
1221
|
+
project: { detected_stack: "unknown", architecture: "unknown", relevant_files: [] },
|
|
1222
|
+
known: [],
|
|
1223
|
+
inferred: [],
|
|
1224
|
+
assumed: [],
|
|
1225
|
+
unknown: ["No hay prompt para analizar"],
|
|
1226
|
+
skills: { candidates: [], selected: [], rejected: [] },
|
|
1227
|
+
risk: "low",
|
|
1228
|
+
complexity: "trivial",
|
|
1229
|
+
ambiguity: "low",
|
|
1230
|
+
strategy: "FAST",
|
|
1231
|
+
ready: true,
|
|
1232
|
+
advice: "Sin prompt - sin análisis necesario",
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const trimmedPrompt = prompt.trim();
|
|
1237
|
+
if (!trimmedPrompt) {
|
|
1238
|
+
return {
|
|
1239
|
+
intent: { classification: "trivial", ambiguity: "low", confidence: 100 },
|
|
1240
|
+
project: { detected_stack: "unknown", architecture: "unknown", relevant_files: [] },
|
|
1241
|
+
known: ["Prompt vacío o nulo"],
|
|
1242
|
+
inferred: [],
|
|
1243
|
+
assumed: [],
|
|
1244
|
+
unknown: ["No hay prompt para analizar"],
|
|
1245
|
+
skills: { candidates: [], selected: [], rejected: [] },
|
|
1246
|
+
risk: "low",
|
|
1247
|
+
complexity: "trivial",
|
|
1248
|
+
ambiguity: "low",
|
|
1249
|
+
strategy: "FAST",
|
|
1250
|
+
ready: true,
|
|
1251
|
+
advice: "Sin prompt - sin análisis necesario",
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const result = await analyze({
|
|
1256
|
+
prompt: trimmedPrompt,
|
|
1257
|
+
projectPath: projectPath || process.cwd(),
|
|
1258
|
+
config,
|
|
1259
|
+
tierCaps: tierCaps || DEFAULT_CONFIG.tierCaps,
|
|
1260
|
+
activePreset: activePreset || DEFAULT_CONFIG.activePreset,
|
|
1261
|
+
activeMode: activeMode || DEFAULT_CONFIG.activeMode,
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
const summary = this.generateSummary(result);
|
|
1265
|
+
|
|
1266
|
+
return {
|
|
1267
|
+
...result,
|
|
1268
|
+
_summary: summary,
|
|
1269
|
+
};
|
|
1270
|
+
},
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Genera un resumen humano-readable del análisis de pre-flight.
|
|
1274
|
+
*/
|
|
1275
|
+
generateSummary: function(result) {
|
|
1276
|
+
const lines = [];
|
|
1277
|
+
|
|
1278
|
+
lines.push(
|
|
1279
|
+
`Intención: ${result.intent.classification} (confianza: ${result.intent.confidence}%)`
|
|
1280
|
+
);
|
|
1281
|
+
lines.push(`Stack: ${result.project.detected_stack}`);
|
|
1282
|
+
if (result.project.architecture !== "unknown") {
|
|
1283
|
+
lines.push(`Arquitectura: ${result.project.architecture}`);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (result.known.length > 0) {
|
|
1287
|
+
lines.push(`\nConocido(s): ${result.known.slice(0, 5).join("; ")}`);
|
|
1288
|
+
}
|
|
1289
|
+
if (result.inferred.length > 0) {
|
|
1290
|
+
lines.push(`Inferido(s): ${result.inferred.slice(0, 5).join("; ")}`);
|
|
1291
|
+
}
|
|
1292
|
+
if (result.assumed.length > 0) {
|
|
1293
|
+
lines.push(`Asumido(s): ${result.assumed.slice(0, 3).join("; ")}`);
|
|
1294
|
+
}
|
|
1295
|
+
if (result.unknown.length > 0) {
|
|
1296
|
+
lines.push(`Desconocido(s): ${result.unknown.slice(0, 3).join("; ")}`);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
lines.push(
|
|
1300
|
+
`\nSkills candidatas: ${result.skills.candidates
|
|
1301
|
+
.slice(0, 5)
|
|
1302
|
+
.map((c) => c.name)
|
|
1303
|
+
.join(", ")}`
|
|
1304
|
+
);
|
|
1305
|
+
lines.push(`Skills seleccionadas: ${result.skills.selected.join(", ")}`);
|
|
1306
|
+
if (result.skills.rejected.length > 0) {
|
|
1307
|
+
lines.push(`Skills rechazadas: ${result.skills.rejected.join(", ")}`);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
lines.push(`\nRiesgo: ${result.risk}`);
|
|
1311
|
+
lines.push(`Complejidad: ${result.complexity}`);
|
|
1312
|
+
lines.push(`Ambigüedad: ${result.ambiguity}`);
|
|
1313
|
+
lines.push(`\nEstrategia: ${result.strategy}`);
|
|
1314
|
+
lines.push(`\n¿Listo para proceder?: ${result.ready ? "SÍ" : "NO"}`);
|
|
1315
|
+
lines.push(`\nConsejo: ${result.advice || ""}`);
|
|
1316
|
+
|
|
1317
|
+
return lines.join("\n");
|
|
1318
|
+
},
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Carga el catálogo embebido (skills/registry.json) del plugin.
|
|
1322
|
+
* El corpus es distribuido con WAM — sin red en runtime.
|
|
1323
|
+
*/
|
|
1324
|
+
loadBundledRegistry: function() {
|
|
1325
|
+
try {
|
|
1326
|
+
const registryPath = path.join(import.meta.dirname, "skills", "registry.json");
|
|
1327
|
+
if (!fs.existsSync(registryPath)) return {};
|
|
1328
|
+
const entries = JSON.parse(fs.readFileSync(registryPath, "utf-8"));
|
|
1329
|
+
const reg = {};
|
|
1330
|
+
for (const s of entries) reg[s.id] = s;
|
|
1331
|
+
return reg;
|
|
1332
|
+
} catch {
|
|
1333
|
+
return {};
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Estado durable de tarea: contrato + requisitos por evidencia.
|
|
1339
|
+
* PROPOSED la primera vez; preserva contrato APPROVED y progreso existente.
|
|
1340
|
+
*/
|
|
1341
|
+
buildPersistedState: function(taskId, analysis, root) {
|
|
1342
|
+
const existing = getTaskState(taskId, root);
|
|
1343
|
+
if (existing && existing.requirements && existing.requirements.length) {
|
|
1344
|
+
// Preservar contrato/requisitos existentes (PROPOSED/APPROVED/REJECTED):
|
|
1345
|
+
// el contrato se sintetiza UNA vez (primer mensaje); los mensajes
|
|
1346
|
+
// siguientes no re-sintetizan del texto actual (claims incluidos).
|
|
1347
|
+
const merged = {
|
|
1348
|
+
...existing,
|
|
1349
|
+
lastAction: existing.lastAction,
|
|
1350
|
+
nextAction: nextActionFrom(existing),
|
|
1351
|
+
};
|
|
1352
|
+
persistTaskState(taskId, merged, root);
|
|
1353
|
+
return merged;
|
|
1354
|
+
}
|
|
1355
|
+
const fresh = {
|
|
1356
|
+
...projectState(analysis),
|
|
1357
|
+
phase: "PROPOSED",
|
|
1358
|
+
nextAction: "Revisar contrato — /wam contract approve o edit",
|
|
1359
|
+
lastAction: "",
|
|
1360
|
+
projectPath: root,
|
|
1361
|
+
};
|
|
1362
|
+
// Safety: consolidar si synthesizeContract generó demasiados requisitos
|
|
1363
|
+
const MAX = 15;
|
|
1364
|
+
if (fresh.requirements && fresh.requirements.length > MAX) {
|
|
1365
|
+
const kept = fresh.requirements.slice(0, MAX);
|
|
1366
|
+
const overflow = fresh.requirements.length - MAX;
|
|
1367
|
+
kept.push({ id: `req-overflow`, title: `(+${overflow} requisitos consolidados de la especificación)`, status: "pending", evidence: [] });
|
|
1368
|
+
fresh.requirements = kept;
|
|
1369
|
+
if (fresh.contract) fresh.contract.requirements = kept.map((r) => r.title);
|
|
1370
|
+
}
|
|
1371
|
+
persistTaskState(taskId, fresh, root);
|
|
1372
|
+
return fresh;
|
|
1373
|
+
},
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Completion Gate: detecta claims de fin de tarea y bloquea si hay requisitos pendientes.
|
|
1377
|
+
*/
|
|
1378
|
+
evaluateCompletionGate: function(state, prompt) {
|
|
1379
|
+
const lower = (prompt || "").toLowerCase().trim();
|
|
1380
|
+
if (lower.includes("aprobar contrato") || lower.includes("continuar")) {
|
|
1381
|
+
return { blocked: false, allDone: false, autoApprove: true };
|
|
1382
|
+
}
|
|
1383
|
+
const doneClaims =
|
|
1384
|
+
/(^|\s)(done|finish|finished|complete|completed|terminate|terminated|listo|termin[eé]|complet[ao]|finalizad[oa])\b|(task|tarea)\s+(complete|complet(a|ada|o)|terminad(a|o))|declare.*done/i;
|
|
1385
|
+
if (!doneClaims.test(lower)) return { blocked: false };
|
|
1386
|
+
const blockingUnknowns = (state?.contract?.unknowns || []).filter((u) => u.status === "blocking");
|
|
1387
|
+
if (blockingUnknowns.length > 0) {
|
|
1388
|
+
return {
|
|
1389
|
+
blocked: true,
|
|
1390
|
+
pending: blockingUnknowns.map((u) => `${u.id} — ${u.question} (DECISION_CRITICAL sin responder)`),
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
const blockingAssumptions = (state?.contract?.assumptions || []).filter(
|
|
1394
|
+
(a) => a.classification === "DECISION_CRITICAL" && a.status !== "resolved"
|
|
1395
|
+
);
|
|
1396
|
+
if (blockingAssumptions.length > 0) {
|
|
1397
|
+
return {
|
|
1398
|
+
blocked: true,
|
|
1399
|
+
pending: blockingAssumptions.map((a) => `${a.id} — ${a.statement} (DECISION_CRITICAL sin resolver)`),
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
const pending = (state?.requirements || []).filter(
|
|
1403
|
+
(r) =>
|
|
1404
|
+
(r.status !== "done" && r.status !== "verified") ||
|
|
1405
|
+
(r.status === "done" && !(r.evidence || []).length)
|
|
1406
|
+
);
|
|
1407
|
+
if (pending.length > 0) {
|
|
1408
|
+
return {
|
|
1409
|
+
blocked: true,
|
|
1410
|
+
pending: pending.map((r) => {
|
|
1411
|
+
const missingEvidence = r.status === "done" && !(r.evidence || []).length;
|
|
1412
|
+
return `${r.id} — ${r.title}${missingEvidence ? " (sin evidencia)" : ""}`;
|
|
1413
|
+
}),
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
const failingCriteria = (state?.completionContract?.criteria || []).filter(
|
|
1418
|
+
(c) => c.status === "FAIL" || c.status === "UNKNOWN"
|
|
1419
|
+
);
|
|
1420
|
+
if (failingCriteria.length > 0) {
|
|
1421
|
+
return {
|
|
1422
|
+
blocked: true,
|
|
1423
|
+
pending: failingCriteria.map((c) => `${c.id} — ${c.criterion} (${c.status})`),
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
if (state?.contract?.status !== "APPROVED") {
|
|
1428
|
+
return {
|
|
1429
|
+
blocked: true,
|
|
1430
|
+
pending: [`contrato ${state.contract?.status || "PROPOSED"} — aprobar con /wam contract approve antes de DONE`],
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
const unverified = (state?.requirements || []).filter((r) => r.status !== "verified");
|
|
1434
|
+
if (unverified.length > 0) {
|
|
1435
|
+
return {
|
|
1436
|
+
blocked: true,
|
|
1437
|
+
verifying: true,
|
|
1438
|
+
pending: unverified.map((r) => `${r.id} — ${r.title} (verificar: /wam progress ${r.id} verified <evidencia>)`),
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
const checks = state?.contract?.checks || [];
|
|
1442
|
+
if (checks.length > 0) {
|
|
1443
|
+
const byReq = new Map();
|
|
1444
|
+
for (const c of checks) {
|
|
1445
|
+
if (!byReq.has(c.reqId)) byReq.set(c.reqId, []);
|
|
1446
|
+
byReq.get(c.reqId).push(c);
|
|
1447
|
+
}
|
|
1448
|
+
const checkFailures = [];
|
|
1449
|
+
for (const [reqId, list] of byReq) {
|
|
1450
|
+
const verdict = evaluateRequirementChecks(list, list);
|
|
1451
|
+
if (verdict.status !== "VERIFIED") checkFailures.push(`${reqId}: ${verdict.reason || "checks pendientes"}`);
|
|
1452
|
+
}
|
|
1453
|
+
if (checkFailures.length > 0) {
|
|
1454
|
+
return { blocked: true, verifying: true, pending: checkFailures };
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return { blocked: false, allDone: true };
|
|
1458
|
+
},
|
|
1459
|
+
|
|
1460
|
+
/** Transición de fase basada en el resultado del Completion Gate. */
|
|
1461
|
+
applyPhaseTransition: function(state, gate) {
|
|
1462
|
+
let phase;
|
|
1463
|
+
if (gate?.allDone) {
|
|
1464
|
+
phase = "DONE";
|
|
1465
|
+
} else if (gate?.blocked && gate?.verifying) {
|
|
1466
|
+
phase = "VERIFYING";
|
|
1467
|
+
} else {
|
|
1468
|
+
phase = state?.phase || "IMPLEMENTING";
|
|
1469
|
+
}
|
|
1470
|
+
let nextAction;
|
|
1471
|
+
if (gate?.allDone) {
|
|
1472
|
+
nextAction = "Tarea completa — contrato verificado";
|
|
1473
|
+
} else if (gate?.blocked) {
|
|
1474
|
+
nextAction = "Continuar con requisitos pendientes";
|
|
1475
|
+
} else {
|
|
1476
|
+
nextAction = state?.nextAction;
|
|
1477
|
+
}
|
|
1478
|
+
return { phase, nextAction };
|
|
1479
|
+
},
|
|
1480
|
+
|
|
1481
|
+
/** Aprueba el contrato: PROPOSED → APPROVED, fase → IMPLEMENTING. */
|
|
1482
|
+
resumeTask: function(taskId, root) {
|
|
1483
|
+
const state = getTaskState(taskId, root);
|
|
1484
|
+
if (!state) return { ok: false, reason: "Sin estado de tarea para: " + taskId };
|
|
1485
|
+
if (state.phase === "DONE") return { ok: false, reason: "Tarea DONE — no se reabre automáticamente. Crea una nueva." };
|
|
1486
|
+
if (!String(taskId).startsWith("ses-")) writeActiveTaskId(taskId, root);
|
|
1487
|
+
try {
|
|
1488
|
+
updateLiveContext(taskId, state, root);
|
|
1489
|
+
} catch {}
|
|
1490
|
+
return { ok: true, taskId, phase: state.phase, contract: state.contract?.status || "?" };
|
|
1491
|
+
},
|
|
1492
|
+
|
|
1493
|
+
approveContract: function(taskId, root) {
|
|
1494
|
+
const state = getTaskState(taskId, root);
|
|
1495
|
+
if (!state) return { ok: false, reason: "Sin estado de tarea" };
|
|
1496
|
+
const blocking = (state.contract?.unknowns || []).filter((u) => u.status === "blocking");
|
|
1497
|
+
if (blocking.length > 0) {
|
|
1498
|
+
return { ok: false, reason: `${blocking[0].id} DECISION_CRITICAL sin responder (bloquea aprobación): ${blocking[0].question}` };
|
|
1499
|
+
}
|
|
1500
|
+
const blockingAssumptions = (state.contract?.assumptions || []).filter(
|
|
1501
|
+
(a) => a.classification === "DECISION_CRITICAL" && a.status !== "resolved"
|
|
1502
|
+
);
|
|
1503
|
+
if (blockingAssumptions.length > 0) {
|
|
1504
|
+
return {
|
|
1505
|
+
ok: false,
|
|
1506
|
+
reason: `${blockingAssumptions[0].id} DECISION_CRITICAL sin resolver (bloquea aprobación): ${blockingAssumptions[0].statement} — /wam resolve ${blockingAssumptions[0].id} <evidencia>`,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
state.contract = { ...(state.contract || {}), status: "APPROVED" };
|
|
1510
|
+
if (state.phase !== "DONE") state.phase = "IMPLEMENTING";
|
|
1511
|
+
state.nextAction = nextActionFrom(state);
|
|
1512
|
+
// Strategy Continuity: persist approved strategy so tool.execute.before
|
|
1513
|
+
// knows which actions are covered without requiring per-step approval.
|
|
1514
|
+
// Approved scope = the strategy itself + all safe execution steps required
|
|
1515
|
+
// to execute it (install deps, run tests, configure browser, retry).
|
|
1516
|
+
const inferredStrategy = (
|
|
1517
|
+
state.contract?.objective ||
|
|
1518
|
+
state.intent?.goal ||
|
|
1519
|
+
state.requirements?.[0]?.title ||
|
|
1520
|
+
state.lastAction?.split("\n")[0] ||
|
|
1521
|
+
"task execution"
|
|
1522
|
+
).slice(0, 100);
|
|
1523
|
+
state.approvedStrategy = {
|
|
1524
|
+
strategy: inferredStrategy,
|
|
1525
|
+
approvedAt: Date.now(),
|
|
1526
|
+
scope: inferredStrategy,
|
|
1527
|
+
allowedActions: [
|
|
1528
|
+
"read", "search", "inspect", "test", "lint", "typecheck",
|
|
1529
|
+
"install dependency", "install browser binary", "run validation",
|
|
1530
|
+
"modify source", "create fixture", "diagnose", "retry",
|
|
1531
|
+
"write", "edit", "bash", "sh",
|
|
1532
|
+
"task", "todowrite", "pty_spawn",
|
|
1533
|
+
"openspec", "openspec new change", "openspec instructions",
|
|
1534
|
+
"openspec validate", "openspec archive",
|
|
1535
|
+
"git commit", "npm test", "pnpm test", "pnpm install",
|
|
1536
|
+
"crear change OpenSpec", "ejecutar test suite",
|
|
1537
|
+
"instalar dependencia", "delegar via Task",
|
|
1538
|
+
],
|
|
1539
|
+
prohibitedActions: [
|
|
1540
|
+
"delete production data", "drop database", "production deploy",
|
|
1541
|
+
"credential modification", "scope expansion", "destructive operation",
|
|
1542
|
+
],
|
|
1543
|
+
invalidationConditions: [
|
|
1544
|
+
"environment cannot satisfy required runtime",
|
|
1545
|
+
"required dependency unavailable and unfixable",
|
|
1546
|
+
"explicit user retraction",
|
|
1547
|
+
"verified evidence contradicts strategy at architectural level",
|
|
1548
|
+
],
|
|
1549
|
+
status: "ACTIVE",
|
|
1550
|
+
};
|
|
1551
|
+
persistTaskState(taskId, state, root);
|
|
1552
|
+
try {
|
|
1553
|
+
updateLiveContext(taskId, state, root);
|
|
1554
|
+
} catch {}
|
|
1555
|
+
return { ok: true, status: "APPROVED", phase: state.phase };
|
|
1556
|
+
},
|
|
1557
|
+
|
|
1558
|
+
/** Rechaza el contrato: REJECTED, fase WAITING. */
|
|
1559
|
+
rejectContract: function(taskId, root) {
|
|
1560
|
+
const state = getTaskState(taskId, root);
|
|
1561
|
+
if (!state) return { ok: false, reason: "Sin estado de tarea" };
|
|
1562
|
+
state.contract = { ...(state.contract || {}), status: "REJECTED" };
|
|
1563
|
+
state.phase = "WAITING";
|
|
1564
|
+
state.nextAction = "Revisar contrato con el usuario";
|
|
1565
|
+
persistTaskState(taskId, state, root);
|
|
1566
|
+
return { ok: true, status: "REJECTED", phase: state.phase };
|
|
1567
|
+
},
|
|
1568
|
+
|
|
1569
|
+
/** Edita el contrato (JSON patch), vuelve a PROPOSED. */
|
|
1570
|
+
editContract: function(taskId, patch, root) {
|
|
1571
|
+
const state = getTaskState(taskId, root);
|
|
1572
|
+
if (!state) return { ok: false, reason: "Sin estado de tarea" };
|
|
1573
|
+
const contract = state.contract || { status: "PROPOSED", requirements: [], verification: [], constraints: [] };
|
|
1574
|
+
if (Array.isArray(patch?.requirements)) {
|
|
1575
|
+
contract.requirements = patch.requirements;
|
|
1576
|
+
state.requirements = patch.requirements.map((title, i) => ({
|
|
1577
|
+
id: `req-${i + 1}`,
|
|
1578
|
+
title,
|
|
1579
|
+
status: "pending",
|
|
1580
|
+
evidence: [],
|
|
1581
|
+
}));
|
|
1582
|
+
}
|
|
1583
|
+
if (Array.isArray(patch?.verification)) contract.verification = patch.verification;
|
|
1584
|
+
if (Array.isArray(patch?.constraints)) contract.constraints = patch.constraints;
|
|
1585
|
+
contract.status = "PROPOSED";
|
|
1586
|
+
state.contract = contract;
|
|
1587
|
+
state.phase = "PROPOSED";
|
|
1588
|
+
state.nextAction = "Revisar contrato — /wam contract approve o edit";
|
|
1589
|
+
persistTaskState(taskId, state, root);
|
|
1590
|
+
return { ok: true, status: "PROPOSED", requirements: contract.requirements.length };
|
|
1591
|
+
},
|
|
1592
|
+
|
|
1593
|
+
/** Marca requisito done/pending con evidencia. DONE exige evidencia (no "parece funcionar"). */
|
|
1594
|
+
markRequirement: async function(taskId, reqId, status, evidence, root) {
|
|
1595
|
+
const state = getTaskState(taskId, root);
|
|
1596
|
+
if (!state) return { ok: false, reason: "Sin estado de tarea" };
|
|
1597
|
+
const req = (state.requirements || []).find((r) => r.id === reqId);
|
|
1598
|
+
if (!req) return { ok: false, reason: `Requisito ${reqId} no existe` };
|
|
1599
|
+
if ((status === "done" || status === "verified") && !(evidence && evidence.trim())) {
|
|
1600
|
+
return { ok: false, reason: `Requisito ${reqId}: evidencia requerida para marcar ${status}` };
|
|
1601
|
+
}
|
|
1602
|
+
if (status === "verified" && req.status !== "done") {
|
|
1603
|
+
return { ok: false, reason: `Requisito ${reqId}: marcar done antes de verified` };
|
|
1604
|
+
}
|
|
1605
|
+
const checksForReq = (state.contract?.checks || []).filter((c) => c.reqId === reqId);
|
|
1606
|
+
if (status === "verified" && checksForReq.length > 0) {
|
|
1607
|
+
const verdictResult = await verifyRequirement(checksForReq, { timeout_ms: 60000 });
|
|
1608
|
+
if (verdictResult.status !== "VERIFIED") {
|
|
1609
|
+
const failedChecks = verdictResult.results?.filter(r => r.status !== "PASS").map(r => `${r.check_id}: ${r.status}`).join(", ") || "unknown";
|
|
1610
|
+
return { ok: false, reason: `Requisito ${reqId}: verificación falló (${failedChecks}).` };
|
|
1611
|
+
}
|
|
1612
|
+
req.evidence.push(...verdictResult.evidence.map(e => `${e.check_type}:${e.check_id}`));
|
|
1613
|
+
}
|
|
1614
|
+
if (status === "done" && req.status === "verified" && checksForReq.length > 0) {
|
|
1615
|
+
return { ok: false, reason: `Requisito ${reqId}: ya está verificado, no se puede retroceder a done.` };
|
|
1616
|
+
}
|
|
1617
|
+
req.status = status;
|
|
1618
|
+
if (status === "done" || status === "verified") req.evidence.push(evidence.trim());
|
|
1619
|
+
if (status === "pending") req.evidence = [];
|
|
1620
|
+
state.nextAction = nextActionFrom(state);
|
|
1621
|
+
persistTaskState(taskId, state, root);
|
|
1622
|
+
return { ok: true, phase: state.phase, nextAction: state.nextAction };
|
|
1623
|
+
},
|
|
1624
|
+
|
|
1625
|
+
/** Responde una pregunta bloqueante: unknown → answered, fase ASKING → ANSWERED → PROPOSED. */
|
|
1626
|
+
answerQuestion: function(taskId, qid, answer, root) {
|
|
1627
|
+
const state = getTaskState(taskId, root);
|
|
1628
|
+
if (!state) return { ok: false, reason: "Sin estado de tarea" };
|
|
1629
|
+
const u = (state.contract?.unknowns || []).find((x) => x.id === qid);
|
|
1630
|
+
if (!u) return { ok: false, reason: `Pregunta ${qid} no existe` };
|
|
1631
|
+
if (!(answer && answer.trim())) return { ok: false, reason: `Respuesta requerida para ${qid}` };
|
|
1632
|
+
u.status = "answered";
|
|
1633
|
+
u.answer = answer.trim();
|
|
1634
|
+
if (u.assumptionId) {
|
|
1635
|
+
const a = (state.contract?.assumptions || []).find((x) => x.id === u.assumptionId);
|
|
1636
|
+
if (a) {
|
|
1637
|
+
a.status = "resolved";
|
|
1638
|
+
a.classification = "RESOLVED";
|
|
1639
|
+
a.resolvedBy = "answer";
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
state.phase = "ANSWERED";
|
|
1643
|
+
persistTaskState(taskId, state, root);
|
|
1644
|
+
state.phase = "PROPOSED";
|
|
1645
|
+
state.nextAction = "Revisar contrato — /wam contract approve";
|
|
1646
|
+
persistTaskState(taskId, state, root);
|
|
1647
|
+
return { ok: true, phase: "PROPOSED", unknown: u };
|
|
1648
|
+
},
|
|
1649
|
+
|
|
1650
|
+
/** Reconocimiento de respuesta natural (spec enforce-clarification-gate): en ASKING,
|
|
1651
|
+
* un mensaje normal resuelve TODAS las preguntas bloqueantes con ese texto y
|
|
1652
|
+
* re-evalúa el estado (assumption asociada → resolved). */
|
|
1653
|
+
answerFromMessage: function(taskId, text, root) {
|
|
1654
|
+
const state = getTaskState(taskId, root);
|
|
1655
|
+
if (!state) return { ok: false, reason: "Sin estado de tarea" };
|
|
1656
|
+
const blocking = (state.contract?.unknowns || []).filter((u) => u.status === "blocking");
|
|
1657
|
+
if (!blocking.length) return { ok: false, reason: "Sin preguntas bloqueantes" };
|
|
1658
|
+
const answer = (text || "").trim();
|
|
1659
|
+
if (!answer) return { ok: false, reason: "Respuesta vacía" };
|
|
1660
|
+
let first = null;
|
|
1661
|
+
for (const u of blocking) {
|
|
1662
|
+
u.status = "answered";
|
|
1663
|
+
u.answer = answer;
|
|
1664
|
+
if (u.assumptionId) {
|
|
1665
|
+
const a = (state.contract?.assumptions || []).find((x) => x.id === u.assumptionId);
|
|
1666
|
+
if (a) {
|
|
1667
|
+
a.status = "resolved";
|
|
1668
|
+
a.classification = "RESOLVED";
|
|
1669
|
+
a.resolvedBy = "answer";
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
first = first || u;
|
|
1673
|
+
}
|
|
1674
|
+
state.phase = "PROPOSED";
|
|
1675
|
+
state.nextAction = "Revisar contrato — /wam contract approve";
|
|
1676
|
+
persistTaskState(taskId, state, root);
|
|
1677
|
+
return { ok: true, u: first, phase: state.phase, nextAction: state.nextAction };
|
|
1678
|
+
},
|
|
1679
|
+
/** Resuelve una asunción con evidencia del repo (spec change 3 R4): → RESOLVED sin preguntar al usuario. */
|
|
1680
|
+
resolveAssumption: function(taskId, aid, evidence, root) {
|
|
1681
|
+
const state = getTaskState(taskId, root);
|
|
1682
|
+
if (!state) return { ok: false, reason: "Sin estado de tarea" };
|
|
1683
|
+
const a = (state.contract?.assumptions || []).find((x) => x.id === aid);
|
|
1684
|
+
if (!a) return { ok: false, reason: `Asunción ${aid} no existe` };
|
|
1685
|
+
if (!(evidence && evidence.trim())) return { ok: false, reason: `Evidencia requerida para resolver ${aid}` };
|
|
1686
|
+
a.status = "resolved";
|
|
1687
|
+
a.classification = "RESOLVED";
|
|
1688
|
+
a.resolvedBy = "evidence";
|
|
1689
|
+
a.evidence = evidence.trim();
|
|
1690
|
+
const u = (state.contract?.unknowns || []).find((x) => x.assumptionId === aid && x.status === "blocking");
|
|
1691
|
+
if (u) {
|
|
1692
|
+
u.status = "answered";
|
|
1693
|
+
u.answer = evidence.trim();
|
|
1694
|
+
}
|
|
1695
|
+
const stillBlocking = (state.contract?.unknowns || []).some((x) => x.status === "blocking");
|
|
1696
|
+
if (state.phase === "ASKING" && !stillBlocking) {
|
|
1697
|
+
state.phase = "PROPOSED";
|
|
1698
|
+
state.nextAction = "Revisar contrato — /wam contract approve";
|
|
1699
|
+
}
|
|
1700
|
+
persistTaskState(taskId, state, root);
|
|
1701
|
+
return { ok: true, assumption: a };
|
|
1702
|
+
},
|
|
1703
|
+
|
|
1704
|
+
/** Carga contenido real de una skill del catálogo bajo demanda. */
|
|
1705
|
+
loadSkillOnDemand: function(skillId, registry, baseDir) {
|
|
1706
|
+
return loadSkillOnDemand(skillId, registry, baseDir);
|
|
1707
|
+
},
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* Verifica si el prompt puede ser procesado en modo FAST (trivial).
|
|
1711
|
+
* Basado en el CONTENIDO del prompt, no en el origen (usuario/agente).
|
|
1712
|
+
*/
|
|
1713
|
+
isTrivial: function(prompt) {
|
|
1714
|
+
const lower = prompt.toLowerCase().trim();
|
|
1715
|
+
const fastPatterns = [
|
|
1716
|
+
/^\s*rename\s+/i,
|
|
1717
|
+
/^\s*change\s+\w+/i,
|
|
1718
|
+
/^\s*what(is|are)\s+/i,
|
|
1719
|
+
/^\s*explain\s+/i,
|
|
1720
|
+
/^\s*how\s+to\s+/i,
|
|
1721
|
+
/^\s*list\s+/i,
|
|
1722
|
+
/^\s*show\s+\w+/i,
|
|
1723
|
+
/^\s*get\s+\w+/i,
|
|
1724
|
+
/^\s*error\s+line/i,
|
|
1725
|
+
/^\s*fix\s+this/i,
|
|
1726
|
+
];
|
|
1727
|
+
|
|
1728
|
+
for (const pattern of fastPatterns) {
|
|
1729
|
+
if (pattern.test(lower)) return true;
|
|
1730
|
+
}
|
|
1731
|
+
return false;
|
|
1732
|
+
},
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* Verifica si el prompt requiere modo STRICT (arquitectura, seguridad, etc.)
|
|
1736
|
+
*/
|
|
1737
|
+
requiresStrict: function(prompt) {
|
|
1738
|
+
const lower = prompt.toLowerCase();
|
|
1739
|
+
const strictPatterns = [
|
|
1740
|
+
/migra|migrate/i,
|
|
1741
|
+
/seguridad|security/i,
|
|
1742
|
+
/arquitectura|architecture/i,
|
|
1743
|
+
/alto impacto|high impact/i,
|
|
1744
|
+
/producción|production/i,
|
|
1745
|
+
/destructivo|destructive/i,
|
|
1746
|
+
];
|
|
1747
|
+
|
|
1748
|
+
return strictPatterns.some((p) => p.test(lower));
|
|
1749
|
+
},
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Presentar validación de pre-flight al agente/usuario.
|
|
1753
|
+
*
|
|
1754
|
+
* Checkpoint estratégico: muestra el resumen y solicita confirmación
|
|
1755
|
+
* sobre la estrategia a seguir. Se muestra para todo mensaje, incluido
|
|
1756
|
+
* modo FAST — el análisis nunca se omite.
|
|
1757
|
+
*
|
|
1758
|
+
* @param {Object} opts
|
|
1759
|
+
* @param {Object} opts.analysis - Resultado de analyze()
|
|
1760
|
+
* @returns {Object} - { mode: 'continue'|'validation-pending', ... }
|
|
1761
|
+
*/
|
|
1762
|
+
presentValidation: async function({ analysis, ctx, meta = {} } = {}) {
|
|
1763
|
+
if (!analysis) {
|
|
1764
|
+
return { mode: "continue", advice: "No hay análisis previo" };
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
const summary = this.generateSummary(analysis);
|
|
1768
|
+
const mode = analysis.strategy || "NORMAL";
|
|
1769
|
+
|
|
1770
|
+
const contractStatus = analysis.contractStatus || analysis.completionContract?.status || "PROPOSED";
|
|
1771
|
+
|
|
1772
|
+
const skillsLine = analysis.skills?.selected?.length
|
|
1773
|
+
? analysis.skills.selected.map((s) => s.name).join(",")
|
|
1774
|
+
: "ninguna";
|
|
1775
|
+
const blockingUnknowns = (analysis.completionContract?.unknowns || []).filter((u) => u.status === "blocking");
|
|
1776
|
+
const blockingAssumptions = (analysis.completionContract?.assumptions || []).filter(
|
|
1777
|
+
(a) => a.classification === "DECISION_CRITICAL" && a.status !== "resolved"
|
|
1778
|
+
);
|
|
1779
|
+
const validationLines = [
|
|
1780
|
+
`wait-a-minute: contrato ${contractStatus}${analysis.phase ? ` (fase ${analysis.phase})` : ""}`,
|
|
1781
|
+
`req: ${(analysis.completionContract?.requirements || []).join("; ") || "—"}`,
|
|
1782
|
+
...(blockingUnknowns.length
|
|
1783
|
+
? [`blocking: ${blockingUnknowns.map((u) => `${u.id} ${u.question}`).join(" | ")}`]
|
|
1784
|
+
: []),
|
|
1785
|
+
...(blockingAssumptions.length
|
|
1786
|
+
? [`assumptions blocking: ${blockingAssumptions.map((a) => `${a.id} ${a.statement}`).join(" | ")}`]
|
|
1787
|
+
: []),
|
|
1788
|
+
"",
|
|
1789
|
+
"Ejecutar lo pedido. Si la instrucción es ambigua, preguntar.",
|
|
1790
|
+
"/wam contract approve → aprobar | /wam compress → resumen terse",
|
|
1791
|
+
];
|
|
1792
|
+
|
|
1793
|
+
if (ctx) {
|
|
1794
|
+
const text = validationLines.filter((l) => l !== false).join("\n") + "\n";
|
|
1795
|
+
if (Array.isArray(ctx.parts)) {
|
|
1796
|
+
ctx.parts.unshift({
|
|
1797
|
+
id: genPartId(),
|
|
1798
|
+
type: "text",
|
|
1799
|
+
text,
|
|
1800
|
+
synthetic: true,
|
|
1801
|
+
...(meta.sessionID ? { sessionID: meta.sessionID } : {}),
|
|
1802
|
+
...(meta.messageID ? { messageID: meta.messageID } : {}),
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
// Agregar directiva clara de ejecución
|
|
1806
|
+
ctx.parts.push({
|
|
1807
|
+
id: genPartId(),
|
|
1808
|
+
type: "text",
|
|
1809
|
+
text: `\n[wam: fase ${analysis.phase || "IMPLEMENTING"} → ${analysis.nextAction || "continuar"}]`,
|
|
1810
|
+
synthetic: true,
|
|
1811
|
+
...(meta.sessionID ? { sessionID: meta.sessionID } : {}),
|
|
1812
|
+
...(meta.messageID ? { messageID: meta.messageID } : {}),
|
|
1813
|
+
});
|
|
1814
|
+
} else if (ctx.system) {
|
|
1815
|
+
ctx.system.unshift({ id: genPartId(), type: "text", text, synthetic: true });
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
return {
|
|
1820
|
+
mode: mode === "FAST" ? "continue" : "validation-pending",
|
|
1821
|
+
summary,
|
|
1822
|
+
validationLines,
|
|
1823
|
+
advice:
|
|
1824
|
+
mode === "FAST"
|
|
1825
|
+
? "Tarea trivial - análisis mostrado, proceder directamente"
|
|
1826
|
+
: "Proceder con la implementación",
|
|
1827
|
+
};
|
|
1828
|
+
},
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
Object.keys(waitAMinute).forEach((k) => {
|
|
1832
|
+
if (k === "name") return;
|
|
1833
|
+
WaitAMinutePlugin[k] = waitAMinute[k];
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
export default WaitAMinutePlugin;
|