ultra-builder-pro-cli 0.26.0 → 0.26.2

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.
Files changed (48) hide show
  1. package/.ultra-template/changes/abandoned/.gitkeep +0 -0
  2. package/.ultra-template/contexts/TEMPLATE.md +7 -0
  3. package/.ultra-template/north-star.md +20 -31
  4. package/.ultra-template/project-brief.md +28 -0
  5. package/.ultra-template/specs/product.md +4 -0
  6. package/.ultra-template/specs/research-distillate.md +8 -0
  7. package/.ultra-template/test-report.json +1 -0
  8. package/CHANGELOG.md +131 -1
  9. package/README.md +61 -29
  10. package/bin/delegate-worker.cjs +5 -0
  11. package/bin/delegate.cjs +7 -0
  12. package/docs/ARCHITECTURE.md +36 -10
  13. package/docs/ARTIFACT-AUTHORITY.md +42 -13
  14. package/docs/DECISIONS.md +24 -5
  15. package/docs/PHILOSOPHY.md +45 -12
  16. package/docs/PLUGIN-ISOLATION-CONTRACT.md +6 -3
  17. package/docs/RUNTIME-COMPAT-MATRIX.md +43 -1
  18. package/docs/SKILL-AUTHORING.md +10 -6
  19. package/docs/WORKFLOW-LIFECYCLE.md +103 -39
  20. package/hooks/README.md +12 -5
  21. package/hooks/_common.py +54 -5
  22. package/hooks/block_dangerous_commands.py +116 -10
  23. package/hooks/session_context.py +34 -8
  24. package/package.json +1 -1
  25. package/skills/ultra-change/SKILL.md +59 -52
  26. package/skills/ultra-change/references/change-contract.md +65 -7
  27. package/skills/ultra-delegate/SKILL.md +21 -3
  28. package/skills/ultra-delegate/references/delegation-contract.md +10 -2
  29. package/skills/ultra-deliver/SKILL.md +63 -21
  30. package/skills/ultra-deliver/references/baseline-reconciliation.md +5 -3
  31. package/skills/ultra-dev/SKILL.md +33 -14
  32. package/skills/ultra-grilling/SKILL.md +63 -24
  33. package/skills/ultra-grilling/references/reframing.md +72 -0
  34. package/skills/ultra-init/SKILL.md +62 -79
  35. package/skills/ultra-init/references/brownfield-adoption.md +23 -40
  36. package/skills/ultra-plan/SKILL.md +48 -24
  37. package/skills/ultra-research/SKILL.md +91 -62
  38. package/skills/ultra-research/references/{03-competitive-landscape.md → 03-alternatives.md} +6 -6
  39. package/skills/ultra-research/references/22-success-metrics.md +8 -4
  40. package/skills/ultra-research/references/99-synthesis.md +8 -1
  41. package/skills/ultra-research/references/wayfinding.md +42 -0
  42. package/skills/ultra-review/SKILL.md +31 -3
  43. package/skills/ultra-status/SKILL.md +62 -32
  44. package/skills/ultra-tdd/SKILL.md +5 -2
  45. package/skills/ultra-test/SKILL.md +31 -12
  46. package/skills/ultra-test/scripts/worktree_digest.cjs +42 -8
  47. package/skills/ultra-think/SKILL.md +5 -10
  48. package/skills/ultra-think/references/interaction-boundary.md +2 -1
package/hooks/README.md CHANGED
@@ -5,18 +5,25 @@ directory and exits silently when the workflow is not active.
5
5
 
6
6
  | Hook | Lifecycle | Purpose |
7
7
  |---|---|---|
8
- | `session_context.py` | Session start | Inject the one-line north star and current acceptance criteria |
9
- | `mid_workflow_recall.py` | Before source reads or edits | Restate current acceptance only |
8
+ | `session_context.py` | Session start | Inject the accepted North Star or Project Brief fallback, plus acceptance from the unique active Change's current task |
9
+ | `mid_workflow_recall.py` | Before source reads or edits | Restate acceptance only for a task matching the unique active `change_id` |
10
10
  | `compact_context.py` | Before/after compaction | Save and restore a disposable snapshot derived from files and Git |
11
11
  | `post_edit_guard.py` | After edits | Record mechanical evidence observations without deciding completion |
12
- | `block_dangerous_commands.py` | Before shell execution | Block a narrow named external effect until the exact command is owner-authorized |
12
+ | `block_dangerous_commands.py` | Before shell execution | Advise on additive protected-branch publication; block history rewrites and a narrow destructive set until the exact command is owner-authorized |
13
13
 
