Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,25 @@ jobs:
- name: Run Go test suite
run: go test ./...

- name: Run Go quality checks
run: |
go vet ./...
if command -v golangci-lint >/dev/null 2>&1; then
golangci-lint run ./...
else
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...
fi

- name: Run shell quality checks
run: |
shellcheck scripts/adapters/cronsnap-git-wrapper.sh
shellcheck scripts/adapters/cronsnap-gh-wrapper.sh
shellcheck scripts/adapters/cronsnap-gog-wrapper.sh
shellcheck scripts/adapters/_common.sh
shellcheck scripts/ci/cs-git-wrapper-smoke.sh
shellcheck scripts/ci/cs-gh-wrapper-smoke.sh
shellcheck scripts/ci/cs-gog-wrapper-smoke.sh
shellcheck scripts/ci/cs-visual-smoke.sh

- name: Run repo validation gates
Expand All @@ -46,6 +59,12 @@ jobs:
- name: Run git shim integration smoke test
run: bash scripts/ci/cs-git-wrapper-smoke.sh

- name: Run gh wrapper integration smoke test
run: bash scripts/ci/cs-gh-wrapper-smoke.sh

- name: Run gog wrapper integration smoke test
run: bash scripts/ci/cs-gog-wrapper-smoke.sh

- name: Validate docs references
run: |
rg -n "hooks enable git|--mode enforced|--mode observe|cronsnap can|action-gate" README.md
Expand Down
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
# Ignore project local docs/assets
/Agent Harness Development Steps.pdf
/.tmp-ab-spike/

# Build artifacts
/cronsnap

