LHST.AI  ·  dsh-org / technology

The engineering dev cycle & the mechanisms of effective development

request to human deploy, no shortcuts. This is how the org builds.

version 1.2 date 2026-10-11 owner CTO / technology skill org-development source state/content/technology/ · KB 1bce3d95
01 · foundation

Three iron rules

Everything below rests on these three. Break one and the rest of the machinery is decoration.

01

Worktree, never the live tree

You build in a worktree, on a branch. The deployed tree is read-only, and a detector enforces it: scripts/check-deployed-tree-pristine.mjs. Shallow boundary, moved HEAD, a changed tracked file, main drift: it goes red. The host never breaks "mysteriously".

02

Deploy is a human act

main reaches the host only through a human running ./scripts/orgctl deploy. orgctl hard-refuses up/restart/deploy/down under an agent identity (DSH_ORG_AGENT). A restart without a human is a finding, not a feature.

03

No direct pushes to main

Branch protection requires the CI gate. The owner keeps an admin bypass: a bypass is a named act, an incident with a register entry. It is never a habit, and a hotfix is never an implementation.

02 · process

The cycle in 8 stages (+ deploy)

Two surfaces, one split (D70). The queue (comm/) is exchange: handoffs between seats, only through comm_* tools. Kanban is durable objects: project, epics, stories, the branch's card. State lives in kanban; conversation lives in the queue. Never the other way round.

diagram 1 · the cycle end to end

