@vibecompass/vibecompass 0.1.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/LICENSE +21 -0
- package/README.md +329 -0
- package/package.json +42 -0
- package/src/cli.js +572 -0
- package/src/frontmatter.js +28 -0
- package/src/generators/agent-files/agents-md.js +13 -0
- package/src/generators/agent-files/claude-md.js +13 -0
- package/src/generators/agent-files/copilot-instructions.js +13 -0
- package/src/generators/agent-files/cursor-rules.js +13 -0
- package/src/generators/agent-files/index.js +147 -0
- package/src/generators/agent-files/markers.js +49 -0
- package/src/generators/agent-files/template.js +75 -0
- package/src/hash.js +29 -0
- package/src/index.js +15 -0
- package/src/init.js +227 -0
- package/src/manifest.js +75 -0
- package/src/project-memory.js +678 -0
- package/src/project-yaml.js +107 -0
- package/src/read-model.js +557 -0
- package/src/scaffold.js +404 -0
- package/src/session.js +553 -0
- package/src/setup.js +719 -0
- package/src/simple-yaml.js +227 -0
- package/src/workflow.js +120 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const KEY_VALUE_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*)\s*:(?:\s+(.*)|\s*)$/;
|
|
2
|
+
|
|
3
|
+
export function parseSimpleYaml(source, options = {}) {
|
|
4
|
+
// This intentionally supports only the YAML subset used by project.yaml and
|
|
5
|
+
// markdown frontmatter today. Multi-line block scalars and continued
|
|
6
|
+
// multi-line string values are not supported.
|
|
7
|
+
const sourceName = options.sourceName ?? 'YAML';
|
|
8
|
+
const lines = preprocessLines(source, sourceName);
|
|
9
|
+
|
|
10
|
+
if (lines.length === 0) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const parsed = parseBlock(lines, 0, lines[0].indent, sourceName);
|
|
15
|
+
if (parsed.nextIndex !== lines.length) {
|
|
16
|
+
const line = lines[parsed.nextIndex];
|
|
17
|
+
throw new Error(`${sourceName}:${line.lineNumber} has unsupported trailing content.`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return parsed.value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function preprocessLines(source, sourceName) {
|
|
24
|
+
return source.split(/\r?\n/).flatMap((rawLine, index) => {
|
|
25
|
+
if (/^\s*$/.test(rawLine) || /^\s*#/.test(rawLine)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (/\t/.test(rawLine)) {
|
|
30
|
+
throw new Error(`${sourceName}:${index + 1} uses tabs for indentation.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const indentMatch = rawLine.match(/^ */);
|
|
34
|
+
const indent = indentMatch ? indentMatch[0].length : 0;
|
|
35
|
+
const text = rawLine.slice(indent);
|
|
36
|
+
|
|
37
|
+
return [{ indent, text, lineNumber: index + 1 }];
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseBlock(lines, startIndex, indent, sourceName) {
|
|
42
|
+
const line = lines[startIndex];
|
|
43
|
+
|
|
44
|
+
if (!line) {
|
|
45
|
+
return { value: {}, nextIndex: startIndex };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (line.indent !== indent) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`${sourceName}:${line.lineNumber} has inconsistent indentation. Expected ${indent} spaces, got ${line.indent}.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (line.text.startsWith('- ')) {
|
|
55
|
+
return parseList(lines, startIndex, indent, sourceName);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return parseMap(lines, startIndex, indent, sourceName);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseMap(lines, startIndex, indent, sourceName) {
|
|
62
|
+
const value = {};
|
|
63
|
+
let index = startIndex;
|
|
64
|
+
|
|
65
|
+
while (index < lines.length) {
|
|
66
|
+
const line = lines[index];
|
|
67
|
+
|
|
68
|
+
if (line.indent < indent) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (line.indent > indent) {
|
|
73
|
+
throw new Error(`${sourceName}:${line.lineNumber} has unexpected indentation.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (line.text.startsWith('- ')) {
|
|
77
|
+
throw new Error(`${sourceName}:${line.lineNumber} mixes list items into a mapping.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const keyValue = splitKeyValue(line.text);
|
|
81
|
+
if (!keyValue) {
|
|
82
|
+
throw new Error(`${sourceName}:${line.lineNumber} is not a supported key/value entry.`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (Object.hasOwn(value, keyValue.key)) {
|
|
86
|
+
throw new Error(`${sourceName}:${line.lineNumber} redefines key "${keyValue.key}".`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
index += 1;
|
|
90
|
+
|
|
91
|
+
if (keyValue.value === null) {
|
|
92
|
+
const nested = lines[index];
|
|
93
|
+
if (nested && nested.indent > indent) {
|
|
94
|
+
const parsed = parseBlock(lines, index, nested.indent, sourceName);
|
|
95
|
+
value[keyValue.key] = parsed.value;
|
|
96
|
+
index = parsed.nextIndex;
|
|
97
|
+
} else {
|
|
98
|
+
value[keyValue.key] = null;
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
value[keyValue.key] = parseScalar(keyValue.value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { value, nextIndex: index };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseList(lines, startIndex, indent, sourceName) {
|
|
110
|
+
const value = [];
|
|
111
|
+
let index = startIndex;
|
|
112
|
+
|
|
113
|
+
while (index < lines.length) {
|
|
114
|
+
const line = lines[index];
|
|
115
|
+
|
|
116
|
+
if (line.indent < indent) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (line.indent > indent) {
|
|
121
|
+
throw new Error(`${sourceName}:${line.lineNumber} has unexpected indentation inside a list.`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!line.text.startsWith('- ')) {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const itemText = line.text.slice(2).trim();
|
|
129
|
+
index += 1;
|
|
130
|
+
|
|
131
|
+
if (itemText === '') {
|
|
132
|
+
const nested = lines[index];
|
|
133
|
+
if (nested && nested.indent > indent) {
|
|
134
|
+
const parsed = parseBlock(lines, index, nested.indent, sourceName);
|
|
135
|
+
value.push(parsed.value);
|
|
136
|
+
index = parsed.nextIndex;
|
|
137
|
+
} else {
|
|
138
|
+
value.push(null);
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const inlineMapping = splitKeyValue(itemText);
|
|
144
|
+
if (inlineMapping) {
|
|
145
|
+
const objectValue = {};
|
|
146
|
+
|
|
147
|
+
if (inlineMapping.value === null) {
|
|
148
|
+
const nested = lines[index];
|
|
149
|
+
if (nested && nested.indent > indent) {
|
|
150
|
+
const parsed = parseBlock(lines, index, nested.indent, sourceName);
|
|
151
|
+
objectValue[inlineMapping.key] = parsed.value;
|
|
152
|
+
index = parsed.nextIndex;
|
|
153
|
+
} else {
|
|
154
|
+
objectValue[inlineMapping.key] = null;
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
objectValue[inlineMapping.key] = parseScalar(inlineMapping.value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const nested = lines[index];
|
|
161
|
+
if (nested && nested.indent > indent) {
|
|
162
|
+
const parsed = parseMap(lines, index, nested.indent, sourceName);
|
|
163
|
+
Object.assign(objectValue, parsed.value);
|
|
164
|
+
index = parsed.nextIndex;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
value.push(objectValue);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const nested = lines[index];
|
|
172
|
+
if (nested && nested.indent > indent) {
|
|
173
|
+
throw new Error(`${sourceName}:${line.lineNumber} has unsupported nested scalar list content.`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
value.push(parseScalar(itemText));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { value, nextIndex: index };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function splitKeyValue(text) {
|
|
183
|
+
const match = text.match(KEY_VALUE_PATTERN);
|
|
184
|
+
if (!match) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
key: match[1],
|
|
190
|
+
value: match[2] === undefined || match[2] === '' ? null : match[2].trim(),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseScalar(rawValue) {
|
|
195
|
+
if (rawValue === 'null' || rawValue === '~') {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (rawValue === 'true') {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (rawValue === 'false') {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (/^-?\d+$/.test(rawValue)) {
|
|
208
|
+
return Number(rawValue);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
(rawValue.startsWith('"') && rawValue.endsWith('"')) ||
|
|
213
|
+
(rawValue.startsWith("'") && rawValue.endsWith("'"))
|
|
214
|
+
) {
|
|
215
|
+
return rawValue.slice(1, -1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (rawValue === '[]') {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (rawValue === '{}') {
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return rawValue;
|
|
227
|
+
}
|
package/src/workflow.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const DEFAULT_REVIEWER_HANDBACK = 'handoff-file';
|
|
2
|
+
export const DEFAULT_GIT_REMOTE = 'origin';
|
|
3
|
+
export const DEFAULT_COMMIT_TEMPLATE = 'docs(session): YYYY-MM-DD-N — <summary>';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalize workflow defaults from either a full project config object or a
|
|
7
|
+
* bare metadata block.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveWorkflowSettings(projectConfigOrMetadata) {
|
|
10
|
+
const metadata = isPlainObject(projectConfigOrMetadata?.metadata)
|
|
11
|
+
? projectConfigOrMetadata.metadata
|
|
12
|
+
: projectConfigOrMetadata;
|
|
13
|
+
const workflow = isPlainObject(metadata?.workflow) ? metadata.workflow : {};
|
|
14
|
+
const closeSession = isPlainObject(workflow.close_session) ? workflow.close_session : {};
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
reviewerHandback: normalizeReviewerHandback(workflow.reviewer_handback),
|
|
18
|
+
closeSession: {
|
|
19
|
+
refreshArchitectureDocs: closeSession.refresh_architecture_docs !== false,
|
|
20
|
+
refreshDecisionFiles: closeSession.refresh_decision_files !== false,
|
|
21
|
+
gitPublish: closeSession.git_publish === true,
|
|
22
|
+
gitRemote: normalizeOptionalString(closeSession.git_remote) ?? DEFAULT_GIT_REMOTE,
|
|
23
|
+
commitTemplate: normalizeOptionalString(closeSession.commit_template) ?? DEFAULT_COMMIT_TEMPLATE,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildWorkflowMetadata(existingMetadata, overrides = {}) {
|
|
29
|
+
const metadata = isPlainObject(existingMetadata) ? existingMetadata : {};
|
|
30
|
+
const existingWorkflow = isPlainObject(metadata.workflow) ? metadata.workflow : {};
|
|
31
|
+
const existingCloseSession = isPlainObject(existingWorkflow.close_session)
|
|
32
|
+
? existingWorkflow.close_session
|
|
33
|
+
: {};
|
|
34
|
+
const resolved = resolveWorkflowSettings(metadata);
|
|
35
|
+
|
|
36
|
+
const gitPublish =
|
|
37
|
+
typeof overrides.gitPublish === 'boolean'
|
|
38
|
+
? overrides.gitPublish
|
|
39
|
+
: resolved.closeSession.gitPublish;
|
|
40
|
+
const gitRemote =
|
|
41
|
+
normalizeOptionalString(overrides.gitRemote) ??
|
|
42
|
+
normalizeOptionalString(existingCloseSession.git_remote) ??
|
|
43
|
+
resolved.closeSession.gitRemote;
|
|
44
|
+
const commitTemplate =
|
|
45
|
+
normalizeOptionalString(overrides.commitTemplate) ??
|
|
46
|
+
normalizeOptionalString(existingCloseSession.commit_template) ??
|
|
47
|
+
resolved.closeSession.commitTemplate;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...metadata,
|
|
51
|
+
workflow: {
|
|
52
|
+
...existingWorkflow,
|
|
53
|
+
reviewer_handback:
|
|
54
|
+
normalizeOptionalString(overrides.reviewerHandback) ??
|
|
55
|
+
normalizeOptionalString(existingWorkflow.reviewer_handback) ??
|
|
56
|
+
resolved.reviewerHandback,
|
|
57
|
+
close_session: {
|
|
58
|
+
...existingCloseSession,
|
|
59
|
+
refresh_architecture_docs:
|
|
60
|
+
typeof overrides.refreshArchitectureDocs === 'boolean'
|
|
61
|
+
? overrides.refreshArchitectureDocs
|
|
62
|
+
: resolved.closeSession.refreshArchitectureDocs,
|
|
63
|
+
refresh_decision_files:
|
|
64
|
+
typeof overrides.refreshDecisionFiles === 'boolean'
|
|
65
|
+
? overrides.refreshDecisionFiles
|
|
66
|
+
: resolved.closeSession.refreshDecisionFiles,
|
|
67
|
+
git_publish: gitPublish,
|
|
68
|
+
...(gitPublish || normalizeOptionalString(existingCloseSession.git_remote) || normalizeOptionalString(overrides.gitRemote)
|
|
69
|
+
? { git_remote: gitRemote }
|
|
70
|
+
: {}),
|
|
71
|
+
commit_template: commitTemplate,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildCloseSessionGuidance(workflowSettings) {
|
|
78
|
+
const guidance = [];
|
|
79
|
+
|
|
80
|
+
guidance.push('Reviewer handback stays in sessions/wip.md and sessions/handoff.md before close-session runs.');
|
|
81
|
+
|
|
82
|
+
if (workflowSettings.closeSession.refreshArchitectureDocs) {
|
|
83
|
+
guidance.push('Refresh any relevant architecture docs before finalizing the session.');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (workflowSettings.closeSession.refreshDecisionFiles) {
|
|
87
|
+
guidance.push('Refresh any relevant decision files before finalizing the session.');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (workflowSettings.closeSession.gitPublish) {
|
|
91
|
+
guidance.push(
|
|
92
|
+
`This workflow includes a Git publish step after close-session; review, commit, and push to ${workflowSettings.closeSession.gitRemote}.`,
|
|
93
|
+
);
|
|
94
|
+
guidance.push(`Use commit message format: ${workflowSettings.closeSession.commitTemplate}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return guidance;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeReviewerHandback(value) {
|
|
101
|
+
const normalized = normalizeOptionalString(value);
|
|
102
|
+
if (!normalized) {
|
|
103
|
+
return DEFAULT_REVIEWER_HANDBACK;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return normalized;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeOptionalString(value) {
|
|
110
|
+
if (typeof value !== 'string') {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const trimmed = value.trim();
|
|
115
|
+
return trimmed ? trimmed : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isPlainObject(value) {
|
|
119
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
120
|
+
}
|