14
14
  `_common.py` is a shared library, not a lifecycle registration. Compact snapshots and
15
15
  progress observations live under ignored `.ultra/.runtime/` or `.ultra/progress/`; the
16
16
  canonical semantic facts remain the owner-readable files and Git.
17
+ Legacy task rows with no `change_id` remain readable from an active-path `change_ref`
18
+ during migration; a present canonical `change_id` always wins, and archived or
19
+ abandoned paths are never revived as current work. Task
20
+ selection prefers the single `in_progress` row, then the first `pending` row whose
21
+ same-Change dependencies are complete; ambiguous concurrent work stays silent.
17
22
 
18
23
  Claude Code runs the hooks directly. Codex, Kimi Code, and Grok Build use the small
19
24
  wire adapters under `hooks/adapters/`. OpenCode calls the same scripts from its native
20
25
  JavaScript plugin. A hook failure is advisory except for the explicitly classified
21
- external effects in `block_dangerous_commands.py`, whose denial includes a reachable
22
- SHA-256-scoped authorization path.
26
+ destructive effects in `block_dangerous_commands.py`, whose denial includes a reachable
27
+ SHA-256-scoped authorization path. Additive protected-branch publication stays advisory
28
+ because portable hooks cannot consume every host's trusted owner-approval receipt;
29
+ history rewrites and branch deletion remain guarded.
package/hooks/_common.py CHANGED
@@ -47,12 +47,61 @@ def read_tasks(root: Path) -> list[dict[str, Any]]:
47
47
  return [item for item in tasks if isinstance(item, dict)] if isinstance(tasks, list) else []
48
48
 
49
49
 
50
+ def active_change_id(root: Path) -> str | None:
51
+ active = root / ".ultra" / "changes" / "active"
52
+ try:
53
+ change_ids = sorted(
54
+ entry.name
55
+ for entry in active.iterdir()
56
+ if entry.is_dir() and (entry / "intent.md").is_file()
57
+ )
58
+ except OSError:
59
+ return None
60
+ if len(change_ids) != 1:
61
+ return None
62
+ change_id = change_ids[0]
63
+ return change_id if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", change_id) else None
64
+
65
+
66
+ def legacy_task_change_id(task: dict[str, Any]) -> str | None:
67
+ value = task.get("change_ref")
68
+ if not isinstance(value, str):
69
+ return None
70
+ match = re.fullmatch(
71
+ r"(?:\.ultra/)?changes/active/([A-Za-z0-9][A-Za-z0-9._-]*)/intent\.md",
72
+ value,
73
+ )
74
+ return match.group(1) if match else None
75
+
76
+
50
77
  def current_task(root: Path) -> dict[str, Any] | None:
51
- tasks = read_tasks(root)
52
- for status in ("in_progress", "pending"):
53
- for task in tasks:
54
- if task.get("status") == status:
55
- return task
78
+ change_id = active_change_id(root)
79
+ if change_id is None:
80
+ return None
81
+ tasks = [
82
+ task for task in read_tasks(root)
83
+ if task.get("change_id") == change_id
84
+ or ("change_id" not in task and legacy_task_change_id(task) == change_id)
85
+ ]
86
+ in_progress = [task for task in tasks if task.get("status") == "in_progress"]
87
+ if len(in_progress) == 1:
88
+ return in_progress[0]
89
+ if len(in_progress) > 1:
90
+ return None
91
+ by_id = {task.get("id"): task for task in tasks if isinstance(task.get("id"), str)}
92
+ for task in tasks:
93
+ if task.get("status") != "pending":
94
+ continue
95
+ dependencies = task.get("dependencies", [])
96
+ if not isinstance(dependencies, list):
97
+ continue
98
+ if all(
99
+ isinstance(dependency, str)
100
+ and dependency in by_id
101
+ and by_id[dependency].get("status") == "completed"
102
+ for dependency in dependencies
103
+ ):
104
+ return task
56
105
  return None
57
106
 
58
107
 
@@ -7,11 +7,88 @@ import hashlib
7
7
  import json
8
8
  import os
9
9
  import re
10
+ import shlex
10
11
  import subprocess
11
12
 
12
13
  from _common import emit_context, project_root, read_payload
13
14
 
14
15
 