Expand Down
11 changes: 0 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,3 @@ The system of record is `docs/`. Keep durable knowledge (specs, plans, logs, dec
intake -> spike (optional) -> plan -> implement -> review -> verify-release -> learn

If this file grows beyond a compact index, move detailed guidance into `docs/` and keep links here.

<!-- he-bootstrap:start -->
## Harness Workflow Entry Points

- Workflow contract: `docs/PLANS.md`
- Specs: `docs/specs/`
- Plans: `docs/plans/`
- Runbooks: `docs/runbooks/`

Workflow phases: intake -> spike (optional) -> plan -> implement -> review -> verify-release -> learn
<!-- he-bootstrap:end -->
66 changes: 59 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,35 @@ CronSnap is a local workflow kernel for AI-assisted development workflows. It en

## What it does today

CronSnap is a local policy kernel for command execution that enforces deterministic, auditable approvals.

- Initializes and stores workflow state in `.harness/state.json`.
- Tracks receipts for named gates (for example `human-signoff`) in `.harness/receipts/<gate>.jsonl`.
- Evaluates whether actions are allowed in `cronsnap can` and `cronsnap status`.
- Writes decision events to `.harness/events.ndjson`.
- Can install a local git shim for command interception (`cronsnap hooks enable git`).
- Can install a local gh (GitHub CLI) shim for comment/PR/issue interception (`cronsnap hooks enable gh`).
- Can install a local gog (gogcli / Google Suite CLI) shim for send/create/write/update/delete interception (`cronsnap hooks enable gog`).
- Records gate proofs in `.harness/receipts/<gate>.jsonl` and decision events in `.harness/events.ndjson`.
- Evaluates whether actions are allowed via `cronsnap can` and the canonical `cronsnap gate` API.
- Installs repo-local shims for `git`, `gh`, and `gog` command interception with `--mode enforced|observe`.
- Supports machine-readable reporting (`--json`) across command and gate checks.

## How it works

```mermaid
flowchart LR
A[initialize policy] --> B[configure action gates]
B --> C[wrappers intercept git/gh/gog]
C --> D[cronsnap gate <action> --mode <observe|enforced> --json]
D -->|allow| E[run original command]
D -->|deny| F[emit DecisionResult to stderr]
F --> G{mode}
G -->|enforced| H[exit non-zero, block]
G -->|observe| I[exit zero, continue]
I --> J[decision/event still logged]
H --> J
```

## Requirements

- Go 1.22+ (or compatible with project toolchain)
- Git repository (for repo-root discovery)
- `jq` runtime dependency for adapter shims (git, gh, and gog wrappers parse kernel JSON output)
- `jq` for CI scripts and lint tooling. Adapter runtime shims use structured stderr payloads and do not require `jq` in normal command paths.

## Install matrix

Expand All @@ -51,6 +67,7 @@ CronSnap is a local workflow kernel for AI-assisted development workflows. It en
- `cronsnap hooks enable gog [--force] [--shim-dir <path>] [--mode enforced|observe] [--json]`
- `cronsnap hooks status [--json]`
- `cronsnap status [--json]`
- `cronsnap gate <action> [--mode enforced|observe] [--json]`
- `cronsnap can <action> [--mode enforced|observe] [--json]`
- `cronsnap run-gate <gate> [--status pass|fail] [--exec <cmd>] [--summary <text>] [--source <provider>] [--json]`
- `cronsnap run-gates <gate=status> ... [--json]`
Expand Down Expand Up @@ -224,15 +241,50 @@ cronsnap run-gate lint-pass --exec "shellcheck scripts/adapters/*.sh"

```bash
go test ./...
go vet ./...
shellcheck scripts/adapters/cronsnap-git-wrapper.sh
shellcheck scripts/adapters/cronsnap-gh-wrapper.sh
shellcheck scripts/adapters/cronsnap-gog-wrapper.sh
shellcheck scripts/adapters/_common.sh
shellcheck scripts/ci/cs-git-wrapper-smoke.sh
shellcheck scripts/ci/cs-gh-wrapper-smoke.sh
shellcheck scripts/ci/cs-gog-wrapper-smoke.sh
bash scripts/ci/cs-docs-lint.sh
bash scripts/ci/cs-plans-lint.sh
bash scripts/ci/cs-spikes-lint.sh
bash scripts/ci/cs-specs-lint.sh
bash scripts/ci/cs-runbooks-lint.sh
bash scripts/ci/cs-git-wrapper-smoke.sh
bash scripts/ci/cs-gh-wrapper-smoke.sh
bash scripts/ci/cs-gog-wrapper-smoke.sh
go test ./cmd/cronsnap -run 'TestCanJSON|TestStatusJSON|TestDoctorJSON|TestCmdIntegrationAllowFlow|TestHooksEnableGitJSONSuccess|TestHooksEnableGitJSONWithoutForce|TestCreateGogShim.*'
```

Example `cronsnap gate` deny payload (from wrapper stderr):

```json
{
"action": "merge",
"decision": "deny",
"reason_code": "missing_receipt",
"reason": "required receipts are missing or failed",
"required_gates": ["human-signoff", "tests-pass"],
"event_id": "..."
}
```

Example `cronsnap status --json` payload:

```json
{
"action": "status",
"decision": "deny",
"reason_code": "blocked",
"reason": "one or more required gates are missing",
"goal": "repo",
"phase": "implement",
"version": "1"
}
```

### Visual QA
Expand Down
58 changes: 58 additions & 0 deletions cmd/cronsnap/cmd_doctor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package main

import (
"flag"
"fmt"
)

func (c *cmdRunner) cmdDoctor(rest []string) {
fs := flag.NewFlagSet("doctor", flag.ContinueOnError)
jsonOut := fs.Bool("json", false, "Emit machine-readable output")
_ = fs.Parse(reorderFlagsFirst(rest))

report := c.services.Doctor(c.rootDir)
if *jsonOut {
c.encodeJSON(report)
return
}

fmt.Fprintf(c.out, "repo: %s\n", report.RepoRoot)
fmt.Fprintf(c.out, "initialized: %v\n", report.Initialized)
fmt.Fprintf(c.out, "git_repo: %v\n", report.GitRepo)
fmt.Fprintf(c.out, "status: %s\n", report.Status)
fmt.Fprintf(c.out, "summary: %s\n", report.DecisionSummary)
if report.Initialized {
fmt.Fprintf(c.out, "goal: %s\n", report.Goal)
fmt.Fprintf(c.out, "phase: %s\n", report.Phase)
fmt.Fprintf(c.out, "version: %s\n", report.Version)
fmt.Fprintf(c.out, "git_integration: %v\n", report.GitIntegration)
if report.GitShimPath != "" {
fmt.Fprintf(c.out, "git_shim: %s\n", report.GitShimPath)
}
fmt.Fprintf(c.out, "gog_integration: %v\n", report.GogIntegration)
if report.GogShimPath != "" {
fmt.Fprintf(c.out, "gog_shim: %s\n", report.GogShimPath)
}
}
if len(report.Blockers) > 0 {
fmt.Fprintln(c.out, "blockers:")
for _, b := range report.Blockers {
fmt.Fprintf(c.out, " - %s\n", b)
}
}
if len(report.Warnings) > 0 {
fmt.Fprintln(c.out, "warnings:")
for _, w := range report.Warnings {
fmt.Fprintf(c.out, " - %s\n", w)
}
}
if len(report.Actionable) > 0 {
fmt.Fprintln(c.out, "next:")
for _, a := range report.Actionable {
fmt.Fprintf(c.out, " - %s: %s\n", a.Command, a.Reason)
}
}
if report.Error != "" {
fmt.Fprintf(c.out, "error: %s\n", report.Error)
}
}
92 changes: 92 additions & 0 deletions cmd/cronsnap/cmd_gate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"context"
"flag"
"fmt"
"io"
"os"
"strings"

"cronsnap/internal/kernel"
)

