@su-record/vibe 4.1.25 → 4.1.26
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +45 -13
- package/card.md +9 -9
- package/dist/cli/common.js +4 -2
- package/dist/cli/common.js.map +1 -1
- package/dist/cli/human.js +2 -2
- package/dist/cli/human.js.map +1 -1
- package/dist/cli/session.js +16 -0
- package/dist/cli/session.js.map +1 -0
- package/dist/cli/work.js +60 -26
- package/dist/cli/work.js.map +1 -1
- package/dist/cli.js +9 -4
- package/dist/cli.js.map +1 -1
- package/dist/core/check-process.js +51 -0
- package/dist/core/check-process.js.map +1 -0
- package/dist/core/check.js +94 -49
- package/dist/core/check.js.map +1 -1
- package/dist/core/checks/eval.js +10 -19
- package/dist/core/checks/eval.js.map +1 -1
- package/dist/core/checks/file.js +4 -0
- package/dist/core/checks/file.js.map +1 -1
- package/dist/core/checks/http.js +22 -3
- package/dist/core/checks/http.js.map +1 -1
- package/dist/core/checks/review.js +13 -9
- package/dist/core/checks/review.js.map +1 -1
- package/dist/core/checks/run.js +7 -29
- package/dist/core/checks/run.js.map +1 -1
- package/dist/core/config.js +5 -3
- package/dist/core/config.js.map +1 -1
- package/dist/core/consent.js +33 -0
- package/dist/core/consent.js.map +1 -0
- package/dist/core/context.js +11 -2
- package/dist/core/context.js.map +1 -1
- package/dist/core/evidence.js +35 -0
- package/dist/core/evidence.js.map +1 -0
- package/dist/core/failure-message.js +23 -0
- package/dist/core/failure-message.js.map +1 -0
- package/dist/core/failure.js +54 -0
- package/dist/core/failure.js.map +1 -0
- package/dist/core/handoff-results.js +44 -0
- package/dist/core/handoff-results.js.map +1 -0
- package/dist/core/handoff.js +65 -0
- package/dist/core/handoff.js.map +1 -0
- package/dist/core/inspect.js +136 -0
- package/dist/core/inspect.js.map +1 -0
- package/dist/core/intent.js +33 -9
- package/dist/core/intent.js.map +1 -1
- package/dist/core/ledger.js.map +1 -1
- package/dist/core/output-capture.js +31 -0
- package/dist/core/output-capture.js.map +1 -0
- package/dist/core/private-store.js +17 -0
- package/dist/core/private-store.js.map +1 -0
- package/dist/core/read-guard.js +6 -0
- package/dist/core/read-guard.js.map +1 -0
- package/dist/core/readerSession.js +8 -27
- package/dist/core/readerSession.js.map +1 -1
- package/dist/core/regress.js +1 -1
- package/dist/core/regress.js.map +1 -1
- package/dist/core/repair.js +39 -0
- package/dist/core/repair.js.map +1 -0
- package/dist/core/run-id.js +29 -0
- package/dist/core/run-id.js.map +1 -0
- package/dist/core/scenarios.js +11 -0
- package/dist/core/scenarios.js.map +1 -1
- package/dist/core/session.js +29 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/source-basis.js +75 -0
- package/dist/core/source-basis.js.map +1 -0
- package/dist/core/state.js.map +1 -1
- package/dist/core/view.js +36 -15
- package/dist/core/view.js.map +1 -1
- package/dist/install/hooks.js +2 -2
- package/dist/install/hooks.js.map +1 -1
- package/dist/install/plugin.js +13 -7
- package/dist/install/plugin.js.map +1 -1
- package/dist/install/tree.js +2 -2
- package/dist/install/tree.js.map +1 -1
- package/hooks/codex-hooks.json +2 -2
- package/hooks/hooks.json +2 -2
- package/hooks/notify.js +29 -71
- package/hooks/private-store.cjs +59 -0
- package/hooks/session-context.cjs +59 -0
- package/hooks/session-files.cjs +81 -0
- package/hooks/session-repair.cjs +24 -0
- package/hooks/session-state.cjs +166 -0
- package/hooks/session.js +17 -66
- package/hooks/slice-command.cjs +121 -0
- package/hooks/slice-guard.cjs +41 -0
- package/hooks/slice-store.cjs +72 -0
- package/package.json +1 -1
- package/skills/vibe/SKILL.md +4 -3
- package/skills/vibe-build/SKILL.md +3 -2
- package/skills/vibe-discover/SKILL.md +12 -11
- package/skills/vibe-discover/work-opportunities.md +13 -0
- package/skills/vibe-handoff/SKILL.md +1 -0
- package/skills/vibe-prove/SKILL.md +7 -11
- package/skills/vibe-scope/SKILL.md +6 -4
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { readPrivate, writePrivate } = require('./private-store.cjs');
|
|
4
|
+
const { optional, json, digest, revision, projectFingerprint } = require('./session-files.cjs');
|
|
5
|
+
const { repairData, repairMessage } = require('./session-repair.cjs');
|
|
6
|
+
const { contextStatus } = require('./session-context.cjs');
|
|
7
|
+
|
|
8
|
+
const ID = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
9
|
+
const HASH = /^[a-f0-9]{16,64}$/;
|
|
10
|
+
const STATUSES = ['pass', 'fail', 'pending', 'blocked', 'stale', 'handoff'];
|
|
11
|
+
const name = (kind, value) => `${kind}-${digest(value)}.json`;
|
|
12
|
+
function identity(payload = {}, env = process.env) {
|
|
13
|
+
const ids = [payload.session_id, env.CODEX_THREAD_ID, env.CLAUDE_SESSION_ID].filter(Boolean);
|
|
14
|
+
if (ids.some(id => typeof id !== 'string' || !/^[a-zA-Z0-9._:-]{1,256}$/.test(id))) throw new Error('session-identity-invalid');
|
|
15
|
+
if (new Set(ids).size > 1) throw new Error('session-identity-conflict');
|
|
16
|
+
if (!ids.length) throw new Error('session-identity-missing');
|
|
17
|
+
return ids[0];
|
|
18
|
+
}
|
|
19
|
+
function hostRoot(payload = {}, env = process.env, cwd = process.cwd()) {
|
|
20
|
+
const roots = [cwd, payload.cwd, env.CLAUDE_PROJECT_DIR].filter(Boolean);
|
|
21
|
+
if (roots.some(root => typeof root !== 'string' || !path.isAbsolute(root))) throw new Error('session-root-invalid');
|
|
22
|
+
const canonical = roots.map(root => fs.realpathSync(root));
|
|
23
|
+
if (new Set(canonical).size !== 1) throw new Error('session-root-conflict');
|
|
24
|
+
return canonical[0];
|
|
25
|
+
}
|
|
26
|
+
function bindSession(root, id, scenarios = []) {
|
|
27
|
+
root = fs.realpathSync(root);
|
|
28
|
+
identity({ session_id: id });
|
|
29
|
+
if (scenarios.length > 1000 || scenarios.some(s => !ID.test(s.id) || (s.needs ?? []).some(id => !ID.test(id)))) throw new Error('invalid session scenarios');
|
|
30
|
+
const guard = readPrivate(root, name('guard', id), 4096);
|
|
31
|
+
if (guard !== null) {
|
|
32
|
+
const existing = JSON.parse(guard);
|
|
33
|
+
if (!Number.isInteger(existing.count) || existing.count < 0 || existing.count > 3 || (existing.key !== null && !/^[a-f0-9]{64}$/.test(existing.key))) throw new Error('invalid session guard');
|
|
34
|
+
}
|
|
35
|
+
const binding = { schemaVersion: 1, root, id, revision: revision(root), scenarios: scenarios.map(s => ({ id: s.id, needs: s.needs ?? [] })) };
|
|
36
|
+
writePrivate(root, name('session', id), JSON.stringify(binding));
|
|
37
|
+
if (guard === null) writePrivate(root, name('guard', id), JSON.stringify({ key: null, count: 0 }));
|
|
38
|
+
return { status: 'bound', ...binding };
|
|
39
|
+
}
|
|
40
|
+
function workflow(root) {
|
|
41
|
+
const questions = new Map(), answered = new Set(), handed = new Set(), changes = [];
|
|
42
|
+
const inbox = optional(path.join(root, '.vibe/inbox.jsonl')) ?? '';
|
|
43
|
+
for (const line of inbox.split('\n').filter(Boolean)) {
|
|
44
|
+
const e = JSON.parse(line);
|
|
45
|
+
if (typeof e.id !== 'string' || !/^[a-zA-Z0-9-]{1,80}$/.test(e.id)) continue;
|
|
46
|
+
if (e.type === 'question') questions.set(e.id, false);
|
|
47
|
+
if (e.type === 'answer' && typeof e.answer === 'string' && e.answer.trim()) answered.add(e.id);
|
|
48
|
+
if (e.type === 'answer' || e.type === 'resolve') questions.delete(e.id);
|
|
49
|
+
}
|
|
50
|
+
const state = json(path.join(root, '.vibe/state.json'));
|
|
51
|
+
const repairQuestion = state.repair?.questionId;
|
|
52
|
+
if (state.repair?.waiting && typeof repairQuestion === 'string' && /^[a-zA-Z0-9-]{1,80}$/.test(repairQuestion) && !answered.has(repairQuestion)) questions.set(repairQuestion, false);
|
|
53
|
+
const ledger = optional(path.join(root, '.vibe/ledger.jsonl'), 2097152) ?? '';
|
|
54
|
+
for (const line of ledger.split('\n').filter(Boolean)) {
|
|
55
|
+
const e = JSON.parse(line), item = e.handoff;
|
|
56
|
+
if (!item || item.intentHash !== state.intentHash || !ID.test(item.scenario)) continue;
|
|
57
|
+
if (e.event === 'handoff') handed.add(item.scenario);
|
|
58
|
+
if (e.event === 'reopen') handed.delete(item.scenario);
|
|
59
|
+
if (e.event === 'handoff' || e.event === 'reopen') changes.push([e.event, item.scenario]);
|
|
60
|
+
}
|
|
61
|
+
return { intentHash: state.intentHash, approvalWaiting: state.state === 'DRAFT', abandoned: state.state === 'ABANDONED', waiting: [...questions.keys()].sort(), handed: [...handed].sort(), changes: digest(JSON.stringify(changes)) };
|
|
62
|
+
}
|
|
63
|
+
function validEvidence(input) {
|
|
64
|
+
return input && /^r-\d+$/.test(input.run) && HASH.test(input.intentHash) && typeof input.done === 'boolean' &&
|
|
65
|
+
Array.isArray(input.scenarios) && input.scenarios.length <= 1000 &&
|
|
66
|
+
input.scenarios.every(s => s && ID.test(s.id) && STATUSES.includes(s.status)) &&
|
|
67
|
+
new Set(input.scenarios.map(s => s.id)).size === input.scenarios.length &&
|
|
68
|
+
(!input.done || input.scenarios.every(s => s.status === 'pass'));
|
|
69
|
+
}
|
|
70
|
+
function stopSnapshot(root, plan) {
|
|
71
|
+
let currentRevision = 'unavailable';
|
|
72
|
+
try {
|
|
73
|
+
currentRevision = revision(root);
|
|
74
|
+
const context = contextStatus(root, plan);
|
|
75
|
+
if (context !== 'fresh') return { snapshotStatus: context, failureCode: 'stop-context-unavailable', revision: currentRevision, files: null, workflow: null };
|
|
76
|
+
return { snapshotStatus: 'complete', revision: currentRevision, files: projectFingerprint(root), workflow: workflow(root).changes };
|
|
77
|
+
} catch {
|
|
78
|
+
return { snapshotStatus: 'unavailable', failureCode: 'stop-snapshot-unavailable', revision: currentRevision, files: null, workflow: null };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function recordStopEvidence(root, input) {
|
|
82
|
+
root = fs.realpathSync(root);
|
|
83
|
+
if (!validEvidence(input)) throw new Error('invalid Stop evidence');
|
|
84
|
+
const proof = { schemaVersion: 2, root, run: input.run, intentHash: input.intentHash, done: input.done,
|
|
85
|
+
scenarios: input.scenarios, repair: input.repair ?? null, failures: input.failures ?? [], executionPlan: input.executionPlan, ...stopSnapshot(root, input.executionPlan) };
|
|
86
|
+
writePrivate(root, name('proof', root), JSON.stringify(proof));
|
|
87
|
+
return proof;
|
|
88
|
+
}
|
|
89
|
+
function unavailableStatus(root, binding, proof) {
|
|
90
|
+
let currentRevision = 'unavailable';
|
|
91
|
+
let waiting = [];
|
|
92
|
+
try { currentRevision = revision(root); } catch { /* Only fixed diagnostic text leaves the hook. */ }
|
|
93
|
+
try { waiting = workflow(root).waiting; } catch { /* An unavailable inbox cannot supply question ids. */ }
|
|
94
|
+
return { root, revision: currentRevision, intent: proof.intentHash, complete: false, fresh: false, snapshotStatus: 'unavailable',
|
|
95
|
+
remaining: proof.scenarios.map(s => s.id), repair: repairData(proof.repair), failures: proof.failures ?? [], waiting, handed: [], graph: binding.scenarios };
|
|
96
|
+
}
|
|
97
|
+
function structuralStatus(root, binding) {
|
|
98
|
+
const raw = readPrivate(root, name('proof', root));
|
|
99
|
+
const proof = raw === null ? null : JSON.parse(raw);
|
|
100
|
+
if (proof && (proof.schemaVersion !== 2 || proof.root !== root || !validEvidence(proof))) throw new Error('invalid private proof');
|
|
101
|
+
if (proof && proof.snapshotStatus !== 'complete') return unavailableStatus(root, binding, proof);
|
|
102
|
+
if (proof && contextStatus(root, proof.executionPlan) !== 'fresh') return unavailableStatus(root, binding, proof);
|
|
103
|
+
const flow = workflow(root);
|
|
104
|
+
const currentRevision = revision(root);
|
|
105
|
+
const scenarios = proof?.scenarios ?? binding.scenarios;
|
|
106
|
+
if (scenarios.length > 1000 || scenarios.some(s => !ID.test(s.id))) throw new Error('invalid private scenarios');
|
|
107
|
+
const filesFresh = Boolean(proof && proof.intentHash === flow.intentHash && proof.revision === currentRevision && proof.files === projectFingerprint(root));
|
|
108
|
+
const fresh = Boolean(filesFresh && proof.workflow === flow.changes);
|
|
109
|
+
const complete = Boolean(fresh && proof.done && proof.scenarios.length && proof.scenarios.every(s => s.status === 'pass') && !flow.waiting.length && !flow.handed.length && !flow.approvalWaiting && !flow.abandoned);
|
|
110
|
+
const remaining = complete ? [] : scenarios.filter(s => !filesFresh || s.status !== 'pass' || flow.handed.includes(s.id)).map(s => s.id);
|
|
111
|
+
const hash = proof?.intentHash ?? 'unverified';
|
|
112
|
+
return { root, revision: currentRevision, intent: hash, complete, fresh, approvalWaiting: flow.approvalWaiting, abandoned: flow.abandoned, waiting: flow.waiting, handed: flow.handed, graph: binding.scenarios,
|
|
113
|
+
remaining, repair: repairData(proof?.repair), failures: proof?.failures ?? [], progress: digest(JSON.stringify({ statuses: proof?.scenarios?.map(s => [s.id, s.status]).sort() ?? [] })) };
|
|
114
|
+
}
|
|
115
|
+
function sessionStatus(payload = {}, env = process.env, cwd = process.cwd()) {
|
|
116
|
+
let root;
|
|
117
|
+
try {
|
|
118
|
+
root = hostRoot(payload, env, cwd);
|
|
119
|
+
const id = identity(payload, env);
|
|
120
|
+
const raw = readPrivate(root, name('session', id), 131072);
|
|
121
|
+
if (!raw) return { status: 'session-unbound', root, revision: revision(root) };
|
|
122
|
+
const binding = JSON.parse(raw);
|
|
123
|
+
if (binding.schemaVersion !== 1 || binding.id !== id || !Array.isArray(binding.scenarios) || binding.scenarios.length > 1000 ||
|
|
124
|
+
binding.scenarios.some(s => !s || !ID.test(s.id) || !Array.isArray(s.needs) || s.needs.length > 1000 || s.needs.some(n => !ID.test(n)))) throw new Error('session-unavailable');
|
|
125
|
+
if (binding.root !== root) return { status: 'session-root-mismatch', root, revision: revision(root) };
|
|
126
|
+
return { status: 'bound', id, ...structuralStatus(root, binding) };
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const allowed = ['session-identity-invalid', 'session-identity-conflict', 'session-identity-missing', 'session-root-invalid', 'session-root-conflict'];
|
|
129
|
+
return { status: allowed.includes(error.message) ? error.message : 'session-unavailable', root: root ?? null, revision: 'unavailable' };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function message(view, status) {
|
|
133
|
+
const root = view.root ? encodeURIComponent(view.root).slice(0, 600) : 'unavailable';
|
|
134
|
+
const ids = (view.remaining ?? []).slice(0, 12).map(id => `${view.intent}/${id}`).join(',') || 'unresolved';
|
|
135
|
+
const questions = (view.waiting ?? []).filter(id => /^[a-zA-Z0-9-]{1,80}$/.test(id)).slice(0, 12);
|
|
136
|
+
return `[vibe] ${status}; root=${root}; revision=${view.revision}; scenarios=${ids}${questions.length ? `; questions=${questions.join(',')}` : ''}. Stop ran no checks.${repairMessage(view)}`;
|
|
137
|
+
}
|
|
138
|
+
function allHanded(view) {
|
|
139
|
+
if (!view.handed.length || !view.remaining.length) return false;
|
|
140
|
+
const covered = new Set(view.handed);
|
|
141
|
+
let changed;
|
|
142
|
+
do {
|
|
143
|
+
changed = false;
|
|
144
|
+
for (const s of view.graph) if (!covered.has(s.id) && s.needs.some(id => covered.has(id))) { covered.add(s.id); changed = true; }
|
|
145
|
+
} while (changed);
|
|
146
|
+
return view.remaining.every(id => covered.has(id));
|
|
147
|
+
}
|
|
148
|
+
function stopDecision(payload = {}, env = process.env, cwd = process.cwd()) {
|
|
149
|
+
const view = sessionStatus(payload, env, cwd);
|
|
150
|
+
if (view.status !== 'bound') return { systemMessage: message(view, `${view.status}; unmet; use explicit vibe session bind in the intended worktree`) };
|
|
151
|
+
if (view.snapshotStatus === 'unavailable') return { systemMessage: message(view, 'explicit-check snapshot unavailable; unmet') };
|
|
152
|
+
if (view.complete) return { systemMessage: message(view, 'verified completion from an explicit check') };
|
|
153
|
+
if (view.approvalWaiting) return { systemMessage: message(view, 'waiting for approval; unmet') };
|
|
154
|
+
if (view.abandoned) return { systemMessage: message(view, 'intent abandoned; unmet') };
|
|
155
|
+
if (view.waiting.length) return { systemMessage: message(view, 'waiting for an answer; unmet') };
|
|
156
|
+
if (allHanded(view)) return { systemMessage: message(view, 'required work is in handoff; unmet') };
|
|
157
|
+
try {
|
|
158
|
+
const raw = readPrivate(view.root, name('guard', view.id), 4096);
|
|
159
|
+
const old = raw ? JSON.parse(raw) : {};
|
|
160
|
+
const count = old.key === view.progress && Number.isInteger(old.count) && old.count >= 0 && old.count <= 3 ? Math.min(old.count + 1, 3) : 1;
|
|
161
|
+
writePrivate(view.root, name('guard', view.id), JSON.stringify({ key: view.progress, count }));
|
|
162
|
+
const reason = message(view, count > 2 ? 'same-state retry limit reached; unmet' : 'work remains unmet; build then explicitly run vibe check');
|
|
163
|
+
return count > 2 ? { systemMessage: reason } : { decision: 'block', reason };
|
|
164
|
+
} catch { return { systemMessage: message(view, 'session guard unavailable; unmet') }; }
|
|
165
|
+
}
|
|
166
|
+
module.exports = { identity, hostRoot, bindSession, recordStopEvidence, sessionStatus, stopDecision, message };
|
package/hooks/session.js
CHANGED
|
@@ -1,76 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
* SessionStart hook for the marketplace plugins (Claude Code · Codex · ChatGPT desktop).
|
|
4
|
-
*
|
|
5
|
-
* A plugin cannot write the client's CLAUDE.md / AGENTS.md, so the always-on card is handed to
|
|
6
|
-
* the model here as context instead, with one line about the `vibe` CLI the skills call (the
|
|
7
|
-
* npm package that registered this plugin; never installed from here). If the client home already
|
|
8
|
-
* carries the card, the plugin steps back: one card, one hook, never two. Always exits 0.
|
|
9
|
-
*/
|
|
10
|
-
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import fs from 'node:fs';
|
|
2
|
+
// A packaged SessionStart reader: no CLI lookup, parent search or check execution.
|
|
12
3
|
import os from 'node:os';
|
|
13
4
|
import path from 'node:path';
|
|
14
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import sessionRuntime from './session-state.cjs';
|
|
7
|
+
import sessionFiles from './session-files.cjs';
|
|
15
8
|
|
|
16
9
|
const client = process.argv[2] || 'claude';
|
|
17
|
-
const
|
|
18
|
-
const root = path.join(here, '..');
|
|
10
|
+
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
11
|
const home = process.env.VIBE_HOME_DIR || os.homedir();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
return fs.readFileSync(file, 'utf-8');
|
|
25
|
-
} catch {
|
|
26
|
-
return '';
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const version = (() => {
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(read(path.join(root, 'package.json'))).version || '0.0.0';
|
|
33
|
-
} catch {
|
|
34
|
-
return '0.0.0';
|
|
35
|
-
}
|
|
36
|
-
})();
|
|
37
|
-
const card = read(path.join(root, 'card.md')).trim();
|
|
38
|
-
const homeCard = client === 'claude' ? path.join(home, '.claude', 'CLAUDE.md') : path.join(home, '.codex', 'AGENTS.md');
|
|
39
|
-
const homeOwns = read(homeCard).includes(CARD_START); // the npm install owns the card for this client; the state is still handed over below
|
|
40
|
-
|
|
41
|
-
function cliVersion() {
|
|
42
|
-
const r = spawnSync('vibe', ['--version'], { encoding: 'utf-8', timeout: 10000, shell: process.platform === 'win32' });
|
|
43
|
-
return r.status === 0 ? (r.stdout || '').trim() : null;
|
|
12
|
+
function read(file, limit = 65536) {
|
|
13
|
+
try { return sessionFiles.optional(file, limit) ?? ''; }
|
|
14
|
+
catch { return ''; }
|
|
44
15
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const pb = b.split('.').map(Number);
|
|
49
|
-
for (let i = 0; i < 3; i += 1) if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) > (pb[i] || 0);
|
|
50
|
-
return false;
|
|
16
|
+
function payload() {
|
|
17
|
+
try { return sessionFiles.readPayload(); }
|
|
18
|
+
catch { return { session_id: 'invalid payload' }; }
|
|
51
19
|
}
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
function stateNote() {
|
|
60
|
-
const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
61
|
-
let dir = cwd;
|
|
62
|
-
for (;;) {
|
|
63
|
-
if (fs.existsSync(path.join(dir, '.vibe', 'state.json'))) break;
|
|
64
|
-
const up = path.dirname(dir);
|
|
65
|
-
if (up === dir || fs.existsSync(path.join(dir, '.git'))) return '';
|
|
66
|
-
dir = up;
|
|
67
|
-
}
|
|
68
|
-
const r = spawnSync('vibe', ['state'], { cwd: dir, encoding: 'utf-8', timeout: 60000, shell: process.platform === 'win32', env: { ...process.env, VIBE_SKIP_SETUP: '1' } });
|
|
69
|
-
return r.status === 0 && r.stdout ? `\n\n[vibe state — this is the first command already run; continue from its next line]\n${r.stdout.trim()}` : '';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const state = stateNote();
|
|
73
|
-
if (homeOwns && !state) process.exit(0);
|
|
74
|
-
const text = homeOwns ? state.trim() : `${card}\n\n[vibe plugin ${version}] ${cliNote}${state}`;
|
|
20
|
+
const card = read(path.join(root, 'card.md'), 4096).trim();
|
|
21
|
+
const homeCard = client === 'claude' ? path.join(home, '.claude', 'CLAUDE.md') : path.join(home, '.codex', 'AGENTS.md');
|
|
22
|
+
const homeOwns = read(homeCard).includes('<!-- vibe:start -->');
|
|
23
|
+
const view = sessionRuntime.sessionStatus(payload());
|
|
24
|
+
const status = view.status === 'bound' ? 'session status only; run vibe state for the procedure' : `${view.status}; use explicit vibe session bind in the intended worktree`;
|
|
25
|
+
const note = sessionRuntime.message(view, status);
|
|
26
|
+
const text = homeOwns ? note : `${card}\n\n${note}`;
|
|
75
27
|
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: text } })}\n`);
|
|
76
|
-
process.exit(0);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
|
|
3
|
+
// A bounded lexical recognizer, never a shell evaluator. Dynamic expansions remain unresolved.
|
|
4
|
+
function tokenize(text) {
|
|
5
|
+
if (typeof text !== 'string' || text.length > 65536) return null;
|
|
6
|
+
const tokens = [];
|
|
7
|
+
let word = '', quote = '';
|
|
8
|
+
const flush = () => { if (word) tokens.push(word); word = ''; };
|
|
9
|
+
for (let i = 0; i < text.length; i++) {
|
|
10
|
+
const ch = text[i];
|
|
11
|
+
if (quote) {
|
|
12
|
+
if (ch === quote) quote = '';
|
|
13
|
+
else if (ch === '\\' && quote === '"' && ['"', '\\'].includes(text[i + 1])) word += text[++i];
|
|
14
|
+
else word += ch;
|
|
15
|
+
} else if (ch === '"' || ch === "'") quote = ch;
|
|
16
|
+
else if (ch === '\\' && /[\s'"|;&<>]/.test(text[i + 1] ?? '')) word += text[++i];
|
|
17
|
+
else if (/[\n|;&<>]/.test(ch)) { flush(); tokens.push(['|', '&', '<', '>'].includes(ch) && text[i + 1] === ch ? ch + text[++i] : ch); }
|
|
18
|
+
else if (/\s/.test(ch)) flush();
|
|
19
|
+
else word += ch;
|
|
20
|
+
if (tokens.length > 2048) return null;
|
|
21
|
+
}
|
|
22
|
+
if (quote) return null;
|
|
23
|
+
flush();
|
|
24
|
+
return tokens;
|
|
25
|
+
}
|
|
26
|
+
function sections(tokens) {
|
|
27
|
+
const out = [];
|
|
28
|
+
let words = [], pipe = false;
|
|
29
|
+
for (const token of tokens) {
|
|
30
|
+
if (['|', '||', ';', '&', '&&', '\n'].includes(token)) {
|
|
31
|
+
if (words.length) out.push({ words, pipe });
|
|
32
|
+
words = []; pipe = token === '|';
|
|
33
|
+
} else words.push(token);
|
|
34
|
+
}
|
|
35
|
+
if (words.length) out.push({ words, pipe });
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function command(words) {
|
|
39
|
+
let index = 0;
|
|
40
|
+
const redirects = [];
|
|
41
|
+
if (words[0] === 'env') index++;
|
|
42
|
+
for (;;) {
|
|
43
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[index] ?? '')) index++;
|
|
44
|
+
else if (['<', '>', '>>'].includes(words[index]) && words[index + 1]) { redirects.push(words[index], words[index + 1]); index += 2; }
|
|
45
|
+
else break;
|
|
46
|
+
}
|
|
47
|
+
return { name: path.basename(words[index] ?? '').replace(/\.exe$/i, ''), args: [...words.slice(index + 1), ...redirects] };
|
|
48
|
+
}
|
|
49
|
+
function operands(args, valued = []) {
|
|
50
|
+
const out = [];
|
|
51
|
+
let options = true;
|
|
52
|
+
for (let i = 0; i < args.length; i++) {
|
|
53
|
+
const word = args[i];
|
|
54
|
+
if (word === '--') { options = false; continue; }
|
|
55
|
+
if (word === '>' || word === '>>') { i++; continue; }
|
|
56
|
+
if (word === '<') { if (args[i + 1]) out.push(args[++i]); continue; }
|
|
57
|
+
if (options && valued.includes(word)) { i++; continue; }
|
|
58
|
+
if (options && word.startsWith('-')) continue;
|
|
59
|
+
out.push(word);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function grepInfo(name, args) {
|
|
64
|
+
const options = args.filter(word => word.startsWith('-'));
|
|
65
|
+
const namesOnly = options.some(word => /^--files(?:-with-matches|-without-match)?$|^-(?!-)[A-Za-z]*[lL][A-Za-z]*$/.test(word));
|
|
66
|
+
const locations = namesOnly || options.some(word => word === '--line-number' || /^-[^-]*n/.test(word));
|
|
67
|
+
const explicitPattern = args.some(word => ['-e', '--regexp', '-f', '--file'].includes(word) || /^--(?:regexp|file)=/.test(word));
|
|
68
|
+
const files = operands(args, ['-e', '--regexp', '-f', '--file', '-g', '--glob', '-t', '--type', '-T', '--type-not', '-m', '--max-count', '--max-columns', '--max-depth', '-A', '-B', '-C']);
|
|
69
|
+
if (!explicitPattern && !options.includes('--files')) files.shift();
|
|
70
|
+
return { files, slice: !locations, namesOnly, implicit: name === 'rg' || options.some(word => /^-[^-]*[rR]/.test(word)) };
|
|
71
|
+
}
|
|
72
|
+
function transformInfo(name, args) {
|
|
73
|
+
if (name === 'tr') return { files: args.flatMap((word, index) => word === '<' && args[index + 1] ? [args[index + 1]] : []), slice: false };
|
|
74
|
+
const values = {
|
|
75
|
+
nl: ['-b', '-d', '-f', '-h', '-i', '-l', '-n', '-s', '-v', '--body-numbering', '--section-delimiter', '--footer-numbering', '--header-numbering', '--line-increment', '--join-blank-lines', '--number-format', '--number-separator', '--starting-line-number'],
|
|
76
|
+
sort: ['-k', '-t', '-o', '-T', '-S', '--key', '--field-separator', '--output', '--temporary-directory', '--buffer-size', '--parallel'],
|
|
77
|
+
uniq: ['-f', '-s', '-w', '--skip-fields', '--skip-chars', '--check-chars'],
|
|
78
|
+
};
|
|
79
|
+
if (!Object.hasOwn(values, name)) return null;
|
|
80
|
+
const files = operands(args, values[name]);
|
|
81
|
+
return { files: name === 'uniq' ? files.slice(0, 1) : files, slice: false };
|
|
82
|
+
}
|
|
83
|
+
function readerInfo(name, args) {
|
|
84
|
+
if (['grep', 'rg'].includes(name)) return grepInfo(name, args);
|
|
85
|
+
if (name === 'cat') return { files: operands(args), slice: false };
|
|
86
|
+
if (['head', 'tail'].includes(name)) return { files: operands(args, ['-n', '-c', '--lines', '--bytes', '--sleep-interval']), slice: true };
|
|
87
|
+
if (name === 'cut') return { files: operands(args, ['-b', '-c', '-f', '-d', '--bytes', '--characters', '--fields', '--delimiter', '--output-delimiter']), slice: true };
|
|
88
|
+
if (['sed', 'awk'].includes(name)) {
|
|
89
|
+
const explicit = args.some(word => ['-e', '-f', '--expression', '--file'].includes(word));
|
|
90
|
+
const files = operands(args, ['-e', '-f', '--expression', '--file', '-F', '-v']);
|
|
91
|
+
if (!explicit) files.shift();
|
|
92
|
+
const slice = name === 'awk' ? args.some(word => /\bNR\b|\bprint\b/.test(word)) : args.some(word => /^-[^-]*n/.test(word) || word === '--quiet' || word === '--silent');
|
|
93
|
+
return { files, slice };
|
|
94
|
+
}
|
|
95
|
+
return transformInfo(name, args);
|
|
96
|
+
}
|
|
97
|
+
function sliceTargets(text, cwd, depth = 0) {
|
|
98
|
+
const tokens = tokenize(text);
|
|
99
|
+
if (!tokens || tokens.includes('<<') || depth > 1) return { unavailable: true, files: [] };
|
|
100
|
+
const found = new Set();
|
|
101
|
+
let input = [], directory = cwd;
|
|
102
|
+
for (const section of sections(tokens)) {
|
|
103
|
+
if (!section.pipe) input = [];
|
|
104
|
+
const { name, args } = command(section.words);
|
|
105
|
+
if (name === 'cd' && args[0] && !/[`$*?]/.test(args[0])) { directory = path.resolve(directory, args[0]); continue; }
|
|
106
|
+
if (['bash', 'sh', 'zsh'].includes(name) && /^-[a-z]*c$/.test(args[0] ?? '')) {
|
|
107
|
+
const nested = sliceTargets(args[1] ?? '', directory, depth + 1);
|
|
108
|
+
if (nested.unavailable) return nested;
|
|
109
|
+
for (const file of nested.files) found.add(file);
|
|
110
|
+
}
|
|
111
|
+
const info = readerInfo(name, args);
|
|
112
|
+
if (!info) { input = []; continue; }
|
|
113
|
+
const files = info.files.filter(file => file !== '-' && !/[`$]/.test(file)).map(file => path.resolve(directory, file));
|
|
114
|
+
if (!files.length && !section.pipe && info.implicit) files.push(directory);
|
|
115
|
+
if (files.length) input = files;
|
|
116
|
+
if (info.slice) for (const file of input) found.add(file);
|
|
117
|
+
if (info.namesOnly) input = [];
|
|
118
|
+
}
|
|
119
|
+
return { unavailable: false, files: [...found].slice(0, 256) };
|
|
120
|
+
}
|
|
121
|
+
module.exports = { sliceTargets };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { sliceTargets } = require('./slice-command.cjs');
|
|
4
|
+
const { readTargets, recordSlice } = require('./slice-store.cjs');
|
|
5
|
+
|
|
6
|
+
function within(directory, file) {
|
|
7
|
+
const relative = path.relative(directory, file);
|
|
8
|
+
return !path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`);
|
|
9
|
+
}
|
|
10
|
+
function exempt(root, file) {
|
|
11
|
+
const relative = path.relative(root, file).split(path.sep).join('/');
|
|
12
|
+
return /(?:^|\/)\.dev(?:\/|$)/.test(relative) || /\.log(?:\.[\w-]+)?$/i.test(file);
|
|
13
|
+
}
|
|
14
|
+
function protectedFile(root, file, files) {
|
|
15
|
+
const relative = path.relative(root, file).split(path.sep).join('/');
|
|
16
|
+
if (relative === '.vibe' || relative === '.vibe/intent.md' || /(?:^|\/)scenarios\.ya?ml$/i.test(relative) || /(?:^|\/)specs?(?:\/|$)/i.test(relative)) return true;
|
|
17
|
+
return files.some(protectedPath => file === protectedPath || within(file, protectedPath));
|
|
18
|
+
}
|
|
19
|
+
function sourceFile(file) {
|
|
20
|
+
if (/\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|c|cpp|h|cs|rb|sh|sql|md|mdx|txt|rst|json|ya?ml|toml|ini|xml|html|css|csv|tsv)$/i.test(file)) return true;
|
|
21
|
+
if (/^(?:README|Dockerfile|Makefile)(?:\..*)?$/i.test(path.basename(file))) return true;
|
|
22
|
+
try { return fs.lstatSync(file).isDirectory(); } catch { return false; }
|
|
23
|
+
}
|
|
24
|
+
function guidance(client) {
|
|
25
|
+
const tool = client === 'codex' ? 'cat (the entire file)' : 'the Read tool (the entire file)';
|
|
26
|
+
return `Open the selected files in full with ${tool}. Read fewer files from vibe state; use grep/rg/find only to locate files or lines. For over 400 lines when not editing, use vibe read --ask.`;
|
|
27
|
+
}
|
|
28
|
+
function sliceGuard(root, command, client = 'claude', cwd = root) {
|
|
29
|
+
try { root = fs.realpathSync(root); cwd = fs.realpathSync(path.resolve(root, cwd)); } catch { return { decision: 'none', unavailable: true }; }
|
|
30
|
+
const targets = sliceTargets(command, cwd);
|
|
31
|
+
if (targets.unavailable) return { decision: 'none', unavailable: true };
|
|
32
|
+
const files = targets.files.filter(file => !exempt(root, file));
|
|
33
|
+
if (!files.length) return { decision: 'none', unavailable: false };
|
|
34
|
+
const mirror = readTargets(root);
|
|
35
|
+
const decision = files.some(file => protectedFile(root, file, mirror.files)) ? 'block' : files.some(sourceFile) ? 'warn' : 'none';
|
|
36
|
+
if (decision === 'none') return { decision, unavailable: !mirror.available };
|
|
37
|
+
const recorded = recordSlice(root, decision);
|
|
38
|
+
const unavailable = !mirror.available || !recorded;
|
|
39
|
+
return { decision, unavailable, recorded, message: `[vibe] ${decision === 'block' ? 'Partial reading of a protected file is blocked.' : 'Partial source/document reading can omit requirements.'} ${guidance(client)}${unavailable ? ' Read-target mirror or count storage unavailable.' : ''}` };
|
|
40
|
+
}
|
|
41
|
+
module.exports = { sliceGuard };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { randomBytes } = require('node:crypto');
|
|
4
|
+
const { privateDirectory, readPrivate, writePrivate } = require('./private-store.cjs');
|
|
5
|
+
const { digest, json } = require('./session-files.cjs');
|
|
6
|
+
|
|
7
|
+
const prefix = root => `slice-${digest(root)}-`;
|
|
8
|
+
const markerName = root => `slice-counts-${digest(root)}.json`;
|
|
9
|
+
function explicitHome(env) {
|
|
10
|
+
if (env === undefined) return undefined;
|
|
11
|
+
const home = process.platform === 'win32' ? env.USERPROFILE : env.HOME;
|
|
12
|
+
if (typeof home !== 'string' || !path.isAbsolute(home)) throw new Error('fixture home required');
|
|
13
|
+
return home;
|
|
14
|
+
}
|
|
15
|
+
function recordReadTargets(root, input, env) {
|
|
16
|
+
try {
|
|
17
|
+
root = fs.realpathSync(root);
|
|
18
|
+
if (!input || !/^[a-f0-9]{16,64}$/.test(input.intentHash) || !Array.isArray(input.files) || input.files.length > 2048 || input.files.some(file => typeof file !== 'string' || file.length >= 4096)) return false;
|
|
19
|
+
const files = [...new Set(input.files.map(file => path.resolve(root, file)))];
|
|
20
|
+
const body = JSON.stringify({ schemaVersion: 1, root, intentHash: input.intentHash, files });
|
|
21
|
+
if (Buffer.byteLength(body) > 131072) return false;
|
|
22
|
+
writePrivate(root, `read-targets-${digest(root)}.json`, body, explicitHome(env));
|
|
23
|
+
return true;
|
|
24
|
+
} catch { return false; }
|
|
25
|
+
}
|
|
26
|
+
function readTargets(root) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = readPrivate(root, `read-targets-${digest(root)}.json`, 131072);
|
|
29
|
+
if (raw === null) return { available: false, files: [] };
|
|
30
|
+
const saved = JSON.parse(raw), state = json(path.join(root, '.vibe/state.json'));
|
|
31
|
+
if (saved.schemaVersion !== 1 || saved.root !== root || saved.intentHash !== state.intentHash || !Array.isArray(saved.files) || saved.files.length > 2048 || saved.files.some(file => typeof file !== 'string' || !path.isAbsolute(file))) return { available: false, files: [] };
|
|
32
|
+
return { available: true, files: saved.files };
|
|
33
|
+
} catch { return { available: false, files: [] }; }
|
|
34
|
+
}
|
|
35
|
+
function recordSlice(root, decision) {
|
|
36
|
+
try {
|
|
37
|
+
if (!['block', 'warn'].includes(decision)) return false;
|
|
38
|
+
if (!initializeSliceCounts(root)) return false;
|
|
39
|
+
const counts = { blocked: Number(decision === 'block'), warned: Number(decision === 'warn') };
|
|
40
|
+
writePrivate(root, `${prefix(root)}${randomBytes(12).toString('hex')}.json`, JSON.stringify(counts));
|
|
41
|
+
return true;
|
|
42
|
+
} catch { return false; }
|
|
43
|
+
}
|
|
44
|
+
function initializeSliceCounts(root, env) {
|
|
45
|
+
try {
|
|
46
|
+
root = fs.realpathSync(root);
|
|
47
|
+
const home = explicitHome(env);
|
|
48
|
+
const raw = readPrivate(root, markerName(root), 4096, home);
|
|
49
|
+
if (raw !== null) { const value = JSON.parse(raw); return value.schemaVersion === 1 && value.root === root; }
|
|
50
|
+
writePrivate(root, markerName(root), JSON.stringify({ schemaVersion: 1, root }), home);
|
|
51
|
+
return true;
|
|
52
|
+
} catch { return false; }
|
|
53
|
+
}
|
|
54
|
+
function readSliceCounts(root, env) {
|
|
55
|
+
try {
|
|
56
|
+
root = fs.realpathSync(root);
|
|
57
|
+
const home = explicitHome(env);
|
|
58
|
+
const marker = JSON.parse(readPrivate(root, markerName(root), 4096, home));
|
|
59
|
+
if (!marker || marker.schemaVersion !== 1 || marker.root !== root) return null;
|
|
60
|
+
const directory = privateDirectory(root, false, home);
|
|
61
|
+
const files = fs.readdirSync(directory);
|
|
62
|
+
if (files.length > 32768) return null;
|
|
63
|
+
const counts = { blocked: 0, warned: 0 };
|
|
64
|
+
for (const file of files.filter(file => file.startsWith(prefix(root)))) {
|
|
65
|
+
const value = JSON.parse(readPrivate(root, file, 128, home));
|
|
66
|
+
if (!((value.blocked === 1 && value.warned === 0) || (value.blocked === 0 && value.warned === 1))) return null;
|
|
67
|
+
counts.blocked += value.blocked; counts.warned += value.warned;
|
|
68
|
+
}
|
|
69
|
+
return counts;
|
|
70
|
+
} catch { return null; }
|
|
71
|
+
}
|
|
72
|
+
module.exports = { recordReadTargets, readTargets, recordSlice, initializeSliceCounts, readSliceCounts };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@su-record/vibe",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.26",
|
|
4
4
|
"description": "An AX/FDE harness for Claude Code, Codex CLI and ChatGPT desktop. Say what you need, approve the scenarios once; the harness proves the work by running the checks itself. Memory in plain files, human tokens for irreversible actions, a ledger instead of claims.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/skills/vibe/SKILL.md
CHANGED
|
@@ -13,6 +13,7 @@ If `vibe` is not on PATH, run `npm i -g @su-record/vibe` once; every command bel
|
|
|
13
13
|
## Procedure
|
|
14
14
|
|
|
15
15
|
1. Run `vibe state --json`. A directory without `.vibe/` answers NONE; the first record creates it.
|
|
16
|
+
- Once the intended worktree is known, run `vibe session bind` when the host supplies `CODEX_THREAD_ID` or `CLAUDE_SESSION_ID`; otherwise use its actual session ID with `--session`. Never invent an ID or bind from inherited ledger entries. SessionStart and Stop read status only; run checks explicitly.
|
|
16
17
|
2. If `notices` is non-empty, show them to the user first.
|
|
17
18
|
3. If work is in progress (`state` is not NONE or ABANDONED) and the user's request is a new one, ask in one line: continue or start over? Starting over means `vibe abandon --reason "…"` first.
|
|
18
19
|
4. Follow the `next` line. It is the procedure; a stage skill is loaded only when `next` names one:
|
|
@@ -20,15 +21,15 @@ If `vibe` is not on PATH, run `npm i -g @su-record/vibe` once; every command bel
|
|
|
20
21
|
| `next` starts with | do |
|
|
21
22
|
|---|---|
|
|
22
23
|
| `discover` | load `vibe-discover` |
|
|
23
|
-
| `approve` | load `vibe-scope` —
|
|
24
|
+
| `approve` | load `vibe-scope` — sufficient agreement, runnable checks, `vibe intent analyze`, one approval message; research only for an unresolved gap |
|
|
24
25
|
The card starts at `vibe state` and loads only the skill `next` names; this router is for an explicit `/vibe` and says the same thing.
|
|
25
26
|
|
|
26
27
|
| `build …` | build what it lists first, then one `vibe check --all`; on a failure, fix what the check names; `vibe context <id>` when that is not enough, then `vibe check <id>` for that scenario only; load `vibe-build` only for a full task with parallel or irreversible scenarios |
|
|
27
28
|
| `check --all` | run it |
|
|
28
|
-
| `prove — STUCK`
|
|
29
|
+
| `prove — STUCK` | load `vibe-prove`; at two same-hash failures diagnose with context and change approach; at five relay the question id and cause, then wait; never invent an answer |
|
|
29
30
|
| `report` | the completion report (card rule 9); HANDOFF.md only if the intent asks; no further checks after DONE |
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
Reuse an approved intent and unchanged evidence. Re-enter scope only for a changed outcome, constraint, or evidence basis: a new draft voids the previous approval.
|
|
32
33
|
|
|
33
34
|
## Never
|
|
34
35
|
|
|
@@ -12,10 +12,10 @@ user-invocable: false
|
|
|
12
12
|
1. Take scenarios in the order of `remaining` from `vibe state --json` — parents before their `needs` dependents (`vibe state --graph` shows the edges). Put scenarios marked `irreversible` last.
|
|
13
13
|
- Scenarios with no edge between them may be built by parallel agents, each in its own worktree, merged before `vibe check --all`. Never two agents in one working tree. Batch scenarios of one shape into one dispatch; never a nested subagent (a reviewer's reviewer counts for nothing). Hand artifacts to an agent as files, never pasted. When the context is nearly full, start a new session from `.vibe/` instead of compacting.
|
|
14
14
|
2. Build only what each scenario needs, and make sure the check itself (`check.cmd`, `check.path`) can actually run. `vibe map` and `vibe context {id}` are tools for a question — where a symbol lives, what a change reaches — not a step before every scenario.
|
|
15
|
-
-
|
|
15
|
+
- Read fewer files from state, each in full (Claude Read; Codex `cat`); grep/rg/find locate files or lines, never substitute content slices. Over 400 lines when not editing: `vibe read <files> --ask "<question>"`.
|
|
16
16
|
3. When everything is built, `vibe check --all --json`; on a failure, fix what the check names — the failed line carries the scenario's files — and `vibe check {id} --json` for that scenario only; `vibe context {id}` when the files are not enough: the decisions, regressions and notes around them.
|
|
17
17
|
- Pass (`code 0`): the next failing scenario, or `vibe check --all` when none is left.
|
|
18
|
-
- Fail (`code 1`):
|
|
18
|
+
- Fail (`code 1`): use the error summary and named files; raw output requires explicit `--diagnostics` and stays private. The same failure twice requires `vibe context` and a changed approach; ask after five failed attempts or outside your authority, including the error and attempts.
|
|
19
19
|
- Blocked (`status: blocked`): a parent has not passed; `vibe check {id}` runs unpassed parents first, so fix the parent named in `blockedBy`.
|
|
20
20
|
4. When a project-local skill (`vibe skill list`) applies to the scenario, follow it and run `vibe skill used {name}` — prune decisions read the ledger.
|
|
21
21
|
5. Record a fixed failure with `vibe regress record --scenario {id} --title "…" --check-from-evidence {run}`.
|
|
@@ -35,3 +35,4 @@ If the response carries a token, show it to the user and execute only after they
|
|
|
35
35
|
|
|
36
36
|
- Weaken a check to make it pass (editing scenarios.yaml voids the approval — the harness enforces this).
|
|
37
37
|
- Claim a pass without `vibe check`.
|
|
38
|
+
- Rely on Stop to run checks. It blocks unchanged unfinished work twice, then releases it as unmet; inbox waits and scenario handoffs also release without success.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vibe-discover
|
|
3
|
-
description: Discover —
|
|
3
|
+
description: Discover — find the customer's problem and agree on success from evidence. Ask only about consequential unknowns; stop when the scope is sufficient.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,19 +8,20 @@ user-invocable: false
|
|
|
8
8
|
|
|
9
9
|
## Procedure
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Resolve the problem and success criteria before implementation choices. A clear request can need no interview. If the request is to find work AI can take over, read [work-opportunities.md](work-opportunities.md) for that procedure; do not load it for other tasks.
|
|
12
12
|
|
|
13
|
-
1.
|
|
13
|
+
1. Use accessible evidence before asking the customer:
|
|
14
|
+
- on resumption, reuse the intent's compact findings when `vibe intent show --json` reports their `sourceBasis` unchanged; re-evaluate only findings affected by changed or missing sources
|
|
14
15
|
- before asking, scan `docs/`, `README*` and `*.md` for relevant rules even when the brief does not name them; read those documents before deciding what is unknown
|
|
15
|
-
- table (csv · tsv · jsonl · json · xlsx)
|
|
16
|
+
- when a table can resolve a relevant unknown (csv · tsv · jsonl · json · xlsx), use `vibe profile {file} --json` [`--sheet`]: columns, types, missing counts, duplicates, up to three anomalies with numbers
|
|
16
17
|
- document (xlsx · docx · pptx · pdf · hwp · hwpx · html) → `vibe read {file} --json` [`--sheet` · `--pages`]; it says which reader it used (pdf: `pdftotext` when installed, else built-in)
|
|
17
18
|
- long material that only needs an answer ("what does this contract require", "which sheet holds the totals") → `vibe read {files} --ask "{question}"`; a low-reasoning model reads it and only the answer enters your context
|
|
18
19
|
- image → your own file reader; vibe does not read images
|
|
19
|
-
- code or plain text →
|
|
20
|
-
If
|
|
21
|
-
2.
|
|
22
|
-
3. Ask
|
|
23
|
-
4.
|
|
20
|
+
- code or plain text → fewer relevant files, each in full (Claude Read; Codex `cat`); grep/rg/find locate only, never content slices; over 400 lines when not editing → `vibe read --ask`
|
|
21
|
+
If a required sample is absent, name what cannot be established and request the smallest useful sample. Do not demand a sample for a task already defined and checkable without one.
|
|
22
|
+
2. Report relevant observed anomalies first — at most three, each with its number and source. Do not invent a profile finding, duration, saving, probability, or missing requirement.
|
|
23
|
+
3. Ask only when the answer changes the problem, success condition, constraint, or permitted action. Bundle at most three high-impact questions per round; explain the decision each resolves. An optional proposal is labelled as a proposal. Silence never answers a material question or authorizes an action; ask a focused follow-up if an important unknown remains. Reports belong in chat, not `vibe ask`.
|
|
24
|
+
4. Write the compact intent (`.vibe/intent.md`, in English), retaining source references, the customer's decisions, and explicit unknowns:
|
|
24
25
|
|
|
25
26
|
```
|
|
26
27
|
# {one-line title}
|
|
@@ -35,10 +36,10 @@ Order the questions by impact: scope first, then security and privacy, then the
|
|
|
35
36
|
- {existing systems, data, deadlines}
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
5.
|
|
39
|
+
5. Move to `vibe-scope` once the selected outcome, evidence basis, material constraints, executable acceptance conditions, and action boundaries are clear. Reuse an existing approved scope unless evidence or the requested outcome changes. Do not expand into another task just to fill a question or scenario quota.
|
|
39
40
|
|
|
40
41
|
## Message format
|
|
41
42
|
|
|
42
|
-
- Questions as a numbered list,
|
|
43
|
+
- Questions as a short numbered list when there is more than one; attach the decision, not an invented default.
|
|
43
44
|
- Anomalies start with "Looking at the sample, …" and carry the number.
|
|
44
45
|
- Talk to the user in the user's language; the intent file itself is English.
|