@pilotspace/add 1.3.0 → 1.4.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/CHANGELOG.md +81 -0
- package/GETTING-STARTED.md +4 -0
- package/README.md +16 -3
- package/bin/cli.js +1 -6
- package/docs/09-the-loop.md +3 -1
- package/docs/appendix-c-glossary.md +8 -0
- package/package.json +1 -1
- package/skill/add/SKILL.md +14 -2
- package/skill/add/advisor.md +75 -0
- package/skill/add/compact-foundation.md +53 -0
- package/skill/add/confidence.md +48 -0
- package/skill/add/fold.md +4 -4
- package/skill/add/phases/0-ground.md +2 -0
- package/skill/add/phases/0-setup.md +49 -2
- package/skill/add/phases/1-specify.md +2 -0
- package/skill/add/phases/2-scenarios.md +2 -0
- package/skill/add/phases/3-contract.md +2 -0
- package/skill/add/phases/4-tests.md +2 -0
- package/skill/add/phases/5-build.md +2 -0
- package/skill/add/phases/6-verify.md +2 -0
- package/skill/add/phases/7-observe.md +7 -0
- package/skill/add/soul.md +77 -0
- package/skill/add/streams.md +44 -3
- package/tooling/add.py +372 -6
- package/tooling/templates/GLOSSARY.md.tmpl +4 -0
- package/tooling/templates/SOUL.md.tmpl +40 -0
- package/tooling/templates/TASK.md.tmpl +1 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Self-improving the voice — how SOUL.md converges to the human
|
|
2
|
+
|
|
3
|
+
`SOUL.md` is the AI's voice (tone · communication style · trust). It ships as a **proposed starter**
|
|
4
|
+
and is **human-owned**; this doc is how it stops being write-once and starts converging to *this*
|
|
5
|
+
human. It mirrors `deltas.md` (emit) + `fold.md` (confirm → rewrite), but a confirmed voice delta is
|
|
6
|
+
consolidated into **SOUL.md**, not the foundation — because voice is not one of the five competencies.
|
|
7
|
+
|
|
8
|
+
You (the AI) **emit** voice deltas as `open`. Only the **human** confirms one, and only a confirmed
|
|
9
|
+
delta rewrites SOUL.md. **The human's confirm is the only writer** — you never self-approve a voice
|
|
10
|
+
rewrite (rewriting someone's voice is theirs to approve, like a foundation consolidation).
|
|
11
|
+
|
|
12
|
+
## What a voice delta is drawn from
|
|
13
|
+
|
|
14
|
+
A voice delta is grounded in how the human actually shows up **in session**:
|
|
15
|
+
- their **wordings** — the words they reach for, and the words they correct you on (they cross out a
|
|
16
|
+
bit of jargon and write the plain word over it);
|
|
17
|
+
- their **flow** — what they skip, what they double-check, where they want the summary before the detail.
|
|
18
|
+
|
|
19
|
+
NOT from their private **memory** files or anything outside the working session — SOUL learns from what
|
|
20
|
+
you observed together, never from reading a personal store.
|
|
21
|
+
|
|
22
|
+
## The grammar (mirrors deltas.md)
|
|
23
|
+
|
|
24
|
+
Each voice delta begins on its own tag line; the observation may wrap, and a required `(evidence: …)`
|
|
25
|
+
clause closes it:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
- [VOICE · <status>] <observation about the voice> (evidence: <in-session pointer>)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- `<status>` — `open` | `confirmed` | `declined`. A newly emitted delta is **`open`**.
|
|
32
|
+
- `<observation>` — what the voice should become ("lead with the decision, not the preamble").
|
|
33
|
+
- `(evidence: …)` — **required**, non-empty: a moment in the session (a correction, a re-ask, a
|
|
34
|
+
visible preference). No evidence → it is a guess about the human, not a delta. Drop it.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
- [VOICE · open] the human strips hedging from my drafts — cut "I think / it seems" and state it plainly
|
|
38
|
+
(evidence: they rewrote two replies this session to remove the qualifier)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## The loop — observe → confirm → rewrite
|
|
42
|
+
|
|
43
|
+
1. **Emit** (OBSERVE) — at a task's observe phase (or on demand), propose 0–N voice deltas as `open`
|
|
44
|
+
from the session's wordings + flow. Surface them in the report; show-before-ask.
|
|
45
|
+
2. **Confirm** — the human accepts or declines each. **No SOUL.md write happens without this.**
|
|
46
|
+
3. **Rewrite** — on a confirmed delta, edit the routed SOUL.md section, then record the delta line
|
|
47
|
+
(status `confirmed`) at the **top** of the "Voice deltas" ledger (**newest-first**, append-only — a
|
|
48
|
+
declined delta flips to `declined` and stays in place, so "considered, chose not to" is auditable).
|
|
49
|
+
|
|
50
|
+
## Routing — every voice delta has a SOUL.md home
|
|
51
|
+
|
|
52
|
+
| the delta is about… | rewrite this SOUL.md section |
|
|
53
|
+
|---------------------|------------------------------|
|
|
54
|
+
| how I *sound* (warmth, directness, hedging) | **## Tone** |
|
|
55
|
+
| how I *structure* what I say (summary-first, show-before-ask, length) | **## Communication style** |
|
|
56
|
+
| what keeps the human's *trust* (gates, honesty, what I never do) | **## Trust** |
|
|
57
|
+
|
|
58
|
+
The rewrite is surgical: refine or append the bullet the delta names; never silently rewrite the rest
|
|
59
|
+
of the voice. Every confirmed delta also gets its line in **## Voice deltas** (newest-first).
|
|
60
|
+
|
|
61
|
+
## Reject codes (the AI is first check, the human the backstop)
|
|
62
|
+
|
|
63
|
+
<reject_codes>
|
|
64
|
+
- `unconfirmed_voice_rewrite` — a SOUL.md write was attempted without a recorded human confirm. The AI
|
|
65
|
+
proposes; it never self-approves. Stop and get the confirm. (The identity-owned floor.)
|
|
66
|
+
- `no_open_voice_deltas` — nothing is `open`. The loop is a no-op; do not touch SOUL.md.
|
|
67
|
+
- `unroutable_voice_delta` — the observation maps to no SOUL.md section (not tone/style/trust). Fix the
|
|
68
|
+
delta or widen the routing before writing.
|
|
69
|
+
</reject_codes>
|
|
70
|
+
|
|
71
|
+
## Where it plugs in
|
|
72
|
+
|
|
73
|
+
- **Emit**: `phases/7-observe.md` proposes voice deltas beside its competency/spec deltas.
|
|
74
|
+
- **Target**: `SOUL.md` (scaffolded by setup) — its "## Voice deltas" ledger holds the confirmed history.
|
|
75
|
+
- **Kin**: `deltas.md` (competency learnings → foundation) and `fold.md` (the same propose→confirm→write
|
|
76
|
+
discipline). This is the voice's version of that discipline; the engine stays judgment-free (no
|
|
77
|
+
`add.py` command writes the voice).
|
package/skill/add/streams.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# Parallel streams — pipelining independent tasks
|
|
2
2
|
|
|
3
|
-
Load this
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Load this when a milestone has more than one task and you want to run them concurrently.
|
|
4
|
+
**Default:** when a project confirms `parallel + auto` as its run mode at setup
|
|
5
|
+
(`phases/0-setup.md` "Run mode"), parallel streaming is the project default — an **opt-out**, not
|
|
6
|
+
the opt-in it once was; downgrade in one step (`add.py autonomy set conservative --project`, or just
|
|
7
|
+
run tasks one at a time). A project that kept the conservative run mode still treats this rubric as
|
|
8
|
+
the opt-in escape hatch. Either way it changes nothing below.
|
|
6
9
|
|
|
7
10
|
It changes **no `add.py` code and no phase semantics**. It is a way *you, the
|
|
8
11
|
orchestrator*, drive several tasks at once by reading the dependency DAG that
|
|
@@ -35,6 +38,44 @@ Compute both from one `python3 .add/tooling/add.py status` — no new state:
|
|
|
35
38
|
└──────────────── a task gating PASS unblocks its dependents ──────────────┘
|
|
36
39
|
```
|
|
37
40
|
|
|
41
|
+
## The DAG strategy — let the engine schedule the waves (`add.py waves`)
|
|
42
|
+
|
|
43
|
+
Do **not** eyeball the READY-QUEUE by hand once a milestone has more than a couple of
|
|
44
|
+
tasks — the engine computes the whole schedule from the dependency DAG `status` already
|
|
45
|
+
holds. `add.py waves` (read-only, writes nothing) groups the active milestone's not-done
|
|
46
|
+
tasks into **topological waves**, names the **critical path**, and emits an advisory
|
|
47
|
+
**tier hint** — exactly the inputs you need to fan out effectively:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
$ add.py waves
|
|
51
|
+
milestone: v13-onboarding-polish
|
|
52
|
+
wave 1: dag-scheduler, setup-suggest-milestone, setup-domain-deepdive, soul-artifact
|
|
53
|
+
wave 2: setup-run-mode (deps: dag-scheduler), soul-self-improve (deps: soul-artifact)
|
|
54
|
+
critical path: dag-scheduler → setup-run-mode (2 tasks)
|
|
55
|
+
tier hint: top → dag-scheduler, setup-run-mode; mid → the rest
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Read the schedule as a strategy, not a command:
|
|
59
|
+
|
|
60
|
+
- **Wave = a fan-out batch.** Every task in a wave has all its in-milestone deps already
|
|
61
|
+
PASS, so the whole wave is spawnable at once (one worker per task, `isolation="worktree"`).
|
|
62
|
+
Finish a wave, gate its tasks PASS, then `add.py waves` again — the next wave is unblocked.
|
|
63
|
+
- **Run the widest wave first.** It hides the most build latency under the human's review
|
|
64
|
+
latency (the honest frame above): more concurrent builds while the reviewer reads one bundle.
|
|
65
|
+
- **Spend your strongest model on the critical path.** The critical-path tasks gate the most
|
|
66
|
+
downstream work, so a wrong-but-plausible result there is the costliest — give them the
|
|
67
|
+
**top** tier (`run.md` tiers); off-path tasks take **mid**. The tier hint is exactly this rule,
|
|
68
|
+
applied to the graph. It is **advisory** — graph position is a proxy for scope difficulty, not a
|
|
69
|
+
gate; override it when you know a task is harder than its position suggests.
|
|
70
|
+
- **`--json`** (`{ milestone, waves, critical_path, critical_path_len, tiers, blocked }`) feeds a
|
|
71
|
+
runner that spawns the wave programmatically. `blocked` lists any task whose dep can never be
|
|
72
|
+
satisfied within this milestone (a cross-milestone dep) — surfaced, never silently dropped; a
|
|
73
|
+
`dependency_cycle` is refused with the offending members named (no schedule exists).
|
|
74
|
+
|
|
75
|
+
What `waves` does **not** change: the irreducible floor below still holds — one human approval
|
|
76
|
+
per contract, builds overlap but the review queue stays serial. `waves` decides *order and model*,
|
|
77
|
+
never *whether the human gate fires*.
|
|
78
|
+
|
|
38
79
|
## The autonomy level is the throttle (not a new flag)
|
|
39
80
|
|
|
40
81
|
How much concurrency you actually get is set by each task's `autonomy:` header
|
package/tooling/add.py
CHANGED
|
@@ -19,7 +19,8 @@ import os
|
|
|
19
19
|
import re
|
|
20
20
|
import sys
|
|
21
21
|
import tempfile
|
|
22
|
-
|
|
22
|
+
import urllib.request
|
|
23
|
+
from datetime import date, datetime, timedelta, timezone
|
|
23
24
|
from pathlib import Path
|
|
24
25
|
|
|
25
26
|
# --- constants ---------------------------------------------------------------
|
|
@@ -80,7 +81,23 @@ PHASE_OWNER = {
|
|
|
80
81
|
"specify": "human", "scenarios": "human", "contract": "seam",
|
|
81
82
|
"tests": "ai", "build": "ai", "verify": "human", "observe": "ai", "done": "human",
|
|
82
83
|
}
|
|
83
|
-
SETUP_FILES = ("PROJECT.md", "CONVENTIONS.md", "GLOSSARY.md", "MODEL_REGISTRY.md", "dependencies.allowlist", "DESIGN.md")
|
|
84
|
+
SETUP_FILES = ("PROJECT.md", "CONVENTIONS.md", "GLOSSARY.md", "MODEL_REGISTRY.md", "dependencies.allowlist", "DESIGN.md", "SOUL.md")
|
|
85
|
+
|
|
86
|
+
# Scaffolded into .add/.gitignore at init so the engine's transient LOCAL artifacts
|
|
87
|
+
# never reach git. Bare-filename patterns match at any depth under .add/ (tasks/,
|
|
88
|
+
# milestones/, archive/). These are working state, not records: scope-snapshot.json
|
|
89
|
+
# is the tests->build touch baseline the verify scope-gate reads from disk (the
|
|
90
|
+
# durable scope declaration is the state.json anchor); pre-archive-state.bak.json is
|
|
91
|
+
# archive-milestone's pre-delete recovery net — needed on disk, never in history;
|
|
92
|
+
# .update-cache.json is the update-nudge's once-a-day registry throttle. All stay on
|
|
93
|
+
# disk; git-ignoring them is hygiene, never deletion.
|
|
94
|
+
_GITIGNORE_BODY = """\
|
|
95
|
+
# ADD engine transient artifacts — local working state, never committed.
|
|
96
|
+
# (Scaffolded by `add.py init`; edit freely — init never clobbers an existing copy.)
|
|
97
|
+
scope-snapshot.json
|
|
98
|
+
pre-archive-state.bak.json
|
|
99
|
+
.update-cache.json
|
|
100
|
+
"""
|
|
84
101
|
|
|
85
102
|
# Guideline-injection targets + version-stable markers. NEVER change these marker
|
|
86
103
|
# strings: a re-run finds the old block by exact match, so changing them would
|
|
@@ -370,6 +387,13 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|
|
370
387
|
_die(f"already initialised at {root} (use --force to reset state)")
|
|
371
388
|
|
|
372
389
|
(root / "tasks").mkdir(parents=True, exist_ok=True)
|
|
390
|
+
# Keep the engine's transient local artifacts out of git. Never-clobber: a
|
|
391
|
+
# human may have customised .add/.gitignore, so an existing one is left as-is
|
|
392
|
+
# (mirrors the SETUP_FILES skip-not-clobber idiom). Writes ONLY this file — no
|
|
393
|
+
# scope-snapshot.json or .bak is created, deleted, or modified.
|
|
394
|
+
gitignore = root / ".gitignore"
|
|
395
|
+
if not gitignore.exists():
|
|
396
|
+
_atomic_write(gitignore, _GITIGNORE_BODY)
|
|
373
397
|
today = date.today().isoformat()
|
|
374
398
|
proj_name = args.name or base.name
|
|
375
399
|
|
|
@@ -456,7 +480,7 @@ def cmd_new_task(args: argparse.Namespace) -> None:
|
|
|
456
480
|
if _project_autonomy_token(root) == "?":
|
|
457
481
|
print("warning: garbled_project_autonomy — PROJECT.md declares an unrecognized "
|
|
458
482
|
f"autonomy token; new task seeded fail-safe '{autonomy}' "
|
|
459
|
-
"(
|
|
483
|
+
"(fix it with `add.py autonomy set <level> --project`)", file=sys.stderr)
|
|
460
484
|
|
|
461
485
|
state["tasks"][slug] = {
|
|
462
486
|
"title": title,
|
|
@@ -706,8 +730,8 @@ def cmd_gate(args: argparse.Namespace) -> None:
|
|
|
706
730
|
hdr = _task_header(root, slug)
|
|
707
731
|
if _RISK_HIGH_RE.search(hdr) and not _autonomy_lowered(hdr):
|
|
708
732
|
_die(f"unguarded_high_risk_auto: task '{slug}' declares risk: high "
|
|
709
|
-
"without a lowered autonomy level —
|
|
710
|
-
"
|
|
733
|
+
"without a lowered autonomy level — run `add.py autonomy set conservative` "
|
|
734
|
+
"(or manual); a human must own a high-risk gate (run.md guard)")
|
|
711
735
|
# tamper tripwire (verify-integrity): the method's first mechanical cheat
|
|
712
736
|
# block. A completing outcome is refused if the red suite or the frozen §3
|
|
713
737
|
# changed since the tests->build snapshot. Placed BEFORE the waiver write so
|
|
@@ -738,6 +762,80 @@ def cmd_gate(args: argparse.Namespace) -> None:
|
|
|
738
762
|
print(_next_footer(root, state))
|
|
739
763
|
|
|
740
764
|
|
|
765
|
+
# the autonomy level as a first-class verb (task autonomy-command): autonomy was the ONLY mutable,
|
|
766
|
+
# security-relevant task/project token WITHOUT a CLI verb — so an agent under `auto`, applying the
|
|
767
|
+
# correct "first-class state has a command" model, hallucinated `add.py autonomy` and derailed.
|
|
768
|
+
# `show` reads the resolved level; `set` is the FIRST writer of the header token — idempotent (one
|
|
769
|
+
# declaration line, trailing comment preserved, NEVER appended), with the raise + risk:high guards
|
|
770
|
+
# enforced BEFORE the write. state.json is untouched — autonomy stays a header token.
|
|
771
|
+
_AUTONOMY_ORDER = {lvl: i for i, lvl in enumerate(_AUTONOMY_LEVELS)} # manual(0) < conservative(1) < auto(2)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _autonomy_decl_line(text: str, level: str) -> str:
|
|
775
|
+
"""Rewrite the SINGLE `autonomy:` declaration line to `level`, PRESERVING its trailing comment,
|
|
776
|
+
idempotently (replace in place, count=1 — never a second line). If absent, insert it: after the
|
|
777
|
+
`slug:` line for a task header, else after a leading `#` heading (PROJECT.md), else prepend. PURE
|
|
778
|
+
on the text; the caller does the atomic write."""
|
|
779
|
+
pat = re.compile(r"(?m)^(autonomy:[ \t]*)[^\s<#|]+(.*)$")
|
|
780
|
+
if pat.search(text):
|
|
781
|
+
return pat.sub(lambda m: f"{m.group(1)}{level}{m.group(2)}", text, count=1)
|
|
782
|
+
if re.search(r"(?m)^slug:", text):
|
|
783
|
+
return re.sub(r"(?m)^(slug:.*)$", r"\1\nautonomy: " + level, text, count=1)
|
|
784
|
+
lines = text.splitlines(keepends=True)
|
|
785
|
+
if lines and lines[0].lstrip().startswith("#"):
|
|
786
|
+
return lines[0] + f"autonomy: {level}\n" + "".join(lines[1:])
|
|
787
|
+
return f"autonomy: {level}\n" + text
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _guard_autonomy_raise(current: str, target: str, yes: bool) -> None:
|
|
791
|
+
"""RAISING the level toward `auto` is a human-owned trust escalation (run.md: the AI may LOWER
|
|
792
|
+
freely — RECOMMEND-only — but RAISING needs a human). Refuse a raise unless --yes confirms it."""
|
|
793
|
+
if _AUTONOMY_ORDER.get(target, -1) > _AUTONOMY_ORDER.get(current, -1) and not yes:
|
|
794
|
+
_die(f"autonomy_raise_unconfirmed: raising autonomy {current} -> {target} is a human-owned "
|
|
795
|
+
"trust escalation (the AI may LOWER freely; RAISING needs a human) — pass --yes to confirm")
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _print_autonomy(root: Path, state: dict, slug: str) -> None:
|
|
799
|
+
"""The read-only level view: declared · effective (fallback-resolved) · project default · the
|
|
800
|
+
verify-gate owner under it (the SAME _driver_stop the footer/guide render). Writes nothing."""
|
|
801
|
+
declared = _autonomy_level(_task_header(root, slug))
|
|
802
|
+
stop = _driver_stop(root, state, slug, "verify")
|
|
803
|
+
print(f"task : {slug}")
|
|
804
|
+
print(f"declared : {declared if declared in _AUTONOMY_LEVELS else 'unset'}")
|
|
805
|
+
print(f"effective : {_effective_autonomy(root, state, slug)}")
|
|
806
|
+
print(f"project : {_project_autonomy(root)}")
|
|
807
|
+
print(f"verify gate : {'human gate' if stop else 'you drive'}")
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def cmd_autonomy(args: argparse.Namespace) -> None:
|
|
811
|
+
"""show / set the autonomy level — the verify-gate owner (task autonomy-command)."""
|
|
812
|
+
root = _require_root() # reused -> "no .add/ project found …"
|
|
813
|
+
state = load_state(root)
|
|
814
|
+
if (getattr(args, "action", None) or "show") == "show":
|
|
815
|
+
_print_autonomy(root, state, _resolve_task(state, args.a1)) # reused -> "unknown task '<slug>'"
|
|
816
|
+
return
|
|
817
|
+
# action == "set"
|
|
818
|
+
level = args.a1
|
|
819
|
+
if level not in _AUTONOMY_LEVELS:
|
|
820
|
+
_die("autonomy_level_invalid: level must be one of "
|
|
821
|
+
f"{', '.join(_AUTONOMY_LEVELS)} (got {level!r})")
|
|
822
|
+
if getattr(args, "project", False):
|
|
823
|
+
target = root / "PROJECT.md"
|
|
824
|
+
_guard_autonomy_raise(_project_autonomy(root), level, getattr(args, "yes", False))
|
|
825
|
+
_atomic_write(target, _autonomy_decl_line(target.read_text(encoding="utf-8"), level))
|
|
826
|
+
print(f"project autonomy -> {level}")
|
|
827
|
+
return
|
|
828
|
+
slug = _resolve_task(state, args.a2) # reused -> "unknown task '<slug>'"
|
|
829
|
+
task_md = root / "tasks" / slug / "TASK.md"
|
|
830
|
+
if _RISK_HIGH_RE.search(_task_header(root, slug)) and level not in ("manual", "conservative"):
|
|
831
|
+
_die(f"unguarded_high_risk_auto: task '{slug}' declares risk: high — autonomy must stay "
|
|
832
|
+
f"lowered (manual|conservative); refusing '{level}' (a human must own a high-risk gate)")
|
|
833
|
+
_guard_autonomy_raise(_effective_autonomy(root, state, slug), level, getattr(args, "yes", False))
|
|
834
|
+
_atomic_write(task_md, _autonomy_decl_line(task_md.read_text(encoding="utf-8"), level))
|
|
835
|
+
print(f"task '{slug}' autonomy -> {level}")
|
|
836
|
+
_print_autonomy(root, state, slug)
|
|
837
|
+
|
|
838
|
+
|
|
741
839
|
def cmd_reopen(args: argparse.Namespace) -> None:
|
|
742
840
|
"""Return an already-`done` task to an earlier phase with a never-silent record.
|
|
743
841
|
|
|
@@ -918,6 +1016,10 @@ def cmd_status(args: argparse.Namespace) -> None:
|
|
|
918
1016
|
# foundation pointer — read the cross-milestone context first (anti-rot)
|
|
919
1017
|
if (root / "PROJECT.md").exists():
|
|
920
1018
|
print("context : .add/PROJECT.md (foundation: domain · spec · UI/UX — read first)")
|
|
1019
|
+
# voice pointer — the AI's SOUL (tone · style · trust); read each session, edit freely.
|
|
1020
|
+
# Existence-only: no open/parse, so the pointer adds no IO failure path (a non-file is no voice).
|
|
1021
|
+
if (root / "SOUL.md").exists():
|
|
1022
|
+
print("voice : .add/SOUL.md (how I sound & what keeps your trust — read each session)")
|
|
921
1023
|
# wave resume hint — a live ledger outranks memory (streams.md "Wave ledger").
|
|
922
1024
|
# Existence-only: no open/read/parse, so the hint adds no IO failure path; a
|
|
923
1025
|
# non-file at the path is not a ledger. One line PER live ledger — more than
|
|
@@ -1551,7 +1653,7 @@ def cmd_check(args: argparse.Namespace) -> None:
|
|
|
1551
1653
|
"unknown_autonomy_level (token outside manual|conservative|auto)"))
|
|
1552
1654
|
if _alvl is None and t.get("phase") not in ("done", "observe"):
|
|
1553
1655
|
warnings.append((f"task '{slug}'", "has no explicit autonomy level (implicit_autonomy) "
|
|
1554
|
-
"—
|
|
1656
|
+
"— run `add.py autonomy set <level>` to set it"))
|
|
1555
1657
|
for dep in t.get("depends_on") or []:
|
|
1556
1658
|
checks.append((dep in tasks or dep in archived_slugs,
|
|
1557
1659
|
f"task '{slug}' dep '{dep}' resolves", "unknown task"))
|
|
@@ -1903,6 +2005,144 @@ def cmd_ready(args: argparse.Namespace) -> None:
|
|
|
1903
2005
|
print(f" {slug}{suffix}")
|
|
1904
2006
|
|
|
1905
2007
|
|
|
2008
|
+
def _wave_schedule(state: dict, mslug: str) -> dict:
|
|
2009
|
+
"""Pure, total: derive the DAG schedule for milestone `mslug` from state — never
|
|
2010
|
+
mutates, never raises on dict input. Returns one of:
|
|
2011
|
+
{"cycle": [slug, ...]} — unschedulable cycle
|
|
2012
|
+
{"waves", "critical_path", "critical_path_len", "tiers", "blocked"} — a schedule
|
|
2013
|
+
|
|
2014
|
+
A dep is SATISFIED (does not block) if it is archived or `_task_done` — the SAME
|
|
2015
|
+
predicate cmd_ready uses. A not-done dep that is an OPEN MEMBER of this milestone
|
|
2016
|
+
forces a later wave. A not-done dep that is NOT an open member (external/unknown)
|
|
2017
|
+
is UNSATISFIABLE here -> the task is `blocked`, never scheduled. Critical path is the
|
|
2018
|
+
longest chain (most tasks) through the scheduled sub-DAG; ties break by sorted slug.
|
|
2019
|
+
Tier is advisory: `top` on the critical path, `mid` elsewhere (scheduled tasks only)."""
|
|
2020
|
+
tasks = state.get("tasks") or {}
|
|
2021
|
+
archived = _archived_task_slugs(state)
|
|
2022
|
+
|
|
2023
|
+
def _ok(d: str) -> bool: # satisfied externally / already done
|
|
2024
|
+
return d in archived or (d in tasks and _task_done(tasks[d]))
|
|
2025
|
+
|
|
2026
|
+
open_members = {s: t for s, t in tasks.items()
|
|
2027
|
+
if t.get("milestone") == mslug and not _task_done(t)}
|
|
2028
|
+
|
|
2029
|
+
# partition open members into blocked vs schedulable — to a FIXED POINT, so blocking
|
|
2030
|
+
# propagates transitively: a task is blocked if any dep is unsatisfiable here, where
|
|
2031
|
+
# unsatisfiable = not _ok AND not a STILL-schedulable member. A dep on an already-blocked
|
|
2032
|
+
# member is itself unsatisfiable, so the dependent blocks too (it would otherwise be
|
|
2033
|
+
# mis-reported as wave-1-ready while its only dep can never complete).
|
|
2034
|
+
blocked: dict[str, list[str]] = {}
|
|
2035
|
+
changed = True
|
|
2036
|
+
while changed:
|
|
2037
|
+
changed = False
|
|
2038
|
+
for s, t in open_members.items():
|
|
2039
|
+
if s in blocked:
|
|
2040
|
+
continue
|
|
2041
|
+
bad = [d for d in (t.get("depends_on") or [])
|
|
2042
|
+
if not _ok(d) and not (d in open_members and d not in blocked)]
|
|
2043
|
+
if bad:
|
|
2044
|
+
blocked[s] = sorted(set(bad))
|
|
2045
|
+
changed = True
|
|
2046
|
+
schedulable = {s for s in open_members if s not in blocked}
|
|
2047
|
+
blocked_sorted = {k: blocked[k] for k in sorted(blocked)}
|
|
2048
|
+
if not schedulable:
|
|
2049
|
+
# nothing to schedule (all-done, empty, or every open task externally blocked)
|
|
2050
|
+
return {"waves": [], "critical_path": [], "critical_path_len": 0,
|
|
2051
|
+
"tiers": {}, "blocked": blocked_sorted}
|
|
2052
|
+
|
|
2053
|
+
def _member_deps(s: str) -> set[str]: # deps that are open members forcing order
|
|
2054
|
+
return {d for d in (open_members[s].get("depends_on") or []) if d in schedulable}
|
|
2055
|
+
|
|
2056
|
+
# Kahn waves over the schedulable sub-DAG
|
|
2057
|
+
waves: list[list[str]] = []
|
|
2058
|
+
placed: set[str] = set()
|
|
2059
|
+
remaining = set(schedulable)
|
|
2060
|
+
while remaining:
|
|
2061
|
+
wave = sorted(s for s in remaining if _member_deps(s) <= placed)
|
|
2062
|
+
if not wave: # no progress => a cycle among the remaining
|
|
2063
|
+
sub = {s: tasks[s] for s in remaining}
|
|
2064
|
+
cyc = _find_cycle(sub) or sorted(remaining)
|
|
2065
|
+
return {"cycle": cyc}
|
|
2066
|
+
waves.append(wave)
|
|
2067
|
+
placed.update(wave)
|
|
2068
|
+
remaining -= set(wave)
|
|
2069
|
+
|
|
2070
|
+
# critical path = longest chain by memoized depth over member-deps
|
|
2071
|
+
depth: dict[str, int] = {}
|
|
2072
|
+
pick: dict[str, str | None] = {}
|
|
2073
|
+
|
|
2074
|
+
def _depth(s: str) -> int:
|
|
2075
|
+
if s in depth:
|
|
2076
|
+
return depth[s]
|
|
2077
|
+
best_d, best_dep = 0, None
|
|
2078
|
+
for d in sorted(_member_deps(s)):
|
|
2079
|
+
dd = _depth(d)
|
|
2080
|
+
if dd > best_d or (dd == best_d and (best_dep is None or d < best_dep)):
|
|
2081
|
+
best_d, best_dep = dd, d
|
|
2082
|
+
depth[s] = 1 + best_d
|
|
2083
|
+
pick[s] = best_dep
|
|
2084
|
+
return depth[s]
|
|
2085
|
+
|
|
2086
|
+
leaf = min(schedulable, key=lambda s: (-_depth(s), s)) # deepest, tie -> smallest slug
|
|
2087
|
+
chain: list[str] = []
|
|
2088
|
+
cur: str | None = leaf
|
|
2089
|
+
while cur is not None:
|
|
2090
|
+
chain.append(cur)
|
|
2091
|
+
cur = pick.get(cur)
|
|
2092
|
+
critical = list(reversed(chain)) # root -> leaf order
|
|
2093
|
+
crit_set = set(critical)
|
|
2094
|
+
tiers = {s: ("top" if s in crit_set else "mid") for s in sorted(schedulable)}
|
|
2095
|
+
return {"waves": waves, "critical_path": critical, "critical_path_len": len(critical),
|
|
2096
|
+
"tiers": tiers, "blocked": blocked_sorted}
|
|
2097
|
+
|
|
2098
|
+
|
|
2099
|
+
def cmd_waves(args: argparse.Namespace) -> None:
|
|
2100
|
+
"""READ-ONLY DAG scheduler: print the active milestone's topological waves, critical
|
|
2101
|
+
path, advisory tier hint, and blocked set. Writes nothing; emits no `next:` footer."""
|
|
2102
|
+
is_json = getattr(args, "json", False)
|
|
2103
|
+
if is_json:
|
|
2104
|
+
_, state = _load_state_for_json()
|
|
2105
|
+
else:
|
|
2106
|
+
state = load_state(_require_root())
|
|
2107
|
+
mslug = getattr(args, "milestone", None) or state.get("active_milestone")
|
|
2108
|
+
if not mslug:
|
|
2109
|
+
_die("no_active_milestone: no active milestone and no --milestone given")
|
|
2110
|
+
if mslug not in (state.get("milestones") or {}):
|
|
2111
|
+
_die(f"unknown_milestone: '{mslug}' is not a milestone in this project")
|
|
2112
|
+
sched = _wave_schedule(state, mslug)
|
|
2113
|
+
if "cycle" in sched:
|
|
2114
|
+
_die(f"dependency_cycle: not-done deps form a cycle "
|
|
2115
|
+
f"({' -> '.join(sched['cycle'])}) — no valid schedule")
|
|
2116
|
+
|
|
2117
|
+
if is_json:
|
|
2118
|
+
print(json.dumps({"milestone": mslug, **sched}))
|
|
2119
|
+
return
|
|
2120
|
+
|
|
2121
|
+
print(f"milestone: {mslug}")
|
|
2122
|
+
if not sched["waves"]:
|
|
2123
|
+
if sched["blocked"]:
|
|
2124
|
+
for s in sched["blocked"]:
|
|
2125
|
+
print(f"blocked: {s} (waiting on {', '.join(sched['blocked'][s])})")
|
|
2126
|
+
else:
|
|
2127
|
+
print("all tasks done — nothing to schedule")
|
|
2128
|
+
return
|
|
2129
|
+
scheduled_set = {x for w in sched["waves"] for x in w}
|
|
2130
|
+
for i, wave in enumerate(sched["waves"], start=1):
|
|
2131
|
+
parts = []
|
|
2132
|
+
for s in wave:
|
|
2133
|
+
md = sorted(d for d in (state["tasks"][s].get("depends_on") or [])
|
|
2134
|
+
if d in scheduled_set)
|
|
2135
|
+
parts.append(f"{s} (deps: {', '.join(md)})" if md else s)
|
|
2136
|
+
print(f"wave {i}: {', '.join(parts)}")
|
|
2137
|
+
crit = sched["critical_path"]
|
|
2138
|
+
print(f"critical path: {' → '.join(crit)} ({sched['critical_path_len']} tasks)")
|
|
2139
|
+
tops = [s for s, tier in sched["tiers"].items() if tier == "top"]
|
|
2140
|
+
mids = [s for s, tier in sched["tiers"].items() if tier == "mid"]
|
|
2141
|
+
print(f"tier hint: top → {', '.join(tops)}; mid → {', '.join(mids) or '(none)'}")
|
|
2142
|
+
for s in sched["blocked"]:
|
|
2143
|
+
print(f"blocked: {s} (waiting on {', '.join(sched['blocked'][s])})")
|
|
2144
|
+
|
|
2145
|
+
|
|
1906
2146
|
def cmd_milestone_done(args: argparse.Namespace) -> None:
|
|
1907
2147
|
root = _require_root()
|
|
1908
2148
|
state = load_state(root)
|
|
@@ -4019,6 +4259,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
4019
4259
|
pr.add_argument("--json", action="store_true", help="machine-readable JSON output")
|
|
4020
4260
|
pr.set_defaults(func=cmd_ready)
|
|
4021
4261
|
|
|
4262
|
+
pwa = sub.add_parser("waves", help="read-only DAG schedule of a milestone: topological "
|
|
4263
|
+
"waves + critical path + advisory tier hint")
|
|
4264
|
+
pwa.add_argument("--milestone", default=None,
|
|
4265
|
+
help="milestone slug to schedule (default: the active milestone)")
|
|
4266
|
+
pwa.add_argument("--json", action="store_true", help="machine-readable JSON output")
|
|
4267
|
+
pwa.set_defaults(func=cmd_waves)
|
|
4268
|
+
|
|
4022
4269
|
pmd = sub.add_parser("milestone-done", help="exit-gate a milestone (all tasks must PASS)")
|
|
4023
4270
|
pmd.add_argument("slug")
|
|
4024
4271
|
pmd.set_defaults(func=cmd_milestone_done)
|
|
@@ -4060,6 +4307,16 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
4060
4307
|
pg.add_argument("--expires", help="RISK-ACCEPTED waiver: expiry date")
|
|
4061
4308
|
pg.set_defaults(func=cmd_gate, _opt_positionals=("slug",))
|
|
4062
4309
|
|
|
4310
|
+
pan = sub.add_parser("autonomy", help="show or set the autonomy level (the verify-gate owner)")
|
|
4311
|
+
pan.add_argument("action", nargs="?", choices=("show", "set"), default="show")
|
|
4312
|
+
pan.add_argument("a1", nargs="?", default=None, help="set: <level>; show: [slug]")
|
|
4313
|
+
pan.add_argument("a2", nargs="?", default=None, help="set: [slug]")
|
|
4314
|
+
pan.add_argument("--project", action="store_true",
|
|
4315
|
+
help="set the PROJECT.md default instead of a task header")
|
|
4316
|
+
pan.add_argument("--yes", action="store_true",
|
|
4317
|
+
help="confirm a RAISE toward auto (a human-owned trust escalation)")
|
|
4318
|
+
pan.set_defaults(func=cmd_autonomy, _opt_positionals=("a1", "a2"))
|
|
4319
|
+
|
|
4063
4320
|
pr = sub.add_parser("reopen", help="return a done task to an earlier phase with a recorded reason")
|
|
4064
4321
|
pr.add_argument("slug", nargs="?", default=None)
|
|
4065
4322
|
# --to / --reason are validated in-body (not argparse choices) so the named reject
|
|
@@ -4171,11 +4428,120 @@ def _rebind_optional_positionals(parser: argparse.ArgumentParser,
|
|
|
4171
4428
|
return args
|
|
4172
4429
|
|
|
4173
4430
|
|
|
4431
|
+
# --- agent-agnostic update nudge --------------------------------------------
|
|
4432
|
+
# ADD is agent-agnostic: ANY agent (Claude Code · Gemini CLI · Codex) is told by the
|
|
4433
|
+
# guideline block to run `add.py status`/`guide` FIRST, every session. That is the one
|
|
4434
|
+
# universal chokepoint to tell a stale install to refresh — a plain line on STDERR the
|
|
4435
|
+
# agent reads and acts on. Bounded + fail-open by design (see _maybe_nudge_update).
|
|
4436
|
+
#
|
|
4437
|
+
# This is the engine's ONE deliberate, isolated network touch. It is justified narrowly:
|
|
4438
|
+
# an agent that is offline cannot run at all, so when the network is unreachable this
|
|
4439
|
+
# silently does nothing and nothing is lost. It NEVER changes a command's stdout or exit.
|
|
4440
|
+
_UPDATE_CACHE = ".update-cache.json"
|
|
4441
|
+
_UPDATE_TTL = timedelta(hours=24) # hit the registry at most once / day
|
|
4442
|
+
_REGISTRY_LATEST = "https://registry.npmjs.org/@pilotspace/add/latest"
|
|
4443
|
+
|
|
4444
|
+
|
|
4445
|
+
def _read_json_safe(path: Path):
|
|
4446
|
+
try:
|
|
4447
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
4448
|
+
except (OSError, json.JSONDecodeError):
|
|
4449
|
+
return None
|
|
4450
|
+
|
|
4451
|
+
|
|
4452
|
+
def _write_json_safe(path: Path, obj) -> None:
|
|
4453
|
+
try:
|
|
4454
|
+
path.write_text(json.dumps(obj, indent=2) + "\n", encoding="utf-8")
|
|
4455
|
+
except OSError:
|
|
4456
|
+
pass
|
|
4457
|
+
|
|
4458
|
+
|
|
4459
|
+
def _version_gt(a: str, b: str) -> bool:
|
|
4460
|
+
"""True if version a is newer than b (dotted numeric; prerelease suffix dropped)."""
|
|
4461
|
+
def key(v: str):
|
|
4462
|
+
out = []
|
|
4463
|
+
for part in str(v).split("."):
|
|
4464
|
+
part = part.split("-", 1)[0]
|
|
4465
|
+
out.append((0, int(part)) if part.isdigit() else (1, part))
|
|
4466
|
+
return out
|
|
4467
|
+
try:
|
|
4468
|
+
return key(a) > key(b)
|
|
4469
|
+
except Exception:
|
|
4470
|
+
return False
|
|
4471
|
+
|
|
4472
|
+
|
|
4473
|
+
def _fetch_latest_version(timeout: float = 1.5):
|
|
4474
|
+
"""GET the registry's latest version. Returns a string, or None on ANY failure
|
|
4475
|
+
(offline, timeout, bad payload) — the caller treats None as 'unknown, skip'."""
|
|
4476
|
+
try:
|
|
4477
|
+
req = urllib.request.Request(_REGISTRY_LATEST, headers={"Accept": "application/json"})
|
|
4478
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
4479
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
4480
|
+
v = data.get("version")
|
|
4481
|
+
return v if isinstance(v, str) and v else None
|
|
4482
|
+
except Exception:
|
|
4483
|
+
return None
|
|
4484
|
+
|
|
4485
|
+
|
|
4486
|
+
def _cached_latest(add_dir: Path):
|
|
4487
|
+
"""The registry's latest version, throttled: served from .update-cache.json within
|
|
4488
|
+
the TTL, else refreshed over the network (fail-open). None when unknown."""
|
|
4489
|
+
cache = _read_json_safe(add_dir / _UPDATE_CACHE)
|
|
4490
|
+
if cache and cache.get("latest") and cache.get("checked_at"):
|
|
4491
|
+
try:
|
|
4492
|
+
ts = datetime.fromisoformat(cache["checked_at"])
|
|
4493
|
+
if datetime.now(timezone.utc) - ts < _UPDATE_TTL:
|
|
4494
|
+
return cache["latest"] # fresh -> no network
|
|
4495
|
+
except ValueError:
|
|
4496
|
+
pass
|
|
4497
|
+
latest = _fetch_latest_version()
|
|
4498
|
+
if latest:
|
|
4499
|
+
_write_json_safe(add_dir / _UPDATE_CACHE, {
|
|
4500
|
+
"checked_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
4501
|
+
"latest": latest,
|
|
4502
|
+
})
|
|
4503
|
+
return latest
|
|
4504
|
+
return cache.get("latest") if cache else None # stale beats nothing
|
|
4505
|
+
|
|
4506
|
+
|
|
4507
|
+
def _maybe_nudge_update(args: argparse.Namespace) -> None:
|
|
4508
|
+
"""If the tooling is stale, print an ACTION-REQUIRED line to STDERR so the agent
|
|
4509
|
+
updates before the user's task. Fully bounded + fail-open — never raises, never
|
|
4510
|
+
touches stdout or the exit code."""
|
|
4511
|
+
try:
|
|
4512
|
+
if os.environ.get("ADD_NO_UPDATE_CHECK"):
|
|
4513
|
+
return
|
|
4514
|
+
fn = getattr(args, "func", None)
|
|
4515
|
+
# orientation reads only — never a mid-run mutating verb (gate/advance/...)
|
|
4516
|
+
if getattr(fn, "__name__", "") not in ("cmd_status", "cmd_guide"):
|
|
4517
|
+
return
|
|
4518
|
+
add_dir = find_root()
|
|
4519
|
+
if add_dir is None:
|
|
4520
|
+
return
|
|
4521
|
+
stamp = _read_json_safe(add_dir / ".add-version")
|
|
4522
|
+
current = (stamp or {}).get("version")
|
|
4523
|
+
if not current:
|
|
4524
|
+
return # no launcher stamp -> inert (engine-only/test projects)
|
|
4525
|
+
latest = _cached_latest(add_dir)
|
|
4526
|
+
if not latest or not _version_gt(latest, current):
|
|
4527
|
+
return
|
|
4528
|
+
cmd = ("pipx run pilotspace-add update"
|
|
4529
|
+
if (stamp or {}).get("channel") == "pip"
|
|
4530
|
+
else "npx @pilotspace/add@latest update")
|
|
4531
|
+
sys.stderr.write(
|
|
4532
|
+
f"\n⚠ ADD is out of date: this project is on {current}, latest is {latest}.\n"
|
|
4533
|
+
f" ACTION REQUIRED before the user's task: run `{cmd}`, then re-read the "
|
|
4534
|
+
f"guide from disk, then continue.\n\n")
|
|
4535
|
+
except Exception:
|
|
4536
|
+
return # a nudge must NEVER break a command (design-for-failure)
|
|
4537
|
+
|
|
4538
|
+
|
|
4174
4539
|
def main(argv: list[str] | None = None) -> int:
|
|
4175
4540
|
parser = build_parser()
|
|
4176
4541
|
args, extras = parser.parse_known_args(argv)
|
|
4177
4542
|
if extras:
|
|
4178
4543
|
args = _rebind_optional_positionals(parser, args, extras)
|
|
4544
|
+
_maybe_nudge_update(args) # advisory preamble; stderr-only, fail-open
|
|
4179
4545
|
args.func(args)
|
|
4180
4546
|
return 0
|
|
4181
4547
|
|
|
@@ -30,3 +30,7 @@ lowest-confidence flag: the AI's ranked declaration of the 1–2 points most lik
|
|
|
30
30
|
decision point: a stop for human judgment — the contract-freeze approval, an escalated verify gate, intake confirmation, milestone close; the machine names seam (--json owner enum, decide key) and seam-audit (CI job) keep their names (formerly "seam").
|
|
31
31
|
retrospective consolidation: gathering confirmed lessons learned at milestone close and writing them append-only into the versioned foundation — human-confirmed, never self-approved; the machine names fold.md, the folded status, and add.py deltas keep their names (formerly "the fold / fold ritual").
|
|
32
32
|
specification bundle: a task's spec, scenarios, contract, and failing tests drafted as one piece and approved by a person once at the contract freeze (formerly "the one-approval front").
|
|
33
|
+
foundation compaction: the retrospective shrink — collapse a foundation spec's stable, shipped, zero-residue tail into one rolled-up settled line; the AI proposes and the human confirms; summarize and point, never delete; a SEPARATE step from the retrospective consolidation; distinct from the engine `add.py compact` (which archives finished-milestone files).
|
|
34
|
+
rolled-up settled line: the single line left when a stable run is collapsed — lossy on prose, lossless on traceability (carries a `see git` pointer).
|
|
35
|
+
per-spec shape: each foundation spec's own tailored rolled-line format (PROJECT §Spec bullets · §Key-Decisions rows · CONVENTIONS learnings · GLOSSARY definition · MODEL_REGISTRY rows), all sharing one eligibility rule: shipped + zero open residues.
|
|
36
|
+
newest-first append-only: every append-only foundation sequence prepends the newest record at the top; the rolled-up settled line anchors at the bottom (the oldest end), so compaction collapses upward.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SOUL — Trusting
|
|
2
|
+
|
|
3
|
+
> The AI's **voice** for {{project}}: how it sounds, how it communicates, and what keeps your trust.
|
|
4
|
+
> This is a **living doc** and it is **human-owned**. The prose below is a **starter** — a proposed
|
|
5
|
+
> default you can rewrite at any time. It is **overridable**, never a frozen claim about who the AI is.
|
|
6
|
+
> The AI proposes voice changes from how you actually talk and work; **your confirm is the only writer**.
|
|
7
|
+
> The observe→confirm→rewrite loop that tunes this file is the `add` skill's `soul-self-improve` path.
|
|
8
|
+
> Edit freely — the engine never clobbers your SOUL.md.
|
|
9
|
+
|
|
10
|
+
## Name
|
|
11
|
+
**Trusting** — the voice earns trust by showing its work, not by sounding confident.
|
|
12
|
+
|
|
13
|
+
## Tone
|
|
14
|
+
- Plain and direct; first-principles over jargon. Say the thing, then the why.
|
|
15
|
+
- Calm under uncertainty: name what you don't know instead of writing past it.
|
|
16
|
+
- Warm but not performative — no flattery, no filler, no congratulating the human for asking.
|
|
17
|
+
|
|
18
|
+
## Communication style
|
|
19
|
+
- Lead with the summary: intent + target first, then the detail.
|
|
20
|
+
- Show before you ask: render the diff / result / artifact, THEN ask the human to decide.
|
|
21
|
+
- Surface tradeoffs and the single lowest-confidence flag; don't hide confusion.
|
|
22
|
+
- Keep it lean: the smallest message that moves the work forward, not more than the work needs.
|
|
23
|
+
|
|
24
|
+
## Trust
|
|
25
|
+
- Never pre-stamp a human decision — freeze / gate / lock only AFTER the human has answered.
|
|
26
|
+
- Close a disclosed gap before recording a PASS; a security finding is always a hard stop.
|
|
27
|
+
- Report outcomes faithfully: failing tests stay failing in the telling, skipped steps are named.
|
|
28
|
+
- Identity decisions (voice, naming, brand) are the human's — ask open, don't offer a menu of guesses.
|
|
29
|
+
|
|
30
|
+
## Learns from
|
|
31
|
+
- The human's **wordings** — the words they reach for, and the words they correct you on — and the
|
|
32
|
+
**flow** of how they actually work (what they skip, what they double-check). NOT their private memory.
|
|
33
|
+
- Each session: read this file when orienting, and notice where your voice diverged from theirs.
|
|
34
|
+
|
|
35
|
+
## Voice deltas
|
|
36
|
+
The self-improve loop (the `add` skill's `soul.md`) proposes a confirmable **voice delta** when it sees
|
|
37
|
+
a gap between this voice and the human's. Lifecycle `open → confirmed`, like a foundation delta; the
|
|
38
|
+
human's confirm rewrites the sections above (the human is the only writer). Until confirmed, the starter
|
|
39
|
+
voice stands. New confirmed deltas are recorded here newest-first.
|
|
40
|
+
- (no confirmed voice deltas yet)
|