func (c *cmdRunner) cmdCan(rest []string) {
result, _, jsonOut, err := c.evaluatePolicyDecision(rest, "can")
if err != nil {
return
}
if jsonOut {
c.encodeJSON(result)
return
}

printDecisionText(c.out, result)
}

func (c *cmdRunner) cmdGate(rest []string) {
result, mode, jsonOut, err := c.evaluatePolicyDecision(rest, "gate")
if err != nil {
return
}

if result.Decision == "allow" {
if jsonOut {
c.encodeJSON(result)
} else {
fmt.Fprintln(c.out, "allow")
}
return
}

if err := c.encodeJSONTo(c.errOut, result); err != nil {
fmt.Fprintf(c.errOut, "cronsnap gate error: could not encode decision payload: %v\n", err)
}
if strings.EqualFold(mode, "observe") {
return
}
c.exit(1)
}

func (c *cmdRunner) evaluatePolicyDecision(rest []string, source string) (kernel.DecisionResult, string, bool, error) {
fs := flag.NewFlagSet(source, flag.ContinueOnError)
mode := fs.String("mode", "enforced", "policy mode: enforced|observe")
jsonOut := fs.Bool("json", false, "Emit machine-readable output")
_ = fs.Parse(reorderFlagsFirst(rest))

normalizedMode := strings.ToLower(strings.TrimSpace(*mode))
if normalizedMode != "enforced" && normalizedMode != "observe" {
c.fail("--mode must be 'enforced' or 'observe'", *jsonOut)
return kernel.DecisionResult{}, normalizedMode, *jsonOut, os.ErrInvalid
}
if fs.NArg() < 1 {
c.fail("usage: cronsnap "+source+" <action> [--mode enforced|observe] [--json]", *jsonOut)
return kernel.DecisionResult{}, normalizedMode, *jsonOut, os.ErrInvalid
}

action := fs.Arg(0)
_ = os.Setenv("CRONSNAP_GIT_MODE", normalizedMode)

state, err := c.services.LoadState(context.Background(), c.rootDir)
if err != nil {
c.fail(fmt.Sprintf("failed to read state: %v", err), *jsonOut)
return kernel.DecisionResult{}, normalizedMode, *jsonOut, err
}
result, err := c.services.EvaluateCan(context.Background(), c.rootDir, state, action)
if err != nil {
c.fail(fmt.Sprintf("policy error: %v", err), *jsonOut)
return kernel.DecisionResult{}, normalizedMode, *jsonOut, err
}

return result, normalizedMode, *jsonOut, nil
}

func printDecisionText(w io.Writer, result kernel.DecisionResult) {
_, _ = fmt.Fprintf(w, "%s\n", result.Decision)
if len(result.ReasonCode) > 0 {
_, _ = fmt.Fprintf(w, "%s: %s\n", result.ReasonCode, result.Reason)
}
if len(result.Required) > 0 {
_, _ = fmt.Fprintf(w, "required: %s\n", strings.Join(result.Required, ", "))
}
}
Loading
Loading