@hanzlaa/rcode 3.4.7 → 3.4.9
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/cli/doctor.js +3 -1
- package/cli/github-sync.js +8 -4
- package/cli/install.js +43 -5
- package/cli/lib/config.cjs +31 -1
- package/cli/lib/fsutil.cjs +3 -2
- package/cli/lib/manifest.cjs +9 -11
- package/cli/postinstall.js +16 -4
- package/dist/rcode.js +74 -17
- package/package.json +1 -1
- package/rihal/agents/rules/executor/task-commit-protocol.md +9 -7
- package/rihal/references/auto-init-guard.md +117 -0
- package/rihal/workflows/council.md +1 -0
- package/rihal/workflows/do.md +6 -0
- package/rihal/workflows/execute.md +1 -0
- package/rihal/workflows/init.md +2 -2
- package/rihal/workflows/new-project.md +1 -0
- package/rihal/workflows/plan.md +1 -0
- package/rihal/workflows/status.md +1 -0
package/cli/doctor.js
CHANGED
|
@@ -54,7 +54,9 @@ function isWritable(dir) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function commandAvailable(cmd) {
|
|
57
|
-
|
|
57
|
+
// 'which' is POSIX-only; 'where' is Windows. Try both so this works cross-platform.
|
|
58
|
+
const checker = process.platform === 'win32' ? 'where' : 'which';
|
|
59
|
+
const result = spawnSync(checker, [cmd], { stdio: 'ignore' });
|
|
58
60
|
return result.status === 0;
|
|
59
61
|
}
|
|
60
62
|
|
package/cli/github-sync.js
CHANGED
|
@@ -649,6 +649,7 @@ async function main(args) {
|
|
|
649
649
|
results.errors.push(`label ${label.name}: ${result.error}`);
|
|
650
650
|
} else if (!result.dryRun) {
|
|
651
651
|
if (!syncMap.labels.includes(label.name)) syncMap.labels.push(label.name);
|
|
652
|
+
if (opts.execute) saveSyncMap(cwd, syncMap);
|
|
652
653
|
console.log(` ✓ ${result.existed ? 'exists' : 'created'}: ${label.name}`);
|
|
653
654
|
}
|
|
654
655
|
}
|
|
@@ -670,6 +671,7 @@ async function main(args) {
|
|
|
670
671
|
url: result.url,
|
|
671
672
|
synced_at: new Date().toISOString(),
|
|
672
673
|
};
|
|
674
|
+
if (opts.execute) saveSyncMap(cwd, syncMap);
|
|
673
675
|
console.log(` ✓ created: ${phase.id} → milestone #${result.number}`);
|
|
674
676
|
}
|
|
675
677
|
}
|
|
@@ -686,7 +688,7 @@ async function main(args) {
|
|
|
686
688
|
``,
|
|
687
689
|
`## 📋 Source Content`,
|
|
688
690
|
``,
|
|
689
|
-
epic.content.slice(0,
|
|
691
|
+
epic.content.slice(0, 60000),
|
|
690
692
|
``,
|
|
691
693
|
`---`,
|
|
692
694
|
``,
|
|
@@ -727,6 +729,7 @@ async function main(args) {
|
|
|
727
729
|
content_hash: contentHash(epic.content),
|
|
728
730
|
child_story_issues: [],
|
|
729
731
|
};
|
|
732
|
+
if (opts.execute) saveSyncMap(cwd, syncMap);
|
|
730
733
|
console.log(` ✓ created: ${epic.id} → #${result.number}`);
|
|
731
734
|
}
|
|
732
735
|
}
|
|
@@ -766,7 +769,7 @@ async function main(args) {
|
|
|
766
769
|
``,
|
|
767
770
|
`## 📋 Source Content`,
|
|
768
771
|
``,
|
|
769
|
-
story.content.slice(0,
|
|
772
|
+
story.content.slice(0, 60000),
|
|
770
773
|
``,
|
|
771
774
|
`---`,
|
|
772
775
|
``,
|
|
@@ -807,6 +810,7 @@ async function main(args) {
|
|
|
807
810
|
synced_at: new Date().toISOString(),
|
|
808
811
|
content_hash: contentHash(story.content),
|
|
809
812
|
};
|
|
813
|
+
if (opts.execute) saveSyncMap(cwd, syncMap);
|
|
810
814
|
// Remember this child on the parent epic so we can update the
|
|
811
815
|
// epic body with a task list after all stories have been created.
|
|
812
816
|
if (parentEpicEntry) {
|
|
@@ -885,7 +889,7 @@ async function main(args) {
|
|
|
885
889
|
``,
|
|
886
890
|
`## 📋 Source Content`,
|
|
887
891
|
``,
|
|
888
|
-
epic.content.slice(0,
|
|
892
|
+
epic.content.slice(0, 60000),
|
|
889
893
|
``,
|
|
890
894
|
`---`,
|
|
891
895
|
``,
|
|
@@ -930,7 +934,7 @@ async function main(args) {
|
|
|
930
934
|
``,
|
|
931
935
|
`## 📋 Source Content`,
|
|
932
936
|
``,
|
|
933
|
-
story.content.slice(0,
|
|
937
|
+
story.content.slice(0, 60000),
|
|
934
938
|
``,
|
|
935
939
|
`---`,
|
|
936
940
|
``,
|
package/cli/install.js
CHANGED
|
@@ -690,7 +690,13 @@ function ensureRcodeGitignore(target, options = {}) {
|
|
|
690
690
|
const start = text.indexOf(BEGIN);
|
|
691
691
|
if (start < 0) return null;
|
|
692
692
|
const endIdx = text.indexOf(END, start);
|
|
693
|
-
|
|
693
|
+
// If BEGIN exists but END is missing (manual edit removed it), strip
|
|
694
|
+
// everything from BEGIN to EOF and rewrite — avoids duplicate blocks.
|
|
695
|
+
if (endIdx < 0) {
|
|
696
|
+
let sliceStart = start;
|
|
697
|
+
if (sliceStart > 0 && text[sliceStart - 1] === '\n') sliceStart -= 1;
|
|
698
|
+
return text.slice(0, sliceStart) + newBlock;
|
|
699
|
+
}
|
|
694
700
|
let sliceStart = start;
|
|
695
701
|
if (sliceStart > 0 && text[sliceStart - 1] === '\n') sliceStart -= 1;
|
|
696
702
|
let sliceEnd = endIdx + END.length;
|
|
@@ -765,7 +771,12 @@ function ensureRcodePreCommitHook(target, options = {}) {
|
|
|
765
771
|
const start = text.indexOf(BEGIN);
|
|
766
772
|
if (start < 0) return null;
|
|
767
773
|
const endIdx = text.indexOf(END, start);
|
|
768
|
-
|
|
774
|
+
// If BEGIN exists but END is missing, strip from BEGIN to EOF and rewrite.
|
|
775
|
+
if (endIdx < 0) {
|
|
776
|
+
let sliceStart = start;
|
|
777
|
+
if (sliceStart > 0 && text[sliceStart - 1] === '\n') sliceStart -= 1;
|
|
778
|
+
return text.slice(0, sliceStart) + newBlock;
|
|
779
|
+
}
|
|
769
780
|
let sliceStart = start;
|
|
770
781
|
if (sliceStart > 0 && text[sliceStart - 1] === '\n') sliceStart -= 1;
|
|
771
782
|
let sliceEnd = endIdx + END.length;
|
|
@@ -1131,14 +1142,41 @@ function generateAgentManifest(plan, target) {
|
|
|
1131
1142
|
* Generate files-manifest.csv with SHA256 per installed file. Used by
|
|
1132
1143
|
* update/doctor to detect drift. Columns: rel, sha256, size.
|
|
1133
1144
|
*/
|
|
1134
|
-
function generateFilesManifest(plan, target) {
|
|
1145
|
+
function generateFilesManifest(plan, target, { mergeExistingManifest = false } = {}) {
|
|
1135
1146
|
const rows = [['rel', 'sha256', 'size']];
|
|
1147
|
+
const newRels = new Set();
|
|
1148
|
+
|
|
1136
1149
|
for (const entry of plan) {
|
|
1137
1150
|
const filePath = path.join(target, entry.rel);
|
|
1138
1151
|
if (!fs.existsSync(filePath)) continue;
|
|
1139
1152
|
const buf = fs.readFileSync(filePath);
|
|
1140
|
-
|
|
1153
|
+
const rel = entry.rel.split(path.sep).join('/');
|
|
1154
|
+
rows.push([rel, sha256(buf), String(buf.length)]);
|
|
1155
|
+
newRels.add(rel);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Merge old manifest entries that are still on disk but not in the current
|
|
1159
|
+
// plan — this keeps orphaned files traceable by doctor/uninstall even when
|
|
1160
|
+
// --force sweep was not run. Without this, a re-install without --force
|
|
1161
|
+
// silently drops stale files from the manifest, making them invisible.
|
|
1162
|
+
if (mergeExistingManifest) {
|
|
1163
|
+
const manifestPath = path.join(target, '.rihal', '_config', 'files-manifest.csv');
|
|
1164
|
+
if (fs.existsSync(manifestPath)) {
|
|
1165
|
+
try {
|
|
1166
|
+
const oldRows = fs.readFileSync(manifestPath, 'utf8').split('\n').slice(1).filter(Boolean);
|
|
1167
|
+
for (const row of oldRows) {
|
|
1168
|
+
const [rel] = row.split(',');
|
|
1169
|
+
if (!rel || newRels.has(rel)) continue;
|
|
1170
|
+
const full = path.join(target, rel);
|
|
1171
|
+
if (!fs.existsSync(full)) continue; // already gone — don't re-add
|
|
1172
|
+
const buf = fs.readFileSync(full);
|
|
1173
|
+
rows.push([rel, sha256(buf), String(buf.length)]);
|
|
1174
|
+
newRels.add(rel);
|
|
1175
|
+
}
|
|
1176
|
+
} catch { /* best-effort */ }
|
|
1177
|
+
}
|
|
1141
1178
|
}
|
|
1179
|
+
|
|
1142
1180
|
return rows.map((r) => r.join(',')).join('\n') + '\n';
|
|
1143
1181
|
}
|
|
1144
1182
|
|
|
@@ -1764,7 +1802,7 @@ async function install(opts) {
|
|
|
1764
1802
|
// self-referential nonsense).
|
|
1765
1803
|
fs.writeFileSync(
|
|
1766
1804
|
path.join(configDir, 'files-manifest.csv'),
|
|
1767
|
-
generateFilesManifest(plan, opts.target),
|
|
1805
|
+
generateFilesManifest(plan, opts.target, { mergeExistingManifest: !opts.force }),
|
|
1768
1806
|
);
|
|
1769
1807
|
|
|
1770
1808
|
// Install v1-style phrase-activated skills (scaffold-project, create-prd,
|
package/cli/lib/config.cjs
CHANGED
|
@@ -109,6 +109,10 @@ function projectLevelPath(cwd) {
|
|
|
109
109
|
return path.join(cwd, '.rihal', 'config.json');
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function projectYamlPath(cwd) {
|
|
113
|
+
return path.join(cwd, '.rihal', 'config.yaml');
|
|
114
|
+
}
|
|
115
|
+
|
|
112
116
|
// ---------- Loaders ----------
|
|
113
117
|
|
|
114
118
|
function readJsonSafe(filePath) {
|
|
@@ -126,8 +130,33 @@ function loadUserDefaults() {
|
|
|
126
130
|
return readJsonSafe(userLevelPath()) || {};
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
// YAML key → JSON key remapping (install.js writes YAML with some different names)
|
|
134
|
+
const YAML_KEY_MAP = {
|
|
135
|
+
mode: 'communication_mode',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
function readYamlFlat(filePath) {
|
|
139
|
+
if (!fs.existsSync(filePath)) return null;
|
|
140
|
+
try {
|
|
141
|
+
const result = {};
|
|
142
|
+
for (const raw of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
|
143
|
+
const m = raw.match(/^([a-zA-Z_]+):\s*["']?([^"'\n#]+?)["']?\s*(?:#.*)?$/);
|
|
144
|
+
if (!m) continue;
|
|
145
|
+
const key = YAML_KEY_MAP[m[1].trim()] || m[1].trim();
|
|
146
|
+
const val = m[2].trim();
|
|
147
|
+
result[key] = val === 'true' ? true : val === 'false' ? false : val;
|
|
148
|
+
}
|
|
149
|
+
return Object.keys(result).length ? result : null;
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
129
155
|
function loadProjectConfig(cwd) {
|
|
130
|
-
|
|
156
|
+
const json = readJsonSafe(projectLevelPath(cwd));
|
|
157
|
+
if (json) return json;
|
|
158
|
+
// Fall back to config.yaml written by installer — migrate keys on the fly
|
|
159
|
+
return readYamlFlat(projectYamlPath(cwd)) || {};
|
|
131
160
|
}
|
|
132
161
|
|
|
133
162
|
/**
|
|
@@ -330,5 +359,6 @@ module.exports = {
|
|
|
330
359
|
loadProjectConfig,
|
|
331
360
|
userLevelPath,
|
|
332
361
|
projectLevelPath,
|
|
362
|
+
projectYamlPath,
|
|
333
363
|
suggestClosest,
|
|
334
364
|
};
|
package/cli/lib/fsutil.cjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* a partial truncation.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
const crypto = require('crypto');
|
|
12
13
|
const fs = require('fs');
|
|
13
14
|
const path = require('path');
|
|
14
15
|
|
|
@@ -34,10 +35,10 @@ function writeFileAtomic(filePath, content, opts = {}) {
|
|
|
34
35
|
const dir = path.dirname(filePath);
|
|
35
36
|
fs.mkdirSync(dir, { recursive: true });
|
|
36
37
|
|
|
37
|
-
// Include pid +
|
|
38
|
+
// Include pid + crypto random so concurrent processes don't collide on tmp.
|
|
38
39
|
const tmpPath = path.join(
|
|
39
40
|
dir,
|
|
40
|
-
`.${path.basename(filePath)}.tmp-${process.pid}-${
|
|
41
|
+
`.${path.basename(filePath)}.tmp-${process.pid}-${crypto.randomBytes(8).toString('hex')}`
|
|
41
42
|
);
|
|
42
43
|
|
|
43
44
|
let fd;
|
package/cli/lib/manifest.cjs
CHANGED
|
@@ -108,24 +108,22 @@ function verifyClaudeInstall(cwd, packageRoot) {
|
|
|
108
108
|
const pkg = readPackageManifest(packageRoot);
|
|
109
109
|
const skillsDir = path.join(cwd, '.claude/skills');
|
|
110
110
|
|
|
111
|
-
// Agents are installed as rihal-{name}
|
|
111
|
+
// Agents are installed as rihal-{name} — strip prefix to match pkg.agents keys
|
|
112
112
|
const installedAgents = readInstalledDirs(skillsDir, 'rihal-');
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
const agentsInstalled = new Set(
|
|
118
|
-
[...installedAgents].filter((n) => pkg.agents.has(n))
|
|
119
|
-
);
|
|
113
|
+
// Do NOT pre-filter against pkg.agents: we want stale entries (installed but
|
|
114
|
+
// not in current package) to appear in the `extra` list of diffSet so that
|
|
115
|
+
// `rcode doctor` can flag them as stale and `rcode uninstall` can remove them.
|
|
116
|
+
// The old intersection filter was hiding orphaned agent dirs after version bumps.
|
|
120
117
|
|
|
121
|
-
// Action skills: installed with their bare name (no rihal- prefix)
|
|
118
|
+
// Action skills: installed with their bare name (no rihal- prefix).
|
|
119
|
+
// Exclude known agent dirs (rihal-prefixed) so actions and agents don't bleed.
|
|
122
120
|
const allInstalled = readInstalledDirs(skillsDir);
|
|
123
121
|
const actionsInstalled = new Set(
|
|
124
|
-
[...allInstalled].filter((n) =>
|
|
122
|
+
[...allInstalled].filter((n) => !n.startsWith('rihal-'))
|
|
125
123
|
);
|
|
126
124
|
|
|
127
125
|
return [
|
|
128
|
-
diffSet('claude', 'agents', pkg.agents,
|
|
126
|
+
diffSet('claude', 'agents', pkg.agents, installedAgents),
|
|
129
127
|
diffSet('claude', 'actions', pkg.actions, actionsInstalled),
|
|
130
128
|
];
|
|
131
129
|
}
|
package/cli/postinstall.js
CHANGED
|
@@ -24,10 +24,22 @@ const isGlobalInstall = (() => {
|
|
|
24
24
|
try {
|
|
25
25
|
// npm sets npm_config_global=true for global installs
|
|
26
26
|
if (process.env.npm_config_global === 'true') return true;
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
// pnpm sets npm_config_global too, but check PNPM_HOME as a fallback
|
|
28
|
+
if (process.env.PNPM_HOME && __dirname.startsWith(process.env.PNPM_HOME)) return true;
|
|
29
|
+
// Check if __dirname is inside a known global node_modules path.
|
|
30
|
+
// Covers: /usr/local/lib, /usr/lib, ~/.nvm/.../lib, ~/.pnpm/..., ~/.yarn/...
|
|
31
|
+
const globalPatterns = [
|
|
32
|
+
/\/node_modules\/@hanzlaa\/rcode/, // any global node_modules
|
|
33
|
+
/[/\\]lib[/\\]node_modules[/\\]/, // /usr/local/lib/node_modules
|
|
34
|
+
/\.nvm[/\\]versions[/\\]/, // nvm
|
|
35
|
+
/\.pnpm[/\\]/, // pnpm global store
|
|
36
|
+
/\.yarn[/\\]global/, // yarn global
|
|
37
|
+
];
|
|
38
|
+
if (globalPatterns.some((re) => re.test(__dirname))) return true;
|
|
39
|
+
// Last resort: package is NOT inside a project's local node_modules
|
|
40
|
+
// (local installs have .../project/node_modules/@hanzlaa/rcode/cli)
|
|
41
|
+
const localNodeModules = path.join(process.cwd(), 'node_modules');
|
|
42
|
+
if (!__dirname.startsWith(localNodeModules)) return true;
|
|
31
43
|
return false;
|
|
32
44
|
} catch {
|
|
33
45
|
return false;
|
package/dist/rcode.js
CHANGED
|
@@ -15456,7 +15456,11 @@ Say "plan a sprint" or run \`/rihal-sprint-planning\` to break Phase 01 into sto
|
|
|
15456
15456
|
const start = text.indexOf(BEGIN);
|
|
15457
15457
|
if (start < 0) return null;
|
|
15458
15458
|
const endIdx = text.indexOf(END, start);
|
|
15459
|
-
if (endIdx < 0)
|
|
15459
|
+
if (endIdx < 0) {
|
|
15460
|
+
let sliceStart2 = start;
|
|
15461
|
+
if (sliceStart2 > 0 && text[sliceStart2 - 1] === "\n") sliceStart2 -= 1;
|
|
15462
|
+
return text.slice(0, sliceStart2) + newBlock;
|
|
15463
|
+
}
|
|
15460
15464
|
let sliceStart = start;
|
|
15461
15465
|
if (sliceStart > 0 && text[sliceStart - 1] === "\n") sliceStart -= 1;
|
|
15462
15466
|
let sliceEnd = endIdx + END.length;
|
|
@@ -15512,7 +15516,11 @@ Say "plan a sprint" or run \`/rihal-sprint-planning\` to break Phase 01 into sto
|
|
|
15512
15516
|
const start = text.indexOf(BEGIN);
|
|
15513
15517
|
if (start < 0) return null;
|
|
15514
15518
|
const endIdx = text.indexOf(END, start);
|
|
15515
|
-
if (endIdx < 0)
|
|
15519
|
+
if (endIdx < 0) {
|
|
15520
|
+
let sliceStart2 = start;
|
|
15521
|
+
if (sliceStart2 > 0 && text[sliceStart2 - 1] === "\n") sliceStart2 -= 1;
|
|
15522
|
+
return text.slice(0, sliceStart2) + newBlock;
|
|
15523
|
+
}
|
|
15516
15524
|
let sliceStart = start;
|
|
15517
15525
|
if (sliceStart > 0 && text[sliceStart - 1] === "\n") sliceStart -= 1;
|
|
15518
15526
|
let sliceEnd = endIdx + END.length;
|
|
@@ -15785,13 +15793,34 @@ ${BLOCK}`);
|
|
|
15785
15793
|
}
|
|
15786
15794
|
return rows.map((r) => r.join(",")).join("\n") + "\n";
|
|
15787
15795
|
}
|
|
15788
|
-
function generateFilesManifest(plan, target) {
|
|
15796
|
+
function generateFilesManifest(plan, target, { mergeExistingManifest = false } = {}) {
|
|
15789
15797
|
const rows = [["rel", "sha256", "size"]];
|
|
15798
|
+
const newRels = /* @__PURE__ */ new Set();
|
|
15790
15799
|
for (const entry of plan) {
|
|
15791
15800
|
const filePath = path2.join(target, entry.rel);
|
|
15792
15801
|
if (!fs2.existsSync(filePath)) continue;
|
|
15793
15802
|
const buf = fs2.readFileSync(filePath);
|
|
15794
|
-
|
|
15803
|
+
const rel = entry.rel.split(path2.sep).join("/");
|
|
15804
|
+
rows.push([rel, sha256(buf), String(buf.length)]);
|
|
15805
|
+
newRels.add(rel);
|
|
15806
|
+
}
|
|
15807
|
+
if (mergeExistingManifest) {
|
|
15808
|
+
const manifestPath = path2.join(target, ".rihal", "_config", "files-manifest.csv");
|
|
15809
|
+
if (fs2.existsSync(manifestPath)) {
|
|
15810
|
+
try {
|
|
15811
|
+
const oldRows = fs2.readFileSync(manifestPath, "utf8").split("\n").slice(1).filter(Boolean);
|
|
15812
|
+
for (const row of oldRows) {
|
|
15813
|
+
const [rel] = row.split(",");
|
|
15814
|
+
if (!rel || newRels.has(rel)) continue;
|
|
15815
|
+
const full = path2.join(target, rel);
|
|
15816
|
+
if (!fs2.existsSync(full)) continue;
|
|
15817
|
+
const buf = fs2.readFileSync(full);
|
|
15818
|
+
rows.push([rel, sha256(buf), String(buf.length)]);
|
|
15819
|
+
newRels.add(rel);
|
|
15820
|
+
}
|
|
15821
|
+
} catch {
|
|
15822
|
+
}
|
|
15823
|
+
}
|
|
15795
15824
|
}
|
|
15796
15825
|
return rows.map((r) => r.join(",")).join("\n") + "\n";
|
|
15797
15826
|
}
|
|
@@ -16296,7 +16325,7 @@ ${BLOCK}`);
|
|
|
16296
16325
|
ensureDir(globalAgentsDir);
|
|
16297
16326
|
fs2.writeFileSync(
|
|
16298
16327
|
path2.join(configDir, "files-manifest.csv"),
|
|
16299
|
-
generateFilesManifest(plan, opts.target)
|
|
16328
|
+
generateFilesManifest(plan, opts.target, { mergeExistingManifest: !opts.force })
|
|
16300
16329
|
);
|
|
16301
16330
|
let skillsInstalled = installSkills(PACKAGE_ROOT2, opts.target);
|
|
16302
16331
|
try {
|
|
@@ -16867,6 +16896,7 @@ var require_prompts = __commonJS({
|
|
|
16867
16896
|
// cli/lib/fsutil.cjs
|
|
16868
16897
|
var require_fsutil = __commonJS({
|
|
16869
16898
|
"cli/lib/fsutil.cjs"(exports2, module2) {
|
|
16899
|
+
var crypto = require("crypto");
|
|
16870
16900
|
var fs2 = require("fs");
|
|
16871
16901
|
var path2 = require("path");
|
|
16872
16902
|
function writeFileAtomic(filePath, content, opts = {}) {
|
|
@@ -16875,7 +16905,7 @@ var require_fsutil = __commonJS({
|
|
|
16875
16905
|
fs2.mkdirSync(dir, { recursive: true });
|
|
16876
16906
|
const tmpPath = path2.join(
|
|
16877
16907
|
dir,
|
|
16878
|
-
`.${path2.basename(filePath)}.tmp-${process.pid}-${
|
|
16908
|
+
`.${path2.basename(filePath)}.tmp-${process.pid}-${crypto.randomBytes(8).toString("hex")}`
|
|
16879
16909
|
);
|
|
16880
16910
|
let fd;
|
|
16881
16911
|
try {
|
|
@@ -16966,15 +16996,12 @@ var require_manifest = __commonJS({
|
|
|
16966
16996
|
const pkg = readPackageManifest(packageRoot);
|
|
16967
16997
|
const skillsDir = path2.join(cwd, ".claude/skills");
|
|
16968
16998
|
const installedAgents = readInstalledDirs(skillsDir, "rihal-");
|
|
16969
|
-
const agentsInstalled = new Set(
|
|
16970
|
-
[...installedAgents].filter((n) => pkg.agents.has(n))
|
|
16971
|
-
);
|
|
16972
16999
|
const allInstalled = readInstalledDirs(skillsDir);
|
|
16973
17000
|
const actionsInstalled = new Set(
|
|
16974
|
-
[...allInstalled].filter((n) =>
|
|
17001
|
+
[...allInstalled].filter((n) => !n.startsWith("rihal-"))
|
|
16975
17002
|
);
|
|
16976
17003
|
return [
|
|
16977
|
-
diffSet("claude", "agents", pkg.agents,
|
|
17004
|
+
diffSet("claude", "agents", pkg.agents, installedAgents),
|
|
16978
17005
|
diffSet("claude", "actions", pkg.actions, actionsInstalled)
|
|
16979
17006
|
];
|
|
16980
17007
|
}
|
|
@@ -18242,7 +18269,8 @@ var require_doctor = __commonJS({
|
|
|
18242
18269
|
}
|
|
18243
18270
|
}
|
|
18244
18271
|
function commandAvailable(cmd) {
|
|
18245
|
-
const
|
|
18272
|
+
const checker = process.platform === "win32" ? "where" : "which";
|
|
18273
|
+
const result = spawnSync(checker, [cmd], { stdio: "ignore" });
|
|
18246
18274
|
return result.status === 0;
|
|
18247
18275
|
}
|
|
18248
18276
|
function runPreflight(cwd, packageRoot) {
|
|
@@ -18606,6 +18634,9 @@ var require_config = __commonJS({
|
|
|
18606
18634
|
function projectLevelPath(cwd) {
|
|
18607
18635
|
return path2.join(cwd, ".rihal", "config.json");
|
|
18608
18636
|
}
|
|
18637
|
+
function projectYamlPath(cwd) {
|
|
18638
|
+
return path2.join(cwd, ".rihal", "config.yaml");
|
|
18639
|
+
}
|
|
18609
18640
|
function readJsonSafe(filePath) {
|
|
18610
18641
|
if (!fs2.existsSync(filePath)) return null;
|
|
18611
18642
|
try {
|
|
@@ -18617,8 +18648,29 @@ var require_config = __commonJS({
|
|
|
18617
18648
|
function loadUserDefaults() {
|
|
18618
18649
|
return readJsonSafe(userLevelPath()) || {};
|
|
18619
18650
|
}
|
|
18651
|
+
var YAML_KEY_MAP = {
|
|
18652
|
+
mode: "communication_mode"
|
|
18653
|
+
};
|
|
18654
|
+
function readYamlFlat(filePath) {
|
|
18655
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
18656
|
+
try {
|
|
18657
|
+
const result = {};
|
|
18658
|
+
for (const raw of fs2.readFileSync(filePath, "utf8").split("\n")) {
|
|
18659
|
+
const m = raw.match(/^([a-zA-Z_]+):\s*["']?([^"'\n#]+?)["']?\s*(?:#.*)?$/);
|
|
18660
|
+
if (!m) continue;
|
|
18661
|
+
const key = YAML_KEY_MAP[m[1].trim()] || m[1].trim();
|
|
18662
|
+
const val = m[2].trim();
|
|
18663
|
+
result[key] = val === "true" ? true : val === "false" ? false : val;
|
|
18664
|
+
}
|
|
18665
|
+
return Object.keys(result).length ? result : null;
|
|
18666
|
+
} catch {
|
|
18667
|
+
return null;
|
|
18668
|
+
}
|
|
18669
|
+
}
|
|
18620
18670
|
function loadProjectConfig(cwd) {
|
|
18621
|
-
|
|
18671
|
+
const json = readJsonSafe(projectLevelPath(cwd));
|
|
18672
|
+
if (json) return json;
|
|
18673
|
+
return readYamlFlat(projectYamlPath(cwd)) || {};
|
|
18622
18674
|
}
|
|
18623
18675
|
function loadConfig(cwd) {
|
|
18624
18676
|
const defaults = { ...HARDCODED_DEFAULTS };
|
|
@@ -18749,6 +18801,7 @@ var require_config = __commonJS({
|
|
|
18749
18801
|
loadProjectConfig,
|
|
18750
18802
|
userLevelPath,
|
|
18751
18803
|
projectLevelPath,
|
|
18804
|
+
projectYamlPath,
|
|
18752
18805
|
suggestClosest
|
|
18753
18806
|
};
|
|
18754
18807
|
}
|
|
@@ -19936,6 +19989,7 @@ var require_github_sync = __commonJS({
|
|
|
19936
19989
|
results.errors.push(`label ${label.name}: ${result.error}`);
|
|
19937
19990
|
} else if (!result.dryRun) {
|
|
19938
19991
|
if (!syncMap.labels.includes(label.name)) syncMap.labels.push(label.name);
|
|
19992
|
+
if (opts.execute) saveSyncMap(cwd, syncMap);
|
|
19939
19993
|
console.log(` \u2713 ${result.existed ? "exists" : "created"}: ${label.name}`);
|
|
19940
19994
|
}
|
|
19941
19995
|
}
|
|
@@ -19956,6 +20010,7 @@ var require_github_sync = __commonJS({
|
|
|
19956
20010
|
url: result.url,
|
|
19957
20011
|
synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
19958
20012
|
};
|
|
20013
|
+
if (opts.execute) saveSyncMap(cwd, syncMap);
|
|
19959
20014
|
console.log(` \u2713 created: ${phase.id} \u2192 milestone #${result.number}`);
|
|
19960
20015
|
}
|
|
19961
20016
|
}
|
|
@@ -19971,7 +20026,7 @@ var require_github_sync = __commonJS({
|
|
|
19971
20026
|
``,
|
|
19972
20027
|
`## \u{1F4CB} Source Content`,
|
|
19973
20028
|
``,
|
|
19974
|
-
epic.content.slice(0,
|
|
20029
|
+
epic.content.slice(0, 6e4),
|
|
19975
20030
|
``,
|
|
19976
20031
|
`---`,
|
|
19977
20032
|
``,
|
|
@@ -20009,6 +20064,7 @@ var require_github_sync = __commonJS({
|
|
|
20009
20064
|
content_hash: contentHash(epic.content),
|
|
20010
20065
|
child_story_issues: []
|
|
20011
20066
|
};
|
|
20067
|
+
if (opts.execute) saveSyncMap(cwd, syncMap);
|
|
20012
20068
|
console.log(` \u2713 created: ${epic.id} \u2192 #${result.number}`);
|
|
20013
20069
|
}
|
|
20014
20070
|
}
|
|
@@ -20033,7 +20089,7 @@ var require_github_sync = __commonJS({
|
|
|
20033
20089
|
``,
|
|
20034
20090
|
`## \u{1F4CB} Source Content`,
|
|
20035
20091
|
``,
|
|
20036
|
-
story.content.slice(0,
|
|
20092
|
+
story.content.slice(0, 6e4),
|
|
20037
20093
|
``,
|
|
20038
20094
|
`---`,
|
|
20039
20095
|
``,
|
|
@@ -20071,6 +20127,7 @@ var require_github_sync = __commonJS({
|
|
|
20071
20127
|
synced_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20072
20128
|
content_hash: contentHash(story.content)
|
|
20073
20129
|
};
|
|
20130
|
+
if (opts.execute) saveSyncMap(cwd, syncMap);
|
|
20074
20131
|
if (parentEpicEntry) {
|
|
20075
20132
|
parentEpicEntry.child_story_issues = parentEpicEntry.child_story_issues || [];
|
|
20076
20133
|
if (!parentEpicEntry.child_story_issues.includes(result.number)) {
|
|
@@ -20137,7 +20194,7 @@ ${taskList}
|
|
|
20137
20194
|
``,
|
|
20138
20195
|
`## \u{1F4CB} Source Content`,
|
|
20139
20196
|
``,
|
|
20140
|
-
epic.content.slice(0,
|
|
20197
|
+
epic.content.slice(0, 6e4),
|
|
20141
20198
|
``,
|
|
20142
20199
|
`---`,
|
|
20143
20200
|
``,
|
|
@@ -20179,7 +20236,7 @@ ${taskList}
|
|
|
20179
20236
|
``,
|
|
20180
20237
|
`## \u{1F4CB} Source Content`,
|
|
20181
20238
|
``,
|
|
20182
|
-
story.content.slice(0,
|
|
20239
|
+
story.content.slice(0, 6e4),
|
|
20183
20240
|
``,
|
|
20184
20241
|
`---`,
|
|
20185
20242
|
``,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.9",
|
|
4
4
|
"description": "rcode — the memory bank for AI-driven SaaS teams. Persistent project context, distinctive engineering personas, and phase-based workflows. Built by Rihal. Works in Claude Code, Cursor, Gemini, VS Code, and Antigravity.",
|
|
5
5
|
"main": "cli/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,13 +10,15 @@ After completing a task's implementation, follow this protocol to atomically com
|
|
|
10
10
|
|
|
11
11
|
Before writing any commit messages, read the project's commit standard from source — do NOT invent one or ask the user unless nothing is found.
|
|
12
12
|
|
|
13
|
-
**Check in this order (stop at first hit):**
|
|
14
|
-
|
|
15
|
-
1. `.
|
|
16
|
-
2. `
|
|
17
|
-
3. `.commitlintrc`, `.commitlintrc.json`, `.commitlintrc.yaml`, `commitlint.config.js`, `commitlint.config.cjs` — commitlint config
|
|
18
|
-
4. `package.json` → `"commitlint"` key
|
|
19
|
-
5. `.
|
|
13
|
+
**Check in this order — enforcement configs first, docs last (stop at first hit):**
|
|
14
|
+
|
|
15
|
+
1. `.git/hooks/commit-msg` — the active enforcement hook (if exists and non-empty, read it to detect the validator)
|
|
16
|
+
2. `.husky/commit-msg` — husky-managed commit hook
|
|
17
|
+
3. `.commitlintrc`, `.commitlintrc.json`, `.commitlintrc.yaml`, `.commitlintrc.js`, `commitlint.config.js`, `commitlint.config.cjs` — commitlint config
|
|
18
|
+
4. `package.json` → `"commitlint"` or `"config.commitizen"` key — commitizen config
|
|
19
|
+
5. `.czrc` — commitizen standalone config
|
|
20
|
+
6. `.github/COMMIT_CONVENTION.md` — explicit doc (lower priority than machine configs)
|
|
21
|
+
7. `CONTRIBUTING.md` — look for a "Commit" or "Git" section (lowest priority)
|
|
20
22
|
|
|
21
23
|
**If a standard is found:** Use it silently for all commits in this sprint. No need to confirm with user.
|
|
22
24
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Auto-Init Guard
|
|
2
|
+
|
|
3
|
+
Run this check at the very start of any workflow that needs project state.
|
|
4
|
+
|
|
5
|
+
## Step: Detect project initialization
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
test -f .rihal/config.yaml && echo "rihal-ready" || echo "rihal-not-initialized"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**If `rihal-ready`:** continue with the workflow normally.
|
|
12
|
+
|
|
13
|
+
**If `rihal-not-initialized`:** the user has never set up Rihal for this project. Do NOT fail or continue blindly. Run the project init flow inline before proceeding:
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
### Inline init flow (when config.yaml is missing)
|
|
18
|
+
|
|
19
|
+
Tell the user:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Rihal isn't configured for this project yet. Let me set it up — takes 30 seconds.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**1. Bootstrap local tooling** — copy bin from the global install:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
GLOBAL_RIHAL="$HOME/.rihal"
|
|
29
|
+
TOOLS_SRC="$GLOBAL_RIHAL/bin/rihal-tools.cjs"
|
|
30
|
+
|
|
31
|
+
if [ ! -f "$TOOLS_SRC" ]; then
|
|
32
|
+
echo "ERROR: Global rihal tools not found at $TOOLS_SRC"
|
|
33
|
+
echo "Run: npm install -g @hanzlaa/rcode"
|
|
34
|
+
echo "Then retry this command."
|
|
35
|
+
# STOP — do not continue without tools; writing config.yaml alone is not enough
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
mkdir -p .rihal/bin
|
|
40
|
+
cp "$TOOLS_SRC" .rihal/bin/rihal-tools.cjs
|
|
41
|
+
if [ $? -ne 0 ]; then
|
|
42
|
+
echo "ERROR: Could not copy rihal-tools.cjs to .rihal/bin/ (permission denied?)"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Note: workflows and references are resolved from `~/.rihal/` at runtime — only the bin needs to be local.
|
|
48
|
+
|
|
49
|
+
**2. Ask the 5 config questions** using AskUserQuestion:
|
|
50
|
+
|
|
51
|
+
| # | Question | Options | Default |
|
|
52
|
+
|---|----------|---------|---------|
|
|
53
|
+
| 1 | Your name (what Rihal calls you) | free text | `$USER` |
|
|
54
|
+
| 2 | Language for agent responses | English / Arabic / Urdu / Roman Urdu | English |
|
|
55
|
+
| 3 | Mode (how Rihal handles decision gates) | `guided` / `yolo` | guided |
|
|
56
|
+
| 4 | Model profile (cost vs quality) | `quality` / `balanced` / `budget` | balanced |
|
|
57
|
+
| 5 | Commit planning artifacts to git? | yes / no | yes |
|
|
58
|
+
|
|
59
|
+
**3. Write `.rihal/config.yaml` directly** (no rihal-tools needed — plain Bash):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
mkdir -p .rihal
|
|
63
|
+
cat > .rihal/config.yaml << 'YAML'
|
|
64
|
+
# Rihal project config — edit any time
|
|
65
|
+
user_name: "{name}"
|
|
66
|
+
project_name: "{basename_of_cwd}"
|
|
67
|
+
communication_language: "{lang}"
|
|
68
|
+
mode: "{mode}"
|
|
69
|
+
model_profile: "{profile}"
|
|
70
|
+
commit_planning: {commit_planning_bool}
|
|
71
|
+
rihal_source_path: ""
|
|
72
|
+
workflow:
|
|
73
|
+
research_by_default: false
|
|
74
|
+
plan_checker: true
|
|
75
|
+
post_execute_gates: true
|
|
76
|
+
ui_safety_gate: true
|
|
77
|
+
git:
|
|
78
|
+
branching_strategy: "none"
|
|
79
|
+
YAML
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**4. Seed `.rihal/state.json` and `.planning/` structure**:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
mkdir -p .planning/phases .planning/council-sessions .rihal/context
|
|
86
|
+
|
|
87
|
+
# state.json
|
|
88
|
+
cat > .rihal/state.json << 'JSON'
|
|
89
|
+
{"project":"{project_name}","phase":null,"sprint":null,"sessions":[],"initialized":"now"}
|
|
90
|
+
JSON
|
|
91
|
+
|
|
92
|
+
# context stubs
|
|
93
|
+
echo "# Active Context\n\n_Run /rihal-init for full setup._" > .rihal/context/active.md
|
|
94
|
+
echo "# Project Brief\n\n_Run /rihal-init for full setup._" > .rihal/context/project-brief.md
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**5. Tell the user:**
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
✓ Rihal configured for this project.
|
|
101
|
+
|
|
102
|
+
Config saved to .rihal/config.yaml — edit any time.
|
|
103
|
+
Run /rihal-init for a full project scan and context setup.
|
|
104
|
+
|
|
105
|
+
Continuing with your original request...
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**6. Continue** with the original workflow as if it had been initialized from the start.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Notes
|
|
113
|
+
|
|
114
|
+
- This guard is **non-blocking** — it asks questions but does not stop the session; once config is written it resumes the original request automatically.
|
|
115
|
+
- If `AskUserQuestion` is unavailable (non-interactive mode), use all defaults and skip the questions. Always derive `project_name` from `basename $(pwd)` — never leave it as a placeholder.
|
|
116
|
+
- If the bootstrap `cp` fails (global rihal not found), print a warning and attempt to continue — some workflows work without local rihal-tools if they only need config.
|
|
117
|
+
- On **subsequent runs**, the guard exits immediately (config exists) with zero overhead.
|
package/rihal/workflows/do.md
CHANGED
|
@@ -3,6 +3,7 @@ Analyze freeform text from the user and route to the most appropriate Rihal comm
|
|
|
3
3
|
</purpose>
|
|
4
4
|
|
|
5
5
|
<required_reading>
|
|
6
|
+
@.rihal/references/auto-init-guard.md
|
|
6
7
|
@.rihal/references/output-format.md
|
|
7
8
|
@.rihal/references/verb-dictionary.md
|
|
8
9
|
@.rihal/references/dispatch-banner.md
|
|
@@ -11,6 +12,11 @@ Read all files referenced by the invoking prompt's execution_context before star
|
|
|
11
12
|
|
|
12
13
|
<process>
|
|
13
14
|
|
|
15
|
+
<step name="auto_init_check">
|
|
16
|
+
Run the auto-init guard from `@.rihal/references/auto-init-guard.md` before anything else.
|
|
17
|
+
If the project is not initialized, complete the inline init flow, then continue.
|
|
18
|
+
</step>
|
|
19
|
+
|
|
14
20
|
<step name="parse_args">
|
|
15
21
|
Extract `$ARGUMENTS`, detect `--auto` flag, and check config mode:
|
|
16
22
|
|
package/rihal/workflows/init.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Workflow: rihal-init
|
|
2
2
|
|
|
3
3
|
<purpose>
|
|
4
|
-
Begin the rihla. This is the
|
|
4
|
+
Begin the rihla. This is the entry point for configuring Rihal in a project. Runs automatically on first use of any rihal command (via auto-init-guard in do.md), or explicitly via /rihal-init for a full setup with codebase scan.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
No manual "rcode install" needed per project — just use any /rihal-* command and this triggers automatically when config is missing.
|
|
7
7
|
</purpose>
|
|
8
8
|
|
|
9
9
|
## Step 0 — Usage check
|
|
@@ -4,6 +4,7 @@ Initialize a new project through unified flow: questioning, research (optional),
|
|
|
4
4
|
</purpose>
|
|
5
5
|
|
|
6
6
|
<required_reading>
|
|
7
|
+
@.rihal/references/auto-init-guard.md
|
|
7
8
|
@.rihal/references/output-format.md
|
|
8
9
|
|
|
9
10
|
Read all files referenced by the invoking prompt's execution_context before starting.
|
package/rihal/workflows/plan.md
CHANGED