opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const OPENCODE_DB_PATH = path.join(
|
|
6
|
+
process.env.HOME || "/home/nicolas",
|
|
7
|
+
".local/share/opencode/opencode.db"
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
let dbInstance = null;
|
|
11
|
+
|
|
12
|
+
function getDb() {
|
|
13
|
+
if (dbInstance) return dbInstance;
|
|
14
|
+
if (!fs.existsSync(OPENCODE_DB_PATH)) {
|
|
15
|
+
console.error("[wait-a-minute] OpenCode DB not found:", OPENCODE_DB_PATH);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
dbInstance = new DatabaseSync(OPENCODE_DB_PATH, { readOnly: true });
|
|
20
|
+
return dbInstance;
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error("[wait-a-minute] Failed to open OpenCode DB:", err.message);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getSessionMessages(sessionId, limit = 20) {
|
|
28
|
+
const db = getDb();
|
|
29
|
+
if (!db) return [];
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const stmt = db.prepare(`
|
|
33
|
+
SELECT id, time_created, data
|
|
34
|
+
FROM message
|
|
35
|
+
WHERE session_id = ?
|
|
36
|
+
ORDER BY time_created DESC
|
|
37
|
+
LIMIT ?
|
|
38
|
+
`);
|
|
39
|
+
const rows = stmt.all(sessionId, limit);
|
|
40
|
+
return rows.map((row) => ({
|
|
41
|
+
id: row.id,
|
|
42
|
+
timeCreated: row.timeCreated,
|
|
43
|
+
...JSON.parse(row.data),
|
|
44
|
+
}));
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error("[wait-a-minute] Failed to query session messages:", err.message);
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getSessionParts(sessionId, limit = 50) {
|
|
52
|
+
const db = getDb();
|
|
53
|
+
if (!db) return [];
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const stmt = db.prepare(`
|
|
57
|
+
SELECT p.id, p.time_created, p.data, p.message_id
|
|
58
|
+
FROM part p
|
|
59
|
+
JOIN message m ON p.message_id = m.id
|
|
60
|
+
WHERE m.session_id = ?
|
|
61
|
+
ORDER BY p.time_created DESC
|
|
62
|
+
LIMIT ?
|
|
63
|
+
`);
|
|
64
|
+
const rows = stmt.all(sessionId, limit);
|
|
65
|
+
return rows.map((row) => ({
|
|
66
|
+
id: row.id,
|
|
67
|
+
timeCreated: row.timeCreated,
|
|
68
|
+
messageId: row.messageId,
|
|
69
|
+
...JSON.parse(row.data),
|
|
70
|
+
}));
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error("[wait-a-minute] Failed to query session parts:", err.message);
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getToolCallsFromSession(sessionId, limit = 100) {
|
|
78
|
+
const parts = getSessionParts(sessionId, limit);
|
|
79
|
+
return parts
|
|
80
|
+
.filter((p) => p.type === "tool" && p.tool)
|
|
81
|
+
.map((p) => ({
|
|
82
|
+
tool: p.tool,
|
|
83
|
+
callId: p.callID,
|
|
84
|
+
command: p.state?.input?.command,
|
|
85
|
+
output: p.state?.output,
|
|
86
|
+
exitCode: p.state?.metadata?.exit,
|
|
87
|
+
startTime: p.time?.start,
|
|
88
|
+
endTime: p.time?.end,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getTextFromSession(sessionId, limit = 10) {
|
|
93
|
+
const parts = getSessionParts(sessionId, limit * 5);
|
|
94
|
+
return parts
|
|
95
|
+
.filter((p) => p.type === "text" && p.text)
|
|
96
|
+
.map((p) => ({
|
|
97
|
+
text: p.text,
|
|
98
|
+
messageId: p.messageId,
|
|
99
|
+
timeCreated: p.timeCreated,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function findSessionBySlug(slug) {
|
|
104
|
+
const db = getDb();
|
|
105
|
+
if (!db) return null;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const stmt = db.prepare(`
|
|
109
|
+
SELECT id, title, time_created
|
|
110
|
+
FROM session
|
|
111
|
+
WHERE id LIKE ?
|
|
112
|
+
LIMIT 1
|
|
113
|
+
`);
|
|
114
|
+
const row = stmt.get(`%${slug}%`);
|
|
115
|
+
return row || null;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error("[wait-a-minute] Failed to find session:", err.message);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getRecentActions(sessionId, minutes = 30) {
|
|
123
|
+
const db = getDb();
|
|
124
|
+
if (!db) return [];
|
|
125
|
+
|
|
126
|
+
const cutoff = Date.now() - minutes * 60 * 1000;
|
|
127
|
+
try {
|
|
128
|
+
const stmt = db.prepare(`
|
|
129
|
+
SELECT p.id, p.time_created, p.data
|
|
130
|
+
FROM part p
|
|
131
|
+
JOIN message m ON p.message_id = m.id
|
|
132
|
+
WHERE m.session_id = ?
|
|
133
|
+
AND p.data LIKE '%"type":"tool"%'
|
|
134
|
+
AND p.time_created > ?
|
|
135
|
+
ORDER BY p.time_created DESC
|
|
136
|
+
`);
|
|
137
|
+
const rows = stmt.all(sessionId, cutoff);
|
|
138
|
+
return rows.map((row) => {
|
|
139
|
+
const data = JSON.parse(row.data);
|
|
140
|
+
return {
|
|
141
|
+
id: row.id,
|
|
142
|
+
timeCreated: row.timeCreated,
|
|
143
|
+
tool: data.tool,
|
|
144
|
+
command: data.state?.input?.command,
|
|
145
|
+
output: data.state?.output,
|
|
146
|
+
exitCode: data.state?.metadata?.exit,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error("[wait-a-minute] Failed to get recent actions:", err.message);
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration — funciones puras de decisión/estado extraídas de index.js.
|
|
3
|
+
*
|
|
4
|
+
* Este módulo contiene Toda la lógica de decisión del ciclo de vida:
|
|
5
|
+
* - Gestión de fases (PROPOSED→APPROVED→IMPLEMENTING→VERIFYING→DONE)
|
|
6
|
+
* - Transiciones de estado (approve/reject/edit contract, mark requirement)
|
|
7
|
+
* - Completion gate (evaluate, apply phase transition)
|
|
8
|
+
*
|
|
9
|
+
* Las funciones aquí son PURAS: toman estado como entrada, retornan
|
|
10
|
+
* decisión como salida. Sin side effects (no persisten, no leen, no escriben).
|
|
11
|
+
*
|
|
12
|
+
* index.js (OpenCode Adapter) importa estas funciones y se encarga de
|
|
13
|
+
* los side effects: getTaskState, persistTaskState, updateLiveContext, etc.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { evaluateRequirement as evaluateRequirementChecks } from "../verification/verification.js";
|
|
17
|
+
import { truncate } from "../execution/formatting.js";
|
|
18
|
+
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════
|
|
20
|
+
// Constantes de fase
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
export const PHASE_PROPOSED = "PROPOSED";
|
|
24
|
+
export const PHASE_APPROVED = "APPROVED";
|
|
25
|
+
export const PHASE_IMPLEMENTING = "IMPLEMENTING";
|
|
26
|
+
export const PHASE_VERIFYING = "VERIFYING";
|
|
27
|
+
export const PHASE_DONE = "DONE";
|
|
28
|
+
export const PHASE_ASKING = "ASKING";
|
|
29
|
+
export const PHASE_WAITING = "WAITING";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Transiciones válidas de fase.
|
|
33
|
+
* Cada clave es una fase origen, el array fases destino permitidas.
|
|
34
|
+
*/
|
|
35
|
+
export const PHASE_TRANSITIONS = Object.freeze({
|
|
36
|
+
[PHASE_PROPOSED]: [PHASE_IMPLEMENTING, PHASE_WAITING, PHASE_ASKING],
|
|
37
|
+
[PHASE_IMPLEMENTING]: [PHASE_VERIFYING, PHASE_DONE, PHASE_ASKING],
|
|
38
|
+
[PHASE_VERIFYING]: [PHASE_IMPLEMENTING, PHASE_DONE],
|
|
39
|
+
[PHASE_DONE]: [],
|
|
40
|
+
[PHASE_ASKING]: [PHASE_PROPOSED, PHASE_IMPLEMENTING],
|
|
41
|
+
[PHASE_WAITING]: [PHASE_PROPOSED, PHASE_IMPLEMENTING],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Valida si una transición de fase es permitida.
|
|
46
|
+
* @param {string} from - Fase origen
|
|
47
|
+
* @param {string} to - Fase destino
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
export function canTransitionPhase(from, to) {
|
|
51
|
+
return (PHASE_TRANSITIONS[from] || []).includes(to);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ═══════════════════════════════════════════════════════════════
|
|
55
|
+
// nextActionFrom — cálculo puro del próximo action
|
|
56
|
+
// ═══════════════════════════════════════════════════════════════
|
|
57
|
+
|
|
58
|
+
// Eliminada definición local de truncate, usando import de formatting.js
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Determina la próxima acción sugerida según el estado de requirements.
|
|
62
|
+
* @param {Object} state - Estado de la tarea
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export function nextActionFrom(state) {
|
|
66
|
+
const pending = (state?.requirements || []).find(
|
|
67
|
+
(r) => !["done", "verified"].includes(r.status)
|
|
68
|
+
);
|
|
69
|
+
if (pending)
|
|
70
|
+
return `Implementar ${truncate(pending.title)} (${pending.id} ${pending.status})`;
|
|
71
|
+
const unverified = (state?.requirements || []).find((r) => r.status === "done");
|
|
72
|
+
if (unverified)
|
|
73
|
+
return `Verificar ${truncate(unverified.title)} — /wam progress ${unverified.id} verified <evidencia>`;
|
|
74
|
+
if (state?.requirements?.length) {
|
|
75
|
+
// If all requirements are verified, we are done
|
|
76
|
+
const allVerified = (state.requirements || []).every(r => r.status === "verified");
|
|
77
|
+
if (allVerified) {
|
|
78
|
+
return "Tarea completa — contrato verificado";
|
|
79
|
+
}
|
|
80
|
+
return "Verificar requisitos completos antes de DONE";
|
|
81
|
+
}
|
|
82
|
+
return "Continuar tarea";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ═══════════════════════════════════════════════════════════════
|
|
86
|
+
// Completion Gate — evaluación pura
|
|
87
|
+
// ═══════════════════════════════════════════════════════════════
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Evalúa si una tarea puede completarse.
|
|
91
|
+
* PURE: toma state + promptText, retorna gate result sin side effects.
|
|
92
|
+
* @param {Object} state - Estado de la tarea
|
|
93
|
+
* @param {string} promptText - Texto del prompt del usuario
|
|
94
|
+
* @returns {{ blocked: boolean, allDone?: boolean, verifying?: boolean, pending?: string[], autoApprove?: boolean }}
|
|
95
|
+
*/
|
|
96
|
+
export function evaluateCompletionGate(state, promptText) {
|
|
97
|
+
const lower = (promptText || "").toLowerCase().trim();
|
|
98
|
+
|
|
99
|
+
if (lower.includes("aprobar contrato") || lower.includes("continuar")) {
|
|
100
|
+
return { blocked: false, allDone: false, autoApprove: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const doneClaims =
|
|
104
|
+
/(^|\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;
|
|
105
|
+
if (!doneClaims.test(lower)) return { blocked: false };
|
|
106
|
+
|
|
107
|
+
// 1. Blocking unknowns
|
|
108
|
+
const blockingUnknowns = (state?.contract?.unknowns || []).filter(
|
|
109
|
+
(u) => u.status === "blocking"
|
|
110
|
+
);
|
|
111
|
+
if (blockingUnknowns.length > 0) {
|
|
112
|
+
return {
|
|
113
|
+
blocked: true,
|
|
114
|
+
pending: blockingUnknowns.map(
|
|
115
|
+
(u) => `${u.id} — ${u.question} (DECISION_CRITICAL sin responder)`
|
|
116
|
+
),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. Blocking assumptions
|
|
121
|
+
const blockingAssumptions = (state?.contract?.assumptions || []).filter(
|
|
122
|
+
(a) =>
|
|
123
|
+
a.classification === "DECISION_CRITICAL" && a.status !== "resolved"
|
|
124
|
+
);
|
|
125
|
+
if (blockingAssumptions.length > 0) {
|
|
126
|
+
return {
|
|
127
|
+
blocked: true,
|
|
128
|
+
pending: blockingAssumptions.map(
|
|
129
|
+
(a) => `${a.id} — ${a.statement} (DECISION_CRITICAL sin resolver)`
|
|
130
|
+
),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 3. Pending requirements (not done/verified, or done without evidence)
|
|
135
|
+
const pending = (state?.requirements || []).filter(
|
|
136
|
+
(r) =>
|
|
137
|
+
(r.status !== "done" && r.status !== "verified") ||
|
|
138
|
+
(r.status === "done" && !(r.evidence || []).length)
|
|
139
|
+
);
|
|
140
|
+
if (pending.length > 0) {
|
|
141
|
+
return {
|
|
142
|
+
blocked: true,
|
|
143
|
+
pending: pending.map((r) => {
|
|
144
|
+
const missingEvidence =
|
|
145
|
+
r.status === "done" && !(r.evidence || []).length;
|
|
146
|
+
return `${r.id} — ${r.title}${missingEvidence ? " (sin evidencia)" : ""}`;
|
|
147
|
+
}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 4. Failing criteria
|
|
152
|
+
const failingCriteria = (state?.completionContract?.criteria || []).filter(
|
|
153
|
+
(c) => c.status === "FAIL" || c.status === "UNKNOWN"
|
|
154
|
+
);
|
|
155
|
+
if (failingCriteria.length > 0) {
|
|
156
|
+
return {
|
|
157
|
+
blocked: true,
|
|
158
|
+
pending: failingCriteria.map(
|
|
159
|
+
(c) => `${c.id} — ${c.criterion} (${c.status})`
|
|
160
|
+
),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 5. Contract must be APPROVED
|
|
165
|
+
if (state?.contract?.status !== "APPROVED") {
|
|
166
|
+
return {
|
|
167
|
+
blocked: true,
|
|
168
|
+
pending: [
|
|
169
|
+
`contrato ${state.contract?.status || "PROPOSED"} — aprobar con /wam contract approve antes de DONE`,
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 6. All requirements must be verified
|
|
175
|
+
const unverified = (state?.requirements || []).filter(
|
|
176
|
+
(r) => r.status !== "verified"
|
|
177
|
+
);
|
|
178
|
+
if (unverified.length > 0) {
|
|
179
|
+
return {
|
|
180
|
+
blocked: true,
|
|
181
|
+
verifying: true,
|
|
182
|
+
pending: unverified.map(
|
|
183
|
+
(r) => `${r.id} — ${r.title} (verificar: /wam progress ${r.id} verified <evidencia>)`
|
|
184
|
+
),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 7. Contract checks must pass
|
|
189
|
+
const checks = state?.contract?.checks || [];
|
|
190
|
+
if (checks.length > 0) {
|
|
191
|
+
const byReq = new Map();
|
|
192
|
+
for (const c of checks) {
|
|
193
|
+
if (!byReq.has(c.reqId)) byReq.set(c.reqId, []);
|
|
194
|
+
byReq.get(c.reqId).push(c);
|
|
195
|
+
}
|
|
196
|
+
const checkFailures = [];
|
|
197
|
+
for (const [reqId, list] of byReq) {
|
|
198
|
+
const verdict = evaluateRequirementChecks(list, list);
|
|
199
|
+
if (verdict.status !== "VERIFIED")
|
|
200
|
+
checkFailures.push(`${reqId}: ${verdict.reason || "checks pendientes"}`);
|
|
201
|
+
}
|
|
202
|
+
if (checkFailures.length > 0) {
|
|
203
|
+
return { blocked: true, verifying: true, pending: checkFailures };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { blocked: false, allDone: true };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Determina la transición de fase resultante de un completion gate evaluation.
|
|
212
|
+
* PURE: retorna la nueva fase y nextAction sin persistir.
|
|
213
|
+
* @param {Object} state - Estado actual
|
|
214
|
+
* @param {{ blocked: boolean, allDone?: boolean, verifying?: boolean }} gate - Resultado del gate
|
|
215
|
+
* @returns {{ phase: string, nextAction: string }}
|
|
216
|
+
*/
|
|
217
|
+
export function applyPhaseTransition(state, gate) {
|
|
218
|
+
if (gate.blocked) {
|
|
219
|
+
return {
|
|
220
|
+
phase: gate.verifying ? PHASE_VERIFYING : PHASE_IMPLEMENTING,
|
|
221
|
+
nextAction: nextActionFrom(state),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (gate.allDone) {
|
|
225
|
+
return {
|
|
226
|
+
phase: PHASE_DONE,
|
|
227
|
+
nextAction: "Tarea completa — contrato verificado",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
phase: state?.phase || PHASE_PROPOSED,
|
|
232
|
+
nextAction: nextActionFrom(state),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ═══════════════════════════════════════════════════════════════
|
|
237
|
+
// Contract transitions — decision logic (pure)
|
|
238
|
+
// ═══════════════════════════════════════════════════════════════
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Lógica para aprobar un contrato (PROPOSED → APPROVED, phase → IMPLEMENTING).
|
|
242
|
+
* PURE: retorna los cambios a aplicar sobre el estado.
|
|
243
|
+
* @param {Object} state - Estado actual
|
|
244
|
+
* @returns {{ ok: boolean, reason?: string, status?: string, phase?: string, nextAction?: string, approvedStrategy?: Object }}
|
|
245
|
+
*/
|
|
246
|
+
export function approveContractLogic(state) {
|
|
247
|
+
if (!state) return { ok: false, reason: "Sin estado de tarea" };
|
|
248
|
+
|
|
249
|
+
const blocking = (state.contract?.unknowns || []).filter(
|
|
250
|
+
(u) => u.status === "blocking"
|
|
251
|
+
);
|
|
252
|
+
if (blocking.length > 0) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
reason: `${blocking[0].id} DECISION_CRITICAL sin responder (bloquea aprobación): ${blocking[0].question}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const blockingAssumptions = (state.contract?.assumptions || []).filter(
|
|
260
|
+
(a) =>
|
|
261
|
+
a.classification === "DECISION_CRITICAL" && a.status !== "resolved"
|
|
262
|
+
);
|
|
263
|
+
if (blockingAssumptions.length > 0) {
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
reason: `${blockingAssumptions[0].id} DECISION_CRITICAL sin resolver (bloquea aprobación): ${blockingAssumptions[0].statement} — /wam resolve ${blockingAssumptions[0].id} <evidencia>`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const inferredStrategy =
|
|
271
|
+
(state.contract?.objective ||
|
|
272
|
+
state.intent?.goal ||
|
|
273
|
+
state.requirements?.[0]?.title ||
|
|
274
|
+
state.lastAction?.split("\n")[0] ||
|
|
275
|
+
"task execution"
|
|
276
|
+
).slice(0, 100);
|
|
277
|
+
|
|
278
|
+
const allowedActions = [
|
|
279
|
+
"write",
|
|
280
|
+
"edit",
|
|
281
|
+
"bash",
|
|
282
|
+
"sh",
|
|
283
|
+
"task",
|
|
284
|
+
"todowrite",
|
|
285
|
+
"pty_spawn",
|
|
286
|
+
"pty_write",
|
|
287
|
+
"pty_kill",
|
|
288
|
+
"openspec",
|
|
289
|
+
"openspec new change",
|
|
290
|
+
"openspec instructions",
|
|
291
|
+
"openspec validate",
|
|
292
|
+
"openspec archive",
|
|
293
|
+
"git status",
|
|
294
|
+
"git diff",
|
|
295
|
+
"git log",
|
|
296
|
+
"git commit",
|
|
297
|
+
"npm test",
|
|
298
|
+
"pnpm test",
|
|
299
|
+
"pnpm install",
|
|
300
|
+
"crear change OpenSpec",
|
|
301
|
+
"delegar via Task",
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const prohibitedActions = [
|
|
305
|
+
"delete production data",
|
|
306
|
+
"drop database",
|
|
307
|
+
"production deploy",
|
|
308
|
+
"credential modification",
|
|
309
|
+
"scope expansion",
|
|
310
|
+
"destructive operation",
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
const invalidationConditions = [
|
|
314
|
+
"environment cannot satisfy required runtime",
|
|
315
|
+
"required dependency unavailable and unfixable",
|
|
316
|
+
"explicit user retraction",
|
|
317
|
+
"verified evidence contradicts strategy at architectural level",
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
ok: true,
|
|
322
|
+
status: "APPROVED",
|
|
323
|
+
phase: state.phase === PHASE_DONE ? state.phase : PHASE_IMPLEMENTING,
|
|
324
|
+
nextAction: nextActionFrom(state),
|
|
325
|
+
approvedStrategy: {
|
|
326
|
+
strategy: inferredStrategy,
|
|
327
|
+
approvedAt: Date.now(),
|
|
328
|
+
scope:
|
|
329
|
+
state.requirements?.map((r) => r.title).join(", ") || inferredStrategy,
|
|
330
|
+
allowedActions,
|
|
331
|
+
prohibitedActions,
|
|
332
|
+
invalidationConditions,
|
|
333
|
+
status: "ACTIVE",
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Lógica para rechazar un contrato (→ REJECTED, phase → WAITING).
|
|
340
|
+
* PURE.
|
|
341
|
+
* @param {Object} state
|
|
342
|
+
* @returns {{ ok: boolean, status: string, phase: string, nextAction: string }}
|
|
343
|
+
*/
|
|
344
|
+
export function rejectContractLogic(state) {
|
|
345
|
+
return {
|
|
346
|
+
ok: true,
|
|
347
|
+
status: "REJECTED",
|
|
348
|
+
phase: PHASE_WAITING,
|
|
349
|
+
nextAction: "Revisar contrato con el usuario",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Lógica para editar un contrato (→ vuelve a PROPOSED).
|
|
355
|
+
* PURE.
|
|
356
|
+
* @param {Object} state
|
|
357
|
+
* @returns {{ ok: boolean, status: string, phase: string, nextAction: string }}
|
|
358
|
+
*/
|
|
359
|
+
export function editContractLogic(state) {
|
|
360
|
+
return {
|
|
361
|
+
ok: true,
|
|
362
|
+
status: "PROPOSED",
|
|
363
|
+
phase: PHASE_PROPOSED,
|
|
364
|
+
nextAction: "Revisar contrato — /wam contract approve o edit",
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ═══════════════════════════════════════════════════════════════
|
|
369
|
+
// markRequirement — decision logic (pure)
|
|
370
|
+
// ═══════════════════════════════════════════════════════════════
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Lógica para marcar un requisito como done/pending/verified.
|
|
374
|
+
* PURE: valida y retorna cambios al estado. No persiste.
|
|
375
|
+
* @param {Object} state - Estado actual
|
|
376
|
+
* @param {string} reqId - ID del requisito
|
|
377
|
+
* @param {"done"|"pending"|"verified"} status - Nuevo estado
|
|
378
|
+
* @param {string} evidence - Evidencia
|
|
379
|
+
* @returns {{ ok: boolean, reason?: string, phase?: string, nextAction?: string }}
|
|
380
|
+
*/
|
|
381
|
+
export function markRequirementLogic(state, reqId, status, evidence) {
|
|
382
|
+
if (!state) return { ok: false, reason: "Sin estado de tarea" };
|
|
383
|
+
|
|
384
|
+
const req = (state.requirements || []).find((r) => r.id === reqId);
|
|
385
|
+
if (!req) return { ok: false, reason: `Requisito ${reqId} no existe` };
|
|
386
|
+
|
|
387
|
+
if (
|
|
388
|
+
(status === "done" || status === "verified") &&
|
|
389
|
+
!(evidence && evidence.trim())
|
|
390
|
+
) {
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
reason: `Requisito ${reqId}: evidencia requerida para marcar ${status}`,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (status === "verified" && req.status !== "done") {
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
reason: `Requisito ${reqId}: marcar done antes de verified`,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Aplicar cambios al estado (retornar nuevo estado, no mutar)
|
|
405
|
+
let newPhase = state.phase;
|
|
406
|
+
let newNextAction = nextActionFrom(state);
|
|
407
|
+
|
|
408
|
+
// Si todos los requirements están verified → auto-advance a VERIFYING
|
|
409
|
+
if (status === "verified") {
|
|
410
|
+
const allVerified = (state.requirements || []).every(
|
|
411
|
+
(r) => r.status === "verified"
|
|
412
|
+
);
|
|
413
|
+
if (allVerified && state.phase !== PHASE_DONE) {
|
|
414
|
+
newPhase = PHASE_VERIFYING;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return { ok: true, phase: newPhase, nextAction: newNextAction };
|
|
419
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provenance tracking for synthetic parts injected by WaitAMinute.
|
|
3
|
+
* Prevents re-entrancy and self-consumption bugs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const WAM_META = "wam";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tags a part as synthetic and injected by WAM.
|
|
10
|
+
* @param {Object} part
|
|
11
|
+
* @param {Object} [options]
|
|
12
|
+
* @param {string} [options.messageID]
|
|
13
|
+
* @param {string} [options.phase]
|
|
14
|
+
* @param {Function} [options.idFactory]
|
|
15
|
+
*/
|
|
16
|
+
export function tagWamPart(part, { messageID, phase, idFactory } = {}) {
|
|
17
|
+
const tagged = {
|
|
18
|
+
...part,
|
|
19
|
+
synthetic: true,
|
|
20
|
+
metadata: {
|
|
21
|
+
...(part.metadata || {}),
|
|
22
|
+
[WAM_META]: true,
|
|
23
|
+
wamMessageId: messageID ?? null,
|
|
24
|
+
wamPhase: phase ?? null,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
if (idFactory && !tagged.id) {
|
|
28
|
+
tagged.id = idFactory();
|
|
29
|
+
}
|
|
30
|
+
return tagged;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks if a part was injected by WAM.
|
|
35
|
+
*/
|
|
36
|
+
export function isWamSynthetic(part) {
|
|
37
|
+
return !!part && part.synthetic === true && part?.metadata?.[WAM_META] === true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Checks if a collection of parts contains a marker for a specific messageID.
|
|
42
|
+
*/
|
|
43
|
+
export function hasWamMarkerFor(parts, messageID) {
|
|
44
|
+
return Array.isArray(parts) && parts.some(p => isWamSynthetic(p) && (messageID == null || p.metadata?.wamMessageId === messageID));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Injects WAM parts into a target sink (parts array or system array).
|
|
49
|
+
*/
|
|
50
|
+
export function injectWamParts(target, textsOrParts, { messageID, phase, idFactory, position = "prepend" } = {}) {
|
|
51
|
+
const sink = Array.isArray(target.parts) ? target.parts : (Array.isArray(target.system) ? target.system : null);
|
|
52
|
+
if (!sink) return;
|
|
53
|
+
|
|
54
|
+
const list = Array.isArray(textsOrParts) ? textsOrParts : [textsOrParts];
|
|
55
|
+
const parts = list.map(item => {
|
|
56
|
+
const part = typeof item === "string" ? { type: "text", text: item } : item;
|
|
57
|
+
return tagWamPart(part, { messageID, phase, idFactory });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (position === "append") {
|
|
61
|
+
sink.push(...parts);
|
|
62
|
+
} else {
|
|
63
|
+
sink.unshift(...parts);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter — fixed-window counter.
|
|
3
|
+
*
|
|
4
|
+
* Limits calls to N per windowMs milliseconds. Returns a function that
|
|
5
|
+
* records a call and indicates if it's allowed. Production hardening (M5-54).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} RateLimiter
|
|
10
|
+
* @property {() => boolean} tryAcquire
|
|
11
|
+
* @property {() => number} remaining
|
|
12
|
+
* @property {() => number} used
|
|
13
|
+
* @property {() => void} reset
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {{limit?: number, windowMs?: number}} opts
|
|
18
|
+
* @returns {RateLimiter}
|
|
19
|
+
*/
|
|
20
|
+
export function createRateLimiter(opts = {}) {
|
|
21
|
+
const limit = opts.limit ?? 5;
|
|
22
|
+
const windowMs = opts.windowMs ?? 1000;
|
|
23
|
+
let windowStart = Date.now();
|
|
24
|
+
let count = 0;
|
|
25
|
+
|
|
26
|
+
function tryAcquire() {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
if (now - windowStart >= windowMs) {
|
|
29
|
+
windowStart = now;
|
|
30
|
+
count = 0;
|
|
31
|
+
}
|
|
32
|
+
if (count >= limit) return false;
|
|
33
|
+
count++;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function remaining() {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
if (now - windowStart >= windowMs) return limit;
|
|
40
|
+
return Math.max(0, limit - count);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function used() {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
if (now - windowStart >= windowMs) return 0;
|
|
46
|
+
return count;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function reset() {
|
|
50
|
+
windowStart = Date.now();
|
|
51
|
+
count = 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { tryAcquire, remaining, used, reset };
|
|
55
|
+
}
|