16
+ APPROVAL_VAR = "UBP_DANGEROUS_COMMAND_APPROVED"
17
+ APPROVAL_PREFIX = re.compile(rf"^\s*{APPROVAL_VAR}=\S*\s+")
18
+
19
+ HEREDOC = re.compile(
20
+ r"<<-?\s*(['\"]?)(?P<tag>[A-Za-z_]\w*)\1(?P<rest>[^\n]*)\n(?P<body>.*?)^\s*(?P=tag)\s*$",
21
+ re.DOTALL | re.MULTILINE,
22
+ )
23
+ # Commands whose heredoc is a payload rather than a script. Deliberately short: an
24
+ # unlisted sink keeps its body in scope, so the cost of an omission is a false positive,
25
+ # never a missed effect.
26
+ DATA_SINK = re.compile(r"\bgit\s+(?:commit|tag|notes)\b", re.IGNORECASE)
27
+ SEARCH_COMMANDS = {"egrep", "fgrep", "grep", "rg"}
28
+ SHELL_CONTROL = {";", ";;", "&", "&&", "|", "||", "(", ")", "\n"}
29
+
30
+
31
+ def strip_data_heredocs(command: str) -> str:
32
+ """Drop heredoc bodies that are data for a known sink.
33
+
34
+ Describing an effect inside a commit message is not performing it, and a message
35
+ that mentions one used to be blocked as though it did.
36
+
37
+ Bodies are kept whenever they might be executed: an interpreter reading a heredoc
38
+ runs it, so stripping there would make quoting style a way around this gate. The
39
+ body survives unless the delimiter is quoted, the segment introducing it is a
40
+ listed data sink, and nothing on the heredoc's own line pipes it somewhere else.
41
+ An unquoted delimiter performs parameter, command and arithmetic expansion before
42
+ the sink receives the data, so its body remains executable shell input.
43
+ """
44
+ def resolve(match: re.Match) -> str:
45
+ segment = re.split(r"\|\||&&|[;&|\n]", command[:match.start()])[-1]
46
+ if (
47
+ not match.group(1)
48
+ or "|" in match.group("rest")
49
+ or not DATA_SINK.search(segment)
50
+ ):
51
+ return match.group(0)
52
+ tag = match.group("tag")
53
+ return f"<<{tag}\n{tag}"
54
+
55
+ return HEREDOC.sub(resolve, command)
56
+
57
+
58
+ def effect_of(command: str) -> str:
59
+ """The command reduced to what it actually does.
60
+
61
+ Strips an inline approval assignment, then any heredoc body that is data for a
62
+ known sink. What remains is the effect: the part worth classifying and hashing.
63
+
64
+ The digest must identify the *effect*, not the spelling of the retry. Without this,
65
+ prepending the variable changes the command text, which changes the digest, which
66
+ means the quoted digest is never the one that matches -- the repair path is a loop.
67
+
68
+ The assigned value is discarded on purpose. Only the process environment authorizes,
69
+ because the command string is composed by the model and honouring an inline value
70
+ would let it approve itself.
71
+ """
72
+ return strip_data_heredocs(APPROVAL_PREFIX.sub("", command, count=1))
73
+
74
+
75
+ def is_inert_search(command: str) -> bool:
76
+ """Return true when a search query is data, not executable shell input."""
77
+ if any(marker in command for marker in ("$(", "<(", ">(", "`")):
78
+ return False
79
+ try:
80
+ lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|()\n")
81
+ lexer.whitespace = " \t\r"
82
+ lexer.whitespace_split = True
83
+ lexer.commenters = ""
84
+ tokens = list(lexer)
85
+ except ValueError:
86
+ return False
87
+ if not tokens or any(token in SHELL_CONTROL for token in tokens):
88
+ return False
89
+ return os.path.basename(tokens[0]) in SEARCH_COMMANDS
90
+
91
+
15
92
  def protected_push(command: str, root) -> bool:
16
93
  if not re.search(r"\bgit\s+push\b", command, re.IGNORECASE):
17
94
  return False
@@ -24,9 +101,26 @@ def protected_push(command: str, root) -> bool:
24
101
  return result.stdout.strip() in {"main", "master", "production", "prod"}
25
102
 
26
103
 
