ultra-builder-pro-cli 5.0.1 → 6.0.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 +103 -3
- package/README.md +67 -478
- package/README.zh-CN.md +49 -422
- package/adapters/_shared/frontmatter.cjs +1 -18
- package/adapters/_shared/plugin-core.cjs +17 -83
- package/adapters/_shared/runtime-assets.cjs +6 -120
- package/adapters/claude.js +8 -24
- package/adapters/codex.js +13 -32
- package/bin/install.js +35 -57
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +13 -101
- package/docs/examples/drive/claude-reviewer.sh +53 -0
- package/docs/examples/drive/claude-writer.sh +43 -0
- package/engine/FORMATS.md +1051 -0
- package/engine/checks.cjs +139 -0
- package/engine/cli.cjs +148 -0
- package/engine/completion.cjs +48 -0
- package/engine/context.cjs +94 -0
- package/engine/drive.cjs +220 -0
- package/engine/expect-worker.cjs +5 -0
- package/engine/expect.cjs +33 -0
- package/engine/formats.cjs +189 -0
- package/engine/git-state.cjs +112 -0
- package/engine/ledger.cjs +205 -0
- package/engine/loading.cjs +28 -0
- package/engine/merge.cjs +273 -0
- package/engine/milestone.cjs +89 -0
- package/engine/plan.cjs +232 -0
- package/engine/project.cjs +5 -0
- package/engine/signals.cjs +3 -0
- package/engine/states.cjs +175 -0
- package/hooks/session-start.cjs +54 -0
- package/package.json +17 -52
- package/skills/ultra/SKILL.md +395 -0
- package/.ultra-template/changes/abandoned/.gitkeep +0 -0
- package/.ultra-template/changes/active/.gitkeep +0 -1
- package/.ultra-template/changes/archive/.gitkeep +0 -1
- package/.ultra-template/contexts/TEMPLATE.md +0 -135
- package/.ultra-template/decisions/.gitkeep +0 -1
- package/.ultra-template/evidence/.gitkeep +0 -1
- package/.ultra-template/north-star.md +0 -52
- package/.ultra-template/project-brief.md +0 -28
- package/.ultra-template/research/.gitkeep +0 -1
- package/.ultra-template/specs/architecture.md +0 -81
- package/.ultra-template/specs/discovery.md +0 -74
- package/.ultra-template/specs/product.md +0 -80
- package/.ultra-template/specs/research-distillate.md +0 -57
- package/.ultra-template/tasks.json +0 -4
- package/.ultra-template/test-report.json +0 -44
- package/adapters/_shared/host-profile.cjs +0 -5
- package/adapters/grok.js +0 -242
- package/adapters/kimi.js +0 -232
- package/adapters/opencode.js +0 -408
- package/adapters/zcode.js +0 -375
- package/bin/delegate-worker.cjs +0 -5
- package/bin/delegate.cjs +0 -5
- package/docs/ARCHITECTURE.md +0 -274
- package/docs/ARTIFACT-AUTHORITY.md +0 -305
- package/docs/DECISIONS.md +0 -229
- package/docs/PHILOSOPHY.md +0 -261
- package/docs/RUNTIME-COMPAT-MATRIX.md +0 -316
- package/docs/SKILL-AUTHORING.md +0 -113
- package/docs/WORKFLOW-LIFECYCLE.md +0 -350
- package/docs/evals/adversarial-review-2026-08-14.md +0 -84
- package/docs/evals/ultra-5-0-journey-results.md +0 -62
- package/docs/evals/ultra-5-0-live-acp-conformance.md +0 -93
- package/docs/evals/ultra-5-0-route-journeys.md +0 -145
- package/docs/evals/zcode-automation-2026-08-14.md +0 -103
- package/hooks/README.md +0 -52
- package/hooks/_common.py +0 -1779
- package/hooks/adapters/codex.py +0 -295
- package/hooks/adapters/grok.py +0 -134
- package/hooks/adapters/kimi.py +0 -156
- package/hooks/adapters/zcode.py +0 -227
- package/hooks/block_dangerous_commands.py +0 -201
- package/hooks/compact_context.py +0 -224
- package/hooks/mid_workflow_recall.py +0 -47
- package/hooks/post_edit_guard.py +0 -215
- package/hooks/session_context.py +0 -741
- package/skills/ultra-change/SKILL.md +0 -165
- package/skills/ultra-change/references/change-contract.md +0 -199
- package/skills/ultra-change/references/execution-grant.md +0 -109
- package/skills/ultra-change/references/primary-transfer.md +0 -273
- package/skills/ultra-change/scripts/validate_primary_transfer.cjs +0 -1928
- package/skills/ultra-delegate/SKILL.md +0 -195
- package/skills/ultra-delegate/patches/zcode-acp-0.1.0-zcode-0.16.5.patch +0 -184
- package/skills/ultra-delegate/references/delegation-contract.md +0 -138
- package/skills/ultra-delegate/scripts/acp-conformance.cjs +0 -287
- package/skills/ultra-delegate/scripts/acp-driver.cjs +0 -762
- package/skills/ultra-delegate/scripts/agent-execution-driver.cjs +0 -320
- package/skills/ultra-delegate/scripts/cli-driver.cjs +0 -61
- package/skills/ultra-delegate/scripts/delegate-profiles/kimi-read-only.md +0 -15
- package/skills/ultra-delegate/scripts/delegate-profiles/kimi-write.md +0 -17
- package/skills/ultra-delegate/scripts/delegate-worker.cjs +0 -328
- package/skills/ultra-delegate/scripts/delegate.cjs +0 -690
- package/skills/ultra-delegate/scripts/delegate_wait.py +0 -86
- package/skills/ultra-delegate/scripts/host-profile.cjs +0 -267
- package/skills/ultra-deliver/SKILL.md +0 -194
- package/skills/ultra-deliver/references/baseline-reconciliation.md +0 -41
- package/skills/ultra-dev/SKILL.md +0 -265
- package/skills/ultra-dev/references/debugging.md +0 -22
- package/skills/ultra-domain-modeling/SKILL.md +0 -79
- package/skills/ultra-grilling/SKILL.md +0 -121
- package/skills/ultra-grilling/references/reframing.md +0 -76
- package/skills/ultra-init/SKILL.md +0 -143
- package/skills/ultra-init/references/brownfield-adoption.md +0 -32
- package/skills/ultra-init/scripts/init_project.cjs +0 -1031
- package/skills/ultra-plan/SKILL.md +0 -160
- package/skills/ultra-plan/references/task-evidence-v2.md +0 -300
- package/skills/ultra-plan/scripts/validate_task_evidence.cjs +0 -859
- package/skills/ultra-research/SKILL.md +0 -167
- package/skills/ultra-research/references/00-problem-validation.md +0 -36
- package/skills/ultra-research/references/01-opportunity-discovery.md +0 -33
- package/skills/ultra-research/references/02-market-assessment.md +0 -31
- package/skills/ultra-research/references/03-alternatives.md +0 -31
- package/skills/ultra-research/references/04-product-strategy.md +0 -41
- package/skills/ultra-research/references/05-assumptions-validation.md +0 -31
- package/skills/ultra-research/references/10-user-personas.md +0 -30
- package/skills/ultra-research/references/11-user-scenarios.md +0 -28
- package/skills/ultra-research/references/20-user-stories.md +0 -31
- package/skills/ultra-research/references/21-features-scope.md +0 -38
- package/skills/ultra-research/references/22-success-metrics.md +0 -33
- package/skills/ultra-research/references/30-architecture-context.md +0 -30
- package/skills/ultra-research/references/31-solution-strategy.md +0 -28
- package/skills/ultra-research/references/32-building-blocks.md +0 -28
- package/skills/ultra-research/references/40-deployment.md +0 -29
- package/skills/ultra-research/references/41-quality-risks.md +0 -28
- package/skills/ultra-research/references/99-synthesis.md +0 -59
- package/skills/ultra-research/references/north-star-v2.md +0 -168
- package/skills/ultra-research/references/wayfinding.md +0 -50
- package/skills/ultra-research/scripts/validate_north_star.cjs +0 -1174
- package/skills/ultra-review/SKILL.md +0 -275
- package/skills/ultra-review/references/adversarial-evaluation.md +0 -31
- package/skills/ultra-review/references/code.md +0 -17
- package/skills/ultra-review/references/comments.md +0 -12
- package/skills/ultra-review/references/design.md +0 -12
- package/skills/ultra-review/references/errors.md +0 -13
- package/skills/ultra-review/references/review-modes.md +0 -29
- package/skills/ultra-review/references/spec.md +0 -18
- package/skills/ultra-review/references/tests.md +0 -12
- package/skills/ultra-review/references/unified-schema.md +0 -284
- package/skills/ultra-review/references/worker-packet.md +0 -183
- package/skills/ultra-review/scripts/review_wait.py +0 -2943
- package/skills/ultra-status/SKILL.md +0 -209
- package/skills/ultra-status/references/route-contract.md +0 -100
- package/skills/ultra-tdd/SKILL.md +0 -91
- package/skills/ultra-tdd/references/templates/README.md +0 -20
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +0 -79
- package/skills/ultra-tdd/references/templates/persistence-real.ts +0 -78
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +0 -78
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +0 -73
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +0 -66
- package/skills/ultra-tdd/references/test-execution.md +0 -23
- package/skills/ultra-test/SKILL.md +0 -244
- package/skills/ultra-test/references/export-syntax.md +0 -18
- package/skills/ultra-test/scripts/validate_review_transport.cjs +0 -549
- package/skills/ultra-test/scripts/worktree_digest.cjs +0 -576
- package/skills/ultra-think/SKILL.md +0 -56
- package/skills/ultra-think/references/autonomy-boundary.md +0 -48
- package/skills/ultra-think/references/interaction-boundary.md +0 -42
|
@@ -1,576 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const crypto = require('node:crypto');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
const { execFileSync } = require('node:child_process');
|
|
8
|
-
|
|
9
|
-
const MAX_SNAPSHOT_BYTES = 8 * 1024 * 1024;
|
|
10
|
-
const SNAPSHOT_CHUNK_BYTES = 64 * 1024;
|
|
11
|
-
const MAX_OBSERVATION_FILES = 256;
|
|
12
|
-
const MAX_OBSERVATION_BYTES = 16 * 1024 * 1024;
|
|
13
|
-
const GIT_TIMEOUT_MS = 5000;
|
|
14
|
-
const GIT_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
15
|
-
const PRODUCT_PATHSPEC = [
|
|
16
|
-
'--', '.',
|
|
17
|
-
':(exclude).ultra/test-report.json',
|
|
18
|
-
':(exclude).ultra/evidence/**',
|
|
19
|
-
':(exclude).ultra/reviews/**',
|
|
20
|
-
':(exclude).ultra/.runtime/**',
|
|
21
|
-
':(exclude).ultra/progress/**',
|
|
22
|
-
':(exclude).ultra/changes/active/**',
|
|
23
|
-
':(exclude).ultra/changes/archive/**',
|
|
24
|
-
':(exclude).ultra/changes/abandoned/**',
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
function git(cwd, args, encoding = 'utf8') {
|
|
28
|
-
try {
|
|
29
|
-
return execFileSync('git', args, {
|
|
30
|
-
cwd,
|
|
31
|
-
encoding,
|
|
32
|
-
maxBuffer: GIT_MAX_OUTPUT_BYTES,
|
|
33
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
34
|
-
timeout: GIT_TIMEOUT_MS,
|
|
35
|
-
});
|
|
36
|
-
} catch (error) {
|
|
37
|
-
const operation = args.slice(0, 2).join(' ');
|
|
38
|
-
if (error && (error.code === 'ETIMEDOUT' || (error.killed && error.signal))) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
`ULTRA_SNAPSHOT_GIT_TIMEOUT: Git ${JSON.stringify(operation)} exceeded the `
|
|
41
|
-
+ `${GIT_TIMEOUT_MS}ms physical observation limit. Restore a responsive repository `
|
|
42
|
-
+ 'and Git process, then retry worktree_digest.',
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
if (error && error.code === 'ENOBUFS') {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`ULTRA_SNAPSHOT_GIT_OUTPUT_LIMIT: Git ${JSON.stringify(operation)} exceeded the `
|
|
48
|
-
+ `${GIT_MAX_OUTPUT_BYTES}-byte output limit. Reduce or split the physical product `
|
|
49
|
-
+ 'observation, then retry worktree_digest.',
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
const stderr = error && error.stderr
|
|
53
|
-
? Buffer.from(error.stderr).toString('utf8').trim().split('\n')[0]
|
|
54
|
-
: 'Git returned an unknown failure';
|
|
55
|
-
throw new Error(
|
|
56
|
-
`ULTRA_SNAPSHOT_GIT_FAILED: Git ${JSON.stringify(operation)} failed (${stderr}). `
|
|
57
|
-
+ 'Repair the repository and Git operation, then retry worktree_digest.',
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function optionsFromArgs(argv) {
|
|
63
|
-
const options = { project: process.cwd(), changeId: null };
|
|
64
|
-
for (let index = 0; index < argv.length; index += 2) {
|
|
65
|
-
const flag = argv[index];
|
|
66
|
-
const value = argv[index + 1];
|
|
67
|
-
if (!value) throw new Error('usage: worktree_digest.cjs [--project <repository-root>] [--change-id <id>]');
|
|
68
|
-
if (flag === '--project') options.project = path.resolve(value);
|
|
69
|
-
else if (flag === '--change-id') options.changeId = value;
|
|
70
|
-
else throw new Error('usage: worktree_digest.cjs [--project <repository-root>] [--change-id <id>]');
|
|
71
|
-
}
|
|
72
|
-
if (options.changeId && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(options.changeId)) {
|
|
73
|
-
throw new Error(`invalid Change id: ${options.changeId}`);
|
|
74
|
-
}
|
|
75
|
-
return options;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function snapshotError(code, relative, observation) {
|
|
79
|
-
return new Error(
|
|
80
|
-
`${code}: cannot snapshot ${JSON.stringify(relative)}: ${observation}. `
|
|
81
|
-
+ 'Restore the repository path as an ordinary regular non-symlink file within the '
|
|
82
|
-
+ '8 MiB limit, then retry worktree_digest.',
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function repositoryPath(root, relative) {
|
|
87
|
-
if (typeof relative !== 'string'
|
|
88
|
-
|| relative.length === 0
|
|
89
|
-
|| relative.includes('\0')
|
|
90
|
-
|| path.posix.isAbsolute(relative)
|
|
91
|
-
|| path.win32.isAbsolute(relative)) {
|
|
92
|
-
throw snapshotError('ULTRA_SNAPSHOT_PATH_ESCAPE', String(relative), 'path is not repository-relative');
|
|
93
|
-
}
|
|
94
|
-
const parts = relative.split('/');
|
|
95
|
-
if (parts.some((part) => part === '' || part === '.' || part === '..')
|
|
96
|
-
|| (path.sep === '\\' && parts.some((part) => part.includes('\\')))) {
|
|
97
|
-
throw snapshotError('ULTRA_SNAPSHOT_PATH_ESCAPE', relative, 'path escapes its repository root');
|
|
98
|
-
}
|
|
99
|
-
const absolute = path.join(root, ...parts);
|
|
100
|
-
const resolvedRelative = path.relative(root, absolute);
|
|
101
|
-
if (resolvedRelative === ''
|
|
102
|
-
|| resolvedRelative === '..'
|
|
103
|
-
|| resolvedRelative.startsWith(`..${path.sep}`)
|
|
104
|
-
|| path.isAbsolute(resolvedRelative)) {
|
|
105
|
-
throw snapshotError('ULTRA_SNAPSHOT_PATH_ESCAPE', relative, 'path escapes its repository root');
|
|
106
|
-
}
|
|
107
|
-
return { absolute, parts };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function lstatSnapshot(absolute, relative) {
|
|
111
|
-
try {
|
|
112
|
-
return fs.lstatSync(absolute, { bigint: true });
|
|
113
|
-
} catch (error) {
|
|
114
|
-
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
|
|
115
|
-
throw snapshotError('ULTRA_SNAPSHOT_MISSING', relative, 'path disappeared during observation');
|
|
116
|
-
}
|
|
117
|
-
throw snapshotError(
|
|
118
|
-
'ULTRA_SNAPSHOT_UNREADABLE',
|
|
119
|
-
relative,
|
|
120
|
-
`lstat failed with ${error && error.code ? error.code : 'an unknown error'}`,
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function sameNode(left, right) {
|
|
126
|
-
return left.dev === right.dev
|
|
127
|
-
&& left.ino === right.ino
|
|
128
|
-
&& left.mode === right.mode;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function sameFileVersion(left, right) {
|
|
132
|
-
return sameNode(left, right)
|
|
133
|
-
&& left.size === right.size
|
|
134
|
-
&& left.mtimeNs === right.mtimeNs
|
|
135
|
-
&& left.ctimeNs === right.ctimeNs;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function walkSnapshotPath(root, relative) {
|
|
139
|
-
const resolved = repositoryPath(root, relative);
|
|
140
|
-
const parents = [];
|
|
141
|
-
let current = root;
|
|
142
|
-
const rootStat = lstatSnapshot(root, '.');
|
|
143
|
-
if (rootStat.isSymbolicLink()) {
|
|
144
|
-
throw snapshotError('ULTRA_SNAPSHOT_SYMLINK', relative, 'repository root is a symlink');
|
|
145
|
-
}
|
|
146
|
-
if (!rootStat.isDirectory()) {
|
|
147
|
-
throw snapshotError('ULTRA_SNAPSHOT_NOT_DIRECTORY', relative, 'repository root is not a directory');
|
|
148
|
-
}
|
|
149
|
-
parents.push({ absolute: root, relative: '.', stat: rootStat });
|
|
150
|
-
|
|
151
|
-
for (const part of resolved.parts.slice(0, -1)) {
|
|
152
|
-
current = path.join(current, part);
|
|
153
|
-
const observedRelative = path.relative(root, current) || '.';
|
|
154
|
-
const stat = lstatSnapshot(current, observedRelative);
|
|
155
|
-
if (stat.isSymbolicLink()) {
|
|
156
|
-
throw snapshotError(
|
|
157
|
-
'ULTRA_SNAPSHOT_SYMLINK',
|
|
158
|
-
relative,
|
|
159
|
-
`parent ${JSON.stringify(observedRelative)} is a symlink`,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
if (!stat.isDirectory()) {
|
|
163
|
-
throw snapshotError(
|
|
164
|
-
'ULTRA_SNAPSHOT_NOT_DIRECTORY',
|
|
165
|
-
relative,
|
|
166
|
-
`parent ${JSON.stringify(observedRelative)} is not a directory`,
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
parents.push({ absolute: current, relative: observedRelative, stat });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const finalStat = lstatSnapshot(resolved.absolute, relative);
|
|
173
|
-
if (finalStat.isSymbolicLink()) {
|
|
174
|
-
throw snapshotError('ULTRA_SNAPSHOT_SYMLINK', relative, 'final entry is a symlink');
|
|
175
|
-
}
|
|
176
|
-
if (!finalStat.isFile()) {
|
|
177
|
-
throw snapshotError('ULTRA_SNAPSHOT_NOT_REGULAR', relative, 'final entry is not a regular file');
|
|
178
|
-
}
|
|
179
|
-
if (finalStat.size > BigInt(MAX_SNAPSHOT_BYTES)) {
|
|
180
|
-
throw snapshotError(
|
|
181
|
-
'ULTRA_SNAPSHOT_TOO_LARGE',
|
|
182
|
-
relative,
|
|
183
|
-
`regular file exceeds the ${MAX_SNAPSHOT_BYTES}-byte physical ceiling`,
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
return {
|
|
187
|
-
absolute: resolved.absolute,
|
|
188
|
-
final: { absolute: resolved.absolute, relative, stat: finalStat },
|
|
189
|
-
parents,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function verifyFreshRewalk(root, relative, initial, phase) {
|
|
194
|
-
let current;
|
|
195
|
-
try {
|
|
196
|
-
current = walkSnapshotPath(root, relative);
|
|
197
|
-
} catch (error) {
|
|
198
|
-
throw snapshotError(
|
|
199
|
-
'ULTRA_SNAPSHOT_REPLACED',
|
|
200
|
-
relative,
|
|
201
|
-
`${phase} rewalk no longer resolves the initial identities (${error.message})`,
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
const parentsStable = current.parents.length === initial.parents.length
|
|
205
|
-
&& current.parents.every((entry, index) => sameNode(entry.stat, initial.parents[index].stat));
|
|
206
|
-
if (!parentsStable || !sameFileVersion(current.final.stat, initial.final.stat)) {
|
|
207
|
-
throw snapshotError(
|
|
208
|
-
'ULTRA_SNAPSHOT_REPLACED',
|
|
209
|
-
relative,
|
|
210
|
-
`${phase} rewalk observed a replaced root, parent, or final identity`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function openSnapshotFile(absolute, relative) {
|
|
216
|
-
const { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } = fs.constants;
|
|
217
|
-
if (![O_NOFOLLOW, O_NONBLOCK, O_RDONLY].every(Number.isInteger)) {
|
|
218
|
-
throw snapshotError(
|
|
219
|
-
'ULTRA_SNAPSHOT_UNSUPPORTED',
|
|
220
|
-
relative,
|
|
221
|
-
'this host cannot enforce O_NONBLOCK and O_NOFOLLOW',
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
return fs.openSync(absolute, O_RDONLY | O_NONBLOCK | O_NOFOLLOW);
|
|
226
|
-
} catch (error) {
|
|
227
|
-
const observed = error && error.code ? error.code : 'an unknown error';
|
|
228
|
-
if (['ELOOP', 'ENOENT', 'ENOTDIR', 'ENXIO'].includes(observed)) {
|
|
229
|
-
throw snapshotError(
|
|
230
|
-
'ULTRA_SNAPSHOT_REPLACED',
|
|
231
|
-
relative,
|
|
232
|
-
`no-follow nonblocking open observed ${observed}`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
throw snapshotError(
|
|
236
|
-
'ULTRA_SNAPSHOT_UNREADABLE',
|
|
237
|
-
relative,
|
|
238
|
-
`no-follow nonblocking open failed with ${observed}`,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function streamStableRepositoryFile(root, relative, consume, budget = null) {
|
|
244
|
-
const initial = walkSnapshotPath(root, relative);
|
|
245
|
-
if (budget) budget.reserve(relative, initial.final.stat.size);
|
|
246
|
-
let descriptor;
|
|
247
|
-
try {
|
|
248
|
-
descriptor = openSnapshotFile(initial.absolute, relative);
|
|
249
|
-
const opened = fs.fstatSync(descriptor, { bigint: true });
|
|
250
|
-
if (!opened.isFile() || !sameFileVersion(opened, initial.final.stat)) {
|
|
251
|
-
throw snapshotError(
|
|
252
|
-
'ULTRA_SNAPSHOT_REPLACED',
|
|
253
|
-
relative,
|
|
254
|
-
'opened descriptor does not match the initial regular-file identity',
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
verifyFreshRewalk(root, relative, initial, 'pre-read');
|
|
258
|
-
|
|
259
|
-
const buffer = Buffer.allocUnsafe(SNAPSHOT_CHUNK_BYTES);
|
|
260
|
-
let offset = 0;
|
|
261
|
-
for (;;) {
|
|
262
|
-
const remaining = MAX_SNAPSHOT_BYTES - offset;
|
|
263
|
-
const requested = Math.min(buffer.length, remaining + 1);
|
|
264
|
-
let bytesRead;
|
|
265
|
-
try {
|
|
266
|
-
bytesRead = fs.readSync(descriptor, buffer, 0, requested, offset);
|
|
267
|
-
} catch (error) {
|
|
268
|
-
throw snapshotError(
|
|
269
|
-
'ULTRA_SNAPSHOT_UNREADABLE',
|
|
270
|
-
relative,
|
|
271
|
-
`stream read failed with ${error && error.code ? error.code : 'an unknown error'}`,
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
if (bytesRead === 0) break;
|
|
275
|
-
offset += bytesRead;
|
|
276
|
-
if (offset > MAX_SNAPSHOT_BYTES) {
|
|
277
|
-
throw snapshotError(
|
|
278
|
-
'ULTRA_SNAPSHOT_TOO_LARGE',
|
|
279
|
-
relative,
|
|
280
|
-
`stream exceeded the ${MAX_SNAPSHOT_BYTES}-byte physical ceiling`,
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
consume(buffer.subarray(0, bytesRead));
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const finished = fs.fstatSync(descriptor, { bigint: true });
|
|
287
|
-
if (!finished.isFile()
|
|
288
|
-
|| !sameFileVersion(finished, initial.final.stat)
|
|
289
|
-
|| BigInt(offset) !== finished.size) {
|
|
290
|
-
throw snapshotError(
|
|
291
|
-
'ULTRA_SNAPSHOT_REPLACED',
|
|
292
|
-
relative,
|
|
293
|
-
'file identity or byte extent changed during the streamed read',
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
verifyFreshRewalk(root, relative, initial, 'post-read');
|
|
297
|
-
return initial;
|
|
298
|
-
} finally {
|
|
299
|
-
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function resourceError(relative, nextFiles, nextBytes) {
|
|
304
|
-
return new Error(
|
|
305
|
-
`ULTRA_SNAPSHOT_RESOURCE_LIMIT: cannot snapshot ${JSON.stringify(relative)}: aggregate `
|
|
306
|
-
+ `product observation would reach ${nextFiles} files and ${nextBytes} bytes, above the `
|
|
307
|
-
+ `${MAX_OBSERVATION_FILES}-file or ${MAX_OBSERVATION_BYTES}-byte physical ceiling. `
|
|
308
|
-
+ 'Reduce or split the included physical product observation, then retry worktree_digest; '
|
|
309
|
-
+ 'this ceiling is not a semantic quality verdict.',
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function observationBudget() {
|
|
314
|
-
let files = 0;
|
|
315
|
-
let bytes = 0;
|
|
316
|
-
return {
|
|
317
|
-
reserve(relative, size) {
|
|
318
|
-
const nextFiles = files + 1;
|
|
319
|
-
const nextBytes = bytes + Number(size);
|
|
320
|
-
if (nextFiles > MAX_OBSERVATION_FILES || nextBytes > MAX_OBSERVATION_BYTES) {
|
|
321
|
-
throw resourceError(relative, nextFiles, nextBytes);
|
|
322
|
-
}
|
|
323
|
-
files = nextFiles;
|
|
324
|
-
bytes = nextBytes;
|
|
325
|
-
},
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function candidateExists(file) {
|
|
330
|
-
try {
|
|
331
|
-
fs.lstatSync(file);
|
|
332
|
-
return true;
|
|
333
|
-
} catch (error) {
|
|
334
|
-
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return false;
|
|
335
|
-
throw error;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function intentSnapshot(root, changeId) {
|
|
340
|
-
if (!changeId) return { change_id: null, intent_digest: null };
|
|
341
|
-
const matches = ['active', 'archive', 'abandoned']
|
|
342
|
-
.map((state) => path.join(root, '.ultra', 'changes', state, changeId, 'intent.md'))
|
|
343
|
-
.filter(candidateExists);
|
|
344
|
-
if (matches.length !== 1) {
|
|
345
|
-
throw new Error(`expected exactly one intent.md for Change ${changeId}; found ${matches.length}`);
|
|
346
|
-
}
|
|
347
|
-
const intentHash = crypto.createHash('sha256');
|
|
348
|
-
const relative = path.relative(root, matches[0]).split(path.sep).join('/');
|
|
349
|
-
const snapshot = streamStableRepositoryFile(root, relative, (chunk) => intentHash.update(chunk));
|
|
350
|
-
return {
|
|
351
|
-
change_id: changeId,
|
|
352
|
-
intent_digest: intentHash.digest('hex'),
|
|
353
|
-
relative,
|
|
354
|
-
identity: snapshotIdentity(snapshot),
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function snapshotIdentity(snapshot) {
|
|
359
|
-
const statIdentity = (stat) => [
|
|
360
|
-
stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeNs, stat.ctimeNs,
|
|
361
|
-
].map((value) => value.toString()).join(':');
|
|
362
|
-
return [
|
|
363
|
-
...snapshot.parents.map((entry) => statIdentity(entry.stat)),
|
|
364
|
-
statIdentity(snapshot.final.stat),
|
|
365
|
-
].join('|');
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function excluded(file) {
|
|
369
|
-
return file === '.ultra/test-report.json'
|
|
370
|
-
|| /^\.ultra\/evidence(?:\/|$)/.test(file)
|
|
371
|
-
|| /^\.ultra\/(?:reviews|\.runtime|progress)(?:\/|$)/.test(file)
|
|
372
|
-
|| /^\.ultra\/changes\/(?:active|archive|abandoned)(?:\/|$)/.test(file);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function changedDuringObservation(observation) {
|
|
376
|
-
return new Error(
|
|
377
|
-
`ULTRA_SNAPSHOT_CHANGED_DURING_OBSERVATION: ${observation}. `
|
|
378
|
-
+ 'Finish or restore the concurrent repository change, then retry worktree_digest once; '
|
|
379
|
-
+ 'no digest was accepted.',
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function splitNul(buffer, label) {
|
|
384
|
-
if (buffer.length === 0) return [];
|
|
385
|
-
if (buffer[buffer.length - 1] !== 0) {
|
|
386
|
-
throw new Error(
|
|
387
|
-
`ULTRA_SNAPSHOT_GIT_FAILED: ${label} was not NUL terminated. `
|
|
388
|
-
+ 'Repair the Git observation, then retry worktree_digest.',
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
const values = [];
|
|
392
|
-
let start = 0;
|
|
393
|
-
for (let index = 0; index < buffer.length; index += 1) {
|
|
394
|
-
if (buffer[index] !== 0) continue;
|
|
395
|
-
values.push(buffer.subarray(start, index));
|
|
396
|
-
start = index + 1;
|
|
397
|
-
}
|
|
398
|
-
return values;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function decodeGitPath(bytes, label) {
|
|
402
|
-
const relative = bytes.toString('utf8');
|
|
403
|
-
if (!Buffer.from(relative, 'utf8').equals(bytes)) {
|
|
404
|
-
throw snapshotError(
|
|
405
|
-
'ULTRA_SNAPSHOT_PATH_ENCODING',
|
|
406
|
-
label,
|
|
407
|
-
'Git returned a path that is not valid UTF-8',
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
return relative;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function trackedManifest(root, head) {
|
|
414
|
-
const raw = git(root, [
|
|
415
|
-
'diff', '--raw', '-z', '--no-renames', '--abbrev=64', head, ...PRODUCT_PATHSPEC,
|
|
416
|
-
], 'buffer');
|
|
417
|
-
const fields = splitNul(raw, 'tracked manifest');
|
|
418
|
-
if (fields.length % 2 !== 0) {
|
|
419
|
-
throw new Error(
|
|
420
|
-
'ULTRA_SNAPSHOT_GIT_FAILED: tracked manifest has an incomplete raw record. '
|
|
421
|
-
+ 'Repair the Git observation, then retry worktree_digest.',
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
const files = [];
|
|
425
|
-
for (let index = 0; index < fields.length; index += 2) {
|
|
426
|
-
const header = fields[index].toString('ascii');
|
|
427
|
-
const match = header.match(
|
|
428
|
-
/^:([0-7]{6}) ([0-7]{6}) ([0-9a-f]+) ([0-9a-f]+) ([A-Z])(?:[0-9]+)?$/u,
|
|
429
|
-
);
|
|
430
|
-
if (!match) {
|
|
431
|
-
throw new Error(
|
|
432
|
-
`ULTRA_SNAPSHOT_GIT_FAILED: tracked manifest record ${JSON.stringify(header)} is invalid. `
|
|
433
|
-
+ 'Repair the Git observation, then retry worktree_digest.',
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
const relative = decodeGitPath(fields[index + 1], 'tracked manifest path');
|
|
437
|
-
files.push({
|
|
438
|
-
path: relative,
|
|
439
|
-
status: match[5],
|
|
440
|
-
present: match[5] !== 'D',
|
|
441
|
-
old_mode: match[1],
|
|
442
|
-
new_mode: match[2],
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
return { raw, files };
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function untrackedManifest(root) {
|
|
449
|
-
const raw = git(root, ['ls-files', '--others', '--exclude-standard', '-z'], 'buffer');
|
|
450
|
-
const entries = splitNul(raw, 'untracked manifest')
|
|
451
|
-
.map((bytes) => ({ bytes, path: decodeGitPath(bytes, 'untracked manifest path') }))
|
|
452
|
-
.filter((entry) => !excluded(entry.path))
|
|
453
|
-
.sort((left, right) => Buffer.compare(left.bytes, right.bytes));
|
|
454
|
-
return {
|
|
455
|
-
files: entries.map((entry) => entry.path),
|
|
456
|
-
raw: Buffer.concat(entries.flatMap((entry) => [entry.bytes, Buffer.from([0])])),
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function exactDiff(root, head) {
|
|
461
|
-
return git(root, [
|
|
462
|
-
'diff', '--binary', '--no-ext-diff', head, ...PRODUCT_PATHSPEC,
|
|
463
|
-
], 'buffer');
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function verifyObservedPaths(root, records) {
|
|
467
|
-
for (const record of records) {
|
|
468
|
-
try {
|
|
469
|
-
verifyFreshRewalk(root, record.relative, record.snapshot, 'final');
|
|
470
|
-
} catch (error) {
|
|
471
|
-
throw changedDuringObservation(
|
|
472
|
-
`included path ${JSON.stringify(record.relative)} changed after its stable read (${error.message})`,
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function sameIntent(left, right) {
|
|
479
|
-
return left.change_id === right.change_id
|
|
480
|
-
&& left.intent_digest === right.intent_digest
|
|
481
|
-
&& left.relative === right.relative
|
|
482
|
-
&& left.identity === right.identity;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function verifyExpectedObservation(root, expected) {
|
|
486
|
-
const observedHead = git(root, ['rev-parse', 'HEAD']).trim();
|
|
487
|
-
const observedTracked = trackedManifest(root, expected.head);
|
|
488
|
-
const observedUntracked = untrackedManifest(root);
|
|
489
|
-
const observedDiff = exactDiff(root, expected.head);
|
|
490
|
-
if (observedHead !== expected.head
|
|
491
|
-
|| !observedTracked.raw.equals(expected.tracked.raw)
|
|
492
|
-
|| !observedUntracked.raw.equals(expected.untracked.raw)
|
|
493
|
-
|| !observedDiff.equals(expected.diff)) {
|
|
494
|
-
throw changedDuringObservation(
|
|
495
|
-
'HEAD, the exact tracked manifest or diff, or the untracked manifest changed during capture',
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const observedIntent = intentSnapshot(root, expected.intent.change_id);
|
|
500
|
-
if (!sameIntent(expected.intent, observedIntent)) {
|
|
501
|
-
throw changedDuringObservation('the Change intent identity or bytes changed during capture');
|
|
502
|
-
}
|
|
503
|
-
verifyObservedPaths(root, expected.records);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function verifyClosingObservation(root, expected) {
|
|
507
|
-
verifyExpectedObservation(root, expected);
|
|
508
|
-
verifyExpectedObservation(root, expected);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function main(options) {
|
|
512
|
-
const root = fs.realpathSync(git(options.project, ['rev-parse', '--show-toplevel']).trim());
|
|
513
|
-
const head = git(root, ['rev-parse', 'HEAD']).trim();
|
|
514
|
-
const tracked = trackedManifest(root, head);
|
|
515
|
-
const untracked = untrackedManifest(root);
|
|
516
|
-
const intent = intentSnapshot(root, options.changeId);
|
|
517
|
-
const budget = observationBudget();
|
|
518
|
-
const records = [];
|
|
519
|
-
|
|
520
|
-
for (const file of tracked.files) {
|
|
521
|
-
if (!file.present) {
|
|
522
|
-
budget.reserve(file.path, 0n);
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
const snapshot = streamStableRepositoryFile(root, file.path, () => {}, budget);
|
|
526
|
-
records.push({ relative: file.path, snapshot });
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const untrackedBytes = [];
|
|
530
|
-
for (const file of untracked.files) {
|
|
531
|
-
const chunks = [];
|
|
532
|
-
const snapshot = streamStableRepositoryFile(
|
|
533
|
-
root,
|
|
534
|
-
file,
|
|
535
|
-
(chunk) => chunks.push(Buffer.from(chunk)),
|
|
536
|
-
budget,
|
|
537
|
-
);
|
|
538
|
-
records.push({ relative: file, snapshot });
|
|
539
|
-
untrackedBytes.push(chunks);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const diff = exactDiff(root, head);
|
|
543
|
-
const closingObservation = { diff, head, intent, records, tracked, untracked };
|
|
544
|
-
verifyClosingObservation(root, closingObservation);
|
|
545
|
-
|
|
546
|
-
const hash = crypto.createHash('sha256');
|
|
547
|
-
hash.update('ultra-worktree-digest-v1\0').update(head).update('\0').update(diff);
|
|
548
|
-
for (let index = 0; index < untracked.files.length; index += 1) {
|
|
549
|
-
hash.update(untracked.files[index]).update('\0');
|
|
550
|
-
for (const chunk of untrackedBytes[index]) hash.update(chunk);
|
|
551
|
-
hash.update('\0');
|
|
552
|
-
}
|
|
553
|
-
return {
|
|
554
|
-
$schema: 'ultra-worktree-digest-v1',
|
|
555
|
-
change_id: intent.change_id,
|
|
556
|
-
intent_digest: intent.intent_digest,
|
|
557
|
-
head,
|
|
558
|
-
dirty: diff.length > 0 || untracked.files.length > 0,
|
|
559
|
-
diff_digest: hash.digest('hex'),
|
|
560
|
-
untracked_files: untracked.files,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
if (require.main === module) {
|
|
565
|
-
try {
|
|
566
|
-
process.stdout.write(`${JSON.stringify(main(optionsFromArgs(process.argv.slice(2))), null, 2)}\n`);
|
|
567
|
-
} catch (error) {
|
|
568
|
-
process.stderr.write(`${error.message}\n`);
|
|
569
|
-
process.exitCode = 1;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// The product-subject pathspec and the stable file-snapshot primitive are the
|
|
574
|
-
// single bounded mechanical definitions shared with the primary-transfer
|
|
575
|
-
// validator, so receipt reads and digest subject bytes can never drift.
|
|
576
|
-
module.exports = { PRODUCT_PATHSPEC, streamStableRepositoryFile };
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: ultra-think
|
|
3
|
-
description: Stress-test one consequential product, architecture, recovery, or scope decision and preserve the accepted result when it must outlive the session. Use when another skill has one evidence-backed trade-off whose answer could materially change accepted work.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Turn one consequential uncertainty into an evidence-backed decision
|
|
7
|
-
|
|
8
|
-
Thinking sharpens one consequential decision; it does not map a research frontier or
|
|
9
|
-
perform the implementation that follows.
|
|
10
|
-
Persist only decisions that are hard to reverse, look surprising without context, and
|
|
11
|
-
arose from a real trade-off. All three criteria must hold.
|
|
12
|
-
|
|
13
|
-
## Before you start
|
|
14
|
-
|
|
15
|
-
1. Resolve at most one active `change_id`, read `.ultra/tasks.json`, and if the decision
|
|
16
|
-
is task-scoped read the matching current task's `context_file` and `## Resume Note`.
|
|
17
|
-
2. Read `CONTEXT.md`, relevant `.ultra/decisions/`, the active Change and its evidence.
|
|
18
|
-
3. State the one decision and the assumptions capable of changing its answer.
|
|
19
|
-
|
|
20
|
-
## Definition of done
|
|
21
|
-
|
|
22
|
-
- Claims are labelled Fact, Inference or Speculation and cite their evidence.
|
|
23
|
-
- The leading answer survived all four adversarial checks below.
|
|
24
|
-
- The conclusion includes Confidence %, Key Assumptions and What would change my mind.
|
|
25
|
-
- A qualifying durable decision is written once; a local reversible judgment is not.
|
|
26
|
-
|
|
27
|
-
## Run the adversarial protocol
|
|
28
|
-
|
|
29
|
-
1. **Steel Man**: build the strongest case for the option you expect to reject.
|
|
30
|
-
2. **Pre-Mortem**: assume the choice failed six months later and name the three most
|
|
31
|
-
plausible causes.
|
|
32
|
-
3. **Sensitivity**: identify the assumption whose failure would reverse the answer.
|
|
33
|
-
4. **Second-Order**: describe the new constraints or failure modes created six to
|
|
34
|
-
twelve months later.
|
|
35
|
-
|
|
36
|
-
Give the owner one recommendation with the decisive evidence and what changes under
|
|
37
|
-
each viable answer. Reuse an answer already explicit in the request.
|
|
38
|
-
|
|
39
|
-
## Write durable decisions
|
|
40
|
-
|
|
41
|
-
Use `.ultra/decisions/<id>.md`: question, status, options and real trade-offs, accepted
|
|
42
|
-
decision, owner, date, evidence, consequences, What would change my mind, and
|
|
43
|
-
`supersedes` when applicable. An accepted file is append-only history: overturn it by
|
|
44
|
-
writing a new decision that supersedes it.
|
|
45
|
-
|
|
46
|
-
## When the owner decides
|
|
47
|
-
|
|
48
|
-
Only the owner accepts a durable decision or a REDUCTION. Reversible implementation
|
|
49
|
-
details stay with the model. If the host cannot ask, keep `status: open`, record the
|
|
50
|
-
missing answer and stop at that safe point.
|
|
51
|
-
|
|
52
|
-
## References
|
|
53
|
-
|
|
54
|
-
- `references/autonomy-boundary.md` — read before changing accepted authority.
|
|
55
|
-
- `references/interaction-boundary.md` — read when the host-facing decision needs one
|
|
56
|
-
carefully framed question.
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# The autonomy boundary and the specification-change verdict
|
|
2
|
-
|
|
3
|
-
Read this when an action is about to cross from "decide freely" into "surface to
|
|
4
|
-
the owner". It is the operational form of PHILOSOPHY C5, Bounded Autonomy; the
|
|
5
|
-
full constitution lives in the package's `docs/PHILOSOPHY.md`.
|
|
6
|
-
|
|
7
|
-
## Inside the boundary — decide and proceed
|
|
8
|
-
|
|
9
|
-
Libraries, naming, file layout, internal abstractions, decomposition strategy,
|
|
10
|
-
evidence interpretation, prioritization, final expression.
|
|
11
|
-
|
|
12
|
-
## Crossing the boundary — surface it
|
|
13
|
-
|
|
14
|
-
Weakening a test assertion, editing the specification to match the
|
|
15
|
-
implementation, shipping a feature flag that defaults off, replacing real
|
|
16
|
-
persistence with an in-memory stand-in, reducing scope, and any external or
|
|
17
|
-
irreversible effect.
|
|
18
|
-
|
|
19
|
-
## Classifying a specification change
|
|
20
|
-
|
|
21
|
-
When the implementation and the specification disagree, classify before touching
|
|
22
|
-
the specification — and classify by one question that can be checked against the
|
|
23
|
-
file, never by intent:
|
|
24
|
-
|
|
25
|
-
> **Does every commitment the specification already made still hold after this
|
|
26
|
-
> change?**
|
|
27
|
-
|
|
28
|
-
| Answer | Classification | Action |
|
|
29
|
-
|---|---|---|
|
|
30
|
-
| Yes, and it now commits to more | EXPANSION | write it, log it |
|
|
31
|
-
| Yes, the commitment is unchanged but stated correctly | CORRECTION | write it, log it |
|
|
32
|
-
| **No — at least one prior commitment no longer holds** | **REDUCTION** | **stop and ask the owner** |
|
|
33
|
-
|
|
34
|
-
The classification follows from the outcome, never from the reason. A
|
|
35
|
-
well-argued rationale does not convert a REDUCTION into a CORRECTION; the
|
|
36
|
-
rationale goes into the question put to the owner.
|
|
37
|
-
|
|
38
|
-
This matters because it is the exact mechanism by which specifications shrink
|
|
39
|
-
silently. "The specification asked for offline mode, but the architecture makes
|
|
40
|
-
it unreasonable, so the specification was wrong" reads as a CORRECTION and
|
|
41
|
-
removes offline mode without anyone deciding to. Under the question above it is
|
|
42
|
-
a REDUCTION, because a commitment stopped holding.
|
|
43
|
-
|
|
44
|
-
## Recording it
|
|
45
|
-
|
|
46
|
-
A boundary crossing produces either an owner question, an entry in the active task
|
|
47
|
-
context's `## Change Log`, or an accepted `.ultra/decisions/<id>.md` when the decision
|
|
48
|
-
outlives that task. Silence and a second drift ledger are not outcomes.
|