@oneie/claude 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/w1-recon.md +9 -4
- package/agents/w2-decide.md +51 -28
- package/agents/w3-edit.md +70 -78
- package/agents/w4-verify.md +164 -69
- package/commands/browser.md +98 -35
- package/commands/cc-connect.md +6 -0
- package/commands/chat.md +13 -1
- package/commands/close.md +45 -19
- package/commands/create.md +20 -10
- package/commands/db-sync.md +5 -1
- package/commands/deploy.md +174 -87
- package/commands/do-autonomous.md +31 -20
- package/commands/do-improve.md +4 -4
- package/commands/do-show.md +4 -3
- package/commands/do.md +53 -13
- package/commands/go.md +75 -0
- package/commands/improve.md +4 -2
- package/commands/kill.md +6 -8
- package/commands/notify.md +15 -2
- package/commands/oo-push.md +24 -9
- package/commands/rag.md +6 -0
- package/commands/release.md +46 -25
- package/commands/restart.md +5 -4
- package/commands/see.md +45 -35
- package/commands/skill-create.md +38 -5
- package/commands/sync.md +21 -11
- package/hooks/scripts/auto-continue.sh +70 -0
- package/hooks/scripts/branch-pin.sh +85 -0
- package/hooks/scripts/git-add-guard.sh +16 -5
- package/hooks/scripts/post-edit-check.sh +16 -0
- package/hooks/scripts/session-start.sh +13 -8
- package/hooks/scripts/sync-priority-todo.sh +5 -0
- package/package.json +1 -1
- package/rules/api.md +9 -0
- package/rules/design.md +13 -3
- package/rules/documentation.md +30 -71
- package/rules/engine.md +8 -1
- package/rules/ui.md +5 -1
- package/scripts/__pycache__/do-rank.cpython-313.pyc +0 -0
- package/scripts/browser-check.mjs +44 -103
- package/scripts/cc-events-proof.sh +13 -12
- package/scripts/chrome.mjs +381 -0
- package/scripts/do-analyze.sh +109 -4
- package/scripts/do-auto.sh +350 -43
- package/scripts/do-consumer-sweep.sh +72 -0
- package/scripts/do-derives-check.sh +88 -0
- package/scripts/do-fleet.sh +50 -1
- package/scripts/do-killswitch-audit.py +33 -14
- package/scripts/do-promise-lint.sh +150 -4
- package/scripts/do-promise-settle.sh +131 -9
- package/scripts/do-prove.sh +197 -17
- package/scripts/do-rank.py +606 -20
- package/scripts/do-rubric.py +21 -2
- package/scripts/do-signal.sh +157 -17
- package/scripts/do-smoke.sh +9 -0
- package/scripts/do-substrate-check.sh +49 -0
- package/scripts/do-tasks-wire-check.sh +81 -0
- package/scripts/do-tier.sh +90 -5
- package/scripts/do-ui-gate.sh +332 -0
- package/scripts/do-untracked-gate.sh +231 -0
- package/scripts/do-walk.sh +344 -0
- package/scripts/factory-check.sh +747 -0
- package/scripts/factory-repo.sh +1361 -0
- package/scripts/factory-tasks-check.sh +85 -0
- package/scripts/outcome-pull.ts +279 -0
- package/scripts/promise-manifest.mjs +167 -0
- package/scripts/reconcile-allow.txt +5 -0
- package/scripts/rubric-weights.json +64 -0
- package/scripts/spine-canary.sh +20 -0
- package/scripts/typedb-cluster-status.sh +128 -0
- package/scripts/typedb-env.sh +182 -0
- package/scripts/typedb-flap-recorder.sh +85 -0
- package/scripts/typedb-probes/containment-probe.py +88 -0
- package/scripts/typedb-probes/panic-probe-version.py +92 -0
- package/scripts/typedb-probes/panic-probe.py +77 -0
- package/scripts/typedb-scratch.sh +73 -0
- package/scripts/urls-lint.sh +58 -0
- package/scripts/w4-rubric.ts +96 -12
- package/scripts/wf-check.mjs +75 -0
- package/skills/ai-ui/SKILL.md +1 -1
- package/skills/astro/SKILL.md +177 -132
- package/skills/astro/add-content-collection.md +29 -13
- package/skills/astro/check-build.md +19 -13
- package/skills/astro/create-component.md +36 -9
- package/skills/astro/create-page.md +39 -10
- package/skills/astro/optimize-performance.md +36 -8
- package/skills/cli/SKILL.md +254 -220
- package/skills/dev/SKILL.md +1 -1
- package/skills/directory-autofill/SKILL.md +68 -0
- package/skills/docs/SKILL.md +4 -4
- package/skills/mcp/SKILL.md +186 -156
- package/skills/promise-make/SKILL.md +2 -2
- package/skills/puck/SKILL.md +465 -250
- package/skills/react19/SKILL.md +196 -296
- package/skills/reactflow/SKILL.md +387 -412
- package/skills/sdk/SKILL.md +167 -106
- package/skills/shadcn/SKILL.md +245 -201
- package/skills/signal/SKILL.md +5 -0
- package/skills/sui/SKILL.md +6 -1
- package/skills/tutorial/SKILL.md +15 -0
- package/skills/typedb/SKILL.md +427 -217
- package/skills/typedb/examples/python-patterns.py +11 -4
- package/skills/typedb/examples/query-patterns.tql +8 -3
- package/skills/typedb/examples/schema-patterns.tql +4 -2
- package/skills/typedb/reference/migration-2x-3x.md +19 -1
- package/skills/typedb/reference/python-driver.md +17 -1
- package/skills/voice/SKILL.md +117 -0
- package/skills/writer/SKILL.md +29 -12
- package/templates/template-feature.md +26 -1
- package/templates/template-tests.md +4 -0
- package/templates/template-todo.md +65 -10
package/scripts/do-rank.py
CHANGED
|
@@ -25,10 +25,13 @@ Usage:
|
|
|
25
25
|
do-rank.py --top N the N highest READY slugs, one per line
|
|
26
26
|
"""
|
|
27
27
|
import json
|
|
28
|
+
import os
|
|
28
29
|
import re
|
|
29
30
|
import signal
|
|
30
31
|
import subprocess
|
|
31
32
|
import sys
|
|
33
|
+
import urllib.parse
|
|
34
|
+
import urllib.request
|
|
32
35
|
from pathlib import Path
|
|
33
36
|
|
|
34
37
|
# Exit cleanly when piped into head/etc. (closes our stdout early).
|
|
@@ -261,11 +264,284 @@ def tasks_view(active):
|
|
|
261
264
|
mark = " ✓proven" if c["proven"] else ""
|
|
262
265
|
out.append(f" {c['weight']:>3} {c['tags'][0]} + {c['tags'][1]:<16} (×{c['seen']}{mark})")
|
|
263
266
|
out.append("")
|
|
264
|
-
out.append(" → signal(receiver:\"tasks:announce\", payload:task, tags:task.tags); agents follow fn:*+topics, mark() moves weight")
|
|
267
|
+
out.append(" → signal(receiver:\"tasks:announce\", payload:task, tags:task.tags) — live; agents follow fn:*+topics, mark() moves weight")
|
|
265
268
|
out.append(" seed: do-rank.py --tasks-json > text/tasks-seed.json · design: text/tasks-plan.md")
|
|
266
269
|
return out
|
|
267
270
|
|
|
268
271
|
|
|
272
|
+
# Which doc-family suffixes an agent should load to work a task, in read order.
|
|
273
|
+
# The promise first (the contract), then how/what, then the cycles, then the spec.
|
|
274
|
+
_CONTEXT_SUFFIXES = ("", "-plan", "-features", "-ui", "-todo", "-docs")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def task_context_for(p):
|
|
278
|
+
"""Build the context a picked-up task carries — the keystone for auto-pickup.
|
|
279
|
+
|
|
280
|
+
Returns (notes, context_docs):
|
|
281
|
+
• notes — human prose description → `thing.notes` (shown on the row / detail).
|
|
282
|
+
Fallback chain: title em-dash detail → outcome_asserts → title.
|
|
283
|
+
• context_docs— the doc *stems* that exist for this slug → `thing.task-context`
|
|
284
|
+
(multi-valued, one per stem). An agent reads these files to know
|
|
285
|
+
HOW to do the work — literally "get context" for `/do <slug>`.
|
|
286
|
+
"""
|
|
287
|
+
slug, title = p["slug"], (p.get("title") or p["slug"])
|
|
288
|
+
# detail = the part after the em-dash / en-dash in the title ("Tasks — the loop…")
|
|
289
|
+
m = re.match(r"^\s*(.+?)\s+[—–-]\s+(.+)$", title)
|
|
290
|
+
detail = m.group(2).strip() if m else title.strip()
|
|
291
|
+
asserts = (p.get("outcome_asserts") or "").strip()
|
|
292
|
+
notes = detail
|
|
293
|
+
if asserts and asserts.lower() not in detail.lower():
|
|
294
|
+
notes = f"{detail}\n\nDone when: {asserts}" if detail else asserts
|
|
295
|
+
docs = [f"{slug}{sfx}" for sfx in _CONTEXT_SUFFIXES
|
|
296
|
+
if (TEXT / f"{slug}{sfx}.md").exists()]
|
|
297
|
+
return notes, docs
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _gateway_key():
|
|
301
|
+
"""Same lookup as do-signal.sh's _gateway_key() — first file that has it wins.
|
|
302
|
+
|
|
303
|
+
GATEWAY_API_KEY, not SERVER_SECRET: /api/ask/<receiver> decides isServiceCaller off
|
|
304
|
+
env.GATEWAY_API_KEY (one.ie/web/src/pages/api/ask/[...receiver].ts). Presenting
|
|
305
|
+
SERVER_SECRET there resolves to no service caller, no ctx.ownerSlug, and every
|
|
306
|
+
tasks:* resolver answers {error:"workspace required"} — which this fetch used to
|
|
307
|
+
swallow as an empty board, so the ranker silently ran file-only forever."""
|
|
308
|
+
# UPDATE 2026-07-26 — prefer the WORLD key. The service-caller path described above is
|
|
309
|
+
# real but fragile: it grants identity only when the receiver's request schema declares
|
|
310
|
+
# `workspace`/`slug`, because /api/ask reads the nomination off validation.payload —
|
|
311
|
+
# i.e. AFTER zod, which strips undeclared keys. tasks:mine and tasks:everywhere did not
|
|
312
|
+
# declare it, so GATEWAY_API_KEY could never address them (fixed in 73dbeca7a, live only
|
|
313
|
+
# after a one-prod deploy).
|
|
314
|
+
#
|
|
315
|
+
# A world key (`one-`/`osk_`) has no such dependency: verifyWorldKey resolves it to an
|
|
316
|
+
# actual actor, so ctx.ownerSlug comes from the key itself and no nomination is needed.
|
|
317
|
+
# It is what the MCP server has always used (ONEIE_API_KEY — packages/mcp/src/env.ts)
|
|
318
|
+
# and it reads the board TODAY. Verified 2026-07-26: POST /api/ask/tasks:everywhere
|
|
319
|
+
# returns ok:true with the world key and "forbidden: authentication required" with
|
|
320
|
+
# GATEWAY_API_KEY. World key first; GATEWAY_API_KEY stays as the fallback.
|
|
321
|
+
candidates = [os.environ.get("DO_ENV_FILE", "")] if os.environ.get("DO_ENV_FILE") else []
|
|
322
|
+
candidates += [str(ROOT / "one.ie/web/.env"), str(ROOT / "one.ie/web/.secrets.generated.local"),
|
|
323
|
+
str(ROOT / "one.ie/web/.dev.vars")]
|
|
324
|
+
# An explicit env var wins over any file — how a fleet/CI run injects its own identity.
|
|
325
|
+
for var in ("ONEIE_API_KEY", "ONE_API_KEY", "GATEWAY_API_KEY"):
|
|
326
|
+
if os.environ.get(var):
|
|
327
|
+
return os.environ[var].strip()
|
|
328
|
+
|
|
329
|
+
# Rank by KEY GENERATION, not by which file we happened to read first. Both `one-` and
|
|
330
|
+
# legacy `osk_` authenticate (ok:true either way), but they resolve to DIFFERENT actors —
|
|
331
|
+
# and on 2026-07-26 the `osk_` key in one.ie/web/.env resolved to an actor that could see
|
|
332
|
+
# nothing, so every board read came back green-and-empty while the operator's own board
|
|
333
|
+
# showed 11 open tasks. A green empty read is the worst failure this script has: it is
|
|
334
|
+
# indistinguishable from "no work queued". So: modern world key > legacy > service secret.
|
|
335
|
+
found = {"one-": "", "osk_": "", "gw": ""}
|
|
336
|
+
|
|
337
|
+
def offer(tok):
|
|
338
|
+
if tok.startswith("one-"):
|
|
339
|
+
found["one-"] = found["one-"] or tok
|
|
340
|
+
elif tok.startswith("osk_"):
|
|
341
|
+
found["osk_"] = found["osk_"] or tok
|
|
342
|
+
|
|
343
|
+
# The working credential lives in the MCP server config, not in any repo .env — that is
|
|
344
|
+
# what the MCP tools authenticate with, and it is the one proven to read the board.
|
|
345
|
+
# Read it in place; never copy key bytes into a repo file.
|
|
346
|
+
mcp_cfg = Path.home() / ".claude.json"
|
|
347
|
+
if mcp_cfg.exists():
|
|
348
|
+
try:
|
|
349
|
+
def walk(o):
|
|
350
|
+
if isinstance(o, dict):
|
|
351
|
+
for k, v in o.items():
|
|
352
|
+
if k in ("ONEIE_API_KEY", "ONE_API_KEY") and isinstance(v, str) and v.strip():
|
|
353
|
+
offer(v.strip())
|
|
354
|
+
else:
|
|
355
|
+
walk(v)
|
|
356
|
+
elif isinstance(o, list):
|
|
357
|
+
for v in o:
|
|
358
|
+
walk(v)
|
|
359
|
+
walk(json.loads(mcp_cfg.read_text(errors="ignore")))
|
|
360
|
+
except Exception:
|
|
361
|
+
pass # a malformed config must never break ranking
|
|
362
|
+
|
|
363
|
+
for f in candidates:
|
|
364
|
+
p = Path(f)
|
|
365
|
+
if not p.exists():
|
|
366
|
+
continue
|
|
367
|
+
for line in p.read_text(errors="ignore").splitlines():
|
|
368
|
+
for var in ("ONEIE_API_KEY=", "ONE_API_KEY="):
|
|
369
|
+
if line.startswith(var):
|
|
370
|
+
offer(line.split("=", 1)[1].strip().strip('"'))
|
|
371
|
+
if line.startswith("GATEWAY_API_KEY=") and not found["gw"]:
|
|
372
|
+
found["gw"] = line.split("=", 1)[1].strip().strip('"')
|
|
373
|
+
|
|
374
|
+
return found["one-"] or found["osk_"] or found["gw"]
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def fetch_tasks_everywhere(timeout=None):
|
|
378
|
+
"""Board-origin open tasks — the substrate half of the ranked queue (tasks-do C2).
|
|
379
|
+
|
|
380
|
+
Returns (rows, reason): rows is the raw `tasks:everywhere` list, reason is "" on a
|
|
381
|
+
real answer or a SHORT diagnostic on any degrade. The ranker still never crashes and
|
|
382
|
+
still falls back to file-only — but the caller can now tell "no key configured" from
|
|
383
|
+
"auth rejected" from "the board is genuinely empty" (closed-loop rule: no silent
|
|
384
|
+
returns). TASKS_EVERYWHERE marker below is what the demo gate greps for."""
|
|
385
|
+
# tasks:everywhere is a four-phase read (actor groups → task scan → strength reads);
|
|
386
|
+
# measured 10.9s against a warm dev server. The old 5s default meant that even a
|
|
387
|
+
# correctly-authenticated ranker timed out every time — the auth bug was masking a
|
|
388
|
+
# latency bug.
|
|
389
|
+
#
|
|
390
|
+
# Two budgets, because this script has two very different callers. The SessionStart
|
|
391
|
+
# hook and the *-todo.md PostToolUse hook run it on EVERY session open and EVERY plan
|
|
392
|
+
# edit; blocking those for 25s to enrich a ranking that degrades gracefully is a bad
|
|
393
|
+
# trade, so hook-driven runs get a short budget and fall back to file-only. An explicit
|
|
394
|
+
# queue read (/do next, the fleet) waits for the real answer.
|
|
395
|
+
if timeout is None:
|
|
396
|
+
default = "4" if os.environ.get("DO_RANK_HOOK") else "25"
|
|
397
|
+
timeout = float(os.environ.get("DO_RANK_BOARD_TIMEOUT", default))
|
|
398
|
+
secret = _gateway_key()
|
|
399
|
+
if not secret:
|
|
400
|
+
return [], "no GATEWAY_API_KEY in one.ie/web/.env" # TASKS_EVERYWHERE: file-only fallback
|
|
401
|
+
url = os.environ.get("DO_SIGNAL_URL", "https://one.ie") + "/api/ask/tasks:everywhere"
|
|
402
|
+
# `workspace` is load-bearing, not decoration: /api/ask reads the service caller's
|
|
403
|
+
# nominated slug off payload.slug|payload.workspace and that becomes ctx.ownerSlug.
|
|
404
|
+
# Omit it and tasks:everywhere answers {ok:false,error:"forbidden: authentication
|
|
405
|
+
# required"} because a service call carries no session actor of its own.
|
|
406
|
+
ws = os.environ.get("CC_TASKS_WORKSPACE", "one")
|
|
407
|
+
body = json.dumps({"data": {"limit": 200, "workspace": ws}}).encode()
|
|
408
|
+
req = urllib.request.Request(
|
|
409
|
+
url, data=body, method="POST",
|
|
410
|
+
headers={
|
|
411
|
+
"Content-Type": "application/json",
|
|
412
|
+
"Authorization": f"Bearer {secret}",
|
|
413
|
+
# Cloudflare answers the default urllib UA with 403 "error code: 1010"
|
|
414
|
+
# (browser-integrity block) before the Worker ever runs. curl gets through,
|
|
415
|
+
# which is why do-signal.sh never hit this and the ranker always did.
|
|
416
|
+
"User-Agent": "one-do-rank/1.0 (+https://one.ie)",
|
|
417
|
+
},
|
|
418
|
+
)
|
|
419
|
+
try:
|
|
420
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
421
|
+
payload = json.loads(r.read().decode())
|
|
422
|
+
except urllib.error.HTTPError as e: # TASKS_EVERYWHERE: fetch failed — file-only fallback
|
|
423
|
+
detail = "CF bot block (UA rejected)" if e.code == 403 else ""
|
|
424
|
+
return [], f"HTTP {e.code}{' — ' + detail if detail else ''}"
|
|
425
|
+
except Exception as e: # TASKS_EVERYWHERE: fetch failed — file-only fallback
|
|
426
|
+
return [], f"unreachable ({type(e).__name__})"
|
|
427
|
+
if not isinstance(payload, dict):
|
|
428
|
+
return [], "malformed response (not an object)"
|
|
429
|
+
# /api/ask wraps every answer: {outcome, result:{ok, tasks}, signalId, receiver}.
|
|
430
|
+
# Reading ok/tasks off the TOP level (as this did) makes even a good answer look
|
|
431
|
+
# like a failure — the merge could never have fired, key or no key.
|
|
432
|
+
inner = payload.get("result")
|
|
433
|
+
inner = inner if isinstance(inner, dict) else payload
|
|
434
|
+
if not inner.get("ok"):
|
|
435
|
+
err = str(inner.get("error") or payload.get("error") or payload.get("outcome") or "unknown")
|
|
436
|
+
hint = ""
|
|
437
|
+
if "authentication" in err or err == "workspace required":
|
|
438
|
+
# This used to read "local key may not match prod", which is WRONG and cost two
|
|
439
|
+
# sessions chasing a secret that was never broken (73dbeca7a). A `forbidden` here
|
|
440
|
+
# is almost never the key: /api/ask resolves a service caller's workspace from
|
|
441
|
+
# validation.payload.slug|workspace — i.e. AFTER zod has run — and z.object()
|
|
442
|
+
# strips any key the request schema doesn't declare. If the receiver's schema
|
|
443
|
+
# omits `workspace`, `nominated` is undefined, ctx.ownerSlug stays empty, and you
|
|
444
|
+
# get this exact error no matter which key you present. Browser calls are immune
|
|
445
|
+
# (locals.slug supplies the workspace), so it fails only for the harness.
|
|
446
|
+
hint = (
|
|
447
|
+
" — check the RECEIVER SCHEMA before the key: /api/ask reads the nominated"
|
|
448
|
+
" workspace off validation.payload (post-zod), so a receiver whose request"
|
|
449
|
+
" schema omits `workspace` can never be addressed by a service caller."
|
|
450
|
+
" Verify the key separately against a receiver that DOES declare it."
|
|
451
|
+
)
|
|
452
|
+
return [], f"receiver refused: {err}{hint}"
|
|
453
|
+
rows = inner.get("tasks", [])
|
|
454
|
+
if not isinstance(rows, list):
|
|
455
|
+
return [], "malformed response (tasks is not a list)"
|
|
456
|
+
# tasks:everywhere answers "workspaces I BELONG TO" — assignment/follow/ownership. A key
|
|
457
|
+
# whose actor belongs to nothing (the MCP bootstrap agent is one) gets ok:true and an
|
|
458
|
+
# empty list while the operator's own board is full. That green-empty read is worse than
|
|
459
|
+
# a refusal: it is indistinguishable from "no work queued", and it is exactly how the
|
|
460
|
+
# ranker ran file-only for months. So an empty answer is not accepted as the final word —
|
|
461
|
+
# fall back to the WORKSPACE BOARD read, which is what /u/<slug>/tasks and the MCP
|
|
462
|
+
# tasks_list tool both make (GET /api/things?type=task&workspace=…). Verified 2026-07-26:
|
|
463
|
+
# everywhere → 0 rows, board → 11.
|
|
464
|
+
if not rows:
|
|
465
|
+
board, why = fetch_workspace_board(timeout=timeout)
|
|
466
|
+
if board:
|
|
467
|
+
return board, ""
|
|
468
|
+
if why:
|
|
469
|
+
return [], f"everywhere empty; board read also failed ({why})"
|
|
470
|
+
return rows, ""
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def fetch_workspace_board(timeout=None, workspace=None):
|
|
474
|
+
"""The workspace board — every task on it, not just what this actor subscribes to.
|
|
475
|
+
|
|
476
|
+
Same read the /u/<slug>/tasks page and the MCP tasks_list tool make. Returns
|
|
477
|
+
(rows, reason) on the same contract as fetch_tasks_everywhere: no silent returns."""
|
|
478
|
+
if timeout is None:
|
|
479
|
+
timeout = float(os.environ.get("DO_RANK_BOARD_TIMEOUT", "25"))
|
|
480
|
+
secret = _gateway_key()
|
|
481
|
+
if not secret:
|
|
482
|
+
return [], "no world key"
|
|
483
|
+
ws = workspace or os.environ.get("CC_TASKS_WORKSPACE", "one")
|
|
484
|
+
base = os.environ.get("DO_SIGNAL_URL", "https://one.ie")
|
|
485
|
+
q = urllib.parse.urlencode({"type": "task", "workspace": ws, "status": "open", "limit": 200})
|
|
486
|
+
req = urllib.request.Request(
|
|
487
|
+
f"{base}/api/things?{q}", method="GET",
|
|
488
|
+
headers={
|
|
489
|
+
"Authorization": f"Bearer {secret}",
|
|
490
|
+
# Cloudflare 403s the default urllib UA before the Worker runs.
|
|
491
|
+
"User-Agent": "one-do-rank/1.0 (+https://one.ie)",
|
|
492
|
+
},
|
|
493
|
+
)
|
|
494
|
+
try:
|
|
495
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
496
|
+
payload = json.loads(r.read().decode())
|
|
497
|
+
except urllib.error.HTTPError as e:
|
|
498
|
+
return [], f"HTTP {e.code}"
|
|
499
|
+
except Exception as e:
|
|
500
|
+
return [], f"unreachable ({type(e).__name__})"
|
|
501
|
+
rows = payload.get("things") if isinstance(payload, dict) else None
|
|
502
|
+
if not isinstance(rows, list):
|
|
503
|
+
return [], "malformed response (things is not a list)"
|
|
504
|
+
return rows, ""
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def board_task_entry(row):
|
|
508
|
+
"""Shape one tasks:everywhere row as a plan-like dict — the same keys tasks_view()/
|
|
509
|
+
tasks_seed() already read off a local todo's `plans[slug]` entry, so a board-created
|
|
510
|
+
task renders in the SAME ranked list with no second code path."""
|
|
511
|
+
tags = [t for t in (row.get("tags") or []) if not t.startswith(("workspace:", "@"))]
|
|
512
|
+
slug_tag = next((t[len("slug:"):] for t in tags if t.startswith("slug:")), "")
|
|
513
|
+
slug = slug_tag or str(row.get("id", "")).split(":", 1)[-1]
|
|
514
|
+
dept = classify_dept(slug, {"title": row.get("name", ""), "tags": tags})
|
|
515
|
+
weight = row.get("weight")
|
|
516
|
+
weight = max(1, min(100, round(weight))) if isinstance(weight, (int, float)) else 50
|
|
517
|
+
return {
|
|
518
|
+
"slug": slug, "title": row.get("name", slug), "outcome_asserts": "",
|
|
519
|
+
"tier": "?", "tags": tags, "priority": row.get("priority", 0) or 0,
|
|
520
|
+
"depends_on": [], "done_boxes": 0, "total_boxes": 0, "is_done": False,
|
|
521
|
+
"gated": False, "strategic": 0, "proximity": 0.0, "momentum": 0.0,
|
|
522
|
+
"_dirs": [], "cycles": [], "dept": dept, "leverage": 0,
|
|
523
|
+
"weight": weight, "waiting_on": [], "_board_origin": True,
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def merge_board_tasks(active, plans):
|
|
528
|
+
"""Merge tasks:everywhere rows into the local `active` list. A board row carrying a
|
|
529
|
+
`slug:` tag that matches a local todo file is authoritative — the local file entry is
|
|
530
|
+
dropped (task-paths.json becomes a cache, not a second source of truth); a row with no
|
|
531
|
+
local match is a pure board-created task and is simply added. Empty/failed fetch is a
|
|
532
|
+
no-op — file-only ranking, unchanged."""
|
|
533
|
+
rows, reason = fetch_tasks_everywhere()
|
|
534
|
+
if not rows:
|
|
535
|
+
why = reason or "board is empty (0 open tasks)"
|
|
536
|
+
print(f"[do-rank] tasks:everywhere: 0 rows — {why} — file-only fallback", file=sys.stderr)
|
|
537
|
+
return active
|
|
538
|
+
board_entries = [board_task_entry(r) for r in rows]
|
|
539
|
+
matched_slugs = {e["slug"] for e in board_entries if e["slug"] in plans}
|
|
540
|
+
print(f"[do-rank] tasks:everywhere: merged {len(board_entries)} row(s), "
|
|
541
|
+
f"{len(matched_slugs)} superseding a local todo", file=sys.stderr)
|
|
542
|
+
return [p for p in active if p["slug"] not in matched_slugs] + board_entries
|
|
543
|
+
|
|
544
|
+
|
|
269
545
|
def tasks_seed(active):
|
|
270
546
|
"""The same data shaped to the substrate, so testnet seeding is a LOAD not a
|
|
271
547
|
reshape: task→thing+tag, combo-weight→path-context.context-strength, payload→signal."""
|
|
@@ -282,6 +558,11 @@ def tasks_seed(active):
|
|
|
282
558
|
"tid": f"task:{p['slug']}",
|
|
283
559
|
"thingType": "task",
|
|
284
560
|
"name": p["title"] or p["slug"],
|
|
561
|
+
# context = what the task IS + which docs load it, so an agent (or a
|
|
562
|
+
# human) that picks it up knows the goal and where to read. notes →
|
|
563
|
+
# thing.notes (prose); contextDocs → thing.task-context (doc stems).
|
|
564
|
+
"notes": task_context_for(p)[0],
|
|
565
|
+
"contextDocs": task_context_for(p)[1],
|
|
285
566
|
"tag": _task_tags(p),
|
|
286
567
|
"taskStatus": "blocked" if p.get("waiting_on") else "open",
|
|
287
568
|
"taskPriority": round(p["priority"] / 3, 2),
|
|
@@ -305,7 +586,7 @@ def tasks_seed(active):
|
|
|
305
586
|
"shape": "signal(sender, receiver, payload, tags)",
|
|
306
587
|
"receiver": "tasks:announce",
|
|
307
588
|
"routing": "tag-intersection fan-out (one.ie/web/src/lib/world-receivers.ts) + subscriptions:register (declared, not yet persisted)",
|
|
308
|
-
"note": "
|
|
589
|
+
"note": "tasks:* receivers are LIVE (one.ie/web/src/lib/resolvers/tasks.ts) — create/claim/status/link/launch and the announce fan-out. This seed is the bulk-load shape; weights here are what a stake moves.",
|
|
309
590
|
},
|
|
310
591
|
}
|
|
311
592
|
|
|
@@ -343,7 +624,11 @@ def parse_frontmatter(text):
|
|
|
343
624
|
if not m:
|
|
344
625
|
return fm
|
|
345
626
|
body = m.group(1)
|
|
346
|
-
|
|
627
|
+
# outcome_asserts is a single-line quoted string that describes what "done" looks
|
|
628
|
+
# like in the reader's language — the richest one-line description a task can carry.
|
|
629
|
+
# Pulled here (not just title/tier/mode/priority) so tasks_seed can hand an agent
|
|
630
|
+
# real context: what the task IS, not just its name. See § "context on task".
|
|
631
|
+
for key in ("title", "tier", "mode", "priority", "outcome_asserts"):
|
|
347
632
|
km = re.search(rf"^{key}:\s*(.+)$", body, re.MULTILINE)
|
|
348
633
|
if km:
|
|
349
634
|
val = km.group(1)
|
|
@@ -599,15 +884,22 @@ def load_task_paths():
|
|
|
599
884
|
|
|
600
885
|
def path_momentum(slug, paths):
|
|
601
886
|
"""Momentum from path strength (the closed-loop source), on the same 0–3 scale as
|
|
602
|
-
learnings momentum so scoring is unchanged. Strength has
|
|
887
|
+
learnings momentum so scoring is unchanged. Strength has three writers, all summed:
|
|
603
888
|
• `closed` — reconciled from learnings.md closes by --sync-closes (idempotent)
|
|
604
889
|
• `strength`/`resistance` — LIVE marks (agent picks, manual --mark)
|
|
605
|
-
|
|
606
|
-
|
|
890
|
+
• `prod.strength`/`prod.resistance` — production outcomes pulled in by
|
|
891
|
+
outcome-pull.ts (text/self-improving-outcome.md): a settled promise, a /do
|
|
892
|
+
cycle that really closed in a live workspace, or a graded delivery. Full
|
|
893
|
+
overwrite per pull, never accumulated — see outcome-pull.ts's merge law.
|
|
894
|
+
net = closed + strength − resistance + prod.strength − prod.resistance. Returns
|
|
895
|
+
None when this slug has no path yet → caller falls back to learnings.md
|
|
896
|
+
(bootstrap for never-closed, never-marked work)."""
|
|
607
897
|
e = paths.get(f"task:{slug}")
|
|
608
898
|
if not e:
|
|
609
899
|
return None
|
|
610
|
-
|
|
900
|
+
prod = e.get("prod") or {}
|
|
901
|
+
net = max(0.0, float(e.get("closed", 0)) + float(e.get("strength", 0)) - float(e.get("resistance", 0))
|
|
902
|
+
+ float(prod.get("strength", 0)) - float(prod.get("resistance", 0)))
|
|
611
903
|
return round(min(3.0, net / 2.0), 2)
|
|
612
904
|
|
|
613
905
|
|
|
@@ -684,6 +976,100 @@ def momentum_score(slug, log, ref):
|
|
|
684
976
|
return round(min(3.0, score), 2)
|
|
685
977
|
|
|
686
978
|
|
|
979
|
+
# The eight-field close-line vector (text/rubrics.md § Code Rubric — eight dimensions).
|
|
980
|
+
# Fixed field order, self-labeling short codes → the full dimension name do-rank prints.
|
|
981
|
+
DIM_FIELDS = [
|
|
982
|
+
("sec", "security"),
|
|
983
|
+
("simp", "simplicity"),
|
|
984
|
+
("struct", "structure"),
|
|
985
|
+
("speed", "speed"),
|
|
986
|
+
("integ", "integration"),
|
|
987
|
+
("reuse", "code-reuse"),
|
|
988
|
+
("test", "test-coverage"),
|
|
989
|
+
("ux", "ux"),
|
|
990
|
+
]
|
|
991
|
+
_VECTOR_RE = re.compile(
|
|
992
|
+
r"\[sec(\d*\.\d+)\s+simp(\d*\.\d+)\s+struct(\d*\.\d+)\s+speed(\d*\.\d+)\s+"
|
|
993
|
+
r"integ(\d*\.\d+)\s+reuse(\d*\.\d+)\s+test(\d*\.\d+)\s+ux(\d*\.\d+)\]"
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def parse_vector(line):
|
|
998
|
+
"""The eight-field close-line vector, if present: {dim_name: float}. None for a
|
|
999
|
+
close-line with no vector (every close before the self-improving-ratchet plan —
|
|
1000
|
+
back-compat: those still count toward the composite trend only)."""
|
|
1001
|
+
m = _VECTOR_RE.search(line)
|
|
1002
|
+
if not m:
|
|
1003
|
+
return None
|
|
1004
|
+
return {name: float(v) for (_short, name), v in zip(DIM_FIELDS, m.groups())}
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def trailing_median(values):
|
|
1008
|
+
"""Median of a list of floats; 0.0 for an empty list (no prior closes yet)."""
|
|
1009
|
+
if not values:
|
|
1010
|
+
return 0.0
|
|
1011
|
+
s = sorted(values)
|
|
1012
|
+
n = len(s)
|
|
1013
|
+
mid = n // 2
|
|
1014
|
+
return s[mid] if n % 2 else round((s[mid - 1] + s[mid]) / 2.0, 4)
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def _slope(points):
|
|
1018
|
+
"""Least-squares slope of value-over-day-offset. 0.0 with fewer than 2 points
|
|
1019
|
+
(insufficient data to trend — printed as flat, not a false rising/slipping)."""
|
|
1020
|
+
n = len(points)
|
|
1021
|
+
if n < 2:
|
|
1022
|
+
return 0.0
|
|
1023
|
+
xs = [p[0] for p in points]
|
|
1024
|
+
ys = [p[1] for p in points]
|
|
1025
|
+
mx = sum(xs) / n
|
|
1026
|
+
my = sum(ys) / n
|
|
1027
|
+
num = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
|
|
1028
|
+
den = sum((x - mx) ** 2 for x in xs)
|
|
1029
|
+
return round(num / den, 4) if den else 0.0
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def ratchet_trend(log, ref, window_days=30):
|
|
1033
|
+
"""Per-dimension trend over the momentum module's own recency window — reuses
|
|
1034
|
+
load_learnings_log's ledger, no second parser. Returns (dims, composite):
|
|
1035
|
+
dims — [(short, name, last_value|None, slope, 'rising'|'flat'|'slipping'), ...]
|
|
1036
|
+
in DIM_FIELDS order; last_value is None until the first vectored close.
|
|
1037
|
+
composite — (last_composite|None, trailing_median, 'rising'|'flat'|'slipping') —
|
|
1038
|
+
the gate C3 reads: composite-ratchet = composite ≥ max(0.65, median).
|
|
1039
|
+
"""
|
|
1040
|
+
if ref is None:
|
|
1041
|
+
return [], (None, None, "flat")
|
|
1042
|
+
|
|
1043
|
+
window = [(d, rub, parse_vector(ln)) for d, rub, ln in log if (ref - d).days < window_days]
|
|
1044
|
+
window.sort(key=lambda t: t[0]) # oldest → newest, so slope reads left-to-right in time
|
|
1045
|
+
composites = [rub for _d, rub, _v in window if rub is not None]
|
|
1046
|
+
vectored = [(d, rub, v) for d, rub, v in window if v is not None]
|
|
1047
|
+
|
|
1048
|
+
dims = []
|
|
1049
|
+
for short, name in DIM_FIELDS:
|
|
1050
|
+
if not vectored:
|
|
1051
|
+
dims.append((short, name, None, 0.0, "flat"))
|
|
1052
|
+
continue
|
|
1053
|
+
origin = vectored[0][0]
|
|
1054
|
+
pts = [((d - origin).days, v[name]) for d, _rub, v in vectored]
|
|
1055
|
+
slope = _slope(pts)
|
|
1056
|
+
last_val = pts[-1][1]
|
|
1057
|
+
label = "rising" if slope > 0.01 else "slipping" if slope < -0.01 else "flat"
|
|
1058
|
+
dims.append((short, name, last_val, slope, label))
|
|
1059
|
+
|
|
1060
|
+
last_composite = vectored[-1][1] if vectored else (composites[-1] if composites else None)
|
|
1061
|
+
median = trailing_median(composites)
|
|
1062
|
+
if last_composite is None or not composites:
|
|
1063
|
+
direction = "flat"
|
|
1064
|
+
elif last_composite > median:
|
|
1065
|
+
direction = "rising"
|
|
1066
|
+
elif last_composite < median:
|
|
1067
|
+
direction = "slipping"
|
|
1068
|
+
else:
|
|
1069
|
+
direction = "flat"
|
|
1070
|
+
return dims, (last_composite, median, direction)
|
|
1071
|
+
|
|
1072
|
+
|
|
687
1073
|
def selftest():
|
|
688
1074
|
"""Lock the three bugs found building the momentum gate. Pure, no mocks,
|
|
689
1075
|
real learnings-shaped strings. Run: do-rank.py --selftest"""
|
|
@@ -791,15 +1177,65 @@ def selftest():
|
|
|
791
1177
|
check("gated: empty build_gate value ⇒ not gated (needs a reason)",
|
|
792
1178
|
is_gated('---\ntitle: x\nbuild_gate:\n---\nbody') is False)
|
|
793
1179
|
|
|
1180
|
+
# 12. ratchet-trend (C2): per-dimension rising/flat/slipping from vectored closes,
|
|
1181
|
+
# plus composite-vs-trailing-median direction. Pure synthetic vectors, deterministic —
|
|
1182
|
+
# security climbs .70→.80→.90 (rising), speed falls .70→.60→.50 (slipping), simplicity
|
|
1183
|
+
# holds flat at .70 throughout.
|
|
1184
|
+
rlog = [
|
|
1185
|
+
(date(2026, 6, 1), 0.70,
|
|
1186
|
+
"- 2026-06-01 · x · c · a [sec.70 simp.70 struct.70 speed.70 integ.70 reuse.70 test.70 ux.70]"),
|
|
1187
|
+
(date(2026, 6, 10), 0.75,
|
|
1188
|
+
"- 2026-06-10 · x · c · b [sec.80 simp.70 struct.70 speed.60 integ.70 reuse.70 test.70 ux.70]"),
|
|
1189
|
+
(date(2026, 6, 19), 0.85,
|
|
1190
|
+
"- 2026-06-19 · x · c · c [sec.90 simp.70 struct.70 speed.50 integ.70 reuse.70 test.70 ux.70]"),
|
|
1191
|
+
]
|
|
1192
|
+
rdims, (rcomp, rmed, rdir) = ratchet_trend(rlog, date(2026, 6, 19))
|
|
1193
|
+
rd = {name: (label, slope) for _short, name, _val, slope, label in rdims}
|
|
1194
|
+
check("ratchet: security rising", rd["security"][0] == "rising" and rd["security"][1] > 0)
|
|
1195
|
+
check("ratchet: speed slipping", rd["speed"][0] == "slipping" and rd["speed"][1] < 0)
|
|
1196
|
+
check("ratchet: simplicity flat (no change)", rd["simplicity"][0] == "flat")
|
|
1197
|
+
check("ratchet: composite above trailing median ⇒ rising", rcomp == 0.85 and rdir == "rising")
|
|
1198
|
+
check("ratchet: no closes ⇒ empty dims, no crash", ratchet_trend([], None)[0] == [])
|
|
1199
|
+
check("ratchet: parse_vector back-compat (no vector ⇒ None)",
|
|
1200
|
+
parse_vector("- 2026-06-01 · x · c · plain rubric=0.80") is None)
|
|
1201
|
+
|
|
1202
|
+
# 13. outcome-weight (self-improving-outcome): path_momentum() folds prod.strength/
|
|
1203
|
+
# prod.resistance in alongside closed/live — a slug the world MARKED (prod.strength)
|
|
1204
|
+
# ranks strictly above an identical slug the world WARNED (prod.resistance), same
|
|
1205
|
+
# closed/local strength/resistance otherwise. Load-bearing assertion, not a label
|
|
1206
|
+
# grep — this is the exact trap this family already hit once (a green string with no
|
|
1207
|
+
# real check behind it). See text/self-improving-outcome-plan.md § Pre-mortem.
|
|
1208
|
+
marked_paths = {"task:x": {"closed": 1.0, "strength": 0.0, "resistance": 0.0,
|
|
1209
|
+
"prod": {"strength": 2.0, "resistance": 0.0}}}
|
|
1210
|
+
warned_paths = {"task:x": {"closed": 1.0, "strength": 0.0, "resistance": 0.0,
|
|
1211
|
+
"prod": {"strength": 0.0, "resistance": 2.0}}}
|
|
1212
|
+
check("outcome-weight: prod.strength raises momentum above an identical warned slug",
|
|
1213
|
+
path_momentum("x", marked_paths) > path_momentum("x", warned_paths))
|
|
1214
|
+
check("outcome-weight: no prod key ⇒ unchanged behaviour (back-compat)",
|
|
1215
|
+
path_momentum("x", {"task:x": {"closed": 1.0, "strength": 0.0, "resistance": 0.0}}) == 0.5)
|
|
1216
|
+
|
|
1217
|
+
# 14. provenance (self-improving-outcome): --trails' NET calc surfaces prod+/prod-
|
|
1218
|
+
# separately from closed/live/resist, so the operator can see WHY a rank moved (world
|
|
1219
|
+
# vs. local). Exercise the same arithmetic --trails prints, not just path_momentum.
|
|
1220
|
+
prov_entry = {"closed": 0.0, "strength": 0.0, "resistance": 0.0,
|
|
1221
|
+
"prod": {"strength": 1.5, "resistance": 0.4}}
|
|
1222
|
+
prov_prod = prov_entry.get("prod") or {}
|
|
1223
|
+
prov_net = round(float(prov_entry.get("closed", 0)) + float(prov_entry.get("strength", 0))
|
|
1224
|
+
- float(prov_entry.get("resistance", 0))
|
|
1225
|
+
+ float(prov_prod.get("strength", 0)) - float(prov_prod.get("resistance", 0)), 2)
|
|
1226
|
+
check("provenance: --trails NET includes prod+/prod- terms", prov_net == 1.1)
|
|
1227
|
+
check("provenance: prod+/prod- readable independent of closed/live/resist",
|
|
1228
|
+
float(prov_prod.get("strength", 0)) == 1.5 and float(prov_prod.get("resistance", 0)) == 0.4)
|
|
1229
|
+
|
|
794
1230
|
if fails:
|
|
795
1231
|
print("SELFTEST FAIL:")
|
|
796
1232
|
for f in fails:
|
|
797
1233
|
print(" ✗", f)
|
|
798
1234
|
sys.exit(1)
|
|
799
|
-
print("SELFTEST:
|
|
1235
|
+
print("SELFTEST: 45/45 pass (hyphen-boundary · plan-close · momentum subject-zone · "
|
|
800
1236
|
"decay · deliverables-lint · subtree-dirs · transitive-leverage · toxic-classifier · "
|
|
801
1237
|
"deliverables-guard · fade-asymmetry · frontier-setdiff · frontier-closed-filter · "
|
|
802
|
-
"gated-exclusion)")
|
|
1238
|
+
"gated-exclusion · ratchet-trend · outcome-weight · provenance)")
|
|
803
1239
|
|
|
804
1240
|
|
|
805
1241
|
def main():
|
|
@@ -808,6 +1244,7 @@ def main():
|
|
|
808
1244
|
toxic_report = "--toxic" in args
|
|
809
1245
|
tasks_human = "--tasks" in args
|
|
810
1246
|
tasks_seed_out = "--tasks-json" in args
|
|
1247
|
+
task_context_out = "--task-context-json" in args # backfill source: context for ALL plans
|
|
811
1248
|
top_n = None
|
|
812
1249
|
if "--top" in args:
|
|
813
1250
|
i = args.index("--top")
|
|
@@ -815,6 +1252,24 @@ def main():
|
|
|
815
1252
|
if "--selftest" in args:
|
|
816
1253
|
return selftest()
|
|
817
1254
|
|
|
1255
|
+
# `--worktags <slug>` — the plan's task-vocabulary tags (dept + topics), comma-joined,
|
|
1256
|
+
# one line. do-auto.sh passes these to the cycle-close do-event so the reputation harvest
|
|
1257
|
+
# credits the SAME tag-nodes real tasks carry (text/reputation.md § Slice A). Self-contained
|
|
1258
|
+
# (one frontmatter read) so the emit stays cheap.
|
|
1259
|
+
if "--worktags" in args:
|
|
1260
|
+
i = args.index("--worktags")
|
|
1261
|
+
slug = args[i + 1] if i + 1 < len(args) else ""
|
|
1262
|
+
f = TEXT / f"{slug}-todo.md"
|
|
1263
|
+
if not f.exists():
|
|
1264
|
+
f = TEXT / f"{slug}-plan.md"
|
|
1265
|
+
tags = []
|
|
1266
|
+
if f.exists():
|
|
1267
|
+
fm = parse_frontmatter(f.read_text(errors="ignore"))
|
|
1268
|
+
dept = classify_dept(slug, {"title": fm.get("title", ""), "tags": fm.get("tags", [])})
|
|
1269
|
+
tags = _task_tags({"dept": dept, "tags": fm.get("tags", [])})
|
|
1270
|
+
print(",".join(t for t in tags if t))
|
|
1271
|
+
return
|
|
1272
|
+
|
|
818
1273
|
# `--dirs <slug>` — the subtree dirs a plan ships into, one per line. The SINGLE
|
|
819
1274
|
# disjointness extractor: do-fleet.sh calls this instead of re-parsing markdown in
|
|
820
1275
|
# bash, so the displayed waves and the fleet's live conflict check can never drift.
|
|
@@ -827,6 +1282,21 @@ def main():
|
|
|
827
1282
|
print(d)
|
|
828
1283
|
return
|
|
829
1284
|
|
|
1285
|
+
# `--board-check` — the one failure mode source-greps cannot catch. do-tasks-wire-check.sh
|
|
1286
|
+
# asserts the SHAPE of the board read; this actually performs it. A key that doesn't match
|
|
1287
|
+
# the deployed worker's GATEWAY_API_KEY passes every static check and still leaves the
|
|
1288
|
+
# ranker file-only forever — which is exactly how the original break hid for months.
|
|
1289
|
+
# Deliberately NOT part of any promise `proof:` (a contract must not depend on the network);
|
|
1290
|
+
# run it explicitly when the board looks empty.
|
|
1291
|
+
if "--board-check" in args:
|
|
1292
|
+
target = os.environ.get("DO_SIGNAL_URL", "https://one.ie")
|
|
1293
|
+
rows, reason = fetch_tasks_everywhere()
|
|
1294
|
+
if reason:
|
|
1295
|
+
print(f"[board-check] RED — {target}: {reason}", file=sys.stderr)
|
|
1296
|
+
return 1
|
|
1297
|
+
print(f"[board-check] OK — {target}: {len(rows)} open task(s) readable")
|
|
1298
|
+
return 0
|
|
1299
|
+
|
|
830
1300
|
# `--lint` — mechanical deliverables hygiene across all OPEN plans. Flags phantom
|
|
831
1301
|
# paths (under-qualified/wrong-rooted) that make a plan a fleet false-parallel, and
|
|
832
1302
|
# plans with NO parseable deliverables (a fleet hazard). Exit 1 if any plan is dirty.
|
|
@@ -868,8 +1338,11 @@ def main():
|
|
|
868
1338
|
return
|
|
869
1339
|
|
|
870
1340
|
# `--trails` — track the loop: show the trails outcomes have worn into the path
|
|
871
|
-
# store. A trail = accumulated path strength (closes + live marks
|
|
872
|
-
# Strong trails are highways the next pick follows; resistance-
|
|
1341
|
+
# store. A trail = accumulated path strength (closes + live marks + prod outcomes)
|
|
1342
|
+
# minus resistance. Strong trails are highways the next pick follows; resistance-
|
|
1343
|
+
# heavy ones are fading. `prod+`/`prod-` (self-improving-outcome) show provenance —
|
|
1344
|
+
# what production confirmed, separate from local closes/marks — so the operator can
|
|
1345
|
+
# tell WHY a rank moved, not just that it did.
|
|
873
1346
|
if "--trails" in args:
|
|
874
1347
|
paths = load_task_paths()
|
|
875
1348
|
rows = []
|
|
@@ -877,21 +1350,25 @@ def main():
|
|
|
877
1350
|
closed = float(e.get("closed", 0))
|
|
878
1351
|
strength = float(e.get("strength", 0))
|
|
879
1352
|
resistance = float(e.get("resistance", 0))
|
|
880
|
-
|
|
881
|
-
|
|
1353
|
+
prod = e.get("prod") or {}
|
|
1354
|
+
prod_strength = float(prod.get("strength", 0))
|
|
1355
|
+
prod_resistance = float(prod.get("resistance", 0))
|
|
1356
|
+
net = round(closed + strength - resistance + prod_strength - prod_resistance, 2)
|
|
1357
|
+
rows.append((net, key, closed, strength, resistance, int(e.get("traversals", 0)), prod_strength, prod_resistance))
|
|
882
1358
|
rows.sort(reverse=True)
|
|
883
|
-
print("\n TRAILS — paths the loop has worn (weighed by outcome · close + live mark − resistance)\n")
|
|
1359
|
+
print("\n TRAILS — paths the loop has worn (weighed by outcome · close + live mark + prod − resistance)\n")
|
|
884
1360
|
if not rows:
|
|
885
1361
|
print(" (no trails yet — a cycle close or a mark wears the first one)\n")
|
|
886
1362
|
return
|
|
887
|
-
print(f" {'NET':>5} {'closed':>6} {'live':>5} {'resist':>6} {'×':>4} TRAIL")
|
|
888
|
-
print(f" {'-'*5} {'-'*6} {'-'*5} {'-'*6} {'-'*4} {'-'*30}")
|
|
889
|
-
for net, key, c, s, r, tv in rows[:25]:
|
|
1363
|
+
print(f" {'NET':>5} {'closed':>6} {'live':>5} {'resist':>6} {'×':>4} {'prod+':>5} {'prod-':>5} TRAIL")
|
|
1364
|
+
print(f" {'-'*5} {'-'*6} {'-'*5} {'-'*6} {'-'*4} {'-'*5} {'-'*5} {'-'*30}")
|
|
1365
|
+
for net, key, c, s, r, tv, ps, pr in rows[:25]:
|
|
890
1366
|
band = "🛣 highway" if net >= 4 else "· trail" if net >= 2 else " forming"
|
|
891
|
-
if r > c + s:
|
|
1367
|
+
if r + pr > c + s + ps:
|
|
892
1368
|
band = "✗ fading"
|
|
893
|
-
print(f" {net:>5.1f} {c:>6.1f} {s:>5.1f} {r:>6.1f} {tv:>4} {key:<26} {band}")
|
|
1369
|
+
print(f" {net:>5.1f} {c:>6.1f} {s:>5.1f} {r:>6.1f} {tv:>4} {ps:>5.1f} {pr:>5.1f} {key:<26} {band}")
|
|
894
1370
|
print(f"\n {len(rows)} trail(s). A close → --sync-closes wears it deeper; warn/--fail lets it fade.")
|
|
1371
|
+
print(f" prod+/prod- come from outcome-pull.ts (production D1, session-cadence) — provenance, never local.")
|
|
895
1372
|
print(f" These are the task-layer trails (text/task-paths.json); the substrate trail is claw_paths/path.strength.\n")
|
|
896
1373
|
return
|
|
897
1374
|
|
|
@@ -943,6 +1420,80 @@ def main():
|
|
|
943
1420
|
f"the migration target (not wired this cycle).\n")
|
|
944
1421
|
return
|
|
945
1422
|
|
|
1423
|
+
# `--ratchet` (C2, self-improving-ratchet) — read-only projection: each of the eight
|
|
1424
|
+
# rubric dimensions' 30-day trend (rising/flat/slipping), reusing load_learnings_log +
|
|
1425
|
+
# the momentum recency window. Composite vs its trailing median is the number C3's W4
|
|
1426
|
+
# gate reads (composite-ratchet: composite ≥ max(0.65, median)) — this prints it, the
|
|
1427
|
+
# gate (C3) enforces it. Answers "are we getting better?" as a command, not a vibe.
|
|
1428
|
+
if "--ratchet" in args:
|
|
1429
|
+
rlog, rref = load_learnings_log()
|
|
1430
|
+
dims, (last_composite, median, direction) = ratchet_trend(rlog, rref)
|
|
1431
|
+
n_vectored = sum(1 for _d, _r, ln in rlog if parse_vector(ln) is not None)
|
|
1432
|
+
print(f"\n RATCHET — eight dimensions (30-day trend, n={n_vectored} vectored close(s))\n")
|
|
1433
|
+
if n_vectored == 0:
|
|
1434
|
+
print(" no vectored closes yet — the eight-field vector starts accumulating from")
|
|
1435
|
+
print(" the first close-line carrying `[sec.. simp.. …]` (text/rubrics.md § the")
|
|
1436
|
+
print(" close-line vector). Composite-only trend is not shown here — use the")
|
|
1437
|
+
print(" existing momentum/toxic reports for that.\n")
|
|
1438
|
+
return
|
|
1439
|
+
arrow = {"rising": "▲", "flat": "▬", "slipping": "▼"}
|
|
1440
|
+
for short, name, last_val, slope, label in dims:
|
|
1441
|
+
val_s = f"{last_val:.2f}" if last_val is not None else " n/a"
|
|
1442
|
+
warn_s = " ⚠ warn" if label == "slipping" else ""
|
|
1443
|
+
print(f" {name:<13} {arrow[label]} {label:<9} {val_s} ({slope:+.2f}){warn_s}")
|
|
1444
|
+
comp_s = f"{last_composite:.2f}" if last_composite is not None else "n/a"
|
|
1445
|
+
med_s = f"{median:.2f}" if median else "n/a"
|
|
1446
|
+
print(f"\n composite {comp_s} {arrow.get(direction, '▬')} "
|
|
1447
|
+
f"(trailing median {med_s} — gate floor; no regression)\n")
|
|
1448
|
+
return
|
|
1449
|
+
|
|
1450
|
+
# `--ratchet-json` (C3 rescope, self-improving-ratchet) — machine-readable sibling of
|
|
1451
|
+
# `--ratchet`: the one source of truth for the trailing numbers so w4-rubric.ts shells
|
|
1452
|
+
# out instead of re-implementing a vector-regex parser in TypeScript. Read-only, no
|
|
1453
|
+
# exit-code contract — safe under any C3 rescope option.
|
|
1454
|
+
if "--ratchet-json" in args:
|
|
1455
|
+
import json as _json
|
|
1456
|
+
rlog, rref = load_learnings_log()
|
|
1457
|
+
dims, (last_composite, median, direction) = ratchet_trend(rlog, rref)
|
|
1458
|
+
n_vectored = sum(1 for _d, _r, ln in rlog if parse_vector(ln) is not None)
|
|
1459
|
+
print(_json.dumps({
|
|
1460
|
+
"last_composite": last_composite,
|
|
1461
|
+
"trailing_median": median,
|
|
1462
|
+
"direction": direction,
|
|
1463
|
+
"n": len(rlog),
|
|
1464
|
+
"n_vectored": n_vectored,
|
|
1465
|
+
"dims": [
|
|
1466
|
+
{"short": short, "name": name, "last_value": last_val, "slope": slope, "label": label}
|
|
1467
|
+
for short, name, last_val, slope, label in dims
|
|
1468
|
+
],
|
|
1469
|
+
}))
|
|
1470
|
+
return
|
|
1471
|
+
|
|
1472
|
+
# `--ratchet-dry-run` (C3 rescope calibration receipt) — replays every close against
|
|
1473
|
+
# the trailing median of ITS OWN priors (not the current median), and counts how many
|
|
1474
|
+
# would fail a strict `composite ≥ median-of-priors` gate. This is the number the W2
|
|
1475
|
+
# escape check argued from prose; this prints it from the real ledger so the author's
|
|
1476
|
+
# rescope decision (warn vs block vs rewrite-the-escape) is made on a receipt, not an
|
|
1477
|
+
# estimate. Never touches an exit code.
|
|
1478
|
+
if "--ratchet-dry-run" in args:
|
|
1479
|
+
rlog, _rref = load_learnings_log()
|
|
1480
|
+
chron = sorted(((d, rub) for d, rub, _ln in rlog if rub is not None), key=lambda t: t[0])
|
|
1481
|
+
reblocked = []
|
|
1482
|
+
for i, (d, rub) in enumerate(chron):
|
|
1483
|
+
priors = [r for _d, r in chron[:i]]
|
|
1484
|
+
if len(priors) < 2:
|
|
1485
|
+
continue
|
|
1486
|
+
med = trailing_median(priors)
|
|
1487
|
+
if rub < med:
|
|
1488
|
+
reblocked.append((d, rub, med))
|
|
1489
|
+
print(f"\n RATCHET DRY-RUN — {len(chron)} composite-bearing close(s), "
|
|
1490
|
+
f"{len(reblocked)} would fail composite ≥ median-of-priors\n")
|
|
1491
|
+
for d, rub, med in reblocked[-15:]:
|
|
1492
|
+
print(f" {d} composite={rub:.2f} < median-of-priors={med:.2f}")
|
|
1493
|
+
print(f"\n {len(reblocked)}/{len(chron) - 2 if len(chron) > 2 else 0} eligible closes "
|
|
1494
|
+
f"would have been re-blocked by a hard median gate.\n")
|
|
1495
|
+
return
|
|
1496
|
+
|
|
946
1497
|
log, ref = load_learnings_log()
|
|
947
1498
|
task_paths = load_task_paths()
|
|
948
1499
|
plans = {}
|
|
@@ -968,6 +1519,7 @@ def main():
|
|
|
968
1519
|
plans[slug] = {
|
|
969
1520
|
"slug": slug,
|
|
970
1521
|
"title": fm.get("title", ""),
|
|
1522
|
+
"outcome_asserts": fm.get("outcome_asserts", ""),
|
|
971
1523
|
"tier": fm.get("tier", "?"),
|
|
972
1524
|
"tags": fm.get("tags", []),
|
|
973
1525
|
"priority": priority,
|
|
@@ -1053,6 +1605,10 @@ def main():
|
|
|
1053
1605
|
|
|
1054
1606
|
# active = open work (ready + blocked); done AND faded plans are not live tasks.
|
|
1055
1607
|
active = ready + blocked
|
|
1608
|
+
# C2 — rank FROM tasks:everywhere too: board-created tasks (never a todo file) join
|
|
1609
|
+
# the same list; a substrate row whose slug: tag matches a local plan wins (see
|
|
1610
|
+
# merge_board_tasks docstring). No-op on fetch failure — file-only, unchanged.
|
|
1611
|
+
active = merge_board_tasks(active, plans)
|
|
1056
1612
|
|
|
1057
1613
|
if toxic_report:
|
|
1058
1614
|
# Read-only report — the `--toxic` deliverable. Nothing is written or deleted;
|
|
@@ -1071,6 +1627,16 @@ def main():
|
|
|
1071
1627
|
if tasks_seed_out:
|
|
1072
1628
|
print(json.dumps(tasks_seed(active), indent=2))
|
|
1073
1629
|
return
|
|
1630
|
+
if task_context_out:
|
|
1631
|
+
# Context for EVERY plan (done/blocked/ready), keyed by tid — the backfill reads
|
|
1632
|
+
# this to add notes + task-context to tasks already seeded into TypeDB (which the
|
|
1633
|
+
# forward `seed-tasks-typedb.ts` skips because they exist). Idempotent by design.
|
|
1634
|
+
contexts = []
|
|
1635
|
+
for p in sorted(plans.values(), key=lambda x: x["slug"]):
|
|
1636
|
+
notes, docs = task_context_for(p)
|
|
1637
|
+
contexts.append({"tid": f"task:{p['slug']}", "notes": notes, "contextDocs": docs})
|
|
1638
|
+
print(json.dumps({"version": "context-backfill-1", "contexts": contexts}, indent=2))
|
|
1639
|
+
return
|
|
1074
1640
|
if tasks_human:
|
|
1075
1641
|
print("\n" + "\n".join(tasks_view(active)) + "\n")
|
|
1076
1642
|
return
|
|
@@ -1096,6 +1662,24 @@ def main():
|
|
|
1096
1662
|
# Human table
|
|
1097
1663
|
print(f"\n PRIORITY QUEUE — {len(ready)} ready · {len(blocked)} blocked · "
|
|
1098
1664
|
f"{len(faded)} faded · {len(done)} done\n")
|
|
1665
|
+
|
|
1666
|
+
# RATCHET — C4: one compact line surfacing C2's eight-dimension trend so the
|
|
1667
|
+
# operator sees "are we getting better?" at session start without running
|
|
1668
|
+
# --ratchet. Reuses `log`/`ref` (already computed above) + ratchet_trend();
|
|
1669
|
+
# the full 8-row table stays behind --ratchet. Degrades to a "no data" line
|
|
1670
|
+
# (still carrying the word "ratchet") when no vectored closes exist yet.
|
|
1671
|
+
r_dims, (r_comp, r_med, r_dir) = ratchet_trend(log, ref)
|
|
1672
|
+
if r_comp is None:
|
|
1673
|
+
print(" RATCHET no vectored closes yet — trend starts at the first "
|
|
1674
|
+
"[sec.. simp.. …] close\n")
|
|
1675
|
+
else:
|
|
1676
|
+
r_arrow = {"rising": "▲", "flat": "▬", "slipping": "▼"}.get(r_dir, "▬")
|
|
1677
|
+
n_slip = sum(1 for _sh, _nm, _lv, _sl, label in r_dims if label == "slipping")
|
|
1678
|
+
slip_s = f" · {n_slip} dim(s) slipping ⚠" if n_slip else ""
|
|
1679
|
+
r_med_s = f"{r_med:.2f}" if r_med else "n/a"
|
|
1680
|
+
print(f" RATCHET composite {r_comp:.2f} {r_arrow} "
|
|
1681
|
+
f"(median {r_med_s}){slip_s}\n")
|
|
1682
|
+
|
|
1099
1683
|
print(f" {'SCORE':>5} {'S':>2} {'L':>2} {'P':>4} {'Pr':>2} {'M':>4} {'TIER':<8} SLUG")
|
|
1100
1684
|
print(f" {'-'*5} {'-'*2} {'-'*2} {'-'*4} {'-'*2} {'-'*4} {'-'*8} {'-'*30}")
|
|
1101
1685
|
for p in ready[:40]:
|
|
@@ -1157,4 +1741,6 @@ def main():
|
|
|
1157
1741
|
|
|
1158
1742
|
|
|
1159
1743
|
if __name__ == "__main__":
|
|
1160
|
-
|
|
1744
|
+
# `or 0` so every path that returns None (the vast majority) still exits 0 — only a
|
|
1745
|
+
# mode that deliberately returns a non-zero int (e.g. --board-check RED) sets a code.
|
|
1746
|
+
sys.exit(main() or 0)
|