104
+ def destructive_protected_push(command: str, root) -> bool:
105
+ """Detect history rewrites or deletion, not ordinary additive publication."""
106
+ if not protected_push(command, root):
107
+ return False
108
+ match = re.search(r"\bgit\s+push\b", command, re.IGNORECASE)
109
+ if match is None:
110
+ return False
111
+ segment = re.split(r"\|\||&&|[;&|\n]", command[match.end():], maxsplit=1)[0]
112
+ force_or_delete = re.search(
113
+ r"(?:^|\s)(?:-f|--force(?:-with-lease(?:=\S+)?|-if-includes)?|--delete)(?=\s|$)",
114
+ segment,
115
+ re.IGNORECASE,
116
+ )
117
+ forced_refspec = re.search(r"(?:^|\s)(?:\+\S+|:\S+)(?=\s|$)", segment)
118
+ return force_or_delete is not None or forced_refspec is not None
119
+
120
+
27
121
  def classify(command: str, root) -> str | None:
28
- if protected_push(command, root):
29
- return "protected branch push"
122
+ if destructive_protected_push(command, root):
123
+ return "protected branch history rewrite or deletion"
30
124
  if re.search(r"\b(?:drop\s+(?:table|database|schema)|truncate\s+(?:table\s+)?)\b", command, re.IGNORECASE):
31
125
  return "destructive database operation"
32
126
  if re.search(r"\b(?:cast\s+send|solana\s+transfer|bitcoin-cli\s+send|near\s+send|aptos\s+move\s+run)\b", command, re.IGNORECASE):
@@ -42,7 +136,14 @@ def classify(command: str, root) -> str | None:
42
136
  return None
43
137
 
44
138
 
