contextos-agents 1.6.1 → 2.0.0-beta.2
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/.agents/AGENTS.md +6 -1
- package/.agents/adapters/aider/export.js +117 -97
- package/.agents/adapters/claude/export.js +68 -26
- package/.agents/adapters/copilot/export.js +90 -51
- package/.agents/adapters/cursor/export.js +83 -68
- package/.agents/adapters/drift-detector.js +196 -0
- package/.agents/adapters/gemini/export.js +76 -45
- package/.agents/adapters/pure-compiler.js +443 -0
- package/.agents/adapters/zed/export.js +109 -62
- package/.agents/compiled/registry.v2.json +504 -0
- package/.agents/compiled/registry.v2.sha256 +1 -0
- package/.agents/compiler/manifest-compiler.js +963 -0
- package/.agents/compiler/vendor/yaml.LICENSE.txt +13 -0
- package/.agents/compiler/vendor/yaml.SBOM.json +6 -0
- package/.agents/compiler/vendor/yaml.js +139 -0
- package/.agents/core/profiles/init.yaml +25 -0
- package/.agents/core/skills/context-manager/references/context-rules.md +59 -0
- package/.agents/core/skills/context-manager/skill.yaml +10 -5
- package/.agents/core/skills/context-os/SKILL.md +3 -6
- package/.agents/core/skills/context-os/skill.yaml +14 -8
- package/.agents/core/skills/engineering-workflow/SKILL.md +1 -1
- package/.agents/core/skills/engineering-workflow/skill.yaml +7 -7
- package/.agents/core/skills/gemini-precision/SKILL.md +4 -0
- package/.agents/core/skills/gemini-precision/skill.yaml +5 -6
- package/.agents/core/skills/gstack-roles/SKILL.md +3 -1
- package/.agents/core/skills/gstack-roles/skill.yaml +9 -6
- package/.agents/core/skills/ponytail-mindset/skill.yaml +7 -7
- package/.agents/core/skills/security/skill.yaml +21 -2
- package/.agents/ctx.js +587 -111
- package/.agents/customization-dx.js +282 -0
- package/.agents/doctor.js +877 -33
- package/.agents/filesystem/index.js +71 -0
- package/.agents/filesystem/journaled-transaction.js +451 -0
- package/.agents/filesystem/lockfile-v2.js +275 -0
- package/.agents/filesystem/platform-hardening.js +222 -0
- package/.agents/filesystem/project-lock.js +218 -0
- package/.agents/filesystem/safe-path.js +256 -0
- package/.agents/generated/claude/skills/context-os/SKILL.md +1 -1
- package/.agents/generated/claude/skills/engineering-workflow/SKILL.md +1 -1
- package/.agents/generated/claude/skills/gemini-precision/SKILL.md +4 -0
- package/.agents/generated/claude/skills/gstack-roles/SKILL.md +3 -1
- package/.agents/generated/gemini/skills/context-os/SKILL.md +2 -2
- package/.agents/generated/gemini/skills/engineering-workflow/SKILL.md +1 -1
- package/.agents/generated/gemini/skills/gemini-precision/SKILL.md +4 -0
- package/.agents/generated/gemini/skills/gstack-roles/SKILL.md +3 -1
- package/.agents/plugins/contextos/hooks.json +25 -0
- package/.agents/plugins/contextos/plugin.json +19 -0
- package/.agents/plugins.js +432 -73
- package/.agents/profiles.js +507 -46
- package/.agents/resolver.js +50 -414
- package/.agents/schemas/attestation.review.v1.json +111 -0
- package/.agents/schemas/attestation.verification.v1.json +85 -0
- package/.agents/schemas/lockfile.v2.schema.json +134 -0
- package/.agents/schemas/profile.v2.schema.json +114 -0
- package/.agents/schemas/runtime.thread.v1.json +192 -0
- package/.agents/schemas/skill.manifest.v2.json +177 -0
- package/.agents/schemas/verification.spec.v1.json +39 -0
- package/.agents/schemas/workspace.graph.schema.json +106 -0
- package/.agents/stats.js +22 -9
- package/.agents/transaction-core/event-store.js +288 -0
- package/.agents/transaction-core/idempotency.js +129 -0
- package/.agents/transaction-core/ipc-lock.js +311 -0
- package/.agents/transaction-core/plugin-supply-chain-bundle.js +436 -0
- package/.agents/validate.js +143 -14
- package/.agents/watch.js +354 -102
- package/.agents/workspace/workspace-graph.js +778 -0
- package/README.md +59 -387
- package/benchmarks/v2/analysis/statistics.js +140 -0
- package/benchmarks/v2/analysis/stats.js +69 -0
- package/benchmarks/v2/arms/arm-definitions.js +79 -0
- package/benchmarks/v2/dataset.schema.json +34 -0
- package/benchmarks/v2/evaluators/index.js +25 -0
- package/benchmarks/v2/evaluators/verified-success.js +116 -0
- package/benchmarks/v2/harness/runner.js +88 -0
- package/benchmarks/v2/pilot-tasks.json +392 -0
- package/bin/commands/recover.js +88 -0
- package/bin/commands/uninstall.js +207 -0
- package/bin/commands/update.js +325 -0
- package/bin/commands.js +342 -0
- package/bin/index.js +326 -149
- package/bin/lib/detector.js +106 -0
- package/bin/lib/lockfile.js +253 -0
- package/bin/lib/safe-writer.js +290 -0
- package/package.json +85 -73
- package/registry.json +15 -7
- package/registry.schema.json +3 -1
- package/registry.v2.schema.json +86 -0
- package/.agents/core/profiles/backend.yaml +0 -47
- package/.agents/core/profiles/enterprise.yaml +0 -46
- package/.agents/core/profiles/frontend.yaml +0 -46
- package/.agents/core/profiles/hackathon.yaml +0 -45
- package/.agents/core/profiles/mvp.yaml +0 -44
- package/.agents/core/profiles/startup.yaml +0 -48
- package/.agents/core/skills/adapters/EXAMPLES.md +0 -19
- package/.agents/core/skills/adapters/SKILL.md +0 -105
- package/.agents/core/skills/adapters/TROUBLESHOOTING.md +0 -7
- package/.agents/core/skills/adapters/VALIDATION.json +0 -12
- package/.agents/core/skills/adapters/skill.yaml +0 -10
- package/.agents/core/skills/architecture-diagrams/SKILL.md +0 -108
- package/.agents/core/skills/architecture-diagrams/VALIDATION.json +0 -12
- package/.agents/core/skills/architecture-diagrams/skill.yaml +0 -8
- package/.agents/core/skills/brutalist-design/SKILL.md +0 -150
- package/.agents/core/skills/brutalist-design/VALIDATION.json +0 -12
- package/.agents/core/skills/brutalist-design/skill.yaml +0 -8
- package/.agents/core/skills/database/EXAMPLES.md +0 -74
- package/.agents/core/skills/database/SKILL.md +0 -101
- package/.agents/core/skills/database/TROUBLESHOOTING.md +0 -18
- package/.agents/core/skills/database/VALIDATION.json +0 -11
- package/.agents/core/skills/database/skill.yaml +0 -25
- package/.agents/core/skills/ddd/EXAMPLES.md +0 -42
- package/.agents/core/skills/ddd/SKILL.md +0 -247
- package/.agents/core/skills/ddd/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/ddd/VALIDATION.json +0 -12
- package/.agents/core/skills/ddd/ddd.md +0 -178
- package/.agents/core/skills/ddd/skill.yaml +0 -10
- package/.agents/core/skills/decisions/EXAMPLES.md +0 -35
- package/.agents/core/skills/decisions/SKILL.md +0 -90
- package/.agents/core/skills/decisions/TROUBLESHOOTING.md +0 -13
- package/.agents/core/skills/decisions/VALIDATION.json +0 -12
- package/.agents/core/skills/decisions/skill.yaml +0 -10
- package/.agents/core/skills/docker/EXAMPLES.md +0 -56
- package/.agents/core/skills/docker/SKILL.md +0 -63
- package/.agents/core/skills/docker/TROUBLESHOOTING.md +0 -18
- package/.agents/core/skills/docker/VALIDATION.json +0 -11
- package/.agents/core/skills/docker/skill.yaml +0 -23
- package/.agents/core/skills/fastapi/EXAMPLES.md +0 -36
- package/.agents/core/skills/fastapi/SKILL.md +0 -148
- package/.agents/core/skills/fastapi/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/fastapi/VALIDATION.json +0 -12
- package/.agents/core/skills/fastapi/fastapi.md +0 -112
- package/.agents/core/skills/fastapi/skill.yaml +0 -10
- package/.agents/core/skills/generators/EXAMPLES.md +0 -19
- package/.agents/core/skills/generators/SKILL.md +0 -112
- package/.agents/core/skills/generators/TROUBLESHOOTING.md +0 -7
- package/.agents/core/skills/generators/VALIDATION.json +0 -12
- package/.agents/core/skills/generators/skill.yaml +0 -10
- package/.agents/core/skills/generators/templates/API.md +0 -77
- package/.agents/core/skills/generators/templates/ARCHITECTURE.md +0 -70
- package/.agents/core/skills/generators/templates/DATABASE.md +0 -42
- package/.agents/core/skills/generators/templates/DECISION.md +0 -46
- package/.agents/core/skills/generators/templates/PRD.md +0 -67
- package/.agents/core/skills/generators/templates/PROJECT_GRAPH.md +0 -56
- package/.agents/core/skills/generators/templates/ROADMAP.md +0 -51
- package/.agents/core/skills/generators/templates/TASKS.md +0 -43
- package/.agents/core/skills/generators/templates/UI.md +0 -73
- package/.agents/core/skills/graphify/EXAMPLES.md +0 -73
- package/.agents/core/skills/graphify/SKILL.md +0 -130
- package/.agents/core/skills/graphify/VALIDATION.json +0 -12
- package/.agents/core/skills/graphify/skill.yaml +0 -13
- package/.agents/core/skills/impeccable-design/EXAMPLES.md +0 -26
- package/.agents/core/skills/impeccable-design/SKILL.md +0 -201
- package/.agents/core/skills/impeccable-design/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/impeccable-design/VALIDATION.json +0 -12
- package/.agents/core/skills/impeccable-design/skill.yaml +0 -14
- package/.agents/core/skills/interview-me/SKILL.md +0 -97
- package/.agents/core/skills/interview-me/VALIDATION.json +0 -12
- package/.agents/core/skills/interview-me/skill.yaml +0 -8
- package/.agents/core/skills/microservices/EXAMPLES.md +0 -38
- package/.agents/core/skills/microservices/SKILL.md +0 -164
- package/.agents/core/skills/microservices/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/microservices/VALIDATION.json +0 -12
- package/.agents/core/skills/microservices/microservices.md +0 -119
- package/.agents/core/skills/microservices/skill.yaml +0 -10
- package/.agents/core/skills/minimalist-design/SKILL.md +0 -113
- package/.agents/core/skills/minimalist-design/VALIDATION.json +0 -12
- package/.agents/core/skills/minimalist-design/skill.yaml +0 -8
- package/.agents/core/skills/nestjs/EXAMPLES.md +0 -40
- package/.agents/core/skills/nestjs/SKILL.md +0 -139
- package/.agents/core/skills/nestjs/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/nestjs/VALIDATION.json +0 -12
- package/.agents/core/skills/nestjs/nestjs.md +0 -103
- package/.agents/core/skills/nestjs/skill.yaml +0 -10
- package/.agents/core/skills/nextjs/EXAMPLES.md +0 -40
- package/.agents/core/skills/nextjs/SKILL.md +0 -163
- package/.agents/core/skills/nextjs/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/nextjs/VALIDATION.json +0 -12
- package/.agents/core/skills/nextjs/nextjs.md +0 -67
- package/.agents/core/skills/nextjs/skill.yaml +0 -10
- package/.agents/core/skills/node/EXAMPLES.md +0 -80
- package/.agents/core/skills/node/SKILL.md +0 -128
- package/.agents/core/skills/node/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/node/VALIDATION.json +0 -12
- package/.agents/core/skills/node/node.md +0 -87
- package/.agents/core/skills/node/skill.yaml +0 -10
- package/.agents/core/skills/performance/EXAMPLES.md +0 -30
- package/.agents/core/skills/performance/SKILL.md +0 -75
- package/.agents/core/skills/performance/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/performance/VALIDATION.json +0 -12
- package/.agents/core/skills/performance/performance.md +0 -52
- package/.agents/core/skills/performance/skill.yaml +0 -10
- package/.agents/core/skills/react/EXAMPLES.md +0 -79
- package/.agents/core/skills/react/SKILL.md +0 -132
- package/.agents/core/skills/react/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/react/VALIDATION.json +0 -12
- package/.agents/core/skills/react/react.md +0 -93
- package/.agents/core/skills/react/skill.yaml +0 -10
- package/.agents/core/skills/react-best-practices/SKILL.md +0 -155
- package/.agents/core/skills/react-best-practices/VALIDATION.json +0 -12
- package/.agents/core/skills/react-best-practices/skill.yaml +0 -10
- package/.agents/core/skills/redesign-audit/SKILL.md +0 -117
- package/.agents/core/skills/redesign-audit/VALIDATION.json +0 -12
- package/.agents/core/skills/redesign-audit/skill.yaml +0 -8
- package/.agents/core/skills/soft-design/SKILL.md +0 -108
- package/.agents/core/skills/soft-design/VALIDATION.json +0 -12
- package/.agents/core/skills/soft-design/skill.yaml +0 -8
- package/.agents/core/skills/state-management/EXAMPLES.md +0 -56
- package/.agents/core/skills/state-management/SKILL.md +0 -48
- package/.agents/core/skills/state-management/TROUBLESHOOTING.md +0 -18
- package/.agents/core/skills/state-management/VALIDATION.json +0 -11
- package/.agents/core/skills/state-management/skill.yaml +0 -22
- package/.agents/core/skills/subagent-orchestrator/SKILL.md +0 -100
- package/.agents/core/skills/subagent-orchestrator/VALIDATION.json +0 -12
- package/.agents/core/skills/subagent-orchestrator/skill.yaml +0 -8
- package/.agents/core/skills/system-design/EXAMPLES.md +0 -75
- package/.agents/core/skills/system-design/SKILL.md +0 -419
- package/.agents/core/skills/system-design/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/system-design/VALIDATION.json +0 -12
- package/.agents/core/skills/system-design/skill.yaml +0 -13
- package/.agents/core/skills/system-design/system-design.md +0 -112
- package/.agents/core/skills/testing/EXAMPLES.md +0 -71
- package/.agents/core/skills/testing/SKILL.md +0 -70
- package/.agents/core/skills/testing/TROUBLESHOOTING.md +0 -18
- package/.agents/core/skills/testing/VALIDATION.json +0 -11
- package/.agents/core/skills/testing/skill.yaml +0 -26
- package/.agents/core/skills/typescript/EXAMPLES.md +0 -64
- package/.agents/core/skills/typescript/SKILL.md +0 -112
- package/.agents/core/skills/typescript/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/typescript/VALIDATION.json +0 -12
- package/.agents/core/skills/typescript/skill.yaml +0 -10
- package/.agents/core/skills/typescript/typescript.md +0 -71
- package/.agents/core/skills/ui-design/EXAMPLES.md +0 -21
- package/.agents/core/skills/ui-design/SKILL.md +0 -124
- package/.agents/core/skills/ui-design/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/ui-design/VALIDATION.json +0 -12
- package/.agents/core/skills/ui-design/skill.yaml +0 -10
- package/.agents/core/skills/ui-design/ui.md +0 -88
- package/.agents/core/skills/ui-ux-pro/EXAMPLES.md +0 -62
- package/.agents/core/skills/ui-ux-pro/SKILL.md +0 -375
- package/.agents/core/skills/ui-ux-pro/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/ui-ux-pro/VALIDATION.json +0 -12
- package/.agents/core/skills/ui-ux-pro/skill.yaml +0 -13
- package/.agents/core/skills/ux-design/EXAMPLES.md +0 -36
- package/.agents/core/skills/ux-design/SKILL.md +0 -116
- package/.agents/core/skills/ux-design/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/ux-design/VALIDATION.json +0 -12
- package/.agents/core/skills/ux-design/skill.yaml +0 -10
- package/.agents/core/skills/ux-design/ux.md +0 -80
- package/.agents/core/skills/vercel-optimize/SKILL.md +0 -83
- package/.agents/core/skills/vercel-optimize/VALIDATION.json +0 -12
- package/.agents/core/skills/vercel-optimize/skill.yaml +0 -10
- package/.agents/core/skills/web-accessibility/EXAMPLES.md +0 -39
- package/.agents/core/skills/web-accessibility/SKILL.md +0 -170
- package/.agents/core/skills/web-accessibility/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/web-accessibility/VALIDATION.json +0 -12
- package/.agents/core/skills/web-accessibility/accessibility.md +0 -63
- package/.agents/core/skills/web-accessibility/skill.yaml +0 -10
- package/.agents/generated/claude/skills/adapters/SKILL.md +0 -126
- package/.agents/generated/claude/skills/architecture-diagrams/SKILL.md +0 -101
- package/.agents/generated/claude/skills/brutalist-design/SKILL.md +0 -145
- package/.agents/generated/claude/skills/database/SKILL.md +0 -191
- package/.agents/generated/claude/skills/ddd/SKILL.md +0 -305
- package/.agents/generated/claude/skills/decisions/SKILL.md +0 -134
- package/.agents/generated/claude/skills/docker/SKILL.md +0 -135
- package/.agents/generated/claude/skills/fastapi/SKILL.md +0 -200
- package/.agents/generated/claude/skills/generators/SKILL.md +0 -133
- package/.agents/generated/claude/skills/graphify/SKILL.md +0 -198
- package/.agents/generated/claude/skills/impeccable-design/SKILL.md +0 -241
- package/.agents/generated/claude/skills/interview-me/SKILL.md +0 -90
- package/.agents/generated/claude/skills/microservices/SKILL.md +0 -218
- package/.agents/generated/claude/skills/minimalist-design/SKILL.md +0 -108
- package/.agents/generated/claude/skills/nestjs/SKILL.md +0 -195
- package/.agents/generated/claude/skills/nextjs/SKILL.md +0 -219
- package/.agents/generated/claude/skills/node/SKILL.md +0 -224
- package/.agents/generated/claude/skills/performance/SKILL.md +0 -121
- package/.agents/generated/claude/skills/react/SKILL.md +0 -227
- package/.agents/generated/claude/skills/react-best-practices/SKILL.md +0 -146
- package/.agents/generated/claude/skills/redesign-audit/SKILL.md +0 -112
- package/.agents/generated/claude/skills/soft-design/SKILL.md +0 -103
- package/.agents/generated/claude/skills/state-management/SKILL.md +0 -120
- package/.agents/generated/claude/skills/subagent-orchestrator/SKILL.md +0 -93
- package/.agents/generated/claude/skills/system-design/SKILL.md +0 -507
- package/.agents/generated/claude/skills/testing/SKILL.md +0 -157
- package/.agents/generated/claude/skills/typescript/SKILL.md +0 -192
- package/.agents/generated/claude/skills/ui-design/SKILL.md +0 -161
- package/.agents/generated/claude/skills/ui-ux-pro/SKILL.md +0 -451
- package/.agents/generated/claude/skills/ux-design/SKILL.md +0 -168
- package/.agents/generated/claude/skills/vercel-optimize/SKILL.md +0 -76
- package/.agents/generated/claude/skills/web-accessibility/SKILL.md +0 -225
- package/.agents/generated/gemini/skills/adapters/SKILL.md +0 -135
- package/.agents/generated/gemini/skills/architecture-diagrams/SKILL.md +0 -107
- package/.agents/generated/gemini/skills/brutalist-design/SKILL.md +0 -151
- package/.agents/generated/gemini/skills/database/SKILL.md +0 -200
- package/.agents/generated/gemini/skills/ddd/SKILL.md +0 -314
- package/.agents/generated/gemini/skills/decisions/SKILL.md +0 -143
- package/.agents/generated/gemini/skills/docker/SKILL.md +0 -144
- package/.agents/generated/gemini/skills/fastapi/SKILL.md +0 -209
- package/.agents/generated/gemini/skills/generators/SKILL.md +0 -142
- package/.agents/generated/gemini/skills/graphify/SKILL.md +0 -205
- package/.agents/generated/gemini/skills/impeccable-design/SKILL.md +0 -250
- package/.agents/generated/gemini/skills/interview-me/SKILL.md +0 -96
- package/.agents/generated/gemini/skills/microservices/SKILL.md +0 -227
- package/.agents/generated/gemini/skills/minimalist-design/SKILL.md +0 -114
- package/.agents/generated/gemini/skills/nestjs/SKILL.md +0 -204
- package/.agents/generated/gemini/skills/nextjs/SKILL.md +0 -298
- package/.agents/generated/gemini/skills/node/SKILL.md +0 -323
- package/.agents/generated/gemini/skills/performance/SKILL.md +0 -185
- package/.agents/generated/gemini/skills/react/SKILL.md +0 -332
- package/.agents/generated/gemini/skills/react-best-practices/SKILL.md +0 -152
- package/.agents/generated/gemini/skills/redesign-audit/SKILL.md +0 -118
- package/.agents/generated/gemini/skills/soft-design/SKILL.md +0 -109
- package/.agents/generated/gemini/skills/state-management/SKILL.md +0 -129
- package/.agents/generated/gemini/skills/subagent-orchestrator/SKILL.md +0 -99
- package/.agents/generated/gemini/skills/system-design/SKILL.md +0 -631
- package/.agents/generated/gemini/skills/testing/SKILL.md +0 -166
- package/.agents/generated/gemini/skills/typescript/SKILL.md +0 -275
- package/.agents/generated/gemini/skills/ui-design/SKILL.md +0 -170
- package/.agents/generated/gemini/skills/ui-ux-pro/SKILL.md +0 -460
- package/.agents/generated/gemini/skills/ux-design/SKILL.md +0 -177
- package/.agents/generated/gemini/skills/vercel-optimize/SKILL.md +0 -82
- package/.agents/generated/gemini/skills/web-accessibility/SKILL.md +0 -300
- package/.agents/mcp/runtime.py +0 -470
- package/.agents/mcp/server.mjs +0 -189271
- package/benchmarks/gemini-issues.js +0 -533
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/runtime/idempotency.js
|
|
3
|
+
* ContextOS — Runtime Idempotency Engine
|
|
4
|
+
*
|
|
5
|
+
* Implements Section 17.5 of CONTEXTOS_IMPLEMENTATION_PLAN.md:
|
|
6
|
+
* - Idempotency key registration and deduplication
|
|
7
|
+
* - Safe replay: returns existing job/worktree without spawning duplicate branches
|
|
8
|
+
* - Conflict detection: raises error if payload differs for the same idempotency key
|
|
9
|
+
* - Atomic persistence to disk
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
function sha256(str) {
|
|
19
|
+
return crypto.createHash('sha256').update(str || '').digest('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class IdempotencyRegistry {
|
|
23
|
+
/**
|
|
24
|
+
* @param {Object} options
|
|
25
|
+
* @param {string} options.baseDir - Base directory (.agents/.contextos)
|
|
26
|
+
*/
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.rootDir = path.resolve(options.baseDir || process.cwd());
|
|
29
|
+
this.registryDir = path.join(this.rootDir, '.agents', '.contextos', 'idempotency');
|
|
30
|
+
fs.mkdirSync(this.registryDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_getKeyPath(idempotencyKey) {
|
|
34
|
+
const safeKey = crypto.createHash('sha256').update(idempotencyKey).digest('hex');
|
|
35
|
+
return path.join(this.registryDir, `${safeKey}.json`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hashes request payload deterministically.
|
|
40
|
+
*/
|
|
41
|
+
hashPayload(payload) {
|
|
42
|
+
if (typeof payload === 'string') return sha256(payload);
|
|
43
|
+
return sha256(JSON.stringify(payload || {}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Checks if an idempotency key already exists.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} idempotencyKey
|
|
50
|
+
* @returns {Object|null}
|
|
51
|
+
*/
|
|
52
|
+
get(idempotencyKey) {
|
|
53
|
+
if (!idempotencyKey) return null;
|
|
54
|
+
const keyPath = this._getKeyPath(idempotencyKey);
|
|
55
|
+
if (!fs.existsSync(keyPath)) return null;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const content = fs.readFileSync(keyPath, 'utf8');
|
|
59
|
+
return JSON.parse(content);
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Executes an operation with idempotency protection.
|
|
67
|
+
* If key was previously executed with identical payload, returns cached result.
|
|
68
|
+
* If payload differs, throws CTX_IDEMPOTENCY_CONFLICT.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} idempotencyKey
|
|
71
|
+
* @param {Object|string} payload
|
|
72
|
+
* @param {Function} factoryFn - Async function returning { jobId, threadId, branchName, worktreePath }
|
|
73
|
+
* @returns {Promise<{ isExisting: boolean, record: Object }>}
|
|
74
|
+
*/
|
|
75
|
+
async execute(idempotencyKey, payload, factoryFn) {
|
|
76
|
+
if (!idempotencyKey) {
|
|
77
|
+
// No idempotency key -> execute directly without caching
|
|
78
|
+
const result = await factoryFn();
|
|
79
|
+
return { isExisting: false, record: result };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const payloadHash = this.hashPayload(payload);
|
|
83
|
+
const existing = this.get(idempotencyKey);
|
|
84
|
+
|
|
85
|
+
if (existing) {
|
|
86
|
+
if (existing.payloadHash !== payloadHash) {
|
|
87
|
+
const err = new Error(
|
|
88
|
+
`Idempotency conflict for key "${idempotencyKey}": payload does not match previously submitted request.`
|
|
89
|
+
);
|
|
90
|
+
err.code = 'CTX_IDEMPOTENCY_CONFLICT';
|
|
91
|
+
err.idempotencyKey = idempotencyKey;
|
|
92
|
+
err.existingJobId = existing.jobId;
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Existing request matched identically -> return existing job without creating duplicate
|
|
97
|
+
return { isExisting: true, record: existing };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Execute operation
|
|
101
|
+
const result = await factoryFn();
|
|
102
|
+
|
|
103
|
+
const record = {
|
|
104
|
+
idempotencyKey,
|
|
105
|
+
payloadHash,
|
|
106
|
+
jobId: result.jobId || result.id || `job-${Date.now()}`,
|
|
107
|
+
threadId: result.threadId || result.id || null,
|
|
108
|
+
branchName: result.branchName || null,
|
|
109
|
+
worktreePath: result.worktreePath || null,
|
|
110
|
+
createdAt: Date.now(),
|
|
111
|
+
metadata: result,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const keyPath = this._getKeyPath(idempotencyKey);
|
|
115
|
+
const tempPath = `${keyPath}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
116
|
+
try {
|
|
117
|
+
fs.writeFileSync(tempPath, JSON.stringify(record, null, 2), 'utf8');
|
|
118
|
+
fs.renameSync(tempPath, keyPath);
|
|
119
|
+
} catch {
|
|
120
|
+
try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { isExisting: false, record };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
IdempotencyRegistry,
|
|
129
|
+
};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/runtime/ipc-lock.js
|
|
3
|
+
* ContextOS — Durable IPC Lease Lock Engine with Fencing Tokens
|
|
4
|
+
*
|
|
5
|
+
* Implements Section 17.1 & 17.4 of CONTEXTOS_IMPLEMENTATION_PLAN.md:
|
|
6
|
+
* - Atomic acquisition with monotonic fencing tokens
|
|
7
|
+
* - Hostname, PID, processStartedAt, instanceId, and ownerToken tracking
|
|
8
|
+
* - Heartbeat lease extension with configurable TTL
|
|
9
|
+
* - Stale owner release prevention (stale owner cannot release new lock)
|
|
10
|
+
* - Pure async wait with bounded exponential backoff, jitter, and AbortSignal
|
|
11
|
+
* - Zero install-time dependencies (pure Node.js crypto & fs)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
|
|
21
|
+
class LeaseLock {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
* @param {string} options.lockFilePath - Absolute path to lock file
|
|
25
|
+
* @param {number} [options.ttlMs=15000] - Lease duration before expiration
|
|
26
|
+
* @param {number} [options.heartbeatIntervalMs=5000] - Interval between lease renewal heartbeats
|
|
27
|
+
* @param {string} [options.instanceId] - Machine/process instance UUID
|
|
28
|
+
* @param {string} [options.repositoryFingerprint]
|
|
29
|
+
*/
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
if (!options.lockFilePath) {
|
|
32
|
+
throw new Error('LeaseLock requires lockFilePath');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.lockFilePath = path.resolve(options.lockFilePath);
|
|
36
|
+
this.lockDir = path.dirname(this.lockFilePath);
|
|
37
|
+
this.ttlMs = Math.max(1000, options.ttlMs || 15000);
|
|
38
|
+
this.heartbeatIntervalMs = Math.max(500, options.heartbeatIntervalMs || 5000);
|
|
39
|
+
this.instanceId = options.instanceId || crypto.randomUUID();
|
|
40
|
+
this.repositoryFingerprint = options.repositoryFingerprint || 'none';
|
|
41
|
+
|
|
42
|
+
this.ownerToken = null;
|
|
43
|
+
this.fencingToken = null;
|
|
44
|
+
this.heartbeatTimer = null;
|
|
45
|
+
this.processStartedAt = Date.now();
|
|
46
|
+
this.isHeld = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reads current lock info from disk if present.
|
|
51
|
+
*
|
|
52
|
+
* @returns {Object|null}
|
|
53
|
+
*/
|
|
54
|
+
readLock() {
|
|
55
|
+
if (!fs.existsSync(this.lockFilePath)) return null;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const content = fs.readFileSync(this.lockFilePath, 'utf8');
|
|
59
|
+
return JSON.parse(content);
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Attempts a single atomic acquisition of the lease.
|
|
67
|
+
*
|
|
68
|
+
* @returns {boolean} True if acquired
|
|
69
|
+
*/
|
|
70
|
+
tryAcquire() {
|
|
71
|
+
fs.mkdirSync(this.lockDir, { recursive: true });
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const current = this.readLock();
|
|
74
|
+
|
|
75
|
+
let nextFencingToken = 1;
|
|
76
|
+
|
|
77
|
+
if (current) {
|
|
78
|
+
const isExpired = current.expiresAt && now > current.expiresAt;
|
|
79
|
+
if (!isExpired) {
|
|
80
|
+
return false; // Valid unexpired lock held by another owner
|
|
81
|
+
}
|
|
82
|
+
// Lock is expired: takeover with incremented fencing token
|
|
83
|
+
nextFencingToken = (current.fencingToken || 0) + 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const newOwnerToken = crypto.randomUUID();
|
|
87
|
+
const payload = {
|
|
88
|
+
ownerToken: newOwnerToken,
|
|
89
|
+
fencingToken: nextFencingToken,
|
|
90
|
+
instanceId: this.instanceId,
|
|
91
|
+
hostname: os.hostname(),
|
|
92
|
+
pid: process.pid,
|
|
93
|
+
processStartedAt: this.processStartedAt,
|
|
94
|
+
heartbeatAt: now,
|
|
95
|
+
expiresAt: now + this.ttlMs,
|
|
96
|
+
repositoryFingerprint: this.repositoryFingerprint,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Atomic write via unique temp file and rename/link
|
|
100
|
+
const tempFile = `${this.lockFilePath}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
101
|
+
try {
|
|
102
|
+
fs.writeFileSync(tempFile, JSON.stringify(payload, null, 2), { flag: 'wx', encoding: 'utf8' });
|
|
103
|
+
|
|
104
|
+
// Atomically link or rename
|
|
105
|
+
try {
|
|
106
|
+
if (process.platform === 'win32') {
|
|
107
|
+
// On Windows, if destination exists, renameSync fails unless unlinked first.
|
|
108
|
+
// Check again that current lock is still expired before replacing.
|
|
109
|
+
const fresh = this.readLock();
|
|
110
|
+
if (fresh && fresh.expiresAt && now <= fresh.expiresAt) {
|
|
111
|
+
fs.unlinkSync(tempFile);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (fs.existsSync(this.lockFilePath)) {
|
|
115
|
+
try { fs.unlinkSync(this.lockFilePath); } catch {}
|
|
116
|
+
}
|
|
117
|
+
fs.renameSync(tempFile, this.lockFilePath);
|
|
118
|
+
} else {
|
|
119
|
+
// On Unix, rename is atomic POSIX replace
|
|
120
|
+
fs.renameSync(tempFile, this.lockFilePath);
|
|
121
|
+
}
|
|
122
|
+
} catch (renameErr) {
|
|
123
|
+
try { if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); } catch {}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Verify that our token won the race
|
|
128
|
+
const verified = this.readLock();
|
|
129
|
+
if (!verified || verified.ownerToken !== newOwnerToken) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.ownerToken = newOwnerToken;
|
|
134
|
+
this.fencingToken = nextFencingToken;
|
|
135
|
+
this.isHeld = true;
|
|
136
|
+
this._startHeartbeat();
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
try { if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); } catch {}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Pure async wait with exponential backoff and jitter (Section 17.4).
|
|
146
|
+
* No Atomics.wait on event loop.
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} [options]
|
|
149
|
+
* @param {number} [options.timeoutMs=30000]
|
|
150
|
+
* @param {AbortSignal} [options.signal]
|
|
151
|
+
* @returns {Promise<boolean>}
|
|
152
|
+
*/
|
|
153
|
+
async acquire(options = {}) {
|
|
154
|
+
const timeoutMs = options.timeoutMs || 30000;
|
|
155
|
+
const signal = options.signal;
|
|
156
|
+
const startTime = Date.now();
|
|
157
|
+
|
|
158
|
+
let attempt = 0;
|
|
159
|
+
const baseDelayMs = 50;
|
|
160
|
+
const maxDelayMs = 1000;
|
|
161
|
+
|
|
162
|
+
while (true) {
|
|
163
|
+
if (signal && signal.aborted) {
|
|
164
|
+
const err = new Error('Lock acquisition aborted');
|
|
165
|
+
err.code = 'CTX_LOCK_ACQUISITION_ABORTED';
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (this.tryAcquire()) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
174
|
+
const err = new Error(`Timed out waiting to acquire lock: "${this.lockFilePath}" (${timeoutMs}ms)`);
|
|
175
|
+
err.code = 'CTX_LOCK_TIMEOUT';
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Exponential backoff with jitter
|
|
180
|
+
attempt++;
|
|
181
|
+
const expDelay = Math.min(maxDelayMs, baseDelayMs * Math.pow(1.5, attempt));
|
|
182
|
+
const jitter = Math.random() * 0.3 * expDelay;
|
|
183
|
+
const sleepMs = Math.floor(expDelay + jitter);
|
|
184
|
+
|
|
185
|
+
await new Promise(resolve => setTimeout(resolve, sleepMs));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Starts periodic heartbeat extending lease expiration.
|
|
191
|
+
*/
|
|
192
|
+
_startHeartbeat() {
|
|
193
|
+
this._stopHeartbeat();
|
|
194
|
+
this.heartbeatTimer = setInterval(() => {
|
|
195
|
+
this.heartbeat();
|
|
196
|
+
}, this.heartbeatIntervalMs);
|
|
197
|
+
// Do not prevent process exit if only heartbeat timer is alive
|
|
198
|
+
if (this.heartbeatTimer.unref) {
|
|
199
|
+
this.heartbeatTimer.unref();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_stopHeartbeat() {
|
|
204
|
+
if (this.heartbeatTimer) {
|
|
205
|
+
clearInterval(this.heartbeatTimer);
|
|
206
|
+
this.heartbeatTimer = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extends lease expiration on disk if we are still the valid owner.
|
|
212
|
+
*
|
|
213
|
+
* @returns {boolean} True if successfully renewed
|
|
214
|
+
*/
|
|
215
|
+
heartbeat() {
|
|
216
|
+
if (!this.isHeld || !this.ownerToken) return false;
|
|
217
|
+
|
|
218
|
+
const current = this.readLock();
|
|
219
|
+
// Safety invariant: only renew if our ownerToken is on disk
|
|
220
|
+
if (!current || current.ownerToken !== this.ownerToken) {
|
|
221
|
+
// We lost the lock or lease expired and was taken over
|
|
222
|
+
this.isHeld = false;
|
|
223
|
+
this._stopHeartbeat();
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
current.heartbeatAt = now;
|
|
229
|
+
current.expiresAt = now + this.ttlMs;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
fs.writeFileSync(this.lockFilePath, JSON.stringify(current, null, 2), 'utf8');
|
|
233
|
+
return true;
|
|
234
|
+
} catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Releases lock strictly if ownerToken matches on disk.
|
|
241
|
+
* Stale owner can NEVER delete a new lock acquired by another process.
|
|
242
|
+
*
|
|
243
|
+
* @returns {boolean} True if released
|
|
244
|
+
*/
|
|
245
|
+
release() {
|
|
246
|
+
this._stopHeartbeat();
|
|
247
|
+
|
|
248
|
+
if (!this.ownerToken) {
|
|
249
|
+
this.isHeld = false;
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const current = this.readLock();
|
|
254
|
+
// Invariant: stale owner cannot delete a new lock
|
|
255
|
+
if (!current || current.ownerToken !== this.ownerToken) {
|
|
256
|
+
this.isHeld = false;
|
|
257
|
+
this.ownerToken = null;
|
|
258
|
+
return false; // Another owner already holds the lock
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
if (fs.existsSync(this.lockFilePath)) {
|
|
263
|
+
fs.unlinkSync(this.lockFilePath);
|
|
264
|
+
}
|
|
265
|
+
this.isHeld = false;
|
|
266
|
+
this.ownerToken = null;
|
|
267
|
+
return true;
|
|
268
|
+
} catch {
|
|
269
|
+
this.isHeld = false;
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Asserts that current process holds a valid unexpired lease with expected fencing token.
|
|
276
|
+
*
|
|
277
|
+
* @param {number} [expectedFencingToken]
|
|
278
|
+
*/
|
|
279
|
+
assertValid(expectedFencingToken) {
|
|
280
|
+
if (!this.isHeld || !this.ownerToken) {
|
|
281
|
+
const err = new Error('Lock is not held by current process');
|
|
282
|
+
err.code = 'CTX_LOCK_NOT_HELD';
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const current = this.readLock();
|
|
287
|
+
if (!current || current.ownerToken !== this.ownerToken) {
|
|
288
|
+
this.isHeld = false;
|
|
289
|
+
const err = new Error('Lock lease was lost or taken over by another process');
|
|
290
|
+
err.code = 'CTX_LOCK_LOST';
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (Date.now() > current.expiresAt) {
|
|
295
|
+
this.isHeld = false;
|
|
296
|
+
const err = new Error('Lock lease has expired');
|
|
297
|
+
err.code = 'CTX_LOCK_EXPIRED';
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (typeof expectedFencingToken === 'number' && current.fencingToken !== expectedFencingToken) {
|
|
302
|
+
const err = new Error(`Fencing token mismatch: expected ${expectedFencingToken}, got ${current.fencingToken}`);
|
|
303
|
+
err.code = 'CTX_FENCING_TOKEN_MISMATCH';
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = {
|
|
310
|
+
LeaseLock,
|
|
311
|
+
};
|