@imix-js/taproot 0.3.0 → 0.5.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/README.md +30 -0
- package/dist/adapters/index.d.ts +11 -1
- package/dist/adapters/index.js +113 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/commithook.js +39 -2
- package/dist/commands/commithook.js.map +1 -1
- package/dist/commands/dod.d.ts +1 -0
- package/dist/commands/dod.js +24 -2
- package/dist/commands/dod.js.map +1 -1
- package/dist/commands/init.js +46 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/truth-sign.d.ts +5 -0
- package/dist/commands/truth-sign.js +58 -0
- package/dist/commands/truth-sign.js.map +1 -0
- package/dist/commands/update.js +22 -5
- package/dist/commands/update.js.map +1 -1
- package/dist/core/configuration.js +25 -0
- package/dist/core/configuration.js.map +1 -1
- package/dist/core/dod-runner.d.ts +4 -2
- package/dist/core/dod-runner.js +30 -1
- package/dist/core/dod-runner.js.map +1 -1
- package/dist/core/fs-walker.js +1 -1
- package/dist/core/fs-walker.js.map +1 -1
- package/dist/core/test-cache.d.ts +39 -0
- package/dist/core/test-cache.js +187 -0
- package/dist/core/test-cache.js.map +1 -0
- package/dist/core/truth-checker.d.ts +45 -0
- package/dist/core/truth-checker.js +152 -0
- package/dist/core/truth-checker.js.map +1 -0
- package/dist/validators/types.d.ts +4 -0
- package/docs/agents.md +11 -0
- package/docs/cli.md +18 -2
- package/docs/configuration.md +45 -1
- package/docs/patterns.md +75 -0
- package/docs/workflows.md +14 -0
- package/package.json +1 -1
- package/skills/backlog.md +7 -3
- package/skills/behaviour.md +7 -1
- package/skills/bug.md +13 -0
- package/skills/commit.md +27 -2
- package/skills/define-truth.md +96 -0
- package/skills/discover-truths.md +129 -0
- package/skills/grill-me.md +1 -0
- package/skills/guide.md +3 -0
- package/skills/implement.md +24 -1
- package/skills/intent.md +5 -0
- package/skills/refine.md +5 -0
- package/skills/review-all.md +30 -2
- package/skills/review.md +1 -0
- package/skills/status.md +3 -2
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, resolve, relative, dirname } from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { readResolutions } from './dod-runner.js';
|
|
5
|
+
const DEFAULT_TEST_TIMEOUT_MS = 300_000; // 300 seconds
|
|
6
|
+
const DEFAULT_MAX_AGE_MS = 60 * 60 * 1000; // 60 minutes
|
|
7
|
+
/** Derive the cache path from an impl.md path.
|
|
8
|
+
* Strips 'taproot/' prefix and '/impl.md' suffix, maps into .taproot/.test-results/.
|
|
9
|
+
* Handles both absolute and relative implPath inputs. */
|
|
10
|
+
export function deriveCachePath(implPath, cwd) {
|
|
11
|
+
const absImpl = resolve(cwd, implPath);
|
|
12
|
+
const relImpl = relative(cwd, absImpl).replace(/\\/g, '/');
|
|
13
|
+
// Strip leading 'taproot/' segment
|
|
14
|
+
const withoutRoot = relImpl.startsWith('taproot/')
|
|
15
|
+
? relImpl.slice('taproot/'.length)
|
|
16
|
+
: relImpl;
|
|
17
|
+
// Strip trailing '/impl.md'
|
|
18
|
+
const key = withoutRoot.endsWith('/impl.md')
|
|
19
|
+
? withoutRoot.slice(0, -'/impl.md'.length)
|
|
20
|
+
: withoutRoot;
|
|
21
|
+
return join(cwd, '.taproot', '.test-results', `${key}.json`);
|
|
22
|
+
}
|
|
23
|
+
/** Read a cached test result. Returns null if absent or malformed. */
|
|
24
|
+
export function readCache(cachePath) {
|
|
25
|
+
if (!existsSync(cachePath))
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
const raw = readFileSync(cachePath, 'utf-8');
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (typeof parsed.exitCode !== 'number' || !parsed.timestamp)
|
|
31
|
+
return null;
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Returns true if the cached result is malformed JSON (exists but unparseable). */
|
|
39
|
+
export function isCacheCorrupt(cachePath) {
|
|
40
|
+
if (!existsSync(cachePath))
|
|
41
|
+
return false;
|
|
42
|
+
try {
|
|
43
|
+
JSON.parse(readFileSync(cachePath, 'utf-8'));
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Check whether a cached result is stale.
|
|
51
|
+
* Stale if: any tracked source file is newer than the cache timestamp,
|
|
52
|
+
* or (if no source files listed) the cache is older than testResultMaxAge. */
|
|
53
|
+
export function isCacheStale(cache, implPath, cwd, testResultMaxAgeMs = DEFAULT_MAX_AGE_MS) {
|
|
54
|
+
const absImpl = resolve(cwd, implPath);
|
|
55
|
+
const cacheTs = Date.parse(cache.timestamp);
|
|
56
|
+
if (isNaN(cacheTs))
|
|
57
|
+
return true;
|
|
58
|
+
// Read source files from impl.md
|
|
59
|
+
const sourceFiles = readSourceFiles(absImpl, cwd);
|
|
60
|
+
if (sourceFiles.length === 0) {
|
|
61
|
+
// Fall back to max-age check
|
|
62
|
+
return Date.now() - cacheTs > testResultMaxAgeMs;
|
|
63
|
+
}
|
|
64
|
+
// Stale if any source file is newer than the cache
|
|
65
|
+
for (const src of sourceFiles) {
|
|
66
|
+
const absSrc = resolve(cwd, src);
|
|
67
|
+
if (!existsSync(absSrc))
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
const mtime = statSync(absSrc).mtimeMs;
|
|
71
|
+
if (mtime > cacheTs)
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// ignore stat errors
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function readSourceFiles(absImplPath, cwd) {
|
|
81
|
+
try {
|
|
82
|
+
const content = readFileSync(absImplPath, 'utf-8');
|
|
83
|
+
const match = content.match(/^## Source Files\s*\n([\s\S]*?)(?=\n## |\n$|$)/m);
|
|
84
|
+
if (!match)
|
|
85
|
+
return [];
|
|
86
|
+
const sources = [];
|
|
87
|
+
for (const m of match[1].matchAll(/`([^`]+)`/g)) {
|
|
88
|
+
sources.push(m[1]);
|
|
89
|
+
}
|
|
90
|
+
return sources;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Write a test result to the cache file, creating parent dirs as needed. */
|
|
97
|
+
export function writeCache(cachePath, result) {
|
|
98
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
99
|
+
writeFileSync(cachePath, JSON.stringify(result, null, 2), 'utf-8');
|
|
100
|
+
}
|
|
101
|
+
/** Execute testsCommand, streaming output to the terminal while also capturing it.
|
|
102
|
+
* Returns the TestResult with the last N lines as summary. */
|
|
103
|
+
export async function executeTestCommand(command, cwd, timeoutMs = DEFAULT_TEST_TIMEOUT_MS) {
|
|
104
|
+
return new Promise((resolvePromise) => {
|
|
105
|
+
const timestamp = new Date().toISOString();
|
|
106
|
+
const lines = [];
|
|
107
|
+
const child = spawn(command, { shell: true, cwd });
|
|
108
|
+
let timedOut = false;
|
|
109
|
+
const timer = setTimeout(() => {
|
|
110
|
+
timedOut = true;
|
|
111
|
+
child.kill();
|
|
112
|
+
}, timeoutMs);
|
|
113
|
+
function handleData(chunk, stream) {
|
|
114
|
+
const text = chunk.toString();
|
|
115
|
+
stream.write(text);
|
|
116
|
+
lines.push(...text.split('\n'));
|
|
117
|
+
}
|
|
118
|
+
child.stdout?.on('data', (chunk) => handleData(chunk, process.stdout));
|
|
119
|
+
child.stderr?.on('data', (chunk) => handleData(chunk, process.stderr));
|
|
120
|
+
child.on('close', (code) => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
const exitCode = timedOut ? -1 : (code ?? 1);
|
|
123
|
+
const timeoutSecs = Math.round(timeoutMs / 1000);
|
|
124
|
+
const summaryLines = timedOut
|
|
125
|
+
? [`timed out after ${timeoutSecs}s`]
|
|
126
|
+
: lines.filter(l => l.trim()).slice(-20);
|
|
127
|
+
resolvePromise({
|
|
128
|
+
timestamp,
|
|
129
|
+
command,
|
|
130
|
+
exitCode,
|
|
131
|
+
summary: summaryLines.join('\n'),
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
child.on('error', (err) => {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
resolvePromise({
|
|
137
|
+
timestamp,
|
|
138
|
+
command,
|
|
139
|
+
exitCode: 127,
|
|
140
|
+
summary: err.message,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** Full evidence-backed tests-passing check.
|
|
146
|
+
* Returns { passed, output, correction, warnNoCache } */
|
|
147
|
+
export async function runTestsPassingWithCache(options) {
|
|
148
|
+
const { implPath, cwd, testsCommand, rerunTests } = options;
|
|
149
|
+
const testResultMaxAgeMs = options.testResultMaxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
150
|
+
const testTimeoutMs = options.testTimeoutMs ?? DEFAULT_TEST_TIMEOUT_MS;
|
|
151
|
+
const cachePath = deriveCachePath(implPath, cwd);
|
|
152
|
+
// Warn and re-run if cache is corrupt
|
|
153
|
+
if (isCacheCorrupt(cachePath)) {
|
|
154
|
+
process.stderr.write(`Warning: Ignoring malformed cache at ${cachePath} — re-running tests.\n`);
|
|
155
|
+
}
|
|
156
|
+
let result = null;
|
|
157
|
+
if (!rerunTests) {
|
|
158
|
+
result = readCache(cachePath);
|
|
159
|
+
if (result && isCacheStale(result, implPath, cwd, testResultMaxAgeMs)) {
|
|
160
|
+
result = null; // stale — will re-execute
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Execute if no fresh cache
|
|
164
|
+
if (!result) {
|
|
165
|
+
result = await executeTestCommand(testsCommand, cwd, testTimeoutMs);
|
|
166
|
+
writeCache(cachePath, result);
|
|
167
|
+
}
|
|
168
|
+
if (result.exitCode === 0) {
|
|
169
|
+
return { passed: true, output: '', correction: '' };
|
|
170
|
+
}
|
|
171
|
+
if (result.exitCode === -1) {
|
|
172
|
+
const timeoutSecs = Math.round(testTimeoutMs / 1000);
|
|
173
|
+
return {
|
|
174
|
+
passed: false,
|
|
175
|
+
output: result.summary,
|
|
176
|
+
correction: `tests-passing blocked: test command timed out after ${timeoutSecs}s. Increase testTimeout in .taproot/settings.yaml if needed.`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
passed: false,
|
|
181
|
+
output: result.summary,
|
|
182
|
+
correction: 'tests-passing blocked: Fix failing tests and re-run taproot dod.',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// Re-export for use in commithook staleness check
|
|
186
|
+
export { readResolutions };
|
|
187
|
+
//# sourceMappingURL=test-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-cache.js","sourceRoot":"","sources":["../../src/core/test-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type TruthScope = 'intent' | 'behaviour' | 'impl';
|
|
2
|
+
export type DocLevel = 'intent' | 'behaviour' | 'impl';
|
|
3
|
+
export interface TruthFile {
|
|
4
|
+
relPath: string;
|
|
5
|
+
scope: TruthScope;
|
|
6
|
+
ambiguous: boolean;
|
|
7
|
+
unreadable: boolean;
|
|
8
|
+
content: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Determine scope from a path relative to the global-truths directory.
|
|
12
|
+
* Sub-folder takes precedence over suffix when they conflict (most restrictive wins).
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveTruthScope(relFromGT: string): {
|
|
15
|
+
scope: TruthScope;
|
|
16
|
+
ambiguous: boolean;
|
|
17
|
+
};
|
|
18
|
+
/** Does a truth at scope X apply to a document at level Y? */
|
|
19
|
+
export declare function scopeAppliesTo(truthScope: TruthScope, docLevel: DocLevel): boolean;
|
|
20
|
+
/** Returns the absolute path to global-truths/, or null if it doesn't exist. */
|
|
21
|
+
export declare function globalTruthsDir(cwd: string): string | null;
|
|
22
|
+
/** Get hierarchy level of a file based on its filename. */
|
|
23
|
+
export declare function docLevelFromFilename(filename: string): DocLevel | null;
|
|
24
|
+
/** Collect all truth files applicable to the given document level. Skips README.md. */
|
|
25
|
+
export declare function collectApplicableTruths(cwd: string, docLevel: DocLevel): TruthFile[];
|
|
26
|
+
/**
|
|
27
|
+
* Write a truth-check session marker after the agent has approved the truth check.
|
|
28
|
+
* Called by `taproot truth-sign`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function writeTruthSession(cwd: string, stagedDocs: Array<{
|
|
31
|
+
path: string;
|
|
32
|
+
content: string;
|
|
33
|
+
}>, truths: TruthFile[]): void;
|
|
34
|
+
export interface SessionValidation {
|
|
35
|
+
valid: boolean;
|
|
36
|
+
reason: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate that a truth-check session exists and matches the current staging state.
|
|
40
|
+
* Called by the pre-commit hook.
|
|
41
|
+
*/
|
|
42
|
+
export declare function validateTruthSession(cwd: string, stagedDocs: Array<{
|
|
43
|
+
path: string;
|
|
44
|
+
content: string;
|
|
45
|
+
}>, truths: TruthFile[]): SessionValidation;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join, dirname, basename, relative } from 'path';
|
|
4
|
+
const SCOPES = ['intent', 'behaviour', 'impl'];
|
|
5
|
+
/**
|
|
6
|
+
* Determine scope from a path relative to the global-truths directory.
|
|
7
|
+
* Sub-folder takes precedence over suffix when they conflict (most restrictive wins).
|
|
8
|
+
*/
|
|
9
|
+
export function resolveTruthScope(relFromGT) {
|
|
10
|
+
const normalised = relFromGT.replace(/\\/g, '/');
|
|
11
|
+
const parts = normalised.split('/');
|
|
12
|
+
const filename = parts[parts.length - 1];
|
|
13
|
+
const topFolder = parts.length > 1 ? parts[0] : null;
|
|
14
|
+
const suffixMatch = filename.match(/_(intent|behaviour|impl)\.md$/);
|
|
15
|
+
const suffixScope = suffixMatch?.[1];
|
|
16
|
+
const folderScope = topFolder && SCOPES.includes(topFolder)
|
|
17
|
+
? topFolder
|
|
18
|
+
: null;
|
|
19
|
+
// Conflicting signals: sub-folder wins (most restrictive)
|
|
20
|
+
if (folderScope && suffixScope && folderScope !== suffixScope) {
|
|
21
|
+
return { scope: folderScope, ambiguous: false };
|
|
22
|
+
}
|
|
23
|
+
if (folderScope)
|
|
24
|
+
return { scope: folderScope, ambiguous: false };
|
|
25
|
+
if (suffixScope)
|
|
26
|
+
return { scope: suffixScope, ambiguous: false };
|
|
27
|
+
// No scope signal — default to intent (broadest)
|
|
28
|
+
return { scope: 'intent', ambiguous: true };
|
|
29
|
+
}
|
|
30
|
+
/** Does a truth at scope X apply to a document at level Y? */
|
|
31
|
+
export function scopeAppliesTo(truthScope, docLevel) {
|
|
32
|
+
if (truthScope === 'intent')
|
|
33
|
+
return true;
|
|
34
|
+
if (truthScope === 'behaviour')
|
|
35
|
+
return docLevel === 'behaviour' || docLevel === 'impl';
|
|
36
|
+
return docLevel === 'impl'; // impl scope — impl only
|
|
37
|
+
}
|
|
38
|
+
const GLOBAL_TRUTHS_SUBDIR = 'taproot/global-truths';
|
|
39
|
+
/** Returns the absolute path to global-truths/, or null if it doesn't exist. */
|
|
40
|
+
export function globalTruthsDir(cwd) {
|
|
41
|
+
const dir = join(cwd, GLOBAL_TRUTHS_SUBDIR);
|
|
42
|
+
return existsSync(dir) ? dir : null;
|
|
43
|
+
}
|
|
44
|
+
/** Get hierarchy level of a file based on its filename. */
|
|
45
|
+
export function docLevelFromFilename(filename) {
|
|
46
|
+
const name = basename(filename);
|
|
47
|
+
if (name === 'intent.md')
|
|
48
|
+
return 'intent';
|
|
49
|
+
if (name === 'usecase.md')
|
|
50
|
+
return 'behaviour';
|
|
51
|
+
if (name === 'impl.md')
|
|
52
|
+
return 'impl';
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** Collect all truth files applicable to the given document level. Skips README.md. */
|
|
56
|
+
export function collectApplicableTruths(cwd, docLevel) {
|
|
57
|
+
const dir = globalTruthsDir(cwd);
|
|
58
|
+
if (!dir)
|
|
59
|
+
return [];
|
|
60
|
+
const results = [];
|
|
61
|
+
function walk(currentDir, relFromGT) {
|
|
62
|
+
let entries;
|
|
63
|
+
try {
|
|
64
|
+
entries = readdirSync(currentDir, { withFileTypes: true });
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const fullPath = join(currentDir, entry.name);
|
|
71
|
+
const childRel = relFromGT ? `${relFromGT}/${entry.name}` : entry.name;
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
walk(fullPath, childRel);
|
|
74
|
+
}
|
|
75
|
+
else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'README.md') {
|
|
76
|
+
const { scope, ambiguous } = resolveTruthScope(childRel);
|
|
77
|
+
if (!scopeAppliesTo(scope, docLevel))
|
|
78
|
+
continue;
|
|
79
|
+
let content = '';
|
|
80
|
+
let unreadable = false;
|
|
81
|
+
try {
|
|
82
|
+
content = readFileSync(fullPath, 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
unreadable = true;
|
|
86
|
+
}
|
|
87
|
+
results.push({
|
|
88
|
+
relPath: relative(cwd, fullPath).replace(/\\/g, '/'),
|
|
89
|
+
scope,
|
|
90
|
+
ambiguous,
|
|
91
|
+
unreadable,
|
|
92
|
+
content,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
walk(dir, '');
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
// ─── Session management ────────────────────────────────────────────────────────
|
|
101
|
+
const SESSION_PATH = '.taproot/.truth-check-session';
|
|
102
|
+
function computeHash(stagedDocs, truths) {
|
|
103
|
+
const h = createHash('sha256');
|
|
104
|
+
for (const d of [...stagedDocs].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
105
|
+
h.update(`doc:${d.path}\x00${d.content}\x00`);
|
|
106
|
+
}
|
|
107
|
+
for (const t of [...truths].sort((a, b) => a.relPath.localeCompare(b.relPath))) {
|
|
108
|
+
h.update(`truth:${t.relPath}\x00${t.content}\x00`);
|
|
109
|
+
}
|
|
110
|
+
return h.digest('hex');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Write a truth-check session marker after the agent has approved the truth check.
|
|
114
|
+
* Called by `taproot truth-sign`.
|
|
115
|
+
*/
|
|
116
|
+
export function writeTruthSession(cwd, stagedDocs, truths) {
|
|
117
|
+
const sessionFile = join(cwd, SESSION_PATH);
|
|
118
|
+
mkdirSync(dirname(sessionFile), { recursive: true });
|
|
119
|
+
const session = {
|
|
120
|
+
hash: computeHash(stagedDocs, truths),
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
};
|
|
123
|
+
writeFileSync(sessionFile, JSON.stringify(session, null, 2) + '\n');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Validate that a truth-check session exists and matches the current staging state.
|
|
127
|
+
* Called by the pre-commit hook.
|
|
128
|
+
*/
|
|
129
|
+
export function validateTruthSession(cwd, stagedDocs, truths) {
|
|
130
|
+
const sessionFile = join(cwd, SESSION_PATH);
|
|
131
|
+
if (!existsSync(sessionFile)) {
|
|
132
|
+
return {
|
|
133
|
+
valid: false,
|
|
134
|
+
reason: 'no truth-check session found — run /tr-commit to check truths before committing',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
let session;
|
|
138
|
+
try {
|
|
139
|
+
session = JSON.parse(readFileSync(sessionFile, 'utf-8'));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return { valid: false, reason: 'truth-check session file is malformed — re-run /tr-commit' };
|
|
143
|
+
}
|
|
144
|
+
if (session.hash !== computeHash(stagedDocs, truths)) {
|
|
145
|
+
return {
|
|
146
|
+
valid: false,
|
|
147
|
+
reason: 'staged files or truths have changed since the last truth check — re-run /tr-commit',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { valid: true, reason: '' };
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=truth-checker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"truth-checker.js","sourceRoot":"","sources":["../../src/core/truth-checker.ts"],"names":[],"mappings":"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"}
|
package/docs/agents.md
CHANGED
|
@@ -8,6 +8,7 @@ Taproot works with any AI assistant that can read files. Run `taproot init --age
|
|
|
8
8
|
|-------|-------------|---------|---------------------|
|
|
9
9
|
| Claude Code | **Tier 1** — fully supported | `taproot init --agent claude` | `.claude/commands/tr-*.md` — one file per slash command |
|
|
10
10
|
| Gemini CLI | **Tier 2** — implemented & tested | `taproot init --agent gemini` | `.gemini/commands/tr-*.toml` — one file per command |
|
|
11
|
+
| Aider | **Tier 2** — implemented & tested | `taproot init --agent aider` | `.aider.conf.yml` with `read:` entries + `CONVENTIONS.md` |
|
|
11
12
|
| Cursor | **Tier 3** — community supported | `taproot init --agent cursor` | `.cursor/rules/taproot.md` — project-wide rules |
|
|
12
13
|
| GitHub Copilot | **Tier 3** — community supported | `taproot init --agent copilot` | `.github/copilot-instructions.md` |
|
|
13
14
|
| Windsurf | **Tier 3** — community supported | `taproot init --agent windsurf` | `.windsurfrules` |
|
|
@@ -69,6 +70,16 @@ This refreshes all adapter files and skill definitions to the current version. R
|
|
|
69
70
|
|
|
70
71
|
---
|
|
71
72
|
|
|
73
|
+
## Aider
|
|
74
|
+
|
|
75
|
+
The Aider adapter installs `.aider.conf.yml` with `read:` entries so every Aider session automatically loads all taproot skill definitions and `CONVENTIONS.md` as context. No extra flags needed — run `aider` in the project directory and it picks up taproot context immediately.
|
|
76
|
+
|
|
77
|
+
Skills are invoked by natural language: *"implement the behaviour at taproot/my-intent/my-feature/"* rather than a slash command.
|
|
78
|
+
|
|
79
|
+
If `.aider.conf.yml` already exists, `taproot init` merges the `read:` entries without removing your existing settings (model, API key, etc.). If the file contains invalid YAML, the command stops with an error rather than overwriting it.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
72
83
|
## Non-Claude agents
|
|
73
84
|
|
|
74
85
|
For Cursor, Copilot, and Windsurf, the adapter installs a rules or instructions file. These give the agent context on the taproot hierarchy, the document formats, and the commit convention — enough to read and navigate the hierarchy, write conformant documents, and use the CLI commands correctly.
|
package/docs/cli.md
CHANGED
|
@@ -170,7 +170,7 @@ Surfaces unimplemented behaviours as work items, ordered by priority (intents wi
|
|
|
170
170
|
### `taproot dod`
|
|
171
171
|
|
|
172
172
|
```bash
|
|
173
|
-
taproot dod [impl-path] [--dry-run]
|
|
173
|
+
taproot dod [impl-path] [--dry-run] [--rerun-tests]
|
|
174
174
|
taproot dod <impl-path> --resolve <condition> --note "<text>" [--resolve <condition> --note "<text>" ...]
|
|
175
175
|
```
|
|
176
176
|
|
|
@@ -178,6 +178,8 @@ Runs all configured DoD conditions from `.taproot/settings.yaml` against the spe
|
|
|
178
178
|
|
|
179
179
|
Use `--resolve`/`--note` to record agent resolutions for agent-driven conditions (e.g. `document-current`, `check-if-affected`, `check-if-affected-by`). Multiple pairs can be supplied in a single invocation — conditions are paired with notes by position.
|
|
180
180
|
|
|
181
|
+
Use `--rerun-tests` to force re-execution of `testsCommand` regardless of any cached result. Requires `<impl-path>`.
|
|
182
|
+
|
|
181
183
|
See [Configuration](configuration.md) for how to define DoD conditions.
|
|
182
184
|
|
|
183
185
|
---
|
|
@@ -196,7 +198,7 @@ The hook uses a three-tier classification, where the implementation tier is dete
|
|
|
196
198
|
|
|
197
199
|
| Staged files | Gate applied |
|
|
198
200
|
|---|---|
|
|
199
|
-
| Only hierarchy files (`intent.md`, `usecase.md`) | `validate-structure` + `validate-format`
|
|
201
|
+
| Only hierarchy files (`intent.md`, `usecase.md`) | `validate-structure` + `validate-format` + truth consistency check (if `taproot/global-truths/` contains applicable truths) |
|
|
200
202
|
| Only `impl.md` (no source files in map) | Definition of Ready — the parent `usecase.md` must be in `specified` state and have `## Flow` and `## Related` sections |
|
|
201
203
|
| Source files found in map + `impl.md` staged | Verify only `## Status` (and `## DoD Resolutions`) changed in `impl.md`; then run DoD |
|
|
202
204
|
| Source files found in map but `impl.md` NOT staged | **Blocked** — "Stage `impl.md` alongside your source files. No implementation commit should proceed without its traceability record." |
|
|
@@ -204,6 +206,20 @@ The hook uses a three-tier classification, where the implementation tier is dete
|
|
|
204
206
|
|
|
205
207
|
The DoR gate prevents committing an implementation record before the behaviour is fully specified. The DoD gate prevents marking an implementation complete without passing the quality checks defined in `.taproot/settings.yaml`.
|
|
206
208
|
|
|
209
|
+
**Truth consistency check:** when hierarchy files are staged and `taproot/global-truths/` exists, the hook validates that a truth-check session marker (`.taproot/.truth-check-session`) is present and matches the current staged content. This marker is written by `taproot truth-sign`, which `/tr-commit` calls after the agent approves the truth check. Committing hierarchy files directly with `git commit` (bypassing `/tr-commit`) will be blocked if applicable truths exist.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### `taproot truth-sign`
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
taproot truth-sign
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Records a truth-check session marker after the agent has verified that staged hierarchy documents are consistent with applicable truths in `taproot/global-truths/`. Called automatically by the `/tr-commit` skill — you do not need to invoke this directly unless scripting a custom commit workflow.
|
|
220
|
+
|
|
221
|
+
The session marker is a SHA-256 hash of the staged document contents combined with all applicable truth file contents. If the staged files or truths change after signing, the marker is invalidated and the pre-commit hook will require re-signing.
|
|
222
|
+
|
|
207
223
|
---
|
|
208
224
|
|
|
209
225
|
## Commit Convention
|
package/docs/configuration.md
CHANGED
|
@@ -64,7 +64,7 @@ The `definitionOfDone` list controls what `taproot dod` checks and what the pre-
|
|
|
64
64
|
|
|
65
65
|
| Form | What it does |
|
|
66
66
|
|------|-------------|
|
|
67
|
-
| `tests-passing` | Built-in: runs `npm test
|
|
67
|
+
| `tests-passing` | Built-in: runs `npm test`. When `testsCommand` is configured in `settings.yaml`, uses evidence-backed execution with a cache file — see [State Transition Guardrails](#state-transition-guardrails). |
|
|
68
68
|
| `linter-clean` | Built-in: runs `npm run lint`. Passes if exit code is 0. |
|
|
69
69
|
| `commit-conventions` | Built-in: runs `npm run check:commits`. Passes if exit code is 0. |
|
|
70
70
|
| `document-current: <description>` | Agent-verified: the agent checks whether the described documentation is current and applies updates if needed. |
|
|
@@ -106,6 +106,50 @@ Taproot's own `.taproot/settings.yaml` ships with several `check-if-affected-by`
|
|
|
106
106
|
|
|
107
107
|
---
|
|
108
108
|
|
|
109
|
+
## Autonomous Execution
|
|
110
|
+
|
|
111
|
+
Setting `autonomous: true` in `settings.yaml` (or `TAPROOT_AUTONOMOUS=1` / `--autonomous` per invocation) puts all agent skills into non-interactive mode.
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
autonomous: true # all sessions in this repo run without confirmation prompts
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**What autonomous mode changes:**
|
|
118
|
+
- `/tr-implement` proceeds from plan to code without pausing for plan approval
|
|
119
|
+
- `/tr-commit` stages and commits without asking for confirmation when nothing is pre-staged
|
|
120
|
+
- DoD conditions are self-evaluated: resolvable conditions are recorded directly; unresolvable `check:` questions are marked `<!-- autonomous: pending-review -->` in `impl.md`
|
|
121
|
+
- Test failures or hook rejections are recorded in `impl.md` (impl marked `needs-rework`) and the agent stops — the developer returns to a clear failure report
|
|
122
|
+
|
|
123
|
+
**Three activation mechanisms (in order of scope):**
|
|
124
|
+
1. `autonomous: true` in `.taproot/settings.yaml` — repo-wide, all sessions
|
|
125
|
+
2. `TAPROOT_AUTONOMOUS=1` environment variable — per process invocation
|
|
126
|
+
3. `--autonomous` flag on a skill invocation (e.g. `/tr-implement path/ --autonomous`) — per skill
|
|
127
|
+
|
|
128
|
+
When none of these is set, confirmation prompts are shown as normal. Autonomous mode is never inferred from context.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## State Transition Guardrails
|
|
133
|
+
|
|
134
|
+
When `testsCommand` is set in `settings.yaml`, the `tests-passing` condition uses evidence-backed execution: it runs the command, caches the result in `.taproot/.test-results/`, and enforces freshness at commit time.
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
testsCommand: npm test # command to run for evidence-backed tests-passing
|
|
138
|
+
testResultMaxAge: 60 # minutes before a no-source-file cache is stale (default: 60)
|
|
139
|
+
testTimeout: 300 # seconds before testsCommand is killed (default: 300)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**How it works:**
|
|
143
|
+
- `taproot dod <impl-path>` runs `testsCommand`, streams output live, and writes `.taproot/.test-results/<intent>/<behaviour>/<impl>.json`
|
|
144
|
+
- On subsequent runs, the cached result is used if it is not stale (no tracked source files changed since the last run)
|
|
145
|
+
- `taproot dod <impl-path> --rerun-tests` forces re-execution regardless of cache
|
|
146
|
+
- `--resolve "tests-passing"` is rejected when `testsCommand` is configured — evidence is required, not agent assertion
|
|
147
|
+
- The pre-commit hook verifies a fresh passing result exists before allowing an implementation commit with a `complete` impl
|
|
148
|
+
|
|
149
|
+
**Add `.taproot/.test-results/` to `.gitignore`** — the cache is a local execution artifact, not a committed record.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
109
153
|
## Definition of Ready
|
|
110
154
|
|
|
111
155
|
The `definitionOfReady` list controls what the pre-commit hook checks when you make a declaration commit (committing `impl.md` without source files). All conditions must pass before the declaration commit is accepted.
|
package/docs/patterns.md
CHANGED
|
@@ -116,3 +116,78 @@ The agent reads the question text, reasons whether the answer is yes, no, or not
|
|
|
116
116
|
|---|---|
|
|
117
117
|
| `does this story introduce a cross-cutting concern that warrants a new check-if-affected-by or check-if-affected entry in .taproot/settings.yaml?` | Agent adds the entry to `.taproot/settings.yaml` |
|
|
118
118
|
| `does this story reveal a reusable pattern worth documenting in docs/patterns.md?` | Agent adds a pattern entry to `docs/patterns.md` |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Autonomous mode preamble (`## Autonomous mode` section in skills)
|
|
123
|
+
|
|
124
|
+
**Problem:** A skill has one or more confirmation prompts (plan approval, staging confirmation, DoD resolution prompts). You want the same skill to work in both interactive and non-interactive (CI, headless agent) contexts without maintaining two versions.
|
|
125
|
+
|
|
126
|
+
**Pattern:** Add an `## Autonomous mode` section at the top of the skill file (before `## Steps`) that tells the agent to check for autonomous mode activation and apply autonomous notes throughout the steps.
|
|
127
|
+
|
|
128
|
+
```markdown
|
|
129
|
+
## Autonomous mode
|
|
130
|
+
|
|
131
|
+
Before following any steps, check whether autonomous mode is active:
|
|
132
|
+
- `TAPROOT_AUTONOMOUS=1` is set in the environment, **or**
|
|
133
|
+
- `--autonomous` was passed as an argument to this skill invocation, **or**
|
|
134
|
+
- `.taproot/settings.yaml` contains `autonomous: true`
|
|
135
|
+
|
|
136
|
+
If any of these is true, **autonomous mode is active** — apply the autonomous notes at each step where they appear. If none is true, autonomous mode is **inactive** — show confirmation prompts as normal.
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Then at each confirmation step, add a conditional note:
|
|
140
|
+
|
|
141
|
+
```markdown
|
|
142
|
+
**Interactive mode:** ask "Should I proceed?" and wait for confirmation.
|
|
143
|
+
|
|
144
|
+
**Autonomous mode:** proceed directly without waiting for confirmation.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**When to use it:**
|
|
148
|
+
- The skill has at least one confirmation prompt that would block unattended execution
|
|
149
|
+
- You want CI agents, headless runners, or `TAPROOT_AUTONOMOUS=1` users to be able to run the skill without interaction
|
|
150
|
+
- The skill already exists and works correctly in interactive mode — this is an additive change
|
|
151
|
+
|
|
152
|
+
**When NOT to use it:**
|
|
153
|
+
- The confirmation prompt exists to prevent destructive irreversible actions (e.g. deleting data, force-pushing) — in these cases, autonomous bypass is unsafe
|
|
154
|
+
- The skill is already fully automated (no prompts) — the preamble adds noise without value
|
|
155
|
+
|
|
156
|
+
**Taproot's built-in uses:**
|
|
157
|
+
|
|
158
|
+
| Skill | Prompt bypassed in autonomous mode |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `skills/implement.md` | Plan approval in step 4 |
|
|
161
|
+
| `skills/commit.md` | Staging confirmation in step 3 |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Agent-verified pre-commit check (`taproot truth-sign`)
|
|
166
|
+
|
|
167
|
+
**Problem:** A pre-commit hook needs to enforce a quality rule that requires semantic reasoning — something a shell command cannot evaluate alone (e.g. "does this spec contradict any active global truths?"). Running an LLM inside a hook is too slow and requires credentials.
|
|
168
|
+
|
|
169
|
+
**Pattern:** Separate the reasoning from the enforcement:
|
|
170
|
+
1. The skill performs the semantic check (via the agent) before `git commit` is called
|
|
171
|
+
2. If the check passes, the skill runs `taproot truth-sign` to write a session marker (`.taproot/.truth-check-session`) containing a SHA-256 hash of the checked content
|
|
172
|
+
3. The hook validates the marker exists and matches the current staged state — a fast, deterministic check
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# In the skill (after agent approves):
|
|
176
|
+
taproot truth-sign
|
|
177
|
+
|
|
178
|
+
# In the hook (synchronous, no LLM needed):
|
|
179
|
+
# validateTruthSession(cwd, stagedDocs, truths)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**When to use it:**
|
|
183
|
+
- The quality rule requires reasoning the CLI cannot perform alone
|
|
184
|
+
- The agent is always in the loop (via a skill) before the commit reaches the hook
|
|
185
|
+
- Content-hash binding is sufficient — the hook does not need to know *why* the check passed, only that it did for this exact content
|
|
186
|
+
|
|
187
|
+
**Limitation:** If the developer bypasses the skill and runs `git commit` directly, the hook blocks. This is intentional — the pattern enforces use of the skill as the authoritative commit path.
|
|
188
|
+
|
|
189
|
+
**Taproot's built-in uses:**
|
|
190
|
+
|
|
191
|
+
| Hook check | Written by | Validates |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| Truth consistency | `taproot truth-sign` (called by `/tr-commit`) | Staged hierarchy docs are consistent with `taproot/global-truths/` |
|
package/docs/workflows.md
CHANGED
|
@@ -136,6 +136,20 @@ Use `/tr-review` on a fresh spec before starting implementation. Use `/tr-review
|
|
|
136
136
|
|
|
137
137
|
---
|
|
138
138
|
|
|
139
|
+
## Surfacing implicit project truths
|
|
140
|
+
|
|
141
|
+
After building up several specs, some domain terms and business rules will recur across specs without being formally captured. Use `/tr-discover-truths` to surface them:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
/tr-discover-truths
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The skill scans all `intent.md` and `usecase.md` files, identifies recurring terms, business rules, and conventions not yet defined in `taproot/global-truths/`, and presents them as candidates. For each candidate you choose: **promote** (routes to `/tr-ineed` → define-truth), **backlog** (saves to `.taproot/backlog.md` for later), **skip** (reappears next run), or **dismiss** (permanently suppressed).
|
|
148
|
+
|
|
149
|
+
Truth discovery also runs as a final pass inside `/tr-review-all`, appending a `## Truth Candidates` section to the review report.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
139
153
|
## When you need to think something through
|
|
140
154
|
|
|
141
155
|
```
|