45
- def advisory(command: str) -> str | None:
139
+ def advisory(command: str, root) -> str | None:
140
+ if protected_push(command, root):
141
+ return (
142
+ "Ultra observed a protected branch push. This portable hook cannot receive "
143
+ "the host's trusted owner-approval receipt, so it does not replace the host's "
144
+ "native authority flow. Proceed only after explicit owner authorization and "
145
+ "verify the remote result."
146
+ )
46
147
  if re.search(
47
148
  r"\b(?:alembic\s+upgrade|prisma\s+migrate|knex\s+migrate|sequelize\s+db:migrate|rails\s+db:migrate|manage\.py\s+migrate)\b",
48
149
  command,
@@ -65,19 +166,24 @@ def main() -> int:
65
166
  command = tool_input.get("command")
66
167
  if not isinstance(command, str) or not command.strip():
67
168
  return 0
68
- threat = classify(command, root)
169
+ effect = effect_of(command)
170
+ if is_inert_search(effect):
171
+ return 0
172
+ threat = classify(effect, root)
69
173
  if threat is None:
70
- note = advisory(command)
174
+ note = advisory(effect, root)
71
175
  if note:
72
176
  emit_context("PreToolUse", note)
73
177
  return 0
74
- digest = hashlib.sha256(command.encode()).hexdigest()
75
- if os.environ.get("UBP_DANGEROUS_COMMAND_APPROVED") == digest:
178
+ digest = hashlib.sha256(effect.encode()).hexdigest()
179
+ if os.environ.get(APPROVAL_VAR) == digest:
76
180
  return 0
77
181
  reason = (
78
- f"Ultra blocked {threat}. Protected effect: the exact shell command. "
79
- f"After explicit owner authorization, rerun with "
80
- f"UBP_DANGEROUS_COMMAND_APPROVED={digest}."
182
+ f"Ultra blocked {threat}. Protected effect: the exact shell command, "
183
+ f"digest {digest}. Authorization comes from the owner's environment, never from "
184
+ f"the command text. Either run it yourself, or export "
185
+ f"{APPROVAL_VAR}={digest} in the environment that launches the agent and ask "
186
+ f"again. Prefixing the assignment onto this command does not authorize it."
81
187
  )
82
188
  print(json.dumps({
83
189
  "decision": "block",
@@ -1,22 +1,48 @@
1
1
  #!/usr/bin/env python3
2
- """Inject the one-line goal and current task acceptance at session start."""
2
+ """Inject the accepted North Star or its pre-Research fallback plus task acceptance."""
3
3
 
4
4
  from _common import current_task, emit_context, markdown_section, project_root, read_payload, task_sections
5
5
 
6
6
 
7
+ def settled_section(text: str) -> str:
8
+ """Return an explicitly populated section, not a packaged unresolved marker."""
9
+ if not text or "[NEEDS CLARIFICATION]" in text or "not yet defined" in text.lower():
10
+ return ""
11
+ return text.strip()
12
+
13
+
14
+ def read_text(file) -> str:
15
+ try:
16
+ return file.read_text(encoding="utf-8")
17
+ except OSError:
18
+ return ""
19
+
20
+
7
21
  def main() -> int:
8
22
  payload = read_payload()
9
23
  root = project_root(payload) # Idle guard: no `.ultra/` means silent exit.
10
24
  if root is None:
11
25
  return 0
12
- try:
13
- north_star = (root / ".ultra" / "north-star.md").read_text(encoding="utf-8")
14
- except OSError:
15
- north_star = ""
26
+ ultra = root / ".ultra"
27
+ north_star = read_text(ultra / "north-star.md")
28
+ project_brief = read_text(ultra / "project-brief.md")
16
29
  parts = []
17
- one_line = markdown_section(north_star, "One-line")
18
- if one_line:
19
- parts.append(f"Ultra north star:\n{one_line}")
30
+ direction = settled_section(markdown_section(north_star, "Project Direction"))
31
+ if direction:
32
+ baseline = [f"Project Direction:\n{direction}"]
33
+ for heading in ("North Star Outcome", "Hard Constraints"):
34
+ section = settled_section(markdown_section(north_star, heading))
35
+ if section:
36
+ baseline.append(f"{heading}:\n{section}")
37
+ baseline_text = "\n\n".join(baseline)
38
+ parts.append(f"Ultra accepted north star:\n{baseline_text}")
39
+ else:
40
+ brief_line = settled_section(markdown_section(project_brief, "One-line"))
41
+ legacy_line = settled_section(markdown_section(north_star, "One-line"))
42
+ if brief_line:
43
+ parts.append(f"Ultra project brief (Research not yet accepted):\n{brief_line}")
44
+ elif legacy_line:
45
+ parts.append(f"Ultra legacy project intent:\n{legacy_line}")
20
46
  task = current_task(root)
21
47
  if task:
22
48
  acceptance = task_sections(root, task)["acceptance"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra-builder-pro-cli",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
4
4
  "description": "File-first Ultra Builder Pro workflows for Claude Code, OpenCode, Codex CLI, Kimi Code, and Grok Build.",
5
5
  "bin": {
6
6
  "ultra-builder-pro-cli": "bin/install.js",
@@ -1,55 +1,56 @@
1
1
  ---
2
2
  name: ultra-change
3
- description: Open one bounded unit of work by first reconciling what the specifications promise against what the code actually does, then writing the intent that closes the gap. Use when nothing is in flight and something new is being asked for — a feature, a fix, a redesign, or an incident response.
3
+ description: Open or reconcile one bounded unit of work by comparing specification promises with actual code, then writing the intent that closes the gap. Use when no Change is active and a feature, fix, redesign, or incident response is requested, or when an active Change's draft, researched evidence, or accepted boundary must be reconciled.
4
4
  ---
5
5
 
6
6
  # Reconcile first, then write down what this Change commits to
7
7
 
8
- A Change is the durable unit connecting research, plan, implementation,
9
- verification, review and delivery. Reconciliation comes first because writing new
10
- intent on top of specifications that already disagree with the code buries the
11
- disagreement instead of resolving it.
8
+ A Change connects research, plan, implementation, verification, review, and delivery.
9
+ Reconcile first so new intent does not bury an existing specification/code conflict.
12
10
 
13
11
  ## Before you start
14
12
 
15
- 1. Read `.ultra/tasks.json`, and the `context_file` of any task not yet finished.
16
- 2. Read `CONTEXT.md` for vocabulary and `.ultra/decisions/` for entries in scope.
17
- 3. Read `.ultra/north-star.md` — a Change that does not serve it is worth
18
- questioning out loud before it is written down.
13
+ 1. List `.ultra/changes/active/`; more than one `intent.md` is a conflict, not a choice.
14
+ One directory name is the stable `change_id`. Read `.ultra/tasks.json`, then only its
15
+ unfinished `context_file` values. Update the same `change_id`; never open a second active Change.
16
+ 2. Read `.ultra/project-brief.md`, `CONTEXT.md`, relevant `.ultra/decisions/`, and
17
+ `.ultra/north-star.md` for raw provenance, accepted language, and direction.
18
+ 3. Grep archive and abandoned Changes that touched this area. Prefer archived
19
+ `delivery.md`; otherwise read `archive-summary.md`, `verification.md`, `intent.md`,
20
+ and remaining Markdown. Read abandoned Changes as history of rejected boundaries and
21
+ reusable evidence, never current intent. A missing `delivery.md` does not mean no history;
22
+ repeated modules are an architecture signal.
19
23
 
20
24
  ## Whether this skill applies at all
21
25
 
22
26
  | Situation | Where it goes |
23
27
  |---|---|
24
- | No pending or in-progress task, and something new is asked for | Here |
28
+ | No active Change, and a new bounded request exists | Here — create one stable id |
29
+ | One active Change needs its draft, Research evidence, or accepted boundary reconciled | Here — update the same `change_id` |
30
+ | A separate request arrives while one Change remains active | Do not create a second Change; finish or deliver the current one, abandon it with owner authority, or explicitly fold the request into its boundary |
25
31
  | A task is in progress and the specification turns out not to match reality | Not here — the dual-write path inside `ultra-dev` |
26
- | The change makes no sentence in the specifications false | Neither — just make the change |
32
+ | The change makes no sentence in the specifications false | Outside the Ultra lifecycle — use ordinary repository TDD and verification |
27
33
 
28
- That third row is the checkable answer to "how small is too small to deserve a
29
- Change file": if nothing the specifications say becomes untrue, there is nothing
30
- to reconcile and nothing to record.
34
+ If no specification sentence becomes untrue, there is nothing to reconcile. Use
35
+ `../ultra-tdd/SKILL.md` when helpful, but create no Change artifacts or Ultra
36
+ Test/Deliver claim for that micro change.
31
37
 
32
38
  ## Definition of done
33
39
 
34
- - `.ultra/changes/active/<id>/intent.md` states the observable outcome, the
35
- executable acceptance, the non-goals, and the public seams touched.
36
- - Each of the three reconciliation buckets is either empty or dispositioned, with
37
- the owner's answer recorded for every bucket that was not empty.
38
- - The draft stays editable until then; the owner's acceptance is what turns it
39
- into authority.
40
+ - Active `intent.md` states outcome, executable acceptance, non-goals, public seams,
41
+ and Research Disposition.
42
+ - Every reconciliation bucket is empty or dispositioned with the owner's answer.
43
+ - The draft remains editable until owner acceptance makes it authority.
40
44
 
41
45
  ## Reconcile against a bounded scope
42
46
 
43
- A whole-repository diff is unusable, so derive the scope with four steps anyone
44
- can rerun:
47
+ A Change does not rebuild the project baseline; reconcile only the touched specification sections
48
+ and leave unrelated gaps visible with four rerunnable steps:
45
49
 
46
50
  1. Decide which specification sections this request touches.
47
- 2. For each section, collect the past tasks whose `trace_to` points at it, and
48
- from those the set of code files they changed.
49
- 3. `git log -- .ultra/specs/<file>` gives the commit where that specification
50
- last changed.
51
- 4. `git diff <that commit>..HEAD -- <those code files>` is the reconciliation
52
- scope.
51
+ 2. Collect past tasks whose `trace_to` points there and the code files they changed.
52
+ 3. Use `git log -- .ultra/specs/<file>` to find its last specification change.
53
+ 4. Inspect `git diff <that commit>..HEAD -- <those code files>`.
53
54
 
54
55
  Sort what you find into three buckets, each with your recommended disposition:
55
56
 
@@ -59,43 +60,49 @@ Sort what you find into three buckets, each with your recommended disposition:
59
60
  | Code implements it, no specification says so | Built and unpromised |
60
61
  | Both speak and they conflict | Contradictory |
61
62
 
62
- Take the buckets to the owner. This is confirmed rather than assumed, because a
63
- disposition can quietly be a REDUCTION.
63
+ Take non-empty buckets to the owner because disposition can withdraw a commitment.
64
+ Ask one recommended question per `HC-<n>` impact or removed promise; group remaining
65
+ low-risk rows into one recommended list for confirmation.
64
66
 
65
67
  ## Write the intent
66
68
 
67
- Read `references/change-contract.md` and use its exact `intent.md` headings. The active
68
- Change directory contains only this accepted contract until planning or delivery adds
69
- its defined artifacts; do not invent a parallel semantic record.
69
+ Read `references/change-contract.md` and use its exact headings. Until planning or
70
+ delivery, the active directory contains only this contract; add no parallel record.
70
71
 
71
- Follow `../ultra-grilling/SKILL.md` to settle the boundary — one question at a
72
- time, each with your recommendation. New domain terms go through
73
- `../ultra-domain-modeling/SKILL.md`.
72
+ Use `../ultra-grilling/SKILL.md` to settle the boundary and
73
+ `../ultra-domain-modeling/SKILL.md` for new domain terms.
74
74
 
75
- When the work is too large *and* the path itself is unclear, follow
76
- `../ultra-think/SKILL.md` for a decision ticket instead of forcing a task
77
- breakdown. What comes out is a question whose answer is a decision, not an
78
- implementation slice.
75
+ Use `../ultra-think/SKILL.md` only for one consequential evidenced trade-off. If the
76
+ whole path is unclear, recommend Research instead of manufacturing tasks.
79
77
 
80
- When reconciliation proves the request is a directly executable one-slice correction,
81
- follow `../ultra-tdd/SKILL.md` at the confirmed public seam instead of manufacturing a
82
- task graph merely to satisfy ceremony.
78
+ Record `## Research Disposition` exactly. `none` needs enough evidence to plan;
79
+ `bounded` or `required` names one question, selected lenses, and exit evidence. After
80
+ Research, reconcile outputs here; an unverified path or bare claim is not evidence.
83
81
 
84
- Output is `.ultra/changes/active/<id>/intent.md`, the specification patches
85
- reconciliation justified, possibly a `.ultra/decisions/<id>.md`, and any
86
- vocabulary update. Recommend `ultra-research` for a real evidence gap or
87
- `ultra-plan` once the contract is evidenced enough; do not invoke either.
82
+ A quick active Change still goes through one-task `ultra-plan`; once durable intent
83
+ exists, that minimal task is what binds development, evidence, audit, and delivery to
84
+ the stable `change_id`.
85
+
86
+ Output active `intent.md` under a stable id, justified specification patches, qualifying
87
+ decision or vocabulary updates. For an evidence gap recommend `ultra-research`; when
88
+ evidenced recommend `ultra-plan`. Invoke neither.
89
+
90
+ ## Abandoning a Change
91
+
92
+ Append the exact `## Abandonment` closure from `references/change-contract.md`, then
93
+ `git mv .ultra/changes/active/<id> .ultra/changes/abandoned/<id>`. Keep completed-task
94
+ evidence. Future Change and Status consume this history; it is neither current intent
95
+ nor an orphan. Abandonment is the owner's call.
88
96
 
89
97
  ## When the owner decides
90
98
 
91
- Every non-empty bucket, the Change boundary, and anything reconciliation turns up
92
- that shrinks a prior commitment. A change that makes no specification sentence
93
- false needs no permission at all.
99
+ Every non-empty bucket, the Change boundary, abandonment, and anything
100
+ reconciliation turns up that shrinks a prior commitment. A change that makes no
101
+ specification sentence false needs no permission at all.
94
102
 
95
103
  ## References
96
104
 
97
105
  - `../ultra-grilling/SKILL.md` — the loop for settling the boundary.
98
106
  - `references/change-contract.md` — exact `intent.md` structure and profile fields.
99
- - `../ultra-think/references/autonomy-boundary.md` — read before dispositioning a
100
- bucket in a way that removes something already promised.
107
+ - `../ultra-think/references/autonomy-boundary.md` — read before removing a promise.
101
108
  - `../ultra-tdd/SKILL.md` — read for an evidenced one-slice correction.
@@ -3,6 +3,10 @@
3
3
  Create one contract before planning. Keep every field specific enough to verify and
4
4
  stable enough to survive a fresh session.
5
5
 
6
+ The directory id is globally unique across active, archive, and abandoned Changes and
7
+ matches `[A-Za-z0-9][A-Za-z0-9._-]*`. It is the stable `change_id`; moving the directory
8
+ never changes it.
9
+
6
10
  ## Exact `intent.md` structure
7
11
 
8
12
  ```markdown
@@ -10,6 +14,8 @@ stable enough to survive a fresh session.
10
14
 
11
15
  > **Status**: draft | accepted
12
16
  > **Profile**: quick | standard | major | incident
17
+ > **Profile rationale**: <why the smallest selected profile covers the blast radius>
18
+ > **Risk flags**: <applicable flags, or none>
13
19
 
14
20
  ## Outcome
15
21
  <one externally observable result>
@@ -29,8 +35,20 @@ stable enough to survive a fresh session.
29
35
  ### Built and Unpromised
30
36
  ### Contradictory
31
37
 
38
+ ## Research Disposition
39
+ - Disposition: none | bounded | required
40
+ - Question: <the exact load-bearing unknown, or none>
41
+ - Selected lenses: <reference ids, or none>
42
+ - Existing evidence: <repository-relative paths and claims>
43
+ - Required exit evidence: <what must exist or be resolved before planning>
44
+ - Rationale: <why this is sufficient for this Change boundary>
45
+
32
46
  ## Planning Posture
33
47
  <EXPAND | SELECTIVE | HOLD | REDUCE, rationale, and owner confirmation>
48
+ <!-- Scope for this Change only. Not the commitment classification: REDUCE here
49
+ means "plan less work now", while a REDUCTION in a Change Log means "a
50
+ promise the specification already made no longer holds". -->
51
+
34
52
 
35
53
  ## Recovery
36
54
  <reversal or recovery path and its verification>
@@ -45,8 +63,10 @@ heading, and do not add another file that restates the same contract.
45
63
  ## Contract fields
46
64
 
47
65
  - `outcome`: one externally observable result.
48
- - `acceptance`: stable ids with a criterion and exact verification for each accepted
49
- behavior.
66
+ - `acceptance`: stable ids, one criterion each, and a `Verification` that is an
67
+ **executable command** — something with an exit code, not a description of an
68
+ intention. `curl -sf localhost:3000/health` qualifies; "check that the endpoint
69
+ responds" does not.
50
70
  - `non_goals`: explicit boundaries that prevent accidental scope growth.
51
71
  - `public_seams`: entry points or outputs through which acceptance is observed.
52
72
  - `recovery`: a bounded reversal or recovery strategy and its verification.
@@ -55,6 +75,26 @@ heading, and do not add another file that restates the same contract.
55
75
  Resolve every blocking decision before planning. Keep non-blocking uncertainty visible
56
76
  in the task and verification contracts.
57
77
 
78
+ ## Why verification has to be executable
79
+
80
+ A criterion whose verification cannot be run is not acceptance, it is a wish. Three
81
+ consumers downstream depend on it being runnable:
82
+
83
+ - task development can treat passing acceptance as an exit condition only when
84
+ something can actually decide "passing";
85
+ - the review tests lens maps each acceptance claim to executable evidence, and can
86
+ only report an honest gap when there is something to map onto;
87
+ - delivery cannot otherwise separate "shipped" from "believed shipped".
88
+
89
+ Writing the command is also the cheapest way to find out that a criterion is vague.
90
+ "The feature is wired up" resists a command until it becomes "this export has a
91
+ non-test importer", which is a `grep`.
92
+
93
+ Where a criterion is genuinely a judgement call — visual polish, wording, feel — it
94
+ does not belong in Acceptance at all. Record it as a taste decision for the owner and
95
+ keep Acceptance to what a machine can settle. Mixing the two is what makes an
96
+ acceptance set undecidable.
97
+
58
98
  ## Profile
59
99
 
60
100
  - `quick`: one low-risk task, no material risk flags, and no research dependency.
@@ -67,9 +107,27 @@ in the task and verification contracts.
67
107
  Record a profile rationale and every applicable risk flag. Select the smallest profile
68
108
  whose guarantees cover the real blast radius; never use profile labels as estimates.
69
109
 
70
- ## Research disposition
110
+ ## Research Disposition
111
+
112
+ The `Disposition` value is exactly `none | bounded | required`. Use `none` with a
113
+ rationale when accepted evidence is sufficient. Use `bounded` for one already-framed
114
+ question whose smallest sufficient lenses are known; it skips Wayfinding. Use `required`
115
+ when several load-bearing claims or the evidence path itself remain unclear, so Research
116
+ may Wayfind before selecting lenses. Both name the exact question, selected or candidate
117
+ Research lenses, existing evidence, and required exit evidence.
118
+ The host model evaluates whether the named evidence exists and supports the claim; do
119
+ not add a `research_complete` bit, score, or workflow state. Planning waits while the
120
+ recorded exit evidence is unsatisfied.
121
+
122
+ ## Exact abandonment closure
71
123
 
72
- Use `none` with a rationale when accepted evidence is sufficient. Use `bounded` or
73
- `required` with a supported mode and the exact selected semantic steps when product,
74
- behavior, architecture, deployment, or risk facts remain load-bearing. The recorded
75
- selection is the planning gate.
124
+ Before moving an owner-abandoned Change, append this exact structure to `intent.md`:
125
+
126
+ ```markdown
127
+ ## Abandonment
128
+ - Date:
129
+ - Owner decision:
130
+ - Reason:
131
+ - Reusable evidence:
132
+ - Recovery or successor:
133
+ ```