diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f093519..9e07d64 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,14 +1,22 @@ -## Summary +## Reviewer brief -Brief description of the changes. +What single outcome does this contribution deliver, and why does it matter? -## Changes +## Outcome and Factfile -- ... +- Keyoku outcome: +- Contribution id: +- Factfile: +- Exact head SHA: + +## Human judgment still required + + ## Checklist -- [ ] Tests pass (`npm test`) -- [ ] Build succeeds (`npm run build`) -- [ ] New functionality has tests +- [ ] This is one coherent reviewer outcome; unrelated work is split or stacked +- [ ] Relevant behavior has evidence (test, screenshot, trace, report, or code tour) - [ ] Breaking changes are documented +- [ ] Declared outcome criteria pass for the exact proposed snapshot +- [ ] I read the Factfile limits and understand what passing does not claim diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0b4387..01b308a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,13 +8,18 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20, 22] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 20 + node-version: ${{ matrix.node-version }} cache: npm - run: npm ci - run: npm run typecheck - run: npm test - run: npm run eval # gate on muscle-memory retrieval quality, not just correctness + - run: npm audit --omit=dev diff --git a/.github/workflows/keyoku-proof.yml b/.github/workflows/keyoku-proof.yml new file mode 100644 index 0000000..b398206 --- /dev/null +++ b/.github/workflows/keyoku-proof.yml @@ -0,0 +1,45 @@ +name: Keyoku proof + +on: + pull_request: + workflow_dispatch: + +# Outcome probes execute the proposed repository revision. Keep this job +# read-only; GitHub's native PR review owns the human decision. +permissions: + contents: read + +concurrency: + group: keyoku-proof-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + factfile: + name: Keyoku / outcome proof + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 20 + cache: npm + - name: Install project dependencies + run: npm ci + - name: Build this source revision + run: npm run build + - name: Prove Keyoku's own GitHub outcome + id: proof + env: + KEYOKU_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before || github.sha }} + run: node dist/index.js proof ci github-proof-v1 --base "$KEYOKU_BASE_SHA" + - name: Attach the full Factfile + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: keyoku-factfile-${{ github.sha }} + path: .keyoku/contributions/${{ steps.proof.outputs.contribution_id }}/factfile.* + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d8d9e7b..358dbc3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,39 +12,45 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22 cache: npm registry-url: https://registry.npmjs.org # npm >= 11.5 is required for OIDC Trusted Publishing (tokenless + provenance). - - run: npm install -g npm@latest + - run: npm install -g npm@12.0.2 - run: npm ci - run: npm run typecheck - run: npm test - - run: npm run eval # quality gate: muscle-memory retrieval, not just correctness + - run: npm run eval + - run: npm audit --omit=dev - run: node scripts/preflight.mjs # release integrity: version single-sourced, changelog present, built artifact matches package.json (npm test already built dist/) # Publish path, in order of preference: # 1. npm Trusted Publishing (OIDC, tokenless, provenance) — configure once at # npmjs.com → package 'keyoku' → Settings → Trusted Publishing → add this repo # (Keyoku-ai/keyoku) + workflow 'release.yml'. Then this just works. # 2. NPM_TOKEN secret (Automation type) as a fallback. - # Idempotent (skips a version already on npm), and degrades to a loud warning - # instead of a red build when neither auth path is configured (maintainer then - # publishes via CLI). It never silently claims success. + # Idempotent (skips a version already on npm). Prereleases go to `next` so + # the v2 `latest` line remains a rollback boundary. Publish failures are + # real release failures; a green workflow must never mean "nothing shipped". - name: Publish to npm run: | VERSION=$(node -p "require('./package.json').version") - if npm view "keyoku@${VERSION}" version >/dev/null 2>&1; then - echo "keyoku@${VERSION} already on npm — skipping publish." - exit 0 + if [[ "${GITHUB_REF_NAME}" != "v${VERSION}" ]]; then + echo "::error title=Tag/version mismatch::Tag ${GITHUB_REF_NAME} does not match package v${VERSION}." + exit 1 fi - if npm publish --provenance --access public; then - echo "Published keyoku@${VERSION} with provenance." + if [[ "${VERSION}" == *-* ]]; then + DIST_TAG=next else - echo "::warning title=npm publish skipped::Could not publish keyoku@${VERSION} — enable Trusted Publishing for Keyoku-ai/keyoku (release.yml) at npmjs.com, or set the NPM_TOKEN secret. Until then publish from the maintainer CLI: npm publish --access public" + DIST_TAG=latest + fi + if npm view "keyoku@${VERSION}" version >/dev/null 2>&1; then + echo "keyoku@${VERSION} already on npm — skipping publish." exit 0 fi + npm publish --provenance --access public --tag "${DIST_TAG}" + echo "Published keyoku@${VERSION} with provenance on ${DIST_TAG}." env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 7e5b7ed..854349b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,23 @@ dist/ .DS_Store *.tmp *.tgz +.keyoku/runtime/ +.keyoku/contributions/ +.playwright-cli/ +output/ +.vitest-results.json +preview-factfile.html + +# Local product/market working papers. Public documentation belongs in the +# README, FACTFILE-STANDARD, GITHUB, SECURITY, and contributor guides. +docs/KEYOKU-PROOF-V1.md +docs/KEYOKU-CONTRIBUTION-GATE.html + +# Retired product explorations are kept locally for reference. Do not publish +# internal briefs or obsolete UI captures with the generic proof harness. +archive/experimental-control-plane/docs/ +docs/artifacts/factfile-human-review.png +docs/artifacts/keyoku-control-plane-desktop.png +docs/artifacts/keyoku-control-plane-mobile.png +docs/artifacts/keyoku-intervention-channel-desktop.png +docs/artifacts/keyoku-intervention-channel-mobile.png diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 0000000..9dbc15b --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,11 @@ +# Reviewed public-history false positives: synthetic credential-redaction tests +# and model identifiers. Fingerprint scoping preserves detection everywhere else. +5d57b56527daf6864c01a1834ce29b05719ec8e8:tests/activity.test.ts:curl-auth-header:154 +5d57b56527daf6864c01a1834ce29b05719ec8e8:tests/mcp-e2e.test.ts:curl-auth-header:92 +8a9db83a91a4df8ab4d2422a22f65f080c79e050:tests/activity.test.ts:generic-api-key:143 +8a9db83a91a4df8ab4d2422a22f65f080c79e050:tests/activity.test.ts:generic-api-key:149 +8a9db83a91a4df8ab4d2422a22f65f080c79e050:tests/activity.test.ts:curl-auth-header:146 +5d57b56527daf6864c01a1834ce29b05719ec8e8:VALIDATION-REPORT-2026-07-02.md:curl-auth-header:43 +66eab48d48f66892cbd2dc4a447f6181f31dd70c:packages/openclaw/src/init.ts:generic-api-key:288 +66eab48d48f66892cbd2dc4a447f6181f31dd70c:packages/openclaw/src/init.ts:generic-api-key:318 +1052ae1dcefda24aeb3dfb3880a17b7939bbb30e:packages/openclaw/src/init.ts:generic-api-key:306 diff --git a/.keyoku/architecture.yaml b/.keyoku/architecture.yaml new file mode 100644 index 0000000..cabad39 --- /dev/null +++ b/.keyoku/architecture.yaml @@ -0,0 +1,94 @@ +schemaVersion: keyoku.dev/architecture/v1alpha1 +projectId: keyoku +title: Keyoku proof and attention layer +updatedAt: 2026-08-15T18:35:00Z +components: + - id: contributors + label: People + coding agents + summary: Any human, harness, model, or CI process can produce a contribution. + layer: execution + icon: agent + view: { x: 40, y: 250 } + external: true + - id: outcome-contract + label: Outcome contract + summary: Repository-owned intent, constraints, proof claims, scope, and human criteria. + layer: source + icon: git + view: { x: 280, y: 70 } + owns: + - .keyoku/outcomes + - docs/FACTFILE-STANDARD.md + - id: repository-snapshot + label: Exact Git snapshot + summary: Base, head, committed diff, worktree digest, changed paths, and outcome history. + layer: source + icon: git + view: { x: 280, y: 300 } + owns: + - src/contribution.ts + - id: project-onboarding + label: One-command setup + summary: Project detection, starter outcome, and safe GitHub workflow generation. + layer: experience + icon: plug + view: { x: 280, y: 500 } + owns: + - src/project-profile.ts + - tests/project-profile.test.ts + - id: proof-evaluator + label: Proof evaluator + summary: Executes repository-defined observations, fails closed, and separates machine facts from judgment. + layer: proof + icon: proof + view: { x: 520, y: 190 } + owns: + - src/engine.ts + - src/probes.ts + - src/assert.ts + - tests/contribution.test.ts + - id: factfile + label: Factfile renderers + summary: Canonical JSON plus concise GitHub Markdown, detailed Markdown, HTML, and architecture SVG. + layer: proof + icon: keyoku + view: { x: 760, y: 190 } + owns: + - src/contribution.ts + - src/architecture.ts + - id: github + label: GitHub pull request + summary: Read-only Check summary and downloadable exact-revision proof artifact. + layer: experience + icon: git + view: { x: 1000, y: 70 } + external: true + - id: human-reviewer + label: Accountable reviewer + summary: Judges coherence, usability, maintainability, risk, and final acceptance. + layer: control + icon: human + view: { x: 1000, y: 330 } + external: true +relations: + - from: contributors + to: repository-snapshot + kind: changes source + - from: outcome-contract + to: proof-evaluator + kind: defines claims + - from: repository-snapshot + to: proof-evaluator + kind: binds exact scope + - from: project-onboarding + to: proof-evaluator + kind: installs workflow + - from: proof-evaluator + to: factfile + kind: emits evidence + - from: factfile + to: github + kind: attaches summary + - from: factfile + to: human-reviewer + kind: requests judgment diff --git a/.keyoku/outcomes/archive-abandoned-surfaces.yaml b/.keyoku/outcomes/archive-abandoned-surfaces.yaml new file mode 100644 index 0000000..7a58009 --- /dev/null +++ b/.keyoku/outcomes/archive-abandoned-surfaces.yaml @@ -0,0 +1,69 @@ +schemaVersion: keyoku.dev/outcome/v1alpha1 +id: archive-abandoned-surfaces +revision: 1 +title: Abandoned product surfaces leave the active build +objective: >- + Code and positioning that only support the abandoned Omnigent fleet-runner and regulated Outcome + Engine directions are recoverably archived, while shared verification, connector, security, + learning, and provenance primitives remain active and tested. +owner: + kind: human + id: keyoku-owner + name: Tye + role: accountable product owner +constraints: + - Archive rather than permanently delete historical implementation and tests. + - Remove archived code from compilation, packaging, CLI, MCP, and active product promises. + - Preserve provider-neutral connectors and autonomy approvals. + - Preserve all deterministic goal, probe, assertion, evidence, and workflow-learning behavior. +criteria: + - description: The active TypeScript source and tests contain no Omnigent-specific runtime dependency + probe: + kind: command + run: >- + sh -c "! grep -R -i -l omnigent src tests --include='*.ts' | grep -q ." + timeoutMs: 30000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + - description: Legacy source, dedicated tests, and positioning are present with recovery documentation + probe: + kind: command + run: >- + sh -c "test -f archive/legacy-omnigent/README.md && + test $(find archive/legacy-omnigent/src -name '*.ts' | wc -l) -eq 5 && + test $(find archive/legacy-omnigent/tests -name '*.ts' | wc -l) -eq 5 && + test -f archive/legacy-positioning/OUTCOME-ENGINE.md && + test -f archive/legacy-positioning/README.md" + timeoutMs: 30000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + - description: CLI and MCP no longer advertise archived fleet-runner commands or tools + probe: + kind: command + run: >- + sh -c "npm run build >/dev/null && + ! node dist/index.js help | grep -E 'omnigent|keyoku (run|converge|guardrails|connect)'" + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + - description: The complete active Keyoku suite passes after archival + probe: + kind: command + run: npm test + timeoutMs: 300000 + parse: text + assert: + path: exitCode + op: eq + value: 0 +createdAt: 2026-08-09T00:28:00Z +updatedAt: 2026-08-09T00:28:00Z diff --git a/.keyoku/outcomes/behavior-iteration-v1.yaml b/.keyoku/outcomes/behavior-iteration-v1.yaml new file mode 100644 index 0000000..6411a27 --- /dev/null +++ b/.keyoku/outcomes/behavior-iteration-v1.yaml @@ -0,0 +1,126 @@ +schemaVersion: keyoku.dev/outcome/v1alpha1 +id: behavior-iteration-v1 +revision: 2 +title: Keyoku governs AI product iteration toward observable behavior +objective: >- + A coding harness can use Keyoku to move one repository-owned product outcome through bounded, + exact-source proof rounds until evidence passes or a truthful stop condition requires a human. +owner: + kind: human + id: keyoku-owner + name: Tye + role: accountable product owner +constraints: + - Keyoku coordinates evidence and instructions but does not silently run an agent, accept work, push, or deploy. + - Every round is bound to the exact Git head and worktree digest. + - Human judgment remains separate from automated proof. + - Checkpoint retries are idempotent and conflicting replays fail closed. + - Usage is explicitly sourced and is never inferred from UI or chat messages. +scope: + include: + - src/iteration.ts + - src/iteration-cli.ts + - src/index.ts + - src/server.ts + - src/contribution.ts + - tests/iteration.test.ts + - tests/contribution.test.ts + - tests/e2e.test.ts + - tests/mcp-e2e.test.ts + - docs/ITERATION.md + - README.md + - CHANGELOG.md + - .keyoku/outcomes/behavior-iteration-v1.yaml + maxChangedFiles: 13 +criteria: + - description: The behavior-iteration state machine, limit rules, idempotency, and tamper detection pass together + probe: + kind: command + run: >- + sh -c "npx vitest run tests/iteration.test.ts && + npx vitest run tests/contribution.test.ts -t 'distinguishes a clean committed contribution'" + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: Deterministic tests exercise convergence, replay, no progress, cost ceilings, human review, and ledger tampering. + whyItMatters: The controller must stop truthfully and must not turn repeated or manipulated activity into proof. + code: + - path: src/iteration.ts + purpose: Implements the append-only protocol, exact-source rounds, limits, instructions, and replay rules. + - path: tests/iteration.test.ts + purpose: Exercises the behavior and adversarial boundaries in real temporary Git repositories. + artifacts: [] + - description: CLI and MCP expose one provider-neutral iteration protocol without an autonomous runner or acceptance tool + probe: + kind: command + run: >- + sh -c "npx tsx src/index.ts iterate help | grep -q 'Keyoku behavior iteration' && + npx vitest run tests/mcp-e2e.test.ts -t 'provider-neutral iteration protocol'" + timeoutMs: 180000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The public CLI and MCP catalog expose start, status, next, and checkpoint while excluding agent-run and auto-accept capabilities. + whyItMatters: Codex, Claude Code, and other harnesses can share the same loop without handing Keyoku undeclared execution authority. + code: + - path: src/iteration-cli.ts + purpose: Provides the human-operable local interface. + - path: src/server.ts + purpose: Provides the harness-neutral MCP interface. + - path: tests/mcp-e2e.test.ts + purpose: Asserts the public capability and absence boundaries. + artifacts: [] + - description: The complete active Keyoku suite passes with the iteration layer enabled + probe: + kind: command + run: npm test + timeoutMs: 300000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: All active Keyoku behavior passes in one clean-revision suite. + whyItMatters: The iteration layer must not regress Factfiles, Pulse, proof sessions, security, or retained compatibility behavior. + code: + - path: tests/ + purpose: Covers the complete active product surface. + - path: package.json + purpose: Defines the reproducible project gate. + artifacts: [] + - description: Strict TypeScript checking accepts the public event, state, CLI, and MCP contracts + probe: + kind: command + run: npm run typecheck + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: TypeScript validates the schemas and every consumer without an error. + whyItMatters: Agent adapters must receive one consistent iteration contract across CLI, MCP, and library use. + code: + - path: src/iteration.ts + purpose: Defines the exported schemas and derived state. + - path: src/index.ts + purpose: Exports the library contract and routes the CLI. + artifacts: [] +humanCriteria: + - id: useful-agent-direction + description: A coding agent receives enough evidence and constraint context to make the next bounded product change without hidden chain-of-thought + guidance: Inspect a failed-round instruction and confirm it names the objective, failed claim, reproduction path, source identity, constraints, and checkpoint contract. + - id: truthful-product-boundary + description: The product reads as an iteration and proof layer rather than an autonomous coding harness or auto-approval system + guidance: Inspect README, CLI help, MCP descriptions, and the behavior-iteration guide for conflicting authority claims. +createdAt: 2026-08-25T17:00:00.000Z +updatedAt: 2026-08-25T17:30:00.000Z diff --git a/.keyoku/outcomes/contribution-gate-pivot.yaml b/.keyoku/outcomes/contribution-gate-pivot.yaml new file mode 100644 index 0000000..9620464 --- /dev/null +++ b/.keyoku/outcomes/contribution-gate-pivot.yaml @@ -0,0 +1,189 @@ +schemaVersion: keyoku.dev/outcome/v1alpha1 +id: contribution-gate-pivot +revision: 4 +title: A maintainer can understand and decide an agent contribution +objective: >- + In one short reading, a maintainer can understand what was requested, what changed, who or what + did the work, which outcome claims have relevant evidence, what remains uncertain, and which + judgments still require an accountable human before the contribution is accepted. +owner: + kind: human + id: keyoku-owner + name: Tye + role: accountable product owner +constraints: + - Core use is free for public and private projects and can run locally without a hosted account. + - GitHub is the first distribution surface, but schemas and verification remain provider-neutral. + - A human remains accountable; agent identity, harness, and model are supporting provenance. + - Verification fails closed and never describes a failed or incomplete probe as proof. + - A Factfile is bound to an exact Git head plus worktree digest and states the limits of its claim. + - Existing user changes in keyoku-engine are preserved; removal requires a dependency audit. +criteria: + - description: A contribution produces portable JSON, Markdown, and HTML receipts tied to the exact source snapshot + probe: + kind: command + run: npx vitest run tests/contribution.test.ts -t "binds a passing Factfile to the exact repository snapshot" + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The workflow created all three receipt formats and rejected review or publication after the source changed. + whyItMatters: A maintainer can share the receipt while knowing exactly which code state it describes. + code: + - path: src/contribution.ts + purpose: Captures the repository digest, creates the receipts, and rejects stale review or publication. + - path: tests/contribution.test.ts + purpose: Exercises receipt generation, exact-snapshot binding, publication, review, acceptance, and stale-proof rejection. + artifacts: + - kind: screenshot + path: docs/artifacts/factfile-human-review.png + label: Human-first Factfile + caption: The rendered receipt leads with the requested outcome and pending human decisions. + - description: A failed automated observation leaves the claimed outcome visibly unproven + probe: + kind: command + run: npx vitest run tests/contribution.test.ts -t "reports evidence gaps and never treats a failed probe as proof" + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: A deliberately failing probe produced an evidence-gaps state instead of a green contribution. + whyItMatters: A broken or incomplete check cannot be presented to a maintainer as successful proof. + code: + - path: src/contribution.ts + purpose: Converts failed automated observations into the evidence_gaps gate state. + - path: tests/contribution.test.ts + purpose: Creates a failing command and asserts that the Factfile remains unproven. + artifacts: [] + - description: Required human judgments remain pending until an identified person records a decision + probe: + kind: command + run: npx vitest run tests/contribution.test.ts -t "keeps required human judgments separate from automated proof" + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: Passing commands left the contribution in human-review-required until a named person judged the declared question. + whyItMatters: Software cannot silently convert test output into a human opinion about clarity, usefulness, or readiness. + code: + - path: src/contribution.ts + purpose: Tracks automated state and human verdicts independently. + - path: tests/contribution.test.ts + purpose: Proves an agent cannot submit the human verdict and acceptance cannot happen early. + artifacts: [] + - description: Agent provenance records the exact harness and model while a human remains accountable + probe: + kind: command + run: npx vitest run tests/contribution.test.ts -t "keeps required human judgments separate from automated proof" + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The contribution records Tye as accountable owner and Codex with the exact gpt-5.6-sol model identity. + whyItMatters: Reviewers can distinguish who is responsible from which agent and harness performed the work. + code: + - path: src/contribution.ts + purpose: Defines first-class human and agent actors, including owner, harness, and model fields. + - path: tests/contribution.test.ts + purpose: Asserts that the exact harness, model, and human owner survive into the contribution. + artifacts: [] + - description: Credential-shaped probe output is removed before any receipt becomes shareable + probe: + kind: command + run: npx vitest run tests/contribution.test.ts -t "redacts credential-shaped evidence before creating shareable files" + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: A probe containing a credential-shaped value produced only redacted JSON, Markdown, HTML, and publishable data. + whyItMatters: Evidence should be safe to review and share without leaking the secrets it encountered. + code: + - path: src/contribution.ts + purpose: Redacts credential-shaped keys and strings before evidence reaches any Factfile format. + - path: tests/contribution.test.ts + purpose: Injects a fake API key and confirms the original value appears nowhere in the published receipt. + artifacts: [] + - description: Existing Keyoku behavior remains compatible with the contribution-gate change + probe: + kind: command + run: npm test + timeoutMs: 300000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: All 320 pre-existing and contribution-gate tests completed successfully after the pivot. + whyItMatters: The new contribution workflow does not require sacrificing the Keyoku behavior people already rely on. + code: + - path: tests/ + purpose: Covers the existing product plus the new repository-local contribution workflow. + - path: package.json + purpose: Defines the complete build-and-test command used for this compatibility check. + artifacts: [] + - description: The implementation remains internally consistent under strict type checking + probe: + kind: command + run: npm run typecheck + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: TypeScript accepted the full source tree without type errors under the repository's strict configuration. + whyItMatters: The evidence and review data structures agree across parsing, gate execution, rendering, and CLI use. + code: + - path: src/contribution.ts + purpose: Implements the portable outcome, contribution, evidence, review, and Factfile types and workflows. + - path: tsconfig.json + purpose: Defines the compile-time rules used by the consistency check. + artifacts: [] + - description: Installed production dependencies have no known npm audit vulnerability + probe: + kind: command + run: npm audit --omit=dev --audit-level=high + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: npm reported no known high-severity vulnerability in the installed production dependency graph. + whyItMatters: The receipt should disclose dependency risk rather than imply that passing product tests cover it. + code: + - path: package.json + purpose: Declares the production dependency surface assessed by npm audit. + - path: package-lock.json + purpose: Pins the exact dependency versions covered by this observation. + artifacts: [] +humanCriteria: + - id: one-minute-comprehension + description: A maintainer can explain the request, change, evidence, uncertainty, and next decision after one short reading + guidance: Read only the main report first. Raw commands or repository inspection should not be necessary. + - id: proof-relevance + description: Each claim is taught through relevant artifacts and code context rather than raw assertion values + guidance: Expand the evidence cards. The meaning should be understandable without opening the raw audit details. + - id: visual-clarity + description: The report's hierarchy makes the decision easier without dashboard theater or unexplained metrics + guidance: Inspect the desktop report, then narrow the window and confirm the reading order remains clear. +createdAt: 2026-08-09T00:06:21Z +updatedAt: 2026-08-09T20:50:00Z diff --git a/.keyoku/outcomes/github-proof-v1.yaml b/.keyoku/outcomes/github-proof-v1.yaml new file mode 100644 index 0000000..ce98b60 --- /dev/null +++ b/.keyoku/outcomes/github-proof-v1.yaml @@ -0,0 +1,219 @@ +schemaVersion: keyoku.dev/outcome/v1alpha1 +id: github-proof-v1 +revision: 10 +title: Keyoku turns agent work into a live, reviewable proof session +objective: >- + A maintainer can add free Keyoku to a Git project, see what connected agents are doing, resolve + genuine blockers through durable instructions, review meaningful exact-revision evidence, and + share a Factfile that explains the outcome without replacing human acceptance. +owner: + kind: human + id: keyoku-owner + name: Tye + role: accountable product owner +constraints: + - Local and GitHub proof works for public and private repositories without a hosted account. + - One contribution represents one coherent reviewer outcome; unrelated work should be split or stacked. + - GitHub proof execution remains read-only and never gives untrusted pull-request code a write token. + - Outcome revisions and definitions remain repository-owned Git history. + - Passing claims are bounded to declared observations and never presented as universal correctness or security. + - Archived control-plane research remains recoverable but outside the active build, MCP tools, tests, and launch promise. +criteria: + - description: One command detects representative project types and installs a safe proof workflow + probe: + kind: command + run: npx vitest run tests/project-profile.test.ts -t "cross-project proof setup" --cache=false --configLoader runner + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: >- + The initializer recognized Node.js, Python, Rust, Go, and generic Git fixtures and generated + repository-owned outcomes plus a read-only GitHub workflow. + whyItMatters: Adoption starts with a useful first Factfile in minutes, not a new hosted platform or agent migration. + code: + - path: src/project-profile.ts + purpose: Detects project conventions and generates starter checks and GitHub setup. + - path: tests/project-profile.test.ts + purpose: Exercises every supported project profile and the one-command installation contract. + artifacts: [] + - description: Pull-request proof covers committed base-to-head changes and fails closed on scope drift + probe: + kind: command + run: npx vitest run tests/project-profile.test.ts -t "Git-native contribution history" --cache=false --configLoader runner + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: >- + Tests create real Git histories, bind contributions to the pull-request base, include committed + changes, and reject paths outside a declared review boundary. + whyItMatters: A clean CI worktree must not produce an empty or misleading change scope. + code: + - path: src/contribution.ts + purpose: Captures committed and uncommitted source, exact digests, scope boundaries, and stale-proof rules. + - path: tests/project-profile.test.ts + purpose: Verifies real base-to-head Git behavior and scope failure semantics. + artifacts: [] + - description: The Factfile separates blockers from contextual next directions and proof + probe: + kind: command + run: npx vitest run tests/contribution.test.ts --cache=false --configLoader runner + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The reviewer sees genuine blockers separately from evidence-grounded next directions, their outcome effects, deeper context, proof, and ordered review path. + whyItMatters: Developers need to orient in seconds and then answer every deeper question without trusting an agent summary or a green badge. + code: + - path: src/contribution.ts + purpose: Renders compact GitHub Markdown and the complete human-readable HTML Factfile from one canonical snapshot. + artifacts: + - kind: screenshot + path: docs/artifacts/keyoku-factfile-current.png + label: Live proof session at a glance + caption: Desktop capture showing an outcome-complete Factfile with agent-prepared next directions and their evidence-bounded consequences. + annotations: + - label: Completion becomes a choice, not an empty prompt + detail: The agent recommends a bounded next move while preserving alternative and custom directions. + x: 24 + y: 29 + - label: Consequences are visible before steering + detail: Each path states how the outcome changes; evidence basis and tradeoffs remain one disclosure away. + x: 63 + y: 55 + - kind: video + path: docs/artifacts/keyoku-live-decision.webm + label: Human decision reaches an agent + caption: Short real-browser recording of a recommended choice becoming a durable queued instruction. + annotations: + - label: Review the decision context + detail: The human sees intent, blocker, ownership reason, and no-response consequence. + atMs: 0 + - label: Send the bounded choice + detail: The live session appends a decision and an instruction; it does not rewrite prior proof. + atMs: 3000 + - description: The two-way protocol is durable, provider-neutral, and token-scoped + probe: + kind: command + run: npx vitest run tests/proof-session.test.ts --cache=false --configLoader runner + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: Real integration tests report agent work, propose context-rich next directions, request and resolve a human decision, deliver and acknowledge instructions, reuse one active contribution, and reject an untokened live-session request. + whyItMatters: The interface must change agent behavior through a durable protocol, not merely display a dashboard that drifts from execution. + code: + - path: src/proof-session.ts + purpose: Defines the append-only provider-neutral work, direction, decision, instruction, acknowledgement, and presence protocol. + - path: src/session-server.ts + purpose: Serves the loopback-only token-scoped interactive Factfile and converts human actions into protocol events. + - path: src/server.ts + purpose: Exposes the protocol to Codex, Claude Code, OpenHands, custom agents, and other MCP clients. + - path: tests/proof-session.test.ts + purpose: Proves the end-to-end human choice to agent instruction path and session security boundary. + artifacts: [] + - description: Outcome definitions have inspectable repository-owned version history + probe: + kind: command + run: npx vitest run tests/project-profile.test.ts -t "binds committed PR changes" --cache=false --configLoader runner + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: A committed outcome revision is recovered through Git and shown with its commit, author, time, subject, and revision number. + whyItMatters: Teams can audit how definition of done evolved without trusting a second Keyoku database. + code: + - path: src/contribution.ts + purpose: Reads the canonical outcome history directly from Git. + - path: src/index.ts + purpose: Exposes keyoku outcome history as a human-readable command. + artifacts: [] + - description: Abandoned control-plane surfaces are outside the active product while reusable proof primitives remain + probe: + kind: command + run: >- + sh -c "test -f archive/experimental-control-plane/README.md && + test ! -f src/project-state.ts && test ! -f src/presentation.ts && + ! grep -R -E 'project_orient|intervention_create|agent_session_heartbeat|view_publish' src --include='*.ts'" + timeoutMs: 30000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The live control-plane prototype is recoverably archived and no longer compiled, registered over MCP, tested, or promised as V1. + whyItMatters: A narrow product earns adoption faster and avoids duplicating coding harnesses and GitHub. + code: + - path: archive/experimental-control-plane/README.md + purpose: Records the archived scope, reason, and rule for selective recovery. + - path: src/server.ts + purpose: Keeps the active MCP surface focused on outcomes, evidence, review, and architecture. + artifacts: [] + - description: Verification runs against a sealed checkout and rejects source mutation + probe: + kind: command + run: npx vitest run tests/source-capsule.test.ts --cache=false --configLoader runner + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: Capsule tests bind tracked and nonignored source bytes, reject writes and mutate-restore attacks, detect concurrent original-source changes, and clean every disposable checkout. + whyItMatters: A passing repository probe cannot silently rewrite the source it is being asked to prove. + code: + - path: src/source-capsule.ts + purpose: Captures exact source, materializes a sealed checkout, monitors mutations, and verifies freshness. + - path: tests/source-capsule.test.ts + purpose: Exercises writes, deletes, mode changes, symlink attacks, mutate-restore attempts, timeouts, and concurrent source changes. + artifacts: [] + - description: The active TypeScript implementation remains internally consistent + probe: + kind: command + run: npm run typecheck + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: Strict TypeScript checking accepts the project-profile, Git history, scope, renderer, CLI, and gate contracts together. + whyItMatters: The generated workflow and canonical Factfile shapes must agree across the public API and CLI. + code: + - path: tsconfig.json + purpose: Defines the strict static consistency rules. + artifacts: [] +humanCriteria: + - id: five-minute-value + description: A developer can reach a useful first Factfile in under five minutes and knows what to customize + guidance: Start from the README in a representative repository and note every point requiring prior Keyoku knowledge. + - id: github-review-clarity + description: The first Factfile screen makes the next decision, supported claims, unknowns, and review path understandable in under ten seconds + guidance: Look only at the first viewport, then explain what is established, what is not established, and what action belongs to the reviewer. + - id: complete-drill-down + description: A developer can drill from any supported claim to useful artifacts, relevant code, a reproduction instruction, and exact verifier details + guidance: Open one claim without reading the outcome YAML and reproduce its observation; confirm missing evidence is labeled rather than implied. + - id: focused-launch + description: The public product reads as a focused proof layer rather than a generic agent control plane, memory system, or certification claim + guidance: Inspect the README, CLI help, package description, MCP instructions, and archived surfaces for conflicting promises. +createdAt: 2026-08-15T18:30:00Z +updatedAt: 2026-08-27T03:44:20Z diff --git a/.keyoku/outcomes/review-ready-change.yaml b/.keyoku/outcomes/review-ready-change.yaml new file mode 100644 index 0000000..24d11df --- /dev/null +++ b/.keyoku/outcomes/review-ready-change.yaml @@ -0,0 +1,69 @@ +schemaVersion: keyoku.dev/outcome/v1alpha1 +id: review-ready-change +revision: 1 +title: A reviewer can confidently decide this change +objective: The proposed Keyoku change is understandable, bounded, and supported by the repository's + own executable checks. +owner: + kind: human + id: repository-owner + name: Repository owner + role: accountable owner +constraints: + - One contribution represents one coherent reviewer outcome; split unrelated work. + - Passing checks support only their declared claims and do not replace human review. + - Evidence must describe the exact Git revision under review. +criteria: + - description: The project test suite passes + probe: + kind: command + run: npm run test + timeoutMs: 300000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The project test suite passes at the exact revision shown in this Factfile. + whyItMatters: The behavior covered by the repository's tests still works at this exact revision. + code: [] + artifacts: [] + - description: Static type checks pass + probe: + kind: command + run: npm run typecheck + timeoutMs: 300000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: Static type checks pass at the exact revision shown in this Factfile. + whyItMatters: The change remains consistent with the project's declared type contracts. + code: [] + artifacts: [] + - description: The production build succeeds + probe: + kind: command + run: npm run build + timeoutMs: 300000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The production build succeeds at the exact revision shown in this Factfile. + whyItMatters: The repository can produce its declared build artifact from this revision. + code: [] + artifacts: [] +humanCriteria: + - id: coherent-review-unit + description: This contribution is one coherent outcome and the implementation is understandable + enough to own + guidance: Read the reviewer brief, inspect the changed areas and evidence, then review the diff + where judgment is still required. +createdAt: 2026-08-19T02:22:31.503Z +updatedAt: 2026-08-19T02:22:31.503Z diff --git a/.keyoku/policy.yaml b/.keyoku/policy.yaml new file mode 100644 index 0000000..06e18af --- /dev/null +++ b/.keyoku/policy.yaml @@ -0,0 +1,16 @@ +schemaVersion: keyoku.dev/policy/v1alpha1 +projectId: keyoku +distribution: + license: MIT + publicProjects: free + privateProjects: free + selfHosted: true +proof: + failClosed: true + exactSnapshotBinding: true + humanAcceptanceRequired: true + generatedEvidenceCommittedByDefault: false +privacy: + publishRawAgentTranscripts: false + publishSecrets: false + publishEvidenceSummary: true diff --git a/.keyoku/project.yaml b/.keyoku/project.yaml new file mode 100644 index 0000000..247d95b --- /dev/null +++ b/.keyoku/project.yaml @@ -0,0 +1,8 @@ +schemaVersion: keyoku.dev/project/v1alpha1 +id: keyoku +name: Keyoku +summary: Free, provider-neutral continuous proof for human-owned software contributions. +repository: https://github.com/Keyoku-ai/keyoku.git +defaultBranch: main +createdAt: 2026-08-09T00:06:21Z +updatedAt: 2026-08-09T00:06:21Z diff --git a/CHANGELOG.md b/CHANGELOG.md index 402d193..9ed9890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,159 @@ # Changelog +## 3.0.0-alpha.1 — 2026-08-25 + +Unreleased candidate for Keyoku's narrow, local, Git-native proof and attention +layer around coding agents. This entry does not claim that the package has been +published or that npm `latest` has moved from v2. + +### Added + +- A durable provider-neutral session protocol for agent work, structured human decisions, queued instructions, acknowledgements, and leased presence. +- A token-scoped loopback UI with **Agent work**, **Needs you**, **Review first**, and claim-by-claim **Proof** surfaces. +- Active contribution reuse per branch and outcome, plus content binding for the exact outcome contract. +- Annotated screenshot and timestamped video evidence in portable Factfiles. +- `keyoku proof serve`, five bounded MCP proof-session tools, seven bounded Pulse tools, one side-effect-free assurance evaluator, and a Marketplace-compatible composite GitHub Action. +- Exact-digest Factfile verification plus `keyoku factfile inspect|verify|assess|publish`. +- Harness-neutral Pulse lifecycle events, verified checkpoints, deterministic dispatch planning, and stakeholder/developer/timeline/email/text/JSON projections. +- An optional neutral `EvidenceProvider` and WorkEvent bridge; callers retain runtime orchestration and assurance-profile policy. +- A SHA-256 source capsule for command-backed Factfile criteria, materialized into one fresh disposable Git checkout per criterion with dirty/untracked bytes, paths, modes, symlinks, mutation detection, original-source race rejection, and validated cleanup. + +### Changed + +- Factfiles now present claim → observation → meaning → limits → reproduction → relevant code and artifacts; raw assertions remain audit detail. +- The launch promise is intentionally narrow: Keyoku coordinates proof and human attention without replacing GitHub, coding harnesses, or project-management systems. +- The v3 package entrypoint now exposes only `proof`, `factfile`, `pulse`, `serve`, `doctor`, `version`, and help. Goals, workflows, connectors, activity recording, memory, execution, and behavior-iteration commands remain compatibility source and are excluded from the v3 archive. +- Synthetic and adapter-attested checkpoints remain visibly attested and nondispatchable. Local promotion rechecks Factfile project/outcome/source identity, rejects symlinked or signature-mismatched media, and public adapter ingestion cannot self-claim local verification. +- Repository identity uses fail-closed Git calls and NUL-delimited paths; gates bracket probes, evidence resolution, and persistence with source captures so checkout mutation fails closed. +- Repository commands are now explicitly read-only proof observations. Writes, additions, deletions, mode changes, and mutate-restore behavior reject proof; this evidence isolation does not claim OS sandboxing of arbitrary repository code. + ## Unreleased +> The entries below record work on the v2 compatibility lineage. They are not +> v3 public commands or MCP tools unless a later release explicitly promotes +> them into the checked `docs/PUBLIC-SURFACE.md` inventory. + +### Added +- **Compatibility behavior iteration: `keyoku iterate` plus four MCP tools.** A provider-neutral, + bounded prove → repair → re-prove protocol now turns failed repository-owned + claims into deterministic agent instructions and re-evaluates only at an + idempotent checkpoint. Each round records the exact Git/worktree identity, + Factfile digest, passing/failing/regressed claim indexes, declared human-review + state, explicitly sourced token/cost usage, and stop reason in a hash-chained + append-only ledger. The controller stops on success, human judgment, failed + human review, no source progress, or configured round/time/token/cost limits. + It deliberately does not run an agent, infer billing, fill human decisions, + accept a contribution, push, or deploy. See `docs/ITERATION.md`. + +- **Demo evidence: `keyoku demo `.** A generic, project-agnostic + "record -> watch -> gate" workflow that makes a recorded product demo + first-class Keyoku evidence (the existing `EvidencePresentationSchema` + `artifacts` already supported `kind: "screenshot"/"video"`; this adds the + workflow that actually produces and validates them). `keyoku demo init` + writes a commented `.keyoku/demo.yaml` template (won't overwrite an + existing one) plus a ready-to-paste outcome criterion snippet. `keyoku demo + record` reads/validates that config, launches Chromium via Playwright + resolved from the *target* project (not a keyoku dependency — clear error + if `playwright` isn't installed there), walks each declared "stop" + (optional auth once, then goto -> actions -> settle -> screenshot), and + writes `.keyoku/demo/frames/*.jpeg` + `.keyoku/demo/manifest.json`. `keyoku + demo watch [--assert]` composes a prompt from the manifest's frames and + per-stop `expect` assertions, spawns `claude -p ... --permission-mode + acceptEdits` to review the frames and run a UI/UX audit, validates the + resulting `.keyoku/demo/verdict.json` against a zod contract, and with + `--assert` exits 0 only when `overall.verdict === "pass"` AND the verdict + is newer than the manifest — usable directly as an outcome criterion probe + (`keyoku demo record && keyoku demo watch --assert`) in any project. See + `docs/demo-evidence.md`. + +- **ADR-35: `Goal.project`/`Goal.cwd` — the keyoku side of belay's cross-project + scoping fix.** belay's loop portfolio/proposals now scope by project to stop + goals bleeding across unrelated repos sharing one `~/.keyoku`; that read a + `project`/`cwd` off the goal row, but the goal record had no such field — + this adds it. + - **What cwd context is actually available to an MCP tool handler** (the + key finding): MCP does not hand a tool call the client's cwd — there is + no protocol-level "caller's cwd" param. The only two reliable signals are + (a) an explicit `cwd` argument the calling agent chooses to pass, and (b) + `process.cwd()` of the long-lived stdio server process itself, fixed at + the moment Claude Code spawned it (typically the project dir the session + started in). `goal_focus` already leaned on exactly this — `cwd` optional, + defaulting to `process.cwd()` — so that's the established, trusted + convention this change follows for `goal_create` too, rather than + inventing a new mechanism. + - **`goal_create` gains an optional `cwd` param**, defaulting to the + server's `process.cwd()` when omitted — so every newly created goal is + stamped going forward, not just focused ones. Stored as two fields: + `Goal.cwd` (the raw dir) and `Goal.project` (the git repo root of that + dir, or the dir itself outside a repo — `never-throw`, via + `projectForCwd()` in `engine.ts`) — repo-root normalization means a goal + created from any subdir of a monorepo checkout lands on the same + `project` value. + - **`goal_focus` backfills `project`/`cwd`** on a goal that doesn't have + them yet, from the focus `cwd` (itself already optional-with-a- + `process.cwd()`-default on that tool). **First stamp wins** — focusing an + already-stamped goal from a different directory never reassigns it, so a + shared/portfolio goal can't get bounced between projects by whoever + focuses it next. + - **Surfaced** in `goal_get` (full goal object) and `goal_list`/`goal_create` + responses (`goalSummary`, `project` only, when set) — as well as directly + in `goals.json`, which is how belay itself reads it. + - **Backward compat, stated plainly:** the ~97 goals that existed before + this field shipped have neither `project` nor `cwd` and are **NOT** + retroactively scoped — there is no backfill migration, because inferring + a project from a goal's free-text objective/activity would produce false + positives that are worse than "unscoped." An old goal becomes scopeable + only once it is re-focused (`goal_focus`) from a real cwd, or recreated. + belay-side scoping logic must treat an absent `project`/`cwd` as + "unknown," not "global." +- **B2: edit a goal's criteria in place.** `goal_update` gains `addCriteria` / + `removeCriteriaIds` / `editCriteria`, so a wrong or incomplete criterion no + longer forces creating a whole new goal (which was fragmenting the loop + portfolio into duplicate `-v2`-style goals with their own, disconnected + learned workflow). `addCriteria` appends new criteria (ids continue the + goal's `c` sequence, never colliding with survivors after a removal); + `removeCriteriaIds` drops criteria by id; `editCriteria` patches an + existing criterion's `description`/`probe`/`assert` by id — fields left + out of the patch are preserved. Criteria not referenced by any of the three + pass through completely unchanged (verified by identity-equality in tests, + not just value equality). Backward compatible: a `goal_update` call with + none of the three new params behaves byte-for-byte as before (same patch + application, same response shape — the redacted `criteria` array is only + added to the response when a criteria edit actually happened). + - **Re-validated on every edit**, through the same gate `goal_create` uses: + at least one criterion must remain, and any `mcp` criterion (added or + edited-in) must reference a connector that's actually registered. A + rejected edit never partially applies — the goal's criteria are left + exactly as they were. + - **Converged-goal guard, safe default (no force flag):** editing criteria + on a `converged` goal reopens it — to `active`, or to `blocked` if its + iteration budget is already exhausted — mirroring the existing + drift-detection auto-reactivation in `assess()`. Rationale: a + `converged` status is a proof that criteria held; changing the criteria + invalidates that proof, so leaving the status untouched would be a + second silent-false-convergence hole right next to the one closed in + 2.18.0. No flag is needed to opt into the safe behavior — the harness + never leaves a goal claiming a convergence it hasn't re-verified. + - **The edit lands in the goal's trace/history** (visible via `goal_get`) + as a new `source:"system"` `ActionRecord` — distinct from `"recorded"` + (an explicit `goal_record` corrective action) and `"activity"` (live + capture). It does NOT spend the corrective-action iteration budget, and + it is excluded from workflow-step promotion and the `totalActions` stat + — a criteria edit is bookkeeping about the goal's definition of done, not + a reusable action toward it, so it must not pollute a learned workflow's + steps. + - Server instructions (`PROTOCOL`) and `goal_update`'s tool description + updated — the old "criteria are IMMUTABLE after goal_create... make a + NEW goal to change them" guidance is now actively wrong and has been + replaced with guidance to refine in place instead. + +### Changed +- **Command probe `timeoutMs` cap raised from 5 minutes to 15 minutes + (300,000ms -> 900,000ms)** in `CommandProbeSchema`/`HttpProbeSchema` + (`src/types.ts`) — real frontend production builds (and the new demo + record/watch pipeline) routinely exceed 5 minutes, and the old cap made + those outcome checks structurally unable to declare an honest timeout. + ## 2.18.0 — 2026-07-02 Security + correctness hardening. A full-codebase adversarial validation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c980c24..04559b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,8 @@ # Contributing to Keyoku -Thanks for helping make Keyoku better. +Thanks for helping make agent-assisted software easier to review. Keyoku is a +generic, agent-neutral proof harness: contributors should not need a particular +model, coding agent, hosted account, or memory service. ## Development setup @@ -8,29 +10,58 @@ Thanks for helping make Keyoku better. git clone https://github.com/Keyoku-ai/keyoku.git cd keyoku npm install -npm run build # tsup → dist/ -npm test # builds, then runs the full vitest suite (incl. MCP e2e) -npm run typecheck # tsc --noEmit +npm run typecheck +npm test ``` -Node 20+ required. State during manual testing goes to `$KEYOKU_HOME` — set it -to a temp dir (`KEYOKU_HOME=/tmp/keyoku-dev node dist/index.js serve`) so you -don't pollute your real `~/.keyoku`. +Node 20+ is required. Use `npm run preflight` before opening a pull request. + +## Prove the outcome you changed + +The repository dogfoods Keyoku. Pick the smallest outcome that describes your +change, customize it when the definition of done changes, and generate a fresh +Factfile for the exact revision under review: + +```bash +npm run build +node dist/index.js outcome list +node dist/index.js proof run +``` + +If no existing outcome fits, create a repository-owned contract with +`node dist/index.js proof init`, then edit the generated YAML. A strong outcome: + +- describes one reviewer-sized result rather than an agent task; +- pairs every automated claim with why the evidence matters; +- asks a human only for decisions that cannot be reduced to a command; +- declares a path scope when the intended review boundary is known; and +- never treats an agent's confidence or a zero exit code as sufficient explanation. + +Screenshots, traces, reports, videos, and logs may support a claim. Keep them +small, redact private data, and attach only evidence that teaches a reviewer +something about the result. ## Project layout -- `src/server.ts` — MCP tool surface (the API) -- `src/activity.ts` — pattern detection over the activity stream -- `src/refine.ts` — optional SLM refinement of suggestions -- `src/executor.ts` — bash / mcp_call step execution -- `src/store.ts` — JSON-file persistence under `~/.keyoku` -- `src/index.ts` — CLI (`serve`, `init`, `record`, …) -- `tests/` — unit + end-to-end tests (e2e drives a real MCP stdio session) +- `src/contribution.ts` — outcome evaluation, revision binding, and Factfile renderers +- `src/architecture.ts` — deterministic architecture projection +- `src/project-profile.ts` — one-command project and GitHub workflow setup +- `src/index.ts` — CLI surface +- `src/server.ts` — agent-neutral MCP tools +- `.keyoku/` — this repository's versioned outcomes and project policy +- `docs/FACTFILE-STANDARD.md` — portable receipt contract +- `archive/` — retired implementations, excluded from the launch surface +- `tests/` — unit and end-to-end verification ## Pull requests -- Every behavior change needs a test. CI (typecheck + full suite) must pass. -- Keep PRs focused; explain *why* in the description, not just what. -- New MCP tools must be added to the tool-surface snapshot in `tests/e2e.test.ts`. +- Keep one coherent outcome per PR; use stacked PRs for independent outcomes. +- Add tests for behavior changes and keep typecheck plus the full suite green. +- Include the generated GitHub summary or Factfile artifact for reviewer context. +- Use native GitHub review for the accountable decision: approve the exact + revision, or request changes with a concrete next instruction. +- Update MCP tool-surface assertions in `tests/e2e.test.ts` when tools change. +- Never commit credentials, private prompts, customer data, local runtime state, + or internal product/market working papers. -By contributing you agree your contributions are licensed under the MIT license. +By contributing, you agree your contributions are licensed under the MIT license. diff --git a/README.md b/README.md index 09c1dcf..71eaa4d 100644 --- a/README.md +++ b/README.md @@ -3,180 +3,282 @@ - keyoku + Keyoku -

- The harness with muscle memory.
- Keyoku watches what you do in Claude Code, Cursor, or Codex, learns your patterns, and turns them into one-command workflows — automatically. -

+

Proof your coding agent's work—not its confidence.
+ One repository-owned outcome. Exact-revision evidence. A clear human decision.

-

- Get Started • - How It Works • - MCP Tools • - Architecture • - keyoku-engine -

- - [![npm](https://img.shields.io/npm/v/keyoku?label=keyoku&style=flat-square&color=6366f1)](https://www.npmjs.com/package/keyoku) + [![npm](https://img.shields.io/npm/v/keyoku?label=keyoku&style=flat-square&color=3159d9)](https://www.npmjs.com/package/keyoku) [![CI](https://img.shields.io/github/actions/workflow/status/Keyoku-ai/keyoku/ci.yml?style=flat-square&label=CI)](https://github.com/Keyoku-ai/keyoku/actions/workflows/ci.yml) - [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) - [![License: MIT](https://img.shields.io/badge/License-MIT-22c55e?style=flat-square)](LICENSE) + [![License: MIT](https://img.shields.io/badge/License-MIT-166a4a?style=flat-square)](LICENSE) -
+Keyoku is a free, local-first proof session between humans and coding agents. It turns a repository-owned definition of done into a live working view and a shareable **Factfile**: meaningful evidence, agent provenance, explicit limits, and an exact Git scope. + +**Factfile proves one checkpoint. Keyoku Pulse carries trusted progress across checkpoints.** Command claims run from one exact, content-addressed source capsule in fresh disposable checkouts; any source write rejects the proof. Pulse accepts typed events from any agent harness, reports only exact-source verified checkpoints, and renders founder, developer, timeline, email-safe, text, and JSON views from one digest. It never silently sends a message. + +**Keyoku is an optional assurance adapter, not an agent protocol or control plane.** A caller may submit a neutral, content-digested evidence envelope and receive a deterministic accepted, rejected, stale, or human-review-required result. The caller chooses whether to use no assurance, basic assurance, or Keyoku high assurance; the neutral work contract does not require Keyoku. See the [adapter contract](docs/ASSURANCE-ADAPTER.md). + +It works with Codex, Claude Code, Copilot, Cursor, OpenHands, custom agents, CI, or no agent at all. Keyoku does not run your agent and does not ask you to move source code off GitHub. + +> GitHub shows the diff. Keyoku shows whether the intended outcome is supported—and where a human still has to decide. + +## Try the v3 source alpha + +```bash +git clone https://github.com/Keyoku-ai/keyoku.git +cd keyoku +npm ci +npm link + +# Run the complete evidence-gap → human-review → stale-proof demo +keyoku proof demo --open -## Get Started +cd /path/to/your-project +keyoku proof init +``` + +`keyoku proof demo` creates a disposable Git repository and uses the real Keyoku +pipeline. It first records a failing evidence state, fixes the sample defect, +produces an exact-revision Factfile, and proves that review is rejected after +the source changes. It needs no account, model key, hosted service, or prepared +video. Pass `--dir ` if you want a predictable location to +inspect afterward. + +The npm `latest` tag still points to the v2 muscle-memory product during this +alpha cutover. The generated GitHub workflow pins `keyoku@3.0.0-alpha.1` and is +staged—not runnable from npm—until that exact candidate is separately approved +and published to the `next` dist-tag. Local source evaluation works now. -Install it once, then wire it up: +Customize the outcome without learning the full YAML schema: ```bash -npm install -g keyoku -keyoku init +keyoku proof customize review-ready-change \ + --objective "A user can complete checkout without losing their cart" + +keyoku proof customize review-ready-change \ + --check "npm run test:checkout" \ + --claim "Checkout completes end to end" \ + --why "This is the behavior being shipped" ``` -> A global install keeps keyoku on a durable path. Running `npx keyoku init` -> from the throwaway npx cache is refused — npm can evict that directory and -> break the hooks — so install globally first. +Run `keyoku proof customize review-ready-change` with no edit flags to see the current claims, human decisions, and copyable customization recipes. Outcome YAML remains portable and Git-owned; each meaningful customization increments its revision. -The init command wires everything automatically: +Keyoku detects Node.js, Python, Rust, Go, or a generic Git repository and creates: -1. **Registers the MCP server** — via `claude mcp add --scope user`, so Claude Code connects on next launch -2. **Installs the hooks** — activity recording (every Bash/Edit/Write/Read/MCP call), a session-start brief, and prompt-time practice injection -3. **Wires Codex too** — when `~/.codex` exists, the MCP server lands in `config.toml` automatically (same tools, same workflows) -4. **Stays local** — no cloud, no telemetry; state lives in `~/.keyoku` with the same file permissions as `~/.aws`. `keyoku pause` stops everything instantly. +```text +.keyoku/ +├── project.yaml +└── outcomes/ + └── review-ready-change.yaml # repository-owned definition of done +.github/workflows/ +└── keyoku-proof.yml # read-only PR proof check +``` -Restart Claude Code and keyoku is live. Then skip the cold start entirely: +Review the generated outcome contract, replace starter checks with behavior that matters to your project, and run it locally: ```bash -keyoku import # backfill months of history from your Claude Code transcripts +keyoku proof run review-ready-change ``` -Now ask your agent to run `workflow_suggest` — keyoku mines your real history immediately instead of waiting days for new activity. Approved workflows appear as native slash commands (MCP prompts), and `keyoku export ` bakes one into your repo as a Claude Code skill your whole team inherits. +The command prints a contribution id. Open its live session while an agent works: -## How It Works +```bash +keyoku proof serve +``` -**Without Keyoku:** you describe the same multi-step process to your agent every session. +The token-scoped loopback link opens automatically. It keeps four surfaces deliberately separate: -**With Keyoku:** you approve a workflow once, then run it with one command. The agent never has to rediscover it. +- **Agent work** — reported task status; useful for coordination, never treated as proof. +- **Needs you** — only decisions that genuinely block safe progress, with options, recommendation, and the cost of no response. +- **Direct** — optional, context-aware next directions with their expected outcome effect, deeper context, tradeoffs, and a custom path. +- **Review first** — deterministic risk and attention signals, not another model verdict. +- **Proof** — claim → observation → meaning → limits → reproduction → relevant code and content-bound artifacts. -### 1. Activity tracing — automatic +A choice in **Needs you** or **Direct** writes a durable instruction. Any MCP-connected agent can receive and acknowledge it; if no agent is online, it stays queued. “Copy instruction” remains the universal fallback for any harness. The portable artifact is dark-first with a local light/dark toggle; the chosen appearance never changes canonical proof. -Every tool call your agent makes is recorded as a lightweight `ActivityEvent` — tool name, summary, extracted entities. Purely local. +On a pull request, GitHub gets a reviewer-first Check summary and a downloadable Factfile artifact. The job executes with `contents: read`; untrusted PR code never receives a write token merely so Keyoku can post a comment. -### 2. Pattern detection — heuristics for recall, a model for precision +The repository also contains a Marketplace-compatible composite action for the +future stable `v3` tag. During alpha, use `proof init`; its generated workflow +pins the source alpha and detects each project's dependencies safely. No `v3` +action tag is claimed until that release exists. -`workflow_suggest` mines recurring sequences from your recent activity (non-overlapping counting, noise suppression, longest-chain collapsing — no model required). If an SLM key is configured (`GEMINI_API_KEY` or `ANTHROPIC_API_KEY`), the model then refines the drafts: filters coincidences, names workflows properly, and parameterizes run-specific values with `{{placeholders}}`. +## What a reviewer sees -### 2b. Muscle memory — converged goals become reusable workflows +The Factfile answers these questions in order: -A goal that converges (`goal_assess` reports all criteria met) promotes its action trace into a learned workflow. Next time you start a *similar* goal, keyoku surfaces what worked before — so the agent never rediscovers it. +1. What are agents doing, and which are currently connected? +2. Does anything genuinely need my decision? +3. Where should I review first? +4. Which declared claims are supported by evidence? +5. Which files and code areas changed? +6. Which person, agent, harness, and model contributed? +7. Which exact base, head, worktree, and Factfile digests does this cover? -- **Capture happens three ways:** explicit `goal_record`, live `goal_focus` (real actions stream into the goal's trace as you work), or **activity backfill** (if you just did the work without recording, keyoku lifts the steps from the activity log). Already have hollow workflows from older runs? `keyoku backfill` repopulates them. -- **Reuse needs no API key.** keyoku is driven by a frontier coding agent, so *the agent is the judge of relevance.* `goal_assess` returns `candidateWorkflows` and the agent picks the ones that genuinely apply — which matches verbose, differently-worded goals that token-overlap never could. A lite model is an optional accelerator for headless runs (`keyoku watch`/cron), not a requirement. -- **It self-prunes.** Suggestions rank by `similarity × precision`, where precision is learned from whether a workflow's steps actually recur — so word-matching-but-never-useful workflows sink. -- **Negative memory too.** Approaches that *failed* on the way to convergence are captured as pitfalls and surfaced as "avoid (failed before): …" on similar goals. -- **Refine raw into clean.** `keyoku refine ` turns noisy captured steps into a tight, `{{parameterized}}` template ready to run. +Raw observations are collapsed audit detail. An exit code is never presented as the explanation. Visible behavior can attach screenshots; runtime claims can attach tests or traces; security claims can attach scanner output; architecture claims can attach code tours and the generated SVG projection. -### 3. Approval — you are the trust boundary +“Review this first” is deterministic—not another model verdict. Failed claims, declared scope violations, security/data/workflow/dependency-sensitive paths, broad changes, and pending human decisions are ordered with their reasons and source paths. -``` -workflow_approve { slug: "deploy-staging", name: "Deploy staging", steps: [...] } -``` +## One outcome is one review unit -Review the draft like you'd review a shell script, then approve. Templates live in `~/.keyoku/templates.json`. +Keyoku does not encourage one enormous PR. A contribution may contain several commits, but it should deliver one coherent reviewer outcome. Unrelated outcomes should become separate or stacked PRs. -### 4. Execution — bash runs, judgment pauses +An optional path boundary can fail closed when a contribution strays outside its declared scope: -``` -workflow_execute { slug: "deploy-staging" } +```yaml +scope: + include: + - src/auth/** + - tests/auth/** + exclude: + - docs/** + maxChangedFiles: 30 ``` -- **bash** steps run directly (per-step `cwd`, timeouts, output captured) -- **agent_prompt** steps pause and hand the step to your coding agent, which resumes with `execution_complete` -- **human_review** steps wait for your explicit sign-off +Path checks cannot prove semantic coherence, so the generated contract also keeps that question as an explicit human judgment. -Every execution persists step-by-step — crash-safe, fully inspectable via `execution_list`. +Graphite and GitHub can own PR stacking. Keyoku owns the outcome and its proof. -## MCP Tools +## Outcome history belongs in Git -| Tool | What it does | -|---|---| -| `activity_record` / `activity_list` | Log and browse the observation stream | -| `workflow_suggest` | Mine patterns → model-refined draft workflows | -| `workflow_capture` | "Save what I just did" — last N session actions become a draft | -| `workflow_approve` / `workflow_update` | Save or edit templates (slash commands stay current) | -| `workflow_template_list` / `workflow_template_delete` | Manage the catalog | -| `workflow_execute` | Run a template (`params` fill `{{placeholders}}`) | -| `execution_complete` / `execution_cancel` / `execution_list` | Resume, stop, browse runs | -| `knowledge_submit` / `knowledge_query` | The context layer — research, conventions, practice | -| `goal_create` / `goal_assess` / … | Goals with machine-checkable success criteria | -| `goal_focus` / `goal_unfocus` | Live capture — record real actions into a goal's trace as you work | -| `connector_add` / `connector_call` / … | Plug in external MCP servers (GitHub, GCP, …) with autonomy gating | - -## CLI +Outcome contracts are normal versioned repository files. Change the meaning or acceptance criteria, increment `revision`, and commit the file. Anyone can inspect its canonical history without a Keyoku account: +```bash +git log -- .keyoku/outcomes/review-ready-change.yaml ``` -keyoku [serve] Start the MCP server on stdio (Claude Code does this automatically) -keyoku init Wire up the hook + MCP registration -keyoku import Backfill activity from Claude Code + Codex transcripts (kills the cold start) -keyoku export Bake a workflow into ./.claude/skills — or AGENTS.md with --agents-md -keyoku pause | resume Privacy switch: stop/start all recording and injection -keyoku doctor Verify hooks, MCP registrations, engine, and activity health -keyoku inspect Show exactly what's stored in ~/.keyoku (--secrets scans for leaks) -keyoku status Show goals, templates, connectors -keyoku learn Mine patterns from the activity log -keyoku backfill Repopulate hollow learned workflows from the activity log (--dry-run) -keyoku refine Turn a workflow's raw steps into a clean, parameterized template (--apply) -keyoku focus Live-capture actions into a goal's trace (--clear to stop; no arg to show) -keyoku assess One-shot convergence check -keyoku watch Re-assess on an interval -keyoku approvals Approve/deny gated connector calls -keyoku audit [n] Show the audit trail + +Each contribution also keeps append-only coordination events and Factfile snapshots: + +```text +.keyoku/contributions// +├── manifest.yaml +├── events.jsonl # work, decisions, instructions, acknowledgements, presence +├── reviews.jsonl # human judgments and exact-snapshot acceptance +├── snapshots/.json +├── factfile.json # canonical machine record +├── factfile.github.md # concise GitHub reviewer surface +├── factfile.md # portable detailed Markdown +└── factfile.html # human-readable evidence and code tour ``` -## Architecture +Projects can keep snapshots local, upload them as CI artifacts, or commit accepted receipts. Generating a receipt does not change its own source digest. + +## The state model tells the truth +| State | Meaning | +|---|---| +| `evidence_gaps` | A declared machine claim failed, timed out, or could not be observed | +| `human_review_required` | Machine evidence passed; a named human question remains | +| `review_blocked` | A required human judgment failed | +| `ready_for_review` | Declared automated and required human criteria passed | +| `accepted` | An identified human accepted that exact snapshot | + +“Passing” means only that the repository's declared checks passed for the shown revision. It never means universally secure, correct, maintainable, or fit for purpose. Any source change makes the Factfile stale and requires re-evaluation. + +## Example outcome + +```yaml +schemaVersion: keyoku.dev/outcome/v1alpha1 +id: working-release +revision: 1 +title: The release can be reviewed and shipped +objective: A maintainer can build the release and confirm its visible behavior. +owner: + kind: human + id: maintainer@example.com + name: Project maintainer +constraints: + - One contribution represents one coherent outcome. +criteria: + - description: The release build succeeds + probe: + kind: command + run: npm run build + timeoutMs: 120000 + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The production build completed for this exact revision. + whyItMatters: A broken build cannot produce a releasable artifact. + code: + - path: src/build.ts + purpose: Produces the release bundle. + artifacts: [] +humanCriteria: + - id: visible-behavior + description: The maintainer confirms the user-facing result matches the request + guidance: Open the preview and inspect the attached screenshots before accepting. +createdAt: 2026-08-15T00:00:00Z +updatedAt: 2026-08-15T00:00:00Z ``` -Your machine -├── Claude Code (or Cursor, Codex) -│ ├── PostToolUse hook → keyoku record (activity logging) -│ └── MCP connection → keyoku serve (tool calls) -│ -└── ~/.keyoku/ - ├── activity.jsonl (event stream, capped) - ├── templates.json (approved workflows) - ├── executions.json (run history) - ├── goals.json (convergence targets) - └── connectors.json (external MCP services) + +## CLI + +```text +keyoku proof demo Run the real fail → repair → stale-proof scenario +keyoku proof init Detect the project and install a proof workflow +keyoku proof customize Edit common proof fields without schema knowledge +keyoku proof run Evaluate the outcome and generate a local Factfile +keyoku proof serve Open the token-scoped human ↔ agent session +keyoku proof review Record an identified human criterion decision +keyoku proof accept Accept one exact passing snapshot as a human +keyoku factfile inspect Validate and explain a content-bound Factfile +keyoku factfile verify Also require the current source to match it +keyoku factfile publish Explicitly publish to an optional Engine +keyoku pulse help Inspect the event, checkpoint, planner, and renderer path +keyoku pulse fixture generic Emit a harness-neutral JSONL integration fixture +keyoku pulse ingest --file F Append strict, idempotent lifecycle events +keyoku pulse plan --json Decide send/defer/dedupe/suppress/coalesce/stale_no_send +keyoku pulse render --audience A Render one content-bound audience projection +keyoku serve Serve the bounded MCP surface over stdio +keyoku doctor --json Inspect install, project, and authority boundaries ``` -The division of labor: **heuristics** generate candidates for free, the **small model** refines them cheaply, and your **coding agent** does the heavy lifting on the subscription you already pay for. Keyoku orchestrates; it never burns frontier tokens. +`proof run` reuses the active contribution for the current branch and outcome, +so fail, repair, and re-proof checkpoints remain one inspectable history. Pass +`--new` only for a genuinely separate attempt. The v2 goals, workflows, +connectors, activity recorder, memory, and execution commands are not v3 public +entrypoints. See the checked [v3 public surface](docs/PUBLIC-SURFACE.md). -## Configuration +## Different tools, different jobs -| Env var | Default | Purpose | +| Product | Primary job | Keyoku's boundary | |---|---|---| -| `KEYOKU_HOME` | `~/.keyoku` | State directory | -| `GEMINI_API_KEY` / `ANTHROPIC_API_KEY` | — | Enable model-refined suggestions | -| `KEYOKU_SLM_PROVIDER` | auto | `gemini`, `anthropic`, `openai-compat`, or `none` | -| `KEYOKU_SLM_BASE_URL` / `KEYOKU_SLM_MODEL` | — | Any OpenAI-compatible endpoint (Ollama, LM Studio, LiteLLM, Groq, …) | -| `KEYOKU_ENGINE_URL` | — | Connect a running [keyoku-engine](https://github.com/Keyoku-ai/keyoku-engine): knowledge mirrors into it and queries upgrade to semantic search | -| `KEYOKU_WF_MIN_SIMILARITY` | `0.2` | Jaccard floor for suggesting a learned workflow on a new goal | -| `KEYOKU_WF_SUGGEST_LIMIT` | `2` | Max learned workflows surfaced per assessment | -| `KEYOKU_BACKFILL_LOOKBACK_MIN` | `45` | Minutes before a goal's creation to scan for build-then-verify work | -| `KEYOKU_BACKFILL_HEAD_STEPS` | `8` | Setup steps kept from the front when a backfilled workflow is capped | -| `KEYOKU_DEBUG` | — | Full error stacks | +| GitHub Copilot agents | Run and track GitHub agent sessions | Keyoku remains harness-neutral and evaluates a repository-owned outcome | +| Entire | Capture prompts, transcripts, and session checkpoints in Git | Keyoku records bounded result evidence; raw transcripts are optional | +| Graphite | Split and navigate stacked pull requests | Keyoku evaluates each coherent outcome in the stack | +| CodeRabbit | AI review and defect suggestions | Keyoku reports the project's own deterministic proof and human decisions | +| CI/test tools | Execute specialized checks | Keyoku explains their relevance and binds results into one portable receipt | + +Keyoku should complement these tools, not recreate them. + +## Two repositories, one product + +| Repository | Free responsibility | +|---|---| +| [`keyoku`](https://github.com/Keyoku-ai/keyoku) | CLI, open Factfile and Pulse schemas, local verifier/ledger/planner/renderers, GitHub workflow, harness adapters | +| [`keyoku-engine`](https://github.com/Keyoku-ai/keyoku-engine) | Optional durable multi-run Factfile/Pulse registry and dispatcher service plus the retained embedded-memory library | + +The CLI repository is the product wedge and source of truth. The engine is an optional registry—not a required memory backend and not a duplicate control plane. Managed team views, retention policy, RBAC, and cross-repository search are possible hosted extensions; they are not presented as finished open-source features. Both repositories remain usable for public or private repositories. + +## Trust and privacy -## Security +- Project proof lives in `.keyoku/`; ephemeral evaluator state lives in `.keyoku/runtime/`. +- Credential-shaped observations are redacted before JSON, Markdown, HTML, or publication. +- Factfile publication is explicit and accepts HTTPS or loopback HTTP only. +- GitHub proof execution is read-only and does not post privileged PR comments from untrusted code. +- Agent identity is provenance. A human or organization remains accountable. -Approved templates execute shell commands with your privileges — the approval step is the trust boundary. Read [SECURITY.md](SECURITY.md) before installing. +Read the [Factfile standard](docs/FACTFILE-STANDARD.md), [v3 public surface](docs/PUBLIC-SURFACE.md), [Pulse contract](docs/PULSE.md), [GitHub integration guide](docs/GITHUB.md), and [security review](docs/SECURITY-REVIEW.md). -## keyoku-engine +## Status -The Go backend for teams: knowledge graph, semantic search, memory decay, and cross-device sync. Available at [github.com/Keyoku-ai/keyoku-engine](https://github.com/Keyoku-ai/keyoku-engine). +The Factfile and Pulse schemas are `v1alpha1`. The local evaluator, exact Git binding, repeated proof history, durable two-way instruction protocol, token-scoped live session, JSON/Markdown/HTML renderers, annotated visual evidence, scope boundary, outcome history, GitHub Check workflow, deterministic Pulse planner, and audience renderers are implemented and tested. Keyoku does not run an agent or deliver a Pulse update. Schema meaning may still evolve during alpha; incompatible changes receive a new schema version. ## License diff --git a/SECURITY.md b/SECURITY.md index b1d88a6..9bd986b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,32 +1,48 @@ -# Security Policy +# Security policy ## Reporting a vulnerability -Use GitHub's private vulnerability reporting on this repository, or email -**support@keyoku.ai**. Please do not open public issues for security reports. -We aim to acknowledge reports within 72 hours. +Do not include secrets, customer evidence, or exploit material in a public +issue. + +This integration candidate does not yet have a verified private vulnerability +intake. Public release is blocked until the owner enables GitHub private +vulnerability reporting or designates and verifies a monitored security +contact. Final reporting instructions and response expectations must describe +that exact intake rather than an unverified mailbox. ## Supported versions -Only the latest published 0.x release receives security fixes. - -## Execution model — read this before installing - -Keyoku is a local automation tool. Be aware of what it does by design: - -- **Approved workflow templates execute shell commands** on your machine with - your privileges via `workflow_execute`. The approval step - (`workflow_approve`) is the trust boundary: review every step of a template - before approving it, exactly as you would review a shell script before - running it. Steps time out (30s, SIGTERM→SIGKILL) and output is captured. -- **The activity log** (`~/.keyoku/activity.jsonl`) records summaries of your - tool usage (commands, file paths). It stays on your machine. There is no - telemetry and no network calls unless you configure an SLM key - (GEMINI_API_KEY / ANTHROPIC_API_KEY), in which case pattern summaries are - sent to that provider for refinement. -- **State files** under `~/.keyoku` are written with mode 0600 (dir 0700), - the same posture as `~/.aws`. Connector configs may contain credentials — - treat the directory accordingly. -- **Connector autonomy**: external MCP connectors default to gated execution; - write-capable calls can be routed through an approvals queue - (`keyoku approvals`). +The public repository and npm `latest` tag still contain v2. The v3 candidate +is unreleased and receives fixes only on its reviewed integration line until a +replacement release is approved. + +## v3 execution model — read this before evaluating + +Keyoku v3 is a local assurance tool. Be aware of what it does by design: + +- **Repository outcome probes are trusted code.** Review an unfamiliar outcome + contract exactly as you would review project test scripts from an untrusted + fork. The release candidate executes command probes in disposable source + snapshots, but does not claim hostile-command containment from an OS sandbox. +- **Human authority is out of band.** The narrow v3 agent-facing CLI and MCP + surfaces cannot fabricate review acceptance, connector approval, or shell + workflows. Acceptance binds an identified human to one exact Factfile + digest and source identity; it is not yet externally signed. +- **Factfiles can contain sensitive data.** Credential-shaped evidence values + are redacted and portable artifacts are path- and signature-checked, but + this is not artifact-wide DLP. Inspect every export before sharing it. +- **Pulse never silently sends.** Fixture and adapter-attested checkpoints are + nondispatchable. A local checkpoint is planned only after current Factfile, + source, and artifact bytes are reverified. Delivery requires separately + authorized channel code and a recorded successful receipt. +- **No telemetry is enabled by default.** The local v3 surface does not require + Engine, an account, an LLM key, or a hosted control plane. + +## v2 compatibility + +The v2 activity, connector, memory, workflow, and shell-execution surfaces are +not registered or shipped by the narrow v3 entrypoint. Existing v2 users must +continue to treat approved workflows and connector calls as local code running +with their user privileges. See the v2 release documentation for that retained +compatibility line. diff --git a/VALIDATION-REPORT-2026-07-02.md b/VALIDATION-REPORT-2026-07-02.md index 19018e1..f24489b 100644 --- a/VALIDATION-REPORT-2026-07-02.md +++ b/VALIDATION-REPORT-2026-07-02.md @@ -40,7 +40,7 @@ Source: 47-agent read-only refute (6 lenses, independent skeptic verification) + - Scenario: Disk fills while appendAudit writes an entry (or the hook process is killed mid-append). From then on, every audit_list MCP call (server.ts:924) and `keyoku audit` CLI invocation (index.ts:1273) throws 'Unexpected end of JSON input' / 'Unterminated string in JSON' whenever the bad line is within the last `limit` lines - Fix: In listAudit (src/store.ts:318-326), match the sibling readers: parse with `.flatMap((l) => { try { return [JSON.parse(l) as AuditEntry]; } catch { return []; } })` over all non-empty lines, then `.slice(-limit)` on the parsed array (as listActivity does at :368-377) so a torn line is skipped instea - 🔧 **src/activity.ts:159** — redactSecrets leaks Basic-auth credentials (mis-redacts the wrong token) and DB connection-string passwords - - Scenario: User (or agent) runs `curl -H "Authorization: Basic dXNlcjpwYXNzd29yZA==" https://api.example.com` or `psql postgres://admin:s3cr3tPass@db:5432/prod`. The PostToolUse hook records the Bash command; redactSecrets leaves the base64 credential / DB password in ~/.keyoku/activity.jsonl. That command later becomes a workflo + - Scenario: User (or agent) runs `curl -H "Authorization: Basic dXNlcjpwYXNzd29yZA==" https://api.example.com` or `psql postgres://admin:s3cr3tPass@db:5432/prod`. The PostToolUse hook records the Bash command; redactSecrets leaves the base64 credential / DB password in ~/.keyoku/activity.jsonl. That command later becomes a workflo - Fix: In src/activity.ts add a BASIC_RE analogous to BEARER_RE (/\b(basic\s+)[A-Za-z0-9+\/=]{8,}/gi → "$1«redacted»", applied before SECRET_ASSIGNMENT_RE and also matching inside quotes) and a URL-userinfo rule (/(\/\/[^\s\/:@"']+:)[^\s@"']+@/g → "$1«redacted»@") covering postgres://user:pass@host-style s - 🔧 **src/engine.ts:589** — Re-promotion of a workflow silently wipes the self-pruning stats (suggested/helped), defeating precision ranking - Scenario: Verified by repro: workflow with stats {convergences:1, suggested:5, helped:0} (a proven word-match-only workflow that would rank at the 0.25 precision floor) drifts and re-converges once → stats become {convergences:2, totalActions:1} with suggested/helped gone → precision multiplier returns to 1.0 and the noisy workf diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..1a2b9b4 --- /dev/null +++ b/action.yml @@ -0,0 +1,54 @@ +name: Keyoku Factfile +description: Turn a repository-owned outcome into exact-revision evidence and a human-readable Factfile. +author: Keyoku +branding: + icon: check-circle + color: gray-dark +inputs: + outcome: + description: Outcome id under .keyoku/outcomes. + required: true + base: + description: Base Git SHA or ref for the contribution boundary. + required: false + default: HEAD^ + keyoku-version: + description: Exact published Keyoku npm version used by this action revision. + required: false + default: "3.0.0-alpha.1" + retention-days: + description: Number of days GitHub retains the full Factfile artifact. + required: false + default: "14" +outputs: + contribution-id: + description: The generated contribution id. + value: ${{ steps.proof.outputs.contribution_id }} + state: + description: The exact-snapshot Keyoku state. + value: ${{ steps.proof.outputs.state }} + factfile: + description: Path to the generated HTML Factfile. + value: ${{ steps.proof.outputs.factfile }} +runs: + using: composite + steps: + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 20 + - name: Generate exact-revision proof + id: proof + shell: bash + env: + KEYOKU_OUTCOME: ${{ inputs.outcome }} + KEYOKU_BASE: ${{ inputs.base }} + KEYOKU_VERSION: ${{ inputs.keyoku-version }} + run: npx --yes "keyoku@${KEYOKU_VERSION}" proof ci "${KEYOKU_OUTCOME}" --base "${KEYOKU_BASE}" + - name: Attach the full Factfile + if: always() && steps.proof.outputs.contribution_id != '' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: keyoku-factfile-${{ github.sha }} + path: .keyoku/contributions/${{ steps.proof.outputs.contribution_id }}/factfile.* + if-no-files-found: error + retention-days: ${{ inputs.retention-days }} diff --git a/archive/README.md b/archive/README.md new file mode 100644 index 0000000..e093b67 --- /dev/null +++ b/archive/README.md @@ -0,0 +1,17 @@ +# Keyoku archive + +This directory preserves code and product documents removed from the active build during the continuous-contribution-gate pivot. Archived files are retained for history and selective reuse; they are excluded from TypeScript compilation, npm packaging, tests, CLI help, and MCP registration. + +Archiving rule: + +1. Prove the code belongs only to an abandoned product surface. +2. Move implementation and dedicated tests together. +3. Remove all active imports, commands, tools, presets, and promises. +4. Keep a recovery note and run the complete active test suite. +5. Never archive a shared primitive merely because one old integration used it. + +See each subdirectory for scope and recovery instructions. + +- `legacy-omnigent/` — abandoned fleet-runner runtime and dedicated tests. +- `legacy-positioning/` — abandoned Outcome Engine positioning. +- `experimental-control-plane/` — recoverable live briefing, steering, presence, and generative-view prototype removed from the proof-first V1 launch path. diff --git a/archive/experimental-control-plane/.keyoku/harnesses.yaml b/archive/experimental-control-plane/.keyoku/harnesses.yaml new file mode 100644 index 0000000..b764954 --- /dev/null +++ b/archive/experimental-control-plane/.keyoku/harnesses.yaml @@ -0,0 +1,9 @@ +schemaVersion: keyoku.dev/harnesses/v1alpha1 +adapters: + - id: codex-headless + label: Codex headless + kind: codex-exec + enabled: true + model: gpt-5.6-sol + sandbox: workspace-write + description: Dispatch one scoped goal to a non-interactive Codex worker. diff --git a/archive/experimental-control-plane/.keyoku/outcomes/project-intelligence-v1.yaml b/archive/experimental-control-plane/.keyoku/outcomes/project-intelligence-v1.yaml new file mode 100644 index 0000000..636286a --- /dev/null +++ b/archive/experimental-control-plane/.keyoku/outcomes/project-intelligence-v1.yaml @@ -0,0 +1,128 @@ +schemaVersion: keyoku.dev/outcome/v1alpha1 +id: project-intelligence-v1 +revision: 1 +title: A Project Steward keeps code, agents, and proof synchronized +objective: >- + A developer can open Keyoku and understand the current architecture, active goals, worker + agents, important changes, evidence, and decisions from a live project model that updates + without manually rewriting dashboard copy. +owner: + kind: human + id: keyoku-owner + name: Tye + role: accountable product owner +constraints: + - The Steward maintains project intelligence and context but does not replace worker-agent harnesses. + - Deterministic observations remain distinct from agent-inferred semantic structure. + - The UI is template-driven and accepts typed, attributed data patches rather than arbitrary agent-authored scripts. + - Multiple goals may be active; one may be focused per project view without deleting or hiding the others. +criteria: + - description: The project intelligence architecture defines the Steward, context graph ontology, worker plane, evidence artifacts, deployment modes, and adoption wedge + probe: + kind: command + run: >- + node -e "const fs=require('fs');const s=fs.readFileSync('docs/KEYOKU-PROJECT-INTELLIGENCE.md','utf8');process.exit(['Project Steward','Core ontology','Context compiler','Worker plane','Projection and artifact plane','Deployment modes','Adoption wedge'].every(x=>s.includes(x))?0:1)" + timeoutMs: 30000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The system design assigns deterministic observation, semantic stewardship, worker execution, and human accountability to separate bounded planes. + whyItMatters: Contributors can implement intelligence without turning Keyoku into another opaque autonomous coding harness. + code: + - path: docs/KEYOKU-PROJECT-INTELLIGENCE.md + purpose: Canonical Project Steward, context graph, synchronization, artifact, and deployment architecture. + artifacts: [] + - description: The architecture contract produces a current projection and portable SVG export + probe: + kind: command + run: >- + npx tsx -e "import {scanArchitecture,renderArchitectureSvg} from './src/architecture.ts';const a=scanArchitecture(process.cwd());const s=renderArchitectureSvg(a);process.exit(a.components.length>=8&&a.relations.length>=8&&s.includes('- + node -e "const fs=require('fs');const files=fs.readdirSync('.keyoku/outcomes').filter(x=>x.endsWith('.yaml'));process.exit(files.length>=2?0:1)" + timeoutMs: 30000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: Project goals are separate versioned outcome contracts, allowing parallel goals while the interface focuses attention on one. + whyItMatters: Multi-agent projects cannot be accurately represented by a single mutable objective. + code: + - path: .keyoku/outcomes + purpose: Versioned collection of independently owned project outcomes. + artifacts: [] + - description: The live interface preserves a canonical selectable record of snapshots, evidence, context, architecture, and Git state + probe: + kind: command + run: >- + node -e "const fs=require('fs');const server=fs.readFileSync('scripts/serve-project-brief.mjs','utf8');const ui=fs.readFileSync('docs/KEYOKU-PROJECT-STATE.html','utf8');process.exit(['briefTokenPath','projectRecord(','/api/record','/api/snapshots/current','/artifacts/snapshots/'].every(x=>server.includes(x))&&['recordView','recordSnapshotList','recordSetCurrent','loadRecord','recordArchitecture','recordEvidence','recordContext','recordGit'].every(x=>ui.includes(x))&&fs.existsSync('src/presentation.ts')?0:1)" + timeoutMs: 30000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: One goal-scoped record combines the live worktree with immutable Factfile revisions and lets a human choose the review baseline without mutating Git. + whyItMatters: A developer can move through causal project history and see exactly which source, proof, decisions, and architecture belong together. + code: + - path: scripts/serve-project-brief.mjs + purpose: Persistent local session credential, canonical record API, review-baseline events, historical artifacts, and live state. + - path: src/presentation.ts + purpose: Safe MCP-facing presentation manifest and attributed publication protocol. + - path: .keyoku/view.yaml + purpose: Repository-owned allowlist of agent-editable human-facing fields. + artifacts: [] + - description: The full TypeScript and protocol suite remains consistent + probe: + kind: command + run: npm test + timeoutMs: 180000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: Architecture, project-state, MCP, CLI, evidence, and legacy compatibility tests pass together. + whyItMatters: The new intelligence layer stays additive and testable. + code: + - path: tests/architecture.test.ts + purpose: Architecture projection and proposal behavior. + - path: tests/project-state.test.ts + purpose: Interventions, receipts, presence, and multi-agent coordination. + artifacts: [] +humanCriteria: + - id: architecture-comprehension + description: A developer can explain the system boundaries and current movement from the architecture view without reading source files + guidance: Select several components, inspect their ownership and changes, and export the SVG. + - id: multi-goal-clarity + description: A developer can distinguish the focused goal from other active goals and understand which agents contribute to each + guidance: Switch focus without changing goal status or losing parallel work. + - id: steward-trust + description: The Steward feels helpful and current without appearing to invent approved project truth + guidance: Review the provenance language for declared, observed, inferred, and approved content. +createdAt: 2026-08-12T02:00:00Z +updatedAt: 2026-08-12T02:00:00Z diff --git a/archive/experimental-control-plane/.keyoku/outcomes/project-state-v1.yaml b/archive/experimental-control-plane/.keyoku/outcomes/project-state-v1.yaml new file mode 100644 index 0000000..cecc65f --- /dev/null +++ b/archive/experimental-control-plane/.keyoku/outcomes/project-state-v1.yaml @@ -0,0 +1,116 @@ +schemaVersion: keyoku.dev/outcome/v1alpha1 +id: project-state-v1 +revision: 1 +title: A developer can keep up with what coding agents are building +objective: >- + From one current project brief, a developer can understand what the software does, the goal + changing it, the important product and architecture movement, the evidence supporting current + claims, the people and agents contributing, and the decisions or steering that require a human. +owner: + kind: human + id: keyoku-owner + name: Tye + role: accountable product owner +constraints: + - Keyoku does not become a coding-agent runtime, terminal multiplexer, or general task manager. + - The open-source V1 is useful without a hosted account, external model, vector database, or transcript ingestion. + - Memory is optional and project-scoped; Keyoku integrates with harness-native memory rather than replacing it. + - Agent proposals, deterministic observations, and human-approved decisions remain visibly distinct. + - Phone and local-network access are explicit, temporary, tokenized, and never enabled silently. + - Shared artifacts exclude hidden reasoning, secrets, and raw transcripts by default. +criteria: + - description: The product and technical plan defines the wedge, V1 features, state graph, components, MCP tools, memory boundary, live access, exports, security, milestones, and acceptance criteria + probe: + kind: command + run: >- + node -e "const fs=require('fs');const s=fs.readFileSync('docs/KEYOKU-PROJECT-STATE-V1.md','utf8');process.exit(['V1 adoption wedge','Project State Graph','MCP design','Memory boundary','Live access and human steering','Shareable artifacts','Security and trust boundaries','Implementation sequence','Acceptance criteria'].every(x=>s.includes(x))?0:1)" + timeoutMs: 30000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The plan fixes the V1 around a current project brief and defines the full system without requiring agent orchestration or generic memory. + whyItMatters: Contributors can implement compatible components without rediscovering the product boundary. + code: + - path: docs/KEYOKU-PROJECT-STATE-V1.md + purpose: Canonical V1 product, architecture, MCP, memory, sharing, security, and rollout decision. + artifacts: [] + - description: The human interface shows current project capabilities, active goal, change path, evidence boundaries, contributors, and human attention + probe: + kind: command + run: >- + node -e "const fs=require('fs');const s=fs.readFileSync('docs/KEYOKU-PROJECT-STATE.html','utf8');process.exit(['No decision needed','Live exchange','Human intent','Shared project state','Agent execution','Intervention channel','Delivery contract'].every(x=>s.includes(x))?0:1)" + timeoutMs: 30000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The Convergence Thread leads with a causal live exchange—human direction, active agent work, and its receipt—then separates human intent, shared project truth, and agent execution into drill-down lanes. + whyItMatters: A maintainer can orient before reading raw files, test output, or agent transcripts. + code: + - path: docs/KEYOKU-PROJECT-STATE.html + purpose: Interactive human-facing current project brief. + artifacts: [] + - description: The local briefing gateway provides authenticated live state, typed interventions, agent heartbeats, decision capture, and portable Markdown and JSON exports + probe: + kind: command + run: >- + sh -c "node --check scripts/serve-project-brief.mjs && + grep -q '/api/events' scripts/serve-project-brief.mjs && + grep -q '/api/steer' scripts/serve-project-brief.mjs && + grep -q '/api/ask' scripts/serve-project-brief.mjs && + grep -q '/api/interventions' scripts/serve-project-brief.mjs && + grep -q '/api/agent-heartbeat' scripts/serve-project-brief.mjs && + grep -q '/api/context' scripts/serve-project-brief.mjs && + grep -q '/api/decision' scripts/serve-project-brief.mjs && + grep -q '/export/project-update.md' scripts/serve-project-brief.mjs && + grep -q 'SameSite=Strict' scripts/serve-project-brief.mjs" + timeoutMs: 30000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: A dependency-free local server creates a temporary authenticated session, streams changes, records typed human interventions and lease-backed agent presence, and exports the current brief. + whyItMatters: Long-running agents gain a useful phone-accessible communication surface without exposing a raw terminal or requiring a hosted service. + code: + - path: scripts/serve-project-brief.mjs + purpose: Local and explicit LAN briefing gateway with server-sent events, steering, decisions, and exports. + artifacts: [] + - description: Existing Keyoku TypeScript behavior remains internally consistent after adding the prototype + probe: + kind: command + run: npm run typecheck + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The existing TypeScript product still type-checks while the new project-state direction is developed alongside it. + whyItMatters: The prototype does not require destabilizing the current reusable goal, evidence, approval, and MCP foundations. + code: + - path: src/ + purpose: Existing Keyoku implementation retained as the V1 foundation. + artifacts: [] +humanCriteria: + - id: one-minute-orientation + description: A developer can explain what Keyoku does, its current goal, what exists, what is planned, and what needs a human after one minute in the interface + guidance: Start on the default view without reading the plan or source files. + - id: wedge-resonance + description: The promise keep up with what your agents build feels like an urgent, specific developer problem rather than a generic documentation or memory product + guidance: Ask whether the first screen makes the intended user and pain immediately recognizable. + - id: mobile-steering-clarity + description: A phone user can see meaningful progress and send a bounded direction without believing they directly controlled an agent that did not acknowledge it + guidance: Review committed, delivered, understood, applied, verified, superseded, and could-not-apply language before shipping the intervention loop. + - id: evidence-clarity + description: The interface clearly distinguishes current supported behavior, prototype hypotheses, and unimplemented plans + guidance: No green state or progress percentage should imply product behavior that has not been demonstrated. +createdAt: 2026-08-11T00:00:00Z +updatedAt: 2026-08-12T01:30:00Z diff --git a/archive/experimental-control-plane/.keyoku/roadmap.yaml b/archive/experimental-control-plane/.keyoku/roadmap.yaml new file mode 100644 index 0000000..1b0fd39 --- /dev/null +++ b/archive/experimental-control-plane/.keyoku/roadmap.yaml @@ -0,0 +1,29 @@ +schemaVersion: keyoku.dev/roadmap/v1alpha1 +goalId: project-intelligence-v1 +updatedAt: 2026-08-12T02:10:00Z +iteration: + current: 1 + target: 4 + confidence: medium + basis: One iteration each for connection, Factfile experience, durable dispatch, and developer validation. +milestones: + - id: connection-truth + title: Persistent daemon and honest agent connection + status: in_progress + targetIteration: 1 + proof: Stable private URL, renewable presence lease, and semantic delivery receipts. + - id: factfile-view + title: Shareable live Factfile + status: in_progress + targetIteration: 2 + proof: Snapshot, architecture, decisions, roadmap, gates, and human review render in Keyoku. + - id: durable-dispatch + title: Harness activation and resumable task dispatch + status: planned + targetIteration: 3 + proof: A queued task wakes a configured harness and receives delivered, understood, applied, and verified receipts. + - id: developer-validation + title: Daily-driver validation + status: planned + targetIteration: 4 + proof: Five external developers complete the goal-to-acceptance loop and can explain project state in under one minute. diff --git a/archive/experimental-control-plane/.keyoku/view.yaml b/archive/experimental-control-plane/.keyoku/view.yaml new file mode 100644 index 0000000..9bddf0d --- /dev/null +++ b/archive/experimental-control-plane/.keyoku/view.yaml @@ -0,0 +1,36 @@ +schemaVersion: keyoku.dev/project-view/v1alpha1 +template: convergence-thread +fields: + exchange.kicker: + value: Live exchange + description: Short label above the causal project summary. + exchange.title: + value: Your intent, its effect, and the proof. + description: Human-facing explanation of the default project lens. + exchange.summary: + value: One causal sentence—not an activity feed. + description: Why this view exists. + composer.placeholder: + value: Tell the project what you need… + description: Prompt for human-to-agent intervention. + product.title: + value: Keyoku is the interface, not another coding agent. + description: Current product boundary shown in the human lane. + product.summary: + value: Execution stays replaceable. Project understanding and proof stay portable. + description: Consequence of the product boundary. + authority.title: + value: Agents propose and act within rails. People own consequential judgment. + description: Current human accountability rule. + authority.summary: + value: Routine uncertainty does not become an approval request. + description: Consequence of the authority rule. + architecture.kicker: + value: System view + description: Label above the architecture projection. + architecture.title: + value: The codebase, as a current system. + description: Architecture projection heading. + architecture.summary: + value: Deterministic repository observations keep the map honest. The Project Steward proposes semantic changes with provenance instead of silently redrawing the system. + description: Architecture trust model. diff --git a/archive/experimental-control-plane/README.md b/archive/experimental-control-plane/README.md new file mode 100644 index 0000000..1b1dae7 --- /dev/null +++ b/archive/experimental-control-plane/README.md @@ -0,0 +1,9 @@ +# Experimental control-plane prototype + +This directory preserves the August 2026 Project State / Project Steward prototype removed from Keyoku's active launch path during the proof-first pivot. + +It includes the live briefing UI, relay server, intervention protocol, presentation manifest, project-state store, tests, and their outcome contracts. The code is recoverable product research, but it is intentionally excluded from the active TypeScript build, MCP tool list, test suite, package, and README promise. + +It was archived because it expanded Keyoku into agent orchestration, presence, steering, project management, and generative UI before the contribution-proof wedge had adoption. That made the five-minute path to a useful PR Factfile harder to understand. + +Reintroduce a capability only after repeated user pull, and then as a focused integration around the Factfile standard rather than a second product category. diff --git a/archive/experimental-control-plane/scripts/serve-project-brief.mjs b/archive/experimental-control-plane/scripts/serve-project-brief.mjs new file mode 100644 index 0000000..35ba8af --- /dev/null +++ b/archive/experimental-control-plane/scripts/serve-project-brief.mjs @@ -0,0 +1,1073 @@ +#!/usr/bin/env node + +import { appendFileSync, createReadStream, existsSync, mkdirSync, readdirSync, readFileSync, statSync, watch, writeFileSync } from "node:fs"; +import { createServer } from "node:http"; +import { networkInterfaces } from "node:os"; +import { dirname, extname, join, normalize, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { createHash, randomBytes } from "node:crypto"; +import { execFileSync } from "node:child_process"; +import { spawn } from "node:child_process"; +import { parse as parseYaml } from "yaml"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const projectRoot = resolve(scriptDir, ".."); +const docsRoot = join(projectRoot, "docs"); +const runtimeRoot = join(projectRoot, ".keyoku", "runtime"); +const pagePath = join(docsRoot, "KEYOKU-PROJECT-STATE.html"); +const steeringPath = join(runtimeRoot, "human-steering.jsonl"); +const decisionsPath = join(runtimeRoot, "human-decisions.jsonl"); +const protocolPath = join(runtimeRoot, "thread-events.jsonl"); +const sessionsPath = join(runtimeRoot, "agent-sessions.jsonl"); +const architecturePath = join(projectRoot, ".keyoku", "architecture.yaml"); +const outcomesRoot = join(projectRoot, ".keyoku", "outcomes"); +const goalFocusPath = join(runtimeRoot, "goal-focus.jsonl"); +const contributionsRoot = join(projectRoot, ".keyoku", "contributions"); +const viewPath = join(projectRoot, ".keyoku", "view.yaml"); +const viewEventsPath = join(runtimeRoot, "view-events.jsonl"); +const briefTokenPath = join(runtimeRoot, "brief-token"); +const roadmapPath = join(projectRoot, ".keyoku", "roadmap.yaml"); +const harnessesPath = join(projectRoot, ".keyoku", "harnesses.yaml"); +const dispatchesPath = join(runtimeRoot, "dispatches.jsonl"); +const currentSnapshotsPath = join(runtimeRoot, "current-snapshots.jsonl"); +const activeDispatches = new Map(); + +const args = process.argv.slice(2); +const lan = args.includes("--lan"); +const host = lan ? "0.0.0.0" : "127.0.0.1"; +const portFlag = args.indexOf("--port"); +const port = portFlag >= 0 ? Number(args[portFlag + 1]) : 4178; +if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new Error("--port must be an integer between 1 and 65535"); +} + +mkdirSync(runtimeRoot, { recursive: true }); +const token = process.env.KEYOKU_BRIEF_TOKEN || (existsSync(briefTokenPath) ? readFileSync(briefTokenPath, "utf8").trim() : randomBytes(24).toString("base64url")); +if (!process.env.KEYOKU_BRIEF_TOKEN && !existsSync(briefTokenPath)) writeFileSync(briefTokenPath, `${token}\n`, { encoding: "utf8", mode: 0o600 }); +const sessionCookie = `keyoku_brief=${token}`; +const clients = new Set(); + +function mime(path) { + return { + ".html": "text/html; charset=utf-8", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".webp": "image/webp", + ".json": "application/json; charset=utf-8", + ".md": "text/markdown; charset=utf-8", + }[extname(path).toLowerCase()] || "application/octet-stream"; +} + +function json(res, status, body) { + res.writeHead(status, { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + "X-Content-Type-Options": "nosniff", + }); + res.end(JSON.stringify(body)); +} + +function authenticated(req, url) { + if (url.searchParams.get("token") === token) return true; + const cookies = req.headers.cookie?.split(";").map((value) => value.trim()) ?? []; + return cookies.includes(sessionCookie); +} + +function assertLocalOrigin(req) { + const origin = req.headers.origin; + if (!origin) return true; + try { + const originUrl = new URL(origin); + return originUrl.host === req.headers.host; + } catch { + return false; + } +} + +async function body(req) { + let raw = ""; + for await (const chunk of req) { + raw += chunk; + if (raw.length > 32_768) throw new Error("Request is too large"); + } + return raw ? JSON.parse(raw) : {}; +} + +function safeText(value, max = 4_000) { + return typeof value === "string" ? value.trim().slice(0, max) : ""; +} + +function appendEvent(path, event) { + appendFileSync(path, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600 }); +} + +function recentEvents(path, limit = 20) { + if (!existsSync(path)) return []; + return readFileSync(path, "utf8").split("\n").filter(Boolean).slice(-limit).map((line) => { + try { return JSON.parse(line); } catch { return null; } + }).filter(Boolean); +} + +function foldedSteering(limit = 20) { + const requests = new Map(); + const acknowledgements = []; + for (const event of recentEvents(steeringPath, 200)) { + if (event.eventType === "acknowledgement") acknowledgements.push(event); + else if (event.id && event.message) requests.set(event.id, { ...event }); + } + for (const acknowledgement of acknowledgements) { + const request = requests.get(acknowledgement.steeringId); + if (!request) continue; + request.status = acknowledgement.status; + request.acknowledgement = { + summary: acknowledgement.summary, + actor: acknowledgement.actor, + createdAt: acknowledgement.createdAt, + }; + } + return [...requests.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, limit); +} + +function foldedInterventions(limit = 30) { + const interventions = new Map(); + const receipts = []; + for (const event of recentEvents(protocolPath, 500)) { + if (event.eventType === "intervention.created" && event.id) interventions.set(event.id, { ...event, phase: "committed", receipts: [] }); + if (event.eventType === "intervention.receipt" && event.interventionId) receipts.push(event); + } + for (const receipt of receipts) { + const intervention = interventions.get(receipt.interventionId); + if (!intervention) continue; + intervention.receipts.push(receipt); + intervention.phase = receipt.phase; + } + return [...interventions.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, limit); +} + +function foldedSessions(limit = 30) { + const sessions = new Map(); + for (const event of recentEvents(sessionsPath, 500)) { + if (event.eventType === "agent.heartbeat" && event.sessionId) sessions.set(event.sessionId, { ...event }); + } + const now = Date.now(); + return [...sessions.values()].map((session) => ({ + ...session, + active: session.status !== "disconnected" && Date.parse(session.leaseUntil) > now, + })).sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, limit); +} + +function coordinationConflicts(sessions) { + const active = sessions.filter((session) => session.active && session.currentWork); + const conflicts = []; + const overlaps = (left, right) => { + const a = String(left).replace(/^\.\//, "").replace(/\/$/, ""); + const b = String(right).replace(/^\.\//, "").replace(/\/$/, ""); + return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`); + }; + for (let left = 0; left < active.length; left += 1) for (let right = left + 1; right < active.length; right += 1) { + const a = active[left]; const b = active[right]; + if (a.currentWork.contributionId && a.currentWork.contributionId === b.currentWork.contributionId) conflicts.push({ sessions: [a.sessionId, b.sessionId], reason: "same_contribution", scope: a.currentWork.contributionId }); + for (const aPath of a.currentWork.paths || []) for (const bPath of b.currentWork.paths || []) if (overlaps(aPath, bPath)) conflicts.push({ sessions: [a.sessionId, b.sessionId], reason: "overlapping_path", scope: aPath.length <= bPath.length ? aPath : bPath }); + } + return conflicts; +} + +function protocolId(prefix) { + return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url")}`; +} + +function broadcast(event, data) { + const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; + for (const client of clients) client.write(payload); +} + +function git(args) { + try { + return execFileSync("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); + } catch { + return ""; + } +} + +function repositoryDigest(baseSha, headSha, files) { + const digest = createHash("sha256"); + digest.update(`base\0${baseSha}\0head\0${headSha}\0`); + digest.update(git(["diff", "--binary", "HEAD"])); + digest.update(git(["diff", "--binary", "--cached", "HEAD"])); + for (const item of files) { + digest.update(`\0${item.path}\0`); + const absolute = join(projectRoot, item.path); + if (item.status === "??" && existsSync(absolute) && statSync(absolute).isFile()) digest.update(readFileSync(absolute)); + } + return digest.digest("hex"); +} + +function repositoryState() { + const status = git(["status", "--porcelain=v1", "--untracked-files=all"]); + const changedFiles = status ? status.split("\n").filter(Boolean) : []; + const upstream = git(["rev-parse", "--abbrev-ref", "@{upstream}"]); + const [behind = 0, ahead = 0] = upstream ? git(["rev-list", "--left-right", "--count", `HEAD...${upstream}`]).split(/\s+/).map((value) => Number(value) || 0) : [0, 0]; + const headSha = git(["rev-parse", "HEAD"]) || "unknown"; + const files = changedFiles + .map((line) => ({ status: line.slice(0, 2), path: line.slice(3).replace(/^.* -> /, "") })) + .filter((item) => !item.path.startsWith(".keyoku/contributions/") && !item.path.startsWith(".keyoku/runtime/")) + .sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0); + return { + head: headSha.slice(0, 12), + headSha, + branch: git(["branch", "--show-current"]) || "detached", + upstream: upstream || null, + ahead, + behind, + remote: git(["config", "--get", "remote.origin.url"]) || null, + lastCommit: git(["log", "-1", "--pretty=%s"]) || "unknown", + changedFiles: files.length, + files, + dirty: files.length > 0, + worktreeDigest: repositoryDigest(headSha, headSha, files), + }; +} + +function architectureFiles(entry) { + const absolute = join(projectRoot, entry); + if (!existsSync(absolute)) return []; + if (!statSync(absolute).isDirectory()) return [entry]; + const files = []; + const visit = (directory) => { + for (const child of readdirSync(directory, { withFileTypes: true })) { + if (["node_modules", ".git", "dist"].includes(child.name)) continue; + const path = join(directory, child.name); + if (child.isDirectory()) visit(path); else files.push(relative(projectRoot, path)); + } + }; + visit(absolute); + return files; +} + +function architectureState() { + if (!existsSync(architecturePath)) return null; + const document = parseYaml(readFileSync(architecturePath, "utf8")); + const status = git(["status", "--porcelain=v1"]); + const changed = status ? status.split("\n").filter(Boolean).map((line) => line.slice(3)) : []; + const owned = new Set(); + const components = (document.components || []).map((component) => { + const files = (component.owns || []).flatMap(architectureFiles); + files.forEach((file) => owned.add(file)); + const changedFiles = changed.filter((file) => files.includes(file) || (component.owns || []).some((entry) => file === entry || file.startsWith(`${entry}/`))); + return { ...component, observedFiles: files.length, changedFiles, state: component.external ? "external" : files.length === 0 ? "missing" : changedFiles.length ? "changing" : "stable" }; + }); + const snapshotRef = createHash("sha256").update(JSON.stringify({ head: git(["rev-parse", "HEAD"]), status, document })).digest("hex").slice(0, 16); + return { + schemaVersion: "keyoku.dev/architecture-projection/v1alpha1", + projectId: document.projectId, + title: document.title, + generatedAt: new Date().toISOString(), + snapshotRef, + source: { kind: "declared+observed", path: ".keyoku/architecture.yaml" }, + components, + relations: document.relations || [], + unownedChanges: changed.filter((file) => !owned.has(file)), + }; +} + +function architectureSvg(projection) { + const nodeWidth = 190; const nodeHeight = 112; + const nodes = new Map(projection.components.map((component) => [component.id, component])); + const edges = projection.relations.map((relation, index) => { + const from = nodes.get(relation.from); const to = nodes.get(relation.to); if (!from?.view || !to?.view) return ""; + const x1 = from.view.x + nodeWidth; const y1 = from.view.y + nodeHeight / 2; const x2 = to.view.x; const y2 = to.view.y + nodeHeight / 2; + const direction = x2 >= x1 ? 1 : -1; const lift = 18 + (index % 4) * 9; + const c1 = x1 + direction * Math.max(45, Math.abs(x2 - x1) * .38); const c2 = x2 - direction * Math.max(45, Math.abs(x2 - x1) * .38); + return ``; + }).join(""); + const componentNodes = projection.components.map((component) => { + if (!component.view) return ""; + const state = component.state === "changing" ? "#9a7cff" : component.state === "missing" ? "#ef9a9a" : component.state === "external" ? "#86d7b0" : "#5d567b"; + const mark = component.icon === "database" ? "DB" : component.icon === "keyoku" ? "K" : component.icon === "git" ? "GIT" : component.icon === "mcp" ? "MCP" : component.icon === "agent" ? "AI" : component.icon.slice(0, 2).toUpperCase(); + const detail = component.external ? "external boundary" : `${component.observedFiles} files${component.changedFiles.length ? ` · ${component.changedFiles.length} changing` : ""}`; + return `${htmlEscape(mark)}${htmlEscape(component.label)}${htmlEscape(detail)}${htmlEscape(component.layer)}`; + }).join(""); + return `${htmlEscape(projection.title)}Live architecture projection for ${htmlEscape(projection.projectId)} at snapshot ${htmlEscape(projection.snapshotRef)}.${htmlEscape(projection.title)}snapshot ${htmlEscape(projection.snapshotRef)} · ${htmlEscape(projection.source.kind)}${edges}${componentNodes}Observed files + declared semantic structure · generated ${htmlEscape(projection.generatedAt)}`; +} + +function projectState() { + const decisions = recentEvents(decisionsPath); + const latestDecision = new Map(decisions.map((event) => [event.decisionId, event])); + const steering = foldedSteering(); + const interventions = foldedInterventions(); + const agentSessions = foldedSessions(); + const activeAgents = agentSessions.filter((session) => session.active); + const architecture = architectureState(); + const goals = projectGoals(); + const proof = projectProof(); + return { + connected: true, + mode: lan ? "local-network" : "local-device", + updatedAt: new Date().toISOString(), + repository: repositoryState(), + bridge: { + protocol: "keyoku.dev/thread-exchange/v1alpha1", + mode: activeAgents.length ? "agent-connected" : "durable-inbox", + liveAdapter: activeAgents.find((session) => session.capabilities?.includes("push-intervention"))?.transport || null, + supports: ["presence", "query", "direction", "control", "proof_challenge", "receipts", "checkpoint", "evidence"], + truth: activeAgents.length + ? `${activeAgents.length} agent session${activeAgents.length === 1 ? " holds" : "s hold"} a valid heartbeat lease.` + : "The UI and durable inbox are live, but no agent session currently holds a heartbeat lease.", + }, + attention: [], + steering, + interventions, + agents: { active: activeAgents, recent: agentSessions, coordinationConflicts: coordinationConflicts(agentSessions) }, + architecture: architecture ? { + snapshotRef: architecture.snapshotRef, + components: architecture.components.length, + changing: architecture.components.filter((component) => component.state === "changing").length, + unownedChanges: architecture.unownedChanges.length, + } : null, + goals, + proof, + roadmap: projectRoadmap(), + history: projectHistory(), + harnesses: projectHarnesses(), + dispatches: projectDispatches(), + view: projectView(), + decisions: [...latestDecision.values()], + }; +} + +function projectHarnesses() { + if (!existsSync(harnessesPath)) return []; + try { + const manifest = parseYaml(readFileSync(harnessesPath, "utf8")); + return (manifest?.adapters || []).filter((item) => item?.enabled && item.kind === "codex-exec").map((item) => ({ id: item.id, label: item.label, kind: item.kind, model: item.model || null, sandbox: item.sandbox === "read-only" ? "read-only" : "workspace-write", description: item.description || "Headless coding worker" })); + } catch { return []; } +} + +function projectDispatches() { + const events = recentEvents(dispatchesPath, 500); + const byId = new Map(); + for (const event of events) if (event.dispatchId) byId.set(event.dispatchId, { ...(byId.get(event.dispatchId) || {}), ...event }); + return [...byId.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); +} + +function startHeadlessDispatch({ adapter, goal }) { + const existing = [...activeDispatches.values()].find((item) => item.goalId === goal.id && item.adapterId === adapter.id); + if (existing) throw new Error("This harness is already running for the focused goal"); + const dispatchId = protocolId("run"); + const sessionId = `headless:${dispatchId}`; + const outputPath = join(runtimeRoot, `${dispatchId}.last-message.md`); + const logPath = join(runtimeRoot, `${dispatchId}.jsonl`); + const prompt = `You are a headless worker dispatched by Keyoku for goal '${goal.id}'.\n\nGoal: ${goal.title}\nObjective: ${goal.objective}\n\nWork toward the smallest safe, testable next task for this goal. Begin by reading .keyoku/outcomes/${goal.id}.yaml, .keyoku/roadmap.yaml when relevant, and the repository status. Preserve unrelated changes. Run proportionate tests. Use the Keyoku MCP tools when connected: project_orient, intervention_list, intervention_receipt, contribution_start, contribution_gate, and agent_session_heartbeat. Do not claim human acceptance. Finish with a concise checkpoint: what changed, evidence, blockers, and next action.`; + const args = ["exec", "--full-auto", "--sandbox", adapter.sandbox, "-C", projectRoot, "--output-last-message", outputPath, "-"]; + if (adapter.model) args.splice(1, 0, "--model", adapter.model); + writeFileSync(logPath, "", { encoding: "utf8", mode: 0o600 }); + const child = spawn("codex", args, { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env } }); + child.stdout.on("data", (chunk) => appendFileSync(logPath, chunk)); + child.stderr.on("data", (chunk) => appendFileSync(logPath, chunk)); + child.stdin.end(prompt); + const startedAt = new Date().toISOString(); + const event = { eventType: "dispatch.started", dispatchId, sessionId, goalId: goal.id, adapterId: adapter.id, label: adapter.label, pid: child.pid, status: "running", createdAt: startedAt, updatedAt: startedAt, outputPath: relative(projectRoot, outputPath), logPath: relative(projectRoot, logPath) }; + appendEvent(dispatchesPath, event); + const heartbeat = () => { + const now = new Date(); + appendEvent(sessionsPath, { schemaVersion: "keyoku.dev/agent-session/v1alpha1", eventType: "agent.heartbeat", eventId: protocolId("evt"), sessionId, actor: { kind: "agent", id: sessionId, name: adapter.label, harness: "Codex exec", ...(adapter.model ? { model: adapter.model } : {}) }, status: "working", currentWork: { summary: `Headless execution for ${goal.title}`, outcomeId: goal.id, baseSnapshot: repositoryState().head, paths: [] }, capabilities: ["checkpoint", "evidence", "headless-process"], transport: "keyoku-process-adapter", createdAt: now.toISOString(), leaseUntil: new Date(now.getTime() + 45_000).toISOString(), active: true }); + broadcast("state", { kind: "agent.heartbeat", sessionId }); + }; + heartbeat(); + const timer = setInterval(heartbeat, 20_000); + activeDispatches.set(dispatchId, { dispatchId, goalId: goal.id, adapterId: adapter.id, child, timer }); + child.on("exit", (code, signal) => { + clearInterval(timer); activeDispatches.delete(dispatchId); + const completedAt = new Date().toISOString(); + const summary = existsSync(outputPath) ? readFileSync(outputPath, "utf8").trim().slice(0, 4_000) : `Worker exited ${signal ? `after ${signal}` : `with code ${code}`}.`; + appendEvent(dispatchesPath, { eventType: "dispatch.completed", dispatchId, goalId: goal.id, adapterId: adapter.id, status: code === 0 ? "completed" : "failed", exitCode: code, signal, summary, updatedAt: completedAt }); + appendEvent(sessionsPath, { schemaVersion: "keyoku.dev/agent-session/v1alpha1", eventType: "agent.heartbeat", eventId: protocolId("evt"), sessionId, actor: { kind: "agent", id: sessionId, name: adapter.label, harness: "Codex exec", ...(adapter.model ? { model: adapter.model } : {}) }, status: "disconnected", capabilities: ["checkpoint", "evidence", "headless-process"], transport: "keyoku-process-adapter", createdAt: completedAt, leaseUntil: completedAt, active: false }); + broadcast("state", { kind: "dispatch.completed", dispatchId, code, signal }); + }); + return event; +} + +function projectRoadmap() { + if (!existsSync(roadmapPath)) return null; + try { return parseYaml(readFileSync(roadmapPath, "utf8")); } catch { return null; } +} + +function projectHistory() { + const events = []; + const interventionGoals = new Map(); + for (const event of recentEvents(protocolPath, 500)) if (event.eventType === "intervention.created") interventionGoals.set(event.id, event.scope?.outcomeId); + for (const event of recentEvents(protocolPath, 500)) { + if (event.eventType === "intervention.created") events.push({ id: event.eventId, type: "intervention", title: `${event.actor?.name || "Someone"}: ${event.message}`, detail: `${event.kind} · ${event.delivery?.policy || "queued"}`, actor: event.actor, createdAt: event.createdAt, goalId: event.scope?.outcomeId, state: event.phase || "committed" }); + if (event.eventType === "intervention.receipt") events.push({ id: event.eventId, type: "receipt", title: event.summary, detail: `${event.phase} receipt`, actor: event.actor, createdAt: event.createdAt, goalId: interventionGoals.get(event.interventionId), state: event.phase }); + } + for (const event of recentEvents(decisionsPath, 200)) events.push({ id: event.id || event.eventId, type: "decision", title: `${event.decisionId}: ${event.choice}`, detail: "human decision", actor: event.actor, createdAt: event.createdAt, state: event.choice }); + for (const event of recentEvents(goalFocusPath, 200)) events.push({ id: event.eventId || `${event.goalId}-${event.createdAt}`, type: "goal", title: `Focused ${event.goalId}`, detail: event.reason || "goal focus changed", actor: event.actor, createdAt: event.createdAt, goalId: event.goalId, state: "focused" }); + for (const event of recentEvents(currentSnapshotsPath, 200)) events.push({ id: event.eventId, type: "baseline", title: `Current baseline set to ${event.snapshotId}`, detail: event.reason || "review baseline changed; Git unchanged", actor: event.actor, createdAt: event.createdAt, goalId: event.goalId, state: "current" }); + for (const event of projectDispatches()) events.push({ id: event.dispatchId, type: "dispatch", title: `${event.label || event.adapterId} ${event.status}`, detail: event.summary || `process ${event.pid || ""}`.trim(), createdAt: event.updatedAt || event.createdAt, goalId: event.goalId, state: event.status }); + for (const artifact of projectProof().artifacts) events.push({ id: `factfile-${artifact.contributionId}-${artifact.generatedAt}`, type: "factfile", title: artifact.title, detail: `${artifact.automated.passed}/${artifact.automated.total} automated · ${artifact.human.pending} human pending`, actor: artifact.actors?.at(-1), createdAt: artifact.generatedAt, goalId: artifact.goalId, state: artifact.state, href: artifact.href }); + return events.filter((event) => event.createdAt).sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, 100); +} + +function projectView() { + if (!existsSync(viewPath)) return { schemaVersion: "keyoku.dev/project-view/v1alpha1", template: "convergence-thread", fields: {} }; + const manifest = parseYaml(readFileSync(viewPath, "utf8")); + const fields = Object.fromEntries(Object.entries(manifest?.fields || {}).flatMap(([name, field]) => typeof field?.value === "string" ? [[name, { value: field.value, description: field.description || "Agent-editable presentation field.", source: "manifest" }]] : [])); + for (const event of recentEvents(viewEventsPath, 500)) { + if (event.eventType !== "view.fields.published") continue; + for (const [name, value] of Object.entries(event.fields || {})) { + if (!fields[name] || typeof value !== "string") continue; + fields[name] = { ...fields[name], value, source: "agent", updatedAt: event.createdAt, actor: event.actor, confidence: event.confidence }; + } + } + return { schemaVersion: "keyoku.dev/project-view/v1alpha1", template: manifest.template || "convergence-thread", fields }; +} + +function projectGoals() { + const goals = existsSync(outcomesRoot) + ? readdirSync(outcomesRoot).filter((name) => name.endsWith(".yaml") || name.endsWith(".yml")).flatMap((name) => { + try { + const value = parseYaml(readFileSync(join(outcomesRoot, name), "utf8")); + return value?.id && value?.title ? [{ id: value.id, title: value.title, objective: value.objective || "", owner: value.owner || null, updatedAt: value.updatedAt || value.createdAt || "", criteria: value.criteria?.length || 0, humanCriteria: value.humanCriteria?.length || 0 }] : []; + } catch { return []; } + }).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) + : []; + const focusEvents = recentEvents(goalFocusPath, 100); + const focusedId = [...focusEvents].reverse().find((event) => event.eventType === "goal.focused" && goals.some((goal) => goal.id === event.goalId))?.goalId || goals[0]?.id || null; + return { focusedId, focused: goals.find((goal) => goal.id === focusedId) || null, active: goals, count: goals.length }; +} + +function projectProof() { + const byGoal = {}; + if (!existsSync(contributionsRoot)) return { byGoal, latest: null, artifacts: [] }; + const records = readdirSync(contributionsRoot).flatMap((name) => { + const path = join(contributionsRoot, name, "factfile.json"); + if (!existsSync(path)) return []; + try { const factfile = JSON.parse(readFileSync(path, "utf8")); return [{ path: relative(projectRoot, path), mtime: statSync(path).mtimeMs, factfile }]; } catch { return []; } + }).sort((a, b) => b.mtime - a.mtime); + for (const record of records) if (record.factfile.outcome?.id && !byGoal[record.factfile.outcome.id]) byGoal[record.factfile.outcome.id] = { + contributionId: record.factfile.contribution?.id || record.factfile.id, + gate: record.factfile.state, + summary: record.factfile.summary, + automated: record.factfile.summary, + humanReview: record.factfile.humanReview, + snapshot: record.factfile.repository, + generatedAt: record.factfile.generatedAt, + path: record.path, + }; + const artifacts = records.map(({ factfile }) => ({ + contributionId: factfile.contribution?.id || factfile.id, + goalId: factfile.outcome?.id || factfile.contribution?.outcomeId, + title: factfile.outcome?.title || factfile.contribution?.title || "Contribution Factfile", + state: factfile.state, + generatedAt: factfile.generatedAt, + automated: factfile.summary || { passed: 0, failed: 0, total: 0 }, + human: factfile.humanReview || { passed: 0, failed: 0, pending: 0, total: 0 }, + snapshot: factfile.repository, + actors: factfile.contribution?.actors || [], + href: `/artifacts/factfiles/${encodeURIComponent(factfile.contribution?.id || factfile.id)}`, + })); + return { byGoal, latest: records[0]?.factfile || null, artifacts }; +} + +function factfileSnapshots(goalId) { + if (!existsSync(contributionsRoot)) return []; + return readdirSync(contributionsRoot).flatMap((contributionId) => { + const snapshotsRoot = join(contributionsRoot, contributionId, "snapshots"); + if (!existsSync(snapshotsRoot)) return []; + return readdirSync(snapshotsRoot).filter((name) => name.endsWith(".json")).flatMap((name) => { + try { + const snapshot = JSON.parse(readFileSync(join(snapshotsRoot, name), "utf8")); + if (goalId && snapshot.outcome?.id !== goalId) return []; + return [{ contributionId, snapshot }]; + } catch { return []; } + }); + }).sort((a, b) => b.snapshot.generatedAt.localeCompare(a.snapshot.generatedAt)); +} + +function currentSnapshotFor(goalId, validIds) { + const event = [...recentEvents(currentSnapshotsPath, 500)].reverse().find((item) => item.eventType === "snapshot.current" && item.goalId === goalId && validIds.has(item.snapshotId)); + return event?.snapshotId || "live"; +} + +function projectRecord(goalIdInput, selectedIdInput) { + const goals = projectGoals(); + const goalId = goalIdInput && goals.active.some((goal) => goal.id === goalIdInput) ? goalIdInput : goals.focusedId; + const goal = goals.active.find((item) => item.id === goalId) || null; + const records = factfileSnapshots(goalId); + const validIds = new Set(["live", ...records.map((item) => item.snapshot.id)]); + const currentId = currentSnapshotFor(goalId, validIds); + const selectedId = validIds.has(selectedIdInput) ? selectedIdInput : currentId; + const liveRepository = repositoryState(); + const history = projectHistory().filter((item) => !goalId || !item.goalId || item.goalId === goalId); + const roadmap = projectRoadmap(); + const summaries = [{ + id: "live", + kind: "working", + title: "Live working state", + generatedAt: new Date().toISOString(), + state: liveRepository.dirty ? "in_progress" : "clean", + branch: liveRepository.branch, + headSha: liveRepository.headSha, + changedFiles: liveRepository.changedFiles, + automated: null, + human: null, + current: currentId === "live", + }, ...records.map(({ contributionId, snapshot }) => ({ + id: snapshot.id, + kind: "factfile", + contributionId, + title: snapshot.contribution?.title || snapshot.outcome?.title || "Factfile revision", + generatedAt: snapshot.generatedAt, + state: snapshot.state, + branch: snapshot.repository?.branch || "unknown", + headSha: snapshot.repository?.headSha, + changedFiles: snapshot.repository?.changedFiles?.length || 0, + automated: snapshot.summary, + human: snapshot.humanReview, + current: currentId === snapshot.id, + href: `/artifacts/snapshots/${encodeURIComponent(contributionId)}/${encodeURIComponent(snapshot.id)}`, + }))]; + + if (selectedId === "live") { + const architecture = architectureState(); + return { + schemaVersion: "keyoku.dev/canonical-record/v1alpha1", + goal, + roadmap: roadmap?.goalId === goalId ? roadmap : null, + currentId, + selectedId, + snapshots: summaries, + selected: { + id: "live", + kind: "working", + title: "Live working state", + generatedAt: new Date().toISOString(), + state: liveRepository.dirty ? "in_progress" : "clean", + isCurrent: currentId === "live", + isExact: true, + repository: liveRepository, + outcome: goal, + architecture, + architectureSvg: architecture ? architectureSvg(architecture) : null, + evidence: [], + summary: null, + humanReview: null, + actors: foldedSessions().filter((session) => session.active && session.currentWork?.outcomeId === goalId).map((session) => session.actor), + reviews: [], + decisions: recentEvents(decisionsPath, 200), + contextHistory: history.slice(0, 30), + changedFiles: liveRepository.files, + shareHref: `/export/project-update.html`, + }, + }; + } + + const record = records.find((item) => item.snapshot.id === selectedId); + if (!record) throw new Error("Snapshot not found"); + const snapshot = record.snapshot; + const currentDigest = repositoryDigest(snapshot.repository.baseSha, liveRepository.headSha, liveRepository.files); + const decisions = recentEvents(decisionsPath, 200).filter((item) => item.createdAt <= snapshot.generatedAt); + return { + schemaVersion: "keyoku.dev/canonical-record/v1alpha1", + goal, + roadmap: roadmap?.goalId === goalId ? roadmap : null, + currentId, + selectedId, + snapshots: summaries, + selected: { + id: snapshot.id, + kind: "factfile", + contributionId: record.contributionId, + title: snapshot.contribution?.title || snapshot.outcome?.title, + generatedAt: snapshot.generatedAt, + state: snapshot.state, + isCurrent: currentId === snapshot.id, + isExact: snapshot.repository.headSha === liveRepository.headSha && snapshot.repository.worktreeDigest === currentDigest, + digest: snapshot.digest, + repository: snapshot.repository, + outcome: snapshot.outcome || goal, + architecture: snapshot.architecture || null, + architectureSvg: snapshot.architecture ? architectureSvg(snapshot.architecture) : null, + evidence: snapshot.evidence || [], + summary: snapshot.summary, + humanReview: snapshot.humanReview, + actors: snapshot.contribution?.actors || [], + reviews: snapshot.reviews || [], + decisions, + contextHistory: history.filter((item) => item.createdAt <= snapshot.generatedAt).slice(0, 30), + changedFiles: (snapshot.repository.changedFiles || []).map((path) => ({ path })), + shareHref: `/artifacts/snapshots/${encodeURIComponent(record.contributionId)}/${encodeURIComponent(snapshot.id)}`, + }, + }; +} + +function contextPacket(question = "") { + const state = projectState(); + const accepted = state.decisions.filter((event) => event.choice === "accepted"); + const pending = state.steering.filter((event) => event.status === "queued"); + const pendingInterventions = state.interventions.filter((event) => !["applied", "verified", "declined", "expired", "superseded", "could_not_apply", "cancelled"].includes(event.phase)); + return { + schemaVersion: "keyoku.dev/agent-context/v1alpha1", + role: "Current contributing agent", + project: { id: "keyoku", name: "Keyoku", root: projectRoot }, + snapshot: state.repository, + goal: state.goals.focused ? { id: state.goals.focused.id, title: state.goals.focused.title, objective: state.goals.focused.objective } : null, + activeGoals: state.goals.active.map((goal) => ({ id: goal.id, title: goal.title })), + currentState: [ + "The human-facing project brief and authenticated local relay are implemented as a prototype.", + "Keyoku is provider-neutral: MCP is the default connection; harness adapters are optional accelerators.", + "The repository scanner and a truly live bidirectional harness adapter remain unproven.", + ], + humanDecisions: accepted.map((event) => ({ id: event.decisionId, choice: event.choice })), + pendingSteering: pending.map((event) => ({ id: event.id, kind: event.kind, message: event.message })), + pendingInterventions: pendingInterventions.map((event) => ({ id: event.id, kind: event.kind, message: event.message, phase: event.phase, delivery: event.delivery })), + question: safeText(question, 2_000), + responseContract: [ + "Answer the question directly in plain language.", + "State your recommendation and its consequence.", + "Name affected goals or components and link evidence instead of pasting raw logs.", + "Say whether human action is required; do not manufacture a decision when safe policy covers it.", + "Publish an understood receipt after interpreting an intervention, an applied receipt only after it changes the work, and a verified receipt only with evidence.", + ], + }; +} + +function promptForAgent(packet) { + return `You are the current coding agent for this project. Use the following compact Keyoku context packet. Do not assume a specific agent harness.\n\n${JSON.stringify(packet, null, 2)}\n\nRespond using the responseContract. If Keyoku MCP is connected, call project_orient first, publish a session heartbeat, read interventions, and record semantic receipts after you understand or change the work.`; +} + +function exportJson() { + const state = projectState(); + return { + schemaVersion: "keyoku.dev/project-status/v1alpha1", + generatedAt: new Date().toISOString(), + project: { id: "keyoku", name: "Keyoku", repository: "https://github.com/Keyoku-ai/keyoku" }, + snapshot: state.repository, + goals: state.goals, + roadmap: state.roadmap, + architecture: architectureState(), + proof: state.proof, + steering: state.steering, + interventions: state.interventions, + agents: state.agents, + decisions: state.decisions, + history: state.history, + }; +} + +function exportMarkdown() { + const state = exportJson(); + const roadmap = state.roadmap?.milestones || []; + const artifacts = state.proof?.artifacts || []; + return `# Keyoku project status\n\nGenerated ${state.generatedAt}\n\n## Focused goal\n\n**${state.goals.focused?.title || "No focused goal"}**\n\n${state.goals.focused?.objective || ""}\n\n## Snapshot\n\n- Branch: ${state.snapshot.branch}\n- Head: ${state.snapshot.head}\n- Working files: ${state.snapshot.changedFiles}\n- Active agents: ${state.agents.active.length}\n- Active goals: ${state.goals.count}\n\n## Roadmap\n\nIteration ${state.roadmap?.iteration?.current || "?"} of ${state.roadmap?.iteration?.target || "?"}\n\n${roadmap.map((item) => `- **${item.status}** — ${item.title} (target iteration ${item.targetIteration})`).join("\n")}\n\n## Factfiles\n\n${artifacts.map((item) => `- **${item.title}** — ${item.automated.passed}/${item.automated.total} automated; ${item.human.pending} human pending; ${item.state}`).join("\n") || "- No Factfiles yet"}\n\n## Decisions\n\n${state.decisions.map((item) => `- ${item.decisionId}: ${item.choice}`).join("\n") || "- No decisions recorded"}\n`; +} + +function htmlEscape(value) { + return String(value).replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]); +} + +function exportHtml() { + const state = exportJson(); + const focused = state.goals.focused; + const roadmap = state.roadmap?.milestones || []; + const artifacts = state.proof?.artifacts || []; + const architecture = state.architecture ? architectureSvg(state.architecture).replace(/^No architecture projection.

"; + return `
Keyoku project status
Keyoku · shareable project status

${htmlEscape(focused?.title || "No focused goal")}

${htmlEscape(focused?.objective || "")}

Iteration${htmlEscape(state.roadmap?.iteration?.current || "?")} / ${htmlEscape(state.roadmap?.iteration?.target || "?")}
Goals${state.goals.count}
Agents${state.agents.active.length} active
Snapshot${htmlEscape(state.snapshot.head)}
Roadmap

How this converges

${roadmap.map((item) => `
${htmlEscape(item.title)}

${htmlEscape(item.proof)}

iteration ${htmlEscape(item.targetIteration)}
`).join("")}
Proof

Contribution Factfiles

${artifacts.slice(0,8).map((item) => `
${htmlEscape(item.title)}${item.automated.passed}/${item.automated.total} automated · ${item.human.pending} human pending · ${htmlEscape(item.state)}
`).join("") || "

No Factfiles yet.

"}
Architecture

Current system projection

${architecture}
Decisions

Human-owned direction

${state.decisions.map((item) => `
${htmlEscape(item.decisionId)}
${htmlEscape(item.choice)}
`).join("") || "

No decisions recorded.

"}
Status

Current execution

${state.agents.active.length ? htmlEscape(state.agents.active.map((item) => `${item.actor.name}: ${item.currentWork?.summary || item.status}`).join(" · ")) : "No agent has a current heartbeat lease."}

${state.snapshot.changedFiles} working-tree files · ${htmlEscape(state.snapshot.branch)} at ${htmlEscape(state.snapshot.head)}

Estimate confidence: ${htmlEscape(state.roadmap?.iteration?.confidence || "unrated")} · ${htmlEscape(state.roadmap?.iteration?.basis || "")}

Generated ${htmlEscape(state.generatedAt)} · Portable status snapshot, not an accepted Factfile unless human review says so.

`; +} + +const server = createServer(async (req, res) => { + try { + const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`); + + if (url.searchParams.get("token") === token) { + res.writeHead(302, { + Location: url.pathname, + "Set-Cookie": `${sessionCookie}; HttpOnly; SameSite=Strict; Path=/; Max-Age=28800`, + "Cache-Control": "no-store", + }); + res.end(); + return; + } + + if (!authenticated(req, url)) { + res.writeHead(401, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }); + res.end("This Keyoku briefing link is missing or has an expired session token."); + return; + } + + if (req.method === "GET" && url.pathname === "/api/state") { + json(res, 200, projectState()); + return; + } + + if (req.method === "GET" && url.pathname === "/api/context") { + json(res, 200, contextPacket(url.searchParams.get("question") || "")); + return; + } + + if (req.method === "GET" && url.pathname === "/api/architecture") { + const architecture = architectureState(); + if (!architecture) return json(res, 404, { error: "No architecture contract exists" }); + json(res, 200, architecture); + return; + } + + if (req.method === "GET" && url.pathname === "/api/view") { + json(res, 200, projectView()); + return; + } + + if (req.method === "GET" && url.pathname === "/api/record") { + json(res, 200, projectRecord(safeText(url.searchParams.get("goalId"), 200), safeText(url.searchParams.get("snapshotId"), 200))); + return; + } + + if (req.method === "POST" && url.pathname === "/api/snapshots/current") { + if (!assertLocalOrigin(req)) return json(res, 403, { error: "Origin does not match this Keyoku session" }); + const input = await body(req); + const goalId = safeText(input.goalId, 200); + const snapshotId = safeText(input.snapshotId, 200); + const record = projectRecord(goalId, snapshotId); + if (!goalId || record.goal?.id !== goalId || record.selectedId !== snapshotId) return json(res, 404, { error: "Goal snapshot not found" }); + const event = { eventType: "snapshot.current", eventId: protocolId("evt"), goalId, snapshotId, previousSnapshotId: record.currentId, actor: { kind: "human", id: "owner", name: "Tye" }, reason: safeText(input.reason, 1_000) || "Selected as the current Keyoku review baseline. Git was not changed.", createdAt: new Date().toISOString() }; + appendEvent(currentSnapshotsPath, event); + broadcast("state", { kind: "snapshot.current", event }); + json(res, 201, { event, record: projectRecord(goalId, snapshotId) }); + return; + } + + if (req.method === "POST" && url.pathname === "/api/goals/focus") { + if (!assertLocalOrigin(req)) return json(res, 403, { error: "Origin does not match this Keyoku session" }); + const input = await body(req); + const goalId = safeText(input.goalId, 200); + const goals = projectGoals(); + if (!goals.active.some((goal) => goal.id === goalId)) return json(res, 404, { error: "Project goal not found" }); + const event = { + eventType: "goal.focused", + eventId: protocolId("evt"), + goalId, + actor: { kind: "human", id: "owner", name: "Tye" }, + reason: safeText(input.reason, 1_000) || "Focused from the Keyoku project interface.", + createdAt: new Date().toISOString(), + }; + appendEvent(goalFocusPath, event); + broadcast("state", { kind: "goal.focused", event }); + json(res, 201, { event, goals: projectGoals() }); + return; + } + + if (req.method === "POST" && url.pathname === "/api/dispatch") { + if (!assertLocalOrigin(req)) return json(res, 403, { error: "Origin does not match this Keyoku session" }); + const input = await body(req); + const goals = projectGoals(); + const goalId = safeText(input.goalId, 200) || goals.focusedId; + const adapterId = safeText(input.adapterId, 200); + const goal = goals.active.find((item) => item.id === goalId); + const adapter = projectHarnesses().find((item) => item.id === adapterId); + if (!goal) return json(res, 404, { error: "Project goal not found" }); + if (!adapter) return json(res, 404, { error: "Enabled harness adapter not found" }); + const dispatch = startHeadlessDispatch({ adapter, goal }); + broadcast("state", { kind: "dispatch.started", dispatch }); + json(res, 201, { dispatch, message: `${adapter.label} started for ${goal.title}` }); + return; + } + + if (req.method === "GET" && url.pathname === "/api/events") { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache, no-transform", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + }); + res.write(`event: connected\ndata: ${JSON.stringify(projectState())}\n\n`); + clients.add(res); + req.on("close", () => clients.delete(res)); + return; + } + + if (req.method === "POST" && url.pathname === "/api/steer") { + if (!assertLocalOrigin(req)) return json(res, 403, { error: "Origin does not match this Keyoku session" }); + const input = await body(req); + const message = safeText(input.message, 2_000); + const kind = safeText(input.kind, 40) || "direction"; + if (!message) return json(res, 400, { error: "A steering message is required" }); + const event = { id: `steer_${Date.now().toString(36)}`, kind, message, actor: { kind: "human", name: "Tye" }, createdAt: new Date().toISOString(), status: "queued" }; + appendEvent(steeringPath, event); + broadcast("state", { kind: "steering", event }); + json(res, 201, event); + return; + } + + if (req.method === "POST" && url.pathname === "/api/ask") { + if (!assertLocalOrigin(req)) return json(res, 403, { error: "Origin does not match this Keyoku session" }); + const input = await body(req); + const question = safeText(input.question, 2_000); + if (!question) return json(res, 400, { error: "A question is required" }); + const packet = contextPacket(question); + const event = { + id: `ask_${Date.now().toString(36)}`, + kind: "question", + message: question, + actor: { kind: "human", name: "Tye" }, + createdAt: new Date().toISOString(), + status: "queued", + }; + appendEvent(steeringPath, event); + broadcast("state", { kind: "question", event }); + json(res, 201, { + mode: "copy", + delivery: "queued_for_connected_agents", + event, + prompt: promptForAgent(packet), + note: "Keyoku recorded this in the project inbox. Copy the prompt into an active agent session unless a live harness adapter is connected.", + }); + return; + } + + if (req.method === "POST" && url.pathname === "/api/interventions") { + if (!assertLocalOrigin(req)) return json(res, 403, { error: "Origin does not match this Keyoku session" }); + const input = await body(req); + const message = safeText(input.message, 4_000); + const allowedKinds = new Set(["query", "direction", "decision_response", "control", "proof_challenge"]); + const allowedPolicies = new Set(["when_available", "next_checkpoint", "interrupt_now"]); + const kind = allowedKinds.has(input.kind) ? input.kind : "query"; + const policy = allowedPolicies.has(input.deliveryPolicy) ? input.deliveryPolicy : "next_checkpoint"; + const targetMode = ["current", "session", "all"].includes(input.targetMode) ? input.targetMode : "current"; + const targetSessionId = safeText(input.targetSessionId, 200); + if (!message) return json(res, 400, { error: "An intervention message is required" }); + if (targetMode === "session" && !targetSessionId) return json(res, 400, { error: "targetSessionId is required for a session target" }); + const id = protocolId("int"); + const repository = repositoryState(); + const focusedGoalId = projectGoals().focusedId; + const event = { + schemaVersion: "keyoku.dev/intervention/v1alpha1", + eventType: "intervention.created", + eventId: protocolId("evt"), + id, + projectId: "keyoku", + threadId: "project", + correlationId: id, + kind, + message, + actor: { kind: "human", id: "owner", name: "Tye" }, + target: { mode: targetMode, ...(targetSessionId ? { sessionId: targetSessionId } : {}) }, + scope: { ...(focusedGoalId ? { outcomeId: focusedGoalId } : {}), snapshotRef: repository.head }, + delivery: { + policy, + require: Array.isArray(input.require) && input.require.length ? input.require.filter((value) => ["understood", "applied", "verified"].includes(value)) : ["understood", "applied"], + }, + createdAt: new Date().toISOString(), + idempotencyKey: safeText(input.idempotencyKey, 300) || protocolId("idem"), + phase: "committed", + receipts: [], + }; + appendEvent(protocolPath, event); + broadcast("state", { kind: "intervention", event }); + const activeAgents = foldedSessions().filter((session) => session.active && (!focusedGoalId || session.currentWork?.outcomeId === focusedGoalId)); + const packet = contextPacket(message); + json(res, 201, { + event, + delivery: { + state: "committed", + activeTargets: activeAgents.length, + mode: activeAgents.length ? "adapter_or_poll" : "durable_inbox", + note: activeAgents.length + ? "Committed to the project thread. Target agents must publish semantic receipts as they understand and apply it." + : "Committed to the durable inbox. No agent heartbeat is active, so this has not been delivered yet.", + }, + fallbackPrompt: promptForAgent(packet), + }); + return; + } + + const cancelMatch = url.pathname.match(/^\/api\/interventions\/([^/]+)\/cancel$/); + if (req.method === "POST" && cancelMatch) { + if (!assertLocalOrigin(req)) return json(res, 403, { error: "Origin does not match this Keyoku session" }); + const interventionId = decodeURIComponent(cancelMatch[1]); + const intervention = foldedInterventions(500).find((item) => item.id === interventionId); + if (!intervention) return json(res, 404, { error: "Intervention not found" }); + if (["applied", "verified", "declined", "expired", "superseded", "could_not_apply", "cancelled"].includes(intervention.phase)) { + return json(res, 409, { error: `A ${intervention.phase} intervention cannot be cancelled` }); + } + const receipt = { + eventType: "intervention.receipt", + eventId: protocolId("evt"), + interventionId, + phase: "cancelled", + summary: "Cancelled by the accountable human before application.", + actor: { kind: "human", id: "owner", name: "Tye" }, + createdAt: new Date().toISOString(), + }; + appendEvent(protocolPath, receipt); + broadcast("state", { kind: "intervention.receipt", event: receipt }); + json(res, 201, { intervention: foldedInterventions(500).find((item) => item.id === interventionId), receipt }); + return; + } + + if (req.method === "POST" && url.pathname === "/api/agent-heartbeat") { + if (!assertLocalOrigin(req)) return json(res, 403, { error: "Origin does not match this Keyoku session" }); + const input = await body(req); + const sessionId = safeText(input.sessionId, 200); + const harness = safeText(input.harness, 200); + const actorName = safeText(input.actorName, 200) || "Coding agent"; + const transport = safeText(input.transport, 100); + const allowedStatuses = new Set(["idle", "working", "waiting", "blocked", "disconnected"]); + const status = allowedStatuses.has(input.status) ? input.status : "working"; + if (!sessionId || !harness || !transport) return json(res, 400, { error: "sessionId, harness, and transport are required" }); + const leaseSeconds = Math.max(15, Math.min(Number(input.leaseSeconds) || 45, 300)); + const now = new Date(); + const event = { + schemaVersion: "keyoku.dev/agent-session/v1alpha1", + eventType: "agent.heartbeat", + eventId: protocolId("evt"), + sessionId, + actor: { kind: "agent", id: safeText(input.actorId, 200) || sessionId, name: actorName, harness, ...(safeText(input.model, 200) ? { model: safeText(input.model, 200) } : {}) }, + status, + ...(safeText(input.workSummary, 1_000) ? { currentWork: { + summary: safeText(input.workSummary, 1_000), + ...(safeText(input.outcomeId, 200) ? { outcomeId: safeText(input.outcomeId, 200) } : {}), + ...(safeText(input.contributionId, 200) ? { contributionId: safeText(input.contributionId, 200) } : {}), + ...(Array.isArray(input.capabilityIds) ? { capabilityIds: input.capabilityIds.map((value) => safeText(value, 200)).filter(Boolean).slice(0, 30) } : {}), + ...(Array.isArray(input.paths) ? { paths: input.paths.map((value) => safeText(value, 500)).filter(Boolean).slice(0, 100) } : {}), + baseSnapshot: repositoryState().head, + } } : {}), + capabilities: Array.isArray(input.capabilities) ? input.capabilities.map((value) => safeText(value, 100)).filter(Boolean).slice(0, 30) : [], + transport, + createdAt: now.toISOString(), + leaseUntil: new Date(now.getTime() + leaseSeconds * 1_000).toISOString(), + active: status !== "disconnected", + }; + appendEvent(sessionsPath, event); + broadcast("state", { kind: "agent.heartbeat", event }); + json(res, 201, event); + return; + } + + if (req.method === "POST" && url.pathname === "/api/decision") { + if (!assertLocalOrigin(req)) return json(res, 403, { error: "Origin does not match this Keyoku session" }); + const input = await body(req); + const decisionId = safeText(input.decisionId, 100); + const choice = safeText(input.choice, 100); + if (!decisionId || !choice) return json(res, 400, { error: "decisionId and choice are required" }); + const event = { id: `decision_${Date.now().toString(36)}`, decisionId, choice, actor: { kind: "human", name: "Tye" }, createdAt: new Date().toISOString() }; + appendEvent(decisionsPath, event); + broadcast("state", { kind: "decision", event }); + json(res, 201, event); + return; + } + + if (req.method === "GET" && url.pathname === "/export/project-brief.json") { + res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Content-Disposition": "attachment; filename=keyoku-project-brief.json", "Cache-Control": "no-store" }); + res.end(JSON.stringify(exportJson(), null, 2)); + return; + } + + if (req.method === "GET" && url.pathname === "/export/project-update.md") { + res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Content-Disposition": "attachment; filename=keyoku-project-update.md", "Cache-Control": "no-store" }); + res.end(exportMarkdown()); + return; + } + + if (req.method === "GET" && url.pathname === "/export/project-update.html") { + res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Disposition": "attachment; filename=keyoku-project-update.html", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" }); + res.end(exportHtml()); + return; + } + + if (req.method === "GET" && url.pathname === "/export/architecture.svg") { + const architecture = architectureState(); + if (!architecture) return json(res, 404, { error: "No architecture contract exists" }); + res.writeHead(200, { "Content-Type": "image/svg+xml; charset=utf-8", "Content-Disposition": "attachment; filename=keyoku-architecture.svg", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" }); + res.end(architectureSvg(architecture)); + return; + } + + const factfileMatch = url.pathname.match(/^\/artifacts\/factfiles\/([^/]+)$/); + if (req.method === "GET" && factfileMatch) { + const contributionId = decodeURIComponent(factfileMatch[1]); + if (!/^[a-zA-Z0-9._-]+$/.test(contributionId)) return json(res, 400, { error: "Invalid contribution id" }); + const artifact = resolve(contributionsRoot, contributionId, "factfile.html"); + if (!artifact.startsWith(`${resolve(contributionsRoot)}/`) || !existsSync(artifact)) return json(res, 404, { error: "Factfile not found" }); + res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Content-Security-Policy": "default-src 'none'; img-src data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'", "X-Content-Type-Options": "nosniff" }); + createReadStream(artifact).pipe(res); + return; + } + + const snapshotMatch = url.pathname.match(/^\/artifacts\/snapshots\/([^/]+)\/([^/]+)$/); + if (req.method === "GET" && snapshotMatch) { + const contributionId = decodeURIComponent(snapshotMatch[1]); + const snapshotId = decodeURIComponent(snapshotMatch[2]); + if (!/^[a-zA-Z0-9._-]+$/.test(contributionId) || !/^[a-zA-Z0-9._-]+$/.test(snapshotId)) return json(res, 400, { error: "Invalid snapshot reference" }); + const artifact = resolve(contributionsRoot, contributionId, "snapshots", `${snapshotId}.html`); + if (!artifact.startsWith(`${resolve(contributionsRoot)}/`) || !existsSync(artifact)) return json(res, 404, { error: "Snapshot artifact not found" }); + res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Content-Security-Policy": "default-src 'none'; img-src data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'", "X-Content-Type-Options": "nosniff" }); + createReadStream(artifact).pipe(res); + return; + } + + if (req.method !== "GET") return json(res, 405, { error: "Method not allowed" }); + + const requested = url.pathname === "/" ? pagePath : join(docsRoot, normalize(url.pathname).replace(/^[/\\]+/, "")); + const absolute = resolve(requested); + if (!absolute.startsWith(`${docsRoot}/`) && absolute !== pagePath) return json(res, 404, { error: "Not found" }); + if (!existsSync(absolute) || !statSync(absolute).isFile()) return json(res, 404, { error: "Not found" }); + res.writeHead(200, { + "Content-Type": mime(absolute), + "Cache-Control": "no-store", + "Content-Security-Policy": "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'", + "Referrer-Policy": "no-referrer", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + }); + createReadStream(absolute).pipe(res); + } catch (error) { + json(res, 500, { error: error instanceof Error ? error.message : String(error) }); + } +}); + +const watched = [pagePath, join(projectRoot, ".keyoku")]; +for (const target of watched) { + if (!existsSync(target)) continue; + watch(target, { recursive: statSync(target).isDirectory() }, (_event, filename) => { + broadcast(target === pagePath ? "page" : "state", { changed: filename || target, at: new Date().toISOString() }); + }); +} + +let lastRepositorySignature = JSON.stringify(repositoryState()); +setInterval(() => { + const repository = repositoryState(); + const signature = JSON.stringify(repository); + if (signature !== lastRepositorySignature) { + lastRepositorySignature = signature; + broadcast("state", { kind: "repository", repository, at: new Date().toISOString() }); + } +}, 2_000).unref(); + +function addresses() { + if (!lan) return ["127.0.0.1"]; + const values = []; + for (const records of Object.values(networkInterfaces())) { + for (const record of records || []) { + if (record.family === "IPv4" && !record.internal) values.push(record.address); + } + } + return values.length ? values : ["127.0.0.1"]; +} + +server.listen(port, host, () => { + console.log("Keyoku project brief is live."); + console.log(""); + for (const address of addresses()) console.log(` http://${address}:${port}/?token=${token}`); + console.log(""); + console.log(lan ? "Anyone on this local network with the temporary link can view and steer this session." : "This session is available only on this device. Add --lan to create a phone-accessible local-network link."); + console.log("Press Ctrl+C to stop sharing."); +}); + +function shutdown() { + broadcast("closed", { at: new Date().toISOString() }); + for (const client of clients) client.end(); + server.close(() => process.exit(0)); +} +process.on("SIGINT", shutdown); +process.on("SIGTERM", shutdown); diff --git a/archive/experimental-control-plane/src/presentation.ts b/archive/experimental-control-plane/src/presentation.ts new file mode 100644 index 0000000..824b255 --- /dev/null +++ b/archive/experimental-control-plane/src/presentation.ts @@ -0,0 +1,109 @@ +import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { randomBytes } from "node:crypto"; +import { parse as parseYaml } from "yaml"; + +export type ViewField = { + value: string; + description: string; + source: "manifest" | "agent"; + updatedAt?: string; + actor?: { id: string; harness?: string; model?: string }; + confidence?: number; +}; + +export type ProjectView = { + schemaVersion: "keyoku.dev/project-view/v1alpha1"; + template: string; + fields: Record; +}; + +type ViewManifest = { + schemaVersion?: string; + template?: string; + fields?: Record; +}; + +type ViewPublication = { + eventType: "view.fields.published"; + eventId: string; + fields: Record; + actor: { id: string; harness?: string; model?: string }; + confidence: number; + reason: string; + createdAt: string; +}; + +const FIELD_NAME = /^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+$/; + +function manifestPath(root: string): string { + return join(root, ".keyoku", "view.yaml"); +} + +function eventsPath(root: string): string { + return join(root, ".keyoku", "runtime", "view-events.jsonl"); +} + +function publications(root: string): ViewPublication[] { + const path = eventsPath(root); + if (!existsSync(path)) return []; + return readFileSync(path, "utf8").split("\n").filter(Boolean).flatMap((line) => { + try { + const event = JSON.parse(line) as ViewPublication; + return event.eventType === "view.fields.published" ? [event] : []; + } catch { + return []; + } + }); +} + +export function readProjectView(root: string): ProjectView { + const path = manifestPath(root); + if (!existsSync(path)) throw new Error("No .keyoku/view.yaml presentation manifest exists"); + const manifest = parseYaml(readFileSync(path, "utf8")) as ViewManifest; + const fields: Record = {}; + for (const [name, field] of Object.entries(manifest.fields ?? {})) { + if (!FIELD_NAME.test(name) || typeof field.value !== "string") continue; + fields[name] = { + value: field.value, + description: typeof field.description === "string" ? field.description : "Agent-editable presentation field.", + source: "manifest", + }; + } + for (const event of publications(root)) { + for (const [name, value] of Object.entries(event.fields)) { + if (!fields[name]) continue; + fields[name] = { ...fields[name], value, source: "agent", updatedAt: event.createdAt, actor: event.actor, confidence: event.confidence }; + } + } + return { schemaVersion: "keyoku.dev/project-view/v1alpha1", template: manifest.template || "convergence-thread", fields }; +} + +export function publishProjectView( + root: string, + input: { fields: Record; actor: { id: string; harness?: string; model?: string }; confidence?: number; reason: string }, +): ViewPublication { + const current = readProjectView(root); + const fields: Record = {}; + for (const [name, raw] of Object.entries(input.fields)) { + if (!FIELD_NAME.test(name) || !current.fields[name]) throw new Error(`Unknown or protected view field '${name}'`); + const value = raw.trim(); + if (!value) throw new Error(`View field '${name}' cannot be empty`); + if (value.length > 2_000) throw new Error(`View field '${name}' exceeds 2,000 characters`); + fields[name] = value; + } + if (!Object.keys(fields).length) throw new Error("At least one view field is required"); + const event: ViewPublication = { + eventType: "view.fields.published", + eventId: `view_${Date.now().toString(36)}_${randomBytes(5).toString("base64url")}`, + fields, + actor: input.actor, + confidence: Math.max(0, Math.min(1, input.confidence ?? 0.8)), + reason: input.reason.trim().slice(0, 1_000), + createdAt: new Date().toISOString(), + }; + const path = eventsPath(root); + mkdirSync(dirname(path), { recursive: true }); + appendFileSync(path, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600 }); + return event; +} diff --git a/archive/experimental-control-plane/src/project-state.ts b/archive/experimental-control-plane/src/project-state.ts new file mode 100644 index 0000000..a26d038 --- /dev/null +++ b/archive/experimental-control-plane/src/project-state.ts @@ -0,0 +1,484 @@ +import { execFileSync } from "node:child_process"; +import { randomBytes } from "node:crypto"; +import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +import { listOutcomes, loadProject } from "./contribution.js"; + +export type SteeringStatus = "queued" | "acknowledged" | "applied" | "superseded" | "could_not_apply"; + +export type InterventionKind = "query" | "direction" | "decision_response" | "control" | "proof_challenge"; +export type InterventionPhase = + | "committed" + | "delivered" + | "understood" + | "planned" + | "applied" + | "verified" + | "declined" + | "expired" + | "superseded" + | "could_not_apply" + | "cancelled"; +export type DeliveryPolicy = "when_available" | "next_checkpoint" | "interrupt_now"; +export type AgentSessionStatus = "idle" | "working" | "waiting" | "blocked" | "disconnected"; + +export interface ProtocolActor { + kind: "human" | "agent" | "system"; + id: string; + name: string; + harness?: string; + model?: string; +} + +export interface InterventionReceipt { + eventType: "intervention.receipt"; + eventId: string; + interventionId: string; + phase: Exclude; + summary: string; + actor: ProtocolActor; + createdAt: string; + evidenceRefs?: string[]; +} + +export interface Intervention { + schemaVersion: "keyoku.dev/intervention/v1alpha1"; + eventType: "intervention.created"; + eventId: string; + id: string; + projectId: string; + threadId: string; + correlationId: string; + causationId?: string; + kind: InterventionKind; + message: string; + actor: ProtocolActor; + target: { mode: "current" | "session" | "all"; sessionId?: string }; + scope: { outcomeId?: string; contributionId?: string; snapshotRef: string }; + delivery: { policy: DeliveryPolicy; require: Array<"understood" | "applied" | "verified"> }; + createdAt: string; + expiresAt?: string; + idempotencyKey: string; + phase: InterventionPhase; + receipts: InterventionReceipt[]; +} + +export interface AgentSession { + schemaVersion: "keyoku.dev/agent-session/v1alpha1"; + eventType: "agent.heartbeat"; + eventId: string; + sessionId: string; + actor: ProtocolActor; + status: AgentSessionStatus; + currentWork?: { + outcomeId?: string; + contributionId?: string; + summary: string; + capabilityIds?: string[]; + paths?: string[]; + baseSnapshot?: string; + }; + capabilities: string[]; + transport: string; + createdAt: string; + leaseUntil: string; + active: boolean; +} + +export interface AgentCoordinationConflict { + sessions: [string, string]; + reason: "same_contribution" | "overlapping_path"; + scope: string; +} + +export interface SteeringRequest { + id: string; + kind: string; + message: string; + actor?: { kind?: string; name?: string }; + createdAt: string; + status: SteeringStatus; + acknowledgement?: { + summary: string; + actor: string; + createdAt: string; + }; +} + +interface SteeringAcknowledgement { + eventType: "acknowledgement"; + steeringId: string; + status: Exclude; + summary: string; + actor: string; + createdAt: string; +} + +function jsonLines(path: string): unknown[] { + if (!existsSync(path)) return []; + return readFileSync(path, "utf8") + .split("\n") + .filter(Boolean) + .flatMap((line) => { + try { return [JSON.parse(line) as unknown]; } catch { return []; } + }); +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object"; +} + +function steeringPath(root: string): string { + return join(root, ".keyoku", "runtime", "human-steering.jsonl"); +} + +function protocolPath(root: string): string { + return join(root, ".keyoku", "runtime", "thread-events.jsonl"); +} + +function sessionPath(root: string): string { + return join(root, ".keyoku", "runtime", "agent-sessions.jsonl"); +} + +function eventId(prefix: string): string { + return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url")}`; +} + +function appendJsonLine(path: string, value: unknown): void { + mkdirSync(dirname(path), { recursive: true }); + appendFileSync(path, `${JSON.stringify(value)}\n`, { encoding: "utf8", mode: 0o600 }); +} + +function decisionsPath(root: string): string { + return join(root, ".keyoku", "runtime", "human-decisions.jsonl"); +} + +function goalFocusPath(root: string): string { + return join(root, ".keyoku", "runtime", "goal-focus.jsonl"); +} + +export function focusedProjectGoalId(root: string): string | undefined { + const events = jsonLines(goalFocusPath(root)).filter(isRecord); + for (let index = events.length - 1; index >= 0; index -= 1) { + const event = events[index]; + if (event && event.eventType === "goal.focused" && typeof event.goalId === "string") return event.goalId; + } + return undefined; +} + +function git(root: string, args: string[], fallback = "unknown"): string { + try { + return execFileSync("git", args, { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim() || fallback; + } catch { + return fallback; + } +} + +export function listSteering(root: string): SteeringRequest[] { + const requests = new Map(); + const acknowledgements: SteeringAcknowledgement[] = []; + + for (const value of jsonLines(steeringPath(root))) { + if (!isRecord(value)) continue; + if (value.eventType === "acknowledgement") { + if ( + typeof value.steeringId === "string" && + typeof value.status === "string" && + typeof value.summary === "string" && + typeof value.actor === "string" && + typeof value.createdAt === "string" + ) acknowledgements.push(value as unknown as SteeringAcknowledgement); + continue; + } + if (typeof value.id !== "string" || typeof value.message !== "string" || typeof value.createdAt !== "string") continue; + requests.set(value.id, { + id: value.id, + kind: typeof value.kind === "string" ? value.kind : "direction", + message: value.message, + actor: isRecord(value.actor) ? { + kind: typeof value.actor.kind === "string" ? value.actor.kind : undefined, + name: typeof value.actor.name === "string" ? value.actor.name : undefined, + } : undefined, + createdAt: value.createdAt, + status: value.status === "acknowledged" || value.status === "applied" || value.status === "superseded" || value.status === "could_not_apply" + ? value.status + : "queued", + }); + } + + for (const acknowledgement of acknowledgements) { + const request = requests.get(acknowledgement.steeringId); + if (!request) continue; + request.status = acknowledgement.status; + request.acknowledgement = { + summary: acknowledgement.summary, + actor: acknowledgement.actor, + createdAt: acknowledgement.createdAt, + }; + } + + return [...requests.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt)); +} + +export function acknowledgeSteering(input: { + root: string; + steeringId: string; + status: Exclude; + summary: string; + actor: string; +}): SteeringRequest { + const request = listSteering(input.root).find((item) => item.id === input.steeringId); + if (!request) throw new Error(`Unknown steering request '${input.steeringId}'.`); + const event: SteeringAcknowledgement = { + eventType: "acknowledgement", + steeringId: input.steeringId, + status: input.status, + summary: input.summary.trim(), + actor: input.actor.trim(), + createdAt: new Date().toISOString(), + }; + if (!event.summary || !event.actor) throw new Error("summary and actor are required"); + const path = steeringPath(input.root); + mkdirSync(dirname(path), { recursive: true }); + appendFileSync(path, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600 }); + return listSteering(input.root).find((item) => item.id === input.steeringId)!; +} + +export function createIntervention(input: { + root: string; + kind: InterventionKind; + message: string; + actor: ProtocolActor; + target?: Intervention["target"]; + outcomeId?: string; + contributionId?: string; + deliveryPolicy?: DeliveryPolicy; + require?: Intervention["delivery"]["require"]; + threadId?: string; + causationId?: string; + idempotencyKey?: string; + expiresAt?: string; +}): Intervention { + const message = input.message.trim(); + if (!message) throw new Error("message is required"); + const existing = input.idempotencyKey + ? listInterventions(input.root).find((item) => item.idempotencyKey === input.idempotencyKey) + : undefined; + if (existing) return existing; + const id = eventId("int"); + const intervention: Intervention = { + schemaVersion: "keyoku.dev/intervention/v1alpha1", + eventType: "intervention.created", + eventId: eventId("evt"), + id, + projectId: loadProject(input.root).id, + threadId: input.threadId || "project", + correlationId: id, + ...(input.causationId ? { causationId: input.causationId } : {}), + kind: input.kind, + message, + actor: input.actor, + target: input.target || { mode: "current" }, + scope: { + ...(input.outcomeId ? { outcomeId: input.outcomeId } : {}), + ...(input.contributionId ? { contributionId: input.contributionId } : {}), + snapshotRef: git(input.root, ["rev-parse", "HEAD"]), + }, + delivery: { + policy: input.deliveryPolicy || "next_checkpoint", + require: input.require || ["understood", "applied"], + }, + createdAt: new Date().toISOString(), + ...(input.expiresAt ? { expiresAt: input.expiresAt } : {}), + idempotencyKey: input.idempotencyKey || eventId("idem"), + phase: "committed", + receipts: [], + }; + appendJsonLine(protocolPath(input.root), intervention); + return intervention; +} + +export function recordInterventionReceipt(input: { + root: string; + interventionId: string; + phase: Exclude; + summary: string; + actor: ProtocolActor; + evidenceRefs?: string[]; +}): Intervention { + const intervention = listInterventions(input.root).find((item) => item.id === input.interventionId); + if (!intervention) throw new Error(`Unknown intervention '${input.interventionId}'.`); + const receipt: InterventionReceipt = { + eventType: "intervention.receipt", + eventId: eventId("evt"), + interventionId: input.interventionId, + phase: input.phase, + summary: input.summary.trim(), + actor: input.actor, + createdAt: new Date().toISOString(), + ...(input.evidenceRefs?.length ? { evidenceRefs: input.evidenceRefs } : {}), + }; + if (!receipt.summary) throw new Error("summary is required"); + appendJsonLine(protocolPath(input.root), receipt); + return listInterventions(input.root).find((item) => item.id === input.interventionId)!; +} + +export function listInterventions(root: string): Intervention[] { + const interventions = new Map(); + const receipts: InterventionReceipt[] = []; + for (const value of jsonLines(protocolPath(root))) { + if (!isRecord(value)) continue; + if (value.eventType === "intervention.receipt" && typeof value.interventionId === "string") { + receipts.push(value as unknown as InterventionReceipt); + } else if (value.eventType === "intervention.created" && typeof value.id === "string") { + interventions.set(value.id, { ...(value as unknown as Intervention), phase: "committed", receipts: [] }); + } + } + for (const receipt of receipts) { + const intervention = interventions.get(receipt.interventionId); + if (!intervention) continue; + intervention.receipts.push(receipt); + intervention.phase = receipt.phase; + } + return [...interventions.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt)); +} + +export function heartbeatAgentSession(input: { + root: string; + sessionId: string; + actor: ProtocolActor; + status: AgentSessionStatus; + currentWork?: AgentSession["currentWork"]; + capabilities?: string[]; + transport: string; + leaseSeconds?: number; +}): AgentSession { + const leaseSeconds = Math.max(15, Math.min(input.leaseSeconds || 45, 300)); + const now = new Date(); + const session: AgentSession = { + schemaVersion: "keyoku.dev/agent-session/v1alpha1", + eventType: "agent.heartbeat", + eventId: eventId("evt"), + sessionId: input.sessionId.trim(), + actor: input.actor, + status: input.status, + ...(input.currentWork ? { currentWork: input.currentWork } : {}), + capabilities: [...new Set(input.capabilities || [])], + transport: input.transport.trim(), + createdAt: now.toISOString(), + leaseUntil: new Date(now.getTime() + leaseSeconds * 1_000).toISOString(), + active: input.status !== "disconnected", + }; + if (!session.sessionId || !session.transport) throw new Error("sessionId and transport are required"); + appendJsonLine(sessionPath(input.root), session); + return session; +} + +export function listAgentSessions(root: string, now = new Date()): AgentSession[] { + const sessions = new Map(); + for (const value of jsonLines(sessionPath(root))) { + if (!isRecord(value) || value.eventType !== "agent.heartbeat" || typeof value.sessionId !== "string") continue; + sessions.set(value.sessionId, value as unknown as AgentSession); + } + return [...sessions.values()] + .map((session) => ({ + ...session, + active: session.status !== "disconnected" && new Date(session.leaseUntil).getTime() > now.getTime(), + })) + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)); +} + +export function findAgentCoordinationConflicts(sessions: AgentSession[]): AgentCoordinationConflict[] { + const active = sessions.filter((session) => session.active && session.currentWork); + const conflicts: AgentCoordinationConflict[] = []; + const pathOverlaps = (left: string, right: string) => { + const a = left.replace(/^\.\//, "").replace(/\/$/, ""); + const b = right.replace(/^\.\//, "").replace(/\/$/, ""); + return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`); + }; + for (let left = 0; left < active.length; left += 1) { + for (let right = left + 1; right < active.length; right += 1) { + const a = active[left]!; + const b = active[right]!; + if (a.currentWork?.contributionId && a.currentWork.contributionId === b.currentWork?.contributionId) { + conflicts.push({ sessions: [a.sessionId, b.sessionId], reason: "same_contribution", scope: a.currentWork.contributionId }); + } + for (const aPath of a.currentWork?.paths || []) { + for (const bPath of b.currentWork?.paths || []) { + if (pathOverlaps(aPath, bPath)) conflicts.push({ sessions: [a.sessionId, b.sessionId], reason: "overlapping_path", scope: aPath.length <= bPath.length ? aPath : bPath }); + } + } + } + } + return conflicts; +} + +export function buildProjectOrientation(root: string) { + const project = loadProject(root); + const outcomes = listOutcomes(root); + const decisions = jsonLines(decisionsPath(root)).filter(isRecord); + const latestDecisions = new Map>(); + for (const decision of decisions) { + if (typeof decision.decisionId === "string") latestDecisions.set(decision.decisionId, decision); + } + const steering = listSteering(root); + const pendingSteering = steering.filter((item) => item.status === "queued"); + const interventions = listInterventions(root); + const pendingInterventions = interventions.filter((item) => !["applied", "verified", "declined", "expired", "superseded", "could_not_apply", "cancelled"].includes(item.phase)); + const agentSessions = listAgentSessions(root); + const agentConflicts = findAgentCoordinationConflicts(agentSessions); + const status = git(root, ["status", "--porcelain=v1"], ""); + const changedFiles = status ? status.split("\n").filter(Boolean) : []; + const focusedGoalId = focusedProjectGoalId(root); + const currentOutcome = outcomes.find((outcome) => outcome.id === focusedGoalId) || [...outcomes].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0]; + + return { + schemaVersion: "keyoku.dev/project-orientation/v1alpha1", + project: { + id: project.id, + name: project.name, + summary: project.summary, + }, + snapshot: { + branch: git(root, ["branch", "--show-current"], "detached"), + head: git(root, ["rev-parse", "--short=12", "HEAD"]), + changedFiles: changedFiles.length, + exact: changedFiles.length === 0, + }, + currentGoal: currentOutcome ? { + id: currentOutcome.id, + title: currentOutcome.title, + objective: currentOutcome.objective, + owner: currentOutcome.owner, + } : null, + goals: { + focusedId: currentOutcome?.id || null, + active: outcomes.map((outcome) => ({ id: outcome.id, title: outcome.title, objective: outcome.objective, owner: outcome.owner, updatedAt: outcome.updatedAt })), + count: outcomes.length, + }, + humanAttention: { + pendingSteering, + pendingInterventions, + count: pendingSteering.length + pendingInterventions.length, + rule: "Interrupt a person only for a consequential, non-inferable, time-sensitive choice. Otherwise continue safely or include it in the next checkpoint.", + }, + decisions: [...latestDecisions.values()], + agents: { + active: agentSessions.filter((session) => session.active), + recent: agentSessions, + coordinationConflicts: agentConflicts, + rule: "A session is active only while its signed heartbeat lease is valid; repository activity alone is not presence.", + }, + instructions: [ + "Use this compact orientation before substantial work; retrieve detailed outcomes or contributions only when needed.", + "Treat human decisions as constraints and distinguish observed evidence from agent proposals.", + "If steering is queued, acknowledge it before claiming it changed the work.", + "Checkpoint at a meaningful outcome boundary; do not dump raw transcripts into project state.", + ], + }; +} diff --git a/archive/experimental-control-plane/tests/presentation.test.ts b/archive/experimental-control-plane/tests/presentation.test.ts new file mode 100644 index 0000000..26477d2 --- /dev/null +++ b/archive/experimental-control-plane/tests/presentation.test.ts @@ -0,0 +1,30 @@ +import { cpSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +import { publishProjectView, readProjectView } from "../src/presentation.js"; + +const roots: string[] = []; +afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }))); + +function fixture(): string { + const root = mkdtempSync(join(tmpdir(), "keyoku-view-")); roots.push(root); + mkdirSync(join(root, ".keyoku"), { recursive: true }); + writeFileSync(join(root, ".keyoku", "view.yaml"), "schemaVersion: keyoku.dev/project-view/v1alpha1\ntemplate: thread\nfields:\n header.summary:\n value: Original\n description: Header summary\n"); + return root; +} + +describe("agent-editable project view", () => { + it("publishes an attributed update to an allowlisted field", () => { + const root = fixture(); + publishProjectView(root, { fields: { "header.summary": "Current and useful" }, actor: { id: "ui-agent", harness: "Codex", model: "gpt-5.6-sol" }, confidence: 0.92, reason: "Project state changed" }); + const view = readProjectView(root); + expect(view.fields["header.summary"]).toMatchObject({ value: "Current and useful", source: "agent", confidence: 0.92 }); + }); + + it("protects unknown facts and arbitrary DOM targets", () => { + const root = fixture(); + expect(() => publishProjectView(root, { fields: { "header.innerHTML": "" }, actor: { id: "ui-agent" }, reason: "no" })).toThrow("Unknown or protected"); + }); +}); diff --git a/archive/experimental-control-plane/tests/project-state.test.ts b/archive/experimental-control-plane/tests/project-state.test.ts new file mode 100644 index 0000000..4913b56 --- /dev/null +++ b/archive/experimental-control-plane/tests/project-state.test.ts @@ -0,0 +1,198 @@ +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { + acknowledgeSteering, + buildProjectOrientation, + createIntervention, + findAgentCoordinationConflicts, + heartbeatAgentSession, + listAgentSessions, + listInterventions, + listSteering, + recordInterventionReceipt, +} from "../src/project-state.js"; + +const roots: string[] = []; + +function projectRoot(): string { + const root = mkdtempSync(join(tmpdir(), "keyoku-project-state-")); + roots.push(root); + mkdirSync(join(root, ".keyoku", "outcomes"), { recursive: true }); + mkdirSync(join(root, ".keyoku", "runtime"), { recursive: true }); + writeFileSync(join(root, ".keyoku", "project.yaml"), `schemaVersion: keyoku.dev/project/v1alpha1 +id: demo +name: Demo +summary: A test project +createdAt: 2026-08-11T00:00:00.000Z +updatedAt: 2026-08-11T00:00:00.000Z +`); + writeFileSync(join(root, ".keyoku", "outcomes", "current.yaml"), `schemaVersion: keyoku.dev/outcome/v1alpha1 +id: current +revision: 1 +title: Make the project understandable +objective: A person can understand the current work +owner: + kind: human + id: owner + name: Owner +constraints: [] +criteria: + - description: A file exists + probe: + kind: command + run: test -f README.md + assert: + path: exitCode + op: eq + value: 0 +humanCriteria: [] +createdAt: 2026-08-11T00:00:00.000Z +updatedAt: 2026-08-11T00:00:00.000Z +`); + writeFileSync(join(root, ".keyoku", "runtime", "human-steering.jsonl"), `${JSON.stringify({ + id: "steer_1", + kind: "direction", + message: "Prioritize the mobile view", + actor: { kind: "human", name: "Owner" }, + createdAt: "2026-08-11T01:00:00.000Z", + status: "queued", + })}\n`); + return root; +} + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +describe("project state", () => { + it("presents compact orientation with queued human steering", () => { + const root = projectRoot(); + const orientation = buildProjectOrientation(root); + expect(orientation.project.name).toBe("Demo"); + expect(orientation.currentGoal?.title).toBe("Make the project understandable"); + expect(orientation.humanAttention.count).toBe(1); + expect(orientation.humanAttention.pendingSteering[0]?.message).toBe("Prioritize the mobile view"); + }); + + it("keeps multiple goals active while focusing one independently of agent assignment", () => { + const root = projectRoot(); + writeFileSync(join(root, ".keyoku", "outcomes", "parallel.yaml"), `schemaVersion: keyoku.dev/outcome/v1alpha1 +id: parallel +revision: 1 +title: Ship a parallel outcome +objective: Complete work without replacing the other goal +owner: { kind: human, id: owner, name: Owner } +constraints: [] +criteria: + - description: A parallel result exists + probe: { kind: command, run: "true" } + assert: { path: exitCode, op: eq, value: 0 } +humanCriteria: [] +createdAt: 2026-08-11T00:00:00.000Z +updatedAt: 2026-08-12T00:00:00.000Z +`); + writeFileSync(join(root, ".keyoku", "runtime", "goal-focus.jsonl"), `${JSON.stringify({ eventType: "goal.focused", goalId: "current", createdAt: "2026-08-12T01:00:00.000Z" })}\n`); + heartbeatAgentSession({ + root, + sessionId: "parallel-agent", + actor: { kind: "agent", id: "parallel-agent", name: "Parallel agent", harness: "test" }, + status: "working", + currentWork: { outcomeId: "parallel", summary: "Working elsewhere" }, + transport: "test", + }); + const orientation = buildProjectOrientation(root); + expect(orientation.goals.count).toBe(2); + expect(orientation.currentGoal?.id).toBe("current"); + expect(orientation.agents.active[0]?.currentWork?.outcomeId).toBe("parallel"); + }); + + it("records an agent acknowledgement without rewriting the human request", () => { + const root = projectRoot(); + const updated = acknowledgeSteering({ + root, + steeringId: "steer_1", + status: "applied", + summary: "Mobile is now the first responsive breakpoint tested.", + actor: "test-agent", + }); + expect(updated.status).toBe("applied"); + expect(updated.acknowledgement?.actor).toBe("test-agent"); + expect(listSteering(root)).toHaveLength(1); + expect(listSteering(root)[0]?.message).toBe("Prioritize the mobile view"); + }); + + it("separates a committed intervention from understood, applied, and verified receipts", () => { + const root = projectRoot(); + const actor = { kind: "human" as const, id: "owner", name: "Owner" }; + const created = createIntervention({ + root, + kind: "direction", + message: "Make the agent channel fully bidirectional", + actor, + deliveryPolicy: "next_checkpoint", + idempotencyKey: "owner-message-1", + }); + expect(created.phase).toBe("committed"); + expect(createIntervention({ root, kind: "direction", message: "duplicate", actor, idempotencyKey: "owner-message-1" }).id).toBe(created.id); + + const understood = recordInterventionReceipt({ + root, + interventionId: created.id, + phase: "understood", + summary: "I will add presence, durable delivery, and semantic receipts.", + actor: { kind: "agent", id: "agent-1", name: "Test agent", harness: "test" }, + }); + expect(understood.phase).toBe("understood"); + expect(understood.receipts).toHaveLength(1); + + const applied = recordInterventionReceipt({ + root, + interventionId: created.id, + phase: "applied", + summary: "The protocol types and relay endpoints changed.", + actor: { kind: "agent", id: "agent-1", name: "Test agent", harness: "test" }, + evidenceRefs: ["tests/project-state.test.ts"], + }); + expect(applied.phase).toBe("applied"); + expect(listInterventions(root)[0]?.receipts[1]?.evidenceRefs).toEqual(["tests/project-state.test.ts"]); + }); + + it("treats agent presence as a renewable lease instead of inferring it", () => { + const root = projectRoot(); + const heartbeat = heartbeatAgentSession({ + root, + sessionId: "session-1", + actor: { kind: "agent", id: "agent-1", name: "Test agent", harness: "test", model: "test-model" }, + status: "working", + currentWork: { outcomeId: "current", summary: "Testing presence" }, + capabilities: ["stream", "intervene"], + transport: "mcp-poll", + leaseSeconds: 30, + }); + expect(heartbeat.active).toBe(true); + expect(listAgentSessions(root, new Date(heartbeat.createdAt))[0]?.active).toBe(true); + expect(listAgentSessions(root, new Date(Date.parse(heartbeat.leaseUntil) + 1))[0]?.active).toBe(false); + }); + + it("surfaces overlapping multi-agent work without pretending to lock Git", () => { + const root = projectRoot(); + for (const [sessionId, path] of [["agent-a", "src"], ["agent-b", "src/server.ts"]]) { + heartbeatAgentSession({ + root, + sessionId, + actor: { kind: "agent", id: sessionId, name: sessionId, harness: "test" }, + status: "working", + currentWork: { summary: "Parallel work", paths: [path], baseSnapshot: "abc123" }, + transport: "test", + }); + } + const conflicts = findAgentCoordinationConflicts(listAgentSessions(root)); + expect(conflicts).toHaveLength(1); + expect(conflicts[0]).toMatchObject({ reason: "overlapping_path", scope: "src" }); + expect(new Set(conflicts[0]?.sessions)).toEqual(new Set(["agent-a", "agent-b"])); + }); +}); diff --git a/archive/legacy-omnigent/README.md b/archive/legacy-omnigent/README.md new file mode 100644 index 0000000..c54eab4 --- /dev/null +++ b/archive/legacy-omnigent/README.md @@ -0,0 +1,25 @@ +# Legacy Omnigent fleet runner + +Archived: 2026-08-09 +Reason: Keyoku’s primary product is now a provider-neutral repository outcome and contribution gate. Owning a special-purpose Omnigent fleet/session/policy runtime made Keyoku look like another agent orchestrator and duplicated the responsibility of full agent-workplace products such as QM. + +## Contents + +- `src/run.ts` — create and drive Omnigent sessions +- `src/dispatch.ts` — select an Omnigent agent +- `src/omnigent-guardrails.ts` — install/remove Omnigent runtime policies +- `src/policy-compiler.ts` — compile constraints into Omnigent policy handlers +- `src/presets.ts` — Omnigent-only connector preset +- `tests/` — the dedicated regression suite for those modules + +## What remains active + +- Provider-neutral MCP/OpenAPI connectors and their autonomy/approval controls +- Machine-checkable command, HTTP, and MCP outcome probes +- Constraints as human-readable contribution boundaries +- Agent identity, harness, and model provenance +- Deterministic goal assessment and workflow learning + +## Recovery + +The move preserves Git history. To revive this integration, copy the files back to `src/` and `tests/`, restore the exports/imports plus `run`, `converge`, `guardrails`, `connect` CLI commands and `goal_run`, `goal_converge`, `goal_guardrails` MCP tools, then update the active product contract and tests. Do not revive it as a hidden dependency of the provider-neutral gate. diff --git a/src/dispatch.ts b/archive/legacy-omnigent/src/dispatch.ts similarity index 100% rename from src/dispatch.ts rename to archive/legacy-omnigent/src/dispatch.ts diff --git a/src/omnigent-guardrails.ts b/archive/legacy-omnigent/src/omnigent-guardrails.ts similarity index 100% rename from src/omnigent-guardrails.ts rename to archive/legacy-omnigent/src/omnigent-guardrails.ts diff --git a/src/policy-compiler.ts b/archive/legacy-omnigent/src/policy-compiler.ts similarity index 100% rename from src/policy-compiler.ts rename to archive/legacy-omnigent/src/policy-compiler.ts diff --git a/src/presets.ts b/archive/legacy-omnigent/src/presets.ts similarity index 100% rename from src/presets.ts rename to archive/legacy-omnigent/src/presets.ts diff --git a/src/run.ts b/archive/legacy-omnigent/src/run.ts similarity index 100% rename from src/run.ts rename to archive/legacy-omnigent/src/run.ts diff --git a/tests/dispatch.test.ts b/archive/legacy-omnigent/tests/dispatch.test.ts similarity index 100% rename from tests/dispatch.test.ts rename to archive/legacy-omnigent/tests/dispatch.test.ts diff --git a/tests/omnigent-guardrails.test.ts b/archive/legacy-omnigent/tests/omnigent-guardrails.test.ts similarity index 100% rename from tests/omnigent-guardrails.test.ts rename to archive/legacy-omnigent/tests/omnigent-guardrails.test.ts diff --git a/tests/policy-compiler.test.ts b/archive/legacy-omnigent/tests/policy-compiler.test.ts similarity index 100% rename from tests/policy-compiler.test.ts rename to archive/legacy-omnigent/tests/policy-compiler.test.ts diff --git a/tests/presets.test.ts b/archive/legacy-omnigent/tests/presets.test.ts similarity index 100% rename from tests/presets.test.ts rename to archive/legacy-omnigent/tests/presets.test.ts diff --git a/tests/run.test.ts b/archive/legacy-omnigent/tests/run.test.ts similarity index 100% rename from tests/run.test.ts rename to archive/legacy-omnigent/tests/run.test.ts diff --git a/docs/OUTCOME-ENGINE.md b/archive/legacy-positioning/OUTCOME-ENGINE.md similarity index 100% rename from docs/OUTCOME-ENGINE.md rename to archive/legacy-positioning/OUTCOME-ENGINE.md diff --git a/archive/legacy-positioning/README.md b/archive/legacy-positioning/README.md new file mode 100644 index 0000000..bd38df2 --- /dev/null +++ b/archive/legacy-positioning/README.md @@ -0,0 +1,5 @@ +# Legacy positioning + +`OUTCOME-ENGINE.md` proposed a broad regulated-enterprise decision engine. It is preserved as strategy history but is no longer an active product promise. + +The current wedge is narrower and distributable: free repository-native outcomes, continuous contribution review, exact-snapshot evidence, human accountability, and portable Factfiles. Enterprise decisioning or security packs can later use the standard; they do not define Keyoku’s category. diff --git a/docs/ASSURANCE-ADAPTER.md b/docs/ASSURANCE-ADAPTER.md new file mode 100644 index 0000000..2a6d598 --- /dev/null +++ b/docs/ASSURANCE-ADAPTER.md @@ -0,0 +1,51 @@ +# Optional assurance adapter + +Keyoku can act as an optional evidence provider around a caller's work. It is not an +agent runner, runtime-neutral protocol, scheduler, or control plane. A caller can use +no assurance, its own basic checks, or Keyoku high assurance; that profile is caller +policy and is deliberately absent from the neutral evidence envelope. + +## EvidenceProvider + +`evidence-provider/v1` accepts a neutral work identity and objective, claims, source +or deployment snapshots, command **results**, artifact digests, limitations, +authority, and a canonical content digest. Keyoku does not execute commands through +this adapter. It deterministically returns `accepted`, `rejected`, `stale`, or +`human_review_required`, with exact reason codes and a canonical result digest. +Evaluation is side-effect free and does not mutate the caller's object. + +```sh +keyoku factfile assess --file evidence.json --json +``` + +MCP exposes the same evaluator as `evidence_evaluate`. It has no shell-execution or +human-acceptance capability. + +## Neutral WorkEvent bridge + +`work-event/v1` carries only neutral `dispatch`, `checkpoint`, `milestone`, +`decision`, `regression`, `recovery`, `stale`, or `terminal` outcomes. The local sink +stores validated, content-digested events in `.keyoku/pulse/work-events.jsonl` with +idempotent IDs and conflict rejection. The local sink rejects symlinked storage +paths, serializes writers with an exclusive fail-closed lock, appends through +identity-checked descriptors, and fsyncs the ledger and parent directory. A crash +may leave a `.lock` file; inspect it and confirm no writer is alive before removing +it and retrying. This is cooperative same-user process safety, not containment +against a malicious process running as that OS user: + +```sh +keyoku pulse work-event ingest --file event.json --root /project --json +keyoku pulse work-event list --root /project --json +``` + +MCP exposes the same functions as `pulse_work_event_ingest` and +`pulse_work_event_list`. WorkEvents are coordination input: they never promote +themselves into a verified Factfile or dispatchable Pulse snapshot. + +An HTTP/webhook integration may wrap these pure functions in caller-owned transport, +authentication, replay protection, and authorization. Keyoku core does not start a +webhook listener, authenticate arbitrary runtimes, send delivery, or prescribe a +neutral agent protocol. + +See `fixtures/assurance/v1` for generic synthetic data. The fixture names no agent +product or harness and is not live proof. diff --git a/docs/FACTFILE-STANDARD.md b/docs/FACTFILE-STANDARD.md new file mode 100644 index 0000000..746b23b --- /dev/null +++ b/docs/FACTFILE-STANDARD.md @@ -0,0 +1,217 @@ +# Keyoku Factfile Standard + +Status: `v1alpha1` +License: MIT +Scope: any Git repository, public or private + +A Factfile is a portable, human-readable receipt for a software contribution. It connects a versioned intended outcome to accountable actors, relevant artifacts, deterministic observations, review history, and the exact repository snapshot those observations cover. + +A Factfile proves one bounded checkpoint. [Keyoku Pulse](PULSE.md) is the separate temporal layer that carries trusted progress across multiple Factfile-bound checkpoints and agent harnesses. Pulse activity never changes what a Factfile establishes. + +For command-backed claims, Keyoku captures the complete Git-visible source tree +(tracked plus non-ignored untracked bytes, paths, symlinks, and executable +modes) into one SHA-256 content-addressed capsule. Each command runs +sequentially in a fresh disposable checkout. A write, add, delete, mode change, +mutate-restore, original-tree race, or unsupported source entry rejects the +proof. The capsule isolates evidence bytes; it does not sandbox arbitrary code +from the caller's operating-system, network, or external-file authority. A probe +that deliberately daemonizes or escapes its process group is outside the trusted +repository-command support boundary. + +It is not an AI-generated claim that a project is “good.” The canonical JSON records bounded facts. HTML and Markdown explain those facts at the level of detail a recipient needs. + +## The standard method + +1. **Declare the outcome.** Write one human-owned objective, its constraints, automated criteria, and any required human judgment criteria under `.keyoku/outcomes/`. +2. **Open a contribution.** Bind work to an outcome revision and base Git SHA. Record the responsible human and any contributing agents, harnesses, or models. +3. **Work in any harness.** Keyoku does not prescribe Claude Code, Codex, Cursor, CI, a custom agent, or human-only development. +4. **Coordinate without pretending activity is proof.** Agents report work, request only material human decisions, and poll for durable instructions. These events remain separate from evidence. +5. **Evaluate continuously.** Reuse one active contribution per branch and outcome; run the gate after meaningful iterations. Failed and incomplete probes fail closed. +6. **Render the receipt.** Store canonical JSON and generate Markdown and HTML views from the same record. +7. **Review as a human.** Automated proof can move work only to `human_review_required` when judgments remain. Named people record those verdicts; only after all required gates pass can the snapshot be accepted. +8. **Re-evaluate after change.** A later Git head or worktree digest is a different proof scope. Old evidence remains history, never silently applies to new code. + +## Canonical hierarchy + +```text +Project +└── Outcome (versioned definition of done) + └── Contribution (bounded attempt) + ├── Actors (human, agent, organization) + ├── Session events (work, decisions, instructions, presence) + ├── Repository snapshot (base, head, worktree digest) + ├── Automated evidence (claim + explanation + artifact + audit trail) + ├── Human criteria (named judgment + guidance + verdict) + ├── Reviews (human decisions and comments) + └── Factfile snapshots (append-only history) +``` + +## Repository layout + +```text +.keyoku/ +├── project.yaml +├── policy.yaml +├── outcomes/ +│ └── .yaml +├── contributions/ +│ └── / +│ ├── manifest.yaml +│ ├── events.jsonl +│ ├── reviews.jsonl +│ ├── snapshots/.json +│ ├── factfile.json +│ ├── factfile.github.md +│ ├── factfile.md +│ └── factfile.html +├── pulse/ +│ └── events.jsonl # optional harness-neutral progress ledger +└── runtime/ # local evaluator state; never canonical proof +``` + +Projects normally commit the project, policy, and outcome files. A project decides whether to commit contribution receipts or attach them to pull requests/releases. The local runtime is implementation state and should not be published. + +## Required records + +### Project + +- Stable `id`, display `name`, and plain-language `summary` +- Optional repository URL and default branch +- Creation and update timestamps + +### Outcome + +- Stable `id` and explicit positive `revision` +- Human-readable `title` and `objective` +- Accountable `owner` +- Constraints that bound acceptable work +- One or more automated criteria +- Zero or more required human judgment criteria with stable ids and review guidance + +Editing meaning, constraints, or criteria requires a new revision. A contribution never silently moves to a newer revision. + +The outcome file is repository-owned. Its canonical revision history is the Git history of `.keyoku/outcomes/.yaml`; inspect it directly with `git log -- .keyoku/outcomes/.yaml` without creating a second source of truth. + +An outcome may declare a deterministic path boundary with `scope.include`, `scope.exclude`, and `scope.maxChangedFiles`. Paths outside that boundary fail the gate. This catches mechanical scope drift but does not claim that a change is semantically coherent; projects should keep coherence as a human criterion. + +### Actor + +- `kind`: `human`, `agent`, or `organization` +- Stable `id` and display `name` +- Optional role +- Agent provenance may include `harness` and `model` +- An agent should identify a human or organization `ownerId` + +Agent identity is provenance, not personhood or legal accountability. + +### Evidence contract + +Every machine-evaluated claim uses the same reading order: + +1. **Claim** — the bounded behavior or property being evaluated +2. **What this shows** — the result in language a maintainer can explain +3. **Why it matters** — its relevance to the requested outcome +4. **Artifacts** — screenshots, short recordings, traces, reports, logs, or other inspectable output when appropriate +5. **Code context** — the paths that deliver the behavior and what each one is responsible for +6. **Audit details** — the probe, observed value, assertion rule, duration, and error + +The Factfile stores a safe reproduction description for each observation. Repository commands are shown directly; HTTP and MCP probes are represented without publishing request credentials. Referenced artifacts must exist inside the project and are SHA-256 content-bound before they are presented as available evidence. A screenshot or short MP4/WebM recording may additionally be embedded in the portable HTML view within the documented size limit. Screenshots may carry percentage-based callouts; recordings may carry timestamped callouts. An annotation explains what a reviewer should notice but remains a demonstration, not an independent verifier. + +The artifact type follows the claim. Visible behavior normally needs a screenshot or rendered capture. Runtime behavior needs a test or trace. Architecture needs a code tour or diff. Security needs the relevant scanner report and scope. A raw exit code by itself is audit data, not a useful human explanation. + +Every criterion evaluation therefore records: + +- Plain-language description +- Human-facing result summary and relevance +- Zero or more evidence artifacts with labels, captions, paths, and digests where available +- Zero or more code references with an explanation of responsibility +- Observed value +- Expected assertion path, operator, and value +- Pass/fail verdict +- Runtime duration +- Probe or evaluation error, when present + +Raw logs may remain private. Published evidence must be enough to understand the verdict without opening the audit details and without exposing credentials, transcripts, customer data, or unrelated source. + +### Two-way session protocol + +The mutable live session and immutable Factfile snapshot have different jobs. The live session coordinates the next iteration; each gate captures its then-current state into a content-addressed Factfile. + +- A work item has a stable id, actor, status (`queued`, `working`, `blocked`, or `done`), detail, and update time. It is agent-reported activity, never completion evidence. +- A decision request states what the agent wants, what blocks it, why a human must decide, bounded options, a recommendation when available, and the consequence of no response. +- A human resolution creates a queued instruction. The choice is not considered delivered until an agent receives and acknowledges that instruction. +- A free-form steering instruction uses the same durable queue. It may target one agent or be available to the next connected agent. +- A heartbeat describes presence only. Keyoku considers it connected for a short lease; absence never discards queued work or instructions. +- Optional steering is separate from **Needs you**. A renderer may derive suggested next directions from attention signals, evidence gaps, architecture, and pending acceptance criteria. Each suggestion explains its expected outcome effect, deep context, and tradeoffs before it becomes an instruction. Custom direction remains available without presenting an empty prompt box as a blocker. + +The MCP surface is provider-neutral: `contribution_report_work`, `contribution_request_decision`, `contribution_propose_directions`, `contribution_next_instruction`, and `contribution_ack_instruction`. The working agent proposes contextual next moves before the final gate; the Factfile records the concise evidence-grounded rationale and references, never private chain-of-thought. Hooks may improve immediacy for a particular harness, but the protocol does not require them. + +### Human judgment + +Not every meaningful property reduces to an exit code. Product fit, visual quality, maintainability, risk acceptance, and contextual correctness can be declared as human criteria. Each verdict records the criterion id, identified human reviewer, pass/fail judgment, reason, time, Factfile digest, and repository snapshot it reviewed. Agents cannot satisfy these criteria. + +The portable HTML receipt can copy an instruction but cannot mutate its historical snapshot. A token-scoped local live session may present decision controls. Those controls append decision and instruction events; they never rewrite an earlier snapshot. Exact-snapshot acceptance still rejects a stale Factfile digest. + +### Repository snapshot + +The proof scope includes: + +- Contribution base SHA +- Current Git head SHA +- Whether source is dirty +- Changed paths +- SHA-256 worktree digest over the complete Git-visible source capsule: + sorted tracked and non-ignored untracked paths, exact bytes, executable modes, + and internal relative symlink targets + +Generated Factfiles, Pulse ledgers, and evaluator runtime are excluded from the +worktree digest so proof bookkeeping cannot invalidate its own source identity. +Project, policy, outcome, and architecture contracts remain included. + +## States + +| State | Meaning | +|---|---| +| `draft` | Work is open; no current evaluation is claimed | +| `evaluating` | Probes are running | +| `evidence_gaps` | One or more declared criteria did not pass | +| `human_review_required` | Automated evidence passed; one or more required human judgments are pending | +| `review_blocked` | Automated evidence passed; a required human judgment failed | +| `ready_for_review` | Automated and required human criteria passed; the snapshot is ready for acceptance | +| `accepted` | An accountable human or organization accepted that snapshot | + +Review events append to `reviews.jsonl` and every rendered Factfile includes the resulting timeline. `keyoku proof review` records an identified human note; `keyoku proof accept` records acceptance only when the current source still exactly matches a passing Factfile. A source change after `ready_for_review` or `accepted` requires re-evaluation. + +## Claim language + +Allowed: + +> 8 of 8 automated checks passed; 1 of 2 required human judgments passed for Git head `abc123` plus worktree digest `def456`. Human review remains required. + +Not allowed: + +> The AI proved this project is secure and correct. + +A security scan, test suite, or sandbox evaluation supports only the property it actually checked. Severe findings may block readiness even when other checks pass, but an absence of findings is never universal safety proof. + +## Human views + +All views derive from canonical records and answer these maintainer questions first: + +- What are agents doing now, and is that status merely reported or actually proven? +- Which decision is truly blocked on me, why, and what happens if I do nothing? +- What was requested? +- What changed? +- What is actually supported, and by which artifacts? +- Who or what did the work, and which human is accountable? +- What does the reviewer need to decide? + +Raw assertions, hashes, changed paths, and verifier output remain available as collapsed audit detail. They must not displace the human explanation. + +## Interoperability + +Keyoku is Git-provider and harness neutral. A GitHub Action may attach the compact `factfile.github.md` summary and full HTML artifact to a pull request. GitLab, Forgejo, CI systems, or an agent runtime can consume the same canonical JSON and exit semantics. `keyoku-engine` may mirror snapshots for shared live views, but the local canonical record must remain usable without it. + +## Versioning + +Schemas use identifiers such as `keyoku.dev/outcome/v1alpha1`. Additive fields may appear during alpha. Incompatible meaning or required-field changes receive a new schema version. A verifier must reject unsupported versions rather than guessing. diff --git a/docs/GITHUB.md b/docs/GITHUB.md new file mode 100644 index 0000000..8701609 --- /dev/null +++ b/docs/GITHUB.md @@ -0,0 +1,71 @@ +# GitHub integration + +Keyoku’s first distribution surface is a normal GitHub Check, reviewer-first job summary, and downloadable Factfile artifact. It does not require a GitHub App or hosted Keyoku account. + +Install it from any Git repository in one command: + +```bash +keyoku proof init +``` + +The workflow generated by the unreleased source candidate pins +`keyoku@3.0.0-alpha.1`; it becomes runnable only if that exact package is +separately approved and published to npm `next`. It never falls through to +`latest`. + +After a stable `v3` tag exists, the Marketplace-compatible composite action can +be used directly after installing project dependencies: + +```yaml +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 +- uses: Keyoku-ai/keyoku@v3 # available only after the stable v3 release + with: + outcome: review-ready-change + base: ${{ github.event.pull_request.base.sha }} +``` + +The action writes the native job summary, exposes `contribution-id`, `state`, and `factfile` outputs, and uploads the portable Factfile bundle. It requests no repository write permission. + +Keyoku detects Node.js, Python, Rust, Go, or a generic Git project, creates a starter outcome contract, and writes `.github/workflows/keyoku-proof.yml`. Review the generated contract before treating it as definition of done. The workflow: + +1. Checks out the proposed revision. +2. Installs the detected project dependencies. +3. Opens an ephemeral contribution attributed to GitHub Actions and binds it to the pull request base SHA. +4. Runs the repository-owned outcome contract. +5. Adds compact `factfile.github.md` to the native GitHub job summary. +6. Uploads JSON, GitHub Markdown, detailed Markdown, and HTML Factfiles as one artifact. +7. Fails the required check when machine evidence gaps or an explicit human block remain. + +`human_review_required` does not fail the machine Check: it means the declared observations passed and normal GitHub human review still owns the decision. The summary makes that boundary visible. + +Use GitHub's existing review controls for PR acceptance. For local agent steering, open the contribution id printed by `proof run`: + +```bash +keyoku proof serve +``` + +The local session keeps agent work, genuine blockers, attention signals, and proof separate. Human choices become durable MCP instructions; GitHub remains the source of truth for code review and merge. + +In GitHub: + +- **Approve** when the requested outcome is satisfied by the current exact revision. +- **Request changes** with the next concrete instruction when it is not. +- The agent or developer pushes another iteration; Keyoku automatically produces a new SHA-bound Factfile. + +This keeps the first release GitHub-native without requiring a privileged Keyoku App or a second conversation UI. + +## Security boundary + +Outcome probes execute commands from the checked-out repository. Treat them like test scripts. The example workflow intentionally grants only `contents: read`; it does not give untrusted pull-request code a token that can comment, merge, publish, or modify the repository. + +An automatic pull-request comment would require a separate privileged workflow that only reads a previously generated artifact and validates its digest. Do not combine untrusted probe execution with `pull-requests: write` merely for a nicer comment. + +## Branch protection + +After the workflow has run once, make `Keyoku proof / Keyoku / outcome proof` a required status check in the repository’s branch rules. This makes the outcome contract the contribution gate while preserving GitHub as the source-code host. + +## Public credit + +The artifact records actors, harness/model provenance, exact source scope, and evidence. A project may also commit accepted Factfiles or link the artifact from a release. Avoid publishing raw transcripts, secrets, customer data, or unrelated source output. diff --git a/docs/ITERATION.md b/docs/ITERATION.md new file mode 100644 index 0000000..39ab622 --- /dev/null +++ b/docs/ITERATION.md @@ -0,0 +1,61 @@ +# Keyoku behavior-iteration compatibility contract + +> This controller remains implemented and regression-tested as compatibility +> source, but its CLI and MCP verbs are not part of the bounded v3 public +> surface. v3 expresses incremental work through repeated `keyoku proof run` +> checkpoints plus Pulse. See [PUBLIC-SURFACE.md](PUBLIC-SURFACE.md). + +Status: `v1alpha1` + +Behavior iteration is Keyoku's provider-neutral loop for getting a software product from an observed evidence gap to a bounded, reviewable outcome. Keyoku owns the outcome contract, evidence rounds, stop policy, and receipt. A coding harness owns execution. + +## Protocol + +1. A human or harness starts a session for one repository-owned outcome. +2. Keyoku opens one contribution and runs every declared probe against the exact source state. +3. If evidence is missing, Keyoku emits a deterministic instruction containing the objective, constraints, failed claims, reproduction commands, regressions, and exact Git/worktree identity. +4. Any MCP-capable harness can fetch that instruction, perform product work, and report one idempotent checkpoint. +5. Keyoku records explicitly sourced usage, reruns the proof, and either emits the next instruction or stops. + +The CLI and MCP surfaces use the same implementation: + +| CLI | MCP | +|---|---| +| `keyoku iterate start ` | `iteration_start` | +| `keyoku iterate status ` | `iteration_status` | +| `keyoku iterate next ` | `iteration_next` | +| `keyoku iterate checkpoint ...` | `iteration_checkpoint` | + +## Ledger and idempotency + +Events live under `.keyoku/runtime/iterations//events.jsonl`. This location is intentionally local evaluator state and is excluded from source proof. Every event has a monotonic sequence, the previous event digest, and its own canonical SHA-256 digest. Replay verifies the full chain before returning state. + +Checkpoint ids are idempotency keys. Replaying an identical checkpoint returns the existing state without another proof round or another usage charge. Reusing the id with a different summary, usage, or provenance fails closed. + +Each proof round records: + +- contribution and portable Factfile identity; +- exact Git head, worktree digest, dirty flag, and changed paths; +- automated pass/fail totals; +- pending, passing, and failed human judgments; +- indexes of passing, failing, and newly regressed claims; +- consecutive checkpoints that produced no source change. + +## Stop semantics + +The controller is bounded by default to five rounds, one hour, and two consecutive no-progress checkpoints. Callers may lower or raise those limits within schema bounds and may add reported-token or reported-cost ceilings. + +Terminal states are intentionally specific: + +- `ready_for_review`: declared automated and human criteria pass for the exact snapshot; accountable acceptance is still separate. +- `human_review_required`: machine evidence passes, but one or more declared human judgments are pending. +- `review_blocked`: machine evidence passes, but an accountable human judgment failed. +- `stopped_round_limit`, `stopped_time_limit`, `stopped_no_progress`, `stopped_token_limit`, or `stopped_cost_limit`: the named bound ended the loop while evidence remained incomplete. + +## Usage boundary + +An agent checkpoint may report input, output, and cached-input tokens, tool calls, and cost. It must label the source as `provider_receipt`, `agent_reported`, or `unknown`. These values are operational bounds, not settled billing. Keyoku does not derive them from rendered chat messages or claim a provider receipt when none exists. + +## Authority boundary + +Keyoku never executes arbitrary agent commands in this protocol. It does not write product source, decide human criteria, accept a Factfile, push Git commits, deploy, or bypass application authorization. Those actions remain with the connected harness and accountable human. This separation lets Keyoku become the iteration layer without turning proof output into an autonomous actor or confusing agent activity with evidence. diff --git a/docs/PRODUCTION-READINESS.md b/docs/PRODUCTION-READINESS.md index ff4e626..cbf0b5b 100644 --- a/docs/PRODUCTION-READINESS.md +++ b/docs/PRODUCTION-READINESS.md @@ -1,89 +1,88 @@ -# Production readiness - -Honest status of keyoku as a product other people depend on. The convergence -engine is production-grade; this tracks the **operational shell** around it. -Grouped by who can close each item: ✅ done · 🟡 needs maintainer (one-time, -out of code) · 🔵 next in code. - -## Tier 1 — table stakes - -### ✅ Release integrity (done — 2.8.0) -- `npm run preflight` + a CI step in `release.yml` verify, before any tag ships: - valid semver, CHANGELOG entry present, `VERSION` single-sourced from - package.json (the 2.7.1 "0.1.0" drift can't recur), the **built artifact - reports the right version**, and `dist` is in the tarball. -- The release workflow already runs typecheck → test → eval → preflight. - -### 🟡 npm Trusted Publishing (maintainer, ~2 min, one-time) -CI **cannot publish** today — there is no `NPM_TOKEN` and no Trusted Publishing -configured, so every release so far went out via the maintainer CLI -(`npm publish`), with **no provenance**. The workflow is already OIDC-ready -(`id-token: write`, npm upgraded). Close it once: - -> npmjs.com → package **`keyoku`** → Settings → **Trusted Publishing** → add -> repository **`Keyoku-ai/keyoku`**, workflow **`release.yml`**. - -After that, pushing a `vX.Y.Z` tag publishes automatically, tokenless, with -provenance. (Full detail in `docs/PUBLISHING.md`.) - -### 🟡 Repo consolidation (maintainer) -Two working trees clone the same repo: `~/Development/Keyoku/keyoku-harness` -(the one the **live MCP server runs** — `dist/index.js`) and -`~/Development/Keyoku Harness/keyoku-harness` (used for site deploys). Editing -the wrong one ships nothing. Pick one canonical checkout; it's a foot-gun for -any contributor. See `docs/REPO-MAP.md`. - -### ✅ Store growth bounds (done, pre-existing) -`activity.jsonl` self-caps (trims at ~2.5 MB → last 8k events, `store.ts`); -observations cap per goal (500 → 400); `audit.jsonl` caps at ~1 MB (v2.12.1); -`executions.json` caps at 200 terminal runs while keeping all in-flight ones -(2.17.x). A large file on disk just means the *old* global install (pre-cap) -was writing it — the current build trims it. Remaining minor gap (🔵): -`knowledge.jsonl` is still unbounded (small today); add the same size-cap if it grows. - -### 🔵 Store concurrency -The JSON store is synchronous, atomic (tmp+rename), cacheless, last-writer-wins -per entry — fine for one machine / a few sessions. Team or heavy multi-session -use wants real atomic ops or a SQLite backend (the `Store` interface is already -the seam for that swap). Documented in `store.ts`. - -## Tier 2 — the product promise (learning that compounds) - -### ✅ Self-pruning (done — 2.8.0) -Suggestions now rank by `similarity × precision`, where precision is learned -from whether a workflow's steps actually recur in later converged goals. A -workflow that only ever word-matches sinks. This is the first real -**outcome-grounded** signal in retrieval. (`recordSuggestionOutcomes` + -`suggestWorkflows` in `engine.ts`.) - -### 🔵 Semantic retrieval -Recall is still jaccard (token overlap) — the lite-model re-rank layers on top -but needs a key. Embeddings via the BSL engine/brain would finish the -"no-heuristics" path for the offline/no-key case. - -### 🔵 Validation depth -Lift is proven directionally (behavioral planning 1/3→3/3, one execution run, -both n=1). A production claim wants multi-trial runs, a weaker-model row, and a -behavioral metric tracked over time — today only the deterministic retrieval -eval gates CI (`npm run eval`); the value evals are manual. - -## Tier 3 — trust & adoption - -### 🔵 Data trust (it reads your activity) -Secret redaction exists at record time (`activity.ts`). Still wanted: a -retention policy, scoping/opt-out controls, and a `keyoku inspect` that shows -exactly what's in `~/.keyoku`. For a tool that ingests computer activity (and -the basis for Lar's onboarding), this is make-or-break before broad promotion. - -### 🔵 Docs / site currency -2.2→2.8 shipped a lot (goal_focus, pitfalls, re-rank, self-pruning). Per the -ship-propagation rule, the site/README/quickstart should teach the new -flagship (`goal_focus`) and land a <5-min "wow". - ---- - -**One-liner:** the engine is production-grade; Tier 1 is now mostly closed -(release integrity ✅, growth bounds ✅) with two small **maintainer** toggles -left (Trusted Publishing, repo consolidation). Tier 2's differentiator -(self-pruning) has its first version shipped. Tiers 2–3 remaining are roadmap, -not blockers. +# Production-readiness gate + +Status: **unreleased v3 candidate — not approved for publication** +Updated: 2026-08-25 + +This document describes the narrow v3 assurance product. It supersedes the old +v2 claim that Keyoku's memory/convergence engine was production-grade. The +public npm `latest` and public website remain on v2 until the owner approves one +exact, immutable replacement candidate. + +## Product boundary + +Keyoku is an independent proof, attention, and stakeholder-presentation layer +around autonomous work. It does not run agents, schedule work, own a control +plane, or mutate an orchestrator's state. A neutral system may select no +assurance, basic receipt checks, or Keyoku high assurance through the optional +EvidenceProvider and WorkEvent interfaces. + +The MIT package provides local Factfiles, Pulse planning/rendering, the CLI, +MCP tools, schemas, fixtures, and adapters. The separately licensed Engine is +optional durable multi-project storage/API infrastructure. The local path must +remain fully useful without Engine. + +## Declared candidate matrix + +| Surface | Candidate target | Current evidence | Release position | +|---|---|---|---| +| Local CLI/library/MCP | Node.js 20 and 22 on Linux and macOS, Git repositories using regular files, internal relative symlinks, and tracked/nonignored untracked source | Full local tests and clean-package runs have passed during integration; Ubuntu CI is configured but has not run on an immutable v3 commit | Candidate only | +| Source capsule probes | Reviewed repository commands, executed sequentially in fresh disposable checkouts | Dirty bytes, odd paths, executable modes, internal symlinks, mutation, mutate-restore, isolation, source races, unsupported entries, and timeout cleanup have regression coverage | Candidate only; not an OS sandbox | +| Pulse local ledger | One repository-local installation with cooperative same-user writers | Canonical event validation, replay/idempotency, conflict rejection, linked-path rejection, exclusive write lock, bounded descriptor-anchored append, and fsync | Candidate only; not a distributed queue or malicious same-user containment boundary | +| Engine | Supported Go toolchain and SQLite on Linux/macOS | Unit, race, vet, lint, semantic Factfile checks, corpus conformance, and backup/restore rehearsal passed during integration | Candidate only; hosted operations not proven | +| Website | Current evergreen Chrome/WebKit-class browsers, keyboard, reduced motion, mobile reflow | Local lint/build/browser checks and original-resolution review passed during integration | Private replacement deployment still pending | +| Windows | Not declared for v3 alpha | No exact-candidate Windows execution evidence | Unsupported until proven; fail reports are requested | + +## Required release gates + +The release candidate is publishable only when all of the following evidence is +bound to committed revisions and exact archives: + +- Public CLI, API, Factfile, Pulse, decision, replay, export, and adapter flows + pass from clean install, including failure, stale proof, recovery, and upgrade + boundaries. +- Generated JSON, JSONL, Markdown, HTML, manifests, receipts, and reports parse + and execute in their native consumers; tampering and semantic mismatches fail + closed. +- Harness and Engine consume the same conformance corpus byte for byte and + produce the same canonical outcomes. +- Dependency, secret, path, permission, concurrency, race, canonicalization, + and supply-chain checks pass for the exact candidate. +- Human UX, terminal UX, keyboard operation, screen-reader semantics, contrast, + reduced motion, 200% zoom, narrow mobile layout, long evidence, and recovery + messages receive direct review. +- Engine backup/restore, migrations, rollback, health checks, resource budgets, + and deployment runbooks are rehearsed against the final revision if Engine is + included in the release. +- Exact archives install on clean Node 20 and 22 environments; checksums, SBOMs, + version mapping, release notes, migration guidance, and rollback instructions + agree across all repositories. +- A fresh agent and a human independently exercise the exact final archives and + interactive product journey. Any source change invalidates that evidence. + +## Open publication blockers + +- Harness, Engine, and site now have local integration commits, but public CI has + not run those unpublished revisions and independent exact-archive acceptance + is still pending. The external evidence packet must record their final mapping. +- Private owner-only deployment of the corrected interactive site is pending. +- No verified private vulnerability intake exists. Enable GitHub private + vulnerability reporting or approve and verify a monitored security contact. +- Engine licensing/licensor and repository-ownership posture require an explicit + owner decision before the public v3 sequence. +- Final exact-revision archive, SBOM, release notes, migration, rollback, and + independent acceptance packets have not yet been issued. +- npm Trusted Publishing and the release candidate itself have not been approved. + +## Rollback boundary + +Do not move npm `latest`, replace the public website, merge protected branches, +or delete the v2 line while v3 is under review. A prerelease, if approved, must +use the `next` dist-tag. The public v2 repository/tag/package/site combination is +the rollback boundary until v3 is separately proven and accepted. + +## Release verdict + +**NO-GO for public release today.** The coherent local product slice is real, +but publication requires exact committed revisions, rerun gates from final +archives, corrected private deployment, independent acceptance, and the owner +decisions listed above. diff --git a/docs/PUBLIC-SURFACE.md b/docs/PUBLIC-SURFACE.md new file mode 100644 index 0000000..6f3a102 --- /dev/null +++ b/docs/PUBLIC-SURFACE.md @@ -0,0 +1,66 @@ +# Keyoku v3 public surface + +This document is a release boundary, not a roadmap. The executable inventory in +[`src/public-surface.ts`](../src/public-surface.ts) drives the public CLI help and +MCP registration. Tests compare the built entrypoint with that inventory. + +## CLI + +| Command | Bounded responsibility | +|---|---| +| `keyoku proof …` | Create, run, review, accept, and present proof for one repository-owned outcome | +| `keyoku factfile inspect …` | Validate and explain one content-bound Factfile | +| `keyoku factfile verify …` | Also require the current Git head and worktree to match the Factfile | +| `keyoku factfile assess …` | Evaluate one neutral evidence envelope without running commands or changing caller state | +| `keyoku factfile publish …` | Explicitly publish a verified Factfile to an optional Engine endpoint | +| `keyoku pulse …` | Ingest lifecycle events, verify checkpoints, plan dispatch, and render projections without sending | +| `keyoku serve` | Serve the bounded MCP tool set over stdio | +| `keyoku doctor` | Report installation, project, optional Engine, and authority boundaries | +| `keyoku version` / `keyoku help` | Discovery only | + +`proof review` and `proof accept` require an identified human on the local CLI. +They are deliberately absent from MCP. Any source change makes the prior +Factfile stale and requires a new proof run before review or acceptance. + +## MCP + +The v3 server exposes thirteen tools: + +- Proof-session coordination: `contribution_report_work`, + `contribution_request_decision`, `contribution_next_instruction`, + `contribution_ack_instruction`, and `contribution_gate`. +- Assurance: `evidence_evaluate`. +- Pulse: `pulse_event_ingest`, `pulse_checkpoint_publish`, + `pulse_work_event_ingest`, `pulse_work_event_list`, `pulse_status`, + `pulse_dispatch_plan`, and `pulse_projection_render`. + +There is no MCP tool that accepts human review, runs an agent, changes delivery +authority, sends a message, manages connectors, executes a learned workflow, or +operates the v2 memory/goal system. + +The optional assurance adapter is not a runtime-neutral agent standard. Callers +retain work orchestration and choose `none`, `basic`, or +`keyoku_high_assurance` in their own policy. That profile is not required by or +encoded in the neutral evidence envelope. + +The human or local workflow opens the contribution with `keyoku proof run` and +passes its id to the coding harness. MCP deliberately cannot create a free-form +goal or silently choose the repository's definition of done. + +## Compatibility boundary + +The v2 goal, workflow, connector, activity, memory, and execution implementation +remains in source for regression and migration work. Its test entrypoint is not +listed in `package.json` files and is not part of the v3 npm archive. Until the +owner approves a v3 release, npm `latest` remains on the v2 release line. The +explicit rollback for a future v3 alpha is `npm install keyoku@2`. + +The bounded v3 verifier runs repository command and HTTP criteria. Command +criteria run sequentially in fresh disposable checkouts from one exact source +capsule and must not write to that checkout. This is evidence isolation, not an +OS sandbox: arbitrary commands still have the caller's user and network +authority, and daemonizing or process-group-escaping commands are outside the +trusted repository-command support boundary. A legacy MCP +criterion fails closed because connector management is not shipped or registered +in the v3 entrypoint; migrate that observation behind a repository-owned command +or HTTP check before adopting v3. diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index 1ab5cea..785c5d5 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -1,51 +1,45 @@ -# Publishing +# Publishing Keyoku v3 -Releases are tag-driven. Pushing a `v*` tag runs `.github/workflows/release.yml`, -which typechecks, tests, runs the muscle-memory eval (a hard quality gate), and -then publishes to npm. The workflow is **idempotent** — it skips a version that is -already on npm — and it **never reports a false success**: if no publish auth is -configured it emits a loud warning and exits 0 rather than pretending it shipped. +This repository is an unpublished v3 candidate. npm `latest` remains on the v2 +release line. A passing build or preflight is evidence for a candidate, not +authorization to tag, push, publish, or move a dist-tag. -## Cut a release +## Candidate sequence -1. Bump `version` in `package.json` and add a `CHANGELOG.md` entry. -2. Commit, then tag and push: - ```bash - git tag vX.Y.Z - git push origin vX.Y.Z - ``` -3. Watch the **Release** workflow. Green + "Published keyoku@X.Y.Z with provenance" - means done. +1. Freeze exact candidate revisions for `keyoku`, `keyoku-engine`, and + `keyoku-site` without changing the existing public release. +2. Run typecheck, full tests, public-surface inventory checks, security gates, + clean archive install, generated-output execution, browser acceptance, and + the declared support matrix against those exact revisions. +3. Generate release notes, migration guidance, checksums, SBOM/provenance where + supported, rollback instructions, and a revision-bound evidence manifest. +4. Obtain the owner's explicit approval for that exact candidate and resolve + any licensing or repository-posture decision. +5. Publish an alpha to the `next` dist-tag. Do not move `latest`. +6. Verify clean installation with `npm install keyoku@next`, the bounded CLI and + MCP inventories, `proof demo`, Factfile stale rejection, and Pulse replay. +7. Only after alpha acceptance, separately approve a stable v3 tag and any + `latest` dist-tag change. -## One-time auth setup (the only manual gap) +## Rollback -The workflow code is complete; npm just needs to trust it. Pick **one**: +The stable rollback boundary is explicit: -### Option A — Trusted Publishing (recommended: tokenless + provenance) - -No secret to manage. On npmjs.com: - -> package **`keyoku`** → **Settings** → **Trusted Publishing** → add publisher: -> repository **`Keyoku-ai/keyoku`**, workflow **`release.yml`**. - -The workflow already requests the OIDC token (`permissions: id-token: write`) and -upgrades npm to a version that supports OIDC, so once this is added the next tag -publishes automatically with provenance. - -### Option B — `NPM_TOKEN` secret (fallback) - -Create an **Automation** access token on npmjs.com and add it as the repo secret -`NPM_TOKEN` (Settings → Secrets and variables → Actions). The workflow already -wires `NODE_AUTH_TOKEN` from it. +```bash +npm install -g keyoku@2 +``` -## Manual fallback +Do not remove the v2 package line or overwrite an existing version. npm package +versions are immutable; a bad v3 candidate should be deprecated or superseded, +not silently replaced. -Until A or B is configured, publish from an authenticated maintainer machine: +## Authentication and provenance -```bash -npm run build -npm publish --access public -``` +Trusted Publishing with npm OIDC is preferred over a long-lived token. The +release workflow requests `id-token: write`, but credentials do not grant +product approval. Publication automation must preserve the chosen dist-tag and +must fail visibly when publish did not occur. -This is safe to run anytime — npm rejects a re-publish of an existing version, -matching the workflow's idempotency. +Never run `npm publish`, push a release tag, or change `latest` merely because +this document or `npm run preflight` is present. Those are separately approved +external actions. diff --git a/docs/PULSE.md b/docs/PULSE.md new file mode 100644 index 0000000..75e278a --- /dev/null +++ b/docs/PULSE.md @@ -0,0 +1,124 @@ +# Keyoku Pulse + +Status: `v1alpha1` local thin slice +License: MIT +Scope: harness-neutral progress across exact-source Factfiles + +A **Factfile** is proof for one bounded checkpoint. **Pulse** is trusted progress across checkpoints. + +Pulse is not an agent transcript, a cron digest, a task tracker, or permission to send a message. It consumes typed lifecycle events from any harness, promotes only Factfile-bound checkpoints, deterministically decides whether an update is reportable, and renders several audiences from one content-bound snapshot. + +## Local path + +No Engine account or service is required: + +```bash +# Inspect the contract with a generic JSONL fixture. +keyoku pulse fixture generic --out /tmp/pulse.jsonl +mkdir -p /tmp/pulse-project +keyoku pulse ingest --root /tmp/pulse-project --file /tmp/pulse.jsonl +keyoku pulse status --root /tmp/pulse-project --json +keyoku pulse plan --root /tmp/pulse-project --now 2026-08-24T16:05:00.000Z --debounce-ms 0 --json +``` + +The generic fixture is synthetic and therefore produces +`suppress/attested_checkpoint`, not a dispatchable snapshot. Audience rendering +requires a locally verified checkpoint created by `pulse checkpoint publish`. + +The append-only ledger is `.keyoku/pulse/events.jsonl`. Replaying the same event id and digest is idempotent. Reusing an id with different content fails. Replay canonicalizes a valid event set by timestamp, lifecycle dependency rank, and event id, so arrival-order permutations produce the same state. Same-lease events that still have an ambiguous timestamp/rank fail closed rather than inheriting JSONL order. + +Local writes reject symlinked ledger paths, serialize writers with an exclusive +fail-closed lock, append through identity-checked descriptors, and fsync the ledger +and parent directory. If a process crashes while holding the lock, Keyoku reports +the exact `.lock` path; remove it only after confirming that no writer is alive, +then retry. This is cooperative same-user process safety, not containment against +a malicious process running as that OS user. + +## Adapter contract + +Any caller may write the same `keyoku.dev/pulse-event/v1alpha1` JSONL. Keyoku is +an optional assurance adapter, not the caller's runtime protocol or control plane. + +Lifecycle types are: + +- `started` +- `heartbeat` +- `verification_started` +- `checkpoint_published` +- `blocked` +- `failed` +- `completed` +- `abandoned` + +Each lease names its harness, project, run, agent, canonical source root, bounded task/outcome, heartbeat, current state, source digest, and latest checkpoint. Every event and source identity has an exact SHA-256 content digest. + +## Checkpoint promotion + +A verified checkpoint contains one or more Factfile references, exact source, verification methods, a change story, visible assets, limitations, next task, and an optional human decision request. + +For local Factfiles, do not hand-author a `checkpoint_published` event. Use: + +```bash +keyoku pulse checkpoint publish --root /path/to/project --file checkpoint-draft.json --json +``` + +The command reads every Factfile, recomputes its canonical digest and bytes digest, +checks project/outcome identity plus Git head and worktree digest, and rejects +symlinked or signature-mismatched media before appending the event. +Before planning or rendering, the CLI and MCP adapter re-read those current +bytes and omit any stale or self-asserted local checkpoint from the planner's +trust set. Such a checkpoint returns `suppress/untrusted_local_checkpoint`. +`adapter_attested` checkpoints must explicitly name the adapter and its +responsibility. Adapter and fixture bindings remain visibly `attested`, never +`verified`, and the local dispatcher always returns +`suppress/attested_checkpoint` for them. + +Visual assets follow the same rule. An asset without a resolved digest renders as **Evidence asset unresolved**, not as a working image or video. A live adapter must resolve and digest the real file before delivery. + +## Deterministic dispatch + +The planner returns exactly one outcome: + +| Outcome | Meaning | +|---|---| +| `send` | One material verified checkpoint is ready for a separately authorized adapter | +| `defer` | Fresh work/verifying activity or the coalescing window is still open | +| `deduplicate` | The content-bound snapshot was already delivered | +| `suppress` | No material checkpoint exists, or source/project/future-state conflict fails closed | +| `coalesce` | Multiple compatible checkpoints share project and source ancestry | +| `stale_no_send` | An active lease is stale; freeze the last locally reverified checkpoint and send no normal update | + +Partial uncheckpointed work never becomes a report. Future-dated events fail closed. Conflicting canonical roots or unconnected source ancestry fail closed. Cron may wake the planner to catch up an undelivered checkpoint, but time alone is not a material event. + +Material triggers are limited to a verified checkpoint, owner decision, stopped regression, confirmed deployment incident, or recovery. + +## Audience projections + +The same snapshot digest renders: + +- founder/stakeholder Markdown; +- developer evidence Markdown; +- accessible control-room timeline HTML; +- email-safe HTML; +- plain text; +- canonical JSON for API or MCP use. + +Friendly model-written copy may be added only after the deterministic planner has selected a reportable snapshot. It must not change source, materiality, freshness, or dispatch decisions. + +## Delivery authority + +`planPulseDelivery` supports email, Slack, Teams, webhook, and MCP adapter plans. It returns a payload only when a current authority matches the channel and project. Fixture-bound checkpoints always return `no_send`, even with authority. The planner still does not perform the send. External delivery, retries, provider receipts, and permission storage belong to an explicit adapter or the optional Engine service. + +## Processyard fixture + +`keyoku pulse fixture processyard` provides a synthetic M0–M6 integration story. It includes: + +- a long-running development lease blocked on an owner decision; +- synthetic checkpoint digests that remain nondispatchable; +- a later `stale_no_send` planning instant; +- unresolved Economy Theatre poster/video paths, labeled as fixture bindings because the media bytes are not present in this repository. + +The fixture exercises parsing, replay, stale handling, and attestation rejection. +It does not establish a production Processyard integration, a deployed service, +coalescing of locally verified Factfiles, a Gmail authority grant, or a sent +founder email. diff --git a/docs/REPO-MAP.md b/docs/REPO-MAP.md index a59a8fc..8067287 100644 --- a/docs/REPO-MAP.md +++ b/docs/REPO-MAP.md @@ -1,63 +1,32 @@ -# Repo map — what's canonical, and what everything else is - -There are several directories named `keyoku*` across the workspace, and **two -different packages literally named `keyoku`**. This note removes the ambiguity. - -## The one canonical, published package - -**This repo (`keyoku-harness/`) is `keyoku` on npm** — the convergence + muscle-memory -harness, MCP-native, with the `keyoku` CLI. - -| | | -|---|---| -| npm | [`keyoku`](https://www.npmjs.com/package/keyoku) (the `latest` dist-tag) | -| package name | `keyoku` (see `package.json`) | -| bin | `keyoku` → `dist/index.js` | -| install | `npm install -g keyoku && keyoku init` | -| repo | `github.com/Keyoku-ai/keyoku` | -| site | `keyoku.ai` | - -If you are installing, depending on, or contributing to "Keyoku" — this is it. -Everything below is supporting or historical and is **not** what `npm i keyoku` -gives you. - -## Supporting components (not this npm package) - -- **`keyoku-engine/`** — the optional Go backend for teams: knowledge graph, - semantic search, memory decay, cross-device sync. The harness runs fully - standalone without it; set `KEYOKU_ENGINE_URL` to connect one. - Repo: `github.com/Keyoku-ai/keyoku-engine`. -- **`keyoku-site/`** — the marketing site for `keyoku.ai` (deployed separately). - -## Historical / sibling (do not confuse with the published package) - -- **`../Keyoku Harness/keyoku/`** — an **earlier, private** package *also* named - `keyoku` (v1.x), the original **AI-memory SDK** (auto-recall / auto-capture / - heartbeat) from before the harness became the product. It is **not** published as - the current `keyoku` and is not what this repo builds. Treat it as legacy unless - you are specifically working on the memory SDK lineage. -- **`keyoku-node/` (`@keyoku/sdk`), `keyoku-python/`, `keyoku-embedded/`, - `keyoku-bot/`, `keyoku-dashboard/`, `keyoku-git*`, `keyoku-infra/`, - `keyoku-demo/`** — experiments, SDKs, deploy infra, and demos in the broader - Keyoku family. None of them is the published `keyoku` CLI. - -## Local working trees — the canonical clone - -Two local clones of **this same repo** exist on the maintainer's machine. Editing -the wrong one ships nothing. The canonical one is: - -> **`~/Development/Keyoku/keyoku-harness`** — this is what the **live Claude Code -> MCP server runs** (`~/.claude.json` points at its `dist/index.js`). Edit here, -> `npm run build`, and the next session picks it up. - -The other clone — `~/Development/Keyoku Harness/keyoku-harness` — is a second -checkout used historically for `keyoku-site` deploys. To remove the foot-gun, -archive or delete it and keep a single working copy. (Both push to the same -GitHub remote, so no history is lost.) - -## Rule of thumb - -> When someone says "Keyoku," they mean **this package** (`keyoku-harness` → -> npm `keyoku`). The convergence loop and muscle memory live here. Anything else -> is a satellite — name it explicitly (`keyoku-engine`, the memory SDK, the site) -> to avoid the collision. +# Keyoku repository map + +Keyoku v3 is one product split across three repositories. This integration +branch is a local release candidate; it is not the published replacement yet. + +| Repository | Candidate responsibility | Current publication boundary | +|---|---|---| +| `keyoku` (this repository) | MIT CLI, Factfile and Pulse schemas, local verifier, planner, renderers, GitHub workflow, and bounded MCP adapters | npm `latest` remains on the v2 release line; no v3 package has been published | +| `keyoku-engine` | Optional durable Factfile/Pulse SQLite registry and API | Optional; not required for local proof or Pulse | +| `keyoku-site` | Authoritative product and documentation website | Replacement remains private until the exact candidate is approved | + +The v3 package contract is: + +```text +package: keyoku +binary: keyoku -> dist/index.js +install candidate: npm install -g keyoku@next +first command: keyoku proof init +rollback after a future alpha: npm install -g keyoku@2 +``` + +Do not use that install candidate as a claim that `next` exists today. Source +evaluation uses `npm ci`, `npm run build`, and `npm link` from this repository. + +The v2 goal, workflow, connector, activity, memory, and execution implementation +remains compatibility source. It is not registered by the v3 MCP server and its +test-only build is excluded from the npm package archive. See +[PUBLIC-SURFACE.md](PUBLIC-SURFACE.md) for the checked public inventory. + +The optional Engine does not make Keyoku an agent runner. Coding harnesses own +agent execution; Keyoku owns bounded evidence, human attention, exact-source +Factfiles, and trusted progress projections. diff --git a/docs/SECURITY-REVIEW.md b/docs/SECURITY-REVIEW.md new file mode 100644 index 0000000..41c2d81 --- /dev/null +++ b/docs/SECURITY-REVIEW.md @@ -0,0 +1,76 @@ +# Security review — contribution gate pivot + +Reviewed: 2026-08-25 +Scope: the new repository-local contribution, gate, review, Factfile rendering, publishing, and shared-ledger paths in `keyoku` and `keyoku-engine`. + +## Result + +The bounded local-alpha trust paths below have direct regression coverage. +Command probes now execute sequentially in fresh disposable Git checkouts made +from one content-addressed source capsule rather than in the mutable caller +checkout. This closes the prior source-binding blocker; it is still not a +general OS sandbox or a claim that arbitrary repository commands are safe. + +## Threat boundaries + +- Outcome command probes are trusted repository code. Adopting a project’s outcome contract is equivalent to trusting its test scripts. The disposable checkout protects evidence integrity, but the command retains the caller's user permissions, process, network, and external-filesystem access; inspect unfamiliar contracts and use ordinary CI isolation for untrusted forks. +- Factfiles are designed to leave the repository. Probe output is recursively redacted before JSON, Markdown, or HTML is written. +- Publishing is explicit. It accepts HTTPS or loopback HTTP, rejects credentials embedded in URLs, disables redirects, times out, and verifies that the repository still matches the Factfile before upload. +- The shared engine validates and stores evidence but never executes uploaded probes. +- A passing gate is not acceptance. Only an identified human can append acceptance, and stale Git/worktree snapshots are rejected. + +## Controls verified + +| Area | Control | +|---|---| +| Input validation | Zod schemas locally; the optional registry validates the stable envelope, enforces a 20 MB body limit, rejects duplicate JSON keys, and rejects unredacted credential-shaped fields | +| Output safety | Contextual HTML escaping and a restrictive CSP in local Factfile reports; registry retrieval returns the original JSON only | +| Evidence integrity | Fail-closed Git identity, NUL-delimited paths, a SHA-256 full-tree capsule containing tracked plus non-ignored untracked bytes and executable/symlink modes, fresh checkout per command criterion, mutation and mutate-restore rejection, original-tree revalidation, base/head/worktree binding, append-only snapshot history, canonical Factfile digest, and stale-proof rejection | +| Pulse promotion | Project/outcome/source binding for local Factfiles; adapter and fixture checkpoints remain attested and nondispatchable; public adapter ingress cannot self-claim local verification | +| Artifact containment | Lexical plus realpath containment, symbolic-link rejection, byte digests, size limits, and PNG/JPEG/WebP/MP4/WebM signature checks before portable embedding | +| Adapter authority | The neutral evaluator consumes submitted results only; it cannot execute a shell, accept human review, mutate caller state, choose a caller's assurance profile, or control an agent runtime | +| Authentication | The registry requires a bearer token for non-loopback binding and compares it in constant time | +| Network exposure | The registry binds to loopback by default; remote binding is explicit and token-gated | +| HTTP hardening | Header/body limits, read/header/write/idle timeouts, `nosniff`, frame denial, no-referrer, permissions policy | +| Storage | SQLite-backed immutable receipts, idempotent retry for identical bytes, and `409 Conflict` for changed content under an existing digest | +| CI permissions | GitHub proof workflow uses read-only repository permissions and uploads the generated receipt as an artifact | +| Secret scanning | Whole-history gitleaks scanning; fingerprint-scoped ignores cover only reviewed synthetic credential-redaction fixtures and model identifiers already present in public history, so new findings and all other locations still fail | + +## Dependency evidence + +- `npm audit`: **0 vulnerabilities** after upgrading Vitest/transitive packages and pinning a fixed `esbuild` through package overrides. +- `govulncheck ./...`: **0 reachable vulnerable symbols** and **0 vulnerable imported packages** after upgrading gRPC, OpenTelemetry, `x/net`, `x/crypto`, and related Go modules. +- The Go scanner reports one module-level advisory for the unmaintained `golang.org/x/crypto/openpgp` subpackage. It has no fixed version, and this codebase does not import or call it. This should remain visible in future scans rather than being mislabeled as resolved. + +## Residual limits + +- A malicious command can still use the caller's OS authority outside its disposable checkout. Keyoku is not a container, VM, syscall sandbox, network sandbox, or secret broker. Use an isolated CI job or stronger sandbox when the repository or probe contract is not trusted. +- Command criteria must be observational. Any write, add, delete, executable-mode change, or mutate-restore inside the disposable checkout rejects the proof. Build or test tools that write caches or generated outputs into the source tree need a read-only mode or a separately declared external work directory. +- The capsule contains tracked and non-ignored untracked Git source. Generated + `.keyoku/contributions`, `.keyoku/pulse`, and `.keyoku/runtime` state is + excluded so proof bookkeeping cannot self-invalidate; project, policy, + outcome, and architecture contracts remain included. Git-ignored + dependencies/build caches are environment inputs, not claimed source bytes. + Escaping symlinks, submodules, invalid UTF-8 paths, FIFOs/sockets/devices, + and cwd paths outside the capsule fail closed. +- Pulse dispatch planning accepts local checkpoints only when their current Factfile, source, and artifact bytes have been reverified through the filesystem adapter. Fixture and unauthenticated adapter attestations remain visible but nondispatchable. +- Redaction is defense in depth, not a replacement for keeping secrets out of test output. +- Factfile digests provide tamper evidence, not an externally authenticated signature. The versioned conformance manifest freezes numeric/Unicode canonicalization, strict UTF-8 and surrogate rejection, duplicate-key rejection, and the shared redaction marker for cross-language implementations. +- “Passed” covers only declared criteria. It does not prove absence of undeclared bugs or vulnerabilities. + +## Reproduction + +```bash +# keyoku +npm audit +npm run typecheck +npm test -- --run +npm run eval +npm run preflight + +# keyoku-engine +go test ./... +go test -race ./factfile ./cmd/keyoku-registry +go vet ./... +go run golang.org/x/vuln/cmd/govulncheck@latest -show verbose ./... +``` diff --git a/docs/artifacts/keyoku-factfile-current.png b/docs/artifacts/keyoku-factfile-current.png new file mode 100644 index 0000000..ce17125 Binary files /dev/null and b/docs/artifacts/keyoku-factfile-current.png differ diff --git a/docs/artifacts/keyoku-live-decision.webm b/docs/artifacts/keyoku-live-decision.webm new file mode 100644 index 0000000..4449451 Binary files /dev/null and b/docs/artifacts/keyoku-live-decision.webm differ diff --git a/docs/demo-evidence.md b/docs/demo-evidence.md new file mode 100644 index 0000000..110f33e --- /dev/null +++ b/docs/demo-evidence.md @@ -0,0 +1,224 @@ +# Demo evidence: record → watch → gate + +`keyoku demo` turns a scripted product demo into a recorded, agent-watched, +machine-checkable piece of evidence — usable in **any** project, not just +Keyoku itself. + +## Why + +Humans digest a demo instantly: open the app, click through the flow, look +at what's on screen. That's exactly the evidence a reviewer actually trusts +— more than a green test suite, because it shows the thing the user will +see. But a demo is normally a one-off, unrecorded, unverifiable act: someone +clicked through it once, said "looks good," and that moment is gone. + +Coding agents can watch a demo the same way a human does — by looking at +screenshots and checking them against stated expectations. `keyoku demo` +makes that watching step first-class: + +1. **`keyoku demo record`** drives a real browser through a scripted walk of + the running app and captures one screenshot ("frame") per stop, plus a + manifest of what each frame is supposed to show. +2. **`keyoku demo watch`** has an agent actually look at each frame, check + it against the stated expectations, and also run a general UI/UX audit + across the whole demo — then write a structured verdict. +3. **`keyoku demo watch --assert`** turns that verdict into a pass/fail exit + code, so `keyoku demo record && keyoku demo watch --assert` can be pasted + straight into an outcome's `criteria[].probe.run` — the watched demo + becomes part of the proof, not a claim about the proof. + +Keyoku's evidence model (`EvidencePresentationSchema` in `src/contribution.ts`) +already supports `artifacts[].kind: "screenshot"` / `"video"` in a Factfile. +`keyoku demo` is the workflow that actually *produces* those artifacts and +validates them before they're presented, instead of leaving it to whoever +proposed the change to attach (or not attach) a screenshot by hand. + +## The `demo.yaml` schema + +`keyoku demo init` writes `.keyoku/demo.yaml` (never overwrites an existing +one) with a commented template. Full shape: + +```yaml +baseUrl: http://localhost:3000 # required — where the app is running + +viewport: # optional, default 1440x900 + width: 1440 + height: 900 + +settleMs: 2500 # optional, default 2500 — wait after + # navigating/acting, before the shot + +fullPage: true # optional, default true — can be + # overridden per-stop + +auth: # optional — runs ONCE, before stop 1 + url: /login # relative to baseUrl, or absolute + steps: # same Action union as stops[].actions + - fill: { selector: "#email", value: "demo@example.com" } + - fill: { selector: "#password", value: "demo-password" } + - click: "button[type=submit]" + - waitMs: 1000 + +stops: # required, at least one + - id: dashboard # required, slug (lowercase/digits/-._) + title: Dashboard # optional, shown to the watching agent + url: /dashboard # optional — relative to baseUrl or + # absolute; omit to stay on the current + # page (e.g. after a click from a + # previous stop) + actions: # optional, run in order after goto + - click: "#nav-settings" + - waitMs: 500 + fullPage: false # optional, overrides the global default + expect: # REQUIRED, at least one — plain + # language assertions about what must + # be VISIBLE in this frame + - "The main navigation is visible" + - "At least one summary metric card is rendered with a non-empty value" + caption: "Landing view after login" # optional, shown in the report +``` + +`Action` (used in both `auth.steps` and `stops[].actions`) is one of: + +```ts +type Action = + | { goto: string } + | { click: string } + | { fill: { selector: string; value: string } } + | { select: { selector: string; label: string } } + | { press: string } + | { waitMs: number }; +``` + +## Recording + +`keyoku demo record`: + +1. Reads and zod-validates `.keyoku/demo.yaml`. +2. Launches Chromium via `playwright`, resolved from **the target project** + (via `createRequire` against that project's own `package.json`) — not + from keyoku's own dependencies. If `playwright` isn't installed there, + the command exits with a clear message: `npm i -D playwright`. +3. Runs `auth` once, if present. +4. For each stop, in order: `goto` (if `url` given) → run `actions` → wait + `settleMs` → screenshot to + `.keyoku/demo/frames/-.jpeg` (JPEG, quality 80, animations + disabled, full-page unless overridden). +5. Writes `.keyoku/demo/manifest.json`: + +```json +{ + "recordedAt": "2026-08-23T12:00:00.000Z", + "baseUrl": "http://localhost:3000", + "stops": [ + { + "id": "dashboard", + "order": 1, + "frame": ".keyoku/demo/frames/01-dashboard.jpeg", + "title": "Dashboard", + "expect": ["The main navigation is visible", "..."], + "caption": "Landing view after login" + } + ] +} +``` + +A stop that throws (bad selector, navigation failure, timeout, ...) is +recorded and reported by id, and the command exits non-zero — but every +*other* stop still gets attempted and included in the manifest. + +## Watching — the verdict contract + +`keyoku demo watch` reads the manifest, builds one prompt covering every +frame plus its `expect` list, and runs it through an agent. The default +runner is the `claude` CLI: + +``` +claude -p "" --permission-mode acceptEdits +``` + +run with `cwd` set to the project root. If `claude` isn't on `PATH`, the +command exits with code `2` and names the contract below, so **any other +agent runner can be substituted** — the only requirement is that it writes +`.keyoku/demo/verdict.json` matching this shape: + +```json +{ + "watched_at": "2026-08-23T12:05:00.000Z", + "frames": [ + { + "id": "dashboard", + "requirement_met": true, + "evidence_seen": "Top nav with 4 links is visible; 3 metric cards show non-empty values.", + "concerns": [] + } + ], + "overall": { + "frames_pass": 1, + "frames_partial": 0, + "frames_fail": 0, + "verdict": "pass", + "summary": "Dashboard renders as expected; no missing elements." + }, + "uiux_audit": { + "findings": [ + { "severity": "low", "description": "Metric card labels truncate on narrow viewports.", "suggested_fix": "Wrap instead of truncating, or shorten labels." } + ], + "top_priorities": ["Fix metric card label truncation"] + } +} +``` + +- `requirement_met` is `true`, `false`, or `"partial"` per frame. +- `overall.verdict` is `"pass"` **if and only if** no frame has + `requirement_met === false`. +- `uiux_audit` is a separate, cross-frame pass — visual hierarchy, density, + truncation, empty/broken charts, color semantics, cross-page consistency — + independent of whether the per-stop `expect` assertions held. + +After the run, `keyoku demo watch` validates `verdict.json` against this +contract with zod and prints a summary (pass/partial/fail counts, failing +frames, top UI/UX findings). + +## Gating: `--assert` + +`keyoku demo watch --assert` exits: + +- **`0`** only if `overall.verdict === "pass"` **and** + `verdict.watched_at` is strictly newer than `manifest.recordedAt` (i.e. + the verdict is actually about the demo that was just recorded, not a + stale one from a previous run). +- **`1`** otherwise, printing which frames failed/partialed or why the + verdict was considered stale. +- **`2`** for setup problems (no manifest, no `claude` CLI, a verdict that + doesn't match the contract). + +This makes `keyoku demo record && keyoku demo watch --assert` a valid +`command` probe for an outcome criterion — `keyoku demo init` prints a +ready-to-paste snippet: + +```yaml +- description: "The recorded product demo passes agent review" + probe: + kind: command + run: "keyoku demo record && keyoku demo watch --assert" + timeoutMs: 900000 + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: "An agent watched the recorded demo frames against their stated expectations and ran a UI/UX audit." + whyItMatters: "The demo is recorded evidence, not a claim about it — the same frames a human would watch are what the agent checked." + code: [] + artifacts: + - kind: screenshot + path: ".keyoku/demo/frames/*.jpeg" + label: "Recorded demo frames" + caption: "One frame per stop, captured by keyoku demo record" +``` + +`timeoutMs: 900000` (15 minutes) matches the raised `CommandProbeSchema` / +`HttpProbeSchema` cap in `src/types.ts` — a real record → launch-agent → +watch round trip, on a real frontend build, routinely exceeds the old +5-minute cap. diff --git a/docs/examples/contribution.yaml b/docs/examples/contribution.yaml new file mode 100644 index 0000000..df1afd1 --- /dev/null +++ b/docs/examples/contribution.yaml @@ -0,0 +1,21 @@ +schemaVersion: keyoku.dev/contribution/v1alpha1 +id: release-build-2026-08-09-a1b2c3d4 +title: Make the release build reproducible +outcomeId: release-build +outcomeRevision: 1 +baseSha: 0123456789abcdef0123456789abcdef01234567 +actors: + - kind: human + id: maintainer@example.com + name: Project maintainer + role: accountable owner + - kind: agent + id: codex:gpt-5.6-sol + name: Codex + role: implementation agent + ownerId: maintainer@example.com + harness: Codex + model: gpt-5.6-sol +status: draft +createdAt: 2026-08-09T00:00:00Z +updatedAt: 2026-08-09T00:00:00Z diff --git a/docs/examples/outcome.yaml b/docs/examples/outcome.yaml new file mode 100644 index 0000000..b1b7581 --- /dev/null +++ b/docs/examples/outcome.yaml @@ -0,0 +1,67 @@ +schemaVersion: keyoku.dev/outcome/v1alpha1 +id: release-build +revision: 1 +title: The release artifact builds from source +objective: A maintainer can produce and smoke-test the release artifact from a clean checkout. +owner: + kind: human + id: maintainer@example.com + name: Project maintainer + role: accountable owner +constraints: + - The check runs locally and in CI. + - No production credential is required. +scope: + include: + - src/** + - tests/** + - package.json + - package-lock.json + exclude: + - docs/** + maxChangedFiles: 30 +criteria: + - description: The release build completes successfully + probe: + kind: command + run: npm run build + timeoutMs: 120000 + parse: text + assert: + path: exitCode + op: eq + value: 0 + evidence: + summary: The repository produced its release build at the exact revision shown in the Factfile. + whyItMatters: A release cannot ship if downstream users cannot build its declared artifact. + code: + - path: src/build.ts + purpose: Produces the release bundle. + artifacts: + - kind: report + path: dist/build-manifest.json + label: Build manifest + caption: Lists the files emitted by the verified build. + - description: The built CLI reports its version + probe: + kind: command + run: node dist/index.js --version + timeoutMs: 30000 + parse: text + assert: + path: output + op: matches + value: ^[0-9]+\.[0-9]+\.[0-9]+ + evidence: + summary: The built command starts and reports a semantic version. + whyItMatters: This smoke test observes the artifact users will actually invoke, not only its source files. + code: + - path: src/index.ts + purpose: Implements the command-line entry point. + artifacts: [] +humanCriteria: + - id: release-notes-quality + description: A maintainer confirms the release notes clearly explain user-visible changes and upgrade risk + guidance: Read the notes as a downstream user and record a pass or fail with a short explanation. +createdAt: 2026-08-09T00:00:00Z +updatedAt: 2026-08-09T00:00:00Z diff --git a/fixtures/assurance/v1/evidence.json b/fixtures/assurance/v1/evidence.json new file mode 100644 index 0000000..d995922 --- /dev/null +++ b/fixtures/assurance/v1/evidence.json @@ -0,0 +1,47 @@ +{ + "schemaVersion": "evidence-provider/v1", + "work": { + "id": "sample-change", + "objective": "Confirm the bounded change behaves as declared." + }, + "claims": [ + { + "id": "behavior", + "statement": "The declared behavior passes its native check.", + "verdict": "pass", + "evidenceRefs": [ + "native-check", + "result-log" + ] + } + ], + "source": { + "capturedDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "currentDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "label": "source snapshot" + }, + "commands": [ + { + "id": "native-check", + "command": "project-test-command", + "exitCode": 0, + "resultDigest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + ], + "artifacts": [ + { + "id": "result-log", + "path": "evidence/result.txt", + "digest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + } + ], + "limitations": [ + "This generic fixture contains synthetic digests and establishes no deployment claim." + ], + "authority": { + "kind": "human", + "id": "review-owner", + "decision": "approved" + }, + "contentDigest": "f9b802d170c98e99b11e7e46fcfd60114ae80c5a31795f133fdf69708dacf22e" +} diff --git a/fixtures/assurance/v1/work-events.jsonl b/fixtures/assurance/v1/work-events.jsonl new file mode 100644 index 0000000..6c89405 --- /dev/null +++ b/fixtures/assurance/v1/work-events.jsonl @@ -0,0 +1,2 @@ +{"schemaVersion":"work-event/v1","id":"sample-checkpoint","kind":"checkpoint","at":"2026-08-25T16:00:00.000Z","workId":"sample-change","summary":"A content-bound evidence result is available for caller review.","outcome":"checkpoint_ready","sourceDigest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","limitations":["This generic fixture contains synthetic digests and establishes no deployment claim."],"eventDigest":"a7176a9ccd8c2ab17c78b464a2d948f7d47c47d63e25f5379fa953c9b5d21e6c"} +{"schemaVersion":"work-event/v1","id":"sample-terminal","kind":"terminal","at":"2026-08-25T16:05:00.000Z","workId":"sample-change","summary":"The caller recorded its terminal outcome.","outcome":"complete","limitations":["This generic fixture contains synthetic digests and establishes no deployment claim."],"eventDigest":"845bb5fef664b1fac1c844d7e860e5a43a1f938fed9ae8732a3fb10be71bc98b"} diff --git a/fixtures/conformance/v1/README.md b/fixtures/conformance/v1/README.md new file mode 100644 index 0000000..fd5d531 --- /dev/null +++ b/fixtures/conformance/v1/README.md @@ -0,0 +1,17 @@ +# Keyoku Pulse conformance vectors · v1 + +These files are the stable cross-implementation contract for Keyoku Pulse v1alpha1. They are deterministic fixtures, not live product or deployment evidence. + +- `manifest.json` defines canonical JSON, exact byte digests, replay expectations, source-conflict behavior, and dispatch outcomes. +- `events/*.jsonl` contains the exact lifecycle inputs, including reversed input, a deliberately ambiguous same-time event set, and incompatible source roots. +- `factfiles/verified.json` is a complete digest-valid Factfile used to bind the raw-file `bytesDigest` vector. +- `assets/demo-bytes.bin` and `assets/poster.svg` bind `digest` and `posterDigest` to exact fixture bytes. The `.bin` file is intentionally not playable media. + +Regenerate and verify from repository source: + +```bash +npm run fixtures:conformance +npx vitest run tests/conformance.test.ts +``` + +Changing any vector is a protocol change: update the conformance version or explicitly reconcile every consuming implementation. diff --git a/fixtures/conformance/v1/assets/demo-bytes.bin b/fixtures/conformance/v1/assets/demo-bytes.bin new file mode 100644 index 0000000..b0344de --- /dev/null +++ b/fixtures/conformance/v1/assets/demo-bytes.bin @@ -0,0 +1 @@ +KEYOKU PULSE CONFORMANCE ASSET BYTES v1 diff --git a/fixtures/conformance/v1/assets/poster.svg b/fixtures/conformance/v1/assets/poster.svg new file mode 100644 index 0000000..6e95efe --- /dev/null +++ b/fixtures/conformance/v1/assets/poster.svg @@ -0,0 +1 @@ +Keyoku Pulse conformance posterDeterministic fixture bytes; not live product evidence.KEYOKU PULSEConformance fixtureExact poster bytes · v1fixture only · not live evidence diff --git a/fixtures/conformance/v1/events/generic-reversed.jsonl b/fixtures/conformance/v1/events/generic-reversed.jsonl new file mode 100644 index 0000000..2612b55 --- /dev/null +++ b/fixtures/conformance/v1/events/generic-reversed.jsonl @@ -0,0 +1,4 @@ +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-completed","at":"2026-08-24T16:04:00.000Z","eventDigest":"f079776e16bb6e69e33580afb34afb77320b18b9a07aa59c17621eaf9c36220a","type":"completed","leaseId":"generic-run-agent-1","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"},"checkpointId":"cart-recovery-verified"} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-checkpoint","at":"2026-08-24T16:03:00.000Z","eventDigest":"05060a1add9a0b247f44267fd9f6ac3104a7fdbdb2b6d5c348910f0ab6f53512","type":"checkpoint_published","leaseId":"generic-run-agent-1","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"cart-recovery-verified","projectId":"checkout-example","outcomeId":"restore-cart","runId":"checkout-example-run-20260824","leaseIds":["generic-run-agent-1"],"title":"Saved-cart fixture checkpoint","changeSummary":"The cart recovery path now restores product ids and quantities from the saved session.","whyItMatters":"Returning shoppers can continue checkout without rebuilding their cart.","publishedAt":"2026-08-24T16:03:00.000Z","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:03:00.000Z","methods":[{"kind":"command","label":"cart-recovery-verified acceptance check","reproduce":"./scripts/verify-checkpoint cart-recovery-verified","result":"Passed in the integration fixture","evidenceDigest":"95e173bc297d6ab5ff167c3dad6ad4cb59380871cd118a4efb0d9647453693d1"}]},"evidenceBinding":{"mode":"fixture","label":"checkout-example demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"cart-recovery-verified-factfile","projectId":"checkout-example","outcomeId":"restore-cart","path":".keyoku/contributions/cart-recovery-verified/factfile.json","digest":"34b9827ff17251b1d578faabd89898c34c6e84458fdbd90d5a1c85e45d9fb3ac","sourceDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c","state":"ready_for_review"}],"assets":[],"limitations":["Visual review of the empty-cart state is still pending."],"nextTask":"Capture the empty-cart visual Factfile.","materialTrigger":"verified_checkpoint","contentDigest":"64bbc59fb709ca09cc7a78b23b45611a3f2bc2cc351a69f5b393fe430f32b734"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-verifying","at":"2026-08-24T16:02:00.000Z","eventDigest":"7290a26fed55502deeb10462743d5ae2d27ef8fab21ff359606f1f1572f51367","type":"verification_started","leaseId":"generic-run-agent-1","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-started","at":"2026-08-24T16:00:00.000Z","eventDigest":"8f4cf64f696e0a95e1d9d9865a609a66531e8fceff0bc1e43d309e4bbb9c4d5a","type":"started","leaseId":"generic-run-agent-1","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"generic-run-agent-1","harness":"generic-jsonl","project":{"id":"checkout-example","name":"Checkout Example"},"runId":"checkout-example-run-20260824","agent":{"id":"agent-1","name":"Fixture agent"},"canonicalSourceRoot":"repo://example/checkout","task":{"id":"restore-cart","title":"Restore the saved cart","outcome":"Returning shoppers recover the same products and quantities."},"startedAt":"2026-08-24T16:00:00.000Z","heartbeatAt":"2026-08-24T16:00:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"b6359aff2fbdabcc47410462203c78e065e2378b03a102fe7112dfd431e792bd"}}} diff --git a/fixtures/conformance/v1/events/generic-same-time-ambiguous.jsonl b/fixtures/conformance/v1/events/generic-same-time-ambiguous.jsonl new file mode 100644 index 0000000..bdd1878 --- /dev/null +++ b/fixtures/conformance/v1/events/generic-same-time-ambiguous.jsonl @@ -0,0 +1,5 @@ +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-started","at":"2026-08-24T16:00:00.000Z","eventDigest":"8f4cf64f696e0a95e1d9d9865a609a66531e8fceff0bc1e43d309e4bbb9c4d5a","type":"started","leaseId":"generic-run-agent-1","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"generic-run-agent-1","harness":"generic-jsonl","project":{"id":"checkout-example","name":"Checkout Example"},"runId":"checkout-example-run-20260824","agent":{"id":"agent-1","name":"Fixture agent"},"canonicalSourceRoot":"repo://example/checkout","task":{"id":"restore-cart","title":"Restore the saved cart","outcome":"Returning shoppers recover the same products and quantities."},"startedAt":"2026-08-24T16:00:00.000Z","heartbeatAt":"2026-08-24T16:00:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"b6359aff2fbdabcc47410462203c78e065e2378b03a102fe7112dfd431e792bd"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-verifying","at":"2026-08-24T16:02:00.000Z","eventDigest":"7290a26fed55502deeb10462743d5ae2d27ef8fab21ff359606f1f1572f51367","type":"verification_started","leaseId":"generic-run-agent-1","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-checkpoint","at":"2026-08-24T16:03:00.000Z","eventDigest":"05060a1add9a0b247f44267fd9f6ac3104a7fdbdb2b6d5c348910f0ab6f53512","type":"checkpoint_published","leaseId":"generic-run-agent-1","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"cart-recovery-verified","projectId":"checkout-example","outcomeId":"restore-cart","runId":"checkout-example-run-20260824","leaseIds":["generic-run-agent-1"],"title":"Saved-cart fixture checkpoint","changeSummary":"The cart recovery path now restores product ids and quantities from the saved session.","whyItMatters":"Returning shoppers can continue checkout without rebuilding their cart.","publishedAt":"2026-08-24T16:03:00.000Z","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:03:00.000Z","methods":[{"kind":"command","label":"cart-recovery-verified acceptance check","reproduce":"./scripts/verify-checkpoint cart-recovery-verified","result":"Passed in the integration fixture","evidenceDigest":"95e173bc297d6ab5ff167c3dad6ad4cb59380871cd118a4efb0d9647453693d1"}]},"evidenceBinding":{"mode":"fixture","label":"checkout-example demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"cart-recovery-verified-factfile","projectId":"checkout-example","outcomeId":"restore-cart","path":".keyoku/contributions/cart-recovery-verified/factfile.json","digest":"34b9827ff17251b1d578faabd89898c34c6e84458fdbd90d5a1c85e45d9fb3ac","sourceDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c","state":"ready_for_review"}],"assets":[],"limitations":["Visual review of the empty-cart state is still pending."],"nextTask":"Capture the empty-cart visual Factfile.","materialTrigger":"verified_checkpoint","contentDigest":"64bbc59fb709ca09cc7a78b23b45611a3f2bc2cc351a69f5b393fe430f32b734"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-completed","at":"2026-08-24T16:04:00.000Z","eventDigest":"f079776e16bb6e69e33580afb34afb77320b18b9a07aa59c17621eaf9c36220a","type":"completed","leaseId":"generic-run-agent-1","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"},"checkpointId":"cart-recovery-verified"} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-same-time-heartbeat","at":"2026-08-24T16:02:00.000Z","eventDigest":"7632d22fff00d0acf98d21e6d597f77d88fd9a0a2ea1093a3a08bf4e545c0129","type":"heartbeat","leaseId":"generic-run-agent-1","state":"working","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"}} diff --git a/fixtures/conformance/v1/events/generic-through-checkpoint.jsonl b/fixtures/conformance/v1/events/generic-through-checkpoint.jsonl new file mode 100644 index 0000000..739b02a --- /dev/null +++ b/fixtures/conformance/v1/events/generic-through-checkpoint.jsonl @@ -0,0 +1,3 @@ +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-started","at":"2026-08-24T16:00:00.000Z","eventDigest":"8f4cf64f696e0a95e1d9d9865a609a66531e8fceff0bc1e43d309e4bbb9c4d5a","type":"started","leaseId":"generic-run-agent-1","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"generic-run-agent-1","harness":"generic-jsonl","project":{"id":"checkout-example","name":"Checkout Example"},"runId":"checkout-example-run-20260824","agent":{"id":"agent-1","name":"Fixture agent"},"canonicalSourceRoot":"repo://example/checkout","task":{"id":"restore-cart","title":"Restore the saved cart","outcome":"Returning shoppers recover the same products and quantities."},"startedAt":"2026-08-24T16:00:00.000Z","heartbeatAt":"2026-08-24T16:00:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"b6359aff2fbdabcc47410462203c78e065e2378b03a102fe7112dfd431e792bd"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-verifying","at":"2026-08-24T16:02:00.000Z","eventDigest":"7290a26fed55502deeb10462743d5ae2d27ef8fab21ff359606f1f1572f51367","type":"verification_started","leaseId":"generic-run-agent-1","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-checkpoint","at":"2026-08-24T16:03:00.000Z","eventDigest":"05060a1add9a0b247f44267fd9f6ac3104a7fdbdb2b6d5c348910f0ab6f53512","type":"checkpoint_published","leaseId":"generic-run-agent-1","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"cart-recovery-verified","projectId":"checkout-example","outcomeId":"restore-cart","runId":"checkout-example-run-20260824","leaseIds":["generic-run-agent-1"],"title":"Saved-cart fixture checkpoint","changeSummary":"The cart recovery path now restores product ids and quantities from the saved session.","whyItMatters":"Returning shoppers can continue checkout without rebuilding their cart.","publishedAt":"2026-08-24T16:03:00.000Z","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:03:00.000Z","methods":[{"kind":"command","label":"cart-recovery-verified acceptance check","reproduce":"./scripts/verify-checkpoint cart-recovery-verified","result":"Passed in the integration fixture","evidenceDigest":"95e173bc297d6ab5ff167c3dad6ad4cb59380871cd118a4efb0d9647453693d1"}]},"evidenceBinding":{"mode":"fixture","label":"checkout-example demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"cart-recovery-verified-factfile","projectId":"checkout-example","outcomeId":"restore-cart","path":".keyoku/contributions/cart-recovery-verified/factfile.json","digest":"34b9827ff17251b1d578faabd89898c34c6e84458fdbd90d5a1c85e45d9fb3ac","sourceDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c","state":"ready_for_review"}],"assets":[],"limitations":["Visual review of the empty-cart state is still pending."],"nextTask":"Capture the empty-cart visual Factfile.","materialTrigger":"verified_checkpoint","contentDigest":"64bbc59fb709ca09cc7a78b23b45611a3f2bc2cc351a69f5b393fe430f32b734"}} diff --git a/fixtures/conformance/v1/events/generic-through-verification.jsonl b/fixtures/conformance/v1/events/generic-through-verification.jsonl new file mode 100644 index 0000000..9d5adf2 --- /dev/null +++ b/fixtures/conformance/v1/events/generic-through-verification.jsonl @@ -0,0 +1,2 @@ +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-started","at":"2026-08-24T16:00:00.000Z","eventDigest":"8f4cf64f696e0a95e1d9d9865a609a66531e8fceff0bc1e43d309e4bbb9c4d5a","type":"started","leaseId":"generic-run-agent-1","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"generic-run-agent-1","harness":"generic-jsonl","project":{"id":"checkout-example","name":"Checkout Example"},"runId":"checkout-example-run-20260824","agent":{"id":"agent-1","name":"Fixture agent"},"canonicalSourceRoot":"repo://example/checkout","task":{"id":"restore-cart","title":"Restore the saved cart","outcome":"Returning shoppers recover the same products and quantities."},"startedAt":"2026-08-24T16:00:00.000Z","heartbeatAt":"2026-08-24T16:00:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"b6359aff2fbdabcc47410462203c78e065e2378b03a102fe7112dfd431e792bd"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-verifying","at":"2026-08-24T16:02:00.000Z","eventDigest":"7290a26fed55502deeb10462743d5ae2d27ef8fab21ff359606f1f1572f51367","type":"verification_started","leaseId":"generic-run-agent-1","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"}} diff --git a/fixtures/conformance/v1/events/generic.jsonl b/fixtures/conformance/v1/events/generic.jsonl new file mode 100644 index 0000000..18d81cd --- /dev/null +++ b/fixtures/conformance/v1/events/generic.jsonl @@ -0,0 +1,4 @@ +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-started","at":"2026-08-24T16:00:00.000Z","eventDigest":"8f4cf64f696e0a95e1d9d9865a609a66531e8fceff0bc1e43d309e4bbb9c4d5a","type":"started","leaseId":"generic-run-agent-1","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"generic-run-agent-1","harness":"generic-jsonl","project":{"id":"checkout-example","name":"Checkout Example"},"runId":"checkout-example-run-20260824","agent":{"id":"agent-1","name":"Fixture agent"},"canonicalSourceRoot":"repo://example/checkout","task":{"id":"restore-cart","title":"Restore the saved cart","outcome":"Returning shoppers recover the same products and quantities."},"startedAt":"2026-08-24T16:00:00.000Z","heartbeatAt":"2026-08-24T16:00:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"b6359aff2fbdabcc47410462203c78e065e2378b03a102fe7112dfd431e792bd"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-verifying","at":"2026-08-24T16:02:00.000Z","eventDigest":"7290a26fed55502deeb10462743d5ae2d27ef8fab21ff359606f1f1572f51367","type":"verification_started","leaseId":"generic-run-agent-1","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-checkpoint","at":"2026-08-24T16:03:00.000Z","eventDigest":"05060a1add9a0b247f44267fd9f6ac3104a7fdbdb2b6d5c348910f0ab6f53512","type":"checkpoint_published","leaseId":"generic-run-agent-1","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"cart-recovery-verified","projectId":"checkout-example","outcomeId":"restore-cart","runId":"checkout-example-run-20260824","leaseIds":["generic-run-agent-1"],"title":"Saved-cart fixture checkpoint","changeSummary":"The cart recovery path now restores product ids and quantities from the saved session.","whyItMatters":"Returning shoppers can continue checkout without rebuilding their cart.","publishedAt":"2026-08-24T16:03:00.000Z","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:03:00.000Z","methods":[{"kind":"command","label":"cart-recovery-verified acceptance check","reproduce":"./scripts/verify-checkpoint cart-recovery-verified","result":"Passed in the integration fixture","evidenceDigest":"95e173bc297d6ab5ff167c3dad6ad4cb59380871cd118a4efb0d9647453693d1"}]},"evidenceBinding":{"mode":"fixture","label":"checkout-example demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"cart-recovery-verified-factfile","projectId":"checkout-example","outcomeId":"restore-cart","path":".keyoku/contributions/cart-recovery-verified/factfile.json","digest":"34b9827ff17251b1d578faabd89898c34c6e84458fdbd90d5a1c85e45d9fb3ac","sourceDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c","state":"ready_for_review"}],"assets":[],"limitations":["Visual review of the empty-cart state is still pending."],"nextTask":"Capture the empty-cart visual Factfile.","materialTrigger":"verified_checkpoint","contentDigest":"64bbc59fb709ca09cc7a78b23b45611a3f2bc2cc351a69f5b393fe430f32b734"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-completed","at":"2026-08-24T16:04:00.000Z","eventDigest":"f079776e16bb6e69e33580afb34afb77320b18b9a07aa59c17621eaf9c36220a","type":"completed","leaseId":"generic-run-agent-1","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"},"checkpointId":"cart-recovery-verified"} diff --git a/fixtures/conformance/v1/events/processyard.jsonl b/fixtures/conformance/v1/events/processyard.jsonl new file mode 100644 index 0000000..3477aa0 --- /dev/null +++ b/fixtures/conformance/v1/events/processyard.jsonl @@ -0,0 +1,13 @@ +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-development-started","at":"2026-08-24T16:00:00.000Z","eventDigest":"879425a35947b4f12a53c501c5abf3cd99dbb8fbcc594cbe784fe14870989395","type":"started","leaseId":"processyard-development","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"processyard-development","harness":"codex","project":{"id":"processyard","name":"Processyard"},"runId":"processyard-run-20260824","agent":{"id":"development-agent","name":"Development agent"},"canonicalSourceRoot":"repo://processyard/main","task":{"id":"economy-theatre","title":"Build the Economy Theatre story","outcome":"A founder can see verified product progress without reading agent transcripts."},"startedAt":"2026-08-24T16:00:00.000Z","heartbeatAt":"2026-08-24T16:00:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"1ceb1ca10e27873d4c61eddd81058dd41ba3616ea8e5a1891d9aea76fbb9c95a"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-evidence-started","at":"2026-08-24T16:01:00.000Z","eventDigest":"44e1c837a78f260f60b6e043c9a23bcbfd7b8fc0a17d54b9ae62b800cde75f06","type":"started","leaseId":"processyard-evidence","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"processyard-evidence","harness":"claude-code","project":{"id":"processyard","name":"Processyard"},"runId":"processyard-run-20260824","agent":{"id":"evidence-agent","name":"Evidence agent"},"canonicalSourceRoot":"repo://processyard/main","task":{"id":"economy-theatre-evidence","title":"Capture human-readable product evidence","outcome":"The milestone story includes replayable UI evidence and explicit limitations."},"startedAt":"2026-08-24T16:01:00.000Z","heartbeatAt":"2026-08-24T16:01:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"1ceb1ca10e27873d4c61eddd81058dd41ba3616ea8e5a1891d9aea76fbb9c95a"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-ci-started","at":"2026-08-24T16:02:00.000Z","eventDigest":"4b5db6b77462a3eee14495eb347190a633ba57544f0c7497f09f9ff1f8907084","type":"started","leaseId":"processyard-ci","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"processyard-ci","harness":"github-actions","project":{"id":"processyard","name":"Processyard"},"runId":"processyard-run-20260824","agent":{"id":"verification-workflow","name":"Verification workflow"},"canonicalSourceRoot":"repo://processyard/main","task":{"id":"economy-theatre-verification","title":"Verify the checkpoint boundary","outcome":"Every reported milestone is bound to a reproducible Factfile and exact source digest."},"startedAt":"2026-08-24T16:02:00.000Z","heartbeatAt":"2026-08-24T16:02:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"1ceb1ca10e27873d4c61eddd81058dd41ba3616ea8e5a1891d9aea76fbb9c95a"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m0-published","at":"2026-08-24T16:10:00.000Z","eventDigest":"00ebb5675a083f4643522730dd21dd160de4709f4356708ec633d6c6e6d98653","type":"checkpoint_published","leaseId":"processyard-development","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m0","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-development"],"title":"M0 · Outcome pinned","changeSummary":"The founder outcome and source boundary were recorded before implementation.","whyItMatters":"The milestone story starts from an explicit definition of done.","publishedAt":"2026-08-24T16:10:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"1ceb1ca10e27873d4c61eddd81058dd41ba3616ea8e5a1891d9aea76fbb9c95a"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:10:00.000Z","methods":[{"kind":"command","label":"processyard-m0 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m0","result":"Passed in the integration fixture","evidenceDigest":"fe5b19d3508b254ac100801b7f9ef21e93359da67f8ac78a42f1d819f767c47e"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m0-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m0/factfile.json","digest":"687f833e5578025cdba94cbefd6cc2f61c66545508c1f1cdf3e36294e0e3253c","sourceDigest":"1ceb1ca10e27873d4c61eddd81058dd41ba3616ea8e5a1891d9aea76fbb9c95a","state":"ready_for_review"}],"assets":[],"limitations":["Later Processyard milestones remain outside this checkpoint."],"nextTask":"Verify M1 without changing the established source boundary.","materialTrigger":"verified_checkpoint","contentDigest":"4cf871a73c22ddc216fdf13333a978296eacdd40abd0e7ee47ea1a813b38105e"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m1-published","at":"2026-08-24T16:15:00.000Z","eventDigest":"829e100c648f57406414915517f48b2133ac5158efdf707542cc1d4fdff28a65","type":"checkpoint_published","leaseId":"processyard-development","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m1","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-development"],"title":"M1 · Storefront path observed","changeSummary":"The current customer journey and evidence gaps were captured.","whyItMatters":"Work begins from the real product path instead of an imagined interface.","publishedAt":"2026-08-24T16:15:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"0760994c45cc1f3c6a6c0c616f3dbb5ef1bf7f6154b7bbcdb0b9556cc29e74cb"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:15:00.000Z","methods":[{"kind":"command","label":"processyard-m1 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m1","result":"Passed in the integration fixture","evidenceDigest":"107bac10ebc481c8ba14b7ecc16aac336c203875fb90a2248087377217d10ca3"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m1-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m1/factfile.json","digest":"ed1472e4ff2362c8dd6cc5d53ea7955995d8ac0b80982326c8e6ed74e4e658d8","sourceDigest":"0760994c45cc1f3c6a6c0c616f3dbb5ef1bf7f6154b7bbcdb0b9556cc29e74cb","state":"ready_for_review"}],"assets":[],"limitations":["Later Processyard milestones remain outside this checkpoint."],"nextTask":"Verify M2 without changing the established source boundary.","materialTrigger":"verified_checkpoint","contentDigest":"585c83ad4c10a2a082d4268721d193417885f9e835137a51d42603654d80a5cf"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m2-published","at":"2026-08-24T16:20:00.000Z","eventDigest":"c472daea268139d4eceb38852c858f76455fd4009fa8a347521d552c7289fd16","type":"checkpoint_published","leaseId":"processyard-development","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m2","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-development"],"title":"M2 · Economy Theatre implemented","changeSummary":"The demonstration flow now shows the product outcome in a browser-facing experience.","whyItMatters":"A non-technical stakeholder can understand the product without a terminal transcript.","publishedAt":"2026-08-24T16:20:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"3333333333333333333333333333333333333333","worktreeDigest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222"],"verifiedDigest":"08c09c1b6927f28aba863849a63163971c9f0c14736b8d31f430067ee40508cd"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:20:00.000Z","methods":[{"kind":"command","label":"processyard-m2 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m2","result":"Passed in the integration fixture","evidenceDigest":"65138d03ca6c74288a4943fbabd5f492c9342e821482a8a757942fc7ff86bc93"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m2-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m2/factfile.json","digest":"fef0f000904d60a217f005a9eeab2461ca452073283ced41b1e6e3c02cf1960c","sourceDigest":"08c09c1b6927f28aba863849a63163971c9f0c14736b8d31f430067ee40508cd","state":"ready_for_review"}],"assets":[],"limitations":["Later Processyard milestones remain outside this checkpoint."],"nextTask":"Verify M3 without changing the established source boundary.","materialTrigger":"verified_checkpoint","contentDigest":"2b06dfadc33bec7f2d54b416d84b5fb8d7a167f2035682f1158d858112abcceb"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m3-published","at":"2026-08-24T16:25:00.000Z","eventDigest":"ebd441eb26f7332dabdc02faf473fff52c0143ee488a7797fcf977f0480faec6","type":"checkpoint_published","leaseId":"processyard-development","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m3","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-development"],"title":"M3 · Checks bound to source","changeSummary":"Automated checks and the changed-file boundary were bound to one source identity.","whyItMatters":"Passing output can no longer drift away from the code it describes.","publishedAt":"2026-08-24T16:25:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"4444444444444444444444444444444444444444","worktreeDigest":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333"],"verifiedDigest":"337133589bea8a4f1d4c467b62390df227178b704612248789af685637982d23"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:25:00.000Z","methods":[{"kind":"command","label":"processyard-m3 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m3","result":"Passed in the integration fixture","evidenceDigest":"840d4ac0247eaab7a9b9ccdda08b9c94f183e8c0c326a904b7545925fe3d035c"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m3-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m3/factfile.json","digest":"7bc36fd3bcabdee84b8b4a3287831e89d4a25f8d69257dd07cb3e1994541d71c","sourceDigest":"337133589bea8a4f1d4c467b62390df227178b704612248789af685637982d23","state":"ready_for_review"}],"assets":[],"limitations":["Later Processyard milestones remain outside this checkpoint."],"nextTask":"Verify M4 without changing the established source boundary.","materialTrigger":"verified_checkpoint","contentDigest":"c20b7d41f4634f1d0c30f15f9acc32c37bb8ad8fc65539fdeca65c556871b1d1"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m4-published","at":"2026-08-24T16:30:00.000Z","eventDigest":"496b57150697b39e97cbc688926c357e1d4d18c0733b4ed434ae3c3d9f9f4575","type":"checkpoint_published","leaseId":"processyard-development","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m4","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-development"],"title":"M4 · Owner decision isolated","changeSummary":"The remaining launch choice was separated from machine verification.","whyItMatters":"The agent can continue independent work without manufacturing stakeholder consent.","publishedAt":"2026-08-24T16:30:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"5555555555555555555555555555555555555555","worktreeDigest":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333","4444444444444444444444444444444444444444"],"verifiedDigest":"3341f7f2f19d06bcbacdcf98bd86e5b54a9e111a3838ab08ebdf7e777813d023"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:30:00.000Z","methods":[{"kind":"command","label":"processyard-m4 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m4","result":"Passed in the integration fixture","evidenceDigest":"1217b4c5e79770835bb24ba1d90673c095bb001fe1e74e4a0a8f79b12b369c64"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m4-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m4/factfile.json","digest":"a9019a697c343aef122f6f1b431c63e8d05619238e43f4c8ce8130f89583c8f9","sourceDigest":"3341f7f2f19d06bcbacdcf98bd86e5b54a9e111a3838ab08ebdf7e777813d023","state":"human_review_required"}],"assets":[],"limitations":["Later Processyard milestones remain outside this checkpoint."],"nextTask":"Verify M5 without changing the established source boundary.","humanDecisionRequest":{"id":"processyard-launch-boundary","title":"Choose the public launch boundary","whyHuman":"This changes the promise made to customers and belongs to the owner.","requestedAction":"Choose whether the launch stays local-first or includes the hosted Engine path.","options":["Local-first Keyoku","Keyoku plus hosted Engine"]},"materialTrigger":"owner_decision","contentDigest":"cef1790fd757ed233d58e4f23e488d3a0e44e3e7eb0785260c8ff8e5d6b1ab3f"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m5-published","at":"2026-08-24T16:35:00.000Z","eventDigest":"ade66ac5a541d706411edf7c658fc5441429b99de5f1785fa2971583edfe09f9","type":"checkpoint_published","leaseId":"processyard-evidence","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m5","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-evidence"],"title":"M5 · Evidence story staged","changeSummary":"A poster and replay path were declared in the synthetic checkpoint.","whyItMatters":"The fixture shows the intended evidence shape but cannot claim the referenced bytes exist.","publishedAt":"2026-08-24T16:35:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"6666666666666666666666666666666666666666","worktreeDigest":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333","4444444444444444444444444444444444444444","5555555555555555555555555555555555555555"],"verifiedDigest":"96ce6096523207ba3f550a5ae5ba392d44d423c618a6901b4c014a9aa51264f6"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:35:00.000Z","methods":[{"kind":"command","label":"processyard-m5 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m5","result":"Passed in the integration fixture","evidenceDigest":"906f1a5d2beed676fa4010ce3450c7b5e9ba50829a4b8648296fd08600764b97"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m5-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m5/factfile.json","digest":"264d1e497735c4ae9c79e980af89c5f1bcb6cdb538969c58affeffe4407511a0","sourceDigest":"96ce6096523207ba3f550a5ae5ba392d44d423c618a6901b4c014a9aa51264f6","state":"ready_for_review"}],"assets":[{"kind":"video","path":"evidence/economy-theatre-demo.mp4","label":"Economy Theatre product demonstration","caption":"Expected Processyard poster and replay binding; no matching media bytes were found in the checked workspace, so a live integration must resolve and digest them before dispatch.","posterPath":"evidence/economy-theatre-poster.png"}],"limitations":["Later Processyard milestones remain outside this checkpoint."],"nextTask":"Verify M6 without changing the established source boundary.","materialTrigger":"verified_checkpoint","contentDigest":"90174f7d442b03c9aeaa78bba3560a07695667e5a6f61e9b818193f291d6e053"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m6-published","at":"2026-08-24T16:40:00.000Z","eventDigest":"2a68c11fb7e0f4223c7f5d18bc1841ff3d74b557d64b26540b8389cb4b217457","type":"checkpoint_published","leaseId":"processyard-ci","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m6","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-ci"],"title":"M6 · Release boundary rehearsed","changeSummary":"The complete M0–M6 story passed the fixture's schema and replay checks.","whyItMatters":"A live integration must still promote local Factfiles before any stakeholder snapshot is dispatchable.","publishedAt":"2026-08-24T16:40:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"7777777777777777777777777777777777777777","worktreeDigest":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333","4444444444444444444444444444444444444444","5555555555555555555555555555555555555555","6666666666666666666666666666666666666666"],"verifiedDigest":"0506d0991fde0f8580576468ed61bbcce4795a96da704c2ecf2dcccac7f598f3"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:40:00.000Z","methods":[{"kind":"command","label":"processyard-m6 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m6","result":"Passed in the integration fixture","evidenceDigest":"b835de76c3ef23734588fdab36234b11a582604e96f6158e4b7877505403879b"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m6-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m6/factfile.json","digest":"b573f29e6a47e3b7c80fd589b5584a5c9f16deb9fde25a30b665792d5c7c0930","sourceDigest":"0506d0991fde0f8580576468ed61bbcce4795a96da704c2ecf2dcccac7f598f3","state":"ready_for_review"}],"assets":[],"limitations":["No production deployment is established by this local fixture.","Gmail delivery authority and sent-message verification are not configured."],"nextTask":"Request explicit channel authority before preparing any founder email delivery.","materialTrigger":"verified_checkpoint","contentDigest":"9b4febd6a6374379c2e7650e7f0d45d2a8240671b4781818c01d957534e5a0f8"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-development-blocked","at":"2026-08-24T16:41:10.000Z","eventDigest":"b7e492b703322a942cec915be785464854fce617081b3770d258d002ac0a1977","type":"blocked","leaseId":"processyard-development","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"7777777777777777777777777777777777777777","worktreeDigest":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333","4444444444444444444444444444444444444444","5555555555555555555555555555555555555555","6666666666666666666666666666666666666666"],"verifiedDigest":"0506d0991fde0f8580576468ed61bbcce4795a96da704c2ecf2dcccac7f598f3"},"reason":"The long-running development lease is waiting for the explicit launch-boundary decision.","humanDecisionRequest":{"id":"processyard-launch-boundary","title":"Choose the public launch boundary","whyHuman":"This changes the promise made to customers and belongs to the owner.","requestedAction":"Choose whether the launch stays local-first or includes the hosted Engine path.","options":["Local-first Keyoku","Keyoku plus hosted Engine"]}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-evidence-completed","at":"2026-08-24T16:41:20.000Z","eventDigest":"637511958e61adeb17e61e876d9617ec01a4c38363a52706d99812f196254a56","type":"completed","leaseId":"processyard-evidence","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"6666666666666666666666666666666666666666","worktreeDigest":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333","4444444444444444444444444444444444444444","5555555555555555555555555555555555555555"],"verifiedDigest":"96ce6096523207ba3f550a5ae5ba392d44d423c618a6901b4c014a9aa51264f6"},"checkpointId":"processyard-m5"} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-ci-completed","at":"2026-08-24T16:41:30.000Z","eventDigest":"94b886f5446fd5634d84ab6e6e5c0ab7dc69c4d3e404da0532aa67404abf6cbb","type":"completed","leaseId":"processyard-ci","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"7777777777777777777777777777777777777777","worktreeDigest":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333","4444444444444444444444444444444444444444","5555555555555555555555555555555555555555","6666666666666666666666666666666666666666"],"verifiedDigest":"0506d0991fde0f8580576468ed61bbcce4795a96da704c2ecf2dcccac7f598f3"},"checkpointId":"processyard-m6"} diff --git a/fixtures/conformance/v1/events/source-conflict.jsonl b/fixtures/conformance/v1/events/source-conflict.jsonl new file mode 100644 index 0000000..da3fcb4 --- /dev/null +++ b/fixtures/conformance/v1/events/source-conflict.jsonl @@ -0,0 +1,6 @@ +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"conflict-left-started","at":"2026-08-25T16:00:00.000Z","eventDigest":"2f8099bf66550ff29a180673ec33ca381b9a4e07f9a4807c255ef70b03f1d7d3","type":"started","leaseId":"conflict-left","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"conflict-left","harness":"conformance-jsonl","project":{"id":"source-conflict-project","name":"Source Conflict Project"},"runId":"source-conflict-run","agent":{"id":"conflict-left-agent","name":"conflict-left agent"},"canonicalSourceRoot":"repo://conformance/left","task":{"id":"conflict-left-task","title":"Produce a source-bound checkpoint","outcome":"Conflicting roots fail closed."},"startedAt":"2026-08-25T16:00:00.000Z","heartbeatAt":"2026-08-25T16:00:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://conformance/left","branch":"main","headSha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","worktreeDigest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","ancestryShas":[],"verifiedDigest":"91d371dcee69c3f0e25314f1aee10466d5ff17eecbda04736c53b84756c41402"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"conflict-right-started","at":"2026-08-25T16:00:00.000Z","eventDigest":"f64768498851f81d20ad7c88d5b74d87ed5a448fdbdeac855de68cccf6a24f12","type":"started","leaseId":"conflict-right","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"conflict-right","harness":"conformance-jsonl","project":{"id":"source-conflict-project","name":"Source Conflict Project"},"runId":"source-conflict-run","agent":{"id":"conflict-right-agent","name":"conflict-right agent"},"canonicalSourceRoot":"repo://conformance/right","task":{"id":"conflict-right-task","title":"Produce a source-bound checkpoint","outcome":"Conflicting roots fail closed."},"startedAt":"2026-08-25T16:00:00.000Z","heartbeatAt":"2026-08-25T16:00:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://conformance/right","branch":"main","headSha":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","worktreeDigest":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","ancestryShas":[],"verifiedDigest":"b29e7b84e37974600489b4b78eec760e0b4367ff80354953562a7ece7ca76762"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"conflict-left-published","at":"2026-08-25T16:01:00.000Z","eventDigest":"cc4683e8064f768205beb91a31988cf0c6834cbdc09adcaa0d3197f9714a0d35","type":"checkpoint_published","leaseId":"conflict-left","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"conflict-left-checkpoint","projectId":"source-conflict-project","outcomeId":"conflict-left-task","runId":"source-conflict-run","leaseIds":["conflict-left"],"title":"conflict-left-checkpoint attested fixture","changeSummary":"A synthetic checkpoint was attested on one declared canonical root.","whyItMatters":"Incompatible roots must never be combined into one report.","publishedAt":"2026-08-25T16:01:00.000Z","source":{"canonicalRoot":"repo://conformance/left","branch":"main","headSha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","worktreeDigest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","ancestryShas":[],"verifiedDigest":"91d371dcee69c3f0e25314f1aee10466d5ff17eecbda04736c53b84756c41402"},"verification":{"status":"attested","verifiedAt":"2026-08-25T16:01:00.000Z","methods":[{"kind":"command","label":"Conformance check","reproduce":"keyoku conformance verify","result":"passed in a synthetic fixture"}]},"evidenceBinding":{"mode":"fixture","label":"Synthetic source-conflict conformance vector"},"factfiles":[{"id":"conflict-left-checkpoint-factfile","projectId":"source-conflict-project","outcomeId":"conflict-left-task","path":".keyoku/conflict-left-checkpoint.json","digest":"0d5f38ce4e70868d390d3be6f943cc42c20dc4234963515cea9a93103fe15ceb","sourceDigest":"91d371dcee69c3f0e25314f1aee10466d5ff17eecbda04736c53b84756c41402","state":"ready_for_review"}],"assets":[],"limitations":["Fixture-only evidence."],"materialTrigger":"verified_checkpoint","contentDigest":"a8b54af3128f8426b6a7db09586429edd2245e61680f01da56009f36460ce3d2"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"conflict-right-published","at":"2026-08-25T16:01:00.000Z","eventDigest":"3f5c38b1b1c0857ca06c1e2ec7b1bccbdf53aad459fca8035b8748de206445ae","type":"checkpoint_published","leaseId":"conflict-right","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"conflict-right-checkpoint","projectId":"source-conflict-project","outcomeId":"conflict-right-task","runId":"source-conflict-run","leaseIds":["conflict-right"],"title":"conflict-right-checkpoint attested fixture","changeSummary":"A synthetic checkpoint was attested on one declared canonical root.","whyItMatters":"Incompatible roots must never be combined into one report.","publishedAt":"2026-08-25T16:01:00.000Z","source":{"canonicalRoot":"repo://conformance/right","branch":"main","headSha":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","worktreeDigest":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","ancestryShas":[],"verifiedDigest":"b29e7b84e37974600489b4b78eec760e0b4367ff80354953562a7ece7ca76762"},"verification":{"status":"attested","verifiedAt":"2026-08-25T16:01:00.000Z","methods":[{"kind":"command","label":"Conformance check","reproduce":"keyoku conformance verify","result":"passed in a synthetic fixture"}]},"evidenceBinding":{"mode":"fixture","label":"Synthetic source-conflict conformance vector"},"factfiles":[{"id":"conflict-right-checkpoint-factfile","projectId":"source-conflict-project","outcomeId":"conflict-right-task","path":".keyoku/conflict-right-checkpoint.json","digest":"df0a4e830b68902f4c5444a0116ba10a86671f3c2c207788a47d5cb511102582","sourceDigest":"b29e7b84e37974600489b4b78eec760e0b4367ff80354953562a7ece7ca76762","state":"ready_for_review"}],"assets":[],"limitations":["Fixture-only evidence."],"materialTrigger":"verified_checkpoint","contentDigest":"2820e78e265788586014dd6aa30ad6b599ebfa6d0e0098dc5799ae4f0d123995"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"conflict-left-completed","at":"2026-08-25T16:02:00.000Z","eventDigest":"87206bd7108745f848ebefad91cc64268d1afd4f2fa2e38c369fd8f0926478dd","type":"completed","leaseId":"conflict-left","source":{"canonicalRoot":"repo://conformance/left","branch":"main","headSha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","worktreeDigest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","ancestryShas":[],"verifiedDigest":"91d371dcee69c3f0e25314f1aee10466d5ff17eecbda04736c53b84756c41402"},"checkpointId":"conflict-left-checkpoint"} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"conflict-right-completed","at":"2026-08-25T16:02:00.000Z","eventDigest":"40842672027099f37db5a8325165b5998468664c604fb3bda3880459fcd78977","type":"completed","leaseId":"conflict-right","source":{"canonicalRoot":"repo://conformance/right","branch":"main","headSha":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","worktreeDigest":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","ancestryShas":[],"verifiedDigest":"b29e7b84e37974600489b4b78eec760e0b4367ff80354953562a7ece7ca76762"},"checkpointId":"conflict-right-checkpoint"} diff --git a/fixtures/conformance/v1/factfiles/verified.json b/fixtures/conformance/v1/factfiles/verified.json new file mode 100644 index 0000000..98a416e --- /dev/null +++ b/fixtures/conformance/v1/factfiles/verified.json @@ -0,0 +1,124 @@ +{ + "schemaVersion": "keyoku.dev/factfile/v1alpha1", + "id": "fact_conformance_v1", + "project": { + "id": "conformance-project", + "name": "Conformance Project", + "summary": "Stable cross-language trust vectors." + }, + "outcome": { + "id": "conformance-outcome", + "revision": 1, + "title": "The conformance record is portable", + "objective": "Bind exact source, verification, and presentation bytes across implementations.", + "constraints": [ + "Treat these bytes as fixtures, never live evidence." + ], + "owner": { + "kind": "human", + "id": "owner@example.com", + "name": "Fixture Owner" + }, + "humanCriteria": [] + }, + "contribution": { + "schemaVersion": "keyoku.dev/contribution/v1alpha1", + "id": "conformance-contribution", + "title": "Portable conformance record", + "summary": "One complete deterministic Factfile.", + "knownLimits": [ + "Fixture-only evidence." + ], + "outcomeId": "conformance-outcome", + "outcomeRevision": 1, + "baseSha": "1111111111111111111111111111111111111111", + "actors": [ + { + "kind": "human", + "id": "owner@example.com", + "name": "Fixture Owner" + } + ], + "status": "ready_for_review", + "createdAt": "2026-08-25T12:00:00.000Z", + "updatedAt": "2026-08-25T12:00:00.000Z" + }, + "repository": { + "repositoryRoot": "/conformance/keyoku", + "branch": "main", + "ahead": 0, + "behind": 0, + "lastCommit": "Conformance fixture", + "baseSha": "1111111111111111111111111111111111111111", + "headSha": "2222222222222222222222222222222222222222", + "worktreeDigest": "3333333333333333333333333333333333333333333333333333333333333333", + "sourceCapsuleDigest": "3333333333333333333333333333333333333333333333333333333333333333", + "dirty": false, + "changedFiles": [] + }, + "scope": { + "declared": false, + "passed": true, + "includedPaths": [], + "unexpectedPaths": [], + "excludedPaths": [], + "topLevelAreas": [], + "note": "No repository path scope was declared for this fixture." + }, + "reviewPlan": [], + "session": { + "work": [], + "decisions": [], + "instructions": [], + "agents": [], + "directions": [], + "eventCount": 0 + }, + "state": "ready_for_review", + "generatedAt": "2026-08-25T12:00:00.000Z", + "reviews": [], + "evidence": [ + { + "id": "criterion-0", + "description": "The structured fixture observation matches.", + "pass": true, + "actual": { + "😀": 6, + "ä": 4, + "a": 2, + "あ": 5, + "Á": 3, + "Z": 1 + }, + "expected": { + "op": "eq", + "value": 2, + "path": "output.a" + }, + "durationMs": 1, + "verification": { + "kind": "command", + "label": "Fixture observation", + "reproduce": "keyoku conformance verify", + "assertion": { + "op": "eq", + "value": 2, + "path": "output.a" + } + } + } + ], + "summary": { + "passed": 1, + "failed": 0, + "total": 1, + "verified": true + }, + "humanReview": { + "passed": 0, + "failed": 0, + "pending": 0, + "total": 0 + }, + "digest": "4558e8787677ff34462d14815c823c94c5542e626688a071fa0715a56fe4b417" +} diff --git a/fixtures/conformance/v1/manifest.json b/fixtures/conformance/v1/manifest.json new file mode 100644 index 0000000..81d3fc4 --- /dev/null +++ b/fixtures/conformance/v1/manifest.json @@ -0,0 +1,288 @@ +{ + "schemaVersion": "keyoku.dev/pulse-conformance/v1alpha1", + "canonicalJson": [ + { + "id": "mixed-case-unicode", + "input": { + "😀": 6, + "ä": 4, + "a": 2, + "あ": 5, + "Á": 3, + "Z": 1 + }, + "inputJson": "{\"😀\":6,\"ä\":4,\"a\":2,\"あ\":5,\"Á\":3,\"Z\":1}", + "canonical": "{\"Z\":1,\"a\":2,\"Á\":3,\"ä\":4,\"あ\":5,\"😀\":6}", + "digest": "0f1fe24d4557ef543b80ef7d10e460dbcecdc0e4dac7955287950337bc4f0853" + }, + { + "id": "negative-zero", + "input": { + "value": 0 + }, + "inputJson": "{\"value\":-0}", + "canonical": "{\"value\":0}", + "digest": "23d7b286bd429460b92a2a1c21b6afc34110446c5034c17363fda363aa0a7c5d" + }, + { + "id": "one-million", + "input": { + "value": 1000000 + }, + "inputJson": "{\"value\":1000000}", + "canonical": "{\"value\":1000000}", + "digest": "7becf1021641e948d71d940cdc4851f26e6231592414a72a8e9f3a36d9a4aadb" + }, + { + "id": "one-millionth", + "input": { + "value": 0.000001 + }, + "inputJson": "{\"value\":0.000001}", + "canonical": "{\"value\":0.000001}", + "digest": "bc1c982dd4e677d72ff5821cc67554e045e838a31ddc55fa0fdb1a32d5a1dce9" + }, + { + "id": "one-ten-millionth", + "input": { + "value": 1e-7 + }, + "inputJson": "{\"value\":1e-7}", + "canonical": "{\"value\":1e-7}", + "digest": "910750038eb30a40d06da88009b6259aabaf3751b3cb212c616d51abb6b008a4" + }, + { + "id": "one-sextillion", + "input": { + "value": 1e+21 + }, + "inputJson": "{\"value\":1e+21}", + "canonical": "{\"value\":1e+21}", + "digest": "776bf0d292f3767fdfca3fdc9683d0ba51886b8e6e79cd228a41bafcfa9a1a6f" + }, + { + "id": "literal-line-separators", + "input": { + "value": "line
paragraph
end" + }, + "inputJson": "{\"value\":\"line
paragraph
end\"}", + "canonical": "{\"value\":\"line
paragraph
end\"}", + "digest": "92f0f4bb82a26bac6b6ff0a8c15283961f927c0664d32c22c795395b59b59c03" + }, + { + "id": "redacted-marker", + "input": { + "value": "«redacted»" + }, + "inputJson": "{\"value\":\"«redacted»\"}", + "canonical": "{\"value\":\"«redacted»\"}", + "digest": "39fa045469afc9faac3f99e320df59328cc73253352fcfbd9107c8aa1fcb2c98" + } + ], + "strictJson": [ + { + "id": "invalid-utf8", + "inputBytesBase64": "eyJ2YWx1ZSI6IsMoIn0=", + "expectedErrorIncludes": "invalid UTF-8" + }, + { + "id": "escaped-high-surrogate", + "inputBytesBase64": "eyJ2YWx1ZSI6Ilx1ZDgwMCJ9", + "expectedErrorIncludes": "surrogate forms" + }, + { + "id": "escaped-surrogate-pair", + "inputBytesBase64": "eyJ2YWx1ZSI6Ilx1ZDgzZFx1ZGUwMCJ9", + "expectedErrorIncludes": "surrogate forms" + } + ], + "bytes": { + "factfile": { + "path": "factfiles/verified.json", + "bytesDigest": "4511a0900d2b7e249645384451879601be1942e23941da42fe2a5644ed1e8520", + "byteLength": 3237, + "factfileDigest": "4558e8787677ff34462d14815c823c94c5542e626688a071fa0715a56fe4b417" + }, + "asset": { + "kind": "video", + "path": "assets/demo-bytes.bin", + "label": "Deterministic conformance asset bytes", + "caption": "Digest fixture only; these bytes are intentionally not playable media or live evidence.", + "digest": "d15e52289df855aecec2dbbe52a784c51a4b3c31959c1be72993192dcea69a8e", + "byteLength": 40, + "posterPath": "assets/poster.svg", + "posterDigest": "93c68dcdcb3949e0c0bb86377b0ea3bcb2b7a0729a1544f5e0d2d03c3a84bd4c" + }, + "poster": { + "path": "assets/poster.svg", + "posterDigest": "93c68dcdcb3949e0c0bb86377b0ea3bcb2b7a0729a1544f5e0d2d03c3a84bd4c", + "byteLength": 863 + } + }, + "eventSets": { + "generic": "events/generic.jsonl", + "generic-through-verification": "events/generic-through-verification.jsonl", + "generic-through-checkpoint": "events/generic-through-checkpoint.jsonl", + "generic-reversed": "events/generic-reversed.jsonl", + "generic-same-time-ambiguous": "events/generic-same-time-ambiguous.jsonl", + "processyard": "events/processyard.jsonl", + "source-conflict": "events/source-conflict.jsonl" + }, + "ordering": [ + { + "id": "canonical-order", + "eventSet": "generic", + "expectedEventIds": [ + "generic-started", + "generic-verifying", + "generic-checkpoint", + "generic-completed" + ], + "expectedCheckpointIds": [ + "cart-recovery-verified" + ] + }, + { + "id": "reversed-input-same-replay", + "eventSet": "generic-reversed", + "expectedEventIds": [ + "generic-started", + "generic-verifying", + "generic-checkpoint", + "generic-completed" + ], + "expectedCheckpointIds": [ + "cart-recovery-verified" + ] + }, + { + "id": "same-time-rank-ambiguity", + "eventSet": "generic-same-time-ambiguous", + "expectedErrorIncludes": "ambiguous ordering" + } + ], + "sourceConflict": { + "eventSet": "source-conflict", + "dispatchVector": "suppress-source-conflict" + }, + "dispatch": [ + { + "id": "suppress-attested-checkpoint", + "eventSet": "generic", + "plan": { + "now": "2026-08-24T16:05:00.000Z", + "staleAfterMs": 300000, + "debounceMs": 0, + "deliveredContentDigests": [] + }, + "expected": { + "outcome": "suppress", + "reasonCode": "attested_checkpoint", + "failClosed": true, + "checkpointIds": [ + "cart-recovery-verified" + ] + } + }, + { + "id": "defer-uncheckpointed-work", + "eventSet": "generic-through-verification", + "plan": { + "now": "2026-08-24T16:02:10.000Z", + "staleAfterMs": 300000, + "debounceMs": 0, + "deliveredContentDigests": [] + }, + "expected": { + "outcome": "defer", + "reasonCode": "fresh_uncheckpointed_work", + "failClosed": false, + "checkpointIds": [] + } + }, + { + "id": "defer-fresh-agent-with-candidate", + "eventSet": "generic-through-checkpoint", + "plan": { + "now": "2026-08-24T16:03:10.000Z", + "staleAfterMs": 300000, + "debounceMs": 0, + "deliveredContentDigests": [] + }, + "expected": { + "outcome": "suppress", + "reasonCode": "attested_checkpoint", + "failClosed": true, + "checkpointIds": [ + "cart-recovery-verified" + ] + } + }, + { + "id": "coalesce-compatible-checkpoints", + "eventSet": "processyard", + "plan": { + "now": "2026-08-24T16:42:00.000Z", + "staleAfterMs": 3600000, + "debounceMs": 0, + "deliveredContentDigests": [ + "0109d493f8993301b26be76be16360cdc7c8e0dad17a9f0d217ce30528b1d8fa", + "4c0f9e4e2b11d590dffb1a77453f16da0e7f899169f6bddc962772dd65bf25f1", + "2b1a303bdee856b4cc22792b95542e3c422a0ed4e76f6a18cec76bcf7546d80f", + "9e04a6fc2293ae7c714f7aab88933ce3eadd75e04466f5e39257a21c67465478", + "d7a37f11701a8ff47bb028b6ce6e43e2fea916e75b256c981b15ff228ce05ba4" + ] + }, + "expected": { + "outcome": "suppress", + "reasonCode": "attested_checkpoint", + "failClosed": true, + "checkpointIds": [ + "processyard-m5", + "processyard-m6" + ] + } + }, + { + "id": "stale-no-send", + "eventSet": "processyard", + "plan": { + "now": "2026-08-24T16:59:00.000Z", + "staleAfterMs": 300000, + "debounceMs": 0, + "deliveredContentDigests": [ + "0109d493f8993301b26be76be16360cdc7c8e0dad17a9f0d217ce30528b1d8fa", + "4c0f9e4e2b11d590dffb1a77453f16da0e7f899169f6bddc962772dd65bf25f1", + "2b1a303bdee856b4cc22792b95542e3c422a0ed4e76f6a18cec76bcf7546d80f", + "9e04a6fc2293ae7c714f7aab88933ce3eadd75e04466f5e39257a21c67465478", + "d7a37f11701a8ff47bb028b6ce6e43e2fea916e75b256c981b15ff228ce05ba4" + ] + }, + "expected": { + "outcome": "stale_no_send", + "reasonCode": "stale_activity_lease", + "failClosed": true, + "checkpointIds": [] + } + }, + { + "id": "suppress-source-conflict", + "eventSet": "source-conflict", + "plan": { + "now": "2026-08-25T16:03:00.000Z", + "staleAfterMs": 300000, + "debounceMs": 0, + "deliveredContentDigests": [] + }, + "expected": { + "outcome": "suppress", + "reasonCode": "source_conflict", + "failClosed": true, + "checkpointIds": [ + "conflict-left-checkpoint", + "conflict-right-checkpoint" + ] + } + } + ] +} diff --git a/fixtures/pulse/README.md b/fixtures/pulse/README.md new file mode 100644 index 0000000..70ea556 --- /dev/null +++ b/fixtures/pulse/README.md @@ -0,0 +1,19 @@ +# Pulse integration fixtures + +- `generic.jsonl` is the harness-neutral JSONL/stdin contract. +- `processyard-m0-m6.jsonl` is the Processyard M0–M6 multi-harness story. +- `processyard-coalesced.json` retains its historical filename but now records the fail-closed `attested_checkpoint` suppression decision. +- `processyard-stale-no-send.json` is the later fail-closed decision after that lease becomes stale. +- `processyard-timeline.html` explains why no audience projection exists for synthetic attested checkpoints. + +These are generated fixtures, not live customer evidence. They are visibly +`attested`, never `verified`, and never dispatchable. The Economy Theatre media +paths intentionally remain unresolved because the corresponding bytes are not +present in this repository. + +Regenerate after building: + +```bash +npm run build +npm run fixtures:pulse +``` diff --git a/fixtures/pulse/generic.jsonl b/fixtures/pulse/generic.jsonl new file mode 100644 index 0000000..18d81cd --- /dev/null +++ b/fixtures/pulse/generic.jsonl @@ -0,0 +1,4 @@ +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-started","at":"2026-08-24T16:00:00.000Z","eventDigest":"8f4cf64f696e0a95e1d9d9865a609a66531e8fceff0bc1e43d309e4bbb9c4d5a","type":"started","leaseId":"generic-run-agent-1","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"generic-run-agent-1","harness":"generic-jsonl","project":{"id":"checkout-example","name":"Checkout Example"},"runId":"checkout-example-run-20260824","agent":{"id":"agent-1","name":"Fixture agent"},"canonicalSourceRoot":"repo://example/checkout","task":{"id":"restore-cart","title":"Restore the saved cart","outcome":"Returning shoppers recover the same products and quantities."},"startedAt":"2026-08-24T16:00:00.000Z","heartbeatAt":"2026-08-24T16:00:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"b6359aff2fbdabcc47410462203c78e065e2378b03a102fe7112dfd431e792bd"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-verifying","at":"2026-08-24T16:02:00.000Z","eventDigest":"7290a26fed55502deeb10462743d5ae2d27ef8fab21ff359606f1f1572f51367","type":"verification_started","leaseId":"generic-run-agent-1","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-checkpoint","at":"2026-08-24T16:03:00.000Z","eventDigest":"05060a1add9a0b247f44267fd9f6ac3104a7fdbdb2b6d5c348910f0ab6f53512","type":"checkpoint_published","leaseId":"generic-run-agent-1","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"cart-recovery-verified","projectId":"checkout-example","outcomeId":"restore-cart","runId":"checkout-example-run-20260824","leaseIds":["generic-run-agent-1"],"title":"Saved-cart fixture checkpoint","changeSummary":"The cart recovery path now restores product ids and quantities from the saved session.","whyItMatters":"Returning shoppers can continue checkout without rebuilding their cart.","publishedAt":"2026-08-24T16:03:00.000Z","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:03:00.000Z","methods":[{"kind":"command","label":"cart-recovery-verified acceptance check","reproduce":"./scripts/verify-checkpoint cart-recovery-verified","result":"Passed in the integration fixture","evidenceDigest":"95e173bc297d6ab5ff167c3dad6ad4cb59380871cd118a4efb0d9647453693d1"}]},"evidenceBinding":{"mode":"fixture","label":"checkout-example demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"cart-recovery-verified-factfile","projectId":"checkout-example","outcomeId":"restore-cart","path":".keyoku/contributions/cart-recovery-verified/factfile.json","digest":"34b9827ff17251b1d578faabd89898c34c6e84458fdbd90d5a1c85e45d9fb3ac","sourceDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c","state":"ready_for_review"}],"assets":[],"limitations":["Visual review of the empty-cart state is still pending."],"nextTask":"Capture the empty-cart visual Factfile.","materialTrigger":"verified_checkpoint","contentDigest":"64bbc59fb709ca09cc7a78b23b45611a3f2bc2cc351a69f5b393fe430f32b734"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"generic-completed","at":"2026-08-24T16:04:00.000Z","eventDigest":"f079776e16bb6e69e33580afb34afb77320b18b9a07aa59c17621eaf9c36220a","type":"completed","leaseId":"generic-run-agent-1","source":{"canonicalRoot":"repo://example/checkout","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"b3f18dcdce5146ed0253c6c773c40971c70aea21b02459d2d6e67bdb92470f3c"},"checkpointId":"cart-recovery-verified"} diff --git a/fixtures/pulse/processyard-coalesced.json b/fixtures/pulse/processyard-coalesced.json new file mode 100644 index 0000000..e8c0295 --- /dev/null +++ b/fixtures/pulse/processyard-coalesced.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": "keyoku.dev/pulse-dispatch/v1alpha1", + "plannedAt": "2026-08-24T16:42:00.000Z", + "outcome": "suppress", + "reasonCode": "attested_checkpoint", + "reason": "2 checkpoints are fixture- or adapter-attested, not locally verified. No dispatchable snapshot was produced.", + "failClosed": true, + "checkpointIds": [ + "processyard-m5", + "processyard-m6" + ] +} diff --git a/fixtures/pulse/processyard-m0-m6.jsonl b/fixtures/pulse/processyard-m0-m6.jsonl new file mode 100644 index 0000000..3477aa0 --- /dev/null +++ b/fixtures/pulse/processyard-m0-m6.jsonl @@ -0,0 +1,13 @@ +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-development-started","at":"2026-08-24T16:00:00.000Z","eventDigest":"879425a35947b4f12a53c501c5abf3cd99dbb8fbcc594cbe784fe14870989395","type":"started","leaseId":"processyard-development","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"processyard-development","harness":"codex","project":{"id":"processyard","name":"Processyard"},"runId":"processyard-run-20260824","agent":{"id":"development-agent","name":"Development agent"},"canonicalSourceRoot":"repo://processyard/main","task":{"id":"economy-theatre","title":"Build the Economy Theatre story","outcome":"A founder can see verified product progress without reading agent transcripts."},"startedAt":"2026-08-24T16:00:00.000Z","heartbeatAt":"2026-08-24T16:00:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"1ceb1ca10e27873d4c61eddd81058dd41ba3616ea8e5a1891d9aea76fbb9c95a"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-evidence-started","at":"2026-08-24T16:01:00.000Z","eventDigest":"44e1c837a78f260f60b6e043c9a23bcbfd7b8fc0a17d54b9ae62b800cde75f06","type":"started","leaseId":"processyard-evidence","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"processyard-evidence","harness":"claude-code","project":{"id":"processyard","name":"Processyard"},"runId":"processyard-run-20260824","agent":{"id":"evidence-agent","name":"Evidence agent"},"canonicalSourceRoot":"repo://processyard/main","task":{"id":"economy-theatre-evidence","title":"Capture human-readable product evidence","outcome":"The milestone story includes replayable UI evidence and explicit limitations."},"startedAt":"2026-08-24T16:01:00.000Z","heartbeatAt":"2026-08-24T16:01:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"1ceb1ca10e27873d4c61eddd81058dd41ba3616ea8e5a1891d9aea76fbb9c95a"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-ci-started","at":"2026-08-24T16:02:00.000Z","eventDigest":"4b5db6b77462a3eee14495eb347190a633ba57544f0c7497f09f9ff1f8907084","type":"started","leaseId":"processyard-ci","lease":{"schemaVersion":"keyoku.dev/pulse-lease/v1alpha1","id":"processyard-ci","harness":"github-actions","project":{"id":"processyard","name":"Processyard"},"runId":"processyard-run-20260824","agent":{"id":"verification-workflow","name":"Verification workflow"},"canonicalSourceRoot":"repo://processyard/main","task":{"id":"economy-theatre-verification","title":"Verify the checkpoint boundary","outcome":"Every reported milestone is bound to a reproducible Factfile and exact source digest."},"startedAt":"2026-08-24T16:02:00.000Z","heartbeatAt":"2026-08-24T16:02:00.000Z","state":"working","currentSource":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"1ceb1ca10e27873d4c61eddd81058dd41ba3616ea8e5a1891d9aea76fbb9c95a"}}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m0-published","at":"2026-08-24T16:10:00.000Z","eventDigest":"00ebb5675a083f4643522730dd21dd160de4709f4356708ec633d6c6e6d98653","type":"checkpoint_published","leaseId":"processyard-development","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m0","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-development"],"title":"M0 · Outcome pinned","changeSummary":"The founder outcome and source boundary were recorded before implementation.","whyItMatters":"The milestone story starts from an explicit definition of done.","publishedAt":"2026-08-24T16:10:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"1111111111111111111111111111111111111111","worktreeDigest":"8888888888888888888888888888888888888888888888888888888888888888","ancestryShas":[],"verifiedDigest":"1ceb1ca10e27873d4c61eddd81058dd41ba3616ea8e5a1891d9aea76fbb9c95a"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:10:00.000Z","methods":[{"kind":"command","label":"processyard-m0 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m0","result":"Passed in the integration fixture","evidenceDigest":"fe5b19d3508b254ac100801b7f9ef21e93359da67f8ac78a42f1d819f767c47e"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m0-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m0/factfile.json","digest":"687f833e5578025cdba94cbefd6cc2f61c66545508c1f1cdf3e36294e0e3253c","sourceDigest":"1ceb1ca10e27873d4c61eddd81058dd41ba3616ea8e5a1891d9aea76fbb9c95a","state":"ready_for_review"}],"assets":[],"limitations":["Later Processyard milestones remain outside this checkpoint."],"nextTask":"Verify M1 without changing the established source boundary.","materialTrigger":"verified_checkpoint","contentDigest":"4cf871a73c22ddc216fdf13333a978296eacdd40abd0e7ee47ea1a813b38105e"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m1-published","at":"2026-08-24T16:15:00.000Z","eventDigest":"829e100c648f57406414915517f48b2133ac5158efdf707542cc1d4fdff28a65","type":"checkpoint_published","leaseId":"processyard-development","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m1","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-development"],"title":"M1 · Storefront path observed","changeSummary":"The current customer journey and evidence gaps were captured.","whyItMatters":"Work begins from the real product path instead of an imagined interface.","publishedAt":"2026-08-24T16:15:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"2222222222222222222222222222222222222222","worktreeDigest":"9999999999999999999999999999999999999999999999999999999999999999","ancestryShas":["1111111111111111111111111111111111111111"],"verifiedDigest":"0760994c45cc1f3c6a6c0c616f3dbb5ef1bf7f6154b7bbcdb0b9556cc29e74cb"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:15:00.000Z","methods":[{"kind":"command","label":"processyard-m1 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m1","result":"Passed in the integration fixture","evidenceDigest":"107bac10ebc481c8ba14b7ecc16aac336c203875fb90a2248087377217d10ca3"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m1-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m1/factfile.json","digest":"ed1472e4ff2362c8dd6cc5d53ea7955995d8ac0b80982326c8e6ed74e4e658d8","sourceDigest":"0760994c45cc1f3c6a6c0c616f3dbb5ef1bf7f6154b7bbcdb0b9556cc29e74cb","state":"ready_for_review"}],"assets":[],"limitations":["Later Processyard milestones remain outside this checkpoint."],"nextTask":"Verify M2 without changing the established source boundary.","materialTrigger":"verified_checkpoint","contentDigest":"585c83ad4c10a2a082d4268721d193417885f9e835137a51d42603654d80a5cf"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m2-published","at":"2026-08-24T16:20:00.000Z","eventDigest":"c472daea268139d4eceb38852c858f76455fd4009fa8a347521d552c7289fd16","type":"checkpoint_published","leaseId":"processyard-development","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m2","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-development"],"title":"M2 · Economy Theatre implemented","changeSummary":"The demonstration flow now shows the product outcome in a browser-facing experience.","whyItMatters":"A non-technical stakeholder can understand the product without a terminal transcript.","publishedAt":"2026-08-24T16:20:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"3333333333333333333333333333333333333333","worktreeDigest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222"],"verifiedDigest":"08c09c1b6927f28aba863849a63163971c9f0c14736b8d31f430067ee40508cd"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:20:00.000Z","methods":[{"kind":"command","label":"processyard-m2 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m2","result":"Passed in the integration fixture","evidenceDigest":"65138d03ca6c74288a4943fbabd5f492c9342e821482a8a757942fc7ff86bc93"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m2-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m2/factfile.json","digest":"fef0f000904d60a217f005a9eeab2461ca452073283ced41b1e6e3c02cf1960c","sourceDigest":"08c09c1b6927f28aba863849a63163971c9f0c14736b8d31f430067ee40508cd","state":"ready_for_review"}],"assets":[],"limitations":["Later Processyard milestones remain outside this checkpoint."],"nextTask":"Verify M3 without changing the established source boundary.","materialTrigger":"verified_checkpoint","contentDigest":"2b06dfadc33bec7f2d54b416d84b5fb8d7a167f2035682f1158d858112abcceb"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m3-published","at":"2026-08-24T16:25:00.000Z","eventDigest":"ebd441eb26f7332dabdc02faf473fff52c0143ee488a7797fcf977f0480faec6","type":"checkpoint_published","leaseId":"processyard-development","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m3","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-development"],"title":"M3 · Checks bound to source","changeSummary":"Automated checks and the changed-file boundary were bound to one source identity.","whyItMatters":"Passing output can no longer drift away from the code it describes.","publishedAt":"2026-08-24T16:25:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"4444444444444444444444444444444444444444","worktreeDigest":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333"],"verifiedDigest":"337133589bea8a4f1d4c467b62390df227178b704612248789af685637982d23"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:25:00.000Z","methods":[{"kind":"command","label":"processyard-m3 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m3","result":"Passed in the integration fixture","evidenceDigest":"840d4ac0247eaab7a9b9ccdda08b9c94f183e8c0c326a904b7545925fe3d035c"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m3-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m3/factfile.json","digest":"7bc36fd3bcabdee84b8b4a3287831e89d4a25f8d69257dd07cb3e1994541d71c","sourceDigest":"337133589bea8a4f1d4c467b62390df227178b704612248789af685637982d23","state":"ready_for_review"}],"assets":[],"limitations":["Later Processyard milestones remain outside this checkpoint."],"nextTask":"Verify M4 without changing the established source boundary.","materialTrigger":"verified_checkpoint","contentDigest":"c20b7d41f4634f1d0c30f15f9acc32c37bb8ad8fc65539fdeca65c556871b1d1"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m4-published","at":"2026-08-24T16:30:00.000Z","eventDigest":"496b57150697b39e97cbc688926c357e1d4d18c0733b4ed434ae3c3d9f9f4575","type":"checkpoint_published","leaseId":"processyard-development","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m4","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-development"],"title":"M4 · Owner decision isolated","changeSummary":"The remaining launch choice was separated from machine verification.","whyItMatters":"The agent can continue independent work without manufacturing stakeholder consent.","publishedAt":"2026-08-24T16:30:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"5555555555555555555555555555555555555555","worktreeDigest":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333","4444444444444444444444444444444444444444"],"verifiedDigest":"3341f7f2f19d06bcbacdcf98bd86e5b54a9e111a3838ab08ebdf7e777813d023"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:30:00.000Z","methods":[{"kind":"command","label":"processyard-m4 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m4","result":"Passed in the integration fixture","evidenceDigest":"1217b4c5e79770835bb24ba1d90673c095bb001fe1e74e4a0a8f79b12b369c64"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m4-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m4/factfile.json","digest":"a9019a697c343aef122f6f1b431c63e8d05619238e43f4c8ce8130f89583c8f9","sourceDigest":"3341f7f2f19d06bcbacdcf98bd86e5b54a9e111a3838ab08ebdf7e777813d023","state":"human_review_required"}],"assets":[],"limitations":["Later Processyard milestones remain outside this checkpoint."],"nextTask":"Verify M5 without changing the established source boundary.","humanDecisionRequest":{"id":"processyard-launch-boundary","title":"Choose the public launch boundary","whyHuman":"This changes the promise made to customers and belongs to the owner.","requestedAction":"Choose whether the launch stays local-first or includes the hosted Engine path.","options":["Local-first Keyoku","Keyoku plus hosted Engine"]},"materialTrigger":"owner_decision","contentDigest":"cef1790fd757ed233d58e4f23e488d3a0e44e3e7eb0785260c8ff8e5d6b1ab3f"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m5-published","at":"2026-08-24T16:35:00.000Z","eventDigest":"ade66ac5a541d706411edf7c658fc5441429b99de5f1785fa2971583edfe09f9","type":"checkpoint_published","leaseId":"processyard-evidence","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m5","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-evidence"],"title":"M5 · Evidence story staged","changeSummary":"A poster and replay path were declared in the synthetic checkpoint.","whyItMatters":"The fixture shows the intended evidence shape but cannot claim the referenced bytes exist.","publishedAt":"2026-08-24T16:35:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"6666666666666666666666666666666666666666","worktreeDigest":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333","4444444444444444444444444444444444444444","5555555555555555555555555555555555555555"],"verifiedDigest":"96ce6096523207ba3f550a5ae5ba392d44d423c618a6901b4c014a9aa51264f6"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:35:00.000Z","methods":[{"kind":"command","label":"processyard-m5 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m5","result":"Passed in the integration fixture","evidenceDigest":"906f1a5d2beed676fa4010ce3450c7b5e9ba50829a4b8648296fd08600764b97"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m5-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m5/factfile.json","digest":"264d1e497735c4ae9c79e980af89c5f1bcb6cdb538969c58affeffe4407511a0","sourceDigest":"96ce6096523207ba3f550a5ae5ba392d44d423c618a6901b4c014a9aa51264f6","state":"ready_for_review"}],"assets":[{"kind":"video","path":"evidence/economy-theatre-demo.mp4","label":"Economy Theatre product demonstration","caption":"Expected Processyard poster and replay binding; no matching media bytes were found in the checked workspace, so a live integration must resolve and digest them before dispatch.","posterPath":"evidence/economy-theatre-poster.png"}],"limitations":["Later Processyard milestones remain outside this checkpoint."],"nextTask":"Verify M6 without changing the established source boundary.","materialTrigger":"verified_checkpoint","contentDigest":"90174f7d442b03c9aeaa78bba3560a07695667e5a6f61e9b818193f291d6e053"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-m6-published","at":"2026-08-24T16:40:00.000Z","eventDigest":"2a68c11fb7e0f4223c7f5d18bc1841ff3d74b557d64b26540b8389cb4b217457","type":"checkpoint_published","leaseId":"processyard-ci","checkpoint":{"schemaVersion":"keyoku.dev/pulse-checkpoint/v1alpha1","id":"processyard-m6","projectId":"processyard","outcomeId":"processyard-modernization","runId":"processyard-run-20260824","leaseIds":["processyard-ci"],"title":"M6 · Release boundary rehearsed","changeSummary":"The complete M0–M6 story passed the fixture's schema and replay checks.","whyItMatters":"A live integration must still promote local Factfiles before any stakeholder snapshot is dispatchable.","publishedAt":"2026-08-24T16:40:00.000Z","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"7777777777777777777777777777777777777777","worktreeDigest":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333","4444444444444444444444444444444444444444","5555555555555555555555555555555555555555","6666666666666666666666666666666666666666"],"verifiedDigest":"0506d0991fde0f8580576468ed61bbcce4795a96da704c2ecf2dcccac7f598f3"},"verification":{"status":"attested","verifiedAt":"2026-08-24T16:40:00.000Z","methods":[{"kind":"command","label":"processyard-m6 acceptance check","reproduce":"./scripts/verify-checkpoint processyard-m6","result":"Passed in the integration fixture","evidenceDigest":"b835de76c3ef23734588fdab36234b11a582604e96f6158e4b7877505403879b"}]},"evidenceBinding":{"mode":"fixture","label":"processyard demonstration fixture; adapters must bind real Factfile bytes before delivery"},"factfiles":[{"id":"processyard-m6-factfile","projectId":"processyard","outcomeId":"processyard-modernization","path":".keyoku/contributions/processyard-m6/factfile.json","digest":"b573f29e6a47e3b7c80fd589b5584a5c9f16deb9fde25a30b665792d5c7c0930","sourceDigest":"0506d0991fde0f8580576468ed61bbcce4795a96da704c2ecf2dcccac7f598f3","state":"ready_for_review"}],"assets":[],"limitations":["No production deployment is established by this local fixture.","Gmail delivery authority and sent-message verification are not configured."],"nextTask":"Request explicit channel authority before preparing any founder email delivery.","materialTrigger":"verified_checkpoint","contentDigest":"9b4febd6a6374379c2e7650e7f0d45d2a8240671b4781818c01d957534e5a0f8"}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-development-blocked","at":"2026-08-24T16:41:10.000Z","eventDigest":"b7e492b703322a942cec915be785464854fce617081b3770d258d002ac0a1977","type":"blocked","leaseId":"processyard-development","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"7777777777777777777777777777777777777777","worktreeDigest":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333","4444444444444444444444444444444444444444","5555555555555555555555555555555555555555","6666666666666666666666666666666666666666"],"verifiedDigest":"0506d0991fde0f8580576468ed61bbcce4795a96da704c2ecf2dcccac7f598f3"},"reason":"The long-running development lease is waiting for the explicit launch-boundary decision.","humanDecisionRequest":{"id":"processyard-launch-boundary","title":"Choose the public launch boundary","whyHuman":"This changes the promise made to customers and belongs to the owner.","requestedAction":"Choose whether the launch stays local-first or includes the hosted Engine path.","options":["Local-first Keyoku","Keyoku plus hosted Engine"]}} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-evidence-completed","at":"2026-08-24T16:41:20.000Z","eventDigest":"637511958e61adeb17e61e876d9617ec01a4c38363a52706d99812f196254a56","type":"completed","leaseId":"processyard-evidence","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"6666666666666666666666666666666666666666","worktreeDigest":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333","4444444444444444444444444444444444444444","5555555555555555555555555555555555555555"],"verifiedDigest":"96ce6096523207ba3f550a5ae5ba392d44d423c618a6901b4c014a9aa51264f6"},"checkpointId":"processyard-m5"} +{"schemaVersion":"keyoku.dev/pulse-event/v1alpha1","id":"processyard-ci-completed","at":"2026-08-24T16:41:30.000Z","eventDigest":"94b886f5446fd5634d84ab6e6e5c0ab7dc69c4d3e404da0532aa67404abf6cbb","type":"completed","leaseId":"processyard-ci","source":{"canonicalRoot":"repo://processyard/main","branch":"main","baseSha":"1111111111111111111111111111111111111111","headSha":"7777777777777777777777777777777777777777","worktreeDigest":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","ancestryShas":["1111111111111111111111111111111111111111","2222222222222222222222222222222222222222","3333333333333333333333333333333333333333","4444444444444444444444444444444444444444","5555555555555555555555555555555555555555","6666666666666666666666666666666666666666"],"verifiedDigest":"0506d0991fde0f8580576468ed61bbcce4795a96da704c2ecf2dcccac7f598f3"},"checkpointId":"processyard-m6"} diff --git a/fixtures/pulse/processyard-stale-no-send.json b/fixtures/pulse/processyard-stale-no-send.json new file mode 100644 index 0000000..474f3f8 --- /dev/null +++ b/fixtures/pulse/processyard-stale-no-send.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": "keyoku.dev/pulse-dispatch/v1alpha1", + "plannedAt": "2026-08-24T16:59:00.000Z", + "outcome": "stale_no_send", + "reasonCode": "stale_activity_lease", + "reason": "No normal update: 1 active lease is stale; the last trusted checkpoint is frozen.", + "failClosed": true, + "checkpointIds": [] +} diff --git a/fixtures/pulse/processyard-timeline.html b/fixtures/pulse/processyard-timeline.html new file mode 100644 index 0000000..fe75862 --- /dev/null +++ b/fixtures/pulse/processyard-timeline.html @@ -0,0 +1 @@ +Attested fixture — no projection

This synthetic fixture is attested, not locally verified. Keyoku correctly produced no dispatchable timeline.

diff --git a/package-lock.json b/package-lock.json index 3777184..b7a4523 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,14 @@ { "name": "keyoku", - "version": "2.17.0", + "version": "3.0.0-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "keyoku", - "version": "2.17.0", + "version": "3.0.0-alpha.1", "license": "MIT", "dependencies": { - "@anthropic-ai/sdk": "^0.104.1", "@modelcontextprotocol/sdk": "^1.12.0", "jmespath": "^0.16.0", "yaml": "^2.9.0", @@ -19,21 +18,26 @@ "keyoku": "dist/index.js" }, "devDependencies": { + "@anthropic-ai/sdk": "^0.104.1", "@types/jmespath": "^0.15.2", "@types/node": "^20.10.0", "tsup": "^8.0.1", "tsx": "^4.19.0", "typescript": "^5.3.0", - "vitest": "^1.0.0" + "vitest": "^4.1.10" }, "engines": { "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" } }, "node_modules/@anthropic-ai/sdk": { "version": "0.104.1", "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.104.1.tgz", "integrity": "sha512-gGACa/+IaiXzRRmF96aOhamoBgapKRBiFWbmmTFP8aMkpaEcuStF+Q61bjo4vPxBM7gqWJNZqsngslRdnLHv0Q==", + "dev": true, "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" @@ -54,18 +58,20 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -75,13 +81,14 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -91,13 +98,14 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -107,13 +115,14 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -123,13 +132,14 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -139,13 +149,14 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -155,13 +166,14 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -171,13 +183,14 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -187,13 +200,14 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -203,13 +217,14 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -219,13 +234,14 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -235,13 +251,14 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -251,13 +268,14 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -267,13 +285,14 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -283,13 +302,14 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -299,13 +319,14 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -315,13 +336,14 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -331,13 +353,14 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -347,13 +370,14 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -363,13 +387,14 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -379,13 +404,14 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -395,13 +421,14 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openharmony" @@ -411,13 +438,14 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -427,13 +455,14 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -443,13 +472,14 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -459,13 +489,14 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -475,28 +506,17 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.0.tgz", + "integrity": "sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==", + "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=20" }, "peerDependencies": { "hono": "^4" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -533,11 +553,12 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -571,6 +592,261 @@ } } }, + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.61.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", @@ -896,16 +1172,36 @@ "win32" ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "dev": true - }, "node_modules/@stablelib/base64": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", - "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "dev": true + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.9", @@ -929,81 +1225,113 @@ } }, "node_modules/@vitest/expect": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", - "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "chai": "^4.3.10" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", - "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/utils": "1.6.1", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@vitest/runner/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true - }, - "node_modules/@vitest/snapshot": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", - "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, + "license": "MIT", "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true - }, - "node_modules/@vitest/spy": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", - "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, + "license": "MIT", "dependencies": { - "tinyspy": "^2.2.0" + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", - "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, + "license": "MIT", "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1033,18 +1361,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -1076,18 +1392,6 @@ } } }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1095,29 +1399,44 @@ "dev": true }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, + "license": "MIT", "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -1186,33 +1505,13 @@ } }, "node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, + "license": "MIT", "engines": { - "node": "*" + "node": ">=18" } }, "node_modules/chokidar": { @@ -1274,6 +1573,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -1335,18 +1641,6 @@ } } }, - "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1355,13 +1649,14 @@ "node": ">= 0.8" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8" } }, "node_modules/dunder-proto": { @@ -1406,6 +1701,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -1418,11 +1720,12 @@ } }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -1430,32 +1733,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escape-html": { @@ -1468,6 +1771,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -1499,27 +1803,14 @@ "node": ">=18.0.0" } }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "node": ">=12.0.0" } }, "node_modules/express": { @@ -1589,12 +1880,13 @@ "node_modules/fast-sha256": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "dev": true }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "funding": [ { "type": "github", @@ -1604,7 +1896,8 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/fdir": { "version": "6.5.0", @@ -1674,7 +1967,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -1692,15 +1984,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1736,18 +2019,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1782,9 +2053,10 @@ } }, "node_modules/hono": { - "version": "4.12.25", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", - "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz", + "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", + "license": "MIT", "engines": { "node": ">=16.9.0" } @@ -1808,15 +2080,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -1838,9 +2101,10 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", + "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", + "license": "MIT", "engines": { "node": ">= 12" } @@ -1858,18 +2122,6 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1900,16 +2152,11 @@ "node": ">=10" } }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true - }, "node_modules/json-schema-to-ts": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" @@ -1928,139 +2175,357 @@ "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==" }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, "engines": { - "node": ">=14" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=14" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dependencies": { - "mime-db": "^1.54.0" - }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://opencollective.com/parcel" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "dev": true, "dependencies": { "acorn": "^8.16.0", @@ -2086,9 +2551,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -2096,6 +2561,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -2111,33 +2577,6 @@ "node": ">= 0.6" } }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2157,6 +2596,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2176,36 +2629,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2237,15 +2660,6 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2253,10 +2667,11 @@ "dev": true }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -2293,9 +2708,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -2311,8 +2726,9 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2362,20 +2778,6 @@ } } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2424,12 +2826,6 @@ "node": ">= 0.10" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -2460,6 +2856,39 @@ "node": ">=8" } }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" + } + }, "node_modules/rollup": { "version": "4.61.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", @@ -2665,18 +3094,6 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -2705,6 +3122,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "dev": true, "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" @@ -2719,34 +3137,11 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", - "dev": true, - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } + "license": "MIT" }, "node_modules/sucrase": { "version": "3.35.1", @@ -2819,20 +3214,12 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -2857,7 +3244,8 @@ "node_modules/ts-algebra": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==" + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true }, "node_modules/ts-interface-checker": { "version": "0.1.13", @@ -2935,1106 +3323,254 @@ "fsevents": "~2.3.3" } }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, "engines": { - "node": ">=18" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", - "cpu": [ - "arm64" - ], + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "optional": true, - "os": [ - "android" - ], + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=18" + "node": ">=14.17" } }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "engines": { - "node": ">=18" + "node": ">= 0.8" } }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "engines": { - "node": ">=18" + "node": ">= 0.8" } }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", - "cpu": [ - "arm64" - ], + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", - "cpu": [ - "x64" - ], + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, "engines": { - "node": ">=18" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } } }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", - "cpu": [ - "arm" - ], + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { "node": ">=18" } }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" - } - }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", - "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", - "dev": true - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", - "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", - "dev": true, - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vitest": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", - "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", - "dev": true, - "dependencies": { - "@vitest/expect": "1.6.1", - "@vitest/runner": "1.6.1", - "@vitest/snapshot": "1.6.1", - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.1", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.1", - "@vitest/ui": "1.6.1", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4084,18 +3620,6 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index a371015..a4f0d63 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,38 @@ { "name": "keyoku", - "version": "2.18.0", - "description": "The harness with muscle memory — watches what you do in Claude Code, Cursor, or Codex, learns your patterns, and turns them into reusable MCP-native workflows.", + "version": "3.0.0-alpha.1", + "description": "Free Git-native proof and human-attention layer for software contributions made by people and coding agents.", "type": "module", "bin": { "keyoku": "dist/index.js" }, "main": "dist/index.js", "exports": { - ".": "./dist/index.js" + ".": "./dist/index.js", + "./fixtures/assurance/*": "./fixtures/assurance/*", + "./fixtures/conformance/*": "./fixtures/conformance/*" }, "files": [ - "dist/*.js", + "dist/index.js", + "dist/index.js.map", + "assets/banner-dark.svg", + "assets/banner-light.svg", + "docs/ASSURANCE-ADAPTER.md", + "docs/FACTFILE-STANDARD.md", + "docs/GITHUB.md", + "docs/PRODUCTION-READINESS.md", + "docs/PUBLIC-SURFACE.md", + "docs/PULSE.md", + "docs/REPO-MAP.md", + "docs/SECURITY-REVIEW.md", + "fixtures/assurance", + "fixtures/conformance", + "fixtures/pulse", + "scripts/render-assurance-fixtures.mjs", + "scripts/render-conformance-fixtures.mjs", + "scripts/render-pulse-fixtures.mjs", "README.md", + "SECURITY.md", "LICENSE" ], "scripts": { @@ -21,7 +41,11 @@ "test": "npm run build && vitest run", "test:watch": "vitest", "eval": "tsx evals/muscle-memory.eval.ts", + "fixtures:pulse": "node scripts/render-pulse-fixtures.mjs", + "fixtures:conformance": "npm run build && node scripts/render-conformance-fixtures.mjs", + "fixtures:assurance": "npm run build && node scripts/render-assurance-fixtures.mjs", "typecheck": "tsc --noEmit", + "prepare": "npm run build", "preflight": "npm run build && node scripts/preflight.mjs", "prepublishOnly": "npm run build" }, @@ -32,11 +56,19 @@ "claude-code", "cursor", "agents", - "workflow", - "activity-tracing", - "automation" + "ai-code-verification", + "github-actions", + "pull-request", + "continuous-proof", + "code-review", + "factfile", + "git", + "open-source", + "evidence", + "human-review", + "progress" ], - "author": "Keyoku ", + "author": "Keyoku", "license": "MIT", "homepage": "https://keyoku.ai", "repository": { @@ -50,18 +82,24 @@ "node": ">=20" }, "dependencies": { - "@anthropic-ai/sdk": "^0.104.1", "@modelcontextprotocol/sdk": "^1.12.0", "jmespath": "^0.16.0", "yaml": "^2.9.0", "zod": "^3.25.0" }, "devDependencies": { + "@anthropic-ai/sdk": "^0.104.1", "@types/jmespath": "^0.15.2", "@types/node": "^20.10.0", "tsup": "^8.0.1", "tsx": "^4.19.0", "typescript": "^5.3.0", - "vitest": "^1.0.0" + "vitest": "^4.1.10" + }, + "optionalDependencies": { + "fsevents": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + }, + "overrides": { + "esbuild": "^0.28.2" } } diff --git a/scripts/preflight.mjs b/scripts/preflight.mjs index 633b28d..7ea6bd2 100644 --- a/scripts/preflight.mjs +++ b/scripts/preflight.mjs @@ -7,7 +7,7 @@ import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; -import { dirname, join } from "node:path"; +import { basename, dirname, join } from "node:path"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const read = (p) => readFileSync(join(root, p), "utf8"); @@ -29,12 +29,17 @@ const changelog = read("CHANGELOG.md"); if (new RegExp(`^##\\s+${version.replace(/[.]/g, "\\.")}\\b`, "m").test(changelog)) ok(`CHANGELOG.md documents ${version}`); else fail("CHANGELOG.md documents this version", `no "## ${version}" heading found — add a changelog entry`); +const action = read("action.yml"); +const actionVersion = action.match(/keyoku-version:[\s\S]*?default:\s*["']?([^"'\s]+)["']?/m)?.[1]; +if (actionVersion === version) ok("GitHub Action installs the exact package version"); +else fail("GitHub Action installs the exact package version", `action.yml defaults to ${actionVersion ?? "no version"}, package.json is ${version}`); + // 3. VERSION must be DERIVED from package.json, never a hardcoded literal — this // is the exact regression that shipped "0.1.0" while the package was 2.7.x. -const server = read("src/server.ts"); +const server = read("src/public-server.ts"); const versionDecl = server.match(/export const VERSION[^\n]*=([^\n]*)/); if (!versionDecl) { - fail("VERSION is single-sourced", "could not find `export const VERSION` in src/server.ts"); + fail("VERSION is single-sourced", "could not find `export const VERSION` in src/public-server.ts"); } else if (/=\s*["'`]\d+\.\d+\.\d+["'`]/.test(versionDecl[0])) { fail("VERSION is single-sourced", `VERSION is a hardcoded literal (${versionDecl[1].trim()}) — derive it from package.json so it can't drift`); } else if (!/package\.json|readFileSync|VERSION:\s*string\s*=/.test(server.slice(server.indexOf("export const VERSION")))) { @@ -59,12 +64,42 @@ try { // npm always includes package.json, README, LICENSE regardless of `files`, // so this is a guard against someone "optimising" it out. const files = pkg.files ?? []; -if (files.some((f) => f.startsWith("dist"))) ok("dist is included in the published files"); -else fail("dist is included in the published files", `package.json "files" = ${JSON.stringify(files)} — dist must ship`); +if (files.includes("dist/index.js")) ok("the v3 entrypoint is included in published files"); +else fail("the v3 entrypoint is included in published files", `package.json "files" = ${JSON.stringify(files)}`); +if (!files.some((file) => file.includes("legacy-cli"))) ok("the compatibility CLI is excluded from published files"); +else fail("the compatibility CLI is excluded from published files", "legacy-cli must remain test-only"); + +// 6. The built help is the customer-facing contract. Legacy muscle-memory +// commands may remain regression-tested, but cannot leak back into v3 help. +try { + const help = execFileSync("node", ["dist/index.js", "help"], { cwd: root, encoding: "utf8" }); + for (const command of ["proof", "factfile", "pulse", "serve", "doctor", "version", "help"]) { + if (!new RegExp(`keyoku\\s+${command}\\b`).test(help)) fail(`public help includes ${command}`, "missing from dist/index.js help"); + } + const leaked = ["goal", "workflow", "connector", "record", "iterate", "contribution", "gate", "project", "outcome"] + .filter((command) => new RegExp(`keyoku\\s+${command}\\b`).test(help)); + if (leaked.length === 0) ok("built help contains no legacy top-level commands"); + else fail("built help contains no legacy top-level commands", `found: ${leaked.join(", ")}`); +} catch (err) { + fail("built public help is inspectable", err instanceof Error ? err.message : String(err)); +} + +// 7. Source maps make accidental bundling inspectable. These v2 subsystems may +// remain in the repository and test-only compatibility build, but must not be +// reachable code in the shipped v3 entrypoint. +try { + const map = JSON.parse(read("dist/index.js.map")); + const prohibited = new Set(["connectors.ts", "engine.ts", "learn.ts", "observe.ts", "openapi.ts", "slm.ts", "server.ts", "store.ts", "executor.ts"]); + const leaked = map.sources.filter((source) => prohibited.has(basename(source))); + if (leaked.length === 0) ok("the public bundle excludes v2 connector, goal, memory, learning, and execution modules"); + else fail("the public bundle excludes v2 connector, goal, memory, learning, and execution modules", leaked.join(", ")); +} catch (err) { + fail("the public bundle source inventory is inspectable", err instanceof Error ? err.message : String(err)); +} console.log("\nkeyoku release preflight\n" + checks.join("\n")); if (failures.length > 0) { console.error("\nFAILED:\n" + failures.join("\n") + "\n"); process.exit(1); } -console.log(`\nAll ${checks.length} checks passed — safe to tag v${version}.\n`); +console.log(`\nAll ${checks.length} candidate checks passed. This is not publish authorization; security, UX, distribution, and owner release gates still apply.\n`); diff --git a/scripts/render-assurance-fixtures.mjs b/scripts/render-assurance-fixtures.mjs new file mode 100644 index 0000000..1d1e38a --- /dev/null +++ b/scripts/render-assurance-fixtures.mjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { sealEvidenceEnvelope, sealWorkEvent } from "../dist/index.js"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const out = join(root, "fixtures", "assurance", "v1"); +const digest = (character) => character.repeat(64); + +const evidence = sealEvidenceEnvelope({ + schemaVersion: "evidence-provider/v1", + work: { id: "sample-change", objective: "Confirm the bounded change behaves as declared." }, + claims: [{ id: "behavior", statement: "The declared behavior passes its native check.", verdict: "pass", evidenceRefs: ["native-check", "result-log"] }], + source: { capturedDigest: digest("a"), currentDigest: digest("a"), label: "source snapshot" }, + commands: [{ id: "native-check", command: "project-test-command", exitCode: 0, resultDigest: digest("b") }], + artifacts: [{ id: "result-log", path: "evidence/result.txt", digest: digest("c") }], + limitations: ["This generic fixture contains synthetic digests and establishes no deployment claim."], + authority: { kind: "human", id: "review-owner", decision: "approved" }, +}); + +const events = [ + sealWorkEvent({ schemaVersion: "work-event/v1", id: "sample-checkpoint", kind: "checkpoint", at: "2026-08-25T16:00:00.000Z", workId: evidence.work.id, summary: "A content-bound evidence result is available for caller review.", outcome: "checkpoint_ready", sourceDigest: evidence.source.capturedDigest, limitations: evidence.limitations }), + sealWorkEvent({ schemaVersion: "work-event/v1", id: "sample-terminal", kind: "terminal", at: "2026-08-25T16:05:00.000Z", workId: evidence.work.id, summary: "The caller recorded its terminal outcome.", outcome: "complete", limitations: evidence.limitations }), +]; + +mkdirSync(out, { recursive: true }); +writeFileSync(join(out, "evidence.json"), `${JSON.stringify(evidence, null, 2)}\n`, "utf8"); +writeFileSync(join(out, "work-events.jsonl"), `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8"); +console.log(`Rendered neutral assurance fixtures in ${out}`); diff --git a/scripts/render-conformance-fixtures.mjs b/scripts/render-conformance-fixtures.mjs new file mode 100644 index 0000000..2a3e894 --- /dev/null +++ b/scripts/render-conformance-fixtures.mjs @@ -0,0 +1,23 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { buildPulseConformanceVectors } from "../dist/index.js"; + +const repositoryRoot = dirname(dirname(fileURLToPath(import.meta.url))); +const outputRoot = join(repositoryRoot, "fixtures", "conformance", "v1"); +const vectors = buildPulseConformanceVectors(); +const write = (path, value) => { + const absolute = join(outputRoot, path); + mkdirSync(dirname(absolute), { recursive: true }); + writeFileSync(absolute, value); +}; +const jsonl = (events) => `${events.map((event) => JSON.stringify(event)).join("\n")}\n`; + +write("manifest.json", `${JSON.stringify(vectors.manifest, null, 2)}\n`); +for (const [id, events] of Object.entries(vectors.eventSets)) write(`events/${id}.jsonl`, jsonl(events)); +write(vectors.manifest.bytes.factfile.path, vectors.factfileBytes); +write(vectors.manifest.bytes.asset.path, vectors.assetBytes); +write(vectors.manifest.bytes.poster.path, vectors.posterBytes); + +console.log(`Rendered Pulse conformance vectors under ${outputRoot}`); diff --git a/scripts/render-github-preview.mjs b/scripts/render-github-preview.mjs new file mode 100644 index 0000000..aaee5b3 --- /dev/null +++ b/scripts/render-github-preview.mjs @@ -0,0 +1,27 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; + +const [input, output = "output/playwright/keyoku-github-proof.html"] = process.argv.slice(2); +if (!input) { + console.error("Usage: node scripts/render-github-preview.mjs [output.html]"); + process.exit(2); +} + +const body = execFileSync("gh", [ + "api", "markdown", "--method", "POST", + "-F", `text=@${resolve(input)}`, + "-f", "mode=gfm", + "-f", "context=Keyoku-ai/keyoku", +], { encoding: "utf8", maxBuffer: 4_000_000 }); + +const html = `Keyoku GitHub proof preview
KKeyoku-ai / keyokuChecks / Outcome proof
Keyoku proofGitHub-rendered preview
${body}
`; + +const target = resolve(output); +mkdirSync(dirname(target), { recursive: true }); +writeFileSync(target, html, "utf8"); +console.log(target); diff --git a/scripts/render-pulse-fixtures.mjs b/scripts/render-pulse-fixtures.mjs new file mode 100644 index 0000000..a0522f4 --- /dev/null +++ b/scripts/render-pulse-fixtures.mjs @@ -0,0 +1,31 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + buildGenericPulseFixture, + buildProcessyardPulseFixture, + planPulseDispatch, +} from "../dist/index.js"; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); +const out = join(root, "fixtures", "pulse"); +mkdirSync(out, { recursive: true }); + +const generic = buildGenericPulseFixture(); +const processyard = buildProcessyardPulseFixture(); +const jsonl = (events) => `${events.map((event) => JSON.stringify(event)).join("\n")}\n`; + +writeFileSync(join(out, "generic.jsonl"), jsonl(generic.events), "utf8"); +writeFileSync(join(out, "processyard-m0-m6.jsonl"), jsonl(processyard.events), "utf8"); + +const coalesced = planPulseDispatch({ events: processyard.events, ...processyard.coalescingPlan }); +if (coalesced.outcome !== "suppress" || coalesced.reasonCode !== "attested_checkpoint" || coalesced.snapshot) throw new Error(`Expected attested fixture suppression, received ${coalesced.outcome}/${coalesced.reasonCode}.`); +writeFileSync(join(out, "processyard-coalesced.json"), `${JSON.stringify(coalesced, null, 2)}\n`, "utf8"); +writeFileSync(join(out, "processyard-timeline.html"), "Attested fixture — no projection

This synthetic fixture is attested, not locally verified. Keyoku correctly produced no dispatchable timeline.

\n", "utf8"); + +const stale = planPulseDispatch({ events: processyard.events, ...processyard.recommendedPlan }); +if (stale.outcome !== "stale_no_send") throw new Error(`Expected Processyard stale_no_send, received ${stale.outcome}.`); +writeFileSync(join(out, "processyard-stale-no-send.json"), `${JSON.stringify(stale, null, 2)}\n`, "utf8"); + +console.log(`Rendered Pulse fixtures under ${out}`); diff --git a/scripts/rerender.mjs b/scripts/rerender.mjs new file mode 100644 index 0000000..cb09418 --- /dev/null +++ b/scripts/rerender.mjs @@ -0,0 +1,76 @@ +#!/usr/bin/env node +// Re-renders a Factfile HTML from an existing factfile.json without re-running any probes. +// +// Usage: +// node scripts/rerender.mjs [output.html] +// node scripts/rerender.mjs [output.html] --with-demo-watch +// +// --with-demo-watch optionally patches in the "demo-watch-pass" criterion's +// screenshot/report artifacts (base64-embedded, same as resolveEvidencePresentation does at +// gate time) so the filmstrip hero can be exercised even when the on-disk factfile.json predates +// that criterion. It never writes back to the source project — only to this script's output. + +import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { dirname, resolve } from "node:path"; +import { renderFactfileHtml } from "../dist/index.js"; + +const args = process.argv.slice(2); +const input = args[0]; +const output = args[1] && !args[1].startsWith("--") ? args[1] : "preview-factfile.html"; +const demoWatchFlagIndex = args.indexOf("--with-demo-watch"); +const demoWatchRoot = demoWatchFlagIndex !== -1 ? args[demoWatchFlagIndex + 1] : undefined; + +if (!input) { + console.error("Usage: node scripts/rerender.mjs [output.html] [--with-demo-watch ]"); + process.exit(2); +} + +const snapshot = JSON.parse(readFileSync(resolve(input), "utf8")); + +if (demoWatchRoot) { + const artifactSpecs = [ + { kind: "screenshot", path: "demo-captures/01-cfo-hero.jpeg", label: "CFO executive glance", caption: "Score 58/critical, 20 controls (12 key/8 non-key), systems 4, entities 5, control/risk deltas +2." }, + { kind: "screenshot", path: "demo-captures/09-reviewer-full.jpeg", label: "Reviewer dashboard (new persona)", caption: "Pending review 6, reviewed 3, avg 4.0 days waiting, pending-by-tester breakdown." }, + { kind: "screenshot", path: "demo-captures/12-pbc-testing-period.jpeg", label: "PBC Testing Period column", caption: "Every evidence request shows the testing period it covers." }, + { kind: "report", path: ".keyoku/contributions/dashboards-feedback-round-2026-08-23-0ad97b6a/demo-walkthrough.html", label: "Full demo walkthrough", caption: "All 14 captioned stops, published as a shareable gallery." }, + ]; + const artifacts = artifactSpecs.map((spec) => { + const absolute = resolve(demoWatchRoot, spec.path); + if (!existsSync(absolute) || !statSync(absolute).isFile()) return { ...spec, annotations: [], unavailable: "Artifact was not found for this preview." }; + const bytes = readFileSync(absolute); + const digest = createHash("sha256").update(bytes).digest("hex"); + if (spec.kind !== "screenshot" && spec.kind !== "video") return { ...spec, annotations: [], digest }; + const lower = spec.path.toLowerCase(); + const mediaType = lower.endsWith(".jpeg") || lower.endsWith(".jpg") ? "image/jpeg" : lower.endsWith(".png") ? "image/png" : lower.endsWith(".webp") ? "image/webp" : undefined; + if (!mediaType) return { ...spec, annotations: [], digest, unavailable: "Unsupported screenshot format." }; + return { ...spec, annotations: [], digest, mediaType, dataUrl: `data:${mediaType};base64,${bytes.toString("base64")}` }; + }); + snapshot.evidence.push({ + id: "c8", + description: "An agent watched the recorded demo and every stop's expectations are visibly met", + pass: true, + actual: { exitCode: 0 }, + expected: { path: "exitCode", op: "eq", value: 0 }, + durationMs: 41200, + verification: { + kind: "command", + label: "Repository command", + reproduce: "bash .keyoku/probes/demo-watch-pass.sh", + assertion: { path: "exitCode", op: "eq", value: 0 }, + }, + presentation: { + summary: "A vision agent reviewed all 14 Playwright-captured demo frames against their declared expectations (and ran a UI/UX audit); the verdict is pass and postdates the frames.", + whyItMatters: "Humans digest demos — this proves the demo a stakeholder would watch actually shows the claimed behavior, not just that endpoints return 200.", + code: [], + artifacts, + }, + }); + snapshot.summary = { ...snapshot.summary, passed: snapshot.summary.passed + 1, total: snapshot.summary.total + 1 }; + console.error(`Patched in criterion c8 (demo-watch-pass) with ${artifacts.filter((a) => a.dataUrl).length} embedded screenshot(s) for filmstrip verification.`); +} + +const html = renderFactfileHtml(snapshot, {}); +const target = resolve(output); +writeFileSync(target, html, "utf8"); +console.log(target); diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..42cd0c9 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,35 @@ +# Bundled skills — harness-portable agent guidance + +Keyoku ships its agent know-how as **bundled skills**: harness-neutral markdown +procedures that `keyoku skills install` materializes into whatever coding harness +the project uses. The package is the source of truth; the harness copy is an +install artifact (regenerate on upgrade, never hand-edit). + +## Distribution design (implemented by `keyoku skills`) + +``` +keyoku skills list # bundled skills + versions +keyoku skills install [--harness claude|codex|cursor|generic|auto] +``` + +Harness targets (auto-detected from the repo, overridable): +- **claude** → `.claude/skills//SKILL.md` (frontmatter added: name, description) +- **codex** → appended/refreshed section in `AGENTS.md` (delimited markers for idempotent refresh) +- **cursor** → `.cursor/rules/keyoku-.mdc` +- **generic**→ `docs/keyoku-skills/.md` + an index line in `AGENTS.md` if present + +The same content is also exposed over the MCP server (resources/prompts), so +MCP-capable harnesses can read skills without any file install at all. + +Each skill file here is pure harness-neutral markdown: first line `# `, +second line a one-sentence description (used as frontmatter/description on +install), then the procedure. No harness-specific paths — reference keyoku by +CLI (`keyoku …`) or MCP tool names only. + +## Bundled skills + +- `arch-diagram.md` — author accurate, beautiful architecture diagrams as specs + rendered by `keyoku arch render` / deck architecture sections. +- `demo-evidence.md` — the record → watch → gate demo pipeline. +- `proof-workflow.md` — the contribution flow: outcome → work → directions → gate. +- `deck-authoring.md` — persona decks: agent plans config, `deck build` renders. diff --git a/skills/arch-diagram.md b/skills/arch-diagram.md new file mode 100644 index 0000000..d4bc3b0 --- /dev/null +++ b/skills/arch-diagram.md @@ -0,0 +1,48 @@ +# Architecture diagrams +Author accurate, beautiful technical architecture diagrams as declarative specs rendered by `keyoku arch render` — never hand-drawn. + +Architecture diagrams are **declarative specs** rendered by keyoku's deterministic +engine, so every diagram shares one visual language and re-renders identically. + +## Pipeline +1. **Derive the truth first.** List real components and real edges from code — + route registrations, service URLs, queue names — not from memory. Verify each + edge with one search where possible. A beautiful wrong diagram is worse than none. +2. **Write the spec** (YAML; same shape as `architecture.diagram` in `.keyoku/deck.yaml`): + +```yaml +nodes: + - { id: browser, icon: browser, label: User's browser, sub: entry point } + - { id: frontend, icon: ui, label: Web app, sub: SPA, zone: app } + - { id: backend, icon: api, label: API, sub: org-scoped, zone: app } + - { id: db, icon: db, label: Database, sub: migrations } +edges: + - { from: browser, to: frontend } + - { from: frontend, to: backend, label: auth header } + - { from: backend, to: db } +zones: + - { id: app, label: Application } +``` + +3. **Render**: `keyoku arch render spec.yaml -o out.svg` — or let + `keyoku deck build` render it inside a deck's architecture section (same engine). +4. **Look at the output** (read the SVG as an image if your harness can): no + overlapping labels, arrows follow data flow, edge labels legible. + +## Authoring rules +- **One icon per node, from the built-in set only** (`keyoku arch icons` lists them; + core set: browser ui api db gear agent doc shield cloud queue cache storage lock + chart mail mobile terminal git user webhook). If nothing fits, `gear` + a precise + label beats a wrong metaphor. +- **Left → right is the request path**; persistence right, async actors off the main + lane. Author edges in true data-flow direction and the layout follows. +- **6 ± 3 nodes.** More means two diagrams — split by concern. +- **`sub` holds one fact** (port, protocol, table count) — not a sentence. +- **Label an edge only when the mechanism is the point** (auth scheme, event name). +- **Zones sparingly** — one boundary type per diagram (trust, deployment, or team). +- **Persona depth goes in prose, not the picture**: one accurate diagram, per-audience + `explain:` text (stakeholder: guarantees; developer: mechanics). + +## Where specs live +- Deck: `architecture` section of `.keyoku/deck.yaml`. +- Standalone: commit the spec beside the doc; the SVG is a build artifact. diff --git a/skills/deck-authoring.md b/skills/deck-authoring.md new file mode 100644 index 0000000..cf27e96 --- /dev/null +++ b/skills/deck-authoring.md @@ -0,0 +1,40 @@ +# Deck authoring +Produce persona-targeted evidence decks: the coding agent plans `.keyoku/deck.yaml`, `keyoku deck build` renders deterministically. + +## Division of labor +- **Agent (you)**: author the CONFIG — audience, section order, copy, frame choices, + annotations, architecture spec. `keyoku deck plan "<natural prompt>"` does this + from a prompt; hand-editing deck.yaml is equally valid. +- **Keyoku**: render. Same config → same deck, any project. Never hand-assemble the + HTML; if the renderer lacks a section type you need, that's a generator gap to + raise, not a reason to freehand. + +## Personas +- `stakeholder`: video first, big visuals, short status (verdict + counts + pending + decisions), concepts explained in plain language. +- `developer`: original change request, requirements → delivered mapping with + evidence keys, annotated before/after pairs, FULL status (every criterion with its + reproduce command), architecture mechanics. Demo video only if it adds something. +- Persona controls inclusion AND order via `personas.<name>.sections`. + +## Content rules +- Captions state what the viewer is seeing plus the number that matters. +- Before/after pairs need REAL "before" captures (previous deploy/image), annotated + with %-box markers (same shape as Factfile artifact annotations). +- Quote YAML values containing `#` (a bare `PR #12` starts a comment mid-flow-map). +- One screen per slide; horizontal navigation; tabs expose every section. +- End with links: PRs, the other persona's deck, the Factfile. + +## Mandatory visual review — no publish without it +Before publishing ANY deck or diagram, LOOK at every image as a viewer will see +it — open the built HTML (or read each slide image) and check each frame IN ITS +FINAL CROP/SIZE, not the raw capture. The classic failure: a raw frame is fine +but the deck's crop slices off a popover, or a caption's subject sits in the +cropped region. Check: nothing meaningful cut off, no clipped tooltips/labels, +text in diagrams fits its boxes, both themes legible. If a frame fails, fix at +the source (re-capture, per-frame `crop: false`, shorter labels) — never ship +and hope. + +## Publishing +Local HTML always; then offer the harness's publishing channel (e.g. an artifact) +rather than assuming one. Keep artifact URLs stable across re-publishes. diff --git a/skills/demo-evidence.md b/skills/demo-evidence.md new file mode 100644 index 0000000..6f67653 --- /dev/null +++ b/skills/demo-evidence.md @@ -0,0 +1,28 @@ +# Demo evidence +Record a product demo with Playwright, have an agent watch and audit it, and gate the result — `keyoku demo record → watch → gate`. + +Humans digest demos; exit codes don't persuade. This pipeline makes the demo +itself machine-checkable evidence. + +## Pipeline +1. `keyoku demo init` → `.keyoku/demo.yaml`: baseUrl, optional auth steps, and + **stops** — each with a url/actions and 1+ human-readable `expect` lines stating + what must be VISIBLE in the frame. +2. `keyoku demo record` → Playwright executes the stops → `.keyoku/demo/frames/*.jpeg` + + `manifest.json` (and video when configured). +3. `keyoku demo watch` → an agent looks at every frame: per-stop verdict + (requirement_met true/false/partial + evidence_seen) **plus a UI/UX audit** + (hierarchy, truncation, broken charts, color semantics) → `.keyoku/demo/verdict.json`. +4. Gate it: add a criterion `run: keyoku demo watch --assert` to the outcome — + passes only if the verdict is a fresh pass (postdates the frames). + +## Rules that make it work +- `expect` lines describe pixels, not intentions ("a TESTING PERIOD column with date + ranges", not "periods work"). The watcher judges only what it can see. +- Fix rounds re-record BEFORE re-watching; the freshness assert exists to catch + stale verdicts — never edit a verdict by hand. +- Treat watcher UI/UX findings as real defects with severities; the demo failing on + a dead button that "worked" in scripted clicks is the pipeline earning its keep. +- After recording, LOOK at every frame yourself before handing to the watcher — a mis-timed capture (popover not yet open, spinner mid-frame) wastes a watch round. +- Keep stops ≈ 8–15; one concept per stop; realistic seeded data (real names, varied + dates) — placeholder data is a credibility finding, not a shortcut. diff --git a/skills/proof-workflow.md b/skills/proof-workflow.md new file mode 100644 index 0000000..c01dadc --- /dev/null +++ b/skills/proof-workflow.md @@ -0,0 +1,27 @@ +# Proof workflow +Bind work to a versioned outcome, keep status current, and close with a fail-closed evidence gate — the Keyoku contribution flow. + +## Flow +1. `project_inspect` / `outcome_list` (MCP) or `keyoku project init` — find or create + the project contract. +2. Author/select an **outcome**: objective, constraints, `criteria` (each with an + executable probe + assert + evidence prose: summary and whyItMatters), and + `humanCriteria` for judgments that stay with the accountable owner. Probes must + assert BEHAVIOR (build passes, live endpoint reconciles, verdict fresh) — never + file presence. +3. `contribution_start` — bind actors (agent includes harness, model, ownerId). +4. `contribution_report_work` — one item per requirement; status done/blocked with a + detail that names the evidence. Activity is coordination, never proof. +5. `contribution_propose_directions` — 1–4 evidence-grounded next moves before gating. +6. `contribution_gate` — executes every probe fail-closed, binds the snapshot, + renders the Factfile (json/md/html). Passing probes + pending human judgments ⇒ + `human_review_required`: correct, not a failure. + +## Invariants (learned the hard way) +- **Changing an outcome file requires a revision bump + a new contribution** — the + gate refuses a silently-drifted contract. Plan criteria before gating. +- Freshness matters: a verdict/evidence artifact must postdate what it judges. +- Multi-repo workspaces without a root git repo lose baseSha binding — add a probe + that pins each subrepo's PR head SHA == local HEAD. +- Write probes as committed scripts (`.keyoku/probes/*.sh`), each with a comment + saying what it PROVES; every Factfile row then carries its reproduce command. diff --git a/src/activity.ts b/src/activity.ts index f993edf..a044e15 100644 --- a/src/activity.ts +++ b/src/activity.ts @@ -1,4 +1,5 @@ import type { ActivityEvent, WorkflowStepTemplate } from "./types.js"; +export { redactSecrets } from "./redaction.js"; export interface ActivitySuggestion { slug: string; @@ -150,34 +151,6 @@ export function detectPatterns( }); } -// Secrets must never enter the activity log — they would propagate into -// drafts, baked skills (committed to repos!), and the engine mirror. Redact -// at record time: key=value/key: value assignments whose key smells like a -// credential, and bearer tokens. Conservative by design — losing a file path -// to over-redaction is fine; leaking a token is not. -const SECRET_ASSIGNMENT_RE = - /([\w-]*(?:token|secret|passwd|password|api[_-]?key|access[_-]?key|credential|auth)[\w-]*["']?\s*[:=]\s*)(["']?)(?!bearer\b)(?!basic\b)(?!«redacted»)[^\s"']{4,}\2/gi; -const BEARER_RE = /\b(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi; -// "Authorization: Basic <base64(user:pass)>" — the assignment rule alone -// mis-redacts the literal word "Basic" and leaks the credential after it. -const BASIC_RE = /\b(basic\s+)[A-Za-z0-9+/=]{8,}/gi; -// URL userinfo: postgres://user:pass@host, redis://:pass@host (empty user), -// mongodb+srv://:pass@host — redact the password between ':' and '@' while -// keeping scheme, user, and host readable. The username is OPTIONAL (`*`, not -// `+`) so the common no-username form is covered. (Ports like host:8080/ aren't -// matched: they have no trailing '@'.) -const URL_USERINFO_RE = /([a-z][a-z0-9+.-]*:\/\/[^\s/:@"']*:)[^\s@"']+@/gi; - -export function redactSecrets(text: string): string { - // Scheme-specific rules first (bearer/basic/url-userinfo) so the generic - // key=value assignment rule doesn't consume the scheme keyword as the value. - return text - .replace(BEARER_RE, "$1«redacted»") - .replace(BASIC_RE, "$1«redacted»") - .replace(URL_USERINFO_RE, "$1«redacted»@") - .replace(SECRET_ASSIGNMENT_RE, "$1«redacted»"); -} - /** Map one observed event to a draft workflow step. Shared by pattern * detection and on-demand capture so both produce identical drafts. */ export function draftStep(ev: ActivityEvent): WorkflowStepTemplate { diff --git a/src/arch.ts b/src/arch.ts new file mode 100644 index 0000000..3e47b01 --- /dev/null +++ b/src/arch.ts @@ -0,0 +1,561 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, isAbsolute, join, relative, resolve } from "node:path"; +import { parse } from "yaml"; +import { z } from "zod"; + +// --------------------------------------------------------------------------- +// `keyoku arch` — a standalone, deterministic architecture-diagram renderer. +// `renderArchSvg` is a pure function (spec in, SVG string out — no I/O, no +// randomness) so it is safely reusable both by `keyoku deck`'s `architecture` +// section (embedded mode, inherits the deck's design tokens) and directly via +// the `keyoku arch render <spec.yaml>` CLI (standalone mode, ships its own +// minimal <style> so the file is legible opened on its own). +// --------------------------------------------------------------------------- + +// ---- icon registry: one small, consistent line-icon family ---------------- +// 24x24 viewBox, stroke="currentColor", stroke-width 1.5, round caps/joins, +// no fills except small accent dots — same visual weight and optical size +// across the whole set so a diagram never looks like mixed clip-art. + +export const ICON_IDS = [ + "browser", + "ui", + "api", + "db", + "gear", + "agent", + "doc", + "shield", + "cloud", + "queue", + "cache", + "storage", + "lock", + "chart", + "mail", + "mobile", + "terminal", + "git", + "user", + "webhook", +] as const; +export type IconId = (typeof ICON_IDS)[number]; + +const ICON_PATHS: Record<IconId, string> = { + browser: `<rect x="2.75" y="4" width="18.5" height="16" rx="2.25"/> +<path d="M2.75 8.75h18.5"/> +<circle cx="5.75" cy="6.375" r="0.55" fill="currentColor" stroke="none"/> +<circle cx="7.85" cy="6.375" r="0.55" fill="currentColor" stroke="none"/>`, + ui: `<rect x="3" y="3" width="7.5" height="7.5" rx="1.5"/> +<rect x="13.5" y="3" width="7.5" height="4.5" rx="1.5"/> +<rect x="13.5" y="9.5" width="7.5" height="11.5" rx="1.5"/> +<rect x="3" y="12.5" width="7.5" height="8.5" rx="1.5"/>`, + api: `<path d="M4 8.5h12.5"/> +<path d="M13 4.5l4 4-4 4"/> +<path d="M20 15.5H7.5"/> +<path d="M11 19.5l-4-4 4-4"/>`, + db: `<ellipse cx="12" cy="5.5" rx="8" ry="2.75"/> +<path d="M4 5.5v13c0 1.52 3.58 2.75 8 2.75s8-1.23 8-2.75v-13"/> +<path d="M4 12c0 1.52 3.58 2.75 8 2.75s8-1.23 8-2.75"/>`, + gear: `<circle cx="12" cy="12" r="3.1"/> +<path d="M12 2.5v3M12 18.5v3M4.4 4.4l2.1 2.1M17.5 17.5l2.1 2.1M2.5 12h3M18.5 12h3M4.4 19.6l2.1-2.1M17.5 6.5l2.1-2.1"/>`, + agent: `<rect x="5" y="8" width="14" height="11" rx="2.25"/> +<circle cx="9.5" cy="13.5" r="1" fill="currentColor" stroke="none"/> +<circle cx="14.5" cy="13.5" r="1" fill="currentColor" stroke="none"/> +<path d="M12 8V4.5"/> +<circle cx="12" cy="3.25" r="1" fill="currentColor" stroke="none"/> +<path d="M2.5 13h2.5M19 13h2.5"/>`, + doc: `<path d="M6.25 2.5h8.5l4.25 4.25V21a1 1 0 0 1-1 1H6.25a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1z"/> +<path d="M14.75 2.5V7h4.25"/> +<path d="M8 12.25h8M8 15.75h8M8 19.25h4.75"/>`, + shield: `<path d="M12 2.5l7.25 2.9v5.8c0 4.85-3.1 8.35-7.25 9.7-4.15-1.35-7.25-4.85-7.25-9.7V5.4z"/> +<path d="M8.75 12l2.35 2.35 4.4-4.6"/>`, + cloud: `<path d="M7.5 18a4.35 4.35 0 0 1-.5-8.67A5.35 5.35 0 0 1 17.65 9.05 3.85 3.85 0 0 1 17.35 18h-9.85z"/>`, + queue: `<rect x="3" y="4.5" width="18" height="3.75" rx="1"/> +<rect x="3" y="10.125" width="18" height="3.75" rx="1"/> +<rect x="3" y="15.75" width="18" height="3.75" rx="1"/>`, + cache: `<path d="M12 3.5l8 4.25-8 4.25-8-4.25z"/> +<path d="M4 12l8 4.25L20 12"/> +<path d="M4 15.75l8 4.25 8-4.25"/>`, + storage: `<path d="M5 6.5h14l-1.4 12.6a2 2 0 0 1-2 1.9H8.4a2 2 0 0 1-2-1.9z"/> +<path d="M3.5 6.5h17"/> +<path d="M9 3.5h6l1 3H8z"/>`, + lock: `<rect x="5" y="10.5" width="14" height="10" rx="2"/> +<path d="M8 10.5V7.75a4 4 0 0 1 8 0v2.75"/> +<circle cx="12" cy="15" r="1.15" fill="currentColor" stroke="none"/> +<path d="M12 16.15v1.85"/>`, + chart: `<path d="M3.5 20.5h17"/> +<rect x="5.5" y="13" width="3.2" height="7.5" rx="0.6"/> +<rect x="10.4" y="8.5" width="3.2" height="12" rx="0.6"/> +<rect x="15.3" y="4.5" width="3.2" height="16" rx="0.6"/>`, + mail: `<rect x="2.75" y="5" width="18.5" height="14" rx="2"/> +<path d="M3.25 6.25l8.75 7 8.75-7"/>`, + mobile: `<rect x="7" y="2.5" width="10" height="19" rx="2.25"/> +<path d="M10.5 19h3"/>`, + terminal: `<rect x="2.75" y="4" width="18.5" height="16" rx="2.25"/> +<path d="M6.5 9.5l3.5 3-3.5 3"/> +<path d="M12.5 15.5h5"/>`, + git: `<circle cx="6" cy="6" r="2"/> +<circle cx="6" cy="18" r="2"/> +<circle cx="18" cy="9" r="2"/> +<path d="M6 8v8"/> +<path d="M6 12c0-3.3 2.7-6 6-6h4"/>`, + user: `<circle cx="12" cy="7.5" r="3.75"/> +<path d="M4.5 20.5c1.1-4 4-6 7.5-6s6.4 2 7.5 6"/>`, + webhook: `<path d="M13 2.5L5.5 13.5h5.25L10.5 21.5l8-11.5H13z"/>`, +}; + +/** SVG `<symbol>` defs for every registered icon — used verbatim in <defs>. */ +export function iconSymbolDefs(): string { + return ICON_IDS.map( + (id) => + `<symbol id="icon-${id}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${ICON_PATHS[id]}</symbol>`, + ).join("\n"); +} + +// ---- spec schema (keyoku.dev/arch/v1alpha1) -------------------------------- +// A superset of the deck's inline diagram shape: adds `zone` on nodes, +// `style` on edges, and top-level `zones` (optional grouping lanes). + +export const ArchNodeSchema = z.object({ + id: z.string().min(1), + icon: z.enum(ICON_IDS), + label: z.string().min(1), + sub: z.string().min(1).optional(), + zone: z.string().min(1).optional(), +}); +export type ArchNode = z.infer<typeof ArchNodeSchema>; + +export const ArchEdgeSchema = z.object({ + from: z.string().min(1), + to: z.string().min(1), + label: z.string().min(1).optional(), + style: z.enum(["solid", "dashed"]).default("solid"), +}); +export type ArchEdge = z.infer<typeof ArchEdgeSchema>; + +export const ArchZoneSchema = z.object({ + id: z.string().min(1), + label: z.string().min(1), +}); +export type ArchZone = z.infer<typeof ArchZoneSchema>; + +export const ArchSpecSchema = z + .object({ + title: z.string().min(1).optional(), + nodes: z.array(ArchNodeSchema).min(1), + edges: z.array(ArchEdgeSchema).default([]), + zones: z.array(ArchZoneSchema).default([]), + }) + .superRefine((spec, ctx) => { + const nodeIds = new Set(spec.nodes.map((n) => n.id)); + const zoneIds = new Set(spec.zones.map((z) => z.id)); + spec.edges.forEach((edge, i) => { + if (!nodeIds.has(edge.from)) { + ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["edges", i, "from"], message: `edges[${i}].from references unknown node id '${edge.from}'` }); + } + if (!nodeIds.has(edge.to)) { + ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["edges", i, "to"], message: `edges[${i}].to references unknown node id '${edge.to}'` }); + } + }); + spec.nodes.forEach((node, i) => { + if (node.zone && !zoneIds.has(node.zone)) { + ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["nodes", i, "zone"], message: `nodes[${i}].zone references unknown zone id '${node.zone}'` }); + } + }); + }); +export type ArchSpec = z.infer<typeof ArchSpecSchema>; + +export interface RenderArchOptions { + /** true = the caller supplies page-level CSS for the `.arch-svg …` + * classes (see ARCH_CSS) and the diagram inherits its own design tokens + * (e.g. `keyoku deck`'s light/dark theme). false/undefined = standalone + * output embeds its own <style> with fallback colors so the file is + * legible opened on its own (white background). */ + embedded?: boolean; + title?: string; +} + +// ---- shared CSS — single source of truth for both embed modes ------------- +// Every color is `var(--arch-token, var(--token, fallback))`: a page that +// defines --ink/--muted/--surface/--line (e.g. keyoku deck's theme) themes +// the diagram automatically; a bare standalone file falls through to the +// literal fallback and stays legible on white. + +export const ARCH_CSS = `.arch-svg{width:100%;height:auto} +.arch-svg .arch-zone-box{fill:var(--arch-zone-bg,rgba(120,120,128,.05));stroke:var(--arch-line,var(--line,#e4e4e2));stroke-dasharray:3 3} +.arch-svg .arch-zone-label{fill:var(--arch-muted,var(--muted,#6b7076));font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase} +.arch-svg .arch-node-box{fill:var(--arch-surface,var(--surface,#ffffff));stroke:var(--arch-line,var(--line,#e4e4e2))} +.arch-svg .arch-node-icon-tile{fill:currentColor;fill-opacity:.08;stroke:currentColor;stroke-opacity:.16} +.arch-svg .arch-node-icon{color:var(--arch-ink,var(--ink,#1a1d20))} +.arch-svg .arch-node-label{fill:var(--arch-ink,var(--ink,#1a1d20));font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13.5px;font-weight:600} +.arch-svg .arch-node-sub{fill:var(--arch-muted,var(--muted,#6b7076));font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:10.5px} +.arch-svg .arch-edge-line{fill:none;stroke:var(--arch-muted,var(--muted,#9ba1a6));stroke-width:1.6} +.arch-svg .arch-edge-dashed{stroke-dasharray:5 4} +.arch-svg .arch-edge-arrow{fill:var(--arch-muted,var(--muted,#9ba1a6))} +.arch-svg .arch-edge-label-bg{fill:var(--arch-surface,var(--surface,#ffffff));stroke:var(--arch-line,var(--line,#e4e4e2))} +.arch-svg .arch-edge-label-text{fill:var(--arch-ink,var(--ink,#1a1d20));font-family:ui-monospace,Menlo,monospace;font-size:10px}`; + +// ---- layout: longest-path layering (same algorithm the deck used) --------- +// A node's layer is 1 + the max layer of every node with an edge into it (0 +// if none). Bounded relaxation passes so a cyclic diagram degrades +// gracefully instead of looping forever. Deterministic: no randomness, and +// iteration order always follows the input node/edge array order. + +function layerArchNodes(nodes: ArchNode[], edges: ArchEdge[]): Map<string, number> { + const incoming = new Map<string, string[]>(); + for (const n of nodes) incoming.set(n.id, []); + for (const e of edges) { + if (!incoming.has(e.to)) continue; // edge references an unknown node — ignore defensively + incoming.get(e.to)!.push(e.from); + } + const layer = new Map<string, number>(); + for (const n of nodes) layer.set(n.id, 0); + for (let pass = 0; pass < nodes.length + 1; pass++) { + let changed = false; + for (const n of nodes) { + let maxIn = -1; + for (const src of incoming.get(n.id) ?? []) { + if (!layer.has(src)) continue; + maxIn = Math.max(maxIn, layer.get(src)!); + } + const next = maxIn + 1; + if (next > (layer.get(n.id) ?? 0)) { + layer.set(n.id, next); + changed = true; + } + } + if (!changed) break; + } + return layer; +} + +// ---- orthogonal edge routing (rounded 6px corners) ------------------------- + +interface Pt { + x: number; + y: number; +} + +function distance(a: Pt, b: Pt): number { + return Math.hypot(b.x - a.x, b.y - a.y); +} + +function pointTowards(from: Pt, to: Pt, dist: number): Pt { + const len = distance(from, to) || 1; + const t = Math.min(dist, len) / len; + return { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t }; +} + +/** Renders a polyline through `pts` as straight segments with each interior + * corner rounded by a quadratic curve of radius `r` (clamped to half the + * shorter adjacent segment so short hops never overshoot). */ +function roundedPolylinePath(pts: Pt[], r: number): string { + if (pts.length < 2) return ""; + if (pts.length === 2) return `M${pts[0]!.x},${pts[0]!.y} L${pts[1]!.x},${pts[1]!.y}`; + let d = `M${pts[0]!.x},${pts[0]!.y}`; + for (let i = 1; i < pts.length - 1; i++) { + const prev = pts[i - 1]!; + const cur = pts[i]!; + const next = pts[i + 1]!; + const rr = Math.min(r, distance(prev, cur) / 2, distance(cur, next) / 2); + const p1 = pointTowards(cur, prev, rr); + const p2 = pointTowards(cur, next, rr); + d += ` L${p1.x},${p1.y} Q${cur.x},${cur.y} ${p2.x},${p2.y}`; + } + const last = pts[pts.length - 1]!; + d += ` L${last.x},${last.y}`; + return d; +} + +/** Via-points for an edge between two node positions. `fromCol`/`toCol` are + * the nodes' column indices (not pixel x) — routing decisions are made on + * column adjacency, not raw coordinates, so a diagram with uneven column + * widths still routes correctly: + * - same column: straight vertical hop within the column's own x. + * - adjacent column (|Δcol| === 1): a direct line, or a two-bend route + * through the empty gutter between the two columns when rows differ. + * - anywhere else (skips one or more columns, or any backward jump): a + * dedicated bus lane OUTSIDE every node's vertical extent (`laneY`, e.g. + * the empty top margin) so the line can never cut through an unrelated + * node sitting in an intermediate column. */ +function edgeViaPoints( + from: Pt, + to: Pt, + fromCol: number, + toCol: number, + boxW: number, + boxH: number, + colGap: number, + laneY: number, + desiredBendSegment?: number, + forceBusLane = false, +): Pt[] { + if (fromCol === toCol) { + const goingDown = to.y > from.y; + const sx = from.x + boxW / 2; + const sy = goingDown ? from.y + boxH : from.y; + const tx = to.x + boxW / 2; + const ty = goingDown ? to.y : to.y + boxH; + return [ + { x: sx, y: sy }, + { x: tx, y: ty }, + ]; + } + const colDelta = toCol - fromCol; + if (Math.abs(colDelta) === 1 && !forceBusLane) { + const forward = colDelta === 1; + const sx = forward ? from.x + boxW : from.x; + const sy = from.y + boxH / 2; + const tx = forward ? to.x : to.x + boxW; + const ty = to.y + boxH / 2; + if (sy === ty) { + return [ + { x: sx, y: sy }, + { x: tx, y: ty }, + ]; + } + // Bias the bend toward whichever segment needs to carry a label: by + // default split the gutter evenly, but grow the first (source-side) + // segment up to fit `desiredBendSegment` (a label's pill width) so a + // long label doesn't bleed back into the source node's own box. + const available = Math.abs(tx - sx); + const offset = Math.min(Math.max(desiredBendSegment ?? available / 2, 24), Math.max(available - 16, 24)); + const midX = sx + (forward ? offset : -offset); + return [ + { x: sx, y: sy }, + { x: midX, y: sy }, + { x: midX, y: ty }, + { x: tx, y: ty }, + ]; + } + // Bus lane: used for any column skip (forward or backward), so the line + // never passes through a node in a column between from and to — and also + // forced for an adjacent-column bend whose label pill can't fit in the + // gutter, so the label never bleeds into the source node's box. + const goingForward = colDelta > 0; + const sx = goingForward ? from.x + boxW : from.x; + const sy = from.y + boxH / 2; + const tx = goingForward ? to.x : to.x + boxW; + const ty = to.y + boxH / 2; + return [ + { x: sx, y: sy }, + { x: sx, y: laneY }, + { x: tx, y: laneY }, + { x: tx, y: ty }, + ]; +} + +// ---- render ----------------------------------------------------------- + +function esc(value: string): string { + return value + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function pillWidth(label: string): number { + return label.length * 5.6 + 14; +} + +/** Places the label pill at the midpoint of the path's LONGEST segment — the + * segment most likely to have room for it — rather than always the + * geometric middle via-point, so a long label on a route with one short + * bend (e.g. a tight adjacent-column jog) still prefers open space. */ +function renderEdgeLabel(label: string, pts: Pt[]): string { + // The pill is wide and horizontal, so a vertical segment's *length* isn't + // useful room for it — prefer the longest HORIZONTAL segment; only fall + // back to a vertical one if the route has no horizontal segment at all + // (e.g. a pure same-column vertical hop). + const segments: Array<{ from: Pt; to: Pt; len: number; horizontal: boolean }> = []; + for (let i = 0; i < pts.length - 1; i++) { + const from = pts[i]!; + const to = pts[i + 1]!; + segments.push({ from, to, len: distance(from, to), horizontal: from.y === to.y }); + } + const horizontals = segments.filter((s) => s.horizontal); + const pool = horizontals.length > 0 ? horizontals : segments; + const best = pool.reduce((a, b) => (b.len > a.len ? b : a)); + const mid = { x: (best.from.x + best.to.x) / 2, y: (best.from.y + best.to.y) / 2 }; + const w = pillWidth(label); + const h = 17; + return `<g class="arch-edge-label"><rect class="arch-edge-label-bg" x="${mid.x - w / 2}" y="${mid.y - h / 2}" width="${w}" height="${h}" rx="5"/><text class="arch-edge-label-text" x="${mid.x}" y="${mid.y + 3.5}" text-anchor="middle">${esc(label)}</text></g>`; +} + +/** + * Pure, deterministic renderer: the same spec always produces byte-identical + * SVG. Consistent node size, generous column/row gaps, vertically centered + * layers, orthogonal rounded edges, dashed-style support, zones rendered + * behind nodes as quiet containers. + */ +export function renderArchSvg(spec: ArchSpec, opts: RenderArchOptions = {}): string { + const nodes = spec.nodes; + const edges = spec.edges ?? []; + const zones = spec.zones ?? []; + const title = opts.title ?? spec.title ?? "Architecture"; + + const boxW = 224; + const boxH = 92; + const colGap = 140; + const rowGap = 36; + const margin = 72; + const iconTile = 40; + const iconSize = 22; + const cornerRadius = 12; + const edgeRadius = 6; + + const layer = layerArchNodes(nodes, edges); + const uniqueLayers = [...new Set(nodes.map((n) => layer.get(n.id) ?? 0))].sort((a, b) => a - b); + const colOf = new Map(uniqueLayers.map((l, i) => [l, i])); + const columns: ArchNode[][] = uniqueLayers.map(() => []); + for (const n of nodes) columns[colOf.get(layer.get(n.id) ?? 0)!]!.push(n); + + const numCols = columns.length; + const maxRows = Math.max(...columns.map((c) => c.length), 1); + const width = margin * 2 + numCols * boxW + Math.max(numCols - 1, 0) * colGap; + const height = margin * 2 + maxRows * boxH + Math.max(maxRows - 1, 0) * rowGap; + + const pos = new Map<string, Pt>(); + const nodeCol = new Map<string, number>(); + columns.forEach((col, ci) => { + const colHeight = col.length * boxH + Math.max(col.length - 1, 0) * rowGap; + const startY = margin + (height - margin * 2 - colHeight) / 2; + col.forEach((n, ri) => { + pos.set(n.id, { x: margin + ci * (boxW + colGap), y: startY + ri * (boxH + rowGap) }); + nodeCol.set(n.id, ci); + }); + }); + // A dedicated bus lane, safely above every node's vertical extent (nodes + // never start above `margin`), used only by edges that skip a column. + const busLaneY = margin / 2; + + const zonesSvg = zones + .map((z) => { + const members = nodes + .filter((n) => n.zone === z.id) + .map((n) => pos.get(n.id)) + .filter((p): p is Pt => !!p); + if (members.length === 0) return ""; + const pad = 22; + const labelH = 24; + const minX = Math.min(...members.map((p) => p.x)) - pad; + const minY = Math.min(...members.map((p) => p.y)) - pad - labelH; + const maxX = Math.max(...members.map((p) => p.x + boxW)) + pad; + const maxY = Math.max(...members.map((p) => p.y + boxH)) + pad; + return `<g class="arch-zone" data-id="${esc(z.id)}"><rect class="arch-zone-box" x="${minX}" y="${minY}" width="${maxX - minX}" height="${maxY - minY}" rx="16"/><text class="arch-zone-label" x="${minX + 14}" y="${minY + 17}">${esc(z.label)}</text></g>`; + }) + .join("\n"); + + let busLaneCount = 0; + const edgesSvg = edges + .map((e) => { + const from = pos.get(e.from); + const to = pos.get(e.to); + if (!from || !to) return ""; // defensive: never fail a build over a typo'd edge id + const fromCol = nodeCol.get(e.from) ?? 0; + const toCol = nodeCol.get(e.to) ?? 0; + // Stagger successive bus-lane edges by a few px so two column-skipping + // edges never trace the exact same horizontal line. + const colDelta = toCol - fromCol; + const columnSkip = fromCol !== toCol && Math.abs(colDelta) !== 1; + // A labeled adjacent-column bend needs its source-side segment to be at + // least as long as the label's pill, or the pill bleeds back into the + // source node's own box. If even the whole gutter can't fit it, fall + // back to the bus lane instead of letting it overlap a node. + const desiredBendSegment = e.label ? pillWidth(e.label) + 16 : undefined; + const bentAdjacent = Math.abs(colDelta) === 1 && from.y !== to.y; + const forceBusLane = bentAdjacent && !!desiredBendSegment && desiredBendSegment > colGap - 16; + const isBusEdge = columnSkip || forceBusLane; + const laneY = isBusEdge ? busLaneY - (busLaneCount++ % 3) * 9 : busLaneY; + const pts = edgeViaPoints(from, to, fromCol, toCol, boxW, boxH, colGap, laneY, desiredBendSegment, forceBusLane); + const d = roundedPolylinePath(pts, edgeRadius); + const dashClass = e.style === "dashed" ? " arch-edge-dashed" : ""; + const labelSvg = e.label ? renderEdgeLabel(e.label, pts) : ""; + return `<path class="arch-edge-line${dashClass}" d="${d}" marker-end="url(#arch-arrow-head)"/>${labelSvg}`; + }) + .join("\n"); + + const nodesSvg = nodes + .map((n) => { + const p = pos.get(n.id); + if (!p) return ""; + const iconId = (ICON_IDS as readonly string[]).includes(n.icon) ? n.icon : "doc"; + const tileX = p.x + 14; + const tileY = p.y + (boxH - iconTile) / 2; + const iconX = tileX + (iconTile - iconSize) / 2; + const iconY = tileY + (iconTile - iconSize) / 2; + const textX = tileX + iconTile + 14; + const labelY = n.sub ? p.y + boxH / 2 - 6 : p.y + boxH / 2 + 5; + const subY = labelY + 18; + return `<g class="arch-node" data-id="${esc(n.id)}"> + <rect class="arch-node-box" x="${p.x}" y="${p.y}" width="${boxW}" height="${boxH}" rx="${cornerRadius}"/> + <rect class="arch-node-icon-tile" x="${tileX}" y="${tileY}" width="${iconTile}" height="${iconTile}" rx="10"/> + <use class="arch-node-icon" href="#icon-${iconId}" x="${iconX}" y="${iconY}" width="${iconSize}" height="${iconSize}"/> + <text class="arch-node-label" x="${textX}" y="${labelY}">${esc(n.label)}</text> + ${n.sub ? `<text class="arch-node-sub" x="${textX}" y="${subY}">${esc(n.sub)}</text>` : ""} +</g>`; + }) + .join("\n"); + + const styleBlock = opts.embedded ? "" : `<style>${ARCH_CSS}</style>`; + + return `<svg class="arch-svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="${esc(title)} architecture diagram"> +<title>${esc(title)} +${styleBlock} +${iconSymbolDefs()} + + +${zonesSvg} +${edgesSvg} +${nodesSvg} +`; +} + +// ---- CLI: keyoku arch render [-o out.svg] ---------------------- + +function flagValue(argv: string[], flags: string[]): string | undefined { + for (const flag of flags) { + const index = argv.indexOf(flag); + if (index >= 0 && argv[index + 1] && !argv[index + 1]!.startsWith("-")) return argv[index + 1]; + } + return undefined; +} + +async function archRender(rest: string[]): Promise { + const specArg = rest.find((a) => !a.startsWith("-") && a !== flagValue(rest, ["-o", "--out"])); + if (!specArg) throw new Error("Usage: keyoku arch render [-o ]"); + const root = resolve(process.cwd()); + const specPath = isAbsolute(specArg) ? specArg : join(root, specArg); + if (!existsSync(specPath)) throw new Error(`Spec file not found: ${specPath}`); + + let raw: unknown; + try { + raw = parse(readFileSync(specPath, "utf8")); + } catch (error) { + throw new Error(`Cannot parse ${specPath}: ${error instanceof Error ? error.message : String(error)}`); + } + const result = ArchSpecSchema.safeParse(raw); + if (!result.success) { + throw new Error( + `Invalid ${specPath}: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, + ); + } + + const svg = renderArchSvg(result.data, { embedded: false }); + const outArg = flagValue(rest, ["-o", "--out"]); + const outPath = outArg ? (isAbsolute(outArg) ? outArg : join(root, outArg)) : join(root, "architecture.svg"); + mkdirSync(dirname(outPath), { recursive: true }); + writeFileSync(outPath, svg, "utf8"); + console.log( + `Rendered architecture diagram (${result.data.nodes.length} node(s), ${result.data.edges.length} edge(s)) -> ${relative(root, outPath) || outPath}`, + ); +} + +export async function archCmd(args: string[]): Promise { + const [sub, ...rest] = args; + if (sub === "render") return archRender(rest); + throw new Error("Usage: keyoku arch render [-o ]"); +} diff --git a/src/architecture.ts b/src/architecture.ts new file mode 100644 index 0000000..4c67f56 --- /dev/null +++ b/src/architecture.ts @@ -0,0 +1,204 @@ +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; + +import { parse } from "yaml"; +import { z } from "zod"; + +export type ArchitectureLayer = "source" | "experience" | "control" | "intelligence" | "execution" | "proof" | "state"; + +export interface ArchitectureComponent { + id: string; + label: string; + summary: string; + layer: ArchitectureLayer; + icon: string; + owns?: string[]; + external?: boolean; + view?: { x: number; y: number }; +} + +export interface ArchitectureRelation { + from: string; + to: string; + kind: string; +} + +export interface ArchitectureProjection { + schemaVersion: "keyoku.dev/architecture-projection/v1alpha1"; + projectId: string; + title: string; + generatedAt: string; + snapshotRef: string; + source: { kind: "declared+observed"; path: string }; + components: Array; + relations: ArchitectureRelation[]; + unownedChanges: string[]; +} + +const ArchitectureComponentSchema = z.object({ + id: z.string().min(1), + label: z.string().min(1), + summary: z.string().min(1), + layer: z.enum(["source", "experience", "control", "intelligence", "execution", "proof", "state"]), + icon: z.string().min(1), + owns: z.array(z.string()).optional(), + external: z.boolean().optional(), + view: z.object({ x: z.number(), y: z.number() }).strict().optional(), + observedFiles: z.number().int().nonnegative(), + changedFiles: z.array(z.string()), + state: z.enum(["external", "stable", "changing", "missing"]), +}).strict(); + +export const ArchitectureProjectionSchema = z.object({ + schemaVersion: z.literal("keyoku.dev/architecture-projection/v1alpha1"), + projectId: z.string().min(1), + title: z.string().min(1), + generatedAt: z.string().datetime(), + snapshotRef: z.string().min(1), + source: z.object({ kind: z.literal("declared+observed"), path: z.string().min(1) }).strict(), + components: z.array(ArchitectureComponentSchema), + relations: z.array(z.object({ from: z.string().min(1), to: z.string().min(1), kind: z.string().min(1) }).strict()), + unownedChanges: z.array(z.string()), +}).strict(); + +export interface ArchitectureProposal { + schemaVersion: "keyoku.dev/architecture-proposal/v1alpha1"; + id: string; + baseSnapshotRef: string; + summary: string; + rationale: string; + operations: Array<{ op: "add" | "update" | "remove"; target: string; value?: unknown }>; + actor: { id: string; name: string; harness?: string; model?: string }; + confidence: number; + createdAt: string; + status: "proposed"; +} + +interface ArchitectureDocument { + projectId: string; + title: string; + components: ArchitectureComponent[]; + relations: ArchitectureRelation[]; +} + +function git(root: string, args: string[], fallback = "unknown"): string { + try { + return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || fallback; + } catch { + return fallback; + } +} + +function filesUnder(root: string, entry: string): string[] { + const absolute = join(root, entry); + if (!existsSync(absolute)) return []; + if (!statSync(absolute).isDirectory()) return [entry]; + const files: string[] = []; + const visit = (directory: string) => { + for (const child of readdirSync(directory, { withFileTypes: true })) { + if (["node_modules", ".git", "dist"].includes(child.name)) continue; + const path = join(directory, child.name); + if (child.isDirectory()) visit(path); + else files.push(relative(root, path)); + } + }; + visit(absolute); + return files; +} + +export function scanArchitecture(root: string): ArchitectureProjection { + const architecturePath = join(root, ".keyoku", "architecture.yaml"); + if (!existsSync(architecturePath)) throw new Error("No .keyoku/architecture.yaml architecture contract exists."); + const document = parse(readFileSync(architecturePath, "utf8")) as ArchitectureDocument; + const status = git(root, ["status", "--porcelain=v1"], ""); + const changed = status ? status.split("\n").filter(Boolean).map((line) => line.slice(3)) : []; + const owned = new Set(); + const components = document.components.map((component) => { + const files = (component.owns || []).flatMap((entry) => filesUnder(root, entry)); + files.forEach((file) => owned.add(file)); + const changedFiles = changed.filter((file) => files.includes(file) || (component.owns || []).some((entry) => file === entry || file.startsWith(`${entry}/`))); + return { + ...component, + observedFiles: files.length, + changedFiles, + state: component.external ? "external" as const : files.length === 0 ? "missing" as const : changedFiles.length ? "changing" as const : "stable" as const, + }; + }); + const snapshotInput = JSON.stringify({ head: git(root, ["rev-parse", "HEAD"]), status, document }); + return { + schemaVersion: "keyoku.dev/architecture-projection/v1alpha1", + projectId: document.projectId, + title: document.title, + generatedAt: new Date().toISOString(), + snapshotRef: createHash("sha256").update(snapshotInput).digest("hex").slice(0, 16), + source: { kind: "declared+observed", path: ".keyoku/architecture.yaml" }, + components, + relations: document.relations, + unownedChanges: changed.filter((file) => !owned.has(file) && ![...owned].some((entry) => file.startsWith(`${entry}/`))), + }; +} + +function xml(value: unknown): string { + return String(value).replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]!); +} + +export function renderArchitectureSvg(projection: ArchitectureProjection): string { + const width = 1200; + const height = 680; + const nodeWidth = 190; + const nodeHeight = 112; + const nodes = new Map(projection.components.map((component) => [component.id, component])); + const edge = (relation: ArchitectureRelation, index: number) => { + const from = nodes.get(relation.from); const to = nodes.get(relation.to); + if (!from?.view || !to?.view) return ""; + const x1 = from.view.x + nodeWidth; const y1 = from.view.y + nodeHeight / 2; + const x2 = to.view.x; const y2 = to.view.y + nodeHeight / 2; + const lift = 20 + (index % 4) * 10; + const direction = x2 >= x1 ? 1 : -1; + const c1 = x1 + direction * Math.max(45, Math.abs(x2 - x1) * .38); + const c2 = x2 - direction * Math.max(45, Math.abs(x2 - x1) * .38); + return ``; + }; + const node = (component: ArchitectureProjection["components"][number]) => { + if (!component.view) return ""; + const state = component.state === "changing" ? "#06b6d4" : component.state === "missing" ? "#ef4444" : component.state === "external" ? "#8b5cf6" : "#6366f1"; + const mark = component.icon === "database" ? "DB" : component.icon === "keyoku" ? "K" : component.icon === "git" ? "GIT" : component.icon === "mcp" ? "MCP" : component.icon === "agent" ? "AI" : component.icon.slice(0, 2).toUpperCase(); + const detail = component.external ? "external boundary" : `${component.observedFiles} files${component.changedFiles.length ? ` · ${component.changedFiles.length} changing` : ""}`; + return `${xml(mark)}${xml(component.label)}${xml(detail)}${xml(component.layer)}`; + }; + return `${xml(projection.title)}Architecture projection for ${xml(projection.projectId)} at snapshot ${xml(projection.snapshotRef)}.${xml(projection.title)}snapshot ${xml(projection.snapshotRef)} · ${xml(projection.source.kind)}${projection.relations.map(edge).join("")}${projection.components.map(node).join("")}Observed files + declared semantic structure · generated ${xml(projection.generatedAt)}`; +} + +export function proposeArchitectureChange(input: { + root: string; + summary: string; + rationale: string; + operations: ArchitectureProposal["operations"]; + actor: ArchitectureProposal["actor"]; + confidence: number; +}): ArchitectureProposal { + const projection = scanArchitecture(input.root); + const proposal: ArchitectureProposal = { + schemaVersion: "keyoku.dev/architecture-proposal/v1alpha1", + id: `arch_${Date.now().toString(36)}_${createHash("sha256").update(`${input.actor.id}:${input.summary}:${Date.now()}`).digest("hex").slice(0, 10)}`, + baseSnapshotRef: projection.snapshotRef, + summary: input.summary.trim(), + rationale: input.rationale.trim(), + operations: input.operations, + actor: input.actor, + confidence: Math.max(0, Math.min(input.confidence, 1)), + createdAt: new Date().toISOString(), + status: "proposed", + }; + if (!proposal.summary || !proposal.rationale || proposal.operations.length === 0) throw new Error("summary, rationale, and at least one operation are required"); + const path = join(input.root, ".keyoku", "runtime", "architecture-proposals.jsonl"); + mkdirSync(dirname(path), { recursive: true }); + appendFileSync(path, `${JSON.stringify(proposal)}\n`, { encoding: "utf8", mode: 0o600 }); + return proposal; +} diff --git a/src/artifact-safety.ts b/src/artifact-safety.ts new file mode 100644 index 0000000..e76627c --- /dev/null +++ b/src/artifact-safety.ts @@ -0,0 +1,52 @@ +import { existsSync, readFileSync, realpathSync, statSync } from "node:fs"; +import { isAbsolute, relative, resolve } from "node:path"; + +export interface BoundedArtifact { + absolutePath: string; + relativePath: string; + bytes: Buffer; +} + +/** Read bytes only after lexical and realpath containment both pass. */ +export function readBoundedArtifact(rootInput: string, pathInput: string): BoundedArtifact { + const root = resolve(rootInput); + const absolutePath = resolve(root, pathInput); + const relativePath = relative(root, absolutePath); + if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) { + throw new Error(`Artifact path is outside the verified root: ${pathInput}`); + } + if (!existsSync(absolutePath) || !statSync(absolutePath).isFile()) { + throw new Error(`Artifact was not found: ${relativePath}`); + } + const canonicalRoot = realpathSync(root); + const canonicalArtifact = realpathSync(absolutePath); + const expectedCanonicalPath = resolve(canonicalRoot, relativePath); + if (canonicalArtifact !== expectedCanonicalPath) { + throw new Error(`Artifact path traverses a symbolic link: ${relativePath}`); + } + const canonicalRelative = relative(canonicalRoot, canonicalArtifact); + if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute(canonicalRelative)) { + throw new Error(`Artifact realpath escapes the verified root: ${relativePath}`); + } + return { absolutePath, relativePath, bytes: readFileSync(canonicalArtifact) }; +} + +export type PortableMediaType = "image/png" | "image/webp" | "image/jpeg" | "video/mp4" | "video/webm"; + +export function mediaTypeForPath(path: string): PortableMediaType | undefined { + const lower = path.toLowerCase(); + if (lower.endsWith(".png")) return "image/png"; + if (lower.endsWith(".webp")) return "image/webp"; + if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; + if (lower.endsWith(".mp4")) return "video/mp4"; + if (lower.endsWith(".webm")) return "video/webm"; + return undefined; +} + +export function mediaSignatureMatches(bytes: Buffer, mediaType: PortableMediaType): boolean { + if (mediaType === "image/png") return bytes.length >= 8 && bytes.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])); + if (mediaType === "image/jpeg") return bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff; + if (mediaType === "image/webp") return bytes.length >= 12 && bytes.subarray(0, 4).toString("ascii") === "RIFF" && bytes.subarray(8, 12).toString("ascii") === "WEBP"; + if (mediaType === "video/mp4") return bytes.length >= 12 && bytes.subarray(4, 8).toString("ascii") === "ftyp"; + return bytes.length >= 4 && bytes.subarray(0, 4).equals(Buffer.from([0x1a, 0x45, 0xdf, 0xa3])); +} diff --git a/src/assurance-adapter.ts b/src/assurance-adapter.ts new file mode 100644 index 0000000..c65c661 --- /dev/null +++ b/src/assurance-adapter.ts @@ -0,0 +1,255 @@ +import { z } from "zod"; + +import { canonicalJsonDigest, decodeUtf8Strict } from "./canonical-json.js"; +import { readLocalLedger, resolveLocalLedger, updateLocalLedger } from "./local-ledger.js"; + +const DigestSchema = z.string().regex(/^[a-f0-9]{64}$/, "must be a lowercase sha256 digest"); +const IdSchema = z.string().min(1).max(240).regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/); + +/** + * These profiles are caller policy, not part of the evidence envelope and not + * a requirement imposed on an agent runtime or neutral work protocol. + */ +export const AssuranceProfileSchema = z.enum(["none", "basic", "keyoku_high_assurance"]); +export type AssuranceProfile = z.infer; + +const SnapshotSchema = z.object({ + capturedDigest: DigestSchema, + currentDigest: DigestSchema.optional(), + label: z.string().min(1).max(1_000).optional(), +}).strict(); + +const EvidenceEnvelopeFields = z.object({ + schemaVersion: z.literal("evidence-provider/v1"), + work: z.object({ + id: IdSchema, + objective: z.string().min(1).max(8_000), + }).strict(), + claims: z.array(z.object({ + id: IdSchema, + statement: z.string().min(1).max(8_000), + verdict: z.enum(["pass", "fail", "pending"]), + evidenceRefs: z.array(IdSchema).max(100).default([]), + }).strict()).min(1).max(500), + source: SnapshotSchema.optional(), + deployment: SnapshotSchema.optional(), + commands: z.array(z.object({ + id: IdSchema, + command: z.string().min(1).max(16_000), + exitCode: z.number().int(), + resultDigest: DigestSchema, + }).strict()).max(500).default([]), + artifacts: z.array(z.object({ + id: IdSchema, + path: z.string().min(1).max(4_000), + digest: DigestSchema, + }).strict()).max(500).default([]), + limitations: z.array(z.string().min(1).max(8_000)).max(500).default([]), + authority: z.object({ + kind: z.enum(["human", "organization", "automation"]), + id: IdSchema, + decision: z.enum(["approved", "pending", "rejected"]), + }).strict(), + contentDigest: DigestSchema, +}).strict(); + +export const EvidenceEnvelopeSchema = EvidenceEnvelopeFields; +export type EvidenceEnvelope = z.infer; + +type UnsignedEvidenceEnvelope = Omit; + +export function sealEvidenceEnvelope(input: UnsignedEvidenceEnvelope): EvidenceEnvelope { + const { contentDigest: _contentDigest, ...candidate } = input as UnsignedEvidenceEnvelope & { contentDigest?: string }; + const unsigned = EvidenceEnvelopeFields.omit({ contentDigest: true }).parse(candidate); + return EvidenceEnvelopeSchema.parse({ ...unsigned, contentDigest: canonicalJsonDigest(unsigned) }); +} + +export const EvidenceReasonCodeSchema = z.enum([ + "invalid_envelope", + "content_digest_mismatch", + "source_changed", + "deployment_changed", + "claim_failed", + "command_failed", + "authority_rejected", + "claim_pending", + "authority_pending", + "evidence_accepted", +]); +export type EvidenceReasonCode = z.infer; + +export const EvidenceProviderStatusSchema = z.enum(["accepted", "rejected", "stale", "human_review_required"]); +export type EvidenceProviderStatus = z.infer; + +const EvidenceResultFields = z.object({ + schemaVersion: z.literal("evidence-result/v1"), + status: EvidenceProviderStatusSchema, + workId: IdSchema.nullable(), + inputDigest: DigestSchema.nullable(), + computedContentDigest: DigestSchema.nullable(), + reasons: z.array(z.object({ + code: EvidenceReasonCodeSchema, + message: z.string().min(1).max(8_000), + path: z.string().max(2_000).optional(), + }).strict()).min(1), + resultDigest: DigestSchema, +}).strict(); + +export const EvidenceProviderResultSchema = EvidenceResultFields.superRefine((result, context) => { + const { resultDigest: _resultDigest, ...unsigned } = result; + const expected = canonicalJsonDigest(unsigned); + if (result.resultDigest !== expected) { + context.addIssue({ code: z.ZodIssueCode.custom, path: ["resultDigest"], message: `does not match result content (expected ${expected})` }); + } +}); +export type EvidenceProviderResult = z.infer; + +export interface EvidenceProvider { + evaluate(input: unknown): EvidenceProviderResult; +} + +function sealEvidenceResult(input: Omit): EvidenceProviderResult { + return EvidenceProviderResultSchema.parse({ ...input, resultDigest: canonicalJsonDigest(input) }); +} + +function invalidEnvelope(input: unknown, error: z.ZodError): EvidenceProviderResult { + let computedContentDigest: string | null = null; + try { + if (input && typeof input === "object" && !Array.isArray(input)) { + const { contentDigest: _contentDigest, ...unsigned } = input as Record; + computedContentDigest = canonicalJsonDigest(unsigned); + } + } catch { + computedContentDigest = null; + } + const record = input && typeof input === "object" && !Array.isArray(input) ? input as Record : undefined; + const work = record?.work && typeof record.work === "object" && !Array.isArray(record.work) ? record.work as Record : undefined; + return sealEvidenceResult({ + schemaVersion: "evidence-result/v1", + status: "rejected", + workId: typeof work?.id === "string" && IdSchema.safeParse(work.id).success ? work.id : null, + inputDigest: typeof record?.contentDigest === "string" && DigestSchema.safeParse(record.contentDigest).success ? record.contentDigest : null, + computedContentDigest, + reasons: error.issues.map((issue) => ({ + code: "invalid_envelope" as const, + message: issue.message, + ...(issue.path.length ? { path: issue.path.join(".") } : {}), + })), + }); +} + +/** Deterministic, side-effect-free evaluation of a caller-owned evidence envelope. */ +export function evaluateEvidence(input: unknown): EvidenceProviderResult { + const parsed = EvidenceEnvelopeSchema.safeParse(input); + if (!parsed.success) return invalidEnvelope(input, parsed.error); + const envelope = parsed.data; + const { contentDigest, ...unsigned } = envelope; + const computedContentDigest = canonicalJsonDigest(unsigned); + const base = { + schemaVersion: "evidence-result/v1" as const, + workId: envelope.work.id, + inputDigest: contentDigest, + computedContentDigest, + }; + if (contentDigest !== computedContentDigest) { + return sealEvidenceResult({ ...base, status: "rejected", reasons: [{ code: "content_digest_mismatch", message: "The canonical content digest does not match the submitted evidence envelope.", path: "contentDigest" }] }); + } + const staleReasons = [ + ...(envelope.source?.currentDigest && envelope.source.currentDigest !== envelope.source.capturedDigest + ? [{ code: "source_changed" as const, message: "The current source snapshot differs from the captured source snapshot.", path: "source.currentDigest" }] + : []), + ...(envelope.deployment?.currentDigest && envelope.deployment.currentDigest !== envelope.deployment.capturedDigest + ? [{ code: "deployment_changed" as const, message: "The current deployment snapshot differs from the captured deployment snapshot.", path: "deployment.currentDigest" }] + : []), + ]; + if (staleReasons.length) return sealEvidenceResult({ ...base, status: "stale", reasons: staleReasons }); + + const rejectionReasons = [ + ...envelope.claims.filter((claim) => claim.verdict === "fail").map((claim) => ({ code: "claim_failed" as const, message: `Claim '${claim.id}' failed.`, path: `claims.${claim.id}` })), + ...envelope.commands.filter((command) => command.exitCode !== 0).map((command) => ({ code: "command_failed" as const, message: `Command '${command.id}' exited with code ${command.exitCode}.`, path: `commands.${command.id}` })), + ...(envelope.authority.decision === "rejected" ? [{ code: "authority_rejected" as const, message: `Authority '${envelope.authority.id}' rejected the evidence.`, path: "authority.decision" }] : []), + ]; + if (rejectionReasons.length) return sealEvidenceResult({ ...base, status: "rejected", reasons: rejectionReasons }); + + const reviewReasons = [ + ...envelope.claims.filter((claim) => claim.verdict === "pending").map((claim) => ({ code: "claim_pending" as const, message: `Claim '${claim.id}' still needs a decision.`, path: `claims.${claim.id}` })), + ...(envelope.authority.decision === "pending" ? [{ code: "authority_pending" as const, message: `Authority '${envelope.authority.id}' has not approved the evidence.`, path: "authority.decision" }] : []), + ]; + if (reviewReasons.length) return sealEvidenceResult({ ...base, status: "human_review_required", reasons: reviewReasons }); + return sealEvidenceResult({ ...base, status: "accepted", reasons: [{ code: "evidence_accepted", message: "All submitted claims and commands passed for the current snapshots, and the declared authority approved the evidence." }] }); +} + +export const defaultEvidenceProvider: EvidenceProvider = Object.freeze({ evaluate: evaluateEvidence }); + +export const WorkEventKindSchema = z.enum(["dispatch", "checkpoint", "milestone", "decision", "regression", "recovery", "stale", "terminal"]); +export type WorkEventKind = z.infer; + +const WorkEventFields = z.object({ + schemaVersion: z.literal("work-event/v1"), + id: IdSchema, + kind: WorkEventKindSchema, + at: z.string().datetime(), + workId: IdSchema, + summary: z.string().min(1).max(8_000), + outcome: z.string().min(1).max(4_000).optional(), + checkpointId: IdSchema.optional(), + decisionId: IdSchema.optional(), + sourceDigest: DigestSchema.optional(), + evidenceResultDigest: DigestSchema.optional(), + limitations: z.array(z.string().min(1).max(8_000)).max(100).default([]), + eventDigest: DigestSchema, +}).strict(); + +export const WorkEventSchema = WorkEventFields.superRefine((event, context) => { + const { eventDigest: _eventDigest, ...unsigned } = event; + const expected = canonicalJsonDigest(unsigned); + if (event.eventDigest !== expected) { + context.addIssue({ code: z.ZodIssueCode.custom, path: ["eventDigest"], message: `does not match event content (expected ${expected})` }); + } +}); +export type WorkEvent = z.infer; + +export function sealWorkEvent(input: Omit): WorkEvent { + const { eventDigest: _eventDigest, ...candidate } = input as Omit & { eventDigest?: string }; + const unsigned = WorkEventFields.omit({ eventDigest: true }).parse(candidate); + return WorkEventSchema.parse({ ...unsigned, eventDigest: canonicalJsonDigest(unsigned) }); +} + +function workEventsPath(rootInput: string, create = false): string { + return resolveLocalLedger(rootInput, "work-events.jsonl", create); +} + +export function readWorkEvents(rootInput: string): WorkEvent[] { + const path = workEventsPath(rootInput); + return decodeUtf8Strict(readLocalLedger(path), `WorkEvent ledger ${path}`).split("\n").filter(Boolean).map((line, index) => { + try { return WorkEventSchema.parse(JSON.parse(line)); } + catch (error) { throw new Error(`Invalid WorkEvent at ${path}:${index + 1}: ${error instanceof Error ? error.message : String(error)}`); } + }); +} + +export interface WorkEventAppendResult { + status: "appended" | "deduplicated"; + event: WorkEvent; + path: string; +} + +export function appendWorkEvent(rootInput: string, input: WorkEvent | Record): WorkEventAppendResult { + const event = WorkEventSchema.parse(input); + const path = workEventsPath(rootInput, true); + let result: WorkEventAppendResult | undefined; + updateLocalLedger(path, (current) => { + const events = decodeUtf8Strict(current, `WorkEvent ledger ${path}`).split("\n").filter(Boolean).map((line, index) => { + try { return WorkEventSchema.parse(JSON.parse(line)); } + catch (error) { throw new Error(`Invalid WorkEvent at ${path}:${index + 1}: ${error instanceof Error ? error.message : String(error)}`); } + }); + const existing = events.find((candidate) => candidate.id === event.id); + if (existing) { + if (existing.eventDigest !== event.eventDigest) throw new Error(`WorkEvent id '${event.id}' already exists with different content.`); + result = { status: "deduplicated", event: existing, path }; + return current; + } + result = { status: "appended", event, path }; + return Buffer.concat([current, Buffer.from(`${JSON.stringify(event)}\n`, "utf8")]); + }); + return result!; +} diff --git a/src/canonical-json.ts b/src/canonical-json.ts new file mode 100644 index 0000000..a2ae31d --- /dev/null +++ b/src/canonical-json.ts @@ -0,0 +1,140 @@ +import { createHash } from "node:crypto"; + +/** Go encoding/json-compatible map-key order: lexicographic UTF-8 bytes. */ +export function compareUtf8Keys(left: string, right: string): number { + return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8")); +} + +function encodeCanonical(value: unknown, arrayItem: boolean): string | undefined { + if (value === undefined || typeof value === "function" || typeof value === "symbol") { + return arrayItem ? "null" : undefined; + } + if (value === null || typeof value === "boolean" || typeof value === "string") return JSON.stringify(value); + if (typeof value === "number") return Number.isFinite(value) ? JSON.stringify(value) : "null"; + if (typeof value === "bigint") throw new Error("Canonical JSON cannot encode bigint values."); + if (Array.isArray(value)) return `[${value.map((item) => encodeCanonical(item, true) ?? "null").join(",")}]`; + if (typeof value === "object") { + const entries = Object.entries(value as Record) + .filter(([, item]) => encodeCanonical(item, false) !== undefined) + .sort(([left], [right]) => compareUtf8Keys(left, right)); + return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${encodeCanonical(item, false)}`).join(",")}}`; + } + throw new Error(`Canonical JSON cannot encode ${typeof value}.`); +} + +export function canonicalJson(value: unknown): string { + const encoded = encodeCanonical(value, false); + if (encoded === undefined) throw new Error("Canonical JSON root cannot be undefined."); + return encoded; +} + +export function canonicalJsonDigest(value: unknown): string { + return createHash("sha256").update(canonicalJson(value)).digest("hex"); +} + +export function bytesDigest(value: Uint8Array): string { + return createHash("sha256").update(value).digest("hex"); +} + +export function decodeUtf8Strict(value: Uint8Array, label = "UTF-8 input"): string { + try { return new TextDecoder("utf-8", { fatal: true }).decode(value); } + catch { throw new Error(`${label}: invalid UTF-8 byte sequence.`); } +} + +/** Strict JSON parser that rejects duplicate decoded object keys. */ +export function parseJsonRejectDuplicateKeys(text: string, label = "JSON"): unknown { + let cursor = 0; + const fail = (message: string): never => { throw new Error(`${label}: ${message} at byte ${Buffer.byteLength(text.slice(0, cursor), "utf8")}.`); }; + const whitespace = () => { while (cursor < text.length && /[\u0020\u000a\u000d\u0009]/.test(text[cursor]!)) cursor += 1; }; + const string = (): string => { + if (text[cursor] !== '"') fail("expected string"); + const start = cursor; + cursor += 1; + while (cursor < text.length) { + const character = text[cursor]!; + if (character === '"') { + cursor += 1; + const token = text.slice(start, cursor); + if (/\\u[dD][89a-fA-F][0-9a-fA-F]{2}/u.test(token)) fail("escaped UTF-16 surrogate forms are not permitted"); + let decoded: string; + try { decoded = JSON.parse(token) as string; } + catch { return fail("invalid string escape"); } + for (let index = 0; index < decoded.length; index += 1) { + const code = decoded.charCodeAt(index); + if (code >= 0xd800 && code <= 0xdbff) { + const low = decoded.charCodeAt(index + 1); + if (low < 0xdc00 || low > 0xdfff) fail("unpaired UTF-16 surrogate is not permitted"); + index += 1; + } else if (code >= 0xdc00 && code <= 0xdfff) fail("unpaired UTF-16 surrogate is not permitted"); + } + return decoded; + } + if (character === "\\") { + cursor += 2; + continue; + } + if (character.charCodeAt(0) < 0x20) fail("unescaped control character"); + cursor += 1; + } + return fail("unterminated string"); + }; + const value = (): unknown => { + whitespace(); + const character = text[cursor]; + if (character === '"') return string(); + if (character === "{") { + cursor += 1; + whitespace(); + const object = Object.create(null) as Record; + const keys = new Set(); + if (text[cursor] === "}") { cursor += 1; return object; } + for (;;) { + whitespace(); + const key = string(); + if (keys.has(key)) fail(`duplicate object key ${JSON.stringify(key)}`); + keys.add(key); + whitespace(); + if (text[cursor] !== ":") fail("expected ':' after object key"); + cursor += 1; + object[key] = value(); + whitespace(); + if (text[cursor] === "}") { cursor += 1; return object; } + if (text[cursor] !== ",") fail("expected ',' or '}'"); + cursor += 1; + } + } + if (character === "[") { + cursor += 1; + whitespace(); + const array: unknown[] = []; + if (text[cursor] === "]") { cursor += 1; return array; } + for (;;) { + array.push(value()); + whitespace(); + if (text[cursor] === "]") { cursor += 1; return array; } + if (text[cursor] !== ",") fail("expected ',' or ']'"); + cursor += 1; + } + } + const rest = text.slice(cursor); + if (rest.startsWith("true")) { cursor += 4; return true; } + if (rest.startsWith("false")) { cursor += 5; return false; } + if (rest.startsWith("null")) { cursor += 4; return null; } + const number = rest.match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u)?.[0]; + if (number) { + cursor += number.length; + const parsed = Number(number); + if (!Number.isFinite(parsed)) fail("number is outside the finite JSON range"); + return parsed; + } + fail("expected JSON value"); + }; + const parsed = value(); + whitespace(); + if (cursor !== text.length) fail("unexpected trailing content"); + return parsed; +} + +export function parseJsonBytesRejectDuplicateKeys(value: Uint8Array, label = "JSON"): unknown { + return parseJsonRejectDuplicateKeys(decodeUtf8Strict(value, label), label); +} diff --git a/src/contribution.ts b/src/contribution.ts new file mode 100644 index 0000000..cf511c2 --- /dev/null +++ b/src/contribution.ts @@ -0,0 +1,2100 @@ +import { createHash, randomUUID } from "node:crypto"; +import { execFileSync } from "node:child_process"; +import { + closeSync, + constants, + existsSync, + fsyncSync, + mkdirSync, + openSync, + readFileSync, + readdirSync, + renameSync, + unlinkSync, + writeFileSync, + writeSync, +} from "node:fs"; +import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"; +import { parse, stringify } from "yaml"; +import { z } from "zod"; + +import { ArchitectureProjectionSchema, renderArchitectureSvg, scanArchitecture, type ArchitectureProjection } from "./architecture.js"; +import { mediaSignatureMatches, mediaTypeForPath, readBoundedArtifact } from "./artifact-safety.js"; +import { evaluateAssertion } from "./assert.js"; +import { canonicalJson, canonicalJsonDigest, parseJsonBytesRejectDuplicateKeys, parseJsonRejectDuplicateKeys } from "./canonical-json.js"; +import { runProbe } from "./probes.js"; +import { ProofSessionStateSchema, readProofSession, type ProofSessionState } from "./proof-session.js"; +import { readLocalLedger, resolvePrivateDirectory, updateLocalLedger } from "./local-ledger.js"; +import { redactSecrets } from "./redaction.js"; +import { + assertOriginalSourceUnchanged, + captureSourceTreeDigest, + createSourceCapsule, + disposeSourceCapsule, + runCommandInSourceCapsule, + watchOriginalSource, + withSourceCapsuleCheckout, + type MutationMonitor, + type SourceCapsule, +} from "./source-capsule.js"; +import { + AssertOpSchema, + CriterionInputSchema, + type ConvergenceReport, + type CriterionEvaluation, + type CriterionInput, + type Probe, + type ProbeEnvelope, +} from "./types.js"; + +export const KEYOKU_DIR = ".keyoku"; +export const PROJECT_FILE = "project.yaml"; + +const SlugSchema = z + .string() + .min(1) + .regex(/^[a-z0-9][a-z0-9._-]*$/, "must be lowercase letters, numbers, dots, dashes, or underscores"); +const DigestSchema = z.string().regex(/^[a-f0-9]{64}$/, "must be a sha256 digest"); + +export const ActorSchema = z.object({ + kind: z.enum(["human", "agent", "organization"]), + id: z.string().min(1), + name: z.string().min(1), + role: z.string().optional(), + ownerId: z.string().optional(), + harness: z.string().optional(), + model: z.string().optional(), +}).strict(); + +export const ProjectManifestSchema = z.object({ + schemaVersion: z.literal("keyoku.dev/project/v1alpha1"), + id: SlugSchema, + name: z.string().min(1), + summary: z.string().min(1), + repository: z.string().optional(), + defaultBranch: z.string().optional(), + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}).strict(); + +const EvidencePresentationSchema = z.object({ + summary: z.string().min(1), + whyItMatters: z.string().min(1), + code: z.array(z.object({ + path: z.string().min(1), + purpose: z.string().min(1), + }).strict()).default([]), + artifacts: z.array(z.object({ + kind: z.enum(["screenshot", "video", "trace", "report", "log", "code"]), + path: z.string().min(1), + label: z.string().min(1), + caption: z.string().min(1), + annotations: z.array(z.object({ + label: z.string().min(1), + detail: z.string().optional(), + x: z.number().min(0).max(100).optional(), + y: z.number().min(0).max(100).optional(), + width: z.number().positive().max(100).optional(), + height: z.number().positive().max(100).optional(), + atMs: z.number().int().nonnegative().optional(), + }).strict()).default([]), + }).strict()).default([]), +}).strict(); + +const OutcomeCriterionSchema = CriterionInputSchema.extend({ + evidence: EvidencePresentationSchema.optional(), +}); + +export const OutcomeSchema = z.object({ + schemaVersion: z.literal("keyoku.dev/outcome/v1alpha1"), + id: SlugSchema, + revision: z.number().int().positive(), + title: z.string().min(1), + objective: z.string().min(1), + owner: ActorSchema, + constraints: z.array(z.string()), + scope: z.object({ + include: z.array(z.string().min(1)).default([]), + exclude: z.array(z.string().min(1)).default([]), + maxChangedFiles: z.number().int().positive().optional(), + }).strict().optional(), + criteria: z.array(OutcomeCriterionSchema).min(1), + humanCriteria: z.array(z.object({ + id: SlugSchema, + description: z.string().min(1), + guidance: z.string().optional(), + }).strict()).default([]), + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}).strict(); + +export const ContributionManifestSchema = z.object({ + schemaVersion: z.literal("keyoku.dev/contribution/v1alpha1"), + id: SlugSchema, + title: z.string().min(1), + summary: z.string().min(1).optional(), + knownLimits: z.array(z.string().min(1)).optional(), + outcomeId: SlugSchema, + outcomeRevision: z.number().int().positive(), + outcomeDigest: z.string().regex(/^[a-f0-9]{64}$/).optional(), + baseSha: z.string().min(1), + actors: z.array(ActorSchema).min(1), + status: z.enum(["draft", "evaluating", "evidence_gaps", "human_review_required", "review_blocked", "ready_for_review", "accepted"]), + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}).strict(); + +export const ReviewEventSchema = z.object({ + id: SlugSchema, + decision: z.enum(["note", "accepted"]), + reviewer: ActorSchema.refine((actor) => actor.kind === "human", "reviewer must be a human"), + comment: z.string().min(1), + criterionId: SlugSchema.optional(), + verdict: z.enum(["pass", "fail"]).optional(), + reviewedAt: z.string().datetime(), + factfileId: SlugSchema, + factfileDigest: z.string().regex(/^[a-f0-9]{64}$/), + repository: z.object({ + headSha: z.string().min(1), + worktreeDigest: z.string().regex(/^[a-f0-9]{64}$/), + }).strict(), +}).strict().refine((event) => Boolean(event.criterionId) === Boolean(event.verdict), { + message: "criterionId and verdict must be supplied together", +}); + +export type Actor = z.infer; +export type ProjectManifest = z.infer; +export type Outcome = z.infer; +export type ContributionManifest = z.infer; +export type ReviewEvent = z.infer; +export type EvidencePresentation = z.infer; + +export interface OutcomeHistoryEntry { + sha: string; + authoredAt: string; + author: string; + subject: string; + revision?: number; +} + +export interface ResolvedEvidencePresentation extends Omit { + artifacts: Array; +} + +export interface VerificationMethod { + kind: "command" | "http" | "mcp"; + label: string; + reproduce: string; + assertion: ConvergenceReport["criteria"][number]["expected"]; +} + +export interface RepositorySnapshot { + repositoryRoot: string; + branch: string; + upstream?: string; + ahead: number; + behind: number; + remote?: string; + lastCommit: string; + baseSha: string; + headSha: string; + worktreeDigest: string; + sourceCapsuleDigest: string; + dirty: boolean; + changedFiles: string[]; +} + +export interface ScopeAssessment { + declared: boolean; + passed: boolean; + includedPaths: string[]; + unexpectedPaths: string[]; + excludedPaths: string[]; + maxChangedFiles?: number; + topLevelAreas: Array<{ name: string; files: number }>; + note: string; +} + +export interface ReviewAttentionItem { + priority: "critical" | "high" | "normal"; + title: string; + why: string; + paths: string[]; + basis: "deterministic" | "declared"; +} + +export interface GateSnapshot { + schemaVersion: "keyoku.dev/factfile/v1alpha1"; + id: string; + project: Pick; + outcome: Pick; + contribution: ContributionManifest; + repository: RepositorySnapshot; + scope: ScopeAssessment; + reviewPlan: ReviewAttentionItem[]; + session: ProofSessionState; + architecture?: ArchitectureProjection; + state: "evidence_gaps" | "human_review_required" | "review_blocked" | "ready_for_review" | "accepted"; + generatedAt: string; + reviews: ReviewEvent[]; + evidence: Array; + summary: { + passed: number; + failed: number; + total: number; + verified: boolean; + }; + humanReview: { + passed: number; + failed: number; + pending: number; + total: number; + }; + digest: string; +} + +const RepositorySnapshotSchema = z.object({ + repositoryRoot: z.string().min(1), + branch: z.string().min(1), + upstream: z.string().optional(), + ahead: z.number().int().nonnegative(), + behind: z.number().int().nonnegative(), + remote: z.string().optional(), + lastCommit: z.string(), + baseSha: z.string().min(1), + headSha: z.string().min(1), + worktreeDigest: DigestSchema, + sourceCapsuleDigest: DigestSchema, + dirty: z.boolean(), + changedFiles: z.array(z.string()), +}).strict().superRefine((repository, context) => { + if (repository.sourceCapsuleDigest !== repository.worktreeDigest) { + context.addIssue({ code: z.ZodIssueCode.custom, path: ["sourceCapsuleDigest"], message: "must equal worktreeDigest" }); + } +}); + +const ScopeAssessmentSchema = z.object({ + declared: z.boolean(), + passed: z.boolean(), + includedPaths: z.array(z.string()), + unexpectedPaths: z.array(z.string()), + excludedPaths: z.array(z.string()), + maxChangedFiles: z.number().int().positive().optional(), + topLevelAreas: z.array(z.object({ name: z.string().min(1), files: z.number().int().nonnegative() }).strict()), + note: z.string().min(1), +}).strict(); + +const ResolvedPresentationSchema = z.object({ + summary: z.string().min(1), + whyItMatters: z.string().min(1), + code: z.array(z.object({ path: z.string().min(1), purpose: z.string().min(1) }).strict()), + artifacts: z.array(z.object({ + kind: z.enum(["screenshot", "video", "trace", "report", "log", "code"]), + path: z.string().min(1), + label: z.string().min(1), + caption: z.string().min(1), + annotations: z.array(z.object({ + label: z.string().min(1), + detail: z.string().optional(), + x: z.number().min(0).max(100).optional(), + y: z.number().min(0).max(100).optional(), + width: z.number().positive().max(100).optional(), + height: z.number().positive().max(100).optional(), + atMs: z.number().int().nonnegative().optional(), + }).strict()), + digest: DigestSchema.optional(), + mediaType: z.string().min(1).optional(), + dataUrl: z.string().min(1).optional(), + unavailable: z.string().min(1).optional(), + }).strict()), +}).strict(); + +const ExpectedObservationSchema = z.object({ + op: AssertOpSchema, + value: z.unknown().optional(), + path: z.string(), +}).strict(); + +const FactfileEvidenceSchema = z.object({ + id: z.string().min(1), + description: z.string().min(1), + pass: z.boolean(), + actual: z.unknown(), + expected: ExpectedObservationSchema, + error: z.string().optional(), + note: z.string().optional(), + durationMs: z.number().nonnegative(), + presentation: ResolvedPresentationSchema.optional(), + verification: z.object({ + kind: z.enum(["command", "http", "mcp"]), + label: z.string().min(1), + reproduce: z.string().min(1), + assertion: ExpectedObservationSchema, + }).strict(), +}).strict().superRefine((evidence, context) => { + if (!Object.prototype.hasOwnProperty.call(evidence, "actual")) context.addIssue({ code: z.ZodIssueCode.custom, path: ["actual"], message: "is required" }); +}); + +export const GateSnapshotSchema = z.object({ + schemaVersion: z.literal("keyoku.dev/factfile/v1alpha1"), + id: SlugSchema, + project: ProjectManifestSchema.pick({ id: true, name: true, summary: true }).strict(), + outcome: z.object({ + id: SlugSchema, + revision: z.number().int().positive(), + title: z.string().min(1), + objective: z.string().min(1), + constraints: z.array(z.string()), + scope: z.object({ + include: z.array(z.string().min(1)), + exclude: z.array(z.string().min(1)), + maxChangedFiles: z.number().int().positive().optional(), + }).strict().optional(), + owner: ActorSchema, + humanCriteria: z.array(z.object({ id: SlugSchema, description: z.string().min(1), guidance: z.string().optional() }).strict()), + }).strict(), + contribution: ContributionManifestSchema, + repository: RepositorySnapshotSchema, + scope: ScopeAssessmentSchema, + reviewPlan: z.array(z.object({ + priority: z.enum(["critical", "high", "normal"]), + title: z.string().min(1), + why: z.string().min(1), + paths: z.array(z.string()), + basis: z.enum(["deterministic", "declared"]), + }).strict()), + session: ProofSessionStateSchema, + architecture: ArchitectureProjectionSchema.optional(), + state: z.enum(["evidence_gaps", "human_review_required", "review_blocked", "ready_for_review", "accepted"]), + generatedAt: z.string().datetime(), + reviews: z.array(ReviewEventSchema), + evidence: z.array(FactfileEvidenceSchema), + summary: z.object({ + passed: z.number().int().nonnegative(), + failed: z.number().int().nonnegative(), + total: z.number().int().nonnegative(), + verified: z.boolean(), + }).strict(), + humanReview: z.object({ + passed: z.number().int().nonnegative(), + failed: z.number().int().nonnegative(), + pending: z.number().int().nonnegative(), + total: z.number().int().nonnegative(), + }).strict(), + digest: DigestSchema, +}).strict().superRefine((snapshot, context) => { + const issue = (path: Array, message: string) => context.addIssue({ code: z.ZodIssueCode.custom, path, message }); + if (snapshot.contribution.outcomeId !== snapshot.outcome.id || snapshot.contribution.outcomeRevision !== snapshot.outcome.revision) { + issue(["contribution", "outcomeId"], "must match the embedded outcome identity"); + } + if (snapshot.contribution.status !== snapshot.state) issue(["contribution", "status"], "must match the Factfile state"); + if (snapshot.summary.total !== snapshot.evidence.length) issue(["summary", "total"], "must equal evidence length"); + if (snapshot.summary.passed !== snapshot.evidence.filter((item) => item.pass).length) issue(["summary", "passed"], "must equal passing evidence count"); + if (snapshot.summary.failed !== snapshot.evidence.filter((item) => !item.pass).length) issue(["summary", "failed"], "must equal failed evidence count"); + if (snapshot.summary.passed + snapshot.summary.failed !== snapshot.summary.total) issue(["summary"], "passed plus failed must equal total"); + const expectedVerified = snapshot.summary.failed === 0 && snapshot.summary.total > 0 && snapshot.scope.passed; + if (snapshot.summary.verified !== expectedVerified) issue(["summary", "verified"], "must equal passing evidence plus scope status"); + snapshot.evidence.forEach((evidence, index) => { + if (canonicalJson(evidence.expected) !== canonicalJson(evidence.verification.assertion)) { + issue(["evidence", index, "verification", "assertion"], "must match the recorded expected observation"); + } + }); + if (snapshot.humanReview.total !== snapshot.outcome.humanCriteria.length) issue(["humanReview", "total"], "must equal declared human criteria"); + if (snapshot.humanReview.passed + snapshot.humanReview.failed + snapshot.humanReview.pending !== snapshot.humanReview.total) issue(["humanReview"], "passed, failed, and pending must equal total"); + const humanCriterionIds = new Set(snapshot.outcome.humanCriteria.map((criterion) => criterion.id)); + const verdicts = new Map(); + snapshot.reviews.forEach((review, index) => { + if (review.reviewer.kind !== "human") issue(["reviews", index, "reviewer", "kind"], "must be human"); + if (review.repository.headSha !== snapshot.repository.headSha || review.repository.worktreeDigest !== snapshot.repository.worktreeDigest) issue(["reviews", index, "repository"], "must match the Factfile source"); + if (review.criterionId) { + if (!humanCriterionIds.has(review.criterionId)) issue(["reviews", index, "criterionId"], "must identify a declared human criterion"); + if (review.verdict) verdicts.set(review.criterionId, review.verdict); + } + }); + const humanPassed = [...verdicts.values()].filter((verdict) => verdict === "pass").length; + const humanFailed = [...verdicts.values()].filter((verdict) => verdict === "fail").length; + if (snapshot.humanReview.passed !== humanPassed) issue(["humanReview", "passed"], "must equal the latest passing human verdict count"); + if (snapshot.humanReview.failed !== humanFailed) issue(["humanReview", "failed"], "must equal the latest failed human verdict count"); + if (snapshot.humanReview.pending !== snapshot.humanReview.total - humanPassed - humanFailed) issue(["humanReview", "pending"], "must equal the remaining human criteria"); + if (snapshot.state === "evidence_gaps" && snapshot.summary.verified) issue(["state"], "evidence_gaps cannot be verified"); + if (snapshot.state !== "evidence_gaps" && !snapshot.summary.verified) issue(["state"], "a reviewable state requires verified automated evidence"); + if (snapshot.state === "human_review_required" && snapshot.humanReview.pending === 0) issue(["state"], "requires a pending human criterion"); + if (snapshot.state === "review_blocked" && snapshot.humanReview.failed === 0) issue(["state"], "requires a failed human criterion"); + if (["ready_for_review", "accepted"].includes(snapshot.state) && (snapshot.humanReview.pending > 0 || snapshot.humanReview.failed > 0)) issue(["state"], "requires all human criteria to pass"); + if (snapshot.state === "accepted" && !snapshot.reviews.some((review) => review.decision === "accepted")) issue(["state"], "accepted requires a human acceptance event"); +}); + +export interface FactfileHistoryItem { + id: string; + generatedAt: string; + state: GateSnapshot["state"]; + digest: string; + headSha: string; + worktreeDigest: string; + passed: number; + total: number; + humanPassed: number; + humanTotal: number; +} + +export interface InitProjectInput { + root?: string; + id?: string; + name?: string; + summary?: string; +} + +export interface StartContributionInput { + root?: string; + outcomeId: string; + title?: string; + summary?: string; + knownLimits?: string[]; + actor?: Actor; + baseSha?: string; + reuseActive?: boolean; +} + +export interface ReviewContributionInput { + root?: string; + contributionId: string; + decision: "note" | "accepted"; + comment: string; + criterionId?: string; + verdict?: "pass" | "fail"; + reviewer?: Actor; +} + +function now(): string { + return new Date().toISOString(); +} + +function slug(value: string): string { + const normalized = value + .toLowerCase() + .trim() + .replace(/[^a-z0-9._-]+/g, "-") + .replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, ""); + return normalized || "project"; +} + +function hash(value: string | Buffer): string { + return createHash("sha256").update(value).digest("hex"); +} + +function git(root: string, args: string[], fallback = "unknown"): string { + try { + return execFileSync("git", args, { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim() || fallback; + } catch { + return fallback; + } +} + +function gitRaw(root: string, args: string[]): string { + try { + return execFileSync("git", args, { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + } catch { + return ""; + } +} + +function gitRequired(root: string, args: string[], label: string): string { + try { + const output = execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); + if (!output) throw new Error("empty output"); + return output; + } catch (error) { + throw new Error(`Cannot establish ${label}: ${error instanceof Error ? error.message : String(error)}`); + } +} + +function gitRawRequired(root: string, args: string[], label: string): string { + try { return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); } + catch (error) { throw new Error(`Cannot establish ${label}: ${error instanceof Error ? error.message : String(error)}`); } +} + +export function findProjectRoot(start = process.cwd()): string { + let cursor = resolve(start); + for (;;) { + if (existsSync(join(cursor, KEYOKU_DIR, PROJECT_FILE))) return cursor; + const parent = dirname(cursor); + if (parent === cursor) break; + cursor = parent; + } + throw new Error(`No ${KEYOKU_DIR}/${PROJECT_FILE} found from ${resolve(start)}. Run 'keyoku project init' first.`); +} + +function readYaml(path: string, schema: S): z.output { + let value: unknown; + try { + value = parse(readFileSync(path, "utf8")); + } catch (error) { + throw new Error(`Cannot read ${path}: ${error instanceof Error ? error.message : String(error)}`); + } + const result = schema.safeParse(value); + if (!result.success) { + throw new Error(`Invalid ${path}: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`); + } + return result.data; +} + +function writeYaml(path: string, value: unknown): void { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, stringify(value, { lineWidth: 100 }), "utf8"); +} + +function writeJson(path: string, value: unknown): void { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +function parseReviews(root: string, path: string, bytes: Buffer): ReviewEvent[] { + return bytes.toString("utf8") + .split("\n") + .filter(Boolean) + .map((line, index) => { + let value: unknown; + try { value = parseJsonRejectDuplicateKeys(line, `Invalid ${relative(root, path)} line ${index + 1}`); } catch (error) { + throw new Error(`Invalid ${relative(root, path)} line ${index + 1}: ${error instanceof Error ? error.message : String(error)}`); + } + const result = ReviewEventSchema.safeParse(value); + if (!result.success) throw new Error(`Invalid review event at line ${index + 1}: ${result.error.message}`); + return result.data; + }); +} + +function readReviews(root: string, contributionId: string): ReviewEvent[] { + const path = join(resolvePrivateDirectory(root, [KEYOKU_DIR, "contributions", slug(contributionId)], false), "reviews.jsonl"); + return parseReviews(root, path, readLocalLedger(path)); +} + +function writeAll(fd: number, bytes: Buffer): void { + let offset = 0; + while (offset < bytes.length) offset += writeSync(fd, bytes, offset, bytes.length - offset); +} + +function fsyncDirectory(path: string): void { + const fd = openSync(path, constants.O_RDONLY); + try { fsyncSync(fd); } finally { closeSync(fd); } +} + +function writeExclusive(path: string, bytes: Buffer): void { + mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); + const fd = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600); + try { writeAll(fd, bytes); fsyncSync(fd); } finally { closeSync(fd); } + fsyncDirectory(dirname(path)); +} + +function writeAtomic(path: string, bytes: Buffer): void { + mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); + const temp = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`); + const fd = openSync(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600); + try { + writeAll(fd, bytes); + fsyncSync(fd); + } finally { + closeSync(fd); + } + try { + renameSync(temp, path); + fsyncDirectory(dirname(path)); + } finally { + if (existsSync(temp)) unlinkSync(temp); + } +} + +export interface VerifiedFactfileExpectations { + contributionId?: string; + snapshotId?: string; +} + +/** + * The sole trust boundary for persisted Factfiles. Parsing, the complete + * schema, semantic counters/bindings, and the canonical content digest are + * checked before any caller may use snapshot fields. + */ +export function readVerifiedFactfile(path: string, expected: VerifiedFactfileExpectations = {}): GateSnapshot { + let raw: unknown; + try { + raw = parseJsonBytesRejectDuplicateKeys(readFileSync(path), `Invalid Factfile ${path}`); + } catch (error) { + throw new Error(error instanceof Error ? error.message : String(error)); + } + const result = GateSnapshotSchema.safeParse(raw); + if (!result.success) { + throw new Error(`Invalid Factfile ${path}: ${result.error.issues.map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`).join("; ")}`); + } + const rawObject = raw as Record; + const { digest: claimedDigest, ...unsigned } = rawObject; + const computedDigest = canonicalJsonDigest(unsigned); + if (claimedDigest !== computedDigest) { + throw new Error(`Invalid Factfile ${path}: digest mismatch (claimed ${String(claimedDigest)}, computed ${computedDigest}).`); + } + if (expected.contributionId && result.data.contribution.id !== slug(expected.contributionId)) { + throw new Error(`Invalid Factfile ${path}: contribution '${result.data.contribution.id}' does not match '${slug(expected.contributionId)}'.`); + } + if (expected.snapshotId && result.data.id !== expected.snapshotId) { + throw new Error(`Invalid Factfile ${path}: snapshot '${result.data.id}' does not match '${expected.snapshotId}'.`); + } + return result.data as GateSnapshot; +} + +export function listFactfileHistory(rootInput: string, contributionId: string): FactfileHistoryItem[] { + const root = findProjectRoot(rootInput); + const dir = join(contributionDir(root, contributionId), "snapshots"); + if (!existsSync(dir)) return []; + return readdirSync(dir) + .filter((name) => name.endsWith(".json")) + .map((name) => { + const item = readVerifiedFactfile(join(dir, name), { contributionId, snapshotId: name.slice(0, -5) }); + return { + id: item.id, + generatedAt: item.generatedAt, + state: item.state, + digest: item.digest, + headSha: item.repository.headSha, + worktreeDigest: item.repository.worktreeDigest, + passed: item.summary.passed, + total: item.summary.total, + humanPassed: item.humanReview.passed, + humanTotal: item.humanReview.total, + } satisfies FactfileHistoryItem; + }) + .sort((a, b) => b.generatedAt.localeCompare(a.generatedAt)); +} + +function persistSnapshot(root: string, contribution: ContributionManifest, snapshot: GateSnapshot): void { + const dir = resolvePrivateDirectory(root, [KEYOKU_DIR, "contributions", slug(contribution.id)]); + const snapshots = resolvePrivateDirectory(dir, ["snapshots"]); + const { digest: _previousDigest, ...unsignedSnapshot } = snapshot; + snapshot.digest = canonicalJsonDigest(unsignedSnapshot); + const snapshotJson = Buffer.from(`${JSON.stringify(snapshot, null, 2)}\n`, "utf8"); + writeExclusive(join(snapshots, `${snapshot.id}.json`), snapshotJson); + const history = listFactfileHistory(root, contribution.id); + writeExclusive(join(snapshots, `${snapshot.id}.html`), Buffer.from(renderFactfileHtml(snapshot, { history, historical: true }), "utf8")); + writeAtomic(join(dir, "manifest.yaml"), Buffer.from(stringify(contribution, { lineWidth: 100 }), "utf8")); + writeAtomic(join(dir, "factfile.json"), snapshotJson); + writeAtomic(join(dir, "factfile.md"), Buffer.from(renderFactfileMarkdown(snapshot), "utf8")); + writeAtomic(join(dir, "factfile.github.md"), Buffer.from(renderFactfileGithubMarkdown(snapshot), "utf8")); + writeAtomic(join(dir, "factfile.html"), Buffer.from(renderFactfileHtml(snapshot, { history }), "utf8")); +} + +function remoteUrl(root: string): string | undefined { + const value = git(root, ["config", "--get", "remote.origin.url"], ""); + return value || undefined; +} + +export function initProject(input: InitProjectInput = {}): ProjectManifest { + const root = resolve(input.root ?? process.cwd()); + const path = join(root, KEYOKU_DIR, PROJECT_FILE); + if (existsSync(path)) throw new Error(`${relative(root, path)} already exists; Keyoku will not overwrite it.`); + const timestamp = now(); + const name = input.name?.trim() || basename(root); + const manifest: ProjectManifest = { + schemaVersion: "keyoku.dev/project/v1alpha1", + id: slug(input.id ?? name), + name, + summary: input.summary?.trim() || `Outcomes and contribution evidence for ${name}.`, + ...(remoteUrl(root) ? { repository: remoteUrl(root) } : {}), + defaultBranch: git(root, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], "").replace(/^origin\//, "") || "main", + createdAt: timestamp, + updatedAt: timestamp, + }; + writeYaml(path, manifest); + mkdirSync(join(root, KEYOKU_DIR, "outcomes"), { recursive: true }); + mkdirSync(join(root, KEYOKU_DIR, "contributions"), { recursive: true }); + return manifest; +} + +export function loadProject(root = findProjectRoot()): ProjectManifest { + return readYaml(join(root, KEYOKU_DIR, PROJECT_FILE), ProjectManifestSchema); +} + +export function loadOutcome(root: string, id: string): Outcome { + return readYaml(join(root, KEYOKU_DIR, "outcomes", `${slug(id)}.yaml`), OutcomeSchema); +} + +export function listOutcomes(root = findProjectRoot()): Outcome[] { + const dir = join(root, KEYOKU_DIR, "outcomes"); + if (!existsSync(dir)) return []; + return readdirSync(dir) + .filter((name) => name.endsWith(".yaml") || name.endsWith(".yml")) + .sort() + .map((name) => readYaml(join(dir, name), OutcomeSchema)); +} + +/** Outcome contracts are normal repository files. Their canonical version + * history is Git, so reviewers do not need a second opaque database. */ +export function listOutcomeHistory(rootInput: string | undefined, id: string): OutcomeHistoryEntry[] { + const root = findProjectRoot(rootInput); + const path = `${KEYOKU_DIR}/outcomes/${slug(id)}.yaml`; + const output = gitRaw(root, ["log", "--follow", "--format=%H%x1f%aI%x1f%an%x1f%s", "--", path]); + return output.split("\n").filter(Boolean).map((line) => { + const [sha = "unknown", authoredAt = "unknown", author = "unknown", subject = ""] = line.split("\x1f"); + let revision: number | undefined; + try { + const contents = execFileSync("git", ["show", `${sha}:${path}`], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); + revision = OutcomeSchema.parse(parse(contents)).revision; + } catch { /* a historical revision may predate the current schema */ } + return { sha, authoredAt, author, subject, ...(revision ? { revision } : {}) }; + }); +} + +function defaultActor(root: string): Actor { + const email = git(root, ["config", "user.email"], "local-human"); + const name = git(root, ["config", "user.name"], "Local human"); + return { kind: "human", id: email, name, role: "accountable owner" }; +} + +function contributionDir(root: string, id: string): string { + return join(root, KEYOKU_DIR, "contributions", slug(id)); +} + +interface ActiveContributionIndex { schemaVersion: "keyoku.dev/active-contributions/v1alpha1"; active: Record; } + +function activeContributionKey(root: string, outcomeId: string): string { + return `${git(root, ["branch", "--show-current"], "detached")}:${slug(outcomeId)}`; +} + +function activeContributionPath(root: string): string { return join(root, KEYOKU_DIR, "runtime", "active-contributions.json"); } + +function readActiveIndex(root: string): ActiveContributionIndex { + const path = activeContributionPath(root); + if (!existsSync(path)) return { schemaVersion: "keyoku.dev/active-contributions/v1alpha1", active: {} }; + try { + const value = JSON.parse(readFileSync(path, "utf8")) as ActiveContributionIndex; + return value.schemaVersion === "keyoku.dev/active-contributions/v1alpha1" && value.active ? value : { schemaVersion: "keyoku.dev/active-contributions/v1alpha1", active: {} }; + } catch { return { schemaVersion: "keyoku.dev/active-contributions/v1alpha1", active: {} }; } +} + +export function getActiveContribution(rootInput: string | undefined, outcomeId: string): ContributionManifest | undefined { + const root = findProjectRoot(rootInput); + const id = readActiveIndex(root).active[activeContributionKey(root, outcomeId)]; + if (!id) return undefined; + try { + const contribution = loadContribution(root, id); + const outcome = loadOutcome(root, outcomeId); + const digest = canonicalJsonDigest(outcome); + return contribution.outcomeRevision === outcome.revision && (!contribution.outcomeDigest || contribution.outcomeDigest === digest) && contribution.status !== "accepted" ? contribution : undefined; + } catch { return undefined; } +} + +function setActiveContribution(root: string, outcomeId: string, contributionId: string): void { + const index = readActiveIndex(root); + index.active[activeContributionKey(root, outcomeId)] = contributionId; + writeJson(activeContributionPath(root), index); +} + +export function loadContribution(root: string, id: string): ContributionManifest { + return readYaml(join(contributionDir(root, id), "manifest.yaml"), ContributionManifestSchema); +} + +export function startContribution(input: StartContributionInput): ContributionManifest { + const root = findProjectRoot(input.root); + const outcome = loadOutcome(root, input.outcomeId); + if (input.reuseActive) { + const active = getActiveContribution(root, outcome.id); + if (active) return active; + } + const timestamp = now(); + const id = slug(`${outcome.id}-${timestamp.slice(0, 10)}-${randomUUID().slice(0, 8)}`); + const primaryActor = input.actor ?? defaultActor(root); + const actors = primaryActor.kind === "human" + ? [primaryActor] + : [outcome.owner, primaryActor]; + const manifest: ContributionManifest = { + schemaVersion: "keyoku.dev/contribution/v1alpha1", + id, + title: input.title?.trim() || outcome.title, + summary: input.summary?.trim() || input.title?.trim() || outcome.title, + ...(input.knownLimits?.length ? { knownLimits: input.knownLimits } : {}), + outcomeId: outcome.id, + outcomeRevision: outcome.revision, + outcomeDigest: canonicalJsonDigest(outcome), + baseSha: gitRequired(root, ["rev-parse", "--verify", input.baseSha ?? "HEAD"], "the contribution base revision"), + actors, + status: "draft", + createdAt: timestamp, + updatedAt: timestamp, + }; + writeYaml(join(contributionDir(root, id), "manifest.yaml"), manifest); + setActiveContribution(root, outcome.id, id); + return manifest; +} + +function ignoredEvidencePath(path: string): boolean { + // Evidence artifacts describe the source snapshot; they are not themselves + // part of that snapshot. Excluding them prevents generating a Factfile from + // making its own proof stale. Outcome and project contracts remain included. + return path.startsWith(".keyoku/contributions/") || path.startsWith(".keyoku/pulse/") || path.startsWith(".keyoku/runtime/"); +} + +export function captureRepository(root: string, baseSha: string): RepositorySnapshot { + const verifiedBaseSha = gitRequired(root, ["rev-parse", "--verify", baseSha], "the repository base revision"); + const headSha = gitRequired(root, ["rev-parse", "--verify", "HEAD"], "the repository head revision"); + const branch = git(root, ["branch", "--show-current"], "detached"); + const upstream = git(root, ["rev-parse", "--abbrev-ref", "@{upstream}"], ""); + const [behind = 0, ahead = 0] = upstream + ? git(root, ["rev-list", "--left-right", "--count", `HEAD...${upstream}`], "0\t0").split(/\s+/).map((value) => Number(value) || 0) + : [0, 0]; + // Porcelain's first column may intentionally be a space. Do not pass this + // through git(), which trims output and would corrupt the first path. + const porcelain = gitRawRequired(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"], "the NUL-delimited worktree status"); + const porcelainEntries = porcelain.split("\0").filter(Boolean); + const worktreeFiles: string[] = []; + for (let index = 0; index < porcelainEntries.length; index += 1) { + const entry = porcelainEntries[index]!; + const status = entry.slice(0, 2); + const path = entry.slice(3); + if (status.includes("R") || status.includes("C")) { + const destination = porcelainEntries[index + 1]; + if (destination) { worktreeFiles.push(destination); index += 1; } + else worktreeFiles.push(path); + } else worktreeFiles.push(path); + } + const sourceFiles = worktreeFiles + .filter((path) => !ignoredEvidencePath(path)) + .sort(); + const committedFiles = gitRawRequired(root, ["diff", "--name-only", "-z", `${verifiedBaseSha}...${headSha}`], "the NUL-delimited committed path set") + .split("\0") + .filter(Boolean) + .filter((path) => !ignoredEvidencePath(path)); + const changedFiles = [...new Set([...committedFiles, ...sourceFiles])].sort(); + const sourceCapsuleDigest = captureSourceTreeDigest(root); + return { + repositoryRoot: root, + branch, + ...(upstream ? { upstream } : {}), + ahead, + behind, + ...(remoteUrl(root) ? { remote: remoteUrl(root) } : {}), + lastCommit: git(root, ["log", "-1", "--pretty=%s"], "unknown"), + baseSha: verifiedBaseSha, + headSha, + worktreeDigest: sourceCapsuleDigest, + sourceCapsuleDigest, + // A committed base-to-head diff is the contribution under review, not a + // dirty worktree. Keep these concepts separate so a clean revision-bound + // Factfile cannot be mislabeled merely because it contains real changes. + dirty: sourceFiles.length > 0, + changedFiles, + }; +} + +function pathMatches(path: string, pattern: string): boolean { + const normalized = pattern.replace(/^\.\//, ""); + if (normalized === "**" || normalized === "**/*") return true; + if (normalized.endsWith("/**")) return path === normalized.slice(0, -3) || path.startsWith(normalized.slice(0, -2)); + if (normalized.endsWith("/")) return path.startsWith(normalized); + if (!normalized.includes("*")) return path === normalized; + const escaped = normalized.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*"); + return new RegExp(`^${escaped}$`).test(path); +} + +function assessScope(outcome: Outcome, changedFiles: string[]): ScopeAssessment { + const declared = Boolean(outcome.scope); + const include = outcome.scope?.include ?? []; + const exclude = outcome.scope?.exclude ?? []; + const excludedPaths = changedFiles.filter((path) => exclude.some((pattern) => pathMatches(path, pattern))); + const considered = changedFiles.filter((path) => !excludedPaths.includes(path)); + const unexpectedPaths = include.length + ? considered.filter((path) => !include.some((pattern) => pathMatches(path, pattern))) + : []; + const sizePassed = !outcome.scope?.maxChangedFiles || considered.length <= outcome.scope.maxChangedFiles; + const passed = unexpectedPaths.length === 0 && sizePassed; + const areas = new Map(); + for (const path of considered) { + const name = path.includes("/") ? path.split("/")[0]! : "repository root"; + areas.set(name, (areas.get(name) ?? 0) + 1); + } + return { + declared, + passed, + includedPaths: considered.filter((path) => !unexpectedPaths.includes(path)), + unexpectedPaths, + excludedPaths, + ...(outcome.scope?.maxChangedFiles ? { maxChangedFiles: outcome.scope.maxChangedFiles } : {}), + topLevelAreas: [...areas].map(([name, files]) => ({ name, files })).sort((a, b) => b.files - a.files || a.name.localeCompare(b.name)), + note: !declared + ? "No machine scope boundary was declared; a human must judge whether this is one coherent review unit." + : passed + ? "All changed paths fit the declared contribution boundary. Semantic coherence still requires human review." + : "Changed paths exceed the declared contribution boundary.", + }; +} + +function buildReviewPlan( + outcome: Outcome, + repository: RepositorySnapshot, + scope: ScopeAssessment, + report: { criteria: CriterionEvaluation[] }, + reviews: ReviewEvent[], +): ReviewAttentionItem[] { + const items: ReviewAttentionItem[] = []; + if (scope.unexpectedPaths.length) items.push({ + priority: "critical", + title: "Resolve work outside the declared outcome boundary", + why: "These paths were changed but do not match the repository-owned scope contract.", + paths: scope.unexpectedPaths.slice(0, 8), + basis: "deterministic", + }); + const failed = report.criteria.filter((criterion) => !criterion.pass); + if (failed.length) items.push({ + priority: "critical", + title: `Investigate ${failed.length} unsupported ${failed.length === 1 ? "claim" : "claims"}`, + why: failed.map((criterion) => criterion.description).join(" · "), + paths: [], + basis: "deterministic", + }); + const sensitive = repository.changedFiles.filter((path) => /(^|\/)(auth|security|permission|policy|migration|migrations|schema|secrets?|\.github\/workflows)(\/|\.|$)|(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock|go\.sum)$/i.test(path)); + if (sensitive.length) items.push({ + priority: "high", + title: "Inspect security-, data-, workflow-, or dependency-sensitive changes", + why: "These paths can alter trust boundaries, persisted data, automation privileges, or the resolved dependency graph.", + paths: sensitive.slice(0, 8), + basis: "deterministic", + }); + if (repository.changedFiles.length > 30 || scope.topLevelAreas.length > 6) items.push({ + priority: "high", + title: "Confirm this is still one reviewable outcome", + why: `${repository.changedFiles.length} files across ${scope.topLevelAreas.length} top-level areas increases reconstruction cost; split or stack unrelated work.`, + paths: [], + basis: "deterministic", + }); + const latest = new Map(reviews.filter((review) => review.criterionId && review.verdict).map((review) => [review.criterionId!, review.verdict!])); + const pending = outcome.humanCriteria.filter((criterion) => !latest.has(criterion.id)); + if (pending.length) items.push({ + priority: "normal", + title: `Make ${pending.length} explicit human ${pending.length === 1 ? "decision" : "decisions"}`, + why: pending.map((criterion) => criterion.description).join(" · "), + paths: [], + basis: "declared", + }); + if (!items.length) items.push({ + priority: "normal", + title: "Review the outcome evidence, then inspect the changed implementation", + why: "No deterministic scope, failure, or sensitive-path signal requires earlier attention.", + paths: repository.changedFiles.slice(0, 8), + basis: "deterministic", + }); + const weight = { critical: 0, high: 1, normal: 2 } as const; + return items.sort((a, b) => weight[a.priority] - weight[b.priority]); +} + +const MAX_FACTFILE_ACTUAL_CHARS = 2_000; + +function capFactfileActual(value: unknown): unknown { + let serialized: string; + try { serialized = JSON.stringify(value) ?? "undefined"; } + catch { serialized = String(value); } + if (serialized.length <= MAX_FACTFILE_ACTUAL_CHARS) return value; + return `${serialized.slice(0, MAX_FACTFILE_ACTUAL_CHARS)}… (truncated ${serialized.length - MAX_FACTFILE_ACTUAL_CHARS} chars)`; +} + +function probeDidNotComplete(probe: Probe, envelope: ProbeEnvelope): boolean { + if (envelope.error === undefined) return false; + if (probe.kind === "command") return envelope.exitCode === -1 || envelope.output === null; + if (probe.kind === "http") return envelope.status === undefined || envelope.output === null; + return true; +} + +/** + * Factfile verification is deliberately independent of the v2 goal/workflow + * engine. It executes only the repository-owned criteria and returns the + * observations needed by the exact-source snapshot. + */ +async function evaluateFactfileCriteria( + capsule: SourceCapsule, + originalMonitor: MutationMonitor, + criteria: CriterionInput[], + presentations: Array, +): Promise<{ + converged: boolean; + criteria: CriterionEvaluation[]; + presentations: Array; + architecture?: ArchitectureProjection; +}> { + const evaluations: CriterionEvaluation[] = []; + for (let index = 0; index < criteria.length; index += 1) { + const criterion = criteria[index]!; + const started = Date.now(); + const envelope = criterion.probe.kind === "command" + ? await runCommandInSourceCapsule(capsule, criterion.probe) + : await runProbe(criterion.probe); + const result = evaluateAssertion(envelope, criterion.assert); + const incomplete = probeDidNotComplete(criterion.probe, envelope); + const path = (criterion.assert.path ?? "").trim(); + const value = criterion.assert.value; + const meaningfulFailureAssertion = + (path === "exitCode" || path === "status") && + criterion.assert.op === "eq" && + (typeof value === "string" ? value.length > 0 : value != null); + const pass = result.pass && !incomplete && (!envelope.error || meaningfulFailureAssertion); + const error = [ + envelope.error, + result.error, + result.pass && !pass ? "assertion passed but the probe itself failed — failing the criterion" : undefined, + ].filter(Boolean).join("; "); + evaluations.push({ + id: `c${index + 1}`, + description: criterion.description, + pass, + actual: capFactfileActual(result.actual), + expected: { + op: criterion.assert.op, + value: criterion.assert.value, + path: criterion.assert.path ?? "output", + }, + ...(error ? { error } : {}), + ...(result.note ? { note: result.note } : {}), + durationMs: Date.now() - started, + }); + await assertOriginalSourceUnchanged(capsule, originalMonitor); + } + const capsuleProjection = await withSourceCapsuleCheckout(capsule, (checkout) => { + let architecture: ArchitectureProjection | undefined; + try { architecture = scanArchitecture(checkout); } catch { /* architecture is optional for adopted repositories */ } + return { + presentations: presentations.map((presentation) => resolveEvidencePresentation(checkout, presentation)), + architecture, + }; + }); + return { + converged: evaluations.every((item) => item.pass), + criteria: evaluations, + presentations: capsuleProjection.presentations, + ...(capsuleProjection.architecture ? { architecture: capsuleProjection.architecture } : {}), + }; +} + +function resolveCriteria(root: string, criteria: CriterionInput[]): CriterionInput[] { + void root; + return criteria; +} + +function summarizeHumanReview(outcome: Pick, reviews: ReviewEvent[]): GateSnapshot["humanReview"] { + const latest = new Map(); + for (const review of reviews) { + if (review.criterionId && review.verdict) latest.set(review.criterionId, review.verdict); + } + let passed = 0; + let failed = 0; + for (const criterion of outcome.humanCriteria) { + const verdict = latest.get(criterion.id); + if (verdict === "pass") passed += 1; + if (verdict === "fail") failed += 1; + } + return { + passed, + failed, + pending: outcome.humanCriteria.length - passed - failed, + total: outcome.humanCriteria.length, + }; +} + +function reviewsForRepository(reviews: ReviewEvent[], repository: Pick): ReviewEvent[] { + return reviews.filter((review) => review.repository.headSha === repository.headSha && review.repository.worktreeDigest === repository.worktreeDigest); +} + +function gateState(machineVerified: boolean, human: GateSnapshot["humanReview"]): GateSnapshot["state"] { + if (!machineVerified) return "evidence_gaps"; + if (human.failed > 0) return "review_blocked"; + if (human.pending > 0) return "human_review_required"; + return "ready_for_review"; +} + +function resolveEvidencePresentation(root: string, presentation?: EvidencePresentation): ResolvedEvidencePresentation | undefined { + if (!presentation) return undefined; + return { + summary: presentation.summary, + whyItMatters: presentation.whyItMatters, + code: presentation.code, + artifacts: presentation.artifacts.map((artifact) => { + let bounded: ReturnType; + try { bounded = readBoundedArtifact(root, artifact.path); } + catch (error) { return { ...artifact, unavailable: error instanceof Error ? error.message : String(error) }; } + const bytes = bounded.bytes; + const digest = hash(bytes); + if (artifact.kind !== "screenshot" && artifact.kind !== "video") return { ...artifact, digest }; + const limit = artifact.kind === "screenshot" ? 2_000_000 : 12_000_000; + if (bytes.length > limit) return { ...artifact, digest, unavailable: `${artifact.kind === "screenshot" ? "Screenshot" : "Video"} exceeds the ${limit / 1_000_000} MB portable-report limit.` }; + const mediaType = mediaTypeForPath(artifact.path); + if (!mediaType) return { ...artifact, digest, unavailable: artifact.kind === "screenshot" ? "Screenshot must be PNG, WebP, or JPEG." : "Video must be MP4 or WebM." }; + if ((artifact.kind === "screenshot") !== mediaType.startsWith("image/")) return { ...artifact, digest, unavailable: `Artifact extension does not match kind '${artifact.kind}'.` }; + if (!mediaSignatureMatches(bytes, mediaType)) return { ...artifact, digest, unavailable: `Artifact bytes do not match the declared ${mediaType} media signature.` }; + return { + ...artifact, + digest, + mediaType, + dataUrl: `data:${mediaType};base64,${bytes.toString("base64")}`, + }; + }), + }; +} + +function verificationMethod(criterion: CriterionInput): VerificationMethod { + const assertion = { + path: criterion.assert.path ?? "output", + op: criterion.assert.op, + ...(criterion.assert.value !== undefined ? { value: redactEvidence(criterion.assert.value) } : {}), + }; + if (criterion.probe.kind === "command") return { + kind: "command", + label: "Repository command", + reproduce: redactSecrets(criterion.probe.run), + assertion, + }; + if (criterion.probe.kind === "http") return { + kind: "http", + label: "HTTP observation", + reproduce: `${criterion.probe.method ?? "GET"} ${redactSecrets(criterion.probe.url)}`, + assertion, + }; + return { + kind: "mcp", + label: "MCP observation", + reproduce: `${criterion.probe.connector}.${criterion.probe.tool}`, + assertion, + }; +} + +export async function runGate(rootInput: string | undefined, contributionId: string): Promise { + const root = findProjectRoot(rootInput); + const project = loadProject(root); + const contribution = loadContribution(root, contributionId); + const outcome = loadOutcome(root, contribution.outcomeId); + if (outcome.revision !== contribution.outcomeRevision) { + throw new Error( + `Contribution ${contribution.id} targets outcome revision ${contribution.outcomeRevision}, but revision ${outcome.revision} is current. Start a new contribution or restore the referenced outcome.`, + ); + } + const currentOutcomeDigest = canonicalJsonDigest(outcome); + if (contribution.outcomeDigest && contribution.outcomeDigest !== currentOutcomeDigest) { + throw new Error(`Outcome '${outcome.id}' changed without a revision increment. Increment its revision and start a new contribution so the proof contract is explicit.`); + } + + // Acceptance is terminal for the exact source snapshot. An explicit gate + // rerun on unchanged source returns the same accepted Factfile rather than + // silently downgrading the contribution to evaluating/ready-for-review. + if (contribution.status === "accepted") { + const acceptedPath = join(contributionDir(root, contribution.id), "factfile.json"); + if (!existsSync(acceptedPath)) throw new Error(`Accepted contribution '${contribution.id}' is missing its Factfile.`); + const accepted = readVerifiedFactfile(acceptedPath, { contributionId: contribution.id }); + if (accepted.state !== "accepted") throw new Error(`Accepted contribution '${contribution.id}' does not contain an accepted Factfile.`); + const current = captureRepository(root, contribution.baseSha); + if (accepted.repository.headSha === current.headSha && accepted.repository.worktreeDigest === current.worktreeDigest) return accepted; + } + + const criteria = resolveCriteria(root, outcome.criteria); + contribution.status = "evaluating"; + contribution.updatedAt = now(); + writeYaml(join(contributionDir(root, contribution.id), "manifest.yaml"), contribution); + const sourceBeforeProbes = captureRepository(root, contribution.baseSha); + const capsule = createSourceCapsule(root); + if (sourceBeforeProbes.worktreeDigest !== capsule.contentDigest) { + disposeSourceCapsule(capsule); + throw new Error("Proof refused: the source changed between repository inspection and immutable capsule capture. Rerun from a stable source tree."); + } + let originalMonitor: MutationMonitor; + try { originalMonitor = watchOriginalSource(capsule); } + catch (error) { + disposeSourceCapsule(capsule); + throw error; + } + try { + await originalMonitor.prepareForVerification(); + await assertOriginalSourceUnchanged(capsule, originalMonitor); + originalMonitor.clear(); + const report = await evaluateFactfileCriteria(capsule, originalMonitor, criteria, outcome.criteria.map((criterion) => criterion.evidence)); + await assertOriginalSourceUnchanged(capsule, originalMonitor); + const repository = captureRepository(root, contribution.baseSha); + if (repository.headSha !== sourceBeforeProbes.headSha || repository.worktreeDigest !== capsule.contentDigest) { + throw new Error("Proof refused: the source no longer matches the immutable verification capsule. Restore or retain the intended changes, then rerun proof."); + } + const scope = assessScope(outcome, repository.changedFiles); + const architecture = report.architecture; + const generatedAt = now(); + // A human verdict is evidence for one exact source identity. Keep the + // append-only review ledger intact, but never project a verdict from an old + // head/worktree into a newly generated Factfile. + const reviews = reviewsForRepository(readReviews(root, contribution.id), repository); + const humanReview = summarizeHumanReview(outcome, reviews); + const reviewPlan = buildReviewPlan(outcome, repository, scope, report, reviews); + const snapshotBase = { + schemaVersion: "keyoku.dev/factfile/v1alpha1" as const, + id: `fact_${generatedAt.replace(/[-:.TZ]/g, "")}_${randomUUID().slice(0, 8)}`, + project: { id: project.id, name: project.name, summary: project.summary }, + outcome: { + id: outcome.id, + revision: outcome.revision, + title: outcome.title, + objective: outcome.objective, + constraints: outcome.constraints, + ...(outcome.scope ? { scope: outcome.scope } : {}), + owner: outcome.owner, + humanCriteria: outcome.humanCriteria, + }, + contribution: { ...contribution }, + repository, + scope, + reviewPlan, + session: readProofSession(root, contribution.id), + ...(architecture ? { architecture } : {}), + state: gateState(report.converged && scope.passed, humanReview), + generatedAt, + reviews, + evidence: report.criteria.map((item, index) => ({ + ...item, + actual: redactEvidence(item.actual), + verification: verificationMethod(outcome.criteria[index]!), + ...(item.error ? { error: redactSecrets(item.error) } : {}), + ...(item.note ? { note: redactSecrets(item.note) } : {}), + ...(report.presentations[index] ? { presentation: report.presentations[index] } : {}), + })), + summary: { + passed: report.criteria.filter((item) => item.pass).length, + failed: report.criteria.filter((item) => !item.pass).length, + total: report.criteria.length, + verified: report.converged && scope.passed, + }, + humanReview, + }; + const snapshot: GateSnapshot = { ...snapshotBase, digest: canonicalJsonDigest(snapshotBase) }; + await assertOriginalSourceUnchanged(capsule, originalMonitor); + contribution.status = snapshot.state; + contribution.updatedAt = generatedAt; + snapshot.contribution.status = contribution.status; + snapshot.contribution.updatedAt = contribution.updatedAt; + persistSnapshot(root, contribution, snapshot); + await assertOriginalSourceUnchanged(capsule, originalMonitor); + const sourceAfterPersist = captureRepository(root, contribution.baseSha); + if (sourceAfterPersist.headSha !== repository.headSha || sourceAfterPersist.worktreeDigest !== capsule.contentDigest) { + throw new Error("Proof refused: the source checkout changed before Factfile persistence completed. The generated files are stale and must not be reported; rerun proof."); + } + return snapshot; + } finally { + originalMonitor.close(); + disposeSourceCapsule(capsule); + } +} + +export function reviewContribution(input: ReviewContributionInput): GateSnapshot { + const root = findProjectRoot(input.root); + const contribution = loadContribution(root, input.contributionId); + const path = join(contributionDir(root, contribution.id), "factfile.json"); + if (!existsSync(path)) throw new Error(`No Factfile for '${contribution.id}'. Run 'keyoku gate ${contribution.id}' first.`); + const reviewer = input.reviewer ?? defaultActor(root); + const reviewerResult = ActorSchema.safeParse(reviewer); + if (!reviewerResult.success || reviewer.kind !== "human") { + throw new Error("Only an identified human can record review or acceptance."); + } + const comment = input.comment.trim(); + if (!comment) throw new Error("A review comment is required."); + if (Boolean(input.criterionId) !== Boolean(input.verdict)) { + throw new Error("A human criterion review requires both criterionId and verdict."); + } + const reviewsPath = join(resolvePrivateDirectory(root, [KEYOKU_DIR, "contributions", slug(contribution.id)]), "reviews.jsonl"); + let snapshot!: GateSnapshot; + let event!: ReviewEvent; + let reviewedAt!: string; + updateLocalLedger(reviewsPath, (ledger) => { + snapshot = readVerifiedFactfile(path, { contributionId: contribution.id }); + const current = captureRepository(root, contribution.baseSha); + if (snapshot.repository.headSha !== current.headSha || snapshot.repository.worktreeDigest !== current.worktreeDigest) { + throw new Error("The repository changed after this Factfile was generated. Run the gate again before reviewing or accepting it."); + } + const projected = reviewsForRepository(parseReviews(root, reviewsPath, ledger), current); + if (canonicalJsonDigest(projected) !== canonicalJsonDigest(snapshot.reviews ?? [])) { + throw new Error("The review ledger is ahead of the Factfile projection. Rerun the gate to rebuild the projection before adding another review."); + } + if (input.criterionId && !snapshot.outcome.humanCriteria.some((criterion) => criterion.id === input.criterionId)) { + throw new Error(`Unknown human criterion '${input.criterionId}'.`); + } + if (snapshot.state === "accepted" && input.decision === "accepted") { + throw new Error("This exact Factfile is already accepted."); + } + if (snapshot.state === "accepted" && input.criterionId) { + throw new Error("Accepted is terminal for this exact Factfile. Create new source evidence before changing a criterion verdict."); + } + if (input.decision === "accepted" && snapshot.state !== "ready_for_review") { + throw new Error("Only a ready-for-review Factfile can be accepted. Resolve evidence gaps and run the gate again."); + } + reviewedAt = now(); + event = ReviewEventSchema.parse({ + id: slug(`review-${reviewedAt.replace(/[-:.TZ]/g, "")}-${randomUUID().slice(0, 8)}`), + decision: input.decision, + reviewer, + comment, + ...(input.criterionId ? { criterionId: input.criterionId, verdict: input.verdict } : {}), + reviewedAt, + factfileId: snapshot.id, + factfileDigest: snapshot.digest, + repository: { headSha: current.headSha, worktreeDigest: current.worktreeDigest }, + }); + return Buffer.concat([ledger, Buffer.from(`${JSON.stringify(event)}\n`, "utf8")]); + }); + snapshot.id = `fact_${reviewedAt.replace(/[-:.TZ]/g, "")}_${randomUUID().slice(0, 8)}`; + snapshot.generatedAt = reviewedAt; + snapshot.reviews = [...(snapshot.reviews ?? []), event]; + snapshot.session = readProofSession(root, contribution.id); + snapshot.humanReview = summarizeHumanReview(snapshot.outcome, snapshot.reviews); + if (input.decision === "accepted" || snapshot.state === "accepted") { + snapshot.state = "accepted"; + contribution.status = "accepted"; + } else { + snapshot.state = gateState(snapshot.summary.verified, snapshot.humanReview); + contribution.status = snapshot.state; + } + contribution.updatedAt = reviewedAt; + snapshot.contribution = { ...contribution }; + persistSnapshot(root, contribution, snapshot); + return snapshot; +} + +export async function publishFactfile( + rootInput: string | undefined, + contributionId: string, + engineUrl: string, + token?: string, +): Promise { + const root = findProjectRoot(rootInput); + const base = new URL(engineUrl); + if (base.username || base.password) { + throw new Error("Engine URLs must not embed credentials; use KEYOKU_ENGINE_TOKEN."); + } + const loopback = base.hostname === "127.0.0.1" || base.hostname === "localhost" || base.hostname === "::1"; + if (base.protocol !== "https:" && !(base.protocol === "http:" && loopback)) { + throw new Error("Factfiles may be published only over HTTPS or loopback HTTP."); + } + const path = join(contributionDir(root, contributionId), "factfile.json"); + if (!existsSync(path)) throw new Error(`No Factfile for '${contributionId}'. Run 'keyoku gate ${contributionId}' first.`); + const snapshot = readVerifiedFactfile(path, { contributionId }); + const current = captureRepository(root, snapshot.repository.baseSha); + if (snapshot.repository.headSha !== current.headSha || snapshot.repository.worktreeDigest !== current.worktreeDigest) { + throw new Error("The repository changed after this Factfile was generated. Run the gate again before publishing it."); + } + const endpoint = new URL("/api/v1/factfiles", base); + const response = await fetch(endpoint, { + method: "POST", + redirect: "error", + signal: AbortSignal.timeout(30_000), + headers: { + "content-type": "application/json", + ...(token ? { authorization: `Bearer ${token}` } : {}), + }, + body: readFileSync(path), + }); + const text = await response.text(); + let body: unknown = text; + try { body = JSON.parse(text); } catch { /* preserve non-JSON server detail */ } + if (!response.ok) { + throw new Error(`Engine rejected Factfile (${response.status}): ${typeof body === "string" ? body : JSON.stringify(body)}`); + } + return body; +} + +function esc(value: unknown): string { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function printable(value: unknown): string { + if (typeof value === "string") return value; + return JSON.stringify(value, null, 2); +} + +const SECRET_KEY = /token|secret|passwd|password|api[_-]?key|access[_-]?key|credential|authorization|cookie/i; + +/** Factfiles are designed to be shared. Redact both credential-shaped strings + * and values stored under credential-shaped object keys before evidence ever + * reaches JSON, Markdown, HTML, or the optional shared engine. */ +function redactEvidence(value: unknown): unknown { + if (typeof value === "string") return redactSecrets(value); + if (Array.isArray(value)) return value.map(redactEvidence); + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value as Record).map(([key, item]) => [ + key, + SECRET_KEY.test(key) ? "«redacted»" : redactEvidence(item), + ]), + ); + } + return value; +} + +export function renderFactfileMarkdown(snapshot: GateSnapshot): string { + const mark = snapshot.state.replaceAll("_", " ").toUpperCase(); + const evidence = snapshot.evidence + .map((item) => { + const story = item.presentation + ? `\n - What it shows: ${item.presentation.summary}\n - Why it matters: ${item.presentation.whyItMatters}${item.presentation.code.map((ref) => `\n - Code: \`${ref.path}\` — ${ref.purpose}`).join("")}${item.presentation.artifacts.map((artifact) => `\n - Artifact: ${artifact.label} (\`${artifact.path}\`) — ${artifact.caption}`).join("")}` + : "\n - No human-facing evidence explanation was supplied."; + return `- **${item.pass ? "PASS" : "FAIL"} — ${item.description}**${story}\n - Audit observation: \`${printable(item.actual).replace(/`/g, "\\`")}\`${item.error ? `\n - error: ${item.error}` : ""}`; + }) + .join("\n"); + const reviews = snapshot.reviews.length + ? snapshot.reviews.map((review) => `- **${review.decision === "accepted" ? "Accepted" : "Review note"}** by ${review.reviewer.name} at ${review.reviewedAt}: ${review.comment}`).join("\n") + : "No human review recorded yet."; + const latestHuman = new Map(snapshot.reviews.filter((review) => review.criterionId && review.verdict).map((review) => [review.criterionId!, review])); + const humanCriteria = snapshot.outcome.humanCriteria.length + ? snapshot.outcome.humanCriteria.map((criterion) => { + const review = latestHuman.get(criterion.id); + return `- **${review?.verdict?.toUpperCase() ?? "PENDING"} — ${criterion.description}**${review ? `\n - ${review.comment} — ${review.reviewer.name}` : criterion.guidance ? `\n - ${criterion.guidance}` : ""}`; + }).join("\n") + : "No additional human judgment criteria declared."; + const reviewPlan = snapshot.reviewPlan.map((item) => `- **${item.priority.toUpperCase()} — ${item.title}**\n - ${item.why}${item.paths.length ? `\n - Paths: ${item.paths.map((path) => `\`${path}\``).join(", ")}` : ""}`).join("\n"); + return `# ${snapshot.outcome.title}\n\n**Gate: ${mark}** · automated ${snapshot.summary.passed}/${snapshot.summary.total} · human ${snapshot.humanReview.passed}/${snapshot.humanReview.total} · exact snapshot \`${snapshot.repository.headSha.slice(0, 12)}+${snapshot.repository.worktreeDigest.slice(0, 12)}\`\n\n## Outcome\n\n${snapshot.outcome.objective}\n\n## Review this first\n\n${reviewPlan}\n\n## Accountable people and agents\n\n${snapshot.contribution.actors.map((actor) => `- ${actor.name} (${actor.kind}${actor.role ? `, ${actor.role}` : ""}${actor.harness ? `; harness: ${actor.harness}` : ""}${actor.model ? `; model: ${actor.model}` : ""})`).join("\n")}\n\n## Automated evidence\n\n${evidence}\n\n## Required human judgments\n\n${humanCriteria}\n\n## Human review history\n\n${reviews}\n\n## Scope\n\n- Base: \`${snapshot.repository.baseSha}\`\n- Head: \`${snapshot.repository.headSha}\`\n- Worktree digest: \`${snapshot.repository.worktreeDigest}\`\n- Changed files: ${snapshot.repository.changedFiles.length}\n- Factfile digest: \`${snapshot.digest}\`\n\nGenerated by Keyoku at ${snapshot.generatedAt}. Automated verification and human judgment are reported separately; neither is a universal safety claim. “Accepted” additionally means the named human accepted this exact snapshot.\n`; +} + +function githubState(snapshot: GateSnapshot): { icon: string; label: string; message: string } { + if (snapshot.state === "accepted") return { icon: "✅", label: "Accepted", message: "A named human accepted this exact snapshot." }; + if (snapshot.state === "ready_for_review") return { icon: "✅", label: "Ready for review", message: "Declared automated and human criteria pass; acceptance remains explicit." }; + if (snapshot.state === "human_review_required") return { icon: "🟡", label: "Human review needed", message: "Repository checks pass. The acceptance questions below still require maintainer judgment." }; + if (snapshot.state === "review_blocked") return { icon: "🔴", label: "Review blocked", message: "A required human judgment currently blocks acceptance." }; + return { icon: "🔴", label: "Evidence gaps", message: "One or more declared claims are not supported at this revision." }; +} + +/** A deliberately short GitHub surface. It gives a reviewer the result and + * remaining attention first; the portable HTML/JSON artifacts hold the full + * teaching and audit views. */ +export function renderFactfileGithubMarkdown(snapshot: GateSnapshot): string { + const status = githubState(snapshot); + const latestHuman = new Map(snapshot.reviews.filter((review) => review.criterionId && review.verdict).map((review) => [review.criterionId!, review])); + const supported = snapshot.evidence.filter((item) => item.pass).map((item) => { + const explanation = item.presentation?.summary ?? "The declared observation matched its rule; no reviewer-facing artifact was supplied."; + const artifactCount = item.presentation?.artifacts.filter((artifact) => artifact.digest && !artifact.unavailable).length ?? 0; + return `- ✅ **${item.description}** — ${explanation}${artifactCount ? ` _(${artifactCount} content-bound ${artifactCount === 1 ? "artifact" : "artifacts"})_` : ""}`; + }).join("\n") || "- No automated claim is currently supported."; + const gaps = snapshot.evidence.filter((item) => !item.pass).map((item) => `- ❌ **${item.description}** — ${item.error ?? "The observed result did not match the declared rule."}`).join("\n"); + const claimDetails = snapshot.evidence.map((item) => { + const presentation = item.presentation; + const artifacts = presentation?.artifacts.length + ? presentation.artifacts.map((artifact) => `- ${artifact.unavailable ? "⚠️" : "📎"} **${artifact.label}** — ${artifact.caption} (\`${artifact.path}\`${artifact.digest ? ` · SHA-256 \`${artifact.digest}\`` : ""}${artifact.unavailable ? ` · ${artifact.unavailable}` : ""})`).join("\n") + : "- No visual or report artifact was attached."; + const code = presentation?.code.length + ? presentation.code.map((ref) => `- \`${ref.path}\` — ${ref.purpose}`).join("\n") + : "- No code-tour paths were declared."; + return `
\n${item.pass ? "✅ Supported" : "❌ Evidence gap"} · ${item.description}\n\n${presentation?.summary ?? "No reviewer-facing explanation was supplied."}\n\n**Why this matters:** ${presentation?.whyItMatters ?? "The outcome author did not explain the relevance of this check."}\n\n**Inspectable artifacts**\n\n${artifacts}\n\n**Relevant code**\n\n${code}\n\n**Reproduce**\n\n\`${item.verification.reproduce.replace(/`/g, "\\`")}\`\n\n${item.verification.label} · completed in ${item.durationMs}ms · rule: \`${printable(item.verification.assertion).replace(/`/g, "\\`")}\`\n\n
`; + }).join("\n\n"); + const decisions = snapshot.outcome.humanCriteria.length + ? snapshot.outcome.humanCriteria.map((criterion) => { + const review = latestHuman.get(criterion.id); + const verdict = review?.verdict === "pass" ? "✅ Passed" : review?.verdict === "fail" ? "❌ Blocked" : "🟡 Needs reviewer"; + return `- **${verdict}:** ${criterion.description}${review ? ` — ${review.comment} _(${review.reviewer.name})_` : criterion.guidance ? `\n
${criterion.guidance}` : ""}`; + }).join("\n") + : "- No additional human judgment criteria were declared."; + const areas = snapshot.scope.topLevelAreas.length + ? snapshot.scope.topLevelAreas.map((area) => `\`${area.name}/\` ${area.files}`).join(" · ") + : "No changed files detected"; + const people = snapshot.contribution.actors.map((actor) => { + const provenance = [actor.role, actor.harness && `via ${actor.harness}`, actor.model].filter(Boolean).join(" · "); + return `- **${actor.name}** — ${actor.kind}${provenance ? ` · ${provenance}` : ""}${actor.ownerId ? ` · accountable to ${actor.ownerId}` : ""}`; + }).join("\n"); + const limits = snapshot.contribution.knownLimits?.length + ? snapshot.contribution.knownLimits.map((limit) => `- ${limit}`).join("\n") + : "- Only the claims listed here were evaluated.\n- Passing commands are not a judgment of product fit, maintainability, or universal safety.\n- Any source change requires a new Factfile."; + const unexpected = snapshot.scope.unexpectedPaths.length + ? `\n\n> [!CAUTION]\n> **Outside declared scope:** ${snapshot.scope.unexpectedPaths.map((path) => `\`${path}\``).join(", ")}` + : ""; + const attention = snapshot.reviewPlan.map((item, index) => `${index + 1}. **${item.priority === "critical" ? "🔴" : item.priority === "high" ? "🟠" : "🔵"} ${item.title}** — ${item.why}${item.paths.length ? `\n
${item.paths.map((path) => `\`${path}\``).join(" · ")}` : ""}`).join("\n"); + const decisionLine = snapshot.state === "human_review_required" + ? `**Decision: do not accept yet.** ${snapshot.humanReview.pending} named human ${snapshot.humanReview.pending === 1 ? "decision remains" : "decisions remain"}.` + : snapshot.state === "evidence_gaps" ? `**Decision: evidence is incomplete.** ${snapshot.summary.failed} declared ${snapshot.summary.failed === 1 ? "claim is" : "claims are"} unsupported.` + : snapshot.state === "review_blocked" ? `**Decision: blocked by human review.** ${snapshot.humanReview.failed} required ${snapshot.humanReview.failed === 1 ? "judgment failed" : "judgments failed"}.` + : snapshot.state === "accepted" ? "**Decision: accepted.** A named human accepted this exact source snapshot." + : "**Decision: ready for explicit acceptance.** Every declared automated and human criterion currently passes."; + return `## ${status.icon} Keyoku · ${status.label}\n\n### ${snapshot.outcome.title}\n\n${decisionLine}\n\n> **Requested outcome:** ${snapshot.outcome.objective}\n>\n> **Delivered change:** ${snapshot.contribution.summary ?? snapshot.contribution.title}\n\n**At a glance:** ${snapshot.summary.passed}/${snapshot.summary.total} automated claims supported · ${snapshot.humanReview.pending} human decisions pending · ${snapshot.repository.changedFiles.length} changed files · exact revision \`${snapshot.repository.headSha.slice(0, 12)}+${snapshot.repository.worktreeDigest.slice(0, 12)}\`\n\n> [!NOTE]\n> **What “proof” means here:** bounded evidence for the declared claims at this exact source snapshot—not proof that the whole project is correct, secure, or ready.\n\n### What is established\n\n${supported}${gaps ? `\n\n### What is not established\n\n${gaps}` : ""}${unexpected}\n\n### What only a human can decide\n\n${decisions}\n\n> [!IMPORTANT]\n> **Make the decision with GitHub's native PR review:** Approve when the outcome is satisfied, or Request changes with the next concrete instruction. The next push produces a new SHA-bound Factfile automatically.\n\n### Review path\n\n${attention}\n\n
\nOpen the evidence chain for every claim\n\n${claimDetails}\n\n
\n\n
\nChange boundary · ${snapshot.repository.changedFiles.length} files\n\n${areas}\n\n${snapshot.scope.note}\n\n
\n\n
\nPeople and agent provenance\n\n${people}\n\n
\n\n
\nLimits and exact audit identity\n\n${limits}\n\n- Base: \`${snapshot.repository.baseSha}\`\n- Head: \`${snapshot.repository.headSha}\`\n- Worktree: \`${snapshot.repository.worktreeDigest}\`\n- Factfile: \`${snapshot.digest}\`\n\n
\n\n---\nGenerated by free, provider-neutral Keyoku. The attached HTML Factfile contains the evidence gallery, code tour, architecture, and reproduction details.\n`; +} + +const FACTFILE_CSS = ` +:root{ + --bg:#fafafa;--bg-raised:#ffffff;--surface:#f2f2f3;--surface-strong:#ececee; + --ink:#18181b;--ink-muted:#52525b;--ink-soft:#84848c; + --line:rgba(24,24,27,.12);--line-strong:rgba(24,24,27,.24); + --good:#1a7f37;--good-bg:rgba(26,127,55,.09);--good-line:rgba(26,127,55,.28); + --bad:#b42318;--bad-bg:rgba(180,35,24,.08);--bad-line:rgba(180,35,24,.28); + --wait:#8a6d1a;--wait-bg:rgba(138,109,26,.09); + --focus:#18181b; + --mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,"Liberation Mono",monospace; + --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,Helvetica,Arial,sans-serif; + --radius:10px;--radius-lg:14px; +} +@media(prefers-color-scheme:dark){ + :root{ + --bg:#0a0a0b;--bg-raised:#141416;--surface:#19191c;--surface-strong:#1f1f23; + --ink:#f4f4f5;--ink-muted:#a1a1aa;--ink-soft:#77777f; + --line:rgba(255,255,255,.11);--line-strong:rgba(255,255,255,.2); + --good:#4ade80;--good-bg:rgba(74,222,128,.1);--good-line:rgba(74,222,128,.28); + --bad:#f87171;--bad-bg:rgba(248,113,113,.1);--bad-line:rgba(248,113,113,.28); + --wait:#f2c94c;--wait-bg:rgba(242,201,76,.1); + } +} +[data-theme="dark"]{--bg:#0a0a0b;--bg-raised:#141416;--surface:#19191c;--surface-strong:#1f1f23;--ink:#f4f4f5;--ink-muted:#a1a1aa;--ink-soft:#77777f;--line:rgba(255,255,255,.11);--line-strong:rgba(255,255,255,.2);--good:#4ade80;--good-bg:rgba(74,222,128,.1);--good-line:rgba(74,222,128,.28);--bad:#f87171;--bad-bg:rgba(248,113,113,.1);--bad-line:rgba(248,113,113,.28);--wait:#f2c94c;--wait-bg:rgba(242,201,76,.1)} +[data-theme="light"]{--bg:#fafafa;--bg-raised:#fff;--surface:#f2f2f3;--surface-strong:#ececee;--ink:#18181b;--ink-muted:#52525b;--ink-soft:#84848c;--line:rgba(24,24,27,.12);--line-strong:rgba(24,24,27,.24);--good:#1a7f37;--good-bg:rgba(26,127,55,.09);--good-line:rgba(26,127,55,.28);--bad:#b42318;--bad-bg:rgba(180,35,24,.08);--bad-line:rgba(180,35,24,.28);--wait:#8a6d1a;--wait-bg:rgba(138,109,26,.09)} + +*{box-sizing:border-box} +html{scroll-behavior:smooth} +body{margin:0;min-height:100vh;background:var(--bg);color:var(--ink);font-family:var(--sans);-webkit-font-smoothing:antialiased;line-height:1.5} +a{color:inherit} +code,pre,kbd{font-family:var(--mono)} +button{font-family:inherit} +h1,h2,h3{font-weight:650;letter-spacing:-.01em} + +/* Header ------------------------------------------------------------- */ +.ff-header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 20px;border-bottom:1px solid var(--line);position:sticky;top:0;background:color-mix(in srgb, var(--bg) 88%, transparent);backdrop-filter:blur(10px);z-index:10} +.ff-brand{display:flex;align-items:center;gap:8px;color:var(--ink)} +.ff-mark{display:grid;place-items:center;width:20px;height:20px;flex:none} +.ff-mark svg{display:block;width:18px;height:18px} +.ff-word{font-family:var(--mono);font-size:14px;font-weight:600;letter-spacing:-.02em} +.ff-meta{display:flex;align-items:center;gap:10px;min-width:0;color:var(--ink-soft);font-size:12px} +.ff-project{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:32vw} +.ff-source{font-family:var(--mono);white-space:nowrap;border:1px solid var(--line);border-radius:999px;padding:4px 9px;color:var(--ink-muted)} +.theme-toggle{appearance:none;border:1px solid var(--line);border-radius:8px;background:var(--bg-raised);color:var(--ink-muted);width:30px;height:30px;display:grid;place-items:center;cursor:pointer;font-size:13px} +.theme-toggle:hover{color:var(--ink);border-color:var(--line-strong)} +.live-banner{display:flex;align-items:center;gap:8px;margin:0 0 18px;padding:8px 12px;border:1px solid var(--line);border-radius:8px;background:var(--surface);color:var(--ink-muted);font-size:12px} +.live-banner i{width:6px;height:6px;border-radius:50%;background:var(--ink-soft);flex:none} +.live-banner.live i{background:var(--good);box-shadow:0 0 0 3px var(--good-bg)} + +/* Layout --------------------------------------------------------------- */ +.ff-main{max-width:860px;margin:0 auto;padding:28px 20px 64px} +.ff-eyebrow{display:block;color:var(--ink-soft);font:11px var(--mono);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px} +.ff-title{font-size:clamp(22px,3.4vw,30px);line-height:1.16;letter-spacing:-.02em;margin:0 0 10px} +.ff-objective{color:var(--ink-muted);font-size:14px;line-height:1.6;margin:0 0 24px;max-width:70ch} + +/* Hero: shared ----------------------------------------------------------- */ +.hero{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--bg-raised);overflow:hidden;margin-bottom:28px} + +/* Hero: filmstrip -------------------------------------------------------- */ +.film-stage{position:relative;background:#000;aspect-ratio:16/10;max-height:480px} +.film-frame{position:absolute;inset:0;display:none;align-items:center;justify-content:center;cursor:zoom-in} +.film-frame.active{display:flex} +.film-frame img,.film-frame video{display:block;width:100%;height:100%;object-fit:contain;background:#000} +.film-expand{position:absolute;right:10px;top:10px;z-index:2;appearance:none;border:1px solid rgba(255,255,255,.28);background:rgba(0,0,0,.5);color:#fff;border-radius:7px;width:30px;height:30px;cursor:pointer;font-size:14px} +.film-expand:hover{background:rgba(0,0,0,.7)} +.film-caption{display:flex;flex-direction:column;gap:2px;padding:12px 16px;border-top:1px solid var(--line)} +.film-caption strong{font-size:13px} +.film-caption span{color:var(--ink-muted);font-size:12px;line-height:1.5} +.film-dots{display:flex;gap:6px;flex-wrap:wrap;padding:0 16px 14px} +.film-dot{appearance:none;border:1px solid var(--line-strong);background:transparent;width:7px;height:7px;border-radius:50%;padding:0;cursor:pointer} +.film-dot.active{background:var(--ink);border-color:var(--ink)} +.lightbox{position:fixed;inset:0;z-index:100;background:rgba(0,0,0,.86);display:flex;align-items:center;justify-content:center;padding:32px} +.lightbox[hidden]{display:none} +.lightbox-stage{max-width:100%;max-height:100%} +.lightbox-stage img,.lightbox-stage video{max-width:100%;max-height:88vh;display:block;margin:0 auto} +.lightbox-close{position:absolute;top:18px;right:22px;appearance:none;border:1px solid rgba(255,255,255,.3);background:rgba(255,255,255,.08);color:#fff;width:34px;height:34px;border-radius:8px;cursor:pointer;font-size:15px} + +/* Hero: CLI replay --------------------------------------------------------- */ +.cli-window{background:#0b0b0c;color:#e4e4e7} +.cli-titlebar{display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.12)} +.cli-dot{width:9px;height:9px;border-radius:50%;background:rgba(255,255,255,.22)} +.cli-path{margin-left:8px;font:11px var(--mono);color:rgba(255,255,255,.5)} +.cli-body{padding:16px 18px 20px;font:12.5px/1.9 var(--mono);max-height:420px;overflow:auto} +.cli-line{opacity:0;transform:translateY(3px);animation:cli-reveal .35s ease forwards;animation-delay:calc(var(--i) * .28s);display:flex;flex-wrap:wrap;gap:0 8px;align-items:baseline;color:rgba(255,255,255,.9)} +.cli-prompt{color:rgba(255,255,255,.4)} +.cli-cmd{word-break:break-word} +.cli-result{margin-left:auto;padding-left:14px;white-space:nowrap;font-size:11.5px} +.cli-result.pass{color:var(--good)} +.cli-result.fail{color:var(--bad)} +@keyframes cli-reveal{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}} + +/* Summary ------------------------------------------------------------------ */ +.summary{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--bg-raised);padding:18px 20px;margin-bottom:22px} +.summary-verdict{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;padding-bottom:12px;margin-bottom:12px;border-bottom:1px solid var(--line)} +.summary-verdict .dot{width:8px;height:8px;border-radius:50%;background:var(--wait);flex:none;align-self:center} +.summary-verdict.tone-good .dot{background:var(--good)} +.summary-verdict.tone-bad .dot{background:var(--bad)} +.summary-verdict strong{font-size:14px} +.verdict-detail{color:var(--ink-muted);font-size:12.5px} +.summary-counts{display:flex;gap:18px;flex-wrap:wrap;margin-bottom:12px;font-size:12.5px;color:var(--ink-muted)} +.summary-counts b{font-family:var(--mono);color:var(--ink)} +.summary-list{list-style:none;margin:0;padding:0;display:grid;gap:6px} +.summary-list li{display:flex;gap:9px;align-items:flex-start;font-size:13px;color:var(--ink-muted);line-height:1.5} +.summary-list .mark{flex:none;width:15px;font-family:var(--mono);font-weight:700} +.summary-list li.pass .mark{color:var(--good)} +.summary-list li.fail .mark{color:var(--bad)} +.summary-list li.pass{color:var(--ink)} + +/* Insight -------------------------------------------------------------------- */ +.insight{margin-bottom:26px} +.insight>h2{font-size:16px;margin:0 0 12px} +.insight-group{margin-bottom:16px} +.insight-group>h3{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:var(--ink-soft);margin:0 0 8px;font-weight:650} +.insight-item{border:1px solid var(--line);border-radius:var(--radius);background:var(--bg-raised);margin-bottom:8px;overflow:hidden} +.insight-item>summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:10px;padding:12px 14px} +.insight-item>summary::-webkit-details-marker{display:none} +.insight-item .mark{flex:none;width:18px;height:18px;border-radius:50%;display:grid;place-items:center;font-size:10px;font-weight:800;background:var(--wait-bg);color:var(--wait)} +.insight-item .mark.pass{background:var(--good-bg);color:var(--good)} +.insight-item .mark.fail{background:var(--bad-bg);color:var(--bad)} +.insight-title{flex:1;min-width:0;font-size:13.5px;font-weight:560} +.insight-state{color:var(--ink-soft);font:10px var(--mono);text-transform:uppercase;letter-spacing:.04em} +.insight-chevron{color:var(--ink-soft);transition:transform .15s} +.insight-item[open] .insight-chevron{transform:rotate(90deg)} +.insight-body{padding:0 14px 16px 42px;color:var(--ink-muted);font-size:13px;line-height:1.6} +.insight-body p{margin:0 0 8px} +.insight-body .meta{color:var(--ink-soft);font-size:11.5px} +.decision-facts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:6px 0 14px} +.decision-facts div{padding:10px;border-radius:8px;background:var(--surface)} +.decision-facts b{display:block;color:var(--ink-soft);font:9.5px var(--mono);text-transform:uppercase;margin-bottom:4px} +.decision-facts span{font-size:12.5px;color:var(--ink)} +.option-list{display:grid;gap:7px;margin-bottom:10px} +.option{display:grid;grid-template-columns:16px minmax(0,1fr);gap:9px;padding:10px;border:1px solid var(--line);border-radius:8px;cursor:pointer} +.option:has(input:checked){border-color:var(--line-strong);background:var(--surface)} +.option input{margin-top:3px} +.option strong{font-size:12.5px} +.option span{display:block;color:var(--ink-muted);font-size:12px;margin-top:2px} +.outcome-effect{margin:10px 0;padding:9px 11px;border-radius:8px;background:var(--surface);font-size:12px;line-height:1.55;color:var(--ink-muted)} +.outcome-effect b{display:block;color:var(--ink-soft);font:9.5px var(--mono);text-transform:uppercase;margin-bottom:4px} +.direction-deep p,.direction-deep li{color:var(--ink-muted);font-size:12px;line-height:1.55} +.direction-deep ul{padding-left:16px;margin:6px 0 0} +.action-row{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px} +.action{appearance:none;border:1px solid var(--line-strong);border-radius:8px;background:var(--surface);color:var(--ink);padding:8px 12px;font:600 12px var(--sans);cursor:pointer} +.action.primary{background:var(--ink);color:var(--bg);border-color:var(--ink)} +.action:hover{filter:brightness(1.05)} +.action-result{min-height:14px;margin:8px 0 0;color:var(--ink-soft);font-size:11.5px} +.instruction-box{display:grid;gap:8px;margin-top:6px} +.instruction-box textarea{min-height:70px;resize:vertical;border:1px solid var(--line);border-radius:8px;background:var(--bg-raised);color:var(--ink);padding:10px;font:12.5px/1.5 var(--sans)} +.custom-direction{margin-top:8px;border:1px solid var(--line);border-radius:8px} +.custom-direction>summary{padding:10px 12px;cursor:pointer;font-size:12px;color:var(--ink-muted);list-style:none} +.custom-direction>summary::-webkit-details-marker{display:none} +.custom-body{padding:0 12px 12px} +.empty-state,.clear-state{padding:14px 16px;border:1px dashed var(--line-strong);border-radius:8px;color:var(--ink-muted);font-size:12.5px;line-height:1.55} +.clear-state{display:flex;gap:10px;align-items:flex-start;border:1px solid var(--good-line);background:var(--good-bg)} +.clear-state i{color:var(--good);font-style:normal;font-weight:800} +.clear-state strong{display:block;color:var(--ink);font-size:13px} + +/* Folds (everything else) ----------------------------------------------------- */ +.fold{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--bg-raised);margin-bottom:10px;overflow:hidden} +.fold>summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:10px;padding:15px 18px} +.fold>summary::-webkit-details-marker{display:none} +.fold[open]>summary{border-bottom:1px solid var(--line)} +.fold-title{flex:1;font-size:13.5px;font-weight:600} +.fold-meta{color:var(--ink-soft);font:10.5px var(--mono)} +.chevron{color:var(--ink-soft);transition:transform .15s} +.fold[open] .chevron{transform:rotate(90deg)} +.fold-body{padding:18px} + +.subhead{font:10px var(--mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-soft);margin:16px 0 8px} +.subhead:first-child{margin-top:0} + +.evidence-list{display:grid;gap:8px} +.evidence-row{border:1px solid var(--line);border-radius:8px} +.evidence-row>summary{list-style:none;cursor:pointer;display:grid;grid-template-columns:22px minmax(0,1fr) auto 16px;gap:10px;align-items:center;padding:12px 14px} +.evidence-row>summary::-webkit-details-marker{display:none} +.evidence-mark{width:20px;height:20px;border-radius:50%;display:grid;place-items:center;background:var(--good-bg);color:var(--good);font-size:10px;font-weight:900} +.evidence-row.fail .evidence-mark{background:var(--bad-bg);color:var(--bad)} +.evidence-copy strong{display:block;font-size:13px} +.evidence-copy span{display:block;color:var(--ink-muted);font-size:12px;margin-top:2px} +.evidence-meta{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end} +.meta-pill{border:1px solid var(--line);border-radius:999px;padding:3px 8px;color:var(--ink-muted);font:10px var(--mono)} +.evidence-body{padding:0 14px 16px 46px} +.story-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:12px 0;border-top:1px solid var(--line)} +.story-block b{display:block;font-size:11.5px;margin-bottom:5px} +.story-block p{font-size:12.5px;line-height:1.55;color:var(--ink-muted);margin:0} +.artifact-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px} +.artifact{border:1px solid var(--line);border-radius:8px;padding:10px;background:var(--surface)} +.artifact.warning{background:var(--wait-bg)} +.artifact b{display:block;font-size:11.5px} +.artifact span{display:block;color:var(--ink-muted);font-size:11.5px;line-height:1.4;margin-top:3px} +.artifact code{display:block;font-size:10px;color:var(--ink-soft);margin-top:6px;word-break:break-all} +.artifact-role{display:block!important;margin:0 0 6px!important;color:var(--ink-soft)!important;font:9px var(--mono)!important;text-transform:uppercase} +.screenshot{margin:8px 0 0;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:#000} +.screenshot img,.screenshot video{display:block;width:100%;max-height:520px;object-fit:contain} +.screenshot figcaption{padding:9px 11px;color:var(--ink-muted);font-size:11px;line-height:1.45;border-top:1px solid var(--line);background:var(--bg-raised)} +.media-frame{position:relative} +.annotation-pin{position:absolute;left:var(--x);top:var(--y);transform:translate(-50%,-50%);width:22px;height:22px;border:2px solid #fff;border-radius:50%;background:var(--ink);color:var(--bg);display:grid;place-items:center;font:750 10px var(--mono)} +.annotation-list{display:grid;gap:4px;margin-top:8px} +.annotation-note{color:var(--ink-muted);font-size:11px;line-height:1.45} +.annotation-note b{color:var(--ink)} +.video-time{font:10px var(--mono);color:var(--ink-soft);margin-right:5px} +.code-list{border:1px solid var(--line);border-radius:8px;overflow:hidden} +.code-row{display:grid;grid-template-columns:minmax(160px,.7fr) minmax(0,1.3fr);gap:12px;padding:9px 11px;border-top:1px solid var(--line)} +.code-row:first-child{border-top:0} +.code-row code{font-size:10.5px;color:var(--ink-soft);word-break:break-word} +.code-row span{font-size:11.5px;color:var(--ink-muted)} +.reproduce{margin-top:8px;padding:11px;border-radius:8px;background:var(--surface)} +.reproduce b{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--ink-soft);margin-bottom:5px} +.reproduce code{font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-word} +.raw{margin-top:8px} +.raw>summary{cursor:pointer;font-size:11.5px;color:var(--ink-muted);padding:6px 0;list-style:none} +.raw-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px} +pre{white-space:pre-wrap;word-break:break-word;margin:0;border:1px solid var(--line);border-radius:8px;background:var(--surface);padding:10px;color:var(--ink-muted);font:10.5px/1.55 var(--mono);max-height:220px;overflow:auto} +.raw p{color:var(--ink-soft);font:9.5px var(--mono)} + +.work-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px} +.work-card{border:1px solid var(--line);border-radius:8px;padding:12px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px} +.work-card strong{font-size:12.5px} +.work-card p{grid-column:1/-1;margin:0;color:var(--ink-muted);font-size:11.5px;line-height:1.5} +.work-status{font:9.5px var(--mono);text-transform:uppercase;color:var(--ink-soft)} +.agent-line{display:flex;align-items:center;gap:7px;color:var(--ink-muted);font-size:11px;margin-top:10px} +.agent-line i{width:6px;height:6px;border-radius:50%;background:var(--ink-soft)} +.agent-line.connected i{background:var(--good)} +.local-time{white-space:nowrap;font-variant-numeric:tabular-nums} + +.queue{display:grid} +.queue-row{display:grid;grid-template-columns:22px minmax(0,1fr) 90px;gap:10px;align-items:start;padding:10px 0;border-top:1px solid var(--line)} +.queue-row:first-child{border-top:0} +.queue-mark{width:20px;height:20px;border-radius:6px;display:grid;place-items:center;background:var(--surface);color:var(--ink-muted);font-size:11px;font-weight:800} +.queue-row.human .queue-mark{background:var(--wait-bg);color:var(--wait)} +.queue-row.pass .queue-mark{background:var(--good-bg);color:var(--good)} +.queue-row.fail .queue-mark{background:var(--bad-bg);color:var(--bad)} +.queue-copy strong{display:block;font-size:12.5px;line-height:1.4} +.queue-copy p{color:var(--ink-muted);font-size:11.5px;line-height:1.5;margin:3px 0 0} +.queue-copy code{display:block;color:var(--ink-soft);font-size:10.5px;line-height:1.5;margin-top:6px;word-break:break-word} +.queue-state{text-align:right;font:9.5px var(--mono);text-transform:uppercase;letter-spacing:.04em;color:var(--ink-soft);padding-top:3px} +.queue-row.human .queue-state{color:var(--wait)} +.queue-row.pass .queue-state{color:var(--good)} +.queue-row.fail .queue-state{color:var(--bad)} + +.history-list{display:grid;gap:6px} +.history-row{display:grid;grid-template-columns:12px minmax(0,1fr) auto;gap:8px;align-items:center;padding:9px 10px;border:1px solid var(--line);border-radius:8px;text-decoration:none;color:inherit} +.history-row.current{border-color:var(--line-strong);background:var(--surface)} +.history-node{color:var(--ink-soft);font-size:9px} +.history-row.current .history-node{color:var(--ink)} +.history-copy strong{display:block;font-size:11.5px} +.history-copy span{display:block;margin-top:2px;color:var(--ink-soft);font:10px var(--mono)} +.history-row code{color:var(--ink-soft);font:9.5px var(--mono)} +.history-empty{color:var(--ink-soft);font-size:12px;line-height:1.55} + +.identity{border:1px solid var(--line);border-radius:8px;overflow:hidden} +.identity-row{display:grid;grid-template-columns:110px minmax(0,1fr);padding:8px 10px;border-top:1px solid var(--line);gap:10px} +.identity-row:first-child{border-top:0} +.identity-row span{font-size:11px;color:var(--ink-soft)} +.identity-row code{font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ink-muted)} +.plain-list{list-style:none;padding:0;margin:0} +.plain-list li{font-size:11.5px;line-height:1.5;color:var(--ink-muted);padding:7px 0;border-top:1px solid var(--line)} +.plain-list li:first-child{border-top:0} +.file-list li{font-family:var(--mono);font-size:10px;word-break:break-word} +.area-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border:1px solid var(--line);border-radius:8px;overflow:hidden;margin-bottom:12px} +.area{padding:10px;border-left:1px solid var(--line);border-top:1px solid var(--line);margin:-1px 0 0 -1px} +.area b{display:block;font-size:11.5px} +.area span{font-size:10.5px;color:var(--ink-muted)} +.architecture-frame{border:1px solid var(--line);border-radius:8px;overflow:auto;background:var(--surface)} +.architecture-frame svg{display:block;width:100%;min-width:700px;height:auto} +.audit-columns{display:grid;grid-template-columns:1fr 1fr;gap:24px} +.audit-columns h3{font-size:12.5px;margin:18px 0 7px} +.audit-columns h3:first-child{margin-top:0} +.people{display:grid;gap:7px} +.person{display:grid;grid-template-columns:26px minmax(0,1fr);gap:9px;align-items:center} +.person i{width:26px;height:26px;border-radius:7px;background:var(--surface);border:1px solid var(--line);color:var(--ink-muted);display:grid;place-items:center;font-style:normal;font-size:10px;font-weight:800} +.person b{display:block;font-size:11.5px} +.person span{display:block;font-size:11px;color:var(--ink-muted)} + +.ff-footer{display:flex;flex-wrap:wrap;justify-content:space-between;gap:12px;padding:18px 4px;color:var(--ink-soft);font-size:11px} +.ff-footer code{font-size:10px;word-break:break-all;text-align:right} + +@media(prefers-reduced-motion:reduce){ + html{scroll-behavior:auto} + .cli-line{animation:none!important;opacity:1!important;transform:none!important} +} +@media(max-width:720px){ + .ff-header{padding:12px 14px} + .ff-project{display:none} + .ff-main{padding:20px 14px 48px} + .story-grid,.artifact-list,.raw-grid,.audit-columns,.work-grid,.decision-facts{grid-template-columns:1fr} + .code-row{grid-template-columns:1fr} + .queue-row{grid-template-columns:22px minmax(0,1fr)} + .queue-state{grid-column:2;text-align:left} + .evidence-row>summary{grid-template-columns:22px minmax(0,1fr) 16px} + .evidence-meta{grid-column:2;justify-content:flex-start} + .evidence-body{padding-left:14px} + .area-grid{grid-template-columns:1fr 1fr} +} +`; + +interface DirectionSuggestion { + id: string; + eyebrow: string; + label: string; + summary: string; + outcomeEffect: string; + deepDive: string; + basis: string; + evidenceRefs: string[]; + tradeoffs: string[]; + instruction: string; + recommended?: boolean; + source: "agent" | "deterministic"; +} + +function buildDirectionSuggestions(snapshot: GateSnapshot): DirectionSuggestion[] { + const suggestions: DirectionSuggestion[] = []; + const firstAttention = snapshot.reviewPlan.find((item) => item.basis === "deterministic"); + if (firstAttention) suggestions.push({ + id: "resolve-review-attention", + eyebrow: "Reduce review risk", + label: firstAttention.title, + summary: firstAttention.why, + outcomeEffect: "The next Factfile can remove or narrow this attention signal and reduce the amount a reviewer must reconstruct.", + deepDive: firstAttention.paths.length + ? `Start with ${firstAttention.paths.join(", ")}. Explain whether each change is necessary for this outcome, then update implementation or scope evidence.` + : "Re-evaluate whether this contribution is one coherent outcome. Split unrelated work or explain why the breadth is necessary.", + basis: `Keyoku raised a ${firstAttention.priority} deterministic attention signal from the exact changed-source snapshot.`, + evidenceRefs: firstAttention.paths, + tradeoffs: ["May add an implementation iteration", "Does not replace the declared outcome checks"], + instruction: `${firstAttention.title}. ${firstAttention.why}${firstAttention.paths.length ? ` Start with: ${firstAttention.paths.join(", ")}.` : ""} Re-run the Keyoku gate and report exactly what changed.`, + recommended: firstAttention.priority === "critical" || firstAttention.priority === "high", + source: "deterministic", + }); + if (snapshot.humanReview.pending > 0) suggestions.push({ + id: "prepare-acceptance", + eyebrow: "Make review decisive", + label: "Prepare the human acceptance pass", + summary: `${snapshot.humanReview.pending} outcome-specific acceptance ${snapshot.humanReview.pending === 1 ? "question remains" : "questions remain"}. Assemble the shortest useful walkthrough for them.`, + outcomeEffect: "The evidence state will not be falsely upgraded, but the accountable reviewer gets a concrete path to make each remaining judgment.", + deepDive: snapshot.outcome.humanCriteria.map((criterion) => `${criterion.description}${criterion.guidance ? ` — ${criterion.guidance}` : ""}`).join(" "), + basis: "The current Factfile has supported automated observations but outcome-specific human acceptance criteria remain pending.", + evidenceRefs: snapshot.outcome.humanCriteria.map((criterion) => `human:${criterion.id}`), + tradeoffs: ["Requires real human judgment", "May reveal another implementation iteration"], + instruction: `Prepare an acceptance walkthrough for these human criteria: ${snapshot.outcome.humanCriteria.map((criterion) => criterion.description).join("; ")}. Point to the most relevant evidence for each criterion, call out what is still unknown, and do not mark any human verdict yourself.`, + source: "deterministic", + }); + if (snapshot.architecture?.components.length) suggestions.push({ + id: "trace-system-impact", + eyebrow: "Understand the system", + label: "Deep-dive the architecture impact", + summary: `Trace this contribution across ${snapshot.architecture.components.length} detected components and explain the changed data or control flow.`, + outcomeEffect: "The next Factfile will make ownership and downstream effects easier to understand; code changes occur only if the analysis exposes a real gap.", + deepDive: "Follow the changed areas through the architecture projection, verify component responsibilities against source, and annotate any boundary that the generated map cannot infer safely.", + basis: `The generated architecture projection contains ${snapshot.architecture.components.length} components and the contribution changes ${snapshot.repository.changedFiles.length} files.`, + evidenceRefs: snapshot.repository.changedFiles.slice(0, 6), + tradeoffs: ["Mostly improves understanding rather than test coverage", "Generated architecture still needs source verification"], + instruction: "Trace the contribution through the current architecture projection. Verify every affected component and relationship against source, update the architecture evidence where it is incomplete, and report any newly discovered risk without inventing dependencies.", + source: "deterministic", + }); + return suggestions.slice(0, 3); +} + +function renderLocalTime(value: string, relative = true): string { + const date = new Date(value); + const fallback = Number.isNaN(date.getTime()) + ? value + : new Intl.DateTimeFormat("en-US", { + timeZone: "UTC", year: "numeric", month: "short", day: "numeric", + hour: "numeric", minute: "2-digit", timeZoneName: "short", + }).format(date); + return ``; +} + +export function renderFactfileHtml(snapshot: GateSnapshot, options: { live?: boolean; sessionToken?: string; history?: FactfileHistoryItem[]; historical?: boolean } = {}): string { + const session = snapshot.session ?? { work: [], decisions: [], instructions: [], agents: [], directions: [], eventCount: 0 }; + const latestHuman = new Map(snapshot.reviews.filter((review) => review.criterionId && review.verdict).map((review) => [review.criterionId!, review])); + const state = snapshot.state === "accepted" + ? { tone: "good", label: "Accepted exact snapshot", result: "Accepted", detail: "A named human accepted this exact source identity." } + : snapshot.state === "ready_for_review" + ? { tone: "good", label: "Ready for acceptance", result: "Acceptance remains explicit", detail: "Every declared criterion passes; an accountable person still owns final acceptance." } + : snapshot.state === "human_review_required" + ? { tone: "wait", label: "Human review remains", result: "Evidence supported", detail: `${snapshot.humanReview.pending} required acceptance ${snapshot.humanReview.pending === 1 ? "judgment remains" : "judgments remain"}. No agent work is necessarily blocked.` } + : snapshot.state === "review_blocked" + ? { tone: "bad", label: "Human review blocked", result: "Blocked", detail: "A named reviewer determined that a required condition is not met." } + : { tone: "bad", label: "Evidence gap", result: "Not ready to accept", detail: `${snapshot.summary.failed} declared ${snapshot.summary.failed === 1 ? "claim is" : "claims are"} unsupported at this snapshot.` }; + const summary = snapshot.contribution.summary ?? snapshot.contribution.title; + const allArtifacts = snapshot.evidence.flatMap((item) => item.presentation?.artifacts ?? []); + const artifactCount = allArtifacts.filter((artifact) => artifact.digest && !artifact.unavailable).length; + const deterministicAttention = snapshot.reviewPlan.filter((item) => item.basis === "deterministic"); + const attentionRows = deterministicAttention.length + ? deterministicAttention.map((item) => `
${esc(item.title)}

${esc(item.why)}

${item.paths.length ? `${item.paths.map(esc).join(" · ")}` : ""}
${esc(item.priority)}
`).join("") + : `
No deterministic attention signal was raised

Review still requires judgment; this only means the declared scope and repository heuristics found no additional hotspot.

baseline
`; + const workRows = session.work.length + ? session.work.map((item) => `
${esc(item.title)}${esc(item.status)}

${esc(item.detail ?? "No additional detail reported.")}

${esc(item.actorId)} · ${renderLocalTime(item.updatedAt)}
`).join("") + : `
No agent has reported work yet. Connected agents use contribution_report_work; this is execution status, not proof.
`; + const pendingDecisions = session.decisions.filter((decision) => decision.status === "pending"); + const resolvedDecisions = session.decisions.filter((decision) => decision.status === "resolved"); + const resolvedDecisionRows = resolvedDecisions.map((decision) => { const option = decision.options.find((candidate) => candidate.id === decision.selectedOptionId); return `
${esc(decision.title)}

${esc(option?.label ?? decision.resolutionNote ?? "Resolved with a custom instruction")}${decision.resolvedBy ? ` · ${esc(decision.resolvedBy)}` : ""}${decision.resolvedAt ? ` · ${renderLocalTime(decision.resolvedAt)}` : ""}

resolved
`; }).join(""); + const instructionRows = session.instructions.map((instruction) => `
  • ${esc(instruction.status)} instruction
    ${esc(instruction.text)}
    ${esc(instruction.id)} · ${renderLocalTime(instruction.createdAt)}${instruction.acknowledgedBy ? ` · acknowledged by ${esc(instruction.acknowledgedBy)}` : ""}
  • `).join("") || "
  • No human instruction has been queued in this session.
  • "; + const connectedAgents = session.agents.filter((agent) => agent.connected); + const agentSummary = session.agents.length ? session.agents.map((agent) => `${esc(agent.name)} · ${agent.connected ? "connected" : `last seen ${renderLocalTime(agent.lastSeenAt)}`}`).join("") : `No agent heartbeat yet · instructions will queue durably`; + const proposedDirections = session.directions ?? []; + const directionSuggestions: DirectionSuggestion[] = proposedDirections.length + ? proposedDirections.map((direction, index) => ({ ...direction, source: "agent" as const, recommended: index === 0 })) + : buildDirectionSuggestions(snapshot); + const history = options.history ?? []; + const historyHref = (id: string): string => { + if (options.sessionToken) return `/snapshots/${encodeURIComponent(id)}.html?token=${encodeURIComponent(options.sessionToken)}`; + return options.historical ? `${encodeURIComponent(id)}.html` : `snapshots/${encodeURIComponent(id)}.html`; + }; + const historyRows = history.slice(0, 6).map((item, index) => `${index === 0 ? "●" : "○"}${item.id === snapshot.id ? "Current snapshot" : item.state.replaceAll("_", " ")}${renderLocalTime(item.generatedAt)} · ${item.passed}/${item.total} checks · ${item.humanPassed}/${item.humanTotal} human${esc(item.worktreeDigest.slice(0, 8))}`).join("") || `
    The first snapshot will appear here after the gate runs.
    `; + + // Evidence, claim by claim (kept as the collapsed deep-dive) ------------------------------ + const claims = snapshot.evidence.map((item) => { + const presentation = item.presentation; + const boundArtifacts = presentation?.artifacts.filter((artifact) => artifact.digest && !artifact.unavailable).length ?? 0; + const codeCount = presentation?.code.length ?? 0; + const media = presentation?.artifacts.filter((artifact) => (artifact.kind === "screenshot" || artifact.kind === "video") && artifact.dataUrl).map((artifact) => { const annotations = artifact.annotations ?? []; const pins = artifact.kind === "screenshot" ? annotations.filter((annotation) => annotation.x !== undefined && annotation.y !== undefined).map((annotation, index) => `${index + 1}`).join("") : ""; const notes = annotations.length ? `
    ${annotations.map((annotation, index) => `
    ${artifact.kind === "video" && annotation.atMs !== undefined ? `${Math.floor(annotation.atMs / 60000)}:${String(Math.floor((annotation.atMs % 60000) / 1000)).padStart(2, "0")}` : `${index + 1}. `}${esc(annotation.label)}${annotation.detail ? ` — ${esc(annotation.detail)}` : ""}
    `).join("")}
    ` : ""; return `
    ${artifact.kind === "video" ? `` : `${esc(artifact.label)}`}${pins}
    ${esc(artifact.label)} · ${esc(artifact.caption)}${artifact.digest ? ` · SHA-256 ${esc(artifact.digest.slice(0, 16))}…` : ""}
    This ${artifact.kind === "video" ? "recording" : "image"} demonstrates observed behavior; it does not independently establish usability or correctness.${notes}
    `; }).join("") ?? ""; + const artifacts = presentation?.artifacts.filter((artifact) => !((artifact.kind === "screenshot" || artifact.kind === "video") && artifact.dataUrl)).map((artifact) => `
    ${artifact.kind === "screenshot" || artifact.kind === "video" ? "Demonstration" : "Supporting artifact"}${artifact.unavailable ? "Unavailable · " : ""}${esc(artifact.label)}${esc(artifact.caption)}${artifact.unavailable ? ` ${esc(artifact.unavailable)}` : ""}${esc(artifact.path)}${artifact.digest ? ` · sha256:${esc(artifact.digest)}` : ""}
    `).join("") ?? ""; + const code = presentation?.code.map((ref) => `
    ${esc(ref.path)}${esc(ref.purpose)}
    `).join("") ?? ""; + const resultLabel = item.pass ? "Supported" : "Gap"; + return `
    ${item.pass ? "✓" : "!"}${esc(item.description)}${esc(presentation?.summary ?? (item.pass ? "The observation matched its declared rule." : "The observation did not match its declared rule."))}${esc(item.verification.kind)}${boundArtifacts ? `${boundArtifacts} ${boundArtifacts === 1 ? "artifact" : "artifacts"}` : ""}${codeCount ? `${codeCount} code ${codeCount === 1 ? "path" : "paths"}` : ""}${item.durationMs}ms${resultLabel}
    What this establishes

    ${esc(presentation?.summary ?? "Only that the observation matched its declared rule at this exact snapshot.")}

    Why it matters

    ${esc(presentation?.whyItMatters ?? "No outcome-specific relevance was supplied. Treat this explanation as incomplete.")}

    ${media}${artifacts ? `
    Inspectable artifacts
    ${artifacts}
    ` : ""}${code ? `
    Relevant implementation
    ${code}
    ` : ""}
    Reproduce this observation${esc(item.verification.reproduce)}
    Open verifier internals · observation, rule, runtime
    Observed\n${esc(printable(item.actual))}
    Rule\n${esc(printable(item.verification.assertion))}

    ${esc(item.verification.label)} · ${item.durationMs}ms${item.error ? ` · ${esc(item.error)}` : ""}

    `; + }).join(""); + const areaNames = new Map(); + for (const file of snapshot.repository.changedFiles) { + const area = file.startsWith("archive/") ? "Archived legacy code" + : file.startsWith("src/") ? "Product code" + : file.startsWith("tests/") ? "Tests" + : file.startsWith("docs/") || file === "README.md" ? "Documentation" + : file.startsWith(".github/") ? "GitHub workflow" + : file.startsWith(".keyoku/") ? "Proof contracts" + : "Project configuration"; + areaNames.set(area, (areaNames.get(area) ?? 0) + 1); + } + const areas = [...areaNames].map(([name, count]) => `
    ${esc(name)}${count} changed ${count === 1 ? "file" : "files"}
    `).join("") || `
    No changed filesThe worktree matches Git head.
    `; + const architecture = snapshot.architecture ? `
    ${renderArchitectureSvg(snapshot.architecture)}
    ` : `

    No architecture projection was captured. This is explicitly unknown, not silently treated as unchanged.

    `; + const people = snapshot.contribution.actors.map((actor) => `
    ${actor.kind === "human" ? "H" : actor.kind === "agent" ? "A" : "O"}
    ${esc(actor.name)}${esc(actor.role ?? actor.kind)}${actor.harness ? ` · ${esc(actor.harness)}` : ""}${actor.model ? ` · ${esc(actor.model)}` : ""}
    `).join(""); + const files = snapshot.repository.changedFiles.map((file) => `
  • ${esc(file)}
  • `).join("") || "
  • Clean Git worktree
  • "; + const constraints = snapshot.outcome.constraints.map((constraint) => `
  • ${esc(constraint)}
  • `).join("") || "
  • No explicit constraints were declared.
  • "; + const limits = (snapshot.contribution.knownLimits?.length ? snapshot.contribution.knownLimits : ["Only the claims shown here were evaluated.", "Passing checks do not establish product fit, maintainability, or universal security.", "Any source change requires a new Factfile."]).map((limit) => `
  • ${esc(limit)}
  • `).join(""); + const reviews = snapshot.reviews.length ? snapshot.reviews.map((review) => `
  • ${esc(review.decision === "accepted" ? "Accepted" : review.criterionId ? `${review.verdict} · ${review.criterionId}` : "Review note")}
    ${esc(review.reviewer.name)} · ${renderLocalTime(review.reviewedAt)}
    ${esc(review.comment)}
  • `).join("") : `
  • No human review has been recorded for this snapshot.
  • `; + + // Hero: visual-proof-first — a filmstrip of bound screenshots/video, or a CLI replay of every probe ---- + const heroFrames = snapshot.evidence.flatMap((item) => (item.presentation?.artifacts ?? []).filter((artifact) => (artifact.kind === "screenshot" || artifact.kind === "video") && artifact.dataUrl && !artifact.unavailable)); + const heroHtml = heroFrames.length + ? `
    +
    ${heroFrames.map((frame, index) => `
    ${frame.kind === "video" ? `` : `${esc(frame.label)}`}
    `).join("")}
    +
    ${esc(heroFrames[0].label)}${esc(heroFrames[0].caption)}
    + ${heroFrames.length > 1 ? `
    ${heroFrames.map((frame, index) => ``).join("")}
    ` : ""} +
    + ` + : `
    ${esc(snapshot.repository.headSha.slice(0, 8))}+${esc(snapshot.repository.worktreeDigest.slice(0, 8))}
    ${snapshot.evidence.map((item, index) => `
    $${esc(item.verification.reproduce)}${item.pass ? "✓" : "✗"} ${item.durationMs}ms
    `).join("")}
    `; + + // Short written summary ------------------------------------------------------------------------ + const summaryBlockHtml = `
    +
    ${esc(state.label)}${esc(state.detail)}
    +
    ${snapshot.summary.passed}/${snapshot.summary.total} automated checks${snapshot.humanReview.passed}/${snapshot.humanReview.total} human decisions
    +
      ${snapshot.evidence.map((item) => `
    • ${item.pass ? "✓" : "✗"}${esc(item.description)}
    • `).join("")}
    +
    `; + + // Human decision — keep judgment distinct from optional agent coordination ---------------------- + const humanInsightItems = snapshot.outcome.humanCriteria.length + ? snapshot.outcome.humanCriteria.map((criterion) => { + const review = latestHuman.get(criterion.id); + const verdict = review?.verdict ?? "pending"; + return `
    ${verdict === "pass" ? "✓" : verdict === "fail" ? "✗" : "?"}${esc(criterion.description)}${esc(verdict)}

    ${esc(review?.comment ?? criterion.guidance ?? "A named human must decide this against the evidence below.")}

    ${review ? `

    ${esc(review.reviewer.name)} · ${renderLocalTime(review.reviewedAt)}

    ` : ""}
    `; + }).join("") + : `
    No outcome-specific judgment questions were declared. A passing command does not silently accept a contribution.
    `; + const blockedInsightItems = pendingDecisions.length + ? pendingDecisions.map((decision) => `
    !${esc(decision.title)}blocked
    What the agent wants${esc(decision.agentIntent)}
    What blocks it${esc(decision.blocker)}
    Why you${esc(decision.whyHuman)}
    If you do nothing${esc(decision.noResponse)}
    ${decision.options.map((option) => `
    ${option.outcomeEffect ? `
    How the outcome changes${esc(option.outcomeEffect)}
    ` : ""}${option.deepDive || option.tradeoffs?.length ? `
    Context and tradeoffs${option.deepDive ? `

    ${esc(option.deepDive)}

    ` : ""}${option.tradeoffs?.length ? `
      ${option.tradeoffs.map((tradeoff) => `
    • ${esc(tradeoff)}
    • `).join("")}
    ` : ""}
    ` : ""}
    `).join("")}

    `).join("") + : `
    No agent work is waiting on you

    Keyoku will put only a material, blocked decision here. Optional steering has its own section below.

    `; + const directionInsightItems = directionSuggestions.length + ? directionSuggestions.map((suggestion, index) => `
    ${esc(suggestion.label)}${esc(suggestion.eyebrow)}

    ${esc(suggestion.summary)}

    How the outcome changes${esc(suggestion.outcomeEffect)}
    Deep-dive context

    ${esc(suggestion.deepDive)}

    Why this is suggested: ${esc(suggestion.basis)}

    ${suggestion.evidenceRefs.length ? `

    Evidence basis: ${suggestion.evidenceRefs.map((reference) => `${esc(reference)}`).join(" · ")}

    ` : ""}${suggestion.tradeoffs.length ? `
      ${suggestion.tradeoffs.map((tradeoff) => `
    • ${esc(tradeoff)}
    • `).join("")}
    ` : ""}
    `).join("") + : `
    No contextual direction has been prepared yet.
    `; + const directionActionsHtml = directionSuggestions.length + ? `

    Write a custom direction — when prepared paths miss your intent

    State what should change, the constraint to preserve, and which evidence should look different afterward.

    ` + : ""; + const humanDecisionHtml = `
    +

    Human decision

    +

    Required judgment

    ${blockedInsightItems}${humanInsightItems}
    +
    `; + + // Evidence is the product: keep a small claim set visible, with coordination subordinate. ------ + const evidenceFold = `
    Evidence & reproduction${snapshot.summary.passed}/${snapshot.summary.total} supported · ${artifactCount} artifacts

    Claim → observation → meaning → limits → reproduction → code.

    ${claims}
    Review attention (deterministic signal, not a verdict)
    ${attentionRows}
    `; + const coordinationFold = directionSuggestions.length + ? `
    Optional agent coordination${directionSuggestions.length} proposed direction${directionSuggestions.length === 1 ? "" : "s"}

    Suggestions are coordination aids, not evidence or verdicts.

    ${directionInsightItems}${directionActionsHtml}
    ` + : ""; + + // Everything else — reachable, collapsed by default ------------------------------------------ + const workFold = `
    Work log${session.work.length} items · ${connectedAgents.length} connected
    ${workRows}
    ${agentSummary}
    `; + const sessionFold = `
    Session & proof history${history.length} snapshot${history.length === 1 ? "" : "s"} · ${session.eventCount} events
    ${resolvedDecisionRows ? `
    Resolved this session
    ${resolvedDecisionRows}
    ` : ""}
    Session instructions
      ${instructionRows}
    Proof history
    ${historyRows}
    `; + const repositoryFold = `
    Repository, scope & provenance${snapshot.repository.changedFiles.length} changed files
    ${areas}
    ${architecture}

    Exact source identity

    Base${esc(snapshot.repository.baseSha)}
    Head${esc(snapshot.repository.headSha)}
    Worktree digest${esc(snapshot.repository.worktreeDigest)}
    Generated${renderLocalTime(snapshot.generatedAt, false)}

    Responsibility

    ${people}

    Human review history

      ${reviews}

    Changed files (${snapshot.repository.changedFiles.length})

      ${files}

    Outcome constraints

      ${constraints}

    Known limits

      ${limits}
    `; + + const logoMark = ``; + + const liveScript = ``; + + return `${esc(snapshot.outcome.title)} · Keyoku +
    keyoku
    ${esc(snapshot.project.name)} · outcome r${snapshot.outcome.revision}${esc(snapshot.repository.headSha.slice(0, 8))}+${esc(snapshot.repository.worktreeDigest.slice(0, 8))}
    +
    + ${options.live || options.historical ? `
    ${options.live ? `Live proof session · ${connectedAgents.length} agent${connectedAgents.length === 1 ? "" : "s"} connected` : "Historical snapshot"}
    ` : ""} + ${esc(summary)} +

    ${esc(snapshot.outcome.title)}

    +

    ${esc(snapshot.outcome.objective)}

    + ${heroHtml} + ${summaryBlockHtml} + ${evidenceFold} + ${humanDecisionHtml} + ${coordinationFold} + ${workFold} + ${sessionFold} + ${repositoryFold} +
    Free, provider-neutral Keyoku · evidence and judgment remain separate${esc(snapshot.id)} · sha256:${esc(snapshot.digest)}
    +
    ${liveScript}`; +} diff --git a/src/deck.ts b/src/deck.ts new file mode 100644 index 0000000..b0aac19 --- /dev/null +++ b/src/deck.ts @@ -0,0 +1,973 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path"; +import { parse } from "yaml"; +import { z } from "zod"; + +import { ArchEdgeSchema, ArchNodeSchema, ArchZoneSchema, ARCH_CSS, ICON_IDS, renderArchSvg } from "./arch.js"; +import { KEYOKU_DIR } from "./contribution.js"; + +// --------------------------------------------------------------------------- +// `keyoku deck` — a config-driven evidence-deck generator. An agent (or a +// human) writes/edits a per-project `.keyoku/deck.yaml`; `keyoku deck build` +// deterministically renders it into ONE self-contained HTML deck per persona +// — no agent calls at build time. Autonomy lives entirely at the planning +// layer (`keyoku deck plan`), which spawns an agent to draft/update the YAML; +// rendering itself is pure templating so the same config always produces the +// same deck. +// --------------------------------------------------------------------------- + +// ---- config schema (keyoku.dev/deck/v1alpha1) ------------------------------ + +const ThemeSchema = z + .object({ mode: z.enum(["auto", "light", "dark"]).default("auto") }) + .default({ mode: "auto" }); + +const FrameCropSchema = z.object({ leftPct: z.number().min(0).max(90) }); + +const SourcesSchema = z.object({ + factfile: z.string().min(1), + demoVideo: z.string().min(1).optional(), + demoVerdict: z.string().min(1).optional(), + framesDir: z.string().min(1).optional(), + frameCrop: FrameCropSchema.optional(), +}); + +const LinkSchema = z.object({ label: z.string().min(1), url: z.string().min(1) }); + +const SECTION_TYPES = ["intro", "slides", "status", "architecture", "signoff", "summary"] as const; +type SectionType = (typeof SECTION_TYPES)[number]; + +const DEFAULT_SECTION_LABEL: Record = { + intro: "Intro", + slides: "Demo", + status: "Status", + architecture: "Architecture", + signoff: "Sign-off", + summary: "Summary", +}; + +const IntroSectionSchema = z.object({ + type: z.literal("intro"), + label: z.string().min(1).optional(), + // The standard opening format is structured: `ask` + `outcome` lists render + // as a fixed "The ask / The outcome" layout every deck shares. `headline`/ + // `body` remain for freeform intros; when ask/outcome are present they win. + headline: z.string().min(1).default("Change request — delivered"), + body: z.string().optional(), + ask: z.array(z.string().min(1)).optional(), + outcome: z.array(z.string().min(1)).optional(), + note: z.string().optional(), + video: z.boolean().default(true), +}); + +const SlideFrameSchema = z.object({ + frame: z.string().min(1), + // Set false to keep the full frame (e.g. a popover that extends into the + // region the global frameCrop would slice off). + crop: z.boolean().default(true), + title: z.string().min(1), + caption: z.string().min(1), +}); + +const SlidesSectionSchema = z.object({ + type: z.literal("slides"), + label: z.string().min(1).optional(), + frames: z.array(SlideFrameSchema).min(1), +}); + +const StatusSectionSchema = z.object({ + type: z.literal("status"), + label: z.string().min(1).optional(), + // Optional plain-language paragraph rendered above the verdict — use it to + // say what this status MEANS for the audience (the factfile supplies the + // machine facts; the lead supplies the framing). + lead: z.string().optional(), + fromFactfile: z.boolean().default(true), +}); + +const SignoffSectionSchema = z.object({ + type: z.literal("signoff"), + label: z.string().min(1).optional(), + // Shown above the decision cards — who to send the response to, deadlines, etc. + note: z.string().optional(), +}); + +// Node/edge/zone shapes come from `./arch.js` (keyoku.dev/arch/v1alpha1) — a +// superset of this section's original inline shape (adds `zone` on nodes, +// `style` on edges, top-level `zones`), so every deck.yaml written against +// the original 10-icon subset still validates unchanged. +const ArchitectureSectionSchema = z.object({ + type: z.literal("architecture"), + label: z.string().min(1).optional(), + diagram: z.object({ + nodes: z.array(ArchNodeSchema).min(1), + edges: z.array(ArchEdgeSchema).default([]), + zones: z.array(ArchZoneSchema).default([]), + }), + explain: z.record(z.string().min(1)).default({}), +}); + +const SummarySectionSchema = z.object({ + type: z.literal("summary"), + label: z.string().min(1).optional(), + bullets: z.array(z.string().min(1)).min(1), + proof: z.string().min(1).optional(), +}); + +const SectionSchema = z.discriminatedUnion("type", [ + IntroSectionSchema, + SignoffSectionSchema, + SlidesSectionSchema, + StatusSectionSchema, + ArchitectureSectionSchema, + SummarySectionSchema, +]); + +const PersonaSchema = z.object({ + sections: z.array(z.enum(SECTION_TYPES)).min(1), + depth: z.enum(["short", "full"]).default("short"), + explainConcepts: z.boolean().default(false), +}); + +const DeckConfigSchema = z.object({ + schemaVersion: z.literal("keyoku.dev/deck/v1alpha1"), + title: z.string().min(1), + project: z.string().min(1), + theme: ThemeSchema, + sources: SourcesSchema, + links: z.array(LinkSchema).default([]), + sections: z.array(SectionSchema).min(1), + personas: z.record(PersonaSchema).refine((p) => Object.keys(p).length > 0, { + message: "at least one persona is required", + }), +}); + +export type DeckConfig = z.infer; +export type DeckSection = z.infer; +export type DeckPersona = z.infer; + +// ---- the (minimal) slice of the Factfile the `status` section consumes ---- + +const FactfileCriterionSchema = z.object({ + id: z.string(), + description: z.string(), + pass: z.boolean(), + durationMs: z.number().optional(), + verification: z.object({ reproduce: z.string().optional() }).partial().optional(), +}); + +const FactfileHumanCriterionSchema = z.object({ + id: z.string(), + description: z.string(), + guidance: z.string().optional(), +}); + +const FactfileWorkItemSchema = z.object({ + id: z.string(), + title: z.string(), + status: z.enum(["queued", "working", "blocked", "done"]), +}); + +const FactfileDirectionSchema = z.object({ + id: z.string(), + eyebrow: z.string().optional(), + label: z.string(), + summary: z.string(), +}); + +const FactfileReviewSchema = z.object({ + criterionId: z.string().optional(), + verdict: z.enum(["pass", "fail"]).optional(), +}); + +const FactfileSchema = z.object({ + state: z.string(), + summary: z.object({ passed: z.number(), failed: z.number(), total: z.number(), verified: z.boolean() }), + humanReview: z.object({ passed: z.number(), failed: z.number(), pending: z.number(), total: z.number() }), + outcome: z.object({ humanCriteria: z.array(FactfileHumanCriterionSchema).default([]) }), + evidence: z.array(FactfileCriterionSchema).default([]), + reviews: z.array(FactfileReviewSchema).default([]), + contribution: z.object({ id: z.string().optional() }).partial().optional(), + digest: z.string().optional(), + session: z + .object({ + work: z.array(FactfileWorkItemSchema).default([]), + directions: z.array(FactfileDirectionSchema).default([]), + }) + .partial() + .default({}), +}); + +type Factfile = z.infer; + +// ---- small local flag helpers (kept local — index.ts's are not exported) -- + +function flagValue(argv: string[], flag: string): string | undefined { + const index = argv.indexOf(flag); + if (index < 0) return undefined; + const value = argv[index + 1]; + return value && !value.startsWith("--") ? value : undefined; +} + +// ---- paths ------------------------------------------------------------ + +function configPath(root: string): string { + return join(root, KEYOKU_DIR, "deck.yaml"); +} + +function resolveSourcePath(root: string, relOrAbs: string): string { + return isAbsolute(relOrAbs) ? relOrAbs : join(root, relOrAbs); +} + +// ---- template (deck init) ---------------------------------------------- + +const DECK_TEMPLATE = `# .keyoku/deck.yaml — Keyoku evidence-deck spec +# Build: keyoku deck build --for [--out ] +# Plan (an agent drafts/updates this file from a natural-language ask): +# keyoku deck plan "a two-minute deck for the exec review, video first" +schemaVersion: keyoku.dev/deck/v1alpha1 +title: +project: +theme: + mode: auto # auto = follow system, with an in-page toggle (system/light/dark, persisted) +sources: + factfile: .keyoku/contributions//factfile.json + # demoVideo: demo-captures/