ventures: one ledger (Time + notplanned), brutalist index that renders ventures.json + backlog.json
This commit is contained in:
1 parent
e06ff914ad
commit
58853febf9
47 files changed
+3612
-119
No files matched your search
@@ -0,0 +1,65 @@
|
||||
# Finishline Supervisor — launch iteration
|
||||
|
||||
Synthesized from the board's submitted launch plans for this venture: **codex** and **claude**.
|
||||
**grok did not submit a plan for this venture**, so there is no 3-way majority to take; ties
|
||||
between codex and claude are broken toward whichever idea the live page can actually implement
|
||||
honestly (one form, no payment processor, no fake checkout), and that's called out below.
|
||||
|
||||
## Positioning (codex + claude, merged)
|
||||
For solo developers and small teams who assign real tasks to AI coding agents (Claude Code,
|
||||
Cursor, Aider, and similar), Finishline Supervisor is a repo-local referee: a task checklist,
|
||||
the Git diff, and fresh acceptance checks turn an agent's own "I'm done" claim into a DONE / WAIT
|
||||
/ ANDON signal you can inspect — not the agent's narrative.
|
||||
|
||||
## Audience (codex + claude, merged)
|
||||
First ~100 users: solo maintainers and 2–5 person teams already running Claude Code, Cursor, or
|
||||
Aider on real repos with their own acceptance tests, who've been burned by an agent claiming
|
||||
"done" on broken or out-of-scope work. Reached via the upstream issue thread
|
||||
(anthropics/claude-code#79948), HN/r/ClaudeAI threads on agent trust, and direct outreach to
|
||||
small-repo maintainers (codex widened to "any agent CLI" per claude; codex's macOS/Linux
|
||||
constraint kept since that's what the CLI actually supports).
|
||||
|
||||
## Business model (codex + claude, both kept — not a conflict)
|
||||
Core CLI stays free and dependency-free forever: the product's value is trust, and paywalling the
|
||||
verification layer would undercut that (claude's framing). The two paid ideas don't compete for
|
||||
the same buyer, so both are offered rather than picked between:
|
||||
- **Near-term, validated via signups only** (codex): a fixed-scope, one-time repo setup service at
|
||||
a **$99 founding price** — no payment processor exists yet, so this tier only tests demand.
|
||||
- **Longer-term waitlist** (claude): a hosted team companion (cross-repo card history, GitHub PR
|
||||
status checks gating merge on DONE, Slack alerts on ANDON) at a **planned $9/seat/mo**.
|
||||
Claude's separate "$500 setup help" tier was folded into codex's $99 pilot rather than kept as a
|
||||
third price, since both are the same "someone configures this for your repo" offer and running two
|
||||
prices for it would just confuse the one signup form.
|
||||
|
||||
## Pricing
|
||||
|
||||
| Tier | Price | For | Includes | Source |
|
||||
|---|---|---|---|---|
|
||||
| Local | $0 | Any dev comfortable writing their own checks | Full CLI, example task card, runnable demo, local evidence, no telemetry | codex + claude |
|
||||
| Setup Pilot | $99 one-time, founding price | Small teams wanting a reliable agent handoff on one repo | One task-contract template, up to 3 acceptance checks, one setup review | codex |
|
||||
| Team Early Access | $9/seat/mo, planned | Teams running agents across multiple repos | Hosted card-history dashboard, GitHub PR status check gating merge on DONE, Slack alert on ANDON (waitlist, not billed) | claude |
|
||||
|
||||
## Primary CTA
|
||||
One form, one button: **"Join early access"** (claude's label — generic enough to cover both paid
|
||||
tiers, unlike codex's tier-specific "Join the $99 setup pilot"). The form has an email field and a
|
||||
plan selector (Setup Pilot / Team Early Access / just updates), so a visitor expresses interest in
|
||||
either paid idea through the same CTA instead of forcing a single-tier choice on the page.
|
||||
|
||||
## Launch channels (drafts for chair approval — none sent by us)
|
||||
1. Reply on anthropics/claude-code#79948 linking the launch page (claude)
|
||||
2. Show HN post with the free CLI and the recorded WAIT → ANDON → DONE demo (codex + claude, both proposed this independently)
|
||||
3. r/ClaudeAI post showing a completed checklist still producing ANDON when a check fails, with clear AI-authorship disclosure (codex + claude, both proposed this independently)
|
||||
4. Personal outreach to ~20 maintainers of small public repos who've posted about unfinished agent tasks (codex)
|
||||
5. Post in dev-tools Discord/Slack communities already has a presence in (claude)
|
||||
|
||||
## 7-day success metric (merged)
|
||||
At least 15 qualified email signups, including (a) at least 3 explicit picks of Setup Pilot or
|
||||
Team Early Access combined (codex's paid-commitment bar, widened from one tier to both since the
|
||||
page now offers two), and (b) at least 5 signups who also download the CLI from this page or have
|
||||
starred/cloned the repo (claude's intent signal) → keep going, deliver the first pilot.
|
||||
|
||||
## 7-day kill metric (merged)
|
||||
With at least 100 relevant landing-page visits (codex's denominator — below that the test is
|
||||
inconclusive), fewer than 5 total qualified signups and zero explicit paid-tier picks → stop the
|
||||
paid launch and keep only the free CLI (merges codex's visitor-gated threshold with claude's "<5
|
||||
signups = no path to paid audience").
|
||||
@@ -0,0 +1,71 @@
|
||||
# Finishline Supervisor
|
||||
|
||||
A repo-local Finishline card: one JSON file and a dependency-free CLI that reports **DONE / WAIT / ANDON** from a task checklist, Git changes, and fresh acceptance checks. No model calls, agent changes, server, or package install.
|
||||
|
||||
Inspired by [anthropics/claude-code#79948](https://github.com/anthropics/claude-code/issues/79948), closed as not planned: agents need to finish the assigned task and show evidence. This is a small evidence wrapper, not the native enforcement boundary requested upstream.
|
||||
|
||||
## Install
|
||||
|
||||
Requires Python 3.9+, Git, and a POSIX host (macOS/Linux). Copy `finishline.py` anywhere you keep tools, or use it from this checkout. No dependencies. Run inside the target Git repository, which needs at least one commit. Commands always run from the repository root, even when invoked in a subdirectory.
|
||||
|
||||
```sh
|
||||
# From the notplanned checkout:
|
||||
python3 ventures/finishline-supervisor/demo.py
|
||||
python3 -m unittest discover -s ventures/finishline-supervisor -p 'test_*.py' -v
|
||||
```
|
||||
|
||||
For another repository, copy the script to a trusted location and invoke `python3 /path/to/finishline.py` there.
|
||||
|
||||
## Usage
|
||||
|
||||
Write `task.md` with concrete acceptance items:
|
||||
|
||||
```markdown
|
||||
- [ ] Build a greeting CLI
|
||||
- [ ] Verify its exact output
|
||||
- [ ] Document usage
|
||||
```
|
||||
|
||||
Initialize before work starts; explicitly allow files or directories relative to the repo root. Directory scope includes descendants, not similarly named siblings. Acceptance checks are JSON argv arrays, never shell strings:
|
||||
|
||||
```sh
|
||||
python3 /path/to/finishline.py init \
|
||||
--allow task.md --allow app.py --allow README.md \
|
||||
--check '["python3","-c","import subprocess; assert subprocess.check_output([\"python3\",\"app.py\"], text=True).strip() == \"finish line\""]'
|
||||
|
||||
# Inspect the card and its command argv before running checks.
|
||||
python3 /path/to/finishline.py check
|
||||
python3 /path/to/finishline.py check --run-checks
|
||||
```
|
||||
|
||||
Repeat `--check` for more checks (up to 20). `--timeout 30` sets seconds per check (1–300). `--card another.json` precedes `init` or `check` and selects a new contract. Existing cards are never overwritten by `init`. If the task definition or scope changes, explicitly create a new reviewed card; a checkbox toggle is normal progress.
|
||||
|
||||
Each check writes the latest evidence into `finishline.json`, retaining at most 20 runs. The card records the base commit, pinned task hashes, scope, command argv, timestamp, current HEAD, task line links, changed paths, and exit codes. Writes use atomic replacement. Put the card in Git if you want a portable handoff; only that exact card is exempt from the scope check. Prior passing runs never substitute for fresh checks.
|
||||
|
||||
| Signal / exit | Meaning |
|
||||
| --- | --- |
|
||||
| DONE / 0 | A nonempty checklist is complete, task definition matches, changes fit scope, every acceptance check just passed, and checks did not change observed repository evidence. |
|
||||
| WAIT / 1 | Checklist incomplete/missing, or checks have not been explicitly executed. |
|
||||
| ANDON / 2 | Task/scope drift, failed/timed-out/unstartable check, evidence mutation, or invalid/unavailable input. Review the reason and repair the task or command. |
|
||||
|
||||
ANDON takes priority over WAIT. Checks stop at the first failure. Output is JSON; init prints a short confirmation. Errors fail closed and do not echo exception contents.
|
||||
|
||||
## Recorded dogfood
|
||||
|
||||
`demo.py` creates a disposable Git repository and runs the CLI three separate times on the three-step greeting task. It asserts the signals, then removes only its own temporary repository:
|
||||
|
||||
1. Build `app.py`, leaving verification and documentation pending: **WAIT**.
|
||||
2. Write usage documentation; the actual output is still `almost`, so the acceptance check fails: **ANDON**.
|
||||
3. Fix output to `finish line`, verify it, complete the checklist: **DONE**.
|
||||
|
||||
[Recorded terminal output](demo.txt) and [full run evidence](demo-evidence.json) were generated by that runnable demo. Commit IDs/timestamps will differ when rerun. The landing page repeats the recorded output.
|
||||
|
||||
## Limits and trust
|
||||
|
||||
- This observes a repository; it does not orchestrate agents, merge, deploy, call models, edit their settings, or enforce a native permission boundary. An agent with write access can edit this script, the card, or the checks. Keep trusted contracts/checks outside that agent's authority if you need enforcement.
|
||||
- `--run-checks` executes arbitrary trusted local programs with your permissions and environment. There is no sandbox, network restriction, or rollback. Review them first. The timeout kills their POSIX process group; a process that deliberately detaches can escape it. Checks should be read-only; evidence mutations trigger ANDON, but are not reverted.
|
||||
- Git scope compares current tracked content to the pinned base and includes staged/untracked paths. It detects committed changes and renames. It excludes Git-ignored files, reverted changes, files outside the repo, and the selected card. This is current-state evidence, not a complete action audit. Large repositories cost time to hash.
|
||||
- DONE proves only your checklist and checks at that instant. Weak tests can pass broken work. External state/deployment needs an explicit acceptance check; uncommitted work may be DONE. Nothing is automatically published by the tool.
|
||||
- Child stdout/stderr are discarded, not saved or printed. Re-run a failed command directly to debug. Do not put secrets in task text, filenames, or argv: those values are evidence/card content. No log uploads or telemetry.
|
||||
- One writer per card; atomic writes prevent partial JSON, but concurrent runs are unsupported. Interrupted checks leave the previous card intact. Run again for fresh evidence.
|
||||
- Verified with all 14 tests and the three-run demo on the local Mac/Python host and in the existing `teahub-linux-runner:latest` Colima container, with network disabled and a read-only root filesystem. No claim that the CLI was run on Fly or Boring.
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"scenario": "Three-step greeting CLI, three separate supervisor invocations",
|
||||
"runs": [
|
||||
{
|
||||
"at": "2026-10-10T03:24:40.081191+00:00",
|
||||
"head": "d5e4d8a80a91bcc784333207ace1bf4935324c38",
|
||||
"task_sha256": "00b0c9d722db57e6094762e2baa5fa6a7fe3a65941bbd6b6bcf501fc2c6028ce",
|
||||
"status": "WAIT",
|
||||
"reasons": [
|
||||
"task checklist is incomplete",
|
||||
"checks not executed; pass --run-checks after reviewing argv"
|
||||
],
|
||||
"evidence": [
|
||||
"task.md#L1",
|
||||
"task.md#L2",
|
||||
"task.md#L3"
|
||||
],
|
||||
"changed": [
|
||||
"app.py",
|
||||
"task.md"
|
||||
],
|
||||
"checks": []
|
||||
},
|
||||
{
|
||||
"at": "2026-10-10T03:24:40.156618+00:00",
|
||||
"head": "d5e4d8a80a91bcc784333207ace1bf4935324c38",
|
||||
"task_sha256": "49ef3ee174c281ea60a5493131e3bec5228e24efd039eb4c6fcc722f0b227eb9",
|
||||
"status": "ANDON",
|
||||
"reasons": [
|
||||
"task checklist is incomplete",
|
||||
"acceptance check failed"
|
||||
],
|
||||
"evidence": [
|
||||
"task.md#L1",
|
||||
"task.md#L2",
|
||||
"task.md#L3"
|
||||
],
|
||||
"changed": [
|
||||
"README.md",
|
||||
"app.py",
|
||||
"task.md"
|
||||
],
|
||||
"checks": [
|
||||
{
|
||||
"argv": [
|
||||
"/Library/Developer/CommandLineTools/usr/bin/python3",
|
||||
"-c",
|
||||
"import subprocess,sys; assert subprocess.check_output([sys.executable,\"app.py\"], text=True).strip() == \"finish line\""
|
||||
],
|
||||
"exit": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"at": "2026-10-10T03:24:40.441378+00:00",
|
||||
"head": "d5e4d8a80a91bcc784333207ace1bf4935324c38",
|
||||
"task_sha256": "1706e7c498b2f84f5d702c308e5526ab5335eea6c54be881e5fc2443ae236c2e",
|
||||
"status": "DONE",
|
||||
"reasons": [],
|
||||
"evidence": [
|
||||
"task.md#L1",
|
||||
"task.md#L2",
|
||||
"task.md#L3"
|
||||
],
|
||||
"changed": [
|
||||
"README.md",
|
||||
"app.py",
|
||||
"task.md"
|
||||
],
|
||||
"checks": [
|
||||
{
|
||||
"argv": [
|
||||
"/Library/Developer/CommandLineTools/usr/bin/python3",
|
||||
"-c",
|
||||
"import subprocess,sys; assert subprocess.check_output([sys.executable,\"app.py\"], text=True).strip() == \"finish line\""
|
||||
],
|
||||
"exit": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
run 1: WAIT (exit 1)
|
||||
task checklist is incomplete
|
||||
checks not executed; pass --run-checks after reviewing argv
|
||||
evidence: task.md#L1, task.md#L2, task.md#L3
|
||||
run 2: ANDON (exit 2)
|
||||
task checklist is incomplete
|
||||
acceptance check failed
|
||||
acceptance exit: 1
|
||||
evidence: task.md#L1, task.md#L2, task.md#L3
|
||||
run 3: DONE (exit 0)
|
||||
acceptance exit: 0
|
||||
evidence: task.md#L1, task.md#L2, task.md#L3
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Repo-local evidence gate. Python 3.9+, Git, no dependencies."""
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
def git(root, *args):
|
||||
return subprocess.check_output(['git', '-C', str(root), *args], stderr=subprocess.DEVNULL, timeout=30)
|
||||
|
||||
|
||||
def digest(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def local(root, name):
|
||||
p = Path(name)
|
||||
if p.is_absolute() or '..' in p.parts or not p.parts or p == Path('.'):
|
||||
raise ValueError('paths must be relative, nonempty, and contain no ..')
|
||||
resolved = (root / p).resolve()
|
||||
if not resolved.is_relative_to(root):
|
||||
raise ValueError('path escapes repository')
|
||||
return resolved
|
||||
|
||||
|
||||
def changed(root, base, card):
|
||||
tracked = git(root, 'diff', '--no-renames', '--name-only', '-z', base)
|
||||
others = git(root, 'ls-files', '--others', '--exclude-standard', '-z')
|
||||
return sorted(set(os.fsdecode(p) for p in (tracked + others).split(b'\0') if p) - {card})
|
||||
|
||||
|
||||
def snapshot(root, base, card):
|
||||
# Hash tracked and untracked contents; never persist command output or file contents.
|
||||
h = hashlib.sha256(git(root, 'rev-parse', 'HEAD'))
|
||||
for name in changed(root, base, card):
|
||||
h.update(os.fsencode(name) + b'\0')
|
||||
p = root / name
|
||||
if p.is_symlink():
|
||||
h.update(os.fsencode(os.readlink(p)))
|
||||
elif p.is_file():
|
||||
with p.open('rb') as f:
|
||||
for block in iter(lambda: f.read(65536), b''):
|
||||
h.update(block)
|
||||
else:
|
||||
h.update(b'<deleted>')
|
||||
# Include staged content, file modes, deletions and the pinned task even if unchanged.
|
||||
h.update(git(root, 'diff', '--binary', base, '--', '.', ':(exclude)' + card))
|
||||
h.update(git(root, 'diff', '--cached', '--binary', base, '--', '.', ':(exclude)' + card))
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def save(path, data):
|
||||
if path.is_symlink():
|
||||
raise ValueError('card must not be a symlink')
|
||||
fd, name = tempfile.mkstemp(prefix='.finishline-', dir=path.parent)
|
||||
try:
|
||||
with os.fdopen(fd, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write('\n')
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
os.replace(name, path)
|
||||
finally:
|
||||
if os.path.exists(name):
|
||||
os.unlink(name)
|
||||
|
||||
|
||||
def validate(root, data):
|
||||
if data.get('version') != 1:
|
||||
raise ValueError('unsupported card version')
|
||||
local(root, data['task'])
|
||||
if not re.fullmatch(r'[0-9a-f]{64}', data['task_sha256']):
|
||||
raise ValueError('invalid task hash')
|
||||
if not re.fullmatch(r'[0-9a-f]{40,64}', data['base']):
|
||||
raise ValueError('invalid base commit')
|
||||
if (not isinstance(data['allow'], list) or not isinstance(data['checks'], list)
|
||||
or not 1 <= len(data['allow']) <= 100 or not 1 <= len(data['checks']) <= 20):
|
||||
raise ValueError('require 1..100 scope paths and 1..20 acceptance checks')
|
||||
for path in data['allow']:
|
||||
local(root, path)
|
||||
if not isinstance(data['timeout'], int) or not 1 <= data['timeout'] <= 300:
|
||||
raise ValueError('timeout must be 1..300 seconds')
|
||||
for argv in data['checks']:
|
||||
if not isinstance(argv, list) or not argv or not all(isinstance(s, str) and s for s in argv):
|
||||
raise ValueError('each check must be a nonempty JSON argv array')
|
||||
|
||||
|
||||
def run_check(root, argv, timeout):
|
||||
# No shell. Checks are still arbitrary local code, explicitly opted into by the user.
|
||||
with subprocess.Popen(argv, cwd=root, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, start_new_session=True) as p:
|
||||
try:
|
||||
code = p.wait(timeout=timeout)
|
||||
return {'argv': argv, 'exit': code}
|
||||
except subprocess.TimeoutExpired:
|
||||
try:
|
||||
os.killpg(p.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
p.wait()
|
||||
return {'argv': argv, 'exit': None, 'error': 'timeout'}
|
||||
|
||||
|
||||
def evaluate(root, data, card, execute):
|
||||
validate(root, data)
|
||||
task = local(root, data['task'])
|
||||
content = task.read_bytes()
|
||||
items = [(i, m.group(1).lower() == 'x') for i, line in enumerate(content.decode('utf-8').splitlines(), 1)
|
||||
if (m := re.match(r'^\s*[-*] \[([ xX])\]\s+\S', line))]
|
||||
paths = changed(root, data['base'], card)
|
||||
outside = [p for p in paths if not any(p == a.rstrip('/') or p.startswith(a.rstrip('/') + '/') for a in data['allow'])]
|
||||
report = {'at': dt.datetime.now(dt.timezone.utc).isoformat(),
|
||||
'head': git(root, 'rev-parse', 'HEAD').decode().strip(),
|
||||
'task_sha256': digest(content), 'status': 'WAIT', 'reasons': [],
|
||||
'evidence': [f"{data['task']}#L{i}" for i, _ in items],
|
||||
'changed': paths, 'checks': []}
|
||||
if digest(content) != data['task_sha256']:
|
||||
# Checkbox progress is permitted; all other task edits require a new card.
|
||||
normalized = re.sub(rb'(?m)^(\s*[-*] \[)[ xX](\]\s+)', rb'\1 \2', content)
|
||||
if digest(normalized) != data['task_definition_sha256']:
|
||||
report['reasons'].append('task definition changed; create a new reviewed card')
|
||||
if outside:
|
||||
report['reasons'].append('outside scope: ' + ', '.join(outside))
|
||||
if report['reasons']:
|
||||
report['status'] = 'ANDON'
|
||||
return report
|
||||
if not items:
|
||||
report['reasons'].append('task has no checklist items')
|
||||
elif not all(done for _, done in items):
|
||||
report['reasons'].append('task checklist is incomplete')
|
||||
if execute:
|
||||
before = snapshot(root, data['base'], card)
|
||||
for argv in data['checks']:
|
||||
try:
|
||||
result = run_check(root, argv, data['timeout'])
|
||||
except OSError:
|
||||
result = {'argv': argv, 'exit': None, 'error': 'could not start executable'}
|
||||
report['checks'].append(result)
|
||||
if result['exit'] != 0:
|
||||
report['status'] = 'ANDON'
|
||||
report['reasons'].append('acceptance check failed')
|
||||
break
|
||||
if snapshot(root, data['base'], card) != before or task.read_bytes() != content:
|
||||
report['status'] = 'ANDON'
|
||||
report['reasons'].append('acceptance checks changed repository evidence; rerun after review')
|
||||
else:
|
||||
report['reasons'].append('checks not executed; pass --run-checks after reviewing argv')
|
||||
if not report['reasons']:
|
||||
report['status'] = 'DONE'
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--card', default='finishline.json')
|
||||
sub = parser.add_subparsers(dest='command', required=True)
|
||||
init = sub.add_parser('init')
|
||||
init.add_argument('--task', default='task.md')
|
||||
init.add_argument('--allow', action='append', required=True)
|
||||
init.add_argument('--check', action='append', required=True, help='JSON argv array; never shell text')
|
||||
init.add_argument('--timeout', type=int, default=30)
|
||||
check = sub.add_parser('check')
|
||||
check.add_argument('--run-checks', action='store_true')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
root = Path(git(Path.cwd(), 'rev-parse', '--show-toplevel').decode().strip()).resolve()
|
||||
path = local(root, args.card)
|
||||
if path.is_symlink():
|
||||
raise ValueError('card must not be a symlink')
|
||||
if args.command == 'init':
|
||||
if path.exists():
|
||||
raise ValueError('card already exists; use --card for a new reviewed contract')
|
||||
task = local(root, args.task).read_bytes()
|
||||
data = {'version': 1, 'task': local(root, args.task).relative_to(root).as_posix(), 'task_sha256': digest(task),
|
||||
'task_definition_sha256': digest(re.sub(rb'(?m)^(\s*[-*] \[)[ xX](\]\s+)', rb'\1 \2', task)),
|
||||
'base': git(root, 'rev-parse', 'HEAD').decode().strip(),
|
||||
'allow': [Path(a).as_posix() for a in args.allow], 'checks': [json.loads(s) for s in args.check],
|
||||
'timeout': args.timeout, 'runs': []}
|
||||
validate(root, data)
|
||||
save(path, data)
|
||||
print('Created ' + args.card + '; review checks before --run-checks.')
|
||||
return 0
|
||||
data = json.loads(path.read_text())
|
||||
original = path.read_bytes()
|
||||
card_name = path.relative_to(root).as_posix()
|
||||
report = evaluate(root, data, card_name, args.run_checks)
|
||||
if not path.exists() or path.is_symlink() or path.read_bytes() != original:
|
||||
report['status'] = 'ANDON'
|
||||
report['reasons'].append('acceptance checks changed the card; review before rerunning')
|
||||
data['runs'] = (data.get('runs', []) + [report])[-20:]
|
||||
save(path, data)
|
||||
print(json.dumps(report, indent=2))
|
||||
return {'DONE': 0, 'WAIT': 1, 'ANDON': 2}[report['status']]
|
||||
except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) as e:
|
||||
# Do not echo exception contents: malformed cards/commands can contain secrets.
|
||||
print(json.dumps({'status': 'ANDON', 'reasons': ['invalid or unavailable repository/card/task; review configuration'], 'error_type': type(e).__name__}))
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,87 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="description" content="A repo-local evidence card for coding-agent work. Read the task, check scope, run acceptance checks: DONE, WAIT, or ANDON.">
|
||||
<title>Finishline Supervisor · notplanned</title>
|
||||
<style>
|
||||
:root{color-scheme:light dark;--bg:#f4f2ec;--fg:#111;--muted:#6a6861;--line:#cbc7bd;--card:#fbfaf6;--hot:#ff5a1f}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#10100f;--fg:#f3f0e8;--muted:#aaa69d;--line:#393732;--card:#171715;--hot:#ff7a45}}
|
||||
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}.wrap{width:min(980px,calc(100% - 36px));margin:auto}a{color:inherit;text-underline-offset:4px}a:focus-visible{outline:3px solid var(--hot);outline-offset:5px}header{min-height:78px;display:flex;align-items:center;justify-content:space-between;gap:18px;border-bottom:1px solid var(--line)}.brand{font-weight:800;letter-spacing:-.04em}.tiny,.label{font-size:12px;color:var(--muted)}.label{text-transform:uppercase;letter-spacing:.1em}.hero{padding:84px 0 48px}h1{font-size:clamp(46px,8.5vw,88px);line-height:.96;letter-spacing:-.075em;margin:20px 0 28px}h1 span{color:var(--hot)}h2{font-family:ui-sans-serif,system-ui,sans-serif;font-size:30px;line-height:1.1;letter-spacing:-.04em;margin:0 0 18px}p,li{font-family:ui-sans-serif,system-ui,sans-serif;line-height:1.55;font-size:17px}.lead{max-width:760px;font-size:clamp(20px,3vw,28px);line-height:1.25}.actions{display:flex;gap:12px;flex-wrap:wrap;margin:28px 0}.button{display:inline-block;padding:13px 18px;border:1px solid var(--fg);border-radius:999px;text-decoration:none;font-size:14px;font-family:ui-sans-serif,system-ui,sans-serif;cursor:pointer}.primary{background:var(--fg);color:var(--bg)}.signals{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:60px}.card{border:1px solid var(--line);background:var(--card);padding:24px;border-radius:18px}.card strong{font-size:25px}.card p{font-size:16px;color:var(--muted);margin-bottom:0}.andon{border-color:var(--hot)}section{margin-bottom:60px}.split{display:grid;grid-template-columns:1.1fr 1fr;gap:24px}pre{overflow:auto;white-space:pre;max-width:100%;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:22px;font-size:13px;line-height:1.6}code{font-family:inherit}p code,li code{font-family:ui-monospace,monospace;font-size:.9em}.note{color:var(--muted);font-size:15px}footer{border-top:1px solid var(--line);padding:26px 0 42px;color:var(--muted);font-size:12px;line-height:1.7}
|
||||
.join{border:1px solid var(--line);background:var(--card);border-radius:18px;padding:26px;margin:30px 0}.join-row{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:12px}.join-row input[type=email],.join-row select{flex:1 1 180px;min-width:0;padding:12px 14px;border:1px solid var(--line);border-radius:999px;background:var(--bg);color:var(--fg);font-family:ui-sans-serif,system-ui,sans-serif;font-size:14px}.join-note{width:100%;padding:12px 14px;border:1px solid var(--line);border-radius:14px;background:var(--bg);color:var(--fg);font-family:ui-sans-serif,system-ui,sans-serif;font-size:14px;margin-bottom:4px}.join .thanks{display:none;font-family:ui-sans-serif,system-ui,sans-serif;font-size:16px}.join.joined .thanks{display:block}.join.joined .join-form{display:none}.secondary-actions{display:flex;gap:16px;flex-wrap:wrap;margin:14px 0 6px;font-family:ui-sans-serif,system-ui,sans-serif;font-size:14px}.pricing{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:60px}.plan{border:1px solid var(--line);background:var(--card);padding:22px;border-radius:18px;display:flex;flex-direction:column}.plan .tier{font-family:ui-sans-serif,system-ui,sans-serif;font-weight:800;font-size:18px;margin-bottom:4px}.plan .price{color:var(--hot);font-weight:800;font-size:15px;margin-bottom:12px}.plan .for{color:var(--muted);font-size:13px;margin-bottom:10px}.plan ul{margin:0 0 14px;padding-left:18px;font-size:14px}.plan li{margin-bottom:4px}.plan .source{margin-top:auto;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em}
|
||||
@media(max-width:700px){.hero{padding-top:55px}.signals,.split,.pricing{grid-template-columns:1fr}header{flex-wrap:wrap;padding:16px 0}pre{padding:16px;font-size:12px}}
|
||||
</style>
|
||||
</head>
|
||||
<body><div class="wrap">
|
||||
<header><a class="brand" href="/">notplanned</a><span class="tiny">venture 001 · finishline-supervisor</span></header>
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="label">one task. one card. fresh evidence.</div>
|
||||
<h1>Make “done”<br><span>show its work.</span></h1>
|
||||
<p class="lead">Stop trusting an agent's own "I'm done." Read <code>task.md</code>, diff it against Git, run your acceptance checks — get DONE, WAIT, or ANDON you can inspect.</p>
|
||||
<div id="join" class="join">
|
||||
<p class="thanks">Thanks — you're on the list. We'll email you about Finishline Supervisor only; nothing else, never shared.</p>
|
||||
<form class="join-form" action="/api/signup" method="post" enctype="application/x-www-form-urlencoded">
|
||||
<input type="hidden" name="venture" value="finishline-supervisor">
|
||||
<input type="hidden" name="website" value="">
|
||||
<div class="join-row">
|
||||
<input type="email" name="email" required placeholder="you@repo.dev" aria-label="Email address">
|
||||
<select name="plan" aria-label="Plan" required>
|
||||
<option value="setup_pilot">Setup Pilot — $99 founding price</option>
|
||||
<option value="team_early_access">Team Early Access — $9/seat/mo (waitlist)</option>
|
||||
<option value="updates_only">Just the free CLI, send updates</option>
|
||||
</select>
|
||||
<button class="button primary" type="submit">Join early access</button>
|
||||
</div>
|
||||
<input class="join-note" type="text" name="note" placeholder="Optional: which repo, or what you need">
|
||||
</form>
|
||||
</div>
|
||||
<p class="secondary-actions"><a href="finishline.py" download>Download the free CLI ↓</a><a href="README.md">Read the limits</a></p>
|
||||
<p class="note">Python 3.9+ · Git · macOS/Linux · no dependencies · no model calls · no payment processor yet — signing up expresses interest, it never charges you · we only email you about this tool, never shared</p>
|
||||
</section>
|
||||
<div class="signals" aria-label="Exit signals">
|
||||
<article class="card"><strong>WAIT <small>1</small></strong><p>The checklist is incomplete, or fresh checks have not been run.</p></article>
|
||||
<article class="card andon"><strong>ANDON <small>2</small></strong><p>Stop and inspect: a check failed, scope drifted, or the task definition changed.</p></article>
|
||||
<article class="card"><strong>DONE <small>0</small></strong><p>Checklist complete. Scope fits. Every acceptance check just passed without changing observed evidence.</p></article>
|
||||
</div>
|
||||
<section><div class="label">pricing</div><h2>Free forever. Paid tiers are early-access, not a checkout.</h2>
|
||||
<div class="pricing">
|
||||
<article class="plan"><div class="tier">Local</div><div class="price">$0</div><div class="for">Any dev comfortable writing their own checks</div><ul><li>Full CLI, example task card, runnable demo</li><li>Local evidence, no account, no telemetry</li></ul><div class="source">source: codex + claude</div></article>
|
||||
<article class="plan"><div class="tier">Setup Pilot</div><div class="price">$99 one-time, founding price</div><div class="for">Small teams wanting a reliable agent handoff on one repo</div><ul><li>One task-contract template</li><li>Up to 3 acceptance checks</li><li>One setup review</li></ul><div class="source">source: codex</div></article>
|
||||
<article class="plan"><div class="tier">Team Early Access</div><div class="price">$9/seat/mo, planned</div><div class="for">Teams running agents across multiple repos</div><ul><li>Hosted card-history dashboard</li><li>GitHub PR status check gating merge on DONE</li><li>Slack alert on ANDON</li></ul><div class="source">source: claude</div></article>
|
||||
</div>
|
||||
<p class="note">Join above and pick a plan — this is a waitlist, not billing. <a href="LAUNCH.md">Full launch plan and synthesis →</a></p>
|
||||
</section>
|
||||
<section class="split">
|
||||
<div><div class="label">why this exists</div><h2>The task survives the handoff.</h2><p>A new agent session can read the same <code>finishline.json</code>: pinned task definition, base commit, allowed paths, acceptance commands, and the last 20 evidence runs.</p><p>Inspired by <a href="https://github.com/anthropics/claude-code/issues/79948">anthropics/claude-code#79948</a>, a request for an accountability layer closed as <em>not planned</em>.</p><p class="note">This implements a small repo-local slice of that request. It cannot provide the native, unbypassable enforcement boundary the issue asks for.</p></div>
|
||||
<div><div class="label">a real three-run task</div><h2>Build. Check. Finish.</h2><p>We dogfooded the wrapper on a disposable Git repo: build a greeting CLI, verify its exact output, document usage. The first output was <code>almost</code>; the required output was <code>finish line</code>.</p><p><a href="demo-evidence.json">Inspect the full recorded evidence →</a><br><a href="demo.txt">Read the recorded terminal output →</a></p></div>
|
||||
</section>
|
||||
<section><div class="label">recorded CLI output · runnable demo included in source</div><pre aria-label="Recorded three-run terminal session"><code>run 1: WAIT (exit 1)
|
||||
task checklist is incomplete
|
||||
checks not executed; pass --run-checks after reviewing argv
|
||||
evidence: task.md#L1, task.md#L2, task.md#L3
|
||||
run 2: ANDON (exit 2)
|
||||
task checklist is incomplete
|
||||
acceptance check failed
|
||||
acceptance exit: 1
|
||||
evidence: task.md#L1, task.md#L2, task.md#L3
|
||||
run 3: DONE (exit 0)
|
||||
acceptance exit: 0
|
||||
evidence: task.md#L1, task.md#L2, task.md#L3</code></pre><p class="note">Run 1 built the CLI. Run 2 added documentation and exposed the wrong output. Run 3 repaired the output and completed verification. Signals came from three separate CLI invocations, not an agent's narrative.</p></section>
|
||||
<section id="use"><div class="label">start small</div><h2>Your task, your definition of done.</h2><p>Keep the downloaded script in a trusted location. In a Git repository with at least one commit, create <code>task.md</code>:</p><pre><code>- [ ] Build a greeting CLI
|
||||
- [ ] Verify its exact output
|
||||
- [ ] Document usage</code></pre><p>Pin the task before work starts. The check below actually runs <code>app.py</code> and verifies its output. Review the argv in the generated card before opting into execution.</p><pre><code>python3 /path/to/finishline.py init \
|
||||
--allow task.md --allow app.py --allow README.md \
|
||||
--check '["python3","-c","import subprocess; assert subprocess.check_output([\"python3\",\"app.py\"], text=True).strip() == \"finish line\""]'
|
||||
|
||||
# Observe without running commands:
|
||||
python3 /path/to/finishline.py check
|
||||
|
||||
# After reviewing the commands:
|
||||
python3 /path/to/finishline.py check --run-checks</code></pre><p>Mark checklist items <code>[x]</code> as you finish them. Run again at each handoff. A prior passing run never counts as a fresh check. ANDON exits 2, WAIT exits 1, DONE exits 0.</p></section>
|
||||
<section class="card"><div class="label">honest limits</div><h2>Evidence, with a trust boundary you supply.</h2><ul><li>Anyone who can edit the card, script, or checks can bypass this wrapper. It is not a sandbox or native agent enforcement.</li><li>Checks execute local programs with your permissions and environment. They can have side effects; there is no rollback. No automatic merge, deploy, or agent orchestration.</li><li>Scope covers current tracked and untracked Git changes relative to the base. Ignored files, external state, reverted changes, and the card itself are excluded.</li><li>DONE is only as strong as your acceptance checks. A deployment needs its own check. Command output is discarded; evidence contains exit codes and paths. Keep secrets out of task text, filenames, and argv.</li></ul><p class="note">Verified on the local Mac/Python host and in the existing Colima Linux runner container: 14 tests and the three-run demo passed in both. The CLI has not been tested on Fly or Boring.</p></section>
|
||||
</main><footer>notplanned · build what fell off the roadmap<br>Pure wrapper. No Codex or Claude changes. No telemetry on the CLI. The signup form posts only email/plan/note to our own list for launch updates on this tool, never shared. <a href="README.md">Full usage and limits</a>.</footer>
|
||||
</div>
|
||||
<script>if(new URLSearchParams(location.search).get("joined")==="1"){var j=document.getElementById("join");if(j)j.classList.add("joined");}</script>
|
||||
</body></html>
|
||||
Reference in new issue
Block a user