flowchart TD
    REQ["1. Request - anyone
queue or card: the PROBLEM, not the fix"] --> ASSESS["2. control - assess
go / no-go + a one-paragraph reason"] ASSESS -->|"no-go → back"| REQ ASSESS -->|"go"| GOAL["3. product - business goal
goal + scope + acceptance criteria
project / epics / stories (kanban)"] GOAL --> SPLIT["4. pmo - split
tasks sized to one implementer
ordering + dependencies"] SPLIT --> IMPL["5. Implement - department
worktree: branch dept/card-short-id
NEVER the live tree"] IMPL --> QA["6. Test - technology (QA)
checks green ON THE BRANCH + qa-report.yml
+ task report dshorg.task-report/v1 (D73)"] QA --> REV["7. Review - quality-review + control
+ the seat for the path class (security/legal)"] REV -->|"findings → stage 5"| IMPL REV --> PR["8. PR to main
gate: verify + gitleaks + verdicts"] PR --> MERGE["Merge - agent, when CI is green
gh pr merge --auto"] MERGE --> DEP["9. Deploy - HUMAN ONLY
./scripts/orgctl deploy"] style MERGE fill:#1a2332,stroke:#ff6600,color:#e8eaed style DEP fill:#ff6600,stroke:#ff4500,color:#0f1824
Diagrams render with mermaid.js from a CDN - without a network you see the diagram sources.
  1. Request - anyone

    The queue or a card names the PROBLEM. Not the fix: the fix is downstream, and the person who names it owns the argument.

  2. Assess - control

    Go or no-go, one paragraph, in writing. A no-go returns to the requestor with the reason attached.

  3. Business goal - product

    Goal, scope, acceptance criteria. More than one task? Then project, epics, stories. If the criteria are fuzzy here, every later stage pays for it.

  4. Split - pmo

    Tasks sized to one implementer, ordered, dependencies named. A task two people could "share" is two tasks.

  5. Implement - department

    git worktree add repos/.worktrees/<card-short-id> -b <department>/<card-short-id>. One task, one branch. The branch name carries the department (D74, enforced in CI by check-branch-name.mjs): everyone commits under one git identity, so the branch name is the only attribution that exists.

  6. Test - technology (QA)

    Touched suites green on the branch. New behavior ships with its own check in both directions: pass AND refuse. A gate that only tests the happy path is an observation, not a gate. The ACs go to qa-report.yml; the stage ends with a dshorg.task-report/v1 report (D73).

  7. Review - quality-review + control + the seat for the path class

    Verdicts per criterion, against the validated task report. Findings go back to stage 5. "Looks good to me" is not a verdict; data is.

  8. PR - technology

    gh pr create, the checks.yml gate, then an agent merges when CI is green (gh pr merge --auto). The deploy stays human. That is stage 9, and no stage skips it.

03 · stage 5

Worktrees and carrying work

repos/.worktrees/<card-short-id>/
The default home of trees. Gitignored, inside the workspace (the sandbox clips writes outside /opt/dsh-org), survives a reboot. Your uncommitted work outlives your session here.
/tmp/dsh-wt-<card-short-id>
Legal scratch. Dies with the reboot, and so does whatever you left in it. ~/worktrees/<id> only under danger-full-access: an escalation, not a habit.
Uncommitted work
Does not survive a turn boundary. Commit WIP to YOUR branch, every time. Never stash into the live tree.
The D27 carrier
scripts/carry-uncommitted.mjs (technology) moves builders' uncommitted work onto a branch: one unit commit, explicit path list, authors named, baseline re-sealed. A path nobody owns is refused, on purpose.
Attribution
One git identity for everyone. The branch name is the only place attribution lives, hence D74: <department-id>/<card-short-id>, department from departments/*.yaml.
04 · stage 6

QA as a machine: the proof comes from a run, not from the implementer

A person claiming "tests pass" is a rumor with confidence. A run URL on the card is evidence. So the evidence is produced by the runner, not typed by the implementer.

diagram 2 · qa-report.yml - no model calls between the check and the evidence

sequenceDiagram
    participant QA as QA (technology)
    participant G as GitHub Actions (runner dsh-org)
    participant C as Named checks (bash+node)
    participant K as Kanban (Postgres)
    QA->>G: dispatch qa-report.yml - card_id + checks AC1=cmd, AC2=cmd
    G->>C: runs EVERY named check
    C-->>G: rc + output tail per criterion
    G->>G: deterministic report - ZERO model calls
    G->>K: addNote(card, report, author=ci-runner)
    Note over K: the run URL in the note = evidence
the implementer could not have typed
Dispatch
gh workflow run qa-report.yml -f card_id=<id> -f checks="AC1=<cmd>,AC2=<cmd>". The runner executes every named check and itself posts the report onto the card as ci-runner. No model call sits between the check and the evidence.
A plugin's tool surface
A card touching a plugin's lib/ or src/ must name node scripts/check-tool-manifests.mjs. A tool changing shape under the same name is a supply-chain event, and the manifest check is how you see it happen.
Staging tree
node scripts/staging.mjs up|status|env|down: a worktree on origin/main. The verify loop picks the tree through DSH_ORG_CHECK_ROOT (unset = deployed, staging = staging, otherwise a path). Every recorded run keeps repo_root/commit/dirty, so "which tree did you measure" always has an answer.
Task report (D73)
One JSON dshorg.task-report/v1 on the card: summary, criteria AC1..ACn (pass|fail|deferred + evidence ref), machineQa.runUrl, which must appear in a ci-runner note, residualRisks, notDone, selfVerdict. validate-task-report.mjs checks it in code; check-task-reports.mjs sweeps every card in review. A report the validator cannot parse is not a report.
Order: machine QA is a POST-MERGE confirmation (card 5041a7b0)
The workflow takes no ref: it always measures the default branch. Dispatch before the merge and you measure main before the change (measured on card eba0172c: two false FAILs, expensive lesson). The order: PR, verdicts, merge, dispatch, the ci-runner report, task report, close.
The checks-map contract: word-split, zero quoting (card 4e3ee456)
The runner executes a command by word splitting ($cmd without a shell, on purpose). Double quotes are literal characters: grep -c "pattern" file searches for the pattern including the quotes, a false FAIL. A pattern with a space splits the command. Write map commands without quotes: grep -c pattern docs/file.md.
05 · stage 7

Review: verdicts are data (card a3c8f952)

The whole org runs on one GitHub identity (wizardeur). GitHub refuses self-approval, so classic required approvals is mechanically impossible here. So we stopped asking reviewers for opinions and started demanding verdicts:

A verdict = a comment carrying JSON
dshorg.pr-verdict/v1: {seat, verdict: "approve", head_sha}. head_sha must be the PR's current head: an approval of an older commit is not an approval of this one. That detail has caught more sloppy reviews than any policy memo would.
A mechanism, not a sentence
scripts/check-review-verdict.mjs in the verify job refuses a PR without approve from the required seats naming the current head. A probe that cannot look ends rc 2 (could not look): an honest "I don't know", never a false certificate.
Path classes pull the seats
scripts/review-verdict-paths.json: data, not code (the gate reads the BASE version; the file names itself). security-review for vault/secrets, scanners, CI (.github/workflows/*), infra/**, acts/**, guard-floor/ops tools. legal-risk-review for LICENSE, Content/**. Touch the class, pull the seat.
Why verify
The gate rides the verify check, a known gating check. A new job is not in the required-check list, and the token gets a 403 reading the protection config. Ride what already gates.
Who may post a verdict: the seat Apps (card 5c81afe9)
Every seat has its own GitHub App (dsh-org-<seat>[bot], vault github/app/<seat>/id + key). scripts/lib/seat-identity.mjs mints a fresh installation token per run, never the owner's token. The gate matches the seat to the comment's author login (authorMatchesSeat), not the text: a verdict is who posted it, not what it claims. Verdicts from before the Apps read only inside a named, dated window (LEGACY_SELF_ASSERTED, expires 2026-11-01).
The Apps only POST
events: [] on all four (measured 2026-10-11). Nothing arrives through the Apps, so nothing can sneak in either. The Apps' descriptions are edited by the owner in the web UI: the REST API has no update-an-App endpoint, a fun fact that cost an afternoon to confirm.
06 · automation

GitHub automation (`.github/workflows/`)

WorkflowTriggerWhat it does
checks.ymlpull_request (+ catch-ups issue_comment / pull_request_review)The PR gate: the verify job (hermetic checks: syntax sweep, us-english, shared-protocols, canonical-lists, generate+task-routing, deps-pinned, branch name D74, review verdicts) + blocking gitleaks; the full job = the whole check-all.sh on the box
qa-report.ymlworkflow_dispatch (card_id + the AC→check map)Machine QA - the report lands on the card (author ci-runner)
security-sast.ymlpush to mainblocking gitleaks + informational semgrep (SARIF)
security-dast.ymlcron: Sunday 03:00 UTCtestssl + nuclei + ZAP passive against the auth gate :8080
merge-time-gate.ymlpush to mainmerge-time measurement; snapshots to the merge-time-gate/snapshots branch, never protected main

Tool supply chain and the merge metric: two mechanisms on top

The golden manifest (G11c pointed inward)
scripts/check-tool-manifests.mjs + tool-manifests.golden.json. Every plugin's tool-definition surface (entry point + every lib/src module holding tool registrations) is hashed into a committed golden. Change a tool's name, description or input schema and the hash moves: check-all fails BY NAME until someone deliberately re-freezes with --freeze --reason "...". No reason, no freeze; the thawed golden lands in the same commit. The pre-commit hook runs the check in --staged mode, so the refusal happens at your commit, not later in CI where you have already moved on. Each plugin carries its own append-only history: only a plugin whose hash moved earns an entry, so the trail stays a trail and not a second golden. Context: the 2025-26 supply-chain series (MCPTox, ShareLock, Deadbugz). A name allowlist answers the wrong question, because the attack lives in a tool's prose.
The merge-time gate and the check over silence (card f4454166)
merge-time-gate.yml records one row per merge to main under presets/control/register/merge-time-gate/ (snapshots to the merge-time-gate/snapshots branch, never protected main). This mechanism's natural defect is silence: a missing row looks exactly like a measurement that found nothing. Twelve runs went by before control went looking. check-merge-time-gate.mjs turns silence into an observable: every merge after the capability commit (located hermetically from the workflow's history) must carry a LIVE row (merge_commit = that merge, backfill ≠ true). The rollout lesson (card a5ff0513): a permissions block zeroes every unlisted scope, so the runner recorded an rc 2 as if it were a measurement, and pushing the row to protected main was refused with GH006. The writer trusts no token and no branch it was not handed.

diagram 3 · the CI pipeline - from trigger to deploy

flowchart LR
    subgraph T["Triggers"]
        PR["pull_request"] --> VERIFY["verify - the hermetic core
+ branch name (D74)
+ review verdicts (a3c8f952)"] PR --> GITLEAKS["gitleaks
secrets - blocking"] CMT["issue_comment /
pull_request_review"] --> VERDICT["verdict job
(catch-up after a comment)"] PUSH["push to main"] --> SAST["security-sast
blocking gitleaks + semgrep info"] PUSH --> MTG["merge-time-gate
merge-time snapshot"] CRON["cron: Sunday 03:00 UTC"] --> DAST["security-dast
testssl + nuclei + ZAP"] DISP["workflow_dispatch
card_id + checks"] --> QAR["qa-report"] end VERIFY --> M["merge - agent, when green
gh pr merge --auto"] GITLEAKS --> M M --> DEP["deploy - human
orgctl deploy"] style M fill:#1a2332,stroke:#ff6600,color:#e8eaed style DEP fill:#ff6600,stroke:#ff4500,color:#0f1824

Runner infrastructure

5× self-hosted runner (label dsh-org)
systemd, user gh-runner, host rx7900xtx.lhst.net. Parallelism = the runner count. We ran one runner for months and verify started 10 minutes after a push; nobody noticed because nobody measured. Now it is measured.
Joining
infra/ci-runner-add.sh. A human with admin mints the registration token: 1 h validity, never stored. A runner counts as OK only when its own log says Listening for Jobs, not when the script exits zero.
Toolchain
Per-step: setup-node 22 + pnpm pinned at 12.8.1. No step depends on the account's PATH, because an account is not a contract.
Branch protection
The verify check is required on main. The owner keeps an admin bypass: an incident is a named act. An agent merges --auto when green; the deploy and every gate bypass are human.
07 · law

Engineering rules - docs/engineering-rules/ (17 files)

verify-before-claim · measure on the live system test-both-directions · a gate passes when it should and screams when it should test-failures · a "pre-existing failure" does not exist pusta-sonda · an empty result questions the probe, not the system spec-is-not-delivery · a card closes on behavior llm-output-validation · parsed ≠ validated git-workflow security · secrets only in the vault docs-outlive-deleted-code config-in-settings-not-hardcoded derive-dont-restate intent-confidence context-hygiene memory-no-business-state coding-style code-in-english self-improvement

Check culture

CI and local run the same check-all.sh. They cannot drift apart, because a gate that behaves differently locally than in CI is two gates and you only wrote one. The PR gate is the hermetic core; the full suite runs on the box at QA. run_known2 absorbs only rc 2 (NOT OBSERVED, a documented limitation); rc 1 is always FAILURE. Suspending a red is a named act, never a silent one. Generated artifacts (MAP.generated.md, ACTS.generated.md) must be fresh: the generate check; check-generate.mjs also pins the ban on reaching the queue from outside the comm_* tools.

08 · configuration

The schema declares the key, the profile supplies the value

A plugin declares its zod/schemastery Config in its source. The deployment supplies values through a profile patch (cord.patch.yml). There is no third place, and "just this once" is the third place.

A second copy of a fact that has an authoritative home is a defect: a derivative or a named write-down, pick one. We have watched five failure shapes come out of this: a constant restating a declared set, an endpoint inside a check, an on-disk artifact treated as the record, a check that cannot run, blindness read as a pass. All five are in docs/DEVELOPMENT.md.
09 · skills

Which skill catches which stage

Stage / situationSkills
The whole org-code cycleorg-development
Stages 1-3: problem, options, architecturesolution-exploration, solution-architecture
Stage 4: split, ACs, a plan for a separate sessionimplementation-planning
Stage 5: branch / worktree / mergebranch-and-worktree-workflow
Stages 5-6: red-green, pinning both directionstest-driven-development
Diagnosis before a fixsystematic-debugging
Stage 7: review and responding to feedbackcode-review
Before "done" (verification instead of declaration)completion-verification
Agent fan-out without write collisionsparallel-agent-delivery
Interfaces / operability / release / debtapi-design, observability-and-reliability, release-and-deployment, technical-debt-management
Queue, config, runtime, decisions across departmentsorg-communication, org-config, org-ops, org-brainstorm / org-wonder
Continuity across sessions / keeping skills honesthandoff, skill-authoring
10 · limits

Where the guarantees stop

The limits are written down. Nobody should rediscover them in production.

The PR gate is the hermetic core. Part of the full suite structurally cannot pass on a fresh clone, so the full check runs on the box at QA. Semgrep is informational until tuned against real findings: a scanner that cries wolf trains you to ignore it. A self-hosted runner executes PR code on the deploy host. Acceptable for a private repo and a single collaborator; to revisit before an external collaborator joins.
The judge measures a tree, and you have to read which one. The default tree for the judge and card_check is the live tree: a merged, approved card can stay red until the human deploy (measured on card eba0172c, and it confused everyone for an hour). card_check runs record their tree (repo_root/commit/dirty) and the judge re-runs in the recorded tree. Since card f8e750e5 an explicit root may also be a tree from the repos/ plane: same origin remote, path inside repos/registry.json. Outside the plane or with a different origin: refused, reason named.
Inbound channels sit outside the systems map. GitHub's only ingress is the repo-level webhook (infra/webhooks/README.md: HMAC X-Hub-Signature-256, vault github/webhook_secret, the dsh-webhook-tunnel.service fallback). Status: designed, not deployed (2026-10-10: 503 on the profile). The seat Apps only POST (events: []); the Apps' descriptions are owner-UI only. Nothing arrives that the map did not invite.