@hybridlabor-api/aos 4.2.0-beta.0 → 4.2.0

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.
Files changed (83) hide show
  1. package/.agents/graph.md +3 -1
  2. package/.agents/nodes.json +4 -2
  3. package/.claude/workflows/startcycle-dispatch.mjs +18 -5
  4. package/.claude/workflows/teamwork-dispatch.mjs +287 -0
  5. package/THIRD_PARTY_NOTICES.md +50 -0
  6. package/docs/skills_table.md +1 -0
  7. package/installer.js +15 -0
  8. package/package.json +1 -1
  9. package/skills/basic/bdbmediastorm/SKILL.md +7 -5
  10. package/skills/basic/startcycle/SKILL.md +3 -1
  11. package/skills/basic/startcycle-graph/SKILL.md +18 -2
  12. package/skills/basic/startcycle-graph-user/SKILL.md +3 -1
  13. package/skills/basic/teamwork-preview/SKILL.md +209 -0
  14. package/skills/bdbrainstorm/SKILL.md +4 -3
  15. package/skills/global_config/ask-tim/SKILL.md +73 -6
  16. package/skills/global_config/bdbresilience/SKILL.md +216 -0
  17. package/skills/global_config/bdbresilience/contracts/nodes-integration.md +225 -0
  18. package/skills/global_config/bdbresilience/references/cicd-triage.md +179 -0
  19. package/skills/global_config/bdbresilience/references/distributed-locking.md +235 -0
  20. package/skills/global_config/bdbresilience/references/error-recovery.md +210 -0
  21. package/skills/global_config/bdbresilience/references/two-phase-go-gate.md +151 -0
  22. package/skills/global_config/domain-modeling/ADR-FORMAT.md +47 -0
  23. package/skills/global_config/domain-modeling/CONTEXT-FORMAT.md +60 -0
  24. package/skills/global_config/domain-modeling/SKILL.md +77 -0
  25. package/skills/global_config/grill-me/SKILL.md +14 -0
  26. package/skills/global_config/grill-with-docs/SKILL.md +24 -0
  27. package/skills/global_config/grilling/SKILL.md +42 -0
  28. package/skills/global_config/openwiki-skill/scripts/install_daemon.sh +55 -12
  29. package/.agents/skills/firecrawl/SKILL.md +0 -149
  30. package/.agents/skills/firecrawl/rules/install.md +0 -82
  31. package/.agents/skills/firecrawl/rules/security.md +0 -26
  32. package/.agents/skills/firecrawl-agent/SKILL.md +0 -58
  33. package/.agents/skills/firecrawl-build/SKILL.md +0 -39
  34. package/.agents/skills/firecrawl-build-interact/SKILL.md +0 -68
  35. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +0 -103
  36. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +0 -39
  37. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +0 -20
  38. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +0 -17
  39. package/.agents/skills/firecrawl-build-scrape/SKILL.md +0 -69
  40. package/.agents/skills/firecrawl-build-search/SKILL.md +0 -69
  41. package/.agents/skills/firecrawl-crawl/SKILL.md +0 -59
  42. package/.agents/skills/firecrawl-download/SKILL.md +0 -70
  43. package/.agents/skills/firecrawl-interact/SKILL.md +0 -84
  44. package/.agents/skills/firecrawl-map/SKILL.md +0 -51
  45. package/.agents/skills/firecrawl-scrape/SKILL.md +0 -69
  46. package/.agents/skills/firecrawl-search/SKILL.md +0 -60
  47. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +0 -10
  48. package/mcps/after-effects-mcp/build/index.js +0 -840
  49. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +0 -153
  50. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +0 -218
  51. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +0 -71
  52. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +0 -147
  53. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +0 -114
  54. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +0 -115
  55. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +0 -192
  56. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +0 -90
  57. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +0 -50
  58. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +0 -1773
  59. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +0 -160
  60. package/mcps/bdb-remoteos-mcp/queue.db +0 -0
  61. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  62. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/incus_client.cpython-312.pyc +0 -0
  63. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/main.cpython-312.pyc +0 -0
  64. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/queue.cpython-312.pyc +0 -0
  65. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/schemas.cpython-312.pyc +0 -0
  66. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/server.cpython-312.pyc +0 -0
  67. package/mcps/bdb-remoteos-mcp/src/bdb_remoteos_mcp/__pycache__/webhook.cpython-312.pyc +0 -0
  68. package/mcps/bdb-remoteos-mcp/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  69. package/mcps/bdb-remoteos-mcp/tests/__pycache__/mock_incus.cpython-312.pyc +0 -0
  70. package/mcps/bdb-remoteos-mcp/tests/__pycache__/test_mcp_server.cpython-312-pytest-9.1.1.pyc +0 -0
  71. package/mcps/bdb-remoteos-mcp/tests/__pycache__/test_security_redteam.cpython-312-pytest-9.1.1.pyc +0 -0
  72. package/mcps/bdb-remoteos-mcp/tests/__pycache__/test_webhook.cpython-312-pytest-9.1.1.pyc +0 -0
  73. package/mcps/computer-use-mcp/dist/client.d.ts +0 -150
  74. package/mcps/computer-use-mcp/dist/client.js +0 -136
  75. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +0 -16
  76. package/mcps/computer-use-mcp/dist/entrypoint.js +0 -26
  77. package/mcps/computer-use-mcp/dist/native.d.ts +0 -212
  78. package/mcps/computer-use-mcp/dist/native.js +0 -50
  79. package/mcps/computer-use-mcp/dist/server.d.ts +0 -32
  80. package/mcps/computer-use-mcp/dist/server.js +0 -342
  81. package/mcps/computer-use-mcp/dist/session.d.ts +0 -101
  82. package/mcps/computer-use-mcp/dist/session.js +0 -2372
  83. package/skills/bdbsaastraining/scripts/__pycache__/build_profile.cpython-314.pyc +0 -0
@@ -0,0 +1,225 @@
1
+ # 🤝 Node & Pipeline Integration Contracts
2
+
3
+ > **Status: partially applied. Read the split below before relying on anything here.**
4
+ >
5
+ > **Applied (2026-09-09).** This skill ships inside AOS at
6
+ > `skills/global_config/bdbresilience/`, and `.agents/nodes.json` lists
7
+ > `bdbresilience` in the `skills` array of two nodes: **`shipping`** (it runs
8
+ > lint/typecheck/tests, which is exactly what the triage parsers read) and
9
+ > **`engineering`** (the error taxonomy and backoff guidance). The dispatcher
10
+ > injects each node's `skills` list into its prompt, so those two nodes are told
11
+ > to reach for this skill. That is the whole of the integration: **guidance, not
12
+ > code.** AOS does not depend on the `bdb-cicd-resilience` package — it has two
13
+ > runtime dependencies and neither is this one.
14
+ >
15
+ > **Not applied, and not currently possible.** Every code-level integration below.
16
+ > `startcycle-dispatch.mjs` contains no `withStateLock` call and cannot contain one:
17
+ > the Workflow runtime gives the dispatcher script **no filesystem access**, which is
18
+ > precisely why AOS solves its own concurrent-write race with single-writer fragments
19
+ > (`production_artifacts/state.d/<node>.json`) plus a merge step instead of a lock.
20
+ > A lock needs a filesystem; the dispatcher does not have one. Do not wire one in.
21
+ >
22
+ > **Where a lock would genuinely fit**, if this is revisited: `installer.js`, which
23
+ > does a real read-modify-write on `~/.agents/.bdb-install-manifest.json` and can be
24
+ > run twice concurrently. That is the only place in AOS with both filesystem access
25
+ > and real contention. Before doing it, note that this library's Windows paths
26
+ > (`EPERM`/`EBUSY` handling, close-before-unlink ordering) are **written but never
27
+ > executed** — two tests skip on non-Windows — and AOS ships to Windows users.
28
+ >
29
+ > §6 documents the `withStateLock()` signature as it actually exists in
30
+ > `src/locking/resource-lock.ts`.
31
+
32
+ **Deliverable**: Requirement R4 Integration Contracts (proposal)
33
+ **Target Architecture**: BDB Agent OS (`.agents/nodes.json`, `/startcycle-graph`, `/bdbrainstorm`)
34
+ **Specification Reference**: `survey_miner_1/survey_report.md`
35
+
36
+ ---
37
+
38
+ ## 1. Proposed Architectural Role
39
+
40
+ The proposal is to wire the `/bdbresilience` skill suite into the BDB Agent OS multi-agent dispatcher graph. Equipping the **Reviewer** and **Shipping** nodes with resilience capabilities would give the ecosystem automated adversarial auditing of network and concurrency boundaries, and deterministic self-healing quality gates.
41
+
42
+ Resilience principles would additionally be seeded upstream in **`/bdbrainstorm`** during concept ideation, so that downstream implementation nodes (Engineering, UI/UX, Media) receive explicit reliability requirements.
43
+
44
+ Applying this proposal is out of scope for the current cycle. It belongs to an AOS integration cycle, which is also where several deferred design questions get their answers — fencing-token verification, the default `ttlMs`, and retry idempotency.
45
+
46
+ ---
47
+
48
+ ## 2. Not adopted: the Reviewer node
49
+
50
+ > **This proposal was considered and declined on 2026-09-09.** The live
51
+ > `nodes.json` does **not** carry `bdbresilience` in `reviewer.skills`, and that is
52
+ > deliberate, not an oversight.
53
+ >
54
+ > Reviewer reads build artifacts and the plan's contract and argues about
55
+ > correctness. It does not run CI, does not read tool logs, and does not classify
56
+ > retryable failures — the three things this skill is for. A skill allowlist that
57
+ > lists everything guides nothing, so the registration went to `shipping` (which
58
+ > runs the gates whose output the triage parsers read) and `engineering` (which
59
+ > owns the error taxonomy) and stopped there.
60
+ >
61
+ > The section is kept because the reasoning is worth having on record, and because
62
+ > the shape of the entry is a useful template if a future node genuinely needs it.
63
+
64
+ The **Reviewer** node executes adversarial verification of build-node outputs against the execution plan contract using the *doubt-driven development* discipline. The JSON below is the entry that **would** be added, had this been adopted.
65
+
66
+ ### Declarative Configuration (`.agents/nodes.json`)
67
+ ```json
68
+ {
69
+ "reviewer": {
70
+ "label": "Reviewer",
71
+ "agentType": "reviewer",
72
+ "personaFile": ".claude/agents/reviewer.md",
73
+ "model": "sonnet",
74
+ "role": "review",
75
+ "artifactKey": "review",
76
+ "writes": null,
77
+ "optional": false,
78
+ "skills": [
79
+ "ui-review",
80
+ "ux-audit",
81
+ "architect-review",
82
+ "systematic-debugging",
83
+ "bdbresilience"
84
+ ],
85
+ "instructions": null
86
+ }
87
+ }
88
+ ```
89
+
90
+ ### Proposed Reviewer Resilience Audit Discipline
91
+ Once equipped with `bdbresilience`, Reviewer would evaluate build outputs (`state.artifacts.{frontend,backend,media}`) against strict resilience checks:
92
+
93
+ 1. **Network & Tool Call Audits**:
94
+ - Verifies that all external HTTP, API, database, and MCP calls implement structured error classification and Full Jitter exponential backoff.
95
+ - Flags bare `catch (e) {}` blocks, unhandled Promise rejections, and silent error swallows as:
96
+ `{ "id": "F-RES-NET-01", "severity": "blocking", "node": "engineering", "status": "open" }`.
97
+ 2. **Concurrency & Resource Access Audits**:
98
+ - Verifies that parallel operations accessing shared files, worktrees, or databases either write isolated fragments (`production_artifacts/state.d/<nodeId>.json`) or acquire a distributed file lock.
99
+ - Flags missing `finally { await handle.release(); }` blocks as `severity: "blocking"`.
100
+ 3. **Ownership Attribution & Stable ID Generation**:
101
+ - Findings must be attributed strictly to the owning build node (`engineering`, `ui_ux`, or `media`).
102
+ - Finding IDs must remain stable across cycles (e.g. `F-ENG-01`) so the dispatcher's **No-Progress Guard** can detect stalled repair loops and escalate immediately to human review.
103
+
104
+ ---
105
+
106
+ ## 3. Applied: the Shipping node (`.agents/nodes.json`)
107
+
108
+ > **Adopted 2026-09-09.** `shipping.skills` now ends with `"bdbresilience"`, and
109
+ > `engineering.skills` likewise. Those two are the whole of the applied
110
+ > integration — see the status block at the top of this file.
111
+
112
+ The **Shipping** node acts as the release gatekeeper, running mechanical verification gates (lint, typecheck, tests, a11y, seo) after Reviewer findings are cleared. It is the natural home for this skill: the triage parsers read exactly the tsc, ESLint and Jest/Vitest output those gates produce, and `isCleanRun()` is the predicate that decides whether an unrecognised log counts as passing (it does not).
113
+
114
+ ### Declarative Configuration (`.agents/nodes.json`)
115
+ ```json
116
+ {
117
+ "shipping": {
118
+ "label": "Godmode_Shipping",
119
+ "agentType": "godmode-shipping",
120
+ "personaFile": ".claude/agents/godmode-shipping.md",
121
+ "model": "sonnet",
122
+ "role": "gate",
123
+ "artifactKey": "report",
124
+ "writes": null,
125
+ "optional": false,
126
+ "skills": [
127
+ "godmode-shipping",
128
+ "webapp-testing",
129
+ "seo-audit",
130
+ "wcag-audit-patterns",
131
+ "github-repo",
132
+ "clean-code",
133
+ "bdbresilience"
134
+ ],
135
+ "instructions": null
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### Proposed Shipping Quality Gate & Verification Discipline
141
+ Once equipped with `bdbresilience`, Shipping would execute automated diagnostic triage and gating:
142
+
143
+ 1. **Automated Diagnostic Triage Execution**:
144
+ - Runs mechanical verification commands: `npm run typecheck`, `npm run build`, `npm test`, `npm run lint`.
145
+ - If a command fails, Shipping pipes stderr/stdout directly into `generateDiagnosticReport(log, runner)`:
146
+ - A run counts as clean **only if `isCleanRun(report)` is true**, never on `totalFailures === 0` alone — an empty or unparseable log also has zero diagnostics, and reading that as a pass is a fail-open gate.
147
+ - If all failures are classified as `transient_infra` (`canAutoRetry: true`): Shipping automatically retries the command up to 2 times with Full Jitter before recording a failure.
148
+ - If any failure is classified as `deterministic_code_regression`: Shipping generates a structured failure section in `production_artifacts/04_release_report.md` detailing the exact failed assertion, target file, line coordinate, and remediation command.
149
+ 2. **Gate Population & Node Attribution**:
150
+ - Updates `state.gate`:
151
+ ```json
152
+ {
153
+ "lint": "pass",
154
+ "typecheck": "fail",
155
+ "tests": "pass",
156
+ "a11y": "skip",
157
+ "seo": "skip",
158
+ "blockingNodes": ["engineering"]
159
+ }
160
+ ```
161
+ - Instructs the dispatcher to re-invoke only the failing node (`engineering`) rather than re-running all build nodes.
162
+ 3. **Pre-Tool GO Gate Release Protocol**:
163
+ - When all checks pass, Shipping sets `state.phase = "ready_to_ship"`.
164
+ - Concludes turn by outputting the Release Report and instructing the operator:
165
+ > *"All quality gates passed. Reply with the literal word GO to authorize release deployment."*
166
+ - Strictly prohibits calling `git push` or `npm publish` without human approval verified via `verifyGoGate()`.
167
+
168
+ ---
169
+
170
+ ## 4. Proposed: Integration with `/startcycle-graph`
171
+
172
+ `~/.claude/workflows/startcycle-dispatch.mjs` and `~/.claude/hooks/graph-gate.mjs` both exist, but neither references this library. The proposal is that `bdbresilience` would power core graph infrastructure:
173
+
174
+ 1. **Parallel Build Fan-Out Isolation**:
175
+ - When `ui_ux`, `engineering`, and `media` execute concurrently, each node would write exclusively to:
176
+ `production_artifacts/state.d/<nodeId>.json`
177
+ - A dedicated barrier folding agent would consolidate fragments into `production_artifacts/state.json` under `withStateLock()`. **This call does not exist in the dispatcher today.**
178
+ 2. **Loop Retention Hook (`graph-gate.mjs`)**:
179
+ - Would intercept turn completion if `state.gate` contains failing checks and `iteration < max_iterations`, blocking session turn-end with exit code 2 to force repair execution. Whether the existing hook already does this independently of `bdbresilience` was not verified for this document.
180
+ 3. **No-Progress Guard**:
181
+ - If Reviewer reports the exact same set of blocking finding IDs across successive iterations, the dispatcher halts immediately (`state.phase = "escalated"`, `state.needs_human = true`). This guard is part of the AOS graph contract, not of this library.
182
+
183
+ ---
184
+
185
+ ## 5. Proposed: Integration with `/bdbrainstorm`
186
+
187
+ The proposal is that `~/.agents/skills/bdbrainstorm/SKILL.md` would embed resilience requirements into concept planning. Its current content was not inspected for this document:
188
+
189
+ 1. **Pillar 2: `/grill-me` Interactive Inquiries**:
190
+ - Grills the user on SLA thresholds, API rate limits, failure blast radius, and recovery procedures:
191
+ - *"What are the rate limits and fallback providers for external dependency X?"*
192
+ - *"How will concurrent writes to shared database entities be serialized?"*
193
+ 2. **Pillar 4: Engineering Godmode (DDD & Clean Architecture)**:
194
+ - Mandates that domain models represent failure states explicitly as Discriminated Unions (e.g. `Result<T, ClassifiedError>`).
195
+ - Requires Architecture Decision Records (ADRs) for locking mechanisms and retry backoff strategies.
196
+ 3. **Pillar 6: Shipping Godmode & Pipeline Hand-off**:
197
+ - Packages resilience specifications directly into `state.goal` so downstream Architect and TechLead nodes include them in `production_artifacts/00_execution_plan.md`.
198
+
199
+ ---
200
+
201
+ ## 6. `withStateLock()` — the one part of this document that is real
202
+
203
+ This function exists today in `src/locking/resource-lock.ts` and behaves as described. It is what a barrier folding agent in §4 would call.
204
+
205
+ ```typescript
206
+ import { withStateLock } from 'bdb-cicd-resilience/locking/index.js';
207
+
208
+ const merged = await withStateLock(
209
+ 'production_artifacts/state.json',
210
+ async (state) => {
211
+ state.findings.push({ id: 'F-ENG-01', status: 'fixed' });
212
+ return state;
213
+ },
214
+ { ttlMs: 15000, acquireTimeoutMs: 10000 }
215
+ );
216
+ ```
217
+
218
+ - Signature: `withStateLock<T>(statePath, updater, options?) => Promise<T>`.
219
+ - The lock resource key is `state:<resolved absolute path>`, so two callers passing different relative paths to the same file still serialize.
220
+ - `acquireTimeoutMs` defaults to `10000` here (not `0` as in `acquireLock`), so callers queue rather than fail on first contention.
221
+ - A missing state file is read as `{}`; any other read error propagates.
222
+ - The updater's return value is written; returning `undefined` writes the state object as mutated.
223
+ - **The write is refused if the lease was lost while the updater ran** — `handle.isExpired()` is checked after the updater returns and before the temp+rename write, and throws rather than clobber a successor's state file.
224
+ - The state file itself *is* written temp+rename. That is unrelated to the lockfile, which is never renamed into place (see `references/distributed-locking.md` §2).
225
+ - The lock is released in a `finally`, and release errors are swallowed.
@@ -0,0 +1,179 @@
1
+ # 🩺 Reference Guide: CI/CD Self-Healing Triage & Diagnostic Reporting
2
+
3
+ **Pattern**: Pattern 3 — Build, Test & Lint Failure Triage
4
+ **Module**: `bdb-cicd-resilience/triage`
5
+ **Authoritative Source**: BDB Agent OS CI/CD Triage Specification
6
+
7
+ ---
8
+
9
+ ## 1. Overview & Problem Statement
10
+
11
+ When automated CI/CD pipelines fail (during `npm test`, `tsc --noEmit`, `eslint`, or GitHub Actions runs), agents frequently hallucinate root causes by reading unstructured terminal output or trying random edits without identifying the real failure. Alternatively, agents waste execution turns retrying deterministic code bugs, or conversely halt prematurely on transient infrastructure hiccups (such as an npm registry timeout or port bind collision).
12
+
13
+ The CI/CD Triage Engine ingests raw terminal logs, normalizes ANSI codes and runner annotations, parses failure coordinates across 5 major runners, classifies failures into transient infrastructure versus deterministic regressions, and produces verified JSON diagnostic summaries with exact remediation commands.
14
+
15
+ ---
16
+
17
+ ## 2. Multi-Runner Log Ingestion & Normalization
18
+
19
+ Raw build logs contain noisy ANSI color sequences, carriage return overwrites (`\r\n`), and runner-specific group wrappers (`##[group]`, `::error::`).
20
+
21
+ ### ANSI Normalization Pipeline
22
+ 1. **Control Sequence Stripping**: Removes terminal escapes (`\x1b[[0-9;]*[mGKF]`).
23
+ 2. **CRLF Normalization**: Converts Windows line endings to standard Unix newlines (`\r\n` → `\n`).
24
+ 3. **CI Annotation Scrubbing**: Normalizes GitHub Actions workflow commands:
25
+ - `##[group]...##[endgroup]`
26
+ - `##[error]...`
27
+ - `::error file={name},line={line}::{message}`
28
+
29
+ ---
30
+
31
+ ## 3. Runner-Specific Log Parsers
32
+
33
+ The engine incorporates dedicated parsers for each standard ecosystem tool:
34
+
35
+ | Runner | Detected Signatures | Extracted Data Coordinates | Remediation Command Generated |
36
+ |--------|---------------------|----------------------------|-------------------------------|
37
+ | **Vitest** | `FAIL tests/...`<br>`AssertionError:`<br>`expected ... to be ...` | Test file path, line, column, assertion diff snippet | `npx vitest run <file> -t "<test>"` |
38
+ | **Jest** | `● <describe> › <test>`<br>`Expected: ... Received: ...`<br>`at ... (<file>:<line>:<col>)` | Test spec file, stack frame line/col (prioritizing user code over `node_modules`) | `npx jest <file> -t "<test>"` |
39
+ | **TypeScript (`tsc`)** | `src/auth.ts(42,15): error TS2322`<br>`src/auth.ts:42:15 - error TS2322` | Source file, line, column, TS error code, type mismatch explanation | `npx tsc --noEmit` |
40
+ | **ESLint** | `<file>:<line>:<col>: <msg> [<rule>]`<br>Tabular/Stylish reporter lines | Target source file, line, col, rule ID, severity | `npx eslint --fix <file>` |
41
+ | **GitHub Actions** | `##[error]Process completed with exit code 137`<br>`The operation was canceled`<br>`Request timeout after 30000ms` | Runner step, exit code, memory or timeout fault | Automated infrastructure retry with jitter |
42
+
43
+ ---
44
+
45
+ ## 4. Flaky Infrastructure vs. Deterministic Regression Classifier
46
+
47
+ The core intelligence distinguishes between errors that can be safely retried and code bugs requiring source modification:
48
+
49
+ ```
50
+ [ Parsed Diagnostic Error ]
51
+ │
52
+ ▼
53
+ ┌───────────────────────────────────────────────┐
54
+ │ classifyDiagnostic(error, context) │
55
+ └───────────────────────┬───────────────────────┘
56
+ │
57
+ ┌───────────────────────┴───────────────────────┐
58
+ ▼ ▼
59
+ [ TRANSIENT INFRASTRUCTURE ] [ DETERMINISTIC REGRESSION ]
60
+ - HTTP 429 Throttling - AssertionError (Expected X, got Y)
61
+ - Socket ETIMEDOUT / ECONNRESET - TypeScript Type Error (TS2322)
62
+ - Runner Exit Code 137 (OOM) - SyntaxError / Parsing Failure
63
+ - EADDRINUSE (Port collision) - ESLint Rule Violation
64
+ - Registry 503 / DNS Blip - Missing Export / ReferenceError
65
+ │ │
66
+ ▼ ▼
67
+ canAutoRetry: true canAutoRetry: false
68
+ (Retry step with Full Jitter) (Requires source code repair)
69
+ ```
70
+
71
+ ### Classification Heuristics
72
+ - **Transient Infrastructure (`transient_infra`)**:
73
+ - `canAutoRetry: true`
74
+ - Does NOT count against agent repair loop limits.
75
+ - Automatically triggered up to 2 times before escalating.
76
+ - **Deterministic Code Regression (`deterministic_code_regression`)**:
77
+ - `canAutoRetry: false`
78
+ - Attributed to the responsible build node (Engineering, UI/UX).
79
+ - Feeds into `production_artifacts/review_findings.md` or `state.findings`.
80
+ - **Unrecognised text** falls to `deterministic_code_regression`, i.e. this classifier **fails closed**. That is the opposite of `classifyError`'s unknown default in the recovery module, and deliberately so — see `error-recovery.md` §7 for why neither should be changed to match the other.
81
+
82
+ ---
83
+
84
+ ## 5. Was Anything Understood? — `parseStatus` and `isCleanRun`
85
+
86
+ `totalFailures === 0` is **not** the answer to "did this run pass". A log that was empty, and a log that no parser recognised, both produce zero diagnostics. Reading either as a clean gate is a fail-open defect, so the report carries a third axis:
87
+
88
+ | `parseStatus` | When | `totalFailures` | `canAutoRetry` | `requiresHumanIntervention` |
89
+ |---|---|---|---|---|
90
+ | `parsed` | a runner signature matched, or a parser extracted ≥1 diagnostic | as extracted | `n > 0 && transient === n` | `deterministic > 0` |
91
+ | `empty` | the log is empty or pure ANSI after stripping | `0` | `false` | `true` |
92
+ | `unparsed` | nothing matched — e.g. a Go, pytest or Maven failure | `0` | `false` | `true` |
93
+
94
+ `empty` is deliberately **not** clean: a build step that produced no output is not evidence that it passed. Neither status ever carries a synthetic diagnostic — inventing one would inflate `totalFailures` and lie to every consumer counting failures.
95
+
96
+ The predicate is exported so no consumer has to reconstruct the rule:
97
+
98
+ ```typescript
99
+ import { isCleanRun } from 'bdb-cicd-resilience/triage/index.js';
100
+
101
+ if (isCleanRun(report)) { /* the only sanctioned "it passed" */ }
102
+ ```
103
+
104
+ `isCleanRun(report) === (report.summary.parseStatus === 'parsed' && report.summary.totalFailures === 0)`. Any AOS integration must call it rather than reading `totalFailures` directly.
105
+
106
+ ---
107
+
108
+ ## 6. Structured JSON Diagnostic Schema
109
+
110
+ `dominantCategory` is `undefined` whenever `totalFailures === 0`; it is not defaulted to a classification.
111
+
112
+ ```json
113
+ {
114
+ "timestamp": "2026-09-05T14:45:00.000Z",
115
+ "runner": "vitest",
116
+ "summary": {
117
+ "totalFailures": 2,
118
+ "transientCount": 1,
119
+ "deterministicCount": 1,
120
+ "parseStatus": "parsed",
121
+ "dominantCategory": "deterministic_code_regression",
122
+ "canAutoRetry": false,
123
+ "requiresHumanIntervention": true
124
+ },
125
+ "diagnostics": [
126
+ {
127
+ "file": "tests/unit/auth.test.ts",
128
+ "line": 42,
129
+ "column": 14,
130
+ "assertionSnippet": "expect(user.isAuthenticated).toBe(true)",
131
+ "classification": "deterministic_code_regression",
132
+ "rootCause": "AssertionError: expected false to be true // Received user object without auth token",
133
+ "remediationCommand": "npx vitest run tests/unit/auth.test.ts -t \"verifies authenticated user\"",
134
+ "canAutoRetry": false
135
+ },
136
+ {
137
+ "file": "src/services/api.ts",
138
+ "classification": "transient_infra",
139
+ "rootCause": "FetchError: request to https://registry.npmjs.org timed out after 30000ms (ETIMEDOUT)",
140
+ "remediationCommand": "npm cache clean --force && npm install",
141
+ "canAutoRetry": true
142
+ }
143
+ ]
144
+ }
145
+ ```
146
+
147
+ ---
148
+
149
+ ## 7. TypeScript API Usage Example
150
+
151
+ ```typescript
152
+ import {
153
+ generateDiagnosticReport,
154
+ classifyDiagnostic,
155
+ isCleanRun,
156
+ stripAnsi
157
+ } from 'bdb-cicd-resilience/triage/index.js';
158
+
159
+ // Clean and parse raw output
160
+ const cleanLog = stripAnsi(rawStderr);
161
+ const report = generateDiagnosticReport(cleanLog);
162
+
163
+ if (isCleanRun(report)) {
164
+ console.log('✅ All checks passed clean.');
165
+ } else if (report.summary.parseStatus !== 'parsed') {
166
+ console.error(`❌ Log was ${report.summary.parseStatus} — nothing was understood, not a pass.`);
167
+ } else if (report.summary.canAutoRetry && report.summary.deterministicCount === 0) {
168
+ console.log('⚠️ Transient infrastructure failure detected. Executing auto-retry...');
169
+ await executeRetryCommand();
170
+ } else {
171
+ console.error('❌ Deterministic code regression detected:');
172
+ for (const diag of report.diagnostics) {
173
+ if (diag.classification === 'deterministic_code_regression') {
174
+ console.error(` - ${diag.file}:${diag.line} -> ${diag.rootCause}`);
175
+ console.error(` Suggested fix command: ${diag.remediationCommand}`);
176
+ }
177
+ }
178
+ }
179
+ ```
@@ -0,0 +1,235 @@
1
+ # 🔒 Reference Guide: Distributed File Locking & Concurrency Control
2
+
3
+ **Pattern**: Pattern 2 — Mutual Exclusion & Concurrency Control
4
+ **Module**: `bdb-cicd-resilience/locking`
5
+ **Authoritative Source**: BDB Agent OS Concurrency Specification
6
+
7
+ ---
8
+
9
+ ## 1. Overview & Problem Statement
10
+
11
+ In the BDB Agent OS multi-agent ecosystem, multiple autonomous agents, terminal sessions, and worktree subprocesses operate concurrently. Without rigorous synchronization, concurrent access to shared resources leads to critical data corruption:
12
+ - **`production_artifacts/state.json` Overwrites**: Parallel agents clobbering each other's status, findings, and phase transitions.
13
+ - **Git Branch & Worktree Collisions**: Concurrent `git checkout`, `git commit`, or branch operations producing index lock contention (`.git/index.lock`).
14
+ - **Database Migration Races**: Competing processes applying conflicting schema alterations simultaneously.
15
+
16
+ The Distributed Locking Engine provides deterministic mutual exclusion using atomic POSIX/APFS filesystem primitives, configurable lease timeouts, background heartbeat renewals, monotonic fencing tokens, and safe stale/orphan lock eviction.
17
+
18
+ ---
19
+
20
+ ## 2. Deterministic Atomic Locking Mechanics
21
+
22
+ ### POSIX Atomic Test-and-Set
23
+ File creation with the flags `O_CREAT | O_EXCL` (Node.js flag `'wx'`) is guaranteed by POSIX and APFS kernel implementations to be strictly atomic.
24
+
25
+ ```
26
+ Worker A (Attempt Acquire) Worker B (Attempt Acquire)
27
+ │ │
28
+ ├─────────────────────┬───────────────────────┤
29
+ │ │ │
30
+ ▼ ▼ ▼
31
+ [ open('...lock', 'wx') ] [ open('...lock', 'wx') ]
32
+ │ │
33
+ Kernel Awards Kernel Rejects
34
+ File Descriptor (FD) with code EEXIST
35
+ │ │
36
+ ▼ ▼
37
+ [ Writes Metadata JSON ] [ Inspects Lock / Enters ]
38
+ [ Enters Critical Sec ] [ Polling Backoff Retry ]
39
+ ```
40
+
41
+ If the lockfile already exists, the OS kernel immediately fails the call with `EEXIST` without modifying the target file, ensuring zero window for race conditions.
42
+
43
+ ### Pre-Serialization, and the window that remains
44
+ 1. The complete metadata JSON payload is formatted in memory *before* `fs.open(lockPath, 'wx')` is invoked.
45
+ 2. The payload is written through the returned file descriptor and `fsync`'d (`fileHandle.sync()`) **before the handle is published to the caller**.
46
+ 3. The descriptor is then **retained**, not closed. It is handed to the heartbeat manager and closed in `release()`. Every renewal writes through it, so a renewal can only ever land on the inode this holder created — never on whatever file currently occupies the path.
47
+
48
+ What step 2 does *not* guarantee: the **containing directory is not `fsync`'d**, so on ext4 the directory entry itself may not survive a power cut. That is deliberate and is the safe failure direction here — a missing lock is recoverable, a phantom lock is not.
49
+
50
+ The gap between `open('wx')` and the payload write is a real 0-byte window and cannot be closed from the writer side: `O_CREAT | O_EXCL` is the only atomic create-if-not-exists primitive available, and replacing it with temp+`rename()` would destroy mutual exclusion, because `rename()` overwrites its target unconditionally and every contender would win. The window is closed from the **reader** side instead, by the unreadable-lock grace in §4.
51
+
52
+ ### Structured Lock Metadata Schema
53
+ ```json
54
+ {
55
+ "lockId": "7f8b9e20-94d3-4f2a-8b1a-9f5e1284d0a1",
56
+ "resource": "production_artifacts/state.json",
57
+ "ownerId": "pid_28419_9f5e1284",
58
+ "pid": 28419,
59
+ "acquiredAt": 1757083200000,
60
+ "heartbeatAt": 1757083210000,
61
+ "ttlMs": 10000,
62
+ "fencingToken": 42,
63
+ "hostname": "denck-studio.local"
64
+ }
65
+ ```
66
+
67
+ - **There is no `leaseExpiresAt` field.** The lease expiry is *derived* — `heartbeatAt + ttlMs` — and evaluated at the point of use. Storing it as well would create a second source of truth that can disagree with its own inputs on every heartbeat.
68
+ - **`fencingToken`**: Monotonically increasing integer, allocated per lock path. **Advisory only** — see §7.
69
+ - **`pid`** / **`hostname`**: Used together for liveness verification during eviction. The PID is only probed when `hostname` matches the local host, because PIDs are not meaningful across machines.
70
+
71
+ ### Lease expiry as seen by the holder
72
+
73
+ `handle.isExpired()` does **not** read `heartbeatAt` from the metadata: that value is frozen at acquisition and only moved by `extend()`, so a healthy, actively-renewing lock would report expired the moment `ttlMs` elapsed. It reads the heartbeat manager's `lastRenewedAt` instead — the timestamp of the last write+truncate that actually resolved:
74
+
75
+ ```
76
+ isExpired() === released || lockLost || Date.now() > lastRenewedAt + ttlMs
77
+ ```
78
+
79
+ `withStateLock` relies on this: after the updater returns and before the state file is written, it throws rather than write if the lease was lost while the updater ran.
80
+
81
+ ---
82
+
83
+ ## 3. Lease Timeouts (TTL) & Background Heartbeat Renewal
84
+
85
+ Static lockfiles are vulnerable to permanent deadlocks if the holding process crashes or is forcefully terminated (`SIGKILL`). The engine solves this using expiring leases backed by active background heartbeat renewal.
86
+
87
+ ### Heartbeat Renewal Architecture
88
+ 1. **Configurable TTL**: The code default is **`10,000 ms`** (`ttlMs` in `LockAcquisitionOptions`). That is aggressive for CI workloads, where a single step can block on a network call for longer than 10 s; **raise it explicitly** for CI-held locks. The default is a candidate for revision once real hold times are measurable against AOS.
89
+ 2. **Periodic Renewal Interval**: `heartbeatIntervalMs` defaults to `Math.max(10, Math.floor(ttlMs / 3))` — i.e. TTL/3 with a 10 ms floor, so a very short TTL cannot produce a busy timer.
90
+ 3. **In-place write through the retained descriptor.** Renewal does **not** use a temporary file and does **not** `rename()`. It writes the new payload at offset 0 through the fd retained from acquisition, then `truncate()`s to the new byte length. A tempfile+rename renewal would write by *path*, which is exactly how a holder ends up clobbering a successor's lock.
91
+
92
+ Each tick:
93
+
94
+ | Step | Condition | Outcome |
95
+ |---|---|---|
96
+ | 1 | read the file **by path**; `ENOENT` | lock lost |
97
+ | 2 | present but unparseable | **skip the tick** — no write, no truncate, no `lastRenewedAt` advance, and *not* lock-lost |
98
+ | 3 | `lockId` in the file ≠ our own | lock lost (evicted or stolen) |
99
+ | 4 | match | `write(payload, 0)` through the fd, `truncate(Buffer.byteLength(payload))`, then record `lastRenewedAt` |
100
+
101
+ **Step 2 is deliberate.** Step 1 reads by path and can legitimately catch a successor mid-write; treating one torn read as terminal would make a healthy lock self-evict. Skipping is not silent either — `lastRenewedAt` stops advancing, so *persistent* unparseability still terminates the lease exactly one `ttlMs` later through the ordinary expiry path.
102
+
103
+ **The truncate in step 4 is not optional.** Writing a shorter payload at offset 0 leaves the tail of the previous one behind, producing permanently invalid JSON — which the §4 grace then hides for its full window, because these very writes keep refreshing `mtime`. `extend()` uses the same write+truncate and records `lastRenewedAt` only after **both** resolve, for the same reason.
104
+
105
+ "Lock lost" means: stop the timer, close the fd, mark the handle released, fire `onEvicted`, and make `isExpired()` return `true`.
106
+
107
+ ```
108
+ [ Active Lock Lease (TTL: 15s) ]
109
+ 0s ├──────────────────────────────────────────────────┤ 15s
110
+ │ │
111
+ ▼ (Heartbeat @ 5s) │
112
+ 5s ├── In-place write + truncate (own fd) ────────────┤ 20s
113
+ │ │
114
+ ▼ (Heartbeat @ 10s) │
115
+ 10s ├── In-place write + truncate (own fd) ────────────┤ 25s
116
+ ```
117
+
118
+ ### Release ordering
119
+
120
+ `release()` sequences **stop heartbeat → close fd → unlink**, and that ordering satisfies two unrelated constraints at once. A refactor can satisfy either while breaking the other, and neither break is visible on the other's platform:
121
+
122
+ 1. **Stop before unlink** — otherwise a tick firing between the unlink and the stop reads `ENOENT` and fires `onEvicted` on a lock that was released normally: a self-inflicted false eviction signal.
123
+ 2. **Close before unlink** — otherwise Windows leaves a pending-delete entry and the next `open(lockPath, 'wx')` fails `EEXIST` against a file that is logically already gone.
124
+
125
+ If the lease was already lost, `release()` skips the unlink entirely: the file at that path belongs to a successor now.
126
+
127
+ ---
128
+
129
+ ## 4. Safe Stale & Orphaned Lock Eviction Protocol
130
+
131
+ When an agent encounters an existing lockfile, it must verify whether the lock is actively held or orphaned.
132
+
133
+ **Eviction is detection, not prevention.** Step 6 narrows the decide-then-break window; it does not close it. Two processes on a POSIX filesystem cannot make "decide stale" and "break" one atomic operation without a shared arbiter. The actual guarantee is weaker and worth stating plainly: an owner whose live lock is wrongly broken **learns within one heartbeat interval**, via renewal step 3.
134
+
135
+ ### Multi-Step Eviction Verification
136
+ 1. **Existence**: `stat` the lockfile. `ENOENT` ⇒ `{evicted: false, reason: "not_found"}`.
137
+ 2. **Unreadable-lock grace**: a 0-byte *or* unparseable-JSON lockfile is evicted only once `Date.now() - stat.mtimeMs > grace`, where `grace = max(1000, callerTtlMs ?? 10000)`. Otherwise `not_stale`. This is what closes the 0-byte creation window from §2 without a debounce.
138
+ 3. **Process liveness**: inspect `lock.pid`, and only if `lock.hostname` is absent or matches the local host. Send POSIX signal 0 (`process.kill(pid, 0)`); `ESRCH` means the holding process is dead.
139
+ 4. **Clock-skew clamp**: `effectiveHeartbeat = Math.min(lock.heartbeatAt, Date.now())`. **There is no skew tolerance constant** — a future-dated heartbeat is clamped to now, and liveness and TTL both still run against it.
140
+ 5. **Lease floor**: `effectiveTtl = Math.max(callerTtlMs ?? 0, lock.ttlMs ?? 10000)`. A contender may *lengthen* the grace it extends to a holder; it may never *shorten* the lease the holder recorded. The lease is a property of the owner.
141
+ 6. **Revalidate immediately before the break**: re-read the file and compare `lockId` and `heartbeatAt` against the values the staleness decision was made on. Any change ⇒ `{evicted: false, reason: "race_lost"}` — most often a stalled holder that resumed and renewed.
142
+ 7. **Atomic rename break protocol**:
143
+ - Competing evictors never call `fs.unlink()` directly, as multiple processes could race and unlink a newly acquired legitimate lock.
144
+ - The evictor renames the stale lockfile to a unique quarantine path: `.evict.<uuid>`.
145
+ - Exactly one evictor succeeds. A competing evictor receives `ENOENT` — or, on Windows, `EPERM`/`EBUSY` because another process still holds the file open — and reports `reason: "race_lost"` in either case.
146
+ - The winning evictor immediately unlinks the quarantine file, and the caller retries acquisition.
147
+
148
+ #### Why step 2 discriminates at all
149
+
150
+ The grace tells crash debris apart from a live holder **only because renewal writes in place and therefore refreshes the file's `mtime` on every tick**. A live holder's unreadable moment is always young relative to `mtime`; genuine debris ages without bound because nothing writes to it. Do not make renewal lazy, conditional, or optional without re-deriving this check — it silently degrades into "evict anything unreadable after 1 s".
151
+
152
+ Step 2 also cannot apply step 5's protection: there is by definition no parseable `ttlMs` to defend. When a contender passes a short TTL, **the 1000 ms floor is doing all of the safety work**, and a holder stalled mid-write for more than 1 s is evictable regardless of the lease it recorded. The window being protected is a single `write()` on an already-open fd, three orders of magnitude under the floor — a real ceiling, not a proof.
153
+
154
+ ### Clock skew, stated as a change of direction
155
+
156
+ Earlier revisions of this guide documented a `+60000 ms` skew tolerance and said a future-dated lock was "treated as invalid". The code did neither: it returned `not_stale` for anything more than 1 s in the future, which made every future-dated lock **immortal — including one whose holder was provably dead**. Both the tolerance and that early return are gone. Step 4 clamps instead, so no number survives to be tuned.
157
+
158
+ ---
159
+
160
+ ## 5. State Partitioning vs. Locking in BDB Agent OS
161
+
162
+ The BDB ecosystem balances coarse-grained locking with fine-grained partition isolation:
163
+
164
+ ```
165
+ +---------------------------------------------------------------------------------------+
166
+ | Concurrency Strategy Selection |
167
+ +-------------------------------------------+-------------------------------------------+
168
+ |
169
+ ┌────────────────────────────────┴────────────────────────────────┐
170
+ ▼ ▼
171
+ ┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐
172
+ │ Parallel Build Fan-Out │ │ Shared Mutable Resources │
173
+ │ (Engineering, UI/UX, Media Nodes) │ │ (state.json, git branches, worktree)│
174
+ ├──────────────────────────────────────┤ ├──────────────────────────────────────┤
175
+ │ Strategy: State Partitioning │ │ Strategy: Distributed File Locking │
176
+ │ Each node writes isolated fragment: │ │ Wrap mutation in withStateLock, which│
177
+ │ production_artifacts/state.d/<id>.json│ │ serializes on the lock and writes the │
178
+ │ Followed by single merge agent │ │ STATE FILE via temp+rename │
179
+ └──────────────────────────────────────┘ └──────────────────────────────────────┘
180
+ ```
181
+
182
+ ---
183
+
184
+ ## 6. TypeScript API Usage Examples
185
+
186
+ ### Guarding Shared State Mutation
187
+ ```typescript
188
+ import { withStateLock } from 'bdb-cicd-resilience/locking/index.js';
189
+
190
+ await withStateLock(
191
+ 'production_artifacts/state.json',
192
+ async (state) => {
193
+ state.phase = 'build';
194
+ state.artifacts.backend = 'production_artifacts/02_backend_schema.md';
195
+ state.findings.push({ id: 'F-ENG-02', status: 'fixed' });
196
+ return state; // Automatically written back via atomic rename
197
+ },
198
+ { ttlMs: 15000, acquireTimeoutMs: 10000 }
199
+ );
200
+ ```
201
+
202
+ ### Resource Guard with Auto-Eviction
203
+ ```typescript
204
+ import { acquireLock } from 'bdb-cicd-resilience/locking/index.js';
205
+
206
+ const handle = await acquireLock('git_worktree_main', {
207
+ lockDir: '.git/locks',
208
+ ttlMs: 20000,
209
+ acquireTimeoutMs: 15000,
210
+ retryIntervalMs: 100,
211
+ });
212
+
213
+ try {
214
+ // Critical section
215
+ await performGitOperations();
216
+ } finally {
217
+ await handle.release();
218
+ }
219
+ ```
220
+
221
+ ### Inspecting a lock without taking it
222
+ `inspectLock(resource, { lockDir })` returns the parsed metadata, or `null`. Because renewal writes in place, a reader can catch a torn write, so `inspectLock` **re-reads once after ~5 ms** before answering `null`. An unreadable lock is not evidence of no lock — the same argument as the §4 eviction grace, applied to a public query.
223
+
224
+ ---
225
+
226
+ ## 7. Known limitations
227
+
228
+ These are load-bearing and deliberately not engineered away in this cycle. Do not design against a stronger guarantee than the ones listed here.
229
+
230
+ - **Fencing tokens are advisory.** No resource in this library verifies a token, and the `.seq` counter backing them is written with a plain `writeFile` whose failure is swallowed — it is **not crash-durable**. Tokens are also allocated *before* the `open('wx')` attempt, so every losing poll burns one. They are strictly increasing per process, non-decreasing across a run, and **must not be relied on for cross-host safety** until some resource actually validates them.
231
+ - **The default lock directory is `process.cwd()/.locks/`.** It follows the working directory of whichever process acquires, which is not the same thing as following the resource.
232
+ - **No re-entrancy.** A second acquisition of a path already held by the same process fast-rejects without touching the filesystem. That is correct for a mutex, but it means a sibling async task in the same process fails fast rather than queuing. Distinguishing "same logical caller re-entering" from "sibling task waiting" needs a caller identity that no consumer currently supplies.
233
+ - **`HeartbeatManager` is not part of the public barrel.** It requires the `lockId` and the fd from the acquiring `open('wx')`, neither of which a caller that did not acquire the lock can synthesise. That removes the *accidental* path to renewing someone else's lock, not the determined one — a caller can still open the path `r+` and copy the `lockId` out of the file. No file-based lock can prevent that.
234
+ - **The containing directory is not `fsync`'d** on acquisition (§2).
235
+ - **Windows behaviour is unverified.** The `EPERM`/`EBUSY` handling in the break protocol and in `release()` exists to be correct on Windows, but has never been executed on a Windows host.