@hanzlaa/rcode 4.3.3 → 4.4.1
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/CONTRIBUTING.md +1 -1
- package/README.md +28 -5
- package/cli/doctor.js +21 -3
- package/cli/generate-command-skills.cjs +1 -1
- package/cli/index.js +5 -1
- package/cli/install.js +7 -9
- package/cli/postinstall.js +22 -2
- package/cli/uninstall.js +48 -6
- package/dist/rcode.js +202 -203
- package/package.json +13 -23
- package/rcode/agents/rcode-codebase-mapper.md +12 -0
- package/rcode/agents/rcode-phase-researcher.md +13 -0
- package/rcode/agents/rcode-project-researcher.md +32 -0
- package/rcode/agents/rcode-roadmapper.md +15 -0
- package/rcode/agents/rules/planner/common-patterns.md +40 -0
- package/rcode/bin/lib/roadmap.cjs +184 -11
- package/rcode/bin/rcode-hooks.cjs +102 -151
- package/rcode/bin/rcode-tools.cjs +669 -55
- package/rcode/commands/lazy.md +10 -0
- package/rcode/data/intent-table.json +87 -0
- package/rcode/modules/seo.yaml +22 -0
- package/rcode/references/project-types.yaml +75 -0
- package/rcode/references/verb-dictionary.md +1 -1
- package/rcode/skills/SKILLS_INDEX.md +18 -1
- package/rcode/skills/actions/1-analysis/rcode-prfaq/SKILL.md +1 -1
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/SKILL.md +4 -21
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/heartbeat.sh +0 -0
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/SKILL.md +60 -1
- package/rcode/skills/core/rcode-init/scripts/__pycache__/rcode_init.cpython-38.pyc +0 -0
- package/rcode/skills/core/rcode-init/scripts/tests/__pycache__/test_rcode_init.cpython-38.pyc +0 -0
- package/rcode/skills/seo/module.yaml +20 -0
- package/rcode/skills/seo/on-page-seo-auditor/SKILL.md +308 -0
- package/rcode/skills/seo/on-page-seo-auditor/references/audit-example.md +109 -0
- package/rcode/skills/seo/on-page-seo-auditor/references/audit-templates.md +427 -0
- package/rcode/skills/seo/on-page-seo-auditor/references/bulk-audit-playbook.md +116 -0
- package/rcode/skills/seo/on-page-seo-auditor/references/scoring-rubric.md +525 -0
- package/rcode/skills/seo/rank-and-rent-local-seo/SKILL.md +94 -0
- package/rcode/skills/seo/rank-and-rent-local-seo/rules/city-matrix-and-pages.md +76 -0
- package/rcode/skills/seo/rank-and-rent-local-seo/rules/monetization.md +70 -0
- package/rcode/skills/seo/rank-and-rent-local-seo/rules/niche-selection.md +60 -0
- package/rcode/skills/seo/rank-and-rent-local-seo/rules/subniche-discovery.md +66 -0
- package/rcode/skills/seo/rank-and-rent-local-seo/templates/service-city-page.md +36 -0
- package/rcode/skills/seo/rank-and-rent-local-seo/templates/subniche-research-prompt.md +34 -0
- package/rcode/skills/seo/seo-audit/SKILL.md +412 -0
- package/rcode/skills/seo/seo-audit/evals/evals.json +136 -0
- package/rcode/skills/seo/seo-audit/references/ai-writing-detection.md +200 -0
- package/rcode/skills/seo/seo-content-factory/SKILL.md +130 -0
- package/rcode/skills/seo/seo-content-factory/rules/agents.md +80 -0
- package/rcode/skills/seo/seo-content-factory/rules/herdr-wave-mapping.md +80 -0
- package/rcode/skills/seo/seo-content-factory/rules/pipeline.md +102 -0
- package/rcode/skills/seo/seo-content-factory/rules/programmatic-pages.md +107 -0
- package/rcode/skills/seo/seo-content-factory/rules/quality-gates.md +68 -0
- package/rcode/skills/seo/seo-content-factory/rules/weekly-cadence.md +61 -0
- package/rcode/skills/seo/seo-content-factory/templates/BACKLOG.md +23 -0
- package/rcode/skills/seo/seo-content-factory/templates/agent-prompts/01-funnel-A1-A4.md +63 -0
- package/rcode/skills/seo/seo-content-factory/templates/agent-prompts/02-fanout-editorial-A5-A6-A8.md +28 -0
- package/rcode/skills/seo/seo-content-factory/templates/agent-prompts/03-fanout-programmatic-A7-A6-A8.md +25 -0
- package/rcode/skills/seo/seo-content-factory/templates/agent-prompts/04-loop-A9-A10.md +34 -0
- package/rcode/skills/seo/seo-content-factory/templates/article.mdx +39 -0
- package/rcode/skills/seo/seo-content-factory/templates/cluster.schema.json +52 -0
- package/rcode/skills/seo/seo-content-factory/templates/content-brief.md +51 -0
- package/rcode/skills/seo/seo-content-factory/templates/jsonld-builders.ts +49 -0
- package/rcode/skills/seo/seo-content-factory/templates/keywords.csv +13 -0
- package/rcode/skills/seo/seo-content-factory/templates/programmatic-page.tsx +58 -0
- package/rcode/skills/seo/seo-content-writer/SKILL.md +263 -0
- package/rcode/skills/seo/seo-content-writer/references/content-structure-templates.md +875 -0
- package/rcode/skills/seo/seo-content-writer/references/instructions-detail.md +239 -0
- package/rcode/skills/seo/seo-content-writer/references/seo-writing-checklist.md +250 -0
- package/rcode/skills/seo/seo-content-writer/references/title-formulas.md +339 -0
- package/rcode/skills/seo/seo-growth-orchestrator/SKILL.md +79 -0
- package/rcode/skills/seo/seo-growth-orchestrator/rules/backlinks.md +29 -0
- package/rcode/skills/seo/seo-growth-orchestrator/rules/content-engine.md +27 -0
- package/rcode/skills/seo/seo-growth-orchestrator/rules/dos-and-donts.md +40 -0
- package/rcode/skills/seo/seo-growth-orchestrator/rules/goals-protocol.md +34 -0
- package/rcode/skills/seo/seo-growth-orchestrator/rules/local-seo-stack.md +35 -0
- package/rcode/skills/seo/seo-growth-orchestrator/rules/mrr-sprint.md +27 -0
- package/rcode/skills/seo/seo-growth-orchestrator/rules/tools.md +19 -0
- package/rcode/skills/seo/seo-growth-orchestrator/templates/backlink-research.md +29 -0
- package/rcode/skills/seo/seo-growth-orchestrator/templates/goal-protocol.md +35 -0
- package/rcode/skills/seo/seo-growth-orchestrator/templates/local-citations.md +24 -0
- package/rcode/skills/seo/seo-growth-orchestrator/templates/local-city-pages.md +27 -0
- package/rcode/skills/seo/seo-growth-orchestrator/templates/local-gbp-categories.md +23 -0
- package/rcode/skills/seo/seo-growth-orchestrator/templates/local-gsc-goldmine.md +27 -0
- package/rcode/skills/seo/seo-growth-orchestrator/templates/local-review-mining.md +26 -0
- package/rcode/skills/seo/seo-growth-orchestrator/templates/mrr-30day-plan.md +19 -0
- package/rcode/skills/seo/seo-growth-orchestrator/templates/mrr-cut.md +13 -0
- package/rcode/skills/seo/seo-growth-orchestrator/templates/mrr-opportunity-finder.md +17 -0
- package/rcode/skills/seo/seo-growth-orchestrator/templates/mrr-role-model.md +18 -0
- package/rcode/skills/seo/seo-site-builder/SKILL.md +149 -0
- package/rcode/skills/seo/seo-site-builder/rules/01-niche-discovery.md +64 -0
- package/rcode/skills/seo/seo-site-builder/rules/02-deep-research.md +164 -0
- package/rcode/skills/seo/seo-site-builder/rules/03-keyword-strategy.md +82 -0
- package/rcode/skills/seo/seo-site-builder/rules/04-content-plan.md +173 -0
- package/rcode/skills/seo/seo-site-builder/rules/05-site-build.md +187 -0
- package/rcode/skills/seo/seo-site-builder/rules/06-yolo.md +127 -0
- package/rcode/skills/seo/seo-site-builder/templates/article-stub.mdx +62 -0
- package/rcode/skills/seo/seo-site-builder/templates/image-prompt.md +97 -0
- package/rcode/skills/seo/seo-site-builder/templates/keyword-export.md +45 -0
- package/rcode/skills/seo/seo-site-builder/templates/niche-scorecard.md +28 -0
- package/rcode/skills/seo/technical-seo-checker/SKILL.md +329 -0
- package/rcode/skills/seo/technical-seo-checker/references/bulk-audit-playbook.md +118 -0
- package/rcode/skills/seo/technical-seo-checker/references/ecommerce-platform-patterns.md +166 -0
- package/rcode/skills/seo/technical-seo-checker/references/http-status-codes.md +705 -0
- package/rcode/skills/seo/technical-seo-checker/references/llm-crawler-handling.md +257 -0
- package/rcode/skills/seo/technical-seo-checker/references/pre-migration-playbook.md +137 -0
- package/rcode/skills/seo/technical-seo-checker/references/robots-txt-reference.md +717 -0
- package/rcode/skills/seo/technical-seo-checker/references/technical-audit-example.md +169 -0
- package/rcode/skills/seo/technical-seo-checker/references/technical-audit-templates.md +436 -0
- package/rcode/templates/settings-hooks.json +4 -0
- package/rcode/workflows/do.md +16 -0
- package/rcode/workflows/execute-sprint.md +22 -0
- package/rcode/workflows/execute.md +17 -3
- package/rcode/workflows/help.md +0 -2
- package/rcode/workflows/new-milestone.md +21 -9
- package/rcode/workflows/new-project-create-roadmap.md +2 -0
- package/rcode/workflows/new-project-roadmap.md +2 -0
- package/rcode/workflows/new-project.md +3 -0
- package/rcode/workflows/plan.md +15 -2
|
@@ -105,27 +105,46 @@ function parseSimpleYaml(text) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function readConfig() {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
108
|
+
// #733 — try config.yaml first, fall back to config.json, return defaults when neither exists.
|
|
109
|
+
const yamlPath = path.join(RCODE_DIR, 'config.yaml');
|
|
110
|
+
const jsonPath = path.join(RCODE_DIR, 'config.json');
|
|
111
|
+
|
|
112
|
+
if (fs.existsSync(yamlPath)) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = parseSimpleYaml(fs.readFileSync(yamlPath, 'utf8'));
|
|
115
|
+
return {
|
|
116
|
+
...parsed, // spread all parsed keys (model_profile, branching_strategy, etc.)
|
|
117
|
+
user_name: parsed.user_name || 'User',
|
|
118
|
+
project_name: parsed.project_name || path.basename(PROJECT_ROOT),
|
|
119
|
+
language: parsed.communication_language || parsed.language || 'English',
|
|
120
|
+
mode: parsed.mode || 'guided',
|
|
121
|
+
};
|
|
122
|
+
} catch (e) {
|
|
123
|
+
throw new Error(`Failed to read config.yaml: ${e.message}`);
|
|
124
|
+
}
|
|
116
125
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
|
|
127
|
+
if (fs.existsSync(jsonPath)) {
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
130
|
+
return {
|
|
131
|
+
...parsed,
|
|
132
|
+
user_name: parsed.user_name || 'User',
|
|
133
|
+
project_name: parsed.project_name || path.basename(PROJECT_ROOT),
|
|
134
|
+
language: parsed.communication_language || parsed.language || 'English',
|
|
135
|
+
mode: parsed.mode || 'guided',
|
|
136
|
+
};
|
|
137
|
+
} catch (e) {
|
|
138
|
+
throw new Error(`Failed to read config.json: ${e.message}`);
|
|
139
|
+
}
|
|
128
140
|
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
user_name: 'User',
|
|
144
|
+
project_name: path.basename(PROJECT_ROOT),
|
|
145
|
+
language: 'English',
|
|
146
|
+
mode: 'guided',
|
|
147
|
+
};
|
|
129
148
|
}
|
|
130
149
|
|
|
131
150
|
/**
|
|
@@ -959,6 +978,7 @@ function cmdInitExecute(rawArgs) {
|
|
|
959
978
|
* resolve-blocker <index> → set blockers[index].resolved = true
|
|
960
979
|
* record-session → update last_session timestamp
|
|
961
980
|
* record-council --slug <s> --panel <csv> --artifact <path>
|
|
981
|
+
* sync-from-git → recover phase/sprint state from git commit history (#915)
|
|
962
982
|
*/
|
|
963
983
|
function cmdState(subArgs) {
|
|
964
984
|
const statePath = path.join(RCODE_DIR, 'state.json');
|
|
@@ -1010,12 +1030,75 @@ function cmdState(subArgs) {
|
|
|
1010
1030
|
throw new Error('state.json exceeds 10 MB limit — possible corruption');
|
|
1011
1031
|
}
|
|
1012
1032
|
try {
|
|
1013
|
-
|
|
1033
|
+
const raw = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
1034
|
+
return migrateState(raw);
|
|
1014
1035
|
} catch (e) {
|
|
1015
1036
|
throw new Error(`Invalid JSON in state.json: ${e.message}`);
|
|
1016
1037
|
}
|
|
1017
1038
|
}
|
|
1018
1039
|
|
|
1040
|
+
/**
|
|
1041
|
+
* migrateState — pure normalizer that upgrades any legacy state shape to v2.
|
|
1042
|
+
*
|
|
1043
|
+
* v0: { milestone: string, no phases[], no schema_version }
|
|
1044
|
+
* v1: { phases[] with mixed shapes, schema_version: 1 }
|
|
1045
|
+
* v2 (target): { schema_version: 2, phases[] uniform, milestones[] array }
|
|
1046
|
+
*
|
|
1047
|
+
* This function is PURE — it never writes to disk. readState() calls it on
|
|
1048
|
+
* every read so all callers transparently receive v2-shaped data. (#735)
|
|
1049
|
+
*/
|
|
1050
|
+
function migrateState(raw) {
|
|
1051
|
+
if (!raw || typeof raw !== 'object') return raw;
|
|
1052
|
+
const state = Object.assign({}, raw);
|
|
1053
|
+
|
|
1054
|
+
// --- milestones[] array (v0 → v2) ---
|
|
1055
|
+
// v0 state has milestone as a plain string and no milestones array.
|
|
1056
|
+
if (typeof state.milestone === 'string' && !Array.isArray(state.milestones)) {
|
|
1057
|
+
state.milestones = [{
|
|
1058
|
+
id: state.milestone,
|
|
1059
|
+
name: state.milestone,
|
|
1060
|
+
status: 'active',
|
|
1061
|
+
}];
|
|
1062
|
+
}
|
|
1063
|
+
if (!Array.isArray(state.milestones)) {
|
|
1064
|
+
state.milestones = [];
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// --- phases[] uniform shape (v1 → v2) ---
|
|
1068
|
+
// v1 phases have mixed shapes: some {number, name}, others {id, name, status}.
|
|
1069
|
+
if (Array.isArray(state.phases)) {
|
|
1070
|
+
state.phases = state.phases.map(p => {
|
|
1071
|
+
if (!p || typeof p !== 'object') return p;
|
|
1072
|
+
// Resolve number: prefer p.number, fall back to numeric part of p.id
|
|
1073
|
+
let number = p.number ?? null;
|
|
1074
|
+
if (number === null && typeof p.id === 'string') {
|
|
1075
|
+
const m = p.id.match(/^(\d+(?:\.\d+)?)/);
|
|
1076
|
+
if (m) number = m[1];
|
|
1077
|
+
}
|
|
1078
|
+
// Resolve id: prefer p.id, synthesize from number
|
|
1079
|
+
const id = p.id ?? (number !== null ? String(number) : undefined);
|
|
1080
|
+
return {
|
|
1081
|
+
number: number ?? p.id ?? null,
|
|
1082
|
+
id: id ?? null,
|
|
1083
|
+
name: p.name ?? null,
|
|
1084
|
+
status: p.status ?? 'planned',
|
|
1085
|
+
started: p.started ?? null,
|
|
1086
|
+
completed: p.completed ?? null,
|
|
1087
|
+
sprints: Array.isArray(p.sprints) ? p.sprints : [],
|
|
1088
|
+
// Preserve any extra fields that callers may rely on
|
|
1089
|
+
...Object.fromEntries(
|
|
1090
|
+
Object.entries(p).filter(([k]) =>
|
|
1091
|
+
!['number', 'id', 'name', 'status', 'started', 'completed', 'sprints'].includes(k)
|
|
1092
|
+
)
|
|
1093
|
+
),
|
|
1094
|
+
};
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
state.schema_version = 2;
|
|
1099
|
+
return state;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1019
1102
|
/** Atomic write: write to temp file then rename. */
|
|
1020
1103
|
function writeState(state) {
|
|
1021
1104
|
function isProcessAlive(pid) {
|
|
@@ -1105,11 +1188,10 @@ function cmdState(subArgs) {
|
|
|
1105
1188
|
const now = new Date().toISOString();
|
|
1106
1189
|
return {
|
|
1107
1190
|
version: '1',
|
|
1108
|
-
// #8 — explicit schema_version field for
|
|
1109
|
-
//
|
|
1110
|
-
//
|
|
1111
|
-
|
|
1112
|
-
schema_version: 1,
|
|
1191
|
+
// #8 / #735 — explicit schema_version field for migration framework.
|
|
1192
|
+
// v2: phases[] uniform shape + milestones[] array. migrateState() upgrades
|
|
1193
|
+
// older state files transparently on read. New state starts at v2.
|
|
1194
|
+
schema_version: 2,
|
|
1113
1195
|
project: projectName || path.basename(PROJECT_ROOT),
|
|
1114
1196
|
created: now,
|
|
1115
1197
|
updated: now,
|
|
@@ -1117,6 +1199,7 @@ function cmdState(subArgs) {
|
|
|
1117
1199
|
current_plan: 0,
|
|
1118
1200
|
current_sprint: null,
|
|
1119
1201
|
phases: [],
|
|
1202
|
+
milestones: [],
|
|
1120
1203
|
velocity_history: [],
|
|
1121
1204
|
executions: [],
|
|
1122
1205
|
decisions: [],
|
|
@@ -1253,6 +1336,18 @@ function cmdState(subArgs) {
|
|
|
1253
1336
|
const name = subArgs[1];
|
|
1254
1337
|
if (!name) throw new Error('set-phase requires a phase name argument');
|
|
1255
1338
|
const state = readState() || defaultState();
|
|
1339
|
+
// Fix #854 — mark the previously active phase as completed before switching.
|
|
1340
|
+
if (state.current_phase && state.current_phase !== name && state.phases && state.phases.length > 0) {
|
|
1341
|
+
const prevIdx = state.phases.findIndex(p =>
|
|
1342
|
+
p.name === state.current_phase ||
|
|
1343
|
+
String(p.number) === String(state.current_phase) ||
|
|
1344
|
+
String(p.id) === String(state.current_phase)
|
|
1345
|
+
);
|
|
1346
|
+
if (prevIdx !== -1 && state.phases[prevIdx].status !== 'completed') {
|
|
1347
|
+
state.phases[prevIdx].status = 'completed';
|
|
1348
|
+
state.phases[prevIdx].completed = new Date().toISOString();
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1256
1351
|
state.current_phase = name;
|
|
1257
1352
|
state.current_plan = 0;
|
|
1258
1353
|
if (!state.phases) state.phases = [];
|
|
@@ -1279,7 +1374,36 @@ function cmdState(subArgs) {
|
|
|
1279
1374
|
// Update name to canonical form when re-entering a phase
|
|
1280
1375
|
state.phases[existingIdx].name = name;
|
|
1281
1376
|
}
|
|
1282
|
-
|
|
1377
|
+
// #894 — Proactively sync state.milestone from ROADMAP when set-phase is called.
|
|
1378
|
+
// If ROADMAP.md is readable, find its last active milestone heading and update
|
|
1379
|
+
// state.milestone if it differs (state can go stale after milestone transitions).
|
|
1380
|
+
try {
|
|
1381
|
+
const roadmapPathSP = path.join(PLANNING_DIR, 'ROADMAP.md');
|
|
1382
|
+
if (fs.existsSync(roadmapPathSP)) {
|
|
1383
|
+
const rmText = fs.readFileSync(roadmapPathSP, 'utf8');
|
|
1384
|
+
const mhRe = /^#{1,2}\s+(M\d+[^\n]*)/gm;
|
|
1385
|
+
let lastLabel = null, mhM;
|
|
1386
|
+
while ((mhM = mhRe.exec(rmText)) !== null) {
|
|
1387
|
+
if (/^milestones?\s*$/i.test(mhM[1].trim())) continue;
|
|
1388
|
+
lastLabel = mhM[1].trim();
|
|
1389
|
+
}
|
|
1390
|
+
if (lastLabel && lastLabel !== (state.milestone || '')) {
|
|
1391
|
+
state.milestone = lastLabel;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
} catch (_) { /* ROADMAP unreadable; leave milestone as-is */ }
|
|
1395
|
+
|
|
1396
|
+
const spResult = writeState(state);
|
|
1397
|
+
// Fix #855 — keep config.yaml in sync when set-phase writes state.json.
|
|
1398
|
+
// One-way guard: only sync if config.yaml is already present (i.e. project is initialised).
|
|
1399
|
+
try {
|
|
1400
|
+
const cfgLib = require(path.join(__dirname, 'lib', 'config.cjs'));
|
|
1401
|
+
const existingCfgPhase = cfgLib.cmdGet(PROJECT_ROOT, 'current_phase');
|
|
1402
|
+
if (String(existingCfgPhase || '') !== String(name)) {
|
|
1403
|
+
cfgLib.cmdSet(PROJECT_ROOT, 'current_phase', name);
|
|
1404
|
+
}
|
|
1405
|
+
} catch (_) { /* config.yaml may not exist yet; silently skip */ }
|
|
1406
|
+
return spResult;
|
|
1283
1407
|
}
|
|
1284
1408
|
|
|
1285
1409
|
// --- advance-plan ---
|
|
@@ -1929,6 +2053,146 @@ function cmdState(subArgs) {
|
|
|
1929
2053
|
return writeState(state);
|
|
1930
2054
|
}
|
|
1931
2055
|
|
|
2056
|
+
// --- sync-from-git ---
|
|
2057
|
+
// Recover execution state by inspecting git log for implementation commits.
|
|
2058
|
+
// For each phase that has sprints, checks whether feat:/fix:/refactor: commits
|
|
2059
|
+
// referencing that phase number exist. If so, marks sprints completed and phase
|
|
2060
|
+
// as executed (not complete — verifier should still run). Issue #915.
|
|
2061
|
+
if (sub === 'sync-from-git') {
|
|
2062
|
+
const state = readState();
|
|
2063
|
+
if (!state) return { ok: false, error: 'No state.json — run `state init` first.' };
|
|
2064
|
+
|
|
2065
|
+
const { execSync } = require('child_process');
|
|
2066
|
+
let gitLog = '';
|
|
2067
|
+
try {
|
|
2068
|
+
gitLog = execSync('git log --oneline', { cwd: PROJECT_ROOT, encoding: 'utf8' });
|
|
2069
|
+
} catch (e) {
|
|
2070
|
+
return { ok: false, error: `git log failed: ${e.message}` };
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
const implPrefixRe = /^[a-f0-9]+ (feat|fix|refactor|perf|style|test|chore)\(/i;
|
|
2074
|
+
const implLines = gitLog.split('\n').filter(l => implPrefixRe.test(l));
|
|
2075
|
+
|
|
2076
|
+
// Read ROADMAP.md once so we can look up each phase's declared status.
|
|
2077
|
+
// Fix #897 — sync-from-git was ignoring ROADMAP status entirely, causing
|
|
2078
|
+
// all phases to stay as 'planned' even when ROADMAP said 'complete'.
|
|
2079
|
+
let roadmapText = '';
|
|
2080
|
+
try {
|
|
2081
|
+
const roadmapPath = path.join(PLANNING_DIR, 'ROADMAP.md');
|
|
2082
|
+
if (fs.existsSync(roadmapPath)) roadmapText = fs.readFileSync(roadmapPath, 'utf8');
|
|
2083
|
+
} catch { /* ignore — ROADMAP is optional */ }
|
|
2084
|
+
|
|
2085
|
+
// Normalise raw status strings from ROADMAP into canonical state.json vocabulary.
|
|
2086
|
+
// Mirrors the normalizeStatus() defined in the `state sync` handler.
|
|
2087
|
+
function normalizeStatusSFG(raw) {
|
|
2088
|
+
if (!raw) return 'planned';
|
|
2089
|
+
const s = String(raw).toLowerCase().replace(/[✅\s]/g, '');
|
|
2090
|
+
if (['complete','completed','shipped','verified','done'].includes(s)) return 'complete';
|
|
2091
|
+
if (['executing','in_progress','inprogress','active','started'].includes(s)) return 'in_progress';
|
|
2092
|
+
return 'planned';
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// Returns the status declared in ROADMAP for a given phase number, or null
|
|
2096
|
+
// if the phase isn't found. Handles both pipe-table and heading-block formats.
|
|
2097
|
+
function readPhaseStatusFromRoadmap(phaseNum) {
|
|
2098
|
+
if (!roadmapText) return null;
|
|
2099
|
+
// Pipe-table row: | <num> | <name> | <goal> | <status> |
|
|
2100
|
+
const tableRe = new RegExp(
|
|
2101
|
+
`^\\|\\s*${phaseNum.replace('.', '\\.')}\\s*\\|[^|]+\\|[^|]*\\|(?:\\s*([^|\\n]*?)\\s*\\|)?`,
|
|
2102
|
+
'm'
|
|
2103
|
+
);
|
|
2104
|
+
const tableMatch = roadmapText.match(tableRe);
|
|
2105
|
+
if (tableMatch && tableMatch[1] !== undefined) return normalizeStatusSFG(tableMatch[1]);
|
|
2106
|
+
|
|
2107
|
+
// Heading-block format: ## Phase <num> — <name>\n...**Status:** <value>
|
|
2108
|
+
const headRe = new RegExp(
|
|
2109
|
+
`^#{2,4}\\s*Phase\\s+${phaseNum.replace('.', '\\.')}\\s*[—\\-:]`,
|
|
2110
|
+
'm'
|
|
2111
|
+
);
|
|
2112
|
+
const headMatch = headRe.exec(roadmapText);
|
|
2113
|
+
if (headMatch) {
|
|
2114
|
+
const after = roadmapText.slice(headMatch.index + headMatch[0].length).split('\n').slice(0, 8).join('\n');
|
|
2115
|
+
const statusMatch = after.match(/\*\*Status:\*\*\s*(.+)/i);
|
|
2116
|
+
if (statusMatch) return normalizeStatusSFG(statusMatch[1].trim());
|
|
2117
|
+
}
|
|
2118
|
+
return null;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
let syncedPhases = 0;
|
|
2122
|
+
let syncedSprints = 0;
|
|
2123
|
+
|
|
2124
|
+
const statusRankSFG = { complete: 3, verified: 3, executed: 2, in_progress: 1, planned: 0 };
|
|
2125
|
+
|
|
2126
|
+
const phases = Array.isArray(state.phases) ? state.phases : [];
|
|
2127
|
+
for (const phase of phases) {
|
|
2128
|
+
if (!phase) continue;
|
|
2129
|
+
const num = String(phase.number || phase.id || '').trim();
|
|
2130
|
+
if (!num) continue;
|
|
2131
|
+
|
|
2132
|
+
// Read the phase's declared status from ROADMAP and apply it when it
|
|
2133
|
+
// advances the current status — never downgrade. (#897)
|
|
2134
|
+
const roadmapStatus = readPhaseStatusFromRoadmap(num);
|
|
2135
|
+
if (roadmapStatus) {
|
|
2136
|
+
const currentRank = statusRankSFG[phase.status] ?? 0;
|
|
2137
|
+
const roadmapRank = statusRankSFG[roadmapStatus] ?? 0;
|
|
2138
|
+
if (roadmapRank > currentRank) {
|
|
2139
|
+
phase.status = roadmapStatus;
|
|
2140
|
+
syncedPhases++;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// Check if any implementation commit references this phase number.
|
|
2145
|
+
// Matches patterns: "phase 1", "phase 1.", "1.1", "(1)", "#1 "
|
|
2146
|
+
const phaseNumEscaped = num.replace('.', '\\.');
|
|
2147
|
+
const phaseRe = new RegExp(
|
|
2148
|
+
`(phase\\s*${phaseNumEscaped}[^\\d]|\\b${phaseNumEscaped}\\.\\d|\\(${phaseNumEscaped}\\)|\\s${phaseNumEscaped}\\s)`,
|
|
2149
|
+
'i'
|
|
2150
|
+
);
|
|
2151
|
+
const hasImplCommit = implLines.some(l => phaseRe.test(l));
|
|
2152
|
+
|
|
2153
|
+
// Also check if SUMMARY.md exists for this phase
|
|
2154
|
+
let hasSummary = false;
|
|
2155
|
+
try {
|
|
2156
|
+
const phaseDirs = fs.existsSync(PLANNING_DIR)
|
|
2157
|
+
? fs.readdirSync(PLANNING_DIR).filter(d => {
|
|
2158
|
+
const m = d.match(/^(\d+)/);
|
|
2159
|
+
return m && m[1] === num;
|
|
2160
|
+
})
|
|
2161
|
+
: [];
|
|
2162
|
+
if (phaseDirs.length > 0) {
|
|
2163
|
+
const summaryPath = path.join(PLANNING_DIR, phaseDirs[0], 'SUMMARY.md');
|
|
2164
|
+
hasSummary = fs.existsSync(summaryPath);
|
|
2165
|
+
}
|
|
2166
|
+
} catch { /* ignore fs errors */ }
|
|
2167
|
+
|
|
2168
|
+
const sprints = Array.isArray(phase.sprints) ? phase.sprints : [];
|
|
2169
|
+
if ((hasImplCommit || hasSummary) && sprints.length > 0) {
|
|
2170
|
+
for (const sprint of sprints) {
|
|
2171
|
+
if (sprint && sprint.status !== 'completed') {
|
|
2172
|
+
sprint.status = 'completed';
|
|
2173
|
+
syncedSprints++;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
if (phase.status !== 'complete' && phase.status !== 'verified') {
|
|
2177
|
+
// Git evidence upgrades to 'executed' only when ROADMAP doesn't already
|
|
2178
|
+
// report a higher status (complete/verified already applied above).
|
|
2179
|
+
if ((statusRankSFG['executed'] ?? 0) > (statusRankSFG[phase.status] ?? 0)) {
|
|
2180
|
+
phase.status = 'executed';
|
|
2181
|
+
syncedPhases++;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
writeState(state);
|
|
2188
|
+
return {
|
|
2189
|
+
ok: true,
|
|
2190
|
+
message: `Synced ${syncedPhases} phases, ${syncedSprints} sprints from git history`,
|
|
2191
|
+
synced_phases: syncedPhases,
|
|
2192
|
+
synced_sprints: syncedSprints,
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
|
|
1932
2196
|
// --- record-chain ---
|
|
1933
2197
|
if (sub === 'record-chain') {
|
|
1934
2198
|
const flags = parseFlags(1);
|
|
@@ -3058,7 +3322,19 @@ function cmdState(subArgs) {
|
|
|
3058
3322
|
parsed.phases_normalized = beforeClean - cleaned.length;
|
|
3059
3323
|
state.phases = cleaned;
|
|
3060
3324
|
|
|
3061
|
-
|
|
3325
|
+
// Normalise any raw status string from ROADMAP into the canonical
|
|
3326
|
+
// vocabulary used by state.json: 'complete' | 'in_progress' | 'planned'.
|
|
3327
|
+
// Fix #897 — status was never read from ROADMAP, so every phase always
|
|
3328
|
+
// landed as 'planned' regardless of what the doc said.
|
|
3329
|
+
function normalizeStatus(raw) {
|
|
3330
|
+
if (!raw) return 'planned';
|
|
3331
|
+
const s = String(raw).toLowerCase().replace(/[✅\s]/g, '');
|
|
3332
|
+
if (['complete','completed','shipped','verified','done'].includes(s)) return 'complete';
|
|
3333
|
+
if (['executing','in_progress','inprogress','active','started'].includes(s)) return 'in_progress';
|
|
3334
|
+
return 'planned';
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
const upsertPhase = (phaseNum, phaseName, phaseGoal, phaseStatus) => {
|
|
3062
3338
|
if (!/^\d/.test(phaseNum)) return;
|
|
3063
3339
|
if (phaseName.toLowerCase() === 'phase') return;
|
|
3064
3340
|
if (seenNums.has(phaseNum)) return;
|
|
@@ -3074,12 +3350,21 @@ function cmdState(subArgs) {
|
|
|
3074
3350
|
String(p.id) === phaseNum ||
|
|
3075
3351
|
p.name === phaseName
|
|
3076
3352
|
);
|
|
3353
|
+
// Status precedence for advancement: complete > in_progress > planned.
|
|
3354
|
+
// A phase should never be downgraded by ROADMAP re-sync.
|
|
3355
|
+
const statusRank = { complete: 2, in_progress: 1, planned: 0 };
|
|
3356
|
+
const incomingStatus = normalizeStatus(phaseStatus);
|
|
3077
3357
|
if (existingIdx >= 0) {
|
|
3078
3358
|
// Backfill both id and number so future readers using either schema find it.
|
|
3079
3359
|
state.phases[existingIdx].number = state.phases[existingIdx].number || phaseNum;
|
|
3080
3360
|
state.phases[existingIdx].id = state.phases[existingIdx].id || phaseNum;
|
|
3081
3361
|
state.phases[existingIdx].name = phaseName;
|
|
3082
3362
|
if (phaseGoal) state.phases[existingIdx].goal = phaseGoal;
|
|
3363
|
+
// Only advance status — never downgrade an existing phase's status via sync.
|
|
3364
|
+
const currentRank = statusRank[normalizeStatus(state.phases[existingIdx].status)] ?? 0;
|
|
3365
|
+
if ((statusRank[incomingStatus] ?? 0) > currentRank) {
|
|
3366
|
+
state.phases[existingIdx].status = incomingStatus;
|
|
3367
|
+
}
|
|
3083
3368
|
} else {
|
|
3084
3369
|
// Write both id and number on every new entry so dedup works regardless
|
|
3085
3370
|
// of which schema future readers expect.
|
|
@@ -3088,7 +3373,7 @@ function cmdState(subArgs) {
|
|
|
3088
3373
|
number: phaseNum,
|
|
3089
3374
|
name: phaseName,
|
|
3090
3375
|
goal: phaseGoal,
|
|
3091
|
-
status:
|
|
3376
|
+
status: incomingStatus,
|
|
3092
3377
|
started: null,
|
|
3093
3378
|
completed: null,
|
|
3094
3379
|
plan_count: 0,
|
|
@@ -3100,10 +3385,12 @@ function cmdState(subArgs) {
|
|
|
3100
3385
|
// Format A — pipe tables
|
|
3101
3386
|
// Phase number: \d+ (not \d{1,3}) — high numbers like 1001 are valid for
|
|
3102
3387
|
// hot-track parking-lot phases per parking-lot-convention.md.
|
|
3103
|
-
|
|
3388
|
+
// The optional 4th capture group reads the status column when present
|
|
3389
|
+
// (fix #897 — status was silently dropped before).
|
|
3390
|
+
const rowRe = /^\|\s*(\d+(?:\.\d+)?)\s*\|\s*([^|]+?)\s*\|\s*([^|]*?)\s*\|(?:\s*([^|\n]*?)\s*\|)?/gm;
|
|
3104
3391
|
let m;
|
|
3105
3392
|
while ((m = rowRe.exec(roadmap)) !== null) {
|
|
3106
|
-
upsertPhase(m[1].trim(), m[2].trim(), m[3].trim());
|
|
3393
|
+
upsertPhase(m[1].trim(), m[2].trim(), m[3].trim(), m[4] || '');
|
|
3107
3394
|
}
|
|
3108
3395
|
|
|
3109
3396
|
// Format B — heading style
|
|
@@ -3113,7 +3400,11 @@ function cmdState(subArgs) {
|
|
|
3113
3400
|
const name = m[2].trim();
|
|
3114
3401
|
const after = roadmap.slice(headRe.lastIndex).split(/\n/).slice(0, 8).join('\n');
|
|
3115
3402
|
const goalMatch = after.match(/\*\*Goal:\*\*\s*([^\n]+)/i);
|
|
3116
|
-
|
|
3403
|
+
// Fix #897 — read **Status:** from the post-heading block so heading-format
|
|
3404
|
+
// ROADMAPs propagate phase status into state just like pipe-table format.
|
|
3405
|
+
const statusMatch = after.match(/\*\*Status:\*\*\s*(.+)/i);
|
|
3406
|
+
const phaseStatus = statusMatch ? statusMatch[1].trim() : '';
|
|
3407
|
+
upsertPhase(num, name, goalMatch ? goalMatch[1].trim() : '', phaseStatus);
|
|
3117
3408
|
}
|
|
3118
3409
|
}
|
|
3119
3410
|
|
|
@@ -3253,6 +3544,25 @@ function cmdState(subArgs) {
|
|
|
3253
3544
|
warnings.push('epics.md exists but no epics parsed — check "## EPIC-NN" or "## Epic N" heading format.');
|
|
3254
3545
|
}
|
|
3255
3546
|
|
|
3547
|
+
// #894 — Proactively sync state.milestone from ROADMAP on state sync.
|
|
3548
|
+
// After upserting phases from ROADMAP, also derive the active milestone from
|
|
3549
|
+
// the last top-level milestone heading and correct state.milestone if stale.
|
|
3550
|
+
if (parsed.roadmap_exists) {
|
|
3551
|
+
try {
|
|
3552
|
+
const rmSync = fs.readFileSync(roadmapPath, 'utf8');
|
|
3553
|
+
const syncMhRe = /^#{1,2}\s+(M\d+[^\n]*)/gm;
|
|
3554
|
+
let syncLastLabel = null, syncMhM;
|
|
3555
|
+
while ((syncMhM = syncMhRe.exec(rmSync)) !== null) {
|
|
3556
|
+
if (/^milestones?\s*$/i.test(syncMhM[1].trim())) continue;
|
|
3557
|
+
syncLastLabel = syncMhM[1].trim();
|
|
3558
|
+
}
|
|
3559
|
+
if (syncLastLabel && syncLastLabel !== (state.milestone || '')) {
|
|
3560
|
+
state.milestone = syncLastLabel;
|
|
3561
|
+
parsed.milestone_synced = syncLastLabel;
|
|
3562
|
+
}
|
|
3563
|
+
} catch (_) { /* ROADMAP unreadable at write time; leave milestone as-is */ }
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3256
3566
|
writeState(state);
|
|
3257
3567
|
return { ok: true, synced: true, ...parsed, ...(warnings.length ? { warnings } : {}) };
|
|
3258
3568
|
}
|
|
@@ -3465,6 +3775,23 @@ function cmdPhase(subArgs) {
|
|
|
3465
3775
|
|
|
3466
3776
|
if (fs.existsSync(roadmapPath)) {
|
|
3467
3777
|
let text = fs.readFileSync(roadmapPath, 'utf8');
|
|
3778
|
+
|
|
3779
|
+
// #895 — Validate state.milestone against ROADMAP before inserting.
|
|
3780
|
+
// Find the last top-level milestone heading ("# M\d+" or "## M\d+") in
|
|
3781
|
+
// ROADMAP.md. That is the active milestone — use it as the insertion
|
|
3782
|
+
// target and correct state.milestone if it is stale.
|
|
3783
|
+
const milestoneHeadingRe = /^#{1,2}\s+(M\d+[^\n]*)/gm;
|
|
3784
|
+
let lastMilestoneLabel = null;
|
|
3785
|
+
let mh;
|
|
3786
|
+
while ((mh = milestoneHeadingRe.exec(text)) !== null) {
|
|
3787
|
+
// Skip the generic "## Milestones" index heading.
|
|
3788
|
+
if (/^milestones?\s*$/i.test(mh[1].trim())) continue;
|
|
3789
|
+
lastMilestoneLabel = mh[1].trim();
|
|
3790
|
+
}
|
|
3791
|
+
if (lastMilestoneLabel && lastMilestoneLabel !== (state.milestone || '')) {
|
|
3792
|
+
state.milestone = lastMilestoneLabel;
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3468
3795
|
const backlogMatch = text.match(/^##\s+Backlog\b/m);
|
|
3469
3796
|
if (backlogMatch) {
|
|
3470
3797
|
const backlogIdx = backlogMatch.index;
|
|
@@ -3851,7 +4178,84 @@ function cmdPhase(subArgs) {
|
|
|
3851
4178
|
return { ok: true, count: created.length, phases: created, roadmap_skipped: roadmapSkipped };
|
|
3852
4179
|
}
|
|
3853
4180
|
|
|
3854
|
-
|
|
4181
|
+
// =====================================================================
|
|
4182
|
+
// phase scaffold-all — materialise folders for every phase in ROADMAP.md
|
|
4183
|
+
// that lacks a directory under .planning/phases/.
|
|
4184
|
+
// Closes #731. No --names arg required — reads the ROADMAP table directly.
|
|
4185
|
+
// Only creates directories; does NOT create .md files inside them.
|
|
4186
|
+
// =====================================================================
|
|
4187
|
+
if (sub === 'scaffold-all') {
|
|
4188
|
+
const roadmapPath = path.join(PLANNING_DIR, 'ROADMAP.md');
|
|
4189
|
+
const phasesDir = path.join(PLANNING_DIR, 'phases');
|
|
4190
|
+
|
|
4191
|
+
if (!fs.existsSync(roadmapPath)) {
|
|
4192
|
+
throw new Error(`No ROADMAP.md found at ${roadmapPath} — run /rcode-init first`);
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
const roadmap = fs.readFileSync(roadmapPath, 'utf8');
|
|
4196
|
+
|
|
4197
|
+
// Collect (number, name) pairs from pipe-table rows: | N | Phase Name | ...
|
|
4198
|
+
// Also pick up ## Phase N — Name headings as a fallback.
|
|
4199
|
+
const phases = [];
|
|
4200
|
+
const seen = new Set();
|
|
4201
|
+
|
|
4202
|
+
// Table rows: | 8 | Feature X | ...
|
|
4203
|
+
const tableRe = /^\|\s*(\d+)\s*\|\s*([^|]+?)\s*\|/gm;
|
|
4204
|
+
let m;
|
|
4205
|
+
while ((m = tableRe.exec(roadmap)) !== null) {
|
|
4206
|
+
const num = m[1];
|
|
4207
|
+
const name = m[2].trim();
|
|
4208
|
+
// Skip header rows (e.g. "Phase" as the number column)
|
|
4209
|
+
if (!seen.has(num) && /^\d+$/.test(num)) {
|
|
4210
|
+
seen.add(num);
|
|
4211
|
+
phases.push({ num: num.padStart(2, '0'), rawNum: num, name });
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
// Heading rows: ## Phase 8 — Feature X
|
|
4216
|
+
const headRe = /^#{2,4}\s*Phase\s+(\d+)\s*[—–-]\s*(.+?)\s*$/gm;
|
|
4217
|
+
while ((m = headRe.exec(roadmap)) !== null) {
|
|
4218
|
+
const num = m[1];
|
|
4219
|
+
const name = m[2].trim();
|
|
4220
|
+
if (!seen.has(num)) {
|
|
4221
|
+
seen.add(num);
|
|
4222
|
+
phases.push({ num: num.padStart(2, '0'), rawNum: num, name });
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
if (phases.length === 0) {
|
|
4227
|
+
return { ok: true, message: 'No phases found in ROADMAP.md — nothing to scaffold', created: [], existed: [] };
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
const created = [];
|
|
4231
|
+
const existed = [];
|
|
4232
|
+
|
|
4233
|
+
for (const p of phases) {
|
|
4234
|
+
const slug = p.name
|
|
4235
|
+
.toLowerCase()
|
|
4236
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
4237
|
+
.replace(/\s+/g, '-')
|
|
4238
|
+
.replace(/-+/g, '-')
|
|
4239
|
+
.replace(/^-+|-+$/g, '');
|
|
4240
|
+
if (!slug) continue; // skip rows with no usable name (e.g. header rows)
|
|
4241
|
+
|
|
4242
|
+
const dirName = `${p.num}-${slug}`;
|
|
4243
|
+
const dirPath = path.join(phasesDir, dirName);
|
|
4244
|
+
|
|
4245
|
+
if (fs.existsSync(dirPath)) {
|
|
4246
|
+
existed.push(dirPath);
|
|
4247
|
+
console.log(`Exists: ${dirPath}`);
|
|
4248
|
+
} else {
|
|
4249
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
4250
|
+
created.push(dirPath);
|
|
4251
|
+
console.log(`Created: ${dirPath}`);
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
return { ok: true, created: created.length, existed: existed.length, dirs: { created, existed } };
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
throw new Error(`Unknown phase subcommand: ${sub || '(none)'}. Valid: add, complete, sync-sprints, set-status, next-range, scaffold-milestone, scaffold-all`);
|
|
3855
4259
|
}
|
|
3856
4260
|
|
|
3857
4261
|
/**
|
|
@@ -3996,14 +4400,15 @@ function cmdCommit(argv) {
|
|
|
3996
4400
|
const tmpMsgPath = path.join(require('os').tmpdir(), `rcode-commit-msg-${Date.now()}.txt`);
|
|
3997
4401
|
fs.writeFileSync(tmpMsgPath, message);
|
|
3998
4402
|
try {
|
|
3999
|
-
|
|
4403
|
+
// execFileSync — no shell, so tmpMsgPath with special chars cannot inject (#754).
|
|
4404
|
+
execFileSync('git', ['commit', '-F', tmpMsgPath], { cwd: PROJECT_ROOT, stdio: 'pipe' });
|
|
4000
4405
|
} finally {
|
|
4001
4406
|
try { fs.unlinkSync(tmpMsgPath); } catch {}
|
|
4002
4407
|
}
|
|
4003
4408
|
|
|
4004
4409
|
// Capture the new HEAD SHA for return value
|
|
4005
|
-
const sha =
|
|
4006
|
-
const filesChanged =
|
|
4410
|
+
const sha = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: PROJECT_ROOT, encoding: 'utf8' }).trim();
|
|
4411
|
+
const filesChanged = execFileSync('git', ['show', '--stat', '--format=', sha], { cwd: PROJECT_ROOT, encoding: 'utf8' })
|
|
4007
4412
|
.trim().split('\n').filter(Boolean);
|
|
4008
4413
|
|
|
4009
4414
|
return {
|
|
@@ -4254,7 +4659,7 @@ function cmdCommitToSubrepo(argv) {
|
|
|
4254
4659
|
}
|
|
4255
4660
|
}
|
|
4256
4661
|
|
|
4257
|
-
const { execSync } = require('child_process');
|
|
4662
|
+
const { execSync, execFileSync: execFileSyncLocal } = require('child_process');
|
|
4258
4663
|
const status = execSync('git diff --cached --name-only', { cwd: subrepoPath, encoding: 'utf8' }).trim();
|
|
4259
4664
|
if (!status) {
|
|
4260
4665
|
throw new Error(`Nothing staged in subrepo ${subrepo}. Stage files inside the subrepo with git add first.`);
|
|
@@ -4263,12 +4668,13 @@ function cmdCommitToSubrepo(argv) {
|
|
|
4263
4668
|
const tmpMsgPath = path.join(require('os').tmpdir(), `rcode-subrepo-msg-${Date.now()}.txt`);
|
|
4264
4669
|
fs.writeFileSync(tmpMsgPath, message);
|
|
4265
4670
|
try {
|
|
4266
|
-
|
|
4671
|
+
// execFileSync — no shell, so tmpMsgPath with special chars cannot inject (#754).
|
|
4672
|
+
execFileSyncLocal('git', ['commit', '-F', tmpMsgPath], { cwd: subrepoPath, stdio: 'pipe' });
|
|
4267
4673
|
} finally {
|
|
4268
4674
|
try { fs.unlinkSync(tmpMsgPath); } catch {}
|
|
4269
4675
|
}
|
|
4270
4676
|
|
|
4271
|
-
const sha =
|
|
4677
|
+
const sha = execFileSyncLocal('git', ['rev-parse', 'HEAD'], { cwd: subrepoPath, encoding: 'utf8' }).trim();
|
|
4272
4678
|
return {
|
|
4273
4679
|
ok: true,
|
|
4274
4680
|
subrepo,
|
|
@@ -5908,7 +6314,7 @@ function cmdBrain(args) {
|
|
|
5908
6314
|
// #170 — global brain cache at ~/.rcode/brain-cache/<sha1(repo+branch+paths)>/.
|
|
5909
6315
|
// Same source pulled from N projects = N clones today, 1 clone + N copies
|
|
5910
6316
|
// after this change. Cache TTL is configurable per source (defaults to 6h).
|
|
5911
|
-
const { execSync } = require('child_process');
|
|
6317
|
+
const { execSync, execFileSync: execFileSyncBrain } = require('child_process');
|
|
5912
6318
|
const crypto = require('crypto');
|
|
5913
6319
|
const os = require('os');
|
|
5914
6320
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'rcode-brain-'));
|
|
@@ -5978,13 +6384,15 @@ function cmdBrain(args) {
|
|
|
5978
6384
|
// Use --no-checkout + explicit sparse-checkout init + set + checkout
|
|
5979
6385
|
// because `git clone --sparse` combined with --filter=blob:none has
|
|
5980
6386
|
// an intermittent failure mode where git misreads the URL as a path.
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
6387
|
+
// execFileSync — repo/branch/tmp/sparsePaths from user config; no shell so
|
|
6388
|
+
// values with spaces, quotes, or semicolons cannot inject commands (#754).
|
|
6389
|
+
execFileSyncBrain('git', [
|
|
6390
|
+
'clone', '--depth=1', '--filter=blob:none', '--no-checkout',
|
|
6391
|
+
`--branch=${branch}`, repo, tmp,
|
|
6392
|
+
], { stdio: 'pipe' });
|
|
6393
|
+
execFileSyncBrain('git', ['-C', tmp, 'sparse-checkout', 'init', '--no-cone'], { stdio: 'pipe' });
|
|
6394
|
+
execFileSyncBrain('git', ['-C', tmp, 'sparse-checkout', 'set', ...sparsePaths], { stdio: 'pipe' });
|
|
6395
|
+
execFileSyncBrain('git', ['-C', tmp, 'checkout'], { stdio: 'pipe' });
|
|
5988
6396
|
|
|
5989
6397
|
// Warm cache before destination copy so a copy failure to dest still
|
|
5990
6398
|
// saves the next pull. Replace any stale slot atomically.
|
|
@@ -5993,7 +6401,7 @@ function cmdBrain(args) {
|
|
|
5993
6401
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
5994
6402
|
copyTree(tmp, cacheDir);
|
|
5995
6403
|
const commitSha = (() => {
|
|
5996
|
-
try { return
|
|
6404
|
+
try { return execFileSyncBrain('git', ['-C', tmp, 'rev-parse', 'HEAD'], { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim(); }
|
|
5997
6405
|
catch { return null; }
|
|
5998
6406
|
})();
|
|
5999
6407
|
fs.writeFileSync(cacheManifest, JSON.stringify({
|
|
@@ -6690,6 +7098,10 @@ function cmdRoadmapDetectStructure() {
|
|
|
6690
7098
|
* Thresholds (kept conservative — bump in config later if needed):
|
|
6691
7099
|
* - "consider closing" when >= 8 open phases under one milestone
|
|
6692
7100
|
* - "should close" when >= 12 open phases (hard nudge)
|
|
7101
|
+
*
|
|
7102
|
+
* Fix #893 — open-phase count is now scoped to the CURRENT milestone only.
|
|
7103
|
+
* Phases that belong to a prior milestone (different ## MN heading in
|
|
7104
|
+
* ROADMAP.md) are excluded so M2 phases never inflate M3's count.
|
|
6693
7105
|
*/
|
|
6694
7106
|
function cmdMilestoneHealth() {
|
|
6695
7107
|
const statePath = path.join(RCODE_DIR, 'state.json');
|
|
@@ -6698,12 +7110,69 @@ function cmdMilestoneHealth() {
|
|
|
6698
7110
|
try { state = JSON.parse(fs.readFileSync(statePath, 'utf8')); }
|
|
6699
7111
|
catch (e) { return { ok: false, error: `invalid state.json: ${e.message}` }; }
|
|
6700
7112
|
|
|
6701
|
-
|
|
6702
|
-
const
|
|
7113
|
+
// Prefer state.milestone; fall back to state.current_milestone for compat.
|
|
7114
|
+
const milestone = state.milestone || state.current_milestone || null;
|
|
7115
|
+
const allPhases = Array.isArray(state.phases) ? state.phases : [];
|
|
7116
|
+
|
|
7117
|
+
// --- milestone-scoped phase filtering (#893) ---
|
|
7118
|
+
// Parse ROADMAP.md to find which phase numbers belong to the current
|
|
7119
|
+
// milestone heading (## M1, ## Milestone 2, etc.). When ROADMAP is absent
|
|
7120
|
+
// or the milestone can't be matched, fall back to ALL phases so the health
|
|
7121
|
+
// check still works on unstructured projects.
|
|
7122
|
+
let milestonePhasesNumbers = null; // null = no filtering
|
|
7123
|
+
const roadmapPath = path.join(PROJECT_ROOT, '.planning', 'ROADMAP.md');
|
|
7124
|
+
if (milestone && fs.existsSync(roadmapPath)) {
|
|
7125
|
+
try {
|
|
7126
|
+
const text = fs.readFileSync(roadmapPath, 'utf8');
|
|
7127
|
+
const lines = text.split('\n');
|
|
7128
|
+
// Find the heading that matches the current milestone label.
|
|
7129
|
+
// Accepted forms: "## M3", "## Milestone 3", "## M3 — Some Title", etc.
|
|
7130
|
+
const milestoneId = String(milestone).trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
7131
|
+
const milestoneHeadRe = new RegExp(`^#{1,3}\\s+${milestoneId}[\\s—:\\-]`, 'i');
|
|
7132
|
+
const milestoneHeadBareRe = new RegExp(`^#{1,3}\\s+${milestoneId}\\s*$`, 'i');
|
|
7133
|
+
// Any milestone-level heading that is NOT a Phase heading
|
|
7134
|
+
const anyMilestoneHeadRe = /^#{1,3}\s+(?!Phase\s)/i;
|
|
7135
|
+
|
|
7136
|
+
let inCurrentMilestone = false;
|
|
7137
|
+
const phaseNums = new Set();
|
|
7138
|
+
const phaseRowRe = /^\|\s*(\d+(?:\.\d+)?)\s*\|/;
|
|
7139
|
+
const phaseHeadRe = /^#{2,4}\s*Phase\s+(\d+(?:\.\d+)?)/i;
|
|
7140
|
+
|
|
7141
|
+
for (const line of lines) {
|
|
7142
|
+
if (milestoneHeadRe.test(line) || milestoneHeadBareRe.test(line)) {
|
|
7143
|
+
inCurrentMilestone = true;
|
|
7144
|
+
continue;
|
|
7145
|
+
}
|
|
7146
|
+
// A different milestone-level heading ends this section
|
|
7147
|
+
if (inCurrentMilestone && anyMilestoneHeadRe.test(line) &&
|
|
7148
|
+
!milestoneHeadRe.test(line) && !milestoneHeadBareRe.test(line)) {
|
|
7149
|
+
break;
|
|
7150
|
+
}
|
|
7151
|
+
if (inCurrentMilestone) {
|
|
7152
|
+
const rm = phaseRowRe.exec(line);
|
|
7153
|
+
if (rm) { phaseNums.add(rm[1]); continue; }
|
|
7154
|
+
const rh = phaseHeadRe.exec(line);
|
|
7155
|
+
if (rh) { phaseNums.add(rh[1]); }
|
|
7156
|
+
}
|
|
7157
|
+
}
|
|
7158
|
+
if (phaseNums.size > 0) milestonePhasesNumbers = phaseNums;
|
|
7159
|
+
} catch { /* ROADMAP unreadable — skip filtering */ }
|
|
7160
|
+
}
|
|
7161
|
+
|
|
7162
|
+
// Select only phases that belong to the current milestone, or all if
|
|
7163
|
+
// ROADMAP filtering wasn't possible.
|
|
7164
|
+
const phases = milestonePhasesNumbers
|
|
7165
|
+
? allPhases.filter(p => {
|
|
7166
|
+
const num = String(p.number ?? p.id ?? '').trim();
|
|
7167
|
+
return milestonePhasesNumbers.has(num);
|
|
7168
|
+
})
|
|
7169
|
+
: allPhases;
|
|
7170
|
+
|
|
6703
7171
|
// "Open" = not done. State schema uses status: 'planned' | 'in_progress' |
|
|
6704
|
-
// 'completed' | 'verified' | 'shipped'. Treat anything not in
|
|
6705
|
-
//
|
|
6706
|
-
|
|
7172
|
+
// 'complete' | 'completed' | 'verified' | 'shipped'. Treat anything not in
|
|
7173
|
+
// that set as open. Fix #897 — 'complete' was excluded, causing
|
|
7174
|
+
// milestone-health to report done phases as open.
|
|
7175
|
+
const doneStatuses = new Set(['complete', 'completed', 'verified', 'shipped']);
|
|
6707
7176
|
const open = phases.filter(p => !doneStatuses.has(p.status));
|
|
6708
7177
|
const done = phases.filter(p => doneStatuses.has(p.status));
|
|
6709
7178
|
|
|
@@ -6720,6 +7189,7 @@ function cmdMilestoneHealth() {
|
|
|
6720
7189
|
recommendation,
|
|
6721
7190
|
threshold_consider: 8,
|
|
6722
7191
|
threshold_should: 12,
|
|
7192
|
+
milestone_scoped: milestonePhasesNumbers !== null,
|
|
6723
7193
|
};
|
|
6724
7194
|
}
|
|
6725
7195
|
|
|
@@ -6901,11 +7371,27 @@ function cmdFindFiles(rawArgs) {
|
|
|
6901
7371
|
return { ok: true, type, matches };
|
|
6902
7372
|
}
|
|
6903
7373
|
|
|
7374
|
+
// Closes #739 — parse human duration strings into milliseconds.
|
|
7375
|
+
// Supports: '30m' → 1800000, '2h' → 7200000, '1d' → 86400000.
|
|
7376
|
+
function parseDuration(str) {
|
|
7377
|
+
if (!str || typeof str !== 'string') return 3600000; // default 1h
|
|
7378
|
+
const m = str.trim().match(/^(\d+(?:\.\d+)?)([smhd]?)$/i);
|
|
7379
|
+
if (!m) return 3600000;
|
|
7380
|
+
const n = parseFloat(m[1]);
|
|
7381
|
+
switch ((m[2] || 'h').toLowerCase()) {
|
|
7382
|
+
case 'd': return Math.round(n * 86400000);
|
|
7383
|
+
case 'h': return Math.round(n * 3600000);
|
|
7384
|
+
case 'm': return Math.round(n * 60000);
|
|
7385
|
+
case 's': return Math.round(n * 1000);
|
|
7386
|
+
default: return Math.round(n * 3600000);
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
7389
|
+
|
|
6904
7390
|
async function main() {
|
|
6905
7391
|
const [, , subcommand, ...args] = process.argv;
|
|
6906
7392
|
// #473 guard runs before any subcommand. Skipped for read-only inspection
|
|
6907
7393
|
// so 'rcode-tools version' / 'help' / 'list-agents' work outside the project.
|
|
6908
|
-
const READ_ONLY_SUBCOMMANDS = new Set(['version', 'help', '--help', '-h', undefined, 'list-agents', 'agent-info', 'agent-skills']);
|
|
7394
|
+
const READ_ONLY_SUBCOMMANDS = new Set(['version', 'help', '--help', '-h', undefined, 'list-agents', 'agent-info', 'agent-skills', 'validate']);
|
|
6909
7395
|
if (!READ_ONLY_SUBCOMMANDS.has(subcommand)) {
|
|
6910
7396
|
assertCwdMatchesProjectRoot();
|
|
6911
7397
|
}
|
|
@@ -7099,6 +7585,42 @@ async function main() {
|
|
|
7099
7585
|
console.log(JSON.stringify(result, null, 2));
|
|
7100
7586
|
return;
|
|
7101
7587
|
}
|
|
7588
|
+
case 'validate': {
|
|
7589
|
+
// #747 — schema checks for state.json and config.yaml.
|
|
7590
|
+
// Usage: rcode-tools validate state|config|all
|
|
7591
|
+
const target = args[0] || 'all';
|
|
7592
|
+
if (!['state', 'config', 'all'].includes(target)) {
|
|
7593
|
+
console.error(`Unknown validate target: '${target}'. Valid: state, config, all`);
|
|
7594
|
+
process.exit(1);
|
|
7595
|
+
}
|
|
7596
|
+
const validateErrors = [];
|
|
7597
|
+
|
|
7598
|
+
if (target === 'state' || target === 'all') {
|
|
7599
|
+
const state = readState();
|
|
7600
|
+
if (!state) {
|
|
7601
|
+
validateErrors.push('state: state.json not found or empty');
|
|
7602
|
+
} else {
|
|
7603
|
+
if (!state.current_phase) validateErrors.push('state: missing current_phase');
|
|
7604
|
+
if (!state.current_milestone) validateErrors.push('state: missing current_milestone');
|
|
7605
|
+
if (state.schema_version !== 2) validateErrors.push(`state: schema_version should be 2, got ${state.schema_version}`);
|
|
7606
|
+
if (!Array.isArray(state.phases) && typeof state.phases !== 'object') validateErrors.push('state: phases must be array or object');
|
|
7607
|
+
}
|
|
7608
|
+
}
|
|
7609
|
+
|
|
7610
|
+
if (target === 'config' || target === 'all') {
|
|
7611
|
+
const config = readConfig();
|
|
7612
|
+
if (!config.project_name) validateErrors.push('config: missing project_name');
|
|
7613
|
+
if (!config.current_phase && !config.phase) validateErrors.push('config: missing current_phase or phase');
|
|
7614
|
+
}
|
|
7615
|
+
|
|
7616
|
+
if (validateErrors.length) {
|
|
7617
|
+
validateErrors.forEach(e => console.error('❌ ' + e));
|
|
7618
|
+
process.exit(1);
|
|
7619
|
+
} else {
|
|
7620
|
+
console.log('✅ All artifacts valid');
|
|
7621
|
+
}
|
|
7622
|
+
return;
|
|
7623
|
+
}
|
|
7102
7624
|
case 'roadmap': {
|
|
7103
7625
|
const roadmap = require(path.join(__dirname, 'lib', 'roadmap.cjs'));
|
|
7104
7626
|
const r = roadmap.dispatch(PROJECT_ROOT, args);
|
|
@@ -7117,7 +7639,19 @@ async function main() {
|
|
|
7117
7639
|
}
|
|
7118
7640
|
case 'config-set': {
|
|
7119
7641
|
const cfg = require(path.join(__dirname, 'lib', 'config.cjs'));
|
|
7120
|
-
|
|
7642
|
+
const csKey = args[0];
|
|
7643
|
+
const csVal = args.slice(1).join(' ');
|
|
7644
|
+
result = cfg.cmdSet(PROJECT_ROOT, csKey, csVal);
|
|
7645
|
+
// Fix #855 — keep state.json in sync when current_phase is updated via config-set.
|
|
7646
|
+
// One-way: config-set → state.json. The set-phase path writes config.yaml separately (below).
|
|
7647
|
+
if (csKey === 'current_phase' && csVal) {
|
|
7648
|
+
const stJson = readState() || defaultState();
|
|
7649
|
+
if (stJson.current_phase !== csVal) {
|
|
7650
|
+
stJson.current_phase = csVal;
|
|
7651
|
+
stJson.current_plan = 0;
|
|
7652
|
+
writeState(stJson);
|
|
7653
|
+
}
|
|
7654
|
+
}
|
|
7121
7655
|
break;
|
|
7122
7656
|
}
|
|
7123
7657
|
case 'config-check-yolo': {
|
|
@@ -7165,6 +7699,79 @@ async function main() {
|
|
|
7165
7699
|
result = { active: true, mode, scope: scopeRaw || 'global', expires_at: expiresAt, reason: 'yolo active' };
|
|
7166
7700
|
break;
|
|
7167
7701
|
}
|
|
7702
|
+
case 'yolo': {
|
|
7703
|
+
// Closes #739 — scoped yolo mode with TTL and milestone/phase scope.
|
|
7704
|
+
// Usage:
|
|
7705
|
+
// yolo scoped --ttl 2h --scope milestone → enable for current milestone
|
|
7706
|
+
// yolo scoped --ttl 30m --scope phase → enable for current phase
|
|
7707
|
+
// yolo off → disable immediately
|
|
7708
|
+
// yolo status → show current yolo state
|
|
7709
|
+
const sub = args[0];
|
|
7710
|
+
const cfg = require(path.join(__dirname, 'lib', 'config.cjs'));
|
|
7711
|
+
switch (sub) {
|
|
7712
|
+
case 'scoped': {
|
|
7713
|
+
const ttlArg = (args.find(a => a.startsWith('--ttl='))?.slice(6))
|
|
7714
|
+
|| (args.indexOf('--ttl') !== -1 ? args[args.indexOf('--ttl') + 1] : null)
|
|
7715
|
+
|| '1h';
|
|
7716
|
+
const scope = (args.find(a => a.startsWith('--scope='))?.slice(8))
|
|
7717
|
+
|| (args.indexOf('--scope') !== -1 ? args[args.indexOf('--scope') + 1] : null)
|
|
7718
|
+
|| 'phase';
|
|
7719
|
+
const ms = parseDuration(ttlArg);
|
|
7720
|
+
const expires = Date.now() + ms;
|
|
7721
|
+
const st = readState() || {};
|
|
7722
|
+
st.yolo = {
|
|
7723
|
+
enabled: true,
|
|
7724
|
+
expires,
|
|
7725
|
+
scope,
|
|
7726
|
+
milestone: st.current_milestone || null,
|
|
7727
|
+
phase: st.current_phase || null,
|
|
7728
|
+
};
|
|
7729
|
+
writeState(st);
|
|
7730
|
+
// Also persist to config.yaml so config-check-yolo can read it.
|
|
7731
|
+
cfg.cmdSet(PROJECT_ROOT, 'mode', 'yolo');
|
|
7732
|
+
cfg.cmdSet(PROJECT_ROOT, 'yolo_scope', scope === 'milestone'
|
|
7733
|
+
? `milestone:${st.current_milestone || 'unknown'}`
|
|
7734
|
+
: `phase:${st.current_phase || 'unknown'}`);
|
|
7735
|
+
cfg.cmdSet(PROJECT_ROOT, 'yolo_ttl', new Date(expires).toISOString());
|
|
7736
|
+
result = { ok: true, enabled: true, scope, ttl: ttlArg, expires: new Date(expires).toISOString() };
|
|
7737
|
+
console.log(`Yolo mode: enabled (${scope} scope, expires in ${ttlArg})`);
|
|
7738
|
+
break;
|
|
7739
|
+
}
|
|
7740
|
+
case 'off': {
|
|
7741
|
+
const st = readState() || {};
|
|
7742
|
+
st.yolo = { enabled: false };
|
|
7743
|
+
writeState(st);
|
|
7744
|
+
cfg.cmdSet(PROJECT_ROOT, 'mode', 'guided');
|
|
7745
|
+
cfg.cmdSet(PROJECT_ROOT, 'yolo_scope', '');
|
|
7746
|
+
cfg.cmdSet(PROJECT_ROOT, 'yolo_ttl', '');
|
|
7747
|
+
result = { ok: true, enabled: false };
|
|
7748
|
+
console.log('Yolo mode: off');
|
|
7749
|
+
break;
|
|
7750
|
+
}
|
|
7751
|
+
case 'status': {
|
|
7752
|
+
const st = readState() || {};
|
|
7753
|
+
const y = st.yolo;
|
|
7754
|
+
if (!y?.enabled || Date.now() > (y.expires || 0)) {
|
|
7755
|
+
result = { active: false };
|
|
7756
|
+
console.log('Yolo mode: off');
|
|
7757
|
+
} else {
|
|
7758
|
+
const remainingMs = y.expires - Date.now();
|
|
7759
|
+
const remainingMin = Math.round(remainingMs / 60000);
|
|
7760
|
+
result = { active: true, scope: y.scope, expires: new Date(y.expires).toISOString(), remaining_min: remainingMin };
|
|
7761
|
+
console.log(`Yolo mode: on (${y.scope} scope, ${remainingMin}m remaining)`);
|
|
7762
|
+
}
|
|
7763
|
+
break;
|
|
7764
|
+
}
|
|
7765
|
+
default: {
|
|
7766
|
+
console.error('Usage: yolo <scoped|off|status>');
|
|
7767
|
+
console.error(' yolo scoped --ttl <2h|30m|1d> --scope <milestone|phase>');
|
|
7768
|
+
console.error(' yolo off');
|
|
7769
|
+
console.error(' yolo status');
|
|
7770
|
+
process.exit(1);
|
|
7771
|
+
}
|
|
7772
|
+
}
|
|
7773
|
+
break;
|
|
7774
|
+
}
|
|
7168
7775
|
case 'verify': {
|
|
7169
7776
|
const verify = require(path.join(__dirname, 'lib', 'verify.cjs'));
|
|
7170
7777
|
result = verify.dispatch(PROJECT_ROOT, args);
|
|
@@ -7242,6 +7849,7 @@ async function main() {
|
|
|
7242
7849
|
console.log(' phase add <name> [--decimal <parent>] → add phase (integer to current milestone, or --decimal slots under parent as parent.M)');
|
|
7243
7850
|
console.log(' phase next-range [count] → return next N contiguous free phase numbers (#730)');
|
|
7244
7851
|
console.log(' phase scaffold-milestone --names "n1|n2|..." → bulk-create phase folders for a milestone (#731)');
|
|
7852
|
+
console.log(' phase scaffold-all → create missing phase folders for all phases in ROADMAP.md (#731)');
|
|
7245
7853
|
console.log(' workflow-config-audit → find workflows still referencing .planning/config.json (#733)');
|
|
7246
7854
|
console.log(' commit "<msg>" [--files p1 p2 ...] → atomic git commit with conventional-commits validation (no AI attribution, no --no-verify, no auto-push)');
|
|
7247
7855
|
console.log(' commit-to-subrepo --subrepo <p> "<msg>" → atomic commit inside a git subrepo (same validation as commit)');
|
|
@@ -7266,10 +7874,15 @@ async function main() {
|
|
|
7266
7874
|
console.log(' notes <subcommand> [args] → manage project notes');
|
|
7267
7875
|
console.log(' config <subcommand> [args] → read/write project config');
|
|
7268
7876
|
console.log(' notify send --title "<t>" [--body "<b>"] [--event <e>] [--only slack|discord|teams] → post to configured webhooks');
|
|
7269
|
-
console.log(' roadmap <get-phase|list-phases|update-plan-progress|clear> → .planning/ROADMAP.md operations');
|
|
7877
|
+
console.log(' roadmap <get-phase|list-phases|update-plan-progress|clear|detect> → .planning/ROADMAP.md operations');
|
|
7878
|
+
console.log(' roadmap detect → report ROADMAP convention in use (single vs per-milestone) (#734)');
|
|
7879
|
+
console.log(' validate <state|config|all> → schema checks for state.json and config.yaml (#747)');
|
|
7270
7880
|
console.log(' config-get <dotted.key> → read scalar from .rcode/config.yaml');
|
|
7271
7881
|
console.log(' config-set <dotted.key> <value> → atomically set a value in .rcode/config.yaml');
|
|
7272
7882
|
console.log(' config-check-yolo [--phase N] [--workflow W] → check if yolo mode is active for scope (#739)');
|
|
7883
|
+
console.log(' yolo scoped --ttl <2h|30m|1d> --scope <milestone|phase> → enable scoped yolo with TTL (#739)');
|
|
7884
|
+
console.log(' yolo off → disable yolo mode immediately');
|
|
7885
|
+
console.log(' yolo status → show current yolo state and remaining TTL');
|
|
7273
7886
|
console.log(' handoff write --from <skill> --to <skill> --phase N [--context "..."] → write cross-skill handoff token (#741)');
|
|
7274
7887
|
console.log(' handoff read [--from <skill>] → read most recent handoff for this skill (#741)');
|
|
7275
7888
|
console.log(' handoff clear → consume (clear) the latest handoff token (#741)');
|
|
@@ -7296,6 +7909,7 @@ async function main() {
|
|
|
7296
7909
|
console.log(' state add-blocker "<description>" → append to blockers[]');
|
|
7297
7910
|
console.log(' state resolve-blocker <index>|--all|--phase <N> --issue <N>|--commit <sha>|--noref → mark blocker(s) resolved (#654, #656)');
|
|
7298
7911
|
console.log(' state record-session → update last_session timestamp');
|
|
7912
|
+
console.log(' state sync-from-git → recover phase/sprint state from git commit history (#915)');
|
|
7299
7913
|
console.log(' state record-council --slug <s> --panel <csv> --artifact <path>');
|
|
7300
7914
|
console.log(' state record-chain --slug <s> --agents <csv> --artifacts <path>');
|
|
7301
7915
|
console.log(' state insert-phase --number <N.M> --name <slug>');
|