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.
- package/.ultra-template/changes/abandoned/.gitkeep +0 -0
- package/.ultra-template/contexts/TEMPLATE.md +7 -0
- package/.ultra-template/north-star.md +20 -31
- package/.ultra-template/project-brief.md +28 -0
- package/.ultra-template/specs/product.md +4 -0
- package/.ultra-template/specs/research-distillate.md +8 -0
- package/.ultra-template/test-report.json +1 -0
- package/CHANGELOG.md +131 -1
- package/README.md +61 -29
- package/bin/delegate-worker.cjs +5 -0
- package/bin/delegate.cjs +7 -0
- package/docs/ARCHITECTURE.md +36 -10
- package/docs/ARTIFACT-AUTHORITY.md +42 -13
- package/docs/DECISIONS.md +24 -5
- package/docs/PHILOSOPHY.md +45 -12
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +6 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +43 -1
- package/docs/SKILL-AUTHORING.md +10 -6
- package/docs/WORKFLOW-LIFECYCLE.md +103 -39
- package/hooks/README.md +12 -5
- package/hooks/_common.py +54 -5
- package/hooks/block_dangerous_commands.py +116 -10
- package/hooks/session_context.py +34 -8
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +59 -52
- package/skills/ultra-change/references/change-contract.md +65 -7
- package/skills/ultra-delegate/SKILL.md +21 -3
- package/skills/ultra-delegate/references/delegation-contract.md +10 -2
- package/skills/ultra-deliver/SKILL.md +63 -21
- package/skills/ultra-deliver/references/baseline-reconciliation.md +5 -3
- package/skills/ultra-dev/SKILL.md +33 -14
- package/skills/ultra-grilling/SKILL.md +63 -24
- package/skills/ultra-grilling/references/reframing.md +72 -0
- package/skills/ultra-init/SKILL.md +62 -79
- package/skills/ultra-init/references/brownfield-adoption.md +23 -40
- package/skills/ultra-plan/SKILL.md +48 -24
- package/skills/ultra-research/SKILL.md +91 -62
- package/skills/ultra-research/references/{03-competitive-landscape.md → 03-alternatives.md} +6 -6
- package/skills/ultra-research/references/22-success-metrics.md +8 -4
- package/skills/ultra-research/references/99-synthesis.md +8 -1
- package/skills/ultra-research/references/wayfinding.md +42 -0
- package/skills/ultra-review/SKILL.md +31 -3
- package/skills/ultra-status/SKILL.md +62 -32
- package/skills/ultra-tdd/SKILL.md +5 -2
- package/skills/ultra-test/SKILL.md +31 -12
- package/skills/ultra-test/scripts/worktree_digest.cjs +42 -8
- package/skills/ultra-think/SKILL.md +5 -10
- 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
|
|
9
|
-
| `mid_workflow_recall.py` | Before source reads or edits | Restate
|
|
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 |
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
29
|
-
return "protected branch
|
|
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
|
-
|
|
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(
|
|
174
|
+
note = advisory(effect, root)
|
|
71
175
|
if note:
|
|
72
176
|
emit_context("PreToolUse", note)
|
|
73
177
|
return 0
|
|
74
|
-
digest = hashlib.sha256(
|
|
75
|
-
if os.environ.get(
|
|
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"
|
|
80
|
-
f"
|
|
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",
|
package/hooks/session_context.py
CHANGED
|
@@ -1,22 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Inject the
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
if
|
|
19
|
-
|
|
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,55 +1,56 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ultra-change
|
|
3
|
-
description: Open one bounded unit of work by
|
|
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
|
|
9
|
-
|
|
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.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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 |
|
|
32
|
+
| The change makes no sentence in the specifications false | Outside the Ultra lifecycle — use ordinary repository TDD and verification |
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
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
|
|
44
|
-
|
|
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.
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
63
|
-
|
|
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
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
92
|
-
that shrinks a prior commitment. A change that makes no
|
|
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
|
|
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
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
```
|