multimodel-dev-os 2.0.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai/intelligence/README.md +14 -0
- package/.ai/intelligence/apply-log.schema.json +65 -0
- package/.ai/intelligence/feedback-log.example.jsonl +2 -0
- package/.ai/intelligence/feedback.schema.json +47 -0
- package/.ai/intelligence/improvement-proposal.schema.json +70 -0
- package/.ai/intelligence/learning-rules.example.md +18 -0
- package/.ai/intelligence/memory.schema.json +97 -0
- package/.ai/policies/approval-gates.md +35 -0
- package/.ai/policies/memory-policy.md +30 -0
- package/.ai/policies/self-improvement-policy.md +39 -0
- package/.ai/proposals/README.md +44 -0
- package/.ai/proposals/apply-operation.example.json +22 -0
- package/.ai/registries/capabilities.yaml +73 -0
- package/.ai/registries/tools.yaml +84 -0
- package/.ai/registries/workflows.yaml +217 -0
- package/README.md +218 -97
- package/bin/multimodel-dev-os.js +2899 -10
- package/docs/.vitepress/config.js +23 -1
- package/docs/CLI.md +89 -2
- package/docs/adapter-sync.md +27 -0
- package/docs/adapters.md +16 -0
- package/docs/agent-handoff.md +40 -0
- package/docs/approved-proposal-apply.md +156 -0
- package/docs/capability-registry.md +24 -0
- package/docs/cli-roadmap.md +14 -22
- package/docs/faq.md +1 -1
- package/docs/feedback-learning.md +33 -0
- package/docs/future-proof-architecture.md +22 -0
- package/docs/hash-compressed-memory.md +72 -0
- package/docs/improvement-proposals.md +70 -0
- package/docs/index.md +5 -5
- package/docs/learning-rules.md +36 -0
- package/docs/npm-publishing.md +16 -16
- package/docs/public/llms-full.txt +32 -2
- package/docs/public/llms.txt +43 -2
- package/docs/public/sitemap.xml +81 -1
- package/docs/quickstart.md +14 -16
- package/docs/real-repo-onboarding.md +27 -0
- package/docs/release-policy.md +5 -22
- package/docs/repository-command-center.md +52 -0
- package/docs/self-improving-codebase.md +46 -0
- package/docs/template-recommendation.md +22 -0
- package/docs/templates-guide.md +14 -3
- package/docs/tool-registry.md +21 -0
- package/docs/v2-release-checklist.md +1 -1
- package/docs/v2-roadmap.md +21 -1
- package/docs/workflow-orchestration.md +59 -0
- package/package.json +1 -1
- package/scripts/install.ps1 +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/prepublish-guard.js +3 -3
- package/scripts/verify.js +107 -3
package/bin/multimodel-dev-os.js
CHANGED
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
9
|
-
import { join, dirname, resolve } from 'path';
|
|
9
|
+
import { join, dirname, resolve, relative, isAbsolute, basename } from 'path';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
|
+
import { createHash } from 'crypto';
|
|
11
12
|
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = dirname(__filename);
|
|
14
15
|
const sourceRoot = resolve(__dirname, '..');
|
|
15
16
|
|
|
16
|
-
let version = '2.0.
|
|
17
|
+
let version = '2.0.1';
|
|
17
18
|
try {
|
|
18
19
|
const pkgData = JSON.parse(readFileSync(resolve(sourceRoot, 'package.json'), 'utf8'));
|
|
19
20
|
version = pkgData.version;
|
|
@@ -42,7 +43,14 @@ function parseArgs(args) {
|
|
|
42
43
|
threshold: null,
|
|
43
44
|
registry: null,
|
|
44
45
|
allRegistries: false,
|
|
45
|
-
release: false
|
|
46
|
+
release: false,
|
|
47
|
+
type: 'unknown',
|
|
48
|
+
tags: '',
|
|
49
|
+
files: '',
|
|
50
|
+
title: null,
|
|
51
|
+
approved: false,
|
|
52
|
+
intelligence: false,
|
|
53
|
+
onboarding: false
|
|
46
54
|
};
|
|
47
55
|
|
|
48
56
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -67,6 +75,10 @@ function parseArgs(args) {
|
|
|
67
75
|
params.allRegistries = true;
|
|
68
76
|
} else if (arg === '--release') {
|
|
69
77
|
params.release = true;
|
|
78
|
+
} else if (arg === '--intelligence') {
|
|
79
|
+
params.intelligence = true;
|
|
80
|
+
} else if (arg === '--onboarding') {
|
|
81
|
+
params.onboarding = true;
|
|
70
82
|
} else if (arg === '--json') {
|
|
71
83
|
params.json = true;
|
|
72
84
|
} else if (arg === '--threshold') {
|
|
@@ -81,8 +93,16 @@ function parseArgs(args) {
|
|
|
81
93
|
params.stack = args[++i];
|
|
82
94
|
} else if (arg === '--mobile') {
|
|
83
95
|
params.mobile = args[++i];
|
|
84
|
-
} else if (arg === '--
|
|
85
|
-
params.
|
|
96
|
+
} else if (arg === '--type') {
|
|
97
|
+
params.type = args[++i];
|
|
98
|
+
} else if (arg === '--tags') {
|
|
99
|
+
params.tags = args[++i];
|
|
100
|
+
} else if (arg === '--files') {
|
|
101
|
+
params.files = args[++i];
|
|
102
|
+
} else if (arg === '--title') {
|
|
103
|
+
params.title = args[++i];
|
|
104
|
+
} else if (arg === '--approved') {
|
|
105
|
+
params.approved = true;
|
|
86
106
|
} else if (!params.command && !arg.startsWith('-')) {
|
|
87
107
|
params.command = arg;
|
|
88
108
|
}
|
|
@@ -90,6 +110,24 @@ function parseArgs(args) {
|
|
|
90
110
|
return params;
|
|
91
111
|
}
|
|
92
112
|
|
|
113
|
+
function getPositionalArgs(args) {
|
|
114
|
+
const positionalArgs = [];
|
|
115
|
+
for (let i = 0; i < args.length; i++) {
|
|
116
|
+
const arg = args[i];
|
|
117
|
+
if (arg === '--target' || arg === '-t' || arg === '--template' || arg === '--adapter' || arg === '-a' ||
|
|
118
|
+
arg === '--threshold' || arg === '--registry' || arg === '--model-preset' || arg === '--agent' ||
|
|
119
|
+
arg === '--stack' || arg === '--mobile' || arg === '--type' || arg === '--tags' || arg === '--files' ||
|
|
120
|
+
arg === '--title') {
|
|
121
|
+
i++; // skip next arg (its value)
|
|
122
|
+
} else if (arg.startsWith('-')) {
|
|
123
|
+
// it's a flag, skip
|
|
124
|
+
} else {
|
|
125
|
+
positionalArgs.push(arg);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return positionalArgs;
|
|
129
|
+
}
|
|
130
|
+
|
|
93
131
|
const params = parseArgs(ARGS);
|
|
94
132
|
const COMMAND = params.command;
|
|
95
133
|
|
|
@@ -143,6 +181,74 @@ if (COMMAND === 'init') {
|
|
|
143
181
|
handleInit(params);
|
|
144
182
|
} else if (COMMAND === 'verify') {
|
|
145
183
|
handleVerify(params);
|
|
184
|
+
} else if (COMMAND === 'scan') {
|
|
185
|
+
handleScan(params);
|
|
186
|
+
} else if (COMMAND === 'memory') {
|
|
187
|
+
const sub = ARGS[1];
|
|
188
|
+
if (sub === 'build') {
|
|
189
|
+
handleMemoryBuild(params);
|
|
190
|
+
} else if (sub === 'refresh') {
|
|
191
|
+
handleMemoryRefresh(params);
|
|
192
|
+
} else if (sub === 'diff') {
|
|
193
|
+
handleMemoryDiff(params);
|
|
194
|
+
} else {
|
|
195
|
+
console.error(`\x1b[31mError: Please specify a memory subcommand: build, refresh, or diff.\x1b[0m`);
|
|
196
|
+
console.error(`Example: node bin/multimodel-dev-os.js memory build`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
} else if (COMMAND === 'feedback') {
|
|
200
|
+
const sub = ARGS[1];
|
|
201
|
+
if (sub === 'add') {
|
|
202
|
+
handleFeedbackAdd(params);
|
|
203
|
+
} else if (sub === 'list') {
|
|
204
|
+
handleFeedbackList(params);
|
|
205
|
+
} else if (sub === 'summarize') {
|
|
206
|
+
handleFeedbackSummarize(params);
|
|
207
|
+
} else {
|
|
208
|
+
console.error(`\x1b[31mError: Please specify a feedback subcommand: add, list, or summarize.\x1b[0m`);
|
|
209
|
+
console.log(`Example: node bin/multimodel-dev-os.js feedback add "Prefer CSS Modules"`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
} else if (COMMAND === 'improve') {
|
|
213
|
+
const positional = getPositionalArgs(ARGS);
|
|
214
|
+
const sub = positional[1];
|
|
215
|
+
if (sub === 'propose') {
|
|
216
|
+
handleImprovePropose(params);
|
|
217
|
+
} else if (sub === 'review') {
|
|
218
|
+
handleImproveReview(params);
|
|
219
|
+
} else if (sub === 'status') {
|
|
220
|
+
handleImproveStatus(params);
|
|
221
|
+
} else if (sub === 'validate') {
|
|
222
|
+
const proposalFile = positional[2];
|
|
223
|
+
if (!proposalFile) {
|
|
224
|
+
console.error(`\x1b[31mError: Please specify a proposal file path.\x1b[0m`);
|
|
225
|
+
console.log(`Example: node bin/multimodel-dev-os.js improve validate .ai/proposals/proposal-xxxx.md`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
handleImproveValidate(proposalFile, params);
|
|
229
|
+
} else if (sub === 'diff') {
|
|
230
|
+
const proposalFile = positional[2];
|
|
231
|
+
if (!proposalFile) {
|
|
232
|
+
console.error(`\x1b[31mError: Please specify a proposal file path.\x1b[0m`);
|
|
233
|
+
console.log(`Example: node bin/multimodel-dev-os.js improve diff .ai/proposals/proposal-xxxx.md`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
handleImproveDiff(proposalFile, params);
|
|
237
|
+
} else if (sub === 'apply') {
|
|
238
|
+
const proposalFile = positional[2];
|
|
239
|
+
if (!proposalFile) {
|
|
240
|
+
console.error(`\x1b[31mError: Please specify a proposal file path.\x1b[0m`);
|
|
241
|
+
console.log(`Example: node bin/multimodel-dev-os.js improve apply .ai/proposals/proposal-xxxx.md --approved`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
handleImproveApply(proposalFile, params);
|
|
245
|
+
} else if (sub === 'log') {
|
|
246
|
+
handleImproveLog(params);
|
|
247
|
+
} else {
|
|
248
|
+
console.error(`\x1b[31mError: Please specify an improve subcommand: propose, review, status, validate, diff, apply, or log.\x1b[0m`);
|
|
249
|
+
console.log(`Example: node bin/multimodel-dev-os.js improve validate .ai/proposals/proposal-xxxx.md`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
146
252
|
} else if (COMMAND === 'templates' || COMMAND === 'list-templates') {
|
|
147
253
|
handleListTemplates(params);
|
|
148
254
|
} else if (COMMAND === 'show-template') {
|
|
@@ -213,6 +319,96 @@ if (COMMAND === 'init') {
|
|
|
213
319
|
process.exit(1);
|
|
214
320
|
}
|
|
215
321
|
handleShowSkill(sName, params);
|
|
322
|
+
} else if (COMMAND === 'status') {
|
|
323
|
+
handleStatus(params);
|
|
324
|
+
} else if (COMMAND === 'workflow') {
|
|
325
|
+
const positional = getPositionalArgs(ARGS);
|
|
326
|
+
const sub = positional[1];
|
|
327
|
+
if (sub === 'list') {
|
|
328
|
+
handleWorkflowList(params);
|
|
329
|
+
} else if (sub === 'show') {
|
|
330
|
+
const wName = positional[2];
|
|
331
|
+
if (!wName) {
|
|
332
|
+
console.error('\x1b[31mError: Please specify a workflow name.\x1b[0m');
|
|
333
|
+
console.log('Example: node bin/multimodel-dev-os.js workflow show repo-health');
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
handleWorkflowShow(wName, params);
|
|
337
|
+
} else if (sub === 'plan') {
|
|
338
|
+
const wName = positional[2];
|
|
339
|
+
if (!wName) {
|
|
340
|
+
console.error('\x1b[31mError: Please specify a workflow name.\x1b[0m');
|
|
341
|
+
console.log('Example: node bin/multimodel-dev-os.js workflow plan repo-health');
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
handleWorkflowPlan(wName, params);
|
|
345
|
+
} else if (sub === 'run') {
|
|
346
|
+
const wName = positional[2];
|
|
347
|
+
if (!wName) {
|
|
348
|
+
console.error('\x1b[31mError: Please specify a workflow name.\x1b[0m');
|
|
349
|
+
console.log('Example: node bin/multimodel-dev-os.js workflow run repo-health');
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
handleWorkflowRun(wName, params);
|
|
353
|
+
} else {
|
|
354
|
+
console.error('\x1b[31mError: Please specify a workflow subcommand: list, show, plan, or run.\x1b[0m');
|
|
355
|
+
console.log('Example: node bin/multimodel-dev-os.js workflow list');
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
} else if (COMMAND === 'handoff') {
|
|
359
|
+
const positional = getPositionalArgs(ARGS);
|
|
360
|
+
const sub = positional[1];
|
|
361
|
+
if (sub === 'build') {
|
|
362
|
+
handleHandoffBuild(params);
|
|
363
|
+
} else if (sub === 'show') {
|
|
364
|
+
handleHandoffShow(params);
|
|
365
|
+
} else {
|
|
366
|
+
console.error('\x1b[31mError: Please specify a handoff subcommand: build or show.\x1b[0m');
|
|
367
|
+
console.log('Example: node bin/multimodel-dev-os.js handoff build');
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
} else if (COMMAND === 'onboard') {
|
|
371
|
+
const positional = getPositionalArgs(ARGS);
|
|
372
|
+
const sub = positional[1];
|
|
373
|
+
if (sub === 'analyze') {
|
|
374
|
+
handleOnboardAnalyze(params);
|
|
375
|
+
} else if (sub === 'recommend') {
|
|
376
|
+
handleOnboardRecommend(params);
|
|
377
|
+
} else if (sub === 'plan') {
|
|
378
|
+
handleOnboardPlan(params);
|
|
379
|
+
} else if (sub === 'apply') {
|
|
380
|
+
handleOnboardApply(params);
|
|
381
|
+
} else if (sub === 'status') {
|
|
382
|
+
handleOnboardStatus(params);
|
|
383
|
+
} else {
|
|
384
|
+
console.error('\x1b[31mError: Please specify an onboard subcommand: analyze, recommend, plan, apply, or status.\x1b[0m');
|
|
385
|
+
console.log('Example: node bin/multimodel-dev-os.js onboard analyze');
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
} else if (COMMAND === 'adapter') {
|
|
389
|
+
const positional = getPositionalArgs(ARGS);
|
|
390
|
+
const sub = positional[1];
|
|
391
|
+
if (sub === 'status') {
|
|
392
|
+
handleAdapterStatus(params);
|
|
393
|
+
} else if (sub === 'diff') {
|
|
394
|
+
const aName = positional[2];
|
|
395
|
+
if (!aName) {
|
|
396
|
+
console.error('\x1b[31mError: Please specify an adapter name (e.g. cursor, claude) or "all".\x1b[0m');
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
handleAdapterDiff(aName, params);
|
|
400
|
+
} else if (sub === 'sync') {
|
|
401
|
+
const aName = positional[2];
|
|
402
|
+
if (!aName) {
|
|
403
|
+
console.error('\x1b[31mError: Please specify an adapter name or "all" to sync.\x1b[0m');
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
handleAdapterSync(aName, params);
|
|
407
|
+
} else {
|
|
408
|
+
console.error('\x1b[31mError: Please specify an adapter subcommand: status, diff, or sync.\x1b[0m');
|
|
409
|
+
console.log('Example: node bin/multimodel-dev-os.js adapter status');
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
216
412
|
} else {
|
|
217
413
|
console.error(`\x1b[31mUnknown command: ${COMMAND}\x1b[0m`);
|
|
218
414
|
showHelp();
|
|
@@ -225,6 +421,15 @@ function showHelp() {
|
|
|
225
421
|
console.log('Usage: node bin/multimodel-dev-os.js <command> [options]\n');
|
|
226
422
|
console.log('Commands:');
|
|
227
423
|
console.log(' init Initialize a project with configs and adapters');
|
|
424
|
+
console.log(' scan Scan project structure and framework signals');
|
|
425
|
+
console.log(' status Show compact dashboard summarizing repository intelligence state');
|
|
426
|
+
console.log(' memory <subcmd> Manage hash-compressed codebase memory (subcmd: build, refresh, diff)');
|
|
427
|
+
console.log(' feedback <subcmd> Manage developer feedback loops (subcmd: add, list, summarize)');
|
|
428
|
+
console.log(' improve <subcmd> Manage codebase self-improvement proposals (subcmd: propose, review, status, validate, diff, apply, log)');
|
|
429
|
+
console.log(' workflow <subcmd> Orchestrate read-only development workflow pipelines (subcmd: list, show, plan, run)');
|
|
430
|
+
console.log(' handoff <subcmd> Compile or print token-compressed agent session summaries (subcmd: build, show)');
|
|
431
|
+
console.log(' onboard <subcmd> Safely integrate MultiModel Dev OS into existing repo (subcmd: analyze, recommend, plan, apply, status)');
|
|
432
|
+
console.log(' adapter <subcmd> Manage and sync rule/settings files for IDE adapters (subcmd: status, diff, sync)');
|
|
228
433
|
console.log(' verify Validate structural integrity of an existing project');
|
|
229
434
|
console.log(' templates List all built-in template profiles with details');
|
|
230
435
|
console.log(' list-templates Alias for templates command');
|
|
@@ -244,10 +449,17 @@ function showHelp() {
|
|
|
244
449
|
console.log(' show-skill <s> View prompt contents of target workspace skill <s>\n');
|
|
245
450
|
console.log('Options:');
|
|
246
451
|
console.log(' -t, --target <path> Target folder destination (default: current working directory)');
|
|
452
|
+
console.log(' --type <type> Feedback classification (correction, preference, bug, etc.)');
|
|
453
|
+
console.log(' --tags <list> Comma-separated descriptor tags for feedback');
|
|
454
|
+
console.log(' --files <list> Comma-separated target files for feedback');
|
|
455
|
+
console.log(' --title <text> Specifies title for codebase improvement proposal');
|
|
456
|
+
console.log(' --approved Explicitly approve and execute proposal/onboarding/adapter sync writes');
|
|
247
457
|
console.log(' --template <name> Template profile: nextjs-saas, expo-react-native-android, etc.');
|
|
248
458
|
console.log(' -a, --adapter <name> Inject specific adapter: cursor, claude, vscode, gemini, etc.');
|
|
249
459
|
console.log(' --caveman Use minimal-token templates (~79% fewer tokens)');
|
|
250
460
|
console.log(' --tokens Run a deeper token-sink size analysis during doctor checkup');
|
|
461
|
+
console.log(' --intelligence Run diagnostic checkup of repository intelligence config');
|
|
462
|
+
console.log(' --onboarding Run diagnostic checkup of repository onboarding setup');
|
|
251
463
|
console.log(' --json Output raw JSON data for listing commands (models, adapters, templates)');
|
|
252
464
|
console.log(' --threshold <val> Set custom size threshold for doctor tokens checks (e.g. 50KB)');
|
|
253
465
|
console.log(' --registry <path> Override default registry (for templates/adapters list or check)');
|
|
@@ -316,8 +528,9 @@ function handleInit(options) {
|
|
|
316
528
|
// Check if requested template is planned
|
|
317
529
|
const tInfo = TEMPLATES[options.template];
|
|
318
530
|
if (tInfo && tInfo.status === 'planned') {
|
|
319
|
-
console.warn(` \x1b[33m[WARNING] Template '${options.template}' is a
|
|
320
|
-
console.warn(`
|
|
531
|
+
console.warn(` \x1b[33m[WARNING] Template '${options.template}' is planned for a future release and is not yet available.\x1b[0m`);
|
|
532
|
+
console.warn(` To view available templates, run: \x1b[36mnpx multimodel-dev-os templates\x1b[0m`);
|
|
533
|
+
console.warn(` Falling back to the stable \x1b[32m'general-app'\x1b[0m profile...\n`);
|
|
321
534
|
options.template = 'general-app';
|
|
322
535
|
}
|
|
323
536
|
|
|
@@ -331,7 +544,9 @@ function handleInit(options) {
|
|
|
331
544
|
// Source path mapping for core files
|
|
332
545
|
let templateDir = join(sourceRoot, 'examples', options.template);
|
|
333
546
|
if (!existsSync(templateDir)) {
|
|
334
|
-
console.warn(` \x1b[33m[WARNING] Template '${options.template}'
|
|
547
|
+
console.warn(` \x1b[33m[WARNING] Template '${options.template}' source files could not be found.\x1b[0m`);
|
|
548
|
+
console.warn(` To view available templates, run: \x1b[36mnpx multimodel-dev-os templates\x1b[0m`);
|
|
549
|
+
console.warn(` Falling back to the stable \x1b[32m'general-app'\x1b[0m profile...\n`);
|
|
335
550
|
templateDir = join(sourceRoot, 'examples', 'general-app');
|
|
336
551
|
}
|
|
337
552
|
|
|
@@ -373,7 +588,7 @@ function handleInit(options) {
|
|
|
373
588
|
}
|
|
374
589
|
|
|
375
590
|
// Fallback to copy default global folders if files aren't already included by template
|
|
376
|
-
const globalAiSubdirs = ['context', 'agents', 'skills', 'prompts', 'checks', 'templates', 'session-logs'];
|
|
591
|
+
const globalAiSubdirs = ['context', 'agents', 'skills', 'prompts', 'checks', 'templates', 'session-logs', 'registries', 'proposals', 'intelligence'];
|
|
377
592
|
globalAiSubdirs.forEach(sub => {
|
|
378
593
|
const globalPath = join(sourceRoot, '.ai', sub);
|
|
379
594
|
if (existsSync(globalPath)) {
|
|
@@ -501,6 +716,27 @@ function handleInit(options) {
|
|
|
501
716
|
}
|
|
502
717
|
|
|
503
718
|
console.log(`\n\x1b[32m✔ Project initialized successfully! [Total Operations: ${operations.length}]\x1b[0m\n`);
|
|
719
|
+
console.log(`\x1b[36mNext Steps to Complete Integration:\x1b[0m`);
|
|
720
|
+
console.log(` 1. \x1b[1mEdit AGENTS.md\x1b[0m in your project root to document your stack context.`);
|
|
721
|
+
console.log(` 2. \x1b[1mEdit .ai/config.yaml\x1b[0m to configure active model routing presets.`);
|
|
722
|
+
if (options.adapters.length > 0) {
|
|
723
|
+
console.log(` 3. \x1b[1mActivate IDE / Agent Rules:\x1b[0m`);
|
|
724
|
+
console.log(` Ensure adapter configuration files are copied or linked to the root of your workspace:`);
|
|
725
|
+
options.adapters.forEach(adapter => {
|
|
726
|
+
const a = ADAPTERS[adapter];
|
|
727
|
+
if (a && a.rules_file) {
|
|
728
|
+
console.log(` - For \x1b[32m${a.name || adapter}\x1b[0m: Check the root-level \x1b[33m${a.rules_file}\x1b[0m file`);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
} else {
|
|
732
|
+
console.log(` 3. \x1b[1mSelect IDE / Tool Adapters:\x1b[0m`);
|
|
733
|
+
console.log(` To generate rules for Cursor, Claude Code, etc., run:`);
|
|
734
|
+
console.log(` \x1b[36mnpx multimodel-dev-os init --adapter cursor --adapter claude\x1b[0m`);
|
|
735
|
+
}
|
|
736
|
+
console.log(` 4. \x1b[1mRun Diagnostics:\x1b[0m`);
|
|
737
|
+
console.log(` Verify your workspace structural compliance:`);
|
|
738
|
+
console.log(` \x1b[36mnpx multimodel-dev-os validate\x1b[0m`);
|
|
739
|
+
console.log(` \x1b[36mnpx multimodel-dev-os doctor\x1b[0m\n`);
|
|
504
740
|
}
|
|
505
741
|
|
|
506
742
|
function handleVerify(options) {
|
|
@@ -549,9 +785,11 @@ function handleVerify(options) {
|
|
|
549
785
|
console.log('\n=====================================');
|
|
550
786
|
if (failed > 0) {
|
|
551
787
|
console.error(` \x1b[31mVerification FAILED. [Passed: ${passed}, Failed: ${failed}]\x1b[0m\n`);
|
|
788
|
+
if (options && options.noExit) return false;
|
|
552
789
|
process.exit(1);
|
|
553
790
|
} else {
|
|
554
791
|
console.log(` \x1b[32mVerification PASSED. [All ${passed} files present]\x1b[0m\n`);
|
|
792
|
+
if (options && options.noExit) return true;
|
|
555
793
|
process.exit(0);
|
|
556
794
|
}
|
|
557
795
|
}
|
|
@@ -565,6 +803,14 @@ function handleDoctor(options) {
|
|
|
565
803
|
handleDoctorRelease(options);
|
|
566
804
|
return;
|
|
567
805
|
}
|
|
806
|
+
if (options.intelligence) {
|
|
807
|
+
handleDoctorIntelligence(options);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (options.onboarding) {
|
|
811
|
+
handleDoctorOnboarding(options);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
568
814
|
console.log(`\n🩺 \x1b[36mRunning advisory doctor checkup in: ${options.target}\x1b[0m\n`);
|
|
569
815
|
|
|
570
816
|
let warnings = 0;
|
|
@@ -645,7 +891,7 @@ function handleDoctor(options) {
|
|
|
645
891
|
checkAdapter('vscode', '.vscode/settings.json');
|
|
646
892
|
checkAdapter('antigravity', '.gemini/settings.json');
|
|
647
893
|
} else {
|
|
648
|
-
warn('.ai/config.yaml is missing
|
|
894
|
+
warn('MultiModel Dev OS is not initialized (.ai/config.yaml is missing). Run "npx multimodel-dev-os init" to bootstrap configuration.');
|
|
649
895
|
}
|
|
650
896
|
|
|
651
897
|
// 6. Token sinks audit
|
|
@@ -1370,3 +1616,2646 @@ function handleDoctorRelease(options) {
|
|
|
1370
1616
|
console.log(' \x1b[32m✔ Release hygiene checks PASSED successfully!\x1b[0m\n');
|
|
1371
1617
|
}
|
|
1372
1618
|
}
|
|
1619
|
+
|
|
1620
|
+
// ==========================================
|
|
1621
|
+
// --- v2.2.0 Intelligence Layer Helpers & Handlers ---
|
|
1622
|
+
// ==========================================
|
|
1623
|
+
|
|
1624
|
+
function hashFile(filePath) {
|
|
1625
|
+
try {
|
|
1626
|
+
const data = readFileSync(filePath);
|
|
1627
|
+
return createHash('sha256').update(data).digest('hex');
|
|
1628
|
+
} catch (e) {
|
|
1629
|
+
return '';
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function shouldIgnorePath(relPath) {
|
|
1634
|
+
const normalized = relPath.replace(/\\/g, '/');
|
|
1635
|
+
const segments = normalized.split('/');
|
|
1636
|
+
|
|
1637
|
+
// Ignored folders
|
|
1638
|
+
const ignoredFolders = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];
|
|
1639
|
+
for (const seg of segments) {
|
|
1640
|
+
if (ignoredFolders.includes(seg)) return true;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// Special check for docs/.vitepress/dist and docs/.vitepress/cache
|
|
1644
|
+
if (normalized.includes('docs/.vitepress/dist') || normalized.includes('docs/.vitepress/cache')) {
|
|
1645
|
+
return true;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// Ignore generated memory and intelligence runtime files
|
|
1649
|
+
if (
|
|
1650
|
+
normalized.endsWith('memory.hash.json') ||
|
|
1651
|
+
normalized.endsWith('memory.summary.md') ||
|
|
1652
|
+
normalized.endsWith('feedback-log.jsonl') ||
|
|
1653
|
+
normalized.endsWith('learning-rules.md') ||
|
|
1654
|
+
normalized.endsWith('apply-log.jsonl') ||
|
|
1655
|
+
normalized.includes('.ai/proposals/')
|
|
1656
|
+
) {
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Skip secret-like files/patterns
|
|
1661
|
+
const lower = normalized.toLowerCase();
|
|
1662
|
+
const filePart = segments[segments.length - 1];
|
|
1663
|
+
if (
|
|
1664
|
+
lower.endsWith('.env') ||
|
|
1665
|
+
lower.includes('.env.') ||
|
|
1666
|
+
lower.endsWith('.npmrc') ||
|
|
1667
|
+
lower.endsWith('.keystore') ||
|
|
1668
|
+
lower.endsWith('.jks') ||
|
|
1669
|
+
lower.endsWith('.key') ||
|
|
1670
|
+
lower.endsWith('.pem') ||
|
|
1671
|
+
lower.endsWith('credentials.json') ||
|
|
1672
|
+
filePart === 'id_rsa' ||
|
|
1673
|
+
filePart === 'id_dsa' ||
|
|
1674
|
+
filePart === 'id_ecdsa' ||
|
|
1675
|
+
filePart === 'id_ed25519'
|
|
1676
|
+
) {
|
|
1677
|
+
return true;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
return false;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
function scanTarget(targetDir) {
|
|
1684
|
+
const files = [];
|
|
1685
|
+
let ignoredCount = 0;
|
|
1686
|
+
|
|
1687
|
+
function walk(dir) {
|
|
1688
|
+
if (!existsSync(dir)) return;
|
|
1689
|
+
const items = readdirSync(dir);
|
|
1690
|
+
for (const item of items) {
|
|
1691
|
+
const fullPath = join(dir, item);
|
|
1692
|
+
const relPath = relative(targetDir, fullPath).replace(/\\/g, '/');
|
|
1693
|
+
|
|
1694
|
+
if (shouldIgnorePath(relPath)) {
|
|
1695
|
+
ignoredCount++;
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
try {
|
|
1700
|
+
const stat = statSync(fullPath);
|
|
1701
|
+
if (stat.isDirectory()) {
|
|
1702
|
+
walk(fullPath);
|
|
1703
|
+
} else if (stat.isFile()) {
|
|
1704
|
+
files.push({
|
|
1705
|
+
relPath,
|
|
1706
|
+
fullPath,
|
|
1707
|
+
size: stat.size,
|
|
1708
|
+
mtime: stat.mtime.toISOString()
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
} catch (e) {
|
|
1712
|
+
// Skip inaccessible files or broken links
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
walk(targetDir);
|
|
1718
|
+
return { files, ignoredCount };
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
function detectFrameworkSignals(files, targetDir) {
|
|
1722
|
+
const signals = [];
|
|
1723
|
+
const hasFile = (name) => files.some(f => f.relPath.toLowerCase() === name.toLowerCase());
|
|
1724
|
+
|
|
1725
|
+
if (hasFile('next.config.js') || hasFile('next.config.mjs')) signals.push('Next.js');
|
|
1726
|
+
if (hasFile('nuxt.config.js') || hasFile('nuxt.config.ts')) signals.push('Nuxt.js');
|
|
1727
|
+
if (hasFile('wp-config.php') || hasFile('index.php')) signals.push('WordPress/PHP');
|
|
1728
|
+
if (hasFile('tsconfig.json')) signals.push('TypeScript');
|
|
1729
|
+
if (hasFile('package.json')) {
|
|
1730
|
+
signals.push('Node.js');
|
|
1731
|
+
try {
|
|
1732
|
+
const pkg = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf8'));
|
|
1733
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1734
|
+
if (deps['react']) signals.push('React');
|
|
1735
|
+
if (deps['vue']) signals.push('Vue');
|
|
1736
|
+
if (deps['svelte']) signals.push('Svelte');
|
|
1737
|
+
if (deps['expo']) signals.push('Expo');
|
|
1738
|
+
if (deps['react-native']) signals.push('React Native');
|
|
1739
|
+
if (deps['vite']) signals.push('Vite');
|
|
1740
|
+
if (deps['express']) signals.push('Express');
|
|
1741
|
+
if (deps['angular']) signals.push('Angular');
|
|
1742
|
+
} catch (e) {}
|
|
1743
|
+
}
|
|
1744
|
+
if (hasFile('requirements.txt') || hasFile('pyproject.toml')) signals.push('Python');
|
|
1745
|
+
if (hasFile('cargo.toml')) signals.push('Rust');
|
|
1746
|
+
if (hasFile('gemfile')) signals.push('Ruby');
|
|
1747
|
+
if (hasFile('go.mod')) signals.push('Go');
|
|
1748
|
+
|
|
1749
|
+
if (signals.length === 0) signals.push('Generic/Unknown');
|
|
1750
|
+
return [...new Set(signals)];
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function detectDependencySignals(files, targetDir) {
|
|
1754
|
+
const signals = [];
|
|
1755
|
+
const hasFile = (name) => files.some(f => f.relPath.toLowerCase() === name.toLowerCase());
|
|
1756
|
+
|
|
1757
|
+
if (hasFile('package-lock.json')) signals.push('npm');
|
|
1758
|
+
else if (hasFile('yarn.lock')) signals.push('Yarn');
|
|
1759
|
+
else if (hasFile('pnpm-lock.yaml')) signals.push('pnpm');
|
|
1760
|
+
else if (hasFile('bun.lockb')) signals.push('Bun');
|
|
1761
|
+
|
|
1762
|
+
if (hasFile('requirements.txt')) signals.push('pip');
|
|
1763
|
+
if (hasFile('poetry.lock')) signals.push('Poetry');
|
|
1764
|
+
if (hasFile('cargo.lock')) signals.push('Cargo');
|
|
1765
|
+
|
|
1766
|
+
return signals;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function detectAiDevOsSignals(files) {
|
|
1770
|
+
const signals = [];
|
|
1771
|
+
const hasFile = (name) => files.some(f => f.relPath.toLowerCase() === name.toLowerCase());
|
|
1772
|
+
|
|
1773
|
+
if (hasFile('agents.md')) signals.push('AGENTS.md');
|
|
1774
|
+
if (hasFile('memory.md')) signals.push('MEMORY.md');
|
|
1775
|
+
if (hasFile('tasks.md')) signals.push('TASKS.md');
|
|
1776
|
+
if (hasFile('runbook.md')) signals.push('RUNBOOK.md');
|
|
1777
|
+
if (hasFile('.ai/config.yaml')) signals.push('.ai/config.yaml');
|
|
1778
|
+
|
|
1779
|
+
const hasPrefix = (prefix) => files.some(f => f.relPath.startsWith(prefix));
|
|
1780
|
+
if (hasPrefix('.ai/templates/')) signals.push('Templates Registry');
|
|
1781
|
+
if (hasPrefix('.ai/adapters/')) signals.push('Adapters Registry');
|
|
1782
|
+
if (hasPrefix('.ai/skills/')) signals.push('Skills Registry');
|
|
1783
|
+
if (hasPrefix('.ai/intelligence/')) signals.push('Intelligence Layer');
|
|
1784
|
+
if (hasPrefix('.ai/policies/')) signals.push('Policy Layer');
|
|
1785
|
+
if (hasPrefix('.ai/registries/')) signals.push('Registry Layer');
|
|
1786
|
+
|
|
1787
|
+
return signals;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function detectRisks(files, targetDir) {
|
|
1791
|
+
const risks = [];
|
|
1792
|
+
const gitignorePath = join(targetDir, '.gitignore');
|
|
1793
|
+
const gitignoreContent = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
|
|
1794
|
+
|
|
1795
|
+
const hasFolder = (name) => files.some(f => f.relPath.split('/')[0] === name);
|
|
1796
|
+
|
|
1797
|
+
if (hasFolder('node_modules') && !gitignoreContent.includes('node_modules')) {
|
|
1798
|
+
risks.push({
|
|
1799
|
+
file_pattern: 'node_modules/',
|
|
1800
|
+
risk_description: 'Large token-sink directory node_modules/ is present but not ignored in .gitignore.',
|
|
1801
|
+
severity: 'high'
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
files.forEach(f => {
|
|
1806
|
+
if (f.relPath.endsWith('.json') && f.relPath.toLowerCase().includes('config') && f.size > 50000) {
|
|
1807
|
+
risks.push({
|
|
1808
|
+
file_pattern: f.relPath,
|
|
1809
|
+
risk_description: `Large config file (${(f.size / 1024).toFixed(1)} KB) might contain sensitive parameters or inflate prompt context.`,
|
|
1810
|
+
severity: 'medium'
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
return risks;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function buildMemoryIndex(targetDir) {
|
|
1819
|
+
const { files, ignoredCount } = scanTarget(targetDir);
|
|
1820
|
+
const framework_signals = detectFrameworkSignals(files, targetDir);
|
|
1821
|
+
const dependency_signals = detectDependencySignals(files, targetDir);
|
|
1822
|
+
const ai_dev_os_signals = detectAiDevOsSignals(files);
|
|
1823
|
+
const risks = detectRisks(files, targetDir);
|
|
1824
|
+
|
|
1825
|
+
const file_fingerprints = {};
|
|
1826
|
+
files.forEach(f => {
|
|
1827
|
+
file_fingerprints[f.relPath] = {
|
|
1828
|
+
hash: hashFile(f.fullPath),
|
|
1829
|
+
size: f.size,
|
|
1830
|
+
last_modified: f.mtime
|
|
1831
|
+
};
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
const recommended_next_steps = [];
|
|
1835
|
+
if (ai_dev_os_signals.length === 0) {
|
|
1836
|
+
recommended_next_steps.push('Run init to bootstrap MultiModel Dev OS.');
|
|
1837
|
+
}
|
|
1838
|
+
if (risks.some(r => r.severity === 'high')) {
|
|
1839
|
+
recommended_next_steps.push('Address Gitignore configuration to exclude large directories (node_modules/ or build artifacts).');
|
|
1840
|
+
}
|
|
1841
|
+
recommended_next_steps.push('Use validate or doctor to check structural integrity.');
|
|
1842
|
+
recommended_next_steps.push('Commit the .ai/ intelligence policies to share constraints across AI agents.');
|
|
1843
|
+
|
|
1844
|
+
return {
|
|
1845
|
+
generated_at: new Date().toISOString(),
|
|
1846
|
+
project_root: targetDir.replace(/\\/g, '/'),
|
|
1847
|
+
file_count: files.length,
|
|
1848
|
+
ignored_count: ignoredCount,
|
|
1849
|
+
file_fingerprints,
|
|
1850
|
+
framework_signals,
|
|
1851
|
+
dependency_signals,
|
|
1852
|
+
ai_dev_os_signals,
|
|
1853
|
+
risks,
|
|
1854
|
+
recommended_next_steps
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
function writeMemoryFiles(targetDir, index) {
|
|
1859
|
+
const intelDir = join(targetDir, '.ai', 'intelligence');
|
|
1860
|
+
if (!existsSync(intelDir)) {
|
|
1861
|
+
mkdirSync(intelDir, { recursive: true });
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
const hashJsonPath = join(intelDir, 'memory.hash.json');
|
|
1865
|
+
writeFileSync(hashJsonPath, JSON.stringify(index, null, 2), 'utf8');
|
|
1866
|
+
|
|
1867
|
+
const summaryMdPath = join(intelDir, 'memory.summary.md');
|
|
1868
|
+
|
|
1869
|
+
let md = `# MultiModel Dev OS Repository Memory Summary\n\n`;
|
|
1870
|
+
md += `**Generated At:** ${index.generated_at}\n`;
|
|
1871
|
+
md += `**Project Root:** ${index.project_root}\n`;
|
|
1872
|
+
md += `**Total Files:** ${index.file_count} (Ignored: ${index.ignored_count})\n\n`;
|
|
1873
|
+
|
|
1874
|
+
md += `## Framework & Environment Signals\n`;
|
|
1875
|
+
md += `- **Frameworks/Languages:** ${index.framework_signals.join(', ') || 'None'}\n`;
|
|
1876
|
+
md += `- **Package Manager/Build:** ${index.dependency_signals.join(', ') || 'None'}\n`;
|
|
1877
|
+
md += `- **AI Dev OS Integration:** ${index.ai_dev_os_signals.join(', ') || 'None'}\n\n`;
|
|
1878
|
+
|
|
1879
|
+
md += `## Codebase Fingerprints\n`;
|
|
1880
|
+
md += `| File Path | Size (Bytes) | Hash (SHA-256) |\n`;
|
|
1881
|
+
md += `|---|---|---|\n`;
|
|
1882
|
+
|
|
1883
|
+
const entries = Object.entries(index.file_fingerprints);
|
|
1884
|
+
entries.forEach(([filePath, fp]) => {
|
|
1885
|
+
md += `| ${filePath} | ${fp.size} | \`${fp.hash.substring(0, 12)}...\` |\n`;
|
|
1886
|
+
});
|
|
1887
|
+
md += `\n`;
|
|
1888
|
+
|
|
1889
|
+
if (index.risks.length > 0) {
|
|
1890
|
+
md += `## Detected Risks\n`;
|
|
1891
|
+
index.risks.forEach(r => {
|
|
1892
|
+
md += `- **[${r.severity.toUpperCase()}]** \`${r.file_pattern}\`: ${r.risk_description}\n`;
|
|
1893
|
+
});
|
|
1894
|
+
md += `\n`;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
md += `## Recommended Next Steps\n`;
|
|
1898
|
+
index.recommended_next_steps.forEach(step => {
|
|
1899
|
+
md += `- ${step}\n`;
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
writeFileSync(summaryMdPath, md, 'utf8');
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function diffMemory(targetDir) {
|
|
1906
|
+
const hashJsonPath = join(targetDir, '.ai', 'intelligence', 'memory.hash.json');
|
|
1907
|
+
if (!existsSync(hashJsonPath)) {
|
|
1908
|
+
return null;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
let existing;
|
|
1912
|
+
try {
|
|
1913
|
+
existing = JSON.parse(readFileSync(hashJsonPath, 'utf8'));
|
|
1914
|
+
} catch (e) {
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const currentScan = buildMemoryIndex(targetDir);
|
|
1919
|
+
|
|
1920
|
+
const added = [];
|
|
1921
|
+
const removed = [];
|
|
1922
|
+
const changed = [];
|
|
1923
|
+
let unchangedCount = 0;
|
|
1924
|
+
|
|
1925
|
+
const currentFp = currentScan.file_fingerprints;
|
|
1926
|
+
const existingFp = existing.file_fingerprints || {};
|
|
1927
|
+
|
|
1928
|
+
Object.keys(currentFp).forEach(file => {
|
|
1929
|
+
if (!existingFp[file]) {
|
|
1930
|
+
added.push(file);
|
|
1931
|
+
} else if (existingFp[file].hash !== currentFp[file].hash || existingFp[file].size !== currentFp[file].size) {
|
|
1932
|
+
changed.push(file);
|
|
1933
|
+
} else {
|
|
1934
|
+
unchangedCount++;
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
Object.keys(existingFp).forEach(file => {
|
|
1939
|
+
if (!currentFp[file]) {
|
|
1940
|
+
removed.push(file);
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
return { added, removed, changed, unchangedCount, currentScan };
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
function handleScan(options) {
|
|
1948
|
+
console.log(`\n🔍 \x1b[36mCodebase Scan target: ${options.target}\x1b[0m`);
|
|
1949
|
+
console.log('==================================================');
|
|
1950
|
+
|
|
1951
|
+
const { files, ignoredCount } = scanTarget(options.target);
|
|
1952
|
+
const frameworkSignals = detectFrameworkSignals(files, options.target);
|
|
1953
|
+
const dependencySignals = detectDependencySignals(files, options.target);
|
|
1954
|
+
const aiDevOsSignals = detectAiDevOsSignals(files);
|
|
1955
|
+
const risks = detectRisks(files, options.target);
|
|
1956
|
+
|
|
1957
|
+
console.log(`\n\x1b[33mProject Stats:\x1b[0m`);
|
|
1958
|
+
console.log(` File Count: ${files.length}`);
|
|
1959
|
+
console.log(` Ignored Files: ${ignoredCount}`);
|
|
1960
|
+
|
|
1961
|
+
console.log(`\n\x1b[33mFramework & Language Signals:\x1b[0m`);
|
|
1962
|
+
frameworkSignals.forEach(sig => console.log(` - ${sig}`));
|
|
1963
|
+
|
|
1964
|
+
console.log(`\n\x1b[33mPackage Manager & Dependency Signals:\x1b[0m`);
|
|
1965
|
+
dependencySignals.forEach(sig => console.log(` - ${sig}`));
|
|
1966
|
+
|
|
1967
|
+
console.log(`\n\x1b[33mMultiModel Dev OS Files:\x1b[0m`);
|
|
1968
|
+
if (aiDevOsSignals.length > 0) {
|
|
1969
|
+
aiDevOsSignals.forEach(sig => console.log(` - ${sig}`));
|
|
1970
|
+
} else {
|
|
1971
|
+
console.log(` No MultiModel Dev OS files detected. Run \x1b[36mmit --template general-app\x1b[0m to initialize.`);
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
if (risks.length > 0) {
|
|
1975
|
+
console.log(`\n\x1b[31mDetected Risks:\x1b[0m`);
|
|
1976
|
+
risks.forEach(r => console.log(` - [${r.severity.toUpperCase()}] ${r.file_pattern}: ${r.risk_description}`));
|
|
1977
|
+
} else {
|
|
1978
|
+
console.log(`\n\x1b[32m✔ No high/medium risks detected in repository structure.\x1b[0m`);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
console.log();
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
function handleMemoryBuild(options) {
|
|
1985
|
+
console.log(`\n🧠 \x1b[36mBuilding Codebase Memory in: ${options.target}\x1b[0m`);
|
|
1986
|
+
console.log('==================================================');
|
|
1987
|
+
|
|
1988
|
+
const index = buildMemoryIndex(options.target);
|
|
1989
|
+
writeMemoryFiles(options.target, index);
|
|
1990
|
+
|
|
1991
|
+
console.log(` \x1b[32mCREATE:\x1b[0m .ai/intelligence/memory.hash.json`);
|
|
1992
|
+
console.log(` \x1b[32mCREATE:\x1b[0m .ai/intelligence/memory.summary.md`);
|
|
1993
|
+
console.log(`\n✔ Memory index built successfully! [Files indexed: ${index.file_count}]`);
|
|
1994
|
+
|
|
1995
|
+
console.log(`\n\x1b[33mRecommended Next Steps:\x1b[0m`);
|
|
1996
|
+
index.recommended_next_steps.forEach(step => console.log(` - ${step}`));
|
|
1997
|
+
console.log();
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function handleMemoryRefresh(options) {
|
|
2001
|
+
console.log(`\n🧠 \x1b[36mRefreshing Codebase Memory in: ${options.target}\x1b[0m`);
|
|
2002
|
+
console.log('==================================================');
|
|
2003
|
+
|
|
2004
|
+
const diff = diffMemory(options.target);
|
|
2005
|
+
if (!diff) {
|
|
2006
|
+
console.log(' No existing memory index found. Building fresh index...');
|
|
2007
|
+
handleMemoryBuild(options);
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
writeMemoryFiles(options.target, diff.currentScan);
|
|
2012
|
+
|
|
2013
|
+
console.log(` \x1b[32mUPDATE:\x1b[0m .ai/intelligence/memory.hash.json`);
|
|
2014
|
+
console.log(` \x1b[32mUPDATE:\x1b[0m .ai/intelligence/memory.summary.md`);
|
|
2015
|
+
|
|
2016
|
+
console.log(`\n✔ Memory index refreshed successfully!`);
|
|
2017
|
+
console.log(` Added: ${diff.added.length}`);
|
|
2018
|
+
console.log(` Removed: ${diff.removed.length}`);
|
|
2019
|
+
console.log(` Changed: ${diff.changed.length}`);
|
|
2020
|
+
console.log(` Unchanged: ${diff.unchangedCount}`);
|
|
2021
|
+
console.log();
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
function handleMemoryDiff(options) {
|
|
2025
|
+
console.log(`\n🧠 \x1b[36mDiffing Codebase State against Memory in: ${options.target}\x1b[0m`);
|
|
2026
|
+
console.log('==================================================');
|
|
2027
|
+
|
|
2028
|
+
const diff = diffMemory(options.target);
|
|
2029
|
+
if (!diff) {
|
|
2030
|
+
console.error(`\x1b[31mError: No existing memory index found. Run 'memory build' first.\x1b[0m\n`);
|
|
2031
|
+
if (options && options.noExit) return false;
|
|
2032
|
+
process.exit(1);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
console.log(`\n\x1b[33mMemory Diff Summary:\x1b[0m`);
|
|
2036
|
+
console.log(` Added Files: ${diff.added.length}`);
|
|
2037
|
+
console.log(` Removed Files: ${diff.removed.length}`);
|
|
2038
|
+
console.log(` Changed Files: ${diff.changed.length}`);
|
|
2039
|
+
console.log(` Unchanged: ${diff.unchangedCount}`);
|
|
2040
|
+
|
|
2041
|
+
if (diff.added.length > 0) {
|
|
2042
|
+
console.log(`\n\x1b[32mAdded Files:\x1b[0m`);
|
|
2043
|
+
diff.added.forEach(f => console.log(` + ${f}`));
|
|
2044
|
+
}
|
|
2045
|
+
if (diff.removed.length > 0) {
|
|
2046
|
+
console.log(`\n\x1b[31mRemoved Files:\x1b[0m`);
|
|
2047
|
+
diff.removed.forEach(f => console.log(` - ${f}`));
|
|
2048
|
+
}
|
|
2049
|
+
if (diff.changed.length > 0) {
|
|
2050
|
+
console.log(`\n\x1b[33mChanged Files:\x1b[0m`);
|
|
2051
|
+
diff.changed.forEach(f => console.log(` M ${f}`));
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
console.log();
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
function handleFeedbackAdd(options) {
|
|
2058
|
+
const intelDir = join(options.target, '.ai', 'intelligence');
|
|
2059
|
+
if (!options.dryRun && !existsSync(intelDir)) {
|
|
2060
|
+
mkdirSync(intelDir, { recursive: true });
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
const addIdx = process.argv.indexOf('add');
|
|
2064
|
+
const text = (addIdx !== -1 && process.argv[addIdx + 1] && !process.argv[addIdx + 1].startsWith('-')) ? process.argv[addIdx + 1] : null;
|
|
2065
|
+
|
|
2066
|
+
if (!text) {
|
|
2067
|
+
console.error(`\x1b[31mError: Please provide feedback text.\x1b[0m`);
|
|
2068
|
+
console.log(`Example: node bin/multimodel-dev-os.js feedback add "Prefer CSS modules"`);
|
|
2069
|
+
process.exit(1);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
const uuid = createHash('md5').update(new Date().toISOString() + Math.random().toString()).digest('hex').substring(0, 16);
|
|
2073
|
+
const tagsStr = options.tags || '';
|
|
2074
|
+
const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()) : [];
|
|
2075
|
+
const filesStr = options.files || '';
|
|
2076
|
+
const related_files = filesStr ? filesStr.split(',').map(f => f.trim()) : [];
|
|
2077
|
+
|
|
2078
|
+
const rawRecord = {
|
|
2079
|
+
id: `fb-${uuid}`,
|
|
2080
|
+
created_at: new Date().toISOString(),
|
|
2081
|
+
source: 'user',
|
|
2082
|
+
type: options.type || 'unknown',
|
|
2083
|
+
text: text,
|
|
2084
|
+
tags: tags,
|
|
2085
|
+
related_files: related_files
|
|
2086
|
+
};
|
|
2087
|
+
|
|
2088
|
+
rawRecord.hash = createHash('sha256').update(JSON.stringify(rawRecord)).digest('hex');
|
|
2089
|
+
|
|
2090
|
+
const recordLine = JSON.stringify(rawRecord) + '\n';
|
|
2091
|
+
const feedbackLogPath = join(intelDir, 'feedback-log.jsonl');
|
|
2092
|
+
|
|
2093
|
+
if (options.dryRun) {
|
|
2094
|
+
console.log(`\x1b[36m[DRY-RUN] WOULD APPEND TO ${feedbackLogPath}:\x1b[0m`);
|
|
2095
|
+
console.log(recordLine.trim());
|
|
2096
|
+
} else {
|
|
2097
|
+
try {
|
|
2098
|
+
let isDuplicate = false;
|
|
2099
|
+
if (existsSync(feedbackLogPath)) {
|
|
2100
|
+
const lines = readFileSync(feedbackLogPath, 'utf8').split('\n');
|
|
2101
|
+
for (const line of lines) {
|
|
2102
|
+
if (!line.trim()) continue;
|
|
2103
|
+
try {
|
|
2104
|
+
const entry = JSON.parse(line);
|
|
2105
|
+
if (entry.text === text && JSON.stringify(entry.related_files) === JSON.stringify(related_files)) {
|
|
2106
|
+
isDuplicate = true;
|
|
2107
|
+
break;
|
|
2108
|
+
}
|
|
2109
|
+
} catch (e) {}
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
if (isDuplicate) {
|
|
2113
|
+
console.log(`\x1b[33mFeedback already exists. Skipping duplicate entry.\x1b[0m`);
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
writeFileSync(feedbackLogPath, recordLine, { flag: 'a', encoding: 'utf8' });
|
|
2118
|
+
console.log(`✔ Feedback successfully added (ID: ${rawRecord.id})`);
|
|
2119
|
+
} catch (e) {
|
|
2120
|
+
console.error(`\x1b[31mError: Failed to write to feedback-log.jsonl: ${e.message}\x1b[0m`);
|
|
2121
|
+
process.exit(1);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
function handleFeedbackList(options) {
|
|
2127
|
+
const feedbackLogPath = join(options.target, '.ai', 'intelligence', 'feedback-log.jsonl');
|
|
2128
|
+
if (!existsSync(feedbackLogPath)) {
|
|
2129
|
+
console.log('No feedback logged yet.');
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
try {
|
|
2134
|
+
const content = readFileSync(feedbackLogPath, 'utf8');
|
|
2135
|
+
const lines = content.split('\n').filter(l => l.trim() !== '');
|
|
2136
|
+
if (lines.length === 0) {
|
|
2137
|
+
console.log('No feedback logged yet.');
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
console.log(`\n🧠 \x1b[36mLogged Feedback Entries\x1b[0m`);
|
|
2142
|
+
console.log('==================================================');
|
|
2143
|
+
lines.forEach(line => {
|
|
2144
|
+
try {
|
|
2145
|
+
const entry = JSON.parse(line);
|
|
2146
|
+
console.log(`\n\x1b[32m* [${entry.type || 'unknown'}] (${entry.id})\x1b[0m`);
|
|
2147
|
+
console.log(` \x1b[37mText:\x1b[0m ${entry.text}`);
|
|
2148
|
+
if (entry.tags && entry.tags.length > 0) {
|
|
2149
|
+
console.log(` \x1b[33mTags:\x1b[0m ${entry.tags.join(', ')}`);
|
|
2150
|
+
}
|
|
2151
|
+
if (entry.related_files && entry.related_files.length > 0) {
|
|
2152
|
+
console.log(` \x1b[33mFiles:\x1b[0m ${entry.related_files.join(', ')}`);
|
|
2153
|
+
}
|
|
2154
|
+
console.log(` \x1b[33mLogged:\x1b[0m ${entry.created_at}`);
|
|
2155
|
+
} catch (e) {}
|
|
2156
|
+
});
|
|
2157
|
+
console.log();
|
|
2158
|
+
} catch (e) {
|
|
2159
|
+
console.error(`\x1b[31mError: Failed to read feedback log: ${e.message}\x1b[0m`);
|
|
2160
|
+
process.exit(1);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
function handleFeedbackSummarize(options) {
|
|
2165
|
+
const intelDir = join(options.target, '.ai', 'intelligence');
|
|
2166
|
+
const feedbackLogPath = join(intelDir, 'feedback-log.jsonl');
|
|
2167
|
+
if (!existsSync(feedbackLogPath)) {
|
|
2168
|
+
console.log('No feedback logs found to compile.');
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
try {
|
|
2173
|
+
const content = readFileSync(feedbackLogPath, 'utf8');
|
|
2174
|
+
const lines = content.split('\n').filter(l => l.trim() !== '');
|
|
2175
|
+
if (lines.length === 0) {
|
|
2176
|
+
console.log('No feedback logs found to compile.');
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
const categories = {};
|
|
2181
|
+
lines.forEach(line => {
|
|
2182
|
+
try {
|
|
2183
|
+
const entry = JSON.parse(line);
|
|
2184
|
+
const cat = entry.type || 'general';
|
|
2185
|
+
if (!categories[cat]) categories[cat] = [];
|
|
2186
|
+
categories[cat].push(entry);
|
|
2187
|
+
} catch (e) {}
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
let md = `# Compiled Learning Rules\n\n`;
|
|
2191
|
+
md += `*Generated automatically by MultiModel Dev OS. Do not modify manually.*\n\n`;
|
|
2192
|
+
md += `**Last compiled:** ${new Date().toISOString()}\n`;
|
|
2193
|
+
md += `**Total source feedback items:** ${lines.length}\n\n`;
|
|
2194
|
+
md += `## Active Instructions\n\n`;
|
|
2195
|
+
|
|
2196
|
+
Object.keys(categories).forEach(cat => {
|
|
2197
|
+
md += `### Category: ${cat}\n`;
|
|
2198
|
+
categories[cat].forEach(entry => {
|
|
2199
|
+
const pattern = entry.related_files && entry.related_files.length > 0 ? entry.related_files.join(', ') : '*';
|
|
2200
|
+
md += `* **Pattern:** \`${pattern}\`\n`;
|
|
2201
|
+
md += ` * **Rule:** ${entry.text}\n`;
|
|
2202
|
+
md += ` * **Source ID:** \`${entry.id}\`\n\n`;
|
|
2203
|
+
});
|
|
2204
|
+
});
|
|
2205
|
+
|
|
2206
|
+
const targetRulesPath = join(intelDir, 'learning-rules.md');
|
|
2207
|
+
if (options.dryRun) {
|
|
2208
|
+
console.log(`\x1b[36m[DRY-RUN] WOULD WRITE TO ${targetRulesPath}:\x1b[0m`);
|
|
2209
|
+
console.log(md);
|
|
2210
|
+
} else {
|
|
2211
|
+
writeFileSync(targetRulesPath, md, 'utf8');
|
|
2212
|
+
console.log(`✔ Compiled ${lines.length} feedback items into learning rules in .ai/intelligence/learning-rules.md`);
|
|
2213
|
+
}
|
|
2214
|
+
} catch (e) {
|
|
2215
|
+
console.error(`\x1b[31mError: Failed to compile learning rules: ${e.message}\x1b[0m`);
|
|
2216
|
+
process.exit(1);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
function handleImprovePropose(options) {
|
|
2221
|
+
const proposalsDir = join(options.target, '.ai', 'proposals');
|
|
2222
|
+
if (!options.dryRun && !existsSync(proposalsDir)) {
|
|
2223
|
+
mkdirSync(proposalsDir, { recursive: true });
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
const now = new Date();
|
|
2227
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
2228
|
+
const dateStr = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
|
|
2229
|
+
const timeStr = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
2230
|
+
const timestamp = `${dateStr}-${timeStr}`;
|
|
2231
|
+
const id = `proposal-${timestamp}`;
|
|
2232
|
+
|
|
2233
|
+
const title = options.title || 'Auto-detected codebase optimization';
|
|
2234
|
+
let problem = 'No specific problems detected.';
|
|
2235
|
+
let evidence = 'N/A';
|
|
2236
|
+
let riskLevel = 'low';
|
|
2237
|
+
let affectedFiles = [];
|
|
2238
|
+
let suggestedChange = 'No code suggestions compiled.';
|
|
2239
|
+
let verifyCommand = 'npm run verify';
|
|
2240
|
+
let rollbackPlan = 'git checkout -- .';
|
|
2241
|
+
|
|
2242
|
+
const gitignorePath = join(options.target, '.gitignore');
|
|
2243
|
+
const agentsPath = join(options.target, 'AGENTS.md');
|
|
2244
|
+
|
|
2245
|
+
if (!existsSync(gitignorePath)) {
|
|
2246
|
+
problem = 'Missing .gitignore file in target workspace. AI agents may scan large build directories and run out of token context.';
|
|
2247
|
+
evidence = `.gitignore file is not present at root directory: ${options.target}`;
|
|
2248
|
+
affectedFiles = ['.gitignore'];
|
|
2249
|
+
suggestedChange = 'Create a standard .gitignore file to exclude node_modules, build/ and dist/ directories.';
|
|
2250
|
+
rollbackPlan = 'git clean -fd .gitignore';
|
|
2251
|
+
} else if (!existsSync(agentsPath)) {
|
|
2252
|
+
problem = 'Missing AGENTS.md document in target workspace. Models will lack stack-specific implementation blueprints.';
|
|
2253
|
+
evidence = `AGENTS.md file is not present at root directory: ${options.target}`;
|
|
2254
|
+
affectedFiles = ['AGENTS.md'];
|
|
2255
|
+
suggestedChange = 'Create an AGENTS.md document specifying the codebase development guidelines and framework profiles.';
|
|
2256
|
+
rollbackPlan = 'git clean -fd AGENTS.md';
|
|
2257
|
+
} else {
|
|
2258
|
+
problem = 'Outdated codebase memory index. Memory files need to be refreshed to sync with recent local changes.';
|
|
2259
|
+
evidence = 'Current memory.hash.json represents a previous commit state.';
|
|
2260
|
+
affectedFiles = ['.ai/intelligence/memory.hash.json', '.ai/intelligence/memory.summary.md'];
|
|
2261
|
+
suggestedChange = 'Refresh codebase memory index using multimodel-dev-os memory refresh CLI command.';
|
|
2262
|
+
riskLevel = 'low';
|
|
2263
|
+
verifyCommand = 'node bin/multimodel-dev-os.js memory refresh';
|
|
2264
|
+
rollbackPlan = 'git checkout -- .ai/intelligence/';
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
let md = `---
|
|
2268
|
+
id: ${id}
|
|
2269
|
+
created_at: ${now.toISOString()}
|
|
2270
|
+
title: ${title}
|
|
2271
|
+
problem: ${problem}
|
|
2272
|
+
evidence: ${evidence}
|
|
2273
|
+
risk_level: ${riskLevel}
|
|
2274
|
+
affected_files:
|
|
2275
|
+
`;
|
|
2276
|
+
affectedFiles.forEach(f => {
|
|
2277
|
+
md += ` - ${f}\n`;
|
|
2278
|
+
});
|
|
2279
|
+
md += `suggested_change: ${suggestedChange}
|
|
2280
|
+
verify_command: ${verifyCommand}
|
|
2281
|
+
rollback_plan: ${rollbackPlan}
|
|
2282
|
+
approval_status: pending
|
|
2283
|
+
---
|
|
2284
|
+
|
|
2285
|
+
# Codebase Improvement Proposal: ${title}
|
|
2286
|
+
|
|
2287
|
+
> [!WARNING]
|
|
2288
|
+
> Manual approval is required before implementing this proposal. Edit the frontmatter metadata block to change \`approval_status\` to \`approved\` to authorize modifications.
|
|
2289
|
+
|
|
2290
|
+
## 1. Problem Description
|
|
2291
|
+
${problem}
|
|
2292
|
+
|
|
2293
|
+
## 2. Evidence
|
|
2294
|
+
${evidence}
|
|
2295
|
+
|
|
2296
|
+
## 3. Suggested Modifications
|
|
2297
|
+
${suggestedChange}
|
|
2298
|
+
|
|
2299
|
+
## 4. Safety & Rollback Parameters
|
|
2300
|
+
* **Risk Level**: ${riskLevel.toUpperCase()}
|
|
2301
|
+
* **Verification Command**: \`${verifyCommand}\`
|
|
2302
|
+
* **Rollback Command**: \`${rollbackPlan}\`
|
|
2303
|
+
* **Approval Status**: PENDING (Manual approval required before implementation)
|
|
2304
|
+
`;
|
|
2305
|
+
|
|
2306
|
+
const proposalFile = join(proposalsDir, `${id}.md`);
|
|
2307
|
+
if (options.dryRun) {
|
|
2308
|
+
console.log(`\x1b[36m[DRY-RUN] WOULD WRITE PROPOSAL TO ${proposalFile}:\x1b[0m`);
|
|
2309
|
+
console.log(md);
|
|
2310
|
+
} else {
|
|
2311
|
+
writeFileSync(proposalFile, md, 'utf8');
|
|
2312
|
+
console.log(`✔ Created codebase improvement proposal: .ai/proposals/${id}.md`);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
function handleImproveReview(options) {
|
|
2317
|
+
const proposalsDir = join(options.target, '.ai', 'proposals');
|
|
2318
|
+
if (!existsSync(proposalsDir)) {
|
|
2319
|
+
console.log('No improvement proposals found.');
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
try {
|
|
2324
|
+
const files = readdirSync(proposalsDir).filter(f => f.startsWith('proposal-') && f.endsWith('.md'));
|
|
2325
|
+
if (files.length === 0) {
|
|
2326
|
+
console.log('No improvement proposals found.');
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
console.log(`\n📋 \x1b[36mCodebase Improvement Proposals\x1b[0m`);
|
|
2331
|
+
console.log('==================================================');
|
|
2332
|
+
|
|
2333
|
+
files.forEach(file => {
|
|
2334
|
+
const fullPath = join(proposalsDir, file);
|
|
2335
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
2336
|
+
|
|
2337
|
+
const fmMatch = content.match(/^---([\s\S]*?)---/);
|
|
2338
|
+
if (!fmMatch) return;
|
|
2339
|
+
|
|
2340
|
+
const fmContent = fmMatch[1];
|
|
2341
|
+
const metadata = parseYaml(fmContent) || {};
|
|
2342
|
+
|
|
2343
|
+
const statusColor = metadata.approval_status === 'approved' ? '\x1b[32m' : metadata.approval_status === 'rejected' ? '\x1b[31m' : '\x1b[33m';
|
|
2344
|
+
console.log(`\n\x1b[34m* [${metadata.id || file.replace('.md', '')}] ${metadata.title || 'Untitled'}\x1b[0m`);
|
|
2345
|
+
console.log(` \x1b[37mRisk Level:\x1b[0m ${metadata.risk_level || 'unknown'}`);
|
|
2346
|
+
console.log(` \x1b[37mStatus:\x1b[0m ${statusColor}${metadata.approval_status || 'pending'}\x1b[0m`);
|
|
2347
|
+
console.log(` \x1b[37mProblem:\x1b[0m ${metadata.problem || 'N/A'}`);
|
|
2348
|
+
if (metadata.affected_files && metadata.affected_files.length > 0) {
|
|
2349
|
+
console.log(` \x1b[37mAffected Files:\x1b[0m ${metadata.affected_files.join(', ')}`);
|
|
2350
|
+
}
|
|
2351
|
+
});
|
|
2352
|
+
console.log();
|
|
2353
|
+
} catch (e) {
|
|
2354
|
+
console.error(`\x1b[31mError: Failed to review proposals: ${e.message}\x1b[0m`);
|
|
2355
|
+
process.exit(1);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
function handleImproveStatus(options) {
|
|
2360
|
+
const proposalsDir = join(options.target, '.ai', 'proposals');
|
|
2361
|
+
if (!existsSync(proposalsDir)) {
|
|
2362
|
+
console.log('Improvement Proposal Engine Status:');
|
|
2363
|
+
console.log(' Total Proposals: 0');
|
|
2364
|
+
console.log(' Pending Approval: 0');
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
try {
|
|
2369
|
+
const files = readdirSync(proposalsDir).filter(f => f.startsWith('proposal-') && f.endsWith('.md'));
|
|
2370
|
+
let pending = 0;
|
|
2371
|
+
let approved = 0;
|
|
2372
|
+
let rejected = 0;
|
|
2373
|
+
|
|
2374
|
+
files.forEach(file => {
|
|
2375
|
+
const content = readFileSync(join(proposalsDir, file), 'utf8');
|
|
2376
|
+
const fmMatch = content.match(/^---([\s\S]*?)---/);
|
|
2377
|
+
if (fmMatch) {
|
|
2378
|
+
const metadata = parseYaml(fmMatch[1]) || {};
|
|
2379
|
+
const status = metadata.approval_status || 'pending';
|
|
2380
|
+
if (status === 'approved') approved++;
|
|
2381
|
+
else if (status === 'rejected') rejected++;
|
|
2382
|
+
else pending++;
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
|
|
2386
|
+
console.log(`\n⚙ \x1b[36mImprovement Proposals Engine Status\x1b[0m`);
|
|
2387
|
+
console.log('==================================================');
|
|
2388
|
+
console.log(` Total Proposals: ${files.length}`);
|
|
2389
|
+
console.log(` Pending Approval: \x1b[33m${pending}\x1b[0m`);
|
|
2390
|
+
console.log(` Approved: \x1b[32m${approved}\x1b[0m`);
|
|
2391
|
+
console.log(` Rejected: \x1b[31m${rejected}\x1b[0m`);
|
|
2392
|
+
console.log();
|
|
2393
|
+
} catch (e) {
|
|
2394
|
+
console.error(`\x1b[31mError: Failed to fetch status: ${e.message}\x1b[0m`);
|
|
2395
|
+
process.exit(1);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
function getSha256(content) {
|
|
2400
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
function validatePath(targetRoot, relPath) {
|
|
2404
|
+
const normalizedRel = relPath.replace(/\\/g, '/');
|
|
2405
|
+
|
|
2406
|
+
if (normalizedRel.startsWith('/') || normalizedRel.includes('..')) {
|
|
2407
|
+
return { valid: false, reason: `Path '${relPath}' contains directory traversal or is absolute.`, type: 'outside' };
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
const resolved = resolve(targetRoot, relPath);
|
|
2411
|
+
const relativeFromRoot = relative(targetRoot, resolved);
|
|
2412
|
+
|
|
2413
|
+
if (relativeFromRoot.startsWith('..') || isAbsolute(relativeFromRoot) || resolved === targetRoot) {
|
|
2414
|
+
return { valid: false, reason: `Path '${relPath}' resolves outside the target root.`, type: 'outside' };
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
const parts = relativeFromRoot.replace(/\\/g, '/').split('/');
|
|
2418
|
+
|
|
2419
|
+
const protectedFolders = [
|
|
2420
|
+
'.git',
|
|
2421
|
+
'node_modules',
|
|
2422
|
+
'dist',
|
|
2423
|
+
'build',
|
|
2424
|
+
'.next',
|
|
2425
|
+
'coverage'
|
|
2426
|
+
];
|
|
2427
|
+
for (const part of parts) {
|
|
2428
|
+
if (protectedFolders.includes(part)) {
|
|
2429
|
+
return { valid: false, reason: `Path '${relPath}' attempts to access protected directory '${part}/'.`, type: 'protected' };
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
const cleanRelativeFromRoot = relativeFromRoot.replace(/\\/g, '/');
|
|
2434
|
+
if (cleanRelativeFromRoot.startsWith('docs/.vitepress/dist') || cleanRelativeFromRoot.startsWith('docs/.vitepress/cache')) {
|
|
2435
|
+
return { valid: false, reason: `Path '${relPath}' attempts to access protected vitepress path.`, type: 'protected' };
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
const filename = parts[parts.length - 1];
|
|
2439
|
+
if (filename === '.env' || filename.startsWith('.env.') || filename === '.npmrc' || filename === 'credentials.json' || filename === 'package-lock.json' || filename === 'apply-log.jsonl') {
|
|
2440
|
+
return { valid: false, reason: `Path '${relPath}' targets a protected config/secret file.`, type: 'protected' };
|
|
2441
|
+
}
|
|
2442
|
+
if (filename.endsWith('.pem') || filename.endsWith('.key') || filename.endsWith('.jks') || filename.endsWith('.keystore')) {
|
|
2443
|
+
return { valid: false, reason: `Path '${relPath}' targets a protected key/certificate file.`, type: 'protected' };
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
return { valid: true, resolved };
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
function validateProposal(proposalFile, targetRoot) {
|
|
2450
|
+
const gates = {
|
|
2451
|
+
frontmatter: { status: 'skip' },
|
|
2452
|
+
approval: { status: 'skip' },
|
|
2453
|
+
json: { status: 'skip' },
|
|
2454
|
+
types: { status: 'skip' },
|
|
2455
|
+
boundaries: { status: 'skip' },
|
|
2456
|
+
permissions: { status: 'skip' },
|
|
2457
|
+
constraints: { status: 'skip' }
|
|
2458
|
+
};
|
|
2459
|
+
|
|
2460
|
+
if (!existsSync(proposalFile)) {
|
|
2461
|
+
gates.frontmatter = { status: 'fail', reason: 'missing frontmatter' };
|
|
2462
|
+
return { valid: false, reason: 'missing frontmatter', gates };
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
const content = readFileSync(proposalFile, 'utf8');
|
|
2466
|
+
const fmMatch = content.match(/^---([\s\S]*?)---/);
|
|
2467
|
+
if (!fmMatch) {
|
|
2468
|
+
gates.frontmatter = { status: 'fail', reason: 'missing frontmatter' };
|
|
2469
|
+
return { valid: false, reason: 'missing frontmatter', gates };
|
|
2470
|
+
}
|
|
2471
|
+
const fmContent = fmMatch[1];
|
|
2472
|
+
const metadata = parseYaml(fmContent);
|
|
2473
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
2474
|
+
gates.frontmatter = { status: 'fail', reason: 'missing frontmatter' };
|
|
2475
|
+
return { valid: false, reason: 'missing frontmatter', gates };
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
gates.frontmatter = { status: 'pass' };
|
|
2479
|
+
const proposalId = metadata.id || basename(proposalFile, '.md');
|
|
2480
|
+
const proposalTitle = metadata.title || 'Untitled Proposal';
|
|
2481
|
+
const proposalStatus = metadata.approval_status || 'pending';
|
|
2482
|
+
|
|
2483
|
+
const isApproved = (metadata.approval_status === 'approved');
|
|
2484
|
+
gates.approval = isApproved ? { status: 'pass' } : { status: 'fail', reason: 'approval_status not approved' };
|
|
2485
|
+
|
|
2486
|
+
const body = content.substring(fmMatch[0].length);
|
|
2487
|
+
const jsonBlockRegex = /```json\s*\n([\s\S]*?)\n\s*```/;
|
|
2488
|
+
const jsonMatch = body.match(jsonBlockRegex);
|
|
2489
|
+
|
|
2490
|
+
let operationsData = null;
|
|
2491
|
+
if (!jsonMatch) {
|
|
2492
|
+
gates.json = { status: 'fail', reason: 'no operations block' };
|
|
2493
|
+
} else {
|
|
2494
|
+
try {
|
|
2495
|
+
operationsData = JSON.parse(jsonMatch[1]);
|
|
2496
|
+
if (!operationsData || !Array.isArray(operationsData.operations) || operationsData.operations.length === 0) {
|
|
2497
|
+
gates.json = { status: 'fail', reason: 'no operations block' };
|
|
2498
|
+
} else {
|
|
2499
|
+
gates.json = { status: 'pass' };
|
|
2500
|
+
}
|
|
2501
|
+
} catch (e) {
|
|
2502
|
+
gates.json = { status: 'fail', reason: 'invalid JSON operations block' };
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
if (gates.json.status !== 'pass') {
|
|
2507
|
+
const gateOrder = ['frontmatter', 'approval', 'json', 'types', 'boundaries', 'permissions', 'constraints'];
|
|
2508
|
+
let firstFailReason = null;
|
|
2509
|
+
for (const g of gateOrder) {
|
|
2510
|
+
if (gates[g].status === 'fail') {
|
|
2511
|
+
firstFailReason = gates[g].reason;
|
|
2512
|
+
break;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
return {
|
|
2516
|
+
valid: false,
|
|
2517
|
+
reason: firstFailReason,
|
|
2518
|
+
gates,
|
|
2519
|
+
proposalId,
|
|
2520
|
+
proposalTitle,
|
|
2521
|
+
proposalStatus,
|
|
2522
|
+
operations: []
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
let typesStatus = 'pass';
|
|
2527
|
+
let typesReason = '';
|
|
2528
|
+
let boundariesStatus = 'pass';
|
|
2529
|
+
let boundariesReason = '';
|
|
2530
|
+
let permissionsStatus = 'pass';
|
|
2531
|
+
let permissionsReason = '';
|
|
2532
|
+
let constraintsStatus = 'pass';
|
|
2533
|
+
let constraintsReason = '';
|
|
2534
|
+
|
|
2535
|
+
const validatedOperations = [];
|
|
2536
|
+
const operations = operationsData.operations;
|
|
2537
|
+
|
|
2538
|
+
for (let idx = 0; idx < operations.length; idx++) {
|
|
2539
|
+
const op = operations[idx];
|
|
2540
|
+
if (!op || typeof op !== 'object' || !op.type) {
|
|
2541
|
+
if (typesStatus === 'pass') {
|
|
2542
|
+
typesStatus = 'fail';
|
|
2543
|
+
typesReason = `unsupported operation type`;
|
|
2544
|
+
}
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
const allowedTypes = ['create_file', 'append_line', 'replace_text'];
|
|
2549
|
+
if (!allowedTypes.includes(op.type)) {
|
|
2550
|
+
if (typesStatus === 'pass') {
|
|
2551
|
+
typesStatus = 'fail';
|
|
2552
|
+
typesReason = `unsupported operation type`;
|
|
2553
|
+
}
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
if (typeof op.path !== 'string' || !op.path.trim()) {
|
|
2558
|
+
if (boundariesStatus === 'pass') {
|
|
2559
|
+
boundariesStatus = 'fail';
|
|
2560
|
+
boundariesReason = `path outside target`;
|
|
2561
|
+
}
|
|
2562
|
+
continue;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
const pathVal = validatePath(targetRoot, op.path);
|
|
2566
|
+
if (!pathVal.valid) {
|
|
2567
|
+
if (pathVal.type === 'outside') {
|
|
2568
|
+
if (boundariesStatus === 'pass') {
|
|
2569
|
+
boundariesStatus = 'fail';
|
|
2570
|
+
boundariesReason = `path outside target`;
|
|
2571
|
+
}
|
|
2572
|
+
} else if (pathVal.type === 'protected') {
|
|
2573
|
+
if (permissionsStatus === 'pass') {
|
|
2574
|
+
permissionsStatus = 'fail';
|
|
2575
|
+
permissionsReason = `protected path`;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
continue;
|
|
2579
|
+
}
|
|
2580
|
+
const resolvedPath = pathVal.resolved;
|
|
2581
|
+
|
|
2582
|
+
if (op.type === 'create_file') {
|
|
2583
|
+
if (typeof op.content !== 'string') {
|
|
2584
|
+
if (constraintsStatus === 'pass') {
|
|
2585
|
+
constraintsStatus = 'fail';
|
|
2586
|
+
constraintsReason = `unsupported operation type`; // Treated as malformed/unsupported or type logic error
|
|
2587
|
+
}
|
|
2588
|
+
} else if (existsSync(resolvedPath) && !op.overwrite) {
|
|
2589
|
+
if (constraintsStatus === 'pass') {
|
|
2590
|
+
constraintsStatus = 'fail';
|
|
2591
|
+
constraintsReason = `create_file target exists without overwrite`;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
} else if (op.type === 'append_line') {
|
|
2595
|
+
if (typeof op.line !== 'string') {
|
|
2596
|
+
if (constraintsStatus === 'pass') {
|
|
2597
|
+
constraintsStatus = 'fail';
|
|
2598
|
+
constraintsReason = `unsupported operation type`;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
} else if (op.type === 'replace_text') {
|
|
2602
|
+
if (typeof op.find !== 'string' || typeof op.replace !== 'string') {
|
|
2603
|
+
if (constraintsStatus === 'pass') {
|
|
2604
|
+
constraintsStatus = 'fail';
|
|
2605
|
+
constraintsReason = `unsupported operation type`;
|
|
2606
|
+
}
|
|
2607
|
+
} else if (!existsSync(resolvedPath)) {
|
|
2608
|
+
if (constraintsStatus === 'pass') {
|
|
2609
|
+
constraintsStatus = 'fail';
|
|
2610
|
+
constraintsReason = `replace_text zero matches`; // file does not exist, so zero matches
|
|
2611
|
+
}
|
|
2612
|
+
} else {
|
|
2613
|
+
const fileContent = readFileSync(resolvedPath, 'utf8');
|
|
2614
|
+
let count = 0;
|
|
2615
|
+
let pos = fileContent.indexOf(op.find);
|
|
2616
|
+
while (pos !== -1) {
|
|
2617
|
+
count++;
|
|
2618
|
+
pos = fileContent.indexOf(op.find, pos + op.find.length);
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
if (count === 0) {
|
|
2622
|
+
if (constraintsStatus === 'pass') {
|
|
2623
|
+
constraintsStatus = 'fail';
|
|
2624
|
+
constraintsReason = `replace_text zero matches`;
|
|
2625
|
+
}
|
|
2626
|
+
} else if (count > 1 && !op.allow_multiple) {
|
|
2627
|
+
if (constraintsStatus === 'pass') {
|
|
2628
|
+
constraintsStatus = 'fail';
|
|
2629
|
+
constraintsReason = `replace_text multiple matches without allow_multiple`;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
validatedOperations.push({
|
|
2636
|
+
...op,
|
|
2637
|
+
resolvedPath
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
gates.types = { status: typesStatus, reason: typesReason };
|
|
2642
|
+
gates.boundaries = { status: boundariesStatus, reason: boundariesReason };
|
|
2643
|
+
gates.permissions = { status: permissionsStatus, reason: permissionsReason };
|
|
2644
|
+
gates.constraints = { status: constraintsStatus, reason: constraintsReason };
|
|
2645
|
+
|
|
2646
|
+
const gateOrder = ['frontmatter', 'approval', 'json', 'types', 'boundaries', 'permissions', 'constraints'];
|
|
2647
|
+
let firstFailReason = null;
|
|
2648
|
+
for (const g of gateOrder) {
|
|
2649
|
+
if (gates[g].status === 'fail') {
|
|
2650
|
+
firstFailReason = gates[g].reason;
|
|
2651
|
+
break;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
const valid = (firstFailReason === null);
|
|
2656
|
+
return {
|
|
2657
|
+
valid,
|
|
2658
|
+
reason: firstFailReason,
|
|
2659
|
+
gates,
|
|
2660
|
+
proposalId,
|
|
2661
|
+
proposalTitle,
|
|
2662
|
+
proposalStatus,
|
|
2663
|
+
operations: valid ? validatedOperations : []
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
function handleImproveValidate(proposalFile, options) {
|
|
2668
|
+
console.log(`🛡 \x1b[34mValidating improvement proposal: ${proposalFile}\x1b[0m\n`);
|
|
2669
|
+
const validation = validateProposal(proposalFile, options.target);
|
|
2670
|
+
|
|
2671
|
+
if (validation.proposalId) {
|
|
2672
|
+
console.log(`Proposal ID: \x1b[33m${validation.proposalId}\x1b[0m`);
|
|
2673
|
+
console.log(`Title: \x1b[37m${validation.proposalTitle}\x1b[0m`);
|
|
2674
|
+
console.log(`Status: ${validation.proposalStatus === 'approved' ? '\x1b[32m' : '\x1b[31m'}${validation.proposalStatus}\x1b[0m\n`);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
console.log(`Safety Gate Checklist:`);
|
|
2678
|
+
|
|
2679
|
+
const gateLabels = {
|
|
2680
|
+
frontmatter: 'Frontmatter Metadata',
|
|
2681
|
+
approval: 'Approval Status',
|
|
2682
|
+
json: 'Operations JSON Block',
|
|
2683
|
+
types: 'Operation Type Safety',
|
|
2684
|
+
boundaries: 'Path Boundaries (Within Target Root)',
|
|
2685
|
+
permissions: 'Path Permissions (No Protected Paths)',
|
|
2686
|
+
constraints: 'Operation Constraints (Overwrites & Replacements)'
|
|
2687
|
+
};
|
|
2688
|
+
|
|
2689
|
+
const gateOrder = ['frontmatter', 'approval', 'json', 'types', 'boundaries', 'permissions', 'constraints'];
|
|
2690
|
+
|
|
2691
|
+
gateOrder.forEach(g => {
|
|
2692
|
+
const gate = validation.gates[g];
|
|
2693
|
+
const label = gateLabels[g];
|
|
2694
|
+
if (gate.status === 'pass') {
|
|
2695
|
+
console.log(` \x1b[32m[✓]\x1b[0m ${label}`);
|
|
2696
|
+
} else if (gate.status === 'fail') {
|
|
2697
|
+
console.log(` \x1b[31m[✗]\x1b[0m ${label} - \x1b[31m${gate.reason}\x1b[0m`);
|
|
2698
|
+
} else {
|
|
2699
|
+
console.log(` \x1b[37m[-]\x1b[0m ${label}`);
|
|
2700
|
+
}
|
|
2701
|
+
});
|
|
2702
|
+
console.log();
|
|
2703
|
+
|
|
2704
|
+
if (!validation.valid) {
|
|
2705
|
+
console.error(`\x1b[31mValidation FAILED: ${validation.reason}\x1b[0m`);
|
|
2706
|
+
console.error(`\x1b[33mActionable Fix:\x1b[0m`);
|
|
2707
|
+
if (validation.reason === 'missing frontmatter') {
|
|
2708
|
+
console.error(` Please verify that the proposal file contains a valid YAML frontmatter block at the very top delimited by '---'.`);
|
|
2709
|
+
} else if (validation.reason === 'approval_status not approved') {
|
|
2710
|
+
console.error(` The proposal approval status is not set to 'approved'. Edit the frontmatter block and set 'approval_status: approved'.`);
|
|
2711
|
+
} else if (validation.reason === 'no operations block') {
|
|
2712
|
+
console.error(` No valid operations JSON block was found. Ensure a \`\`\`json block exists containing an "operations" array.`);
|
|
2713
|
+
} else if (validation.reason === 'invalid JSON operations block') {
|
|
2714
|
+
console.error(` The operations block inside \`\`\`json is not valid JSON. Run it through a JSON validator to fix syntax errors.`);
|
|
2715
|
+
} else if (validation.reason === 'unsupported operation type') {
|
|
2716
|
+
console.error(` An operation type is disallowed. Allowed types are: 'create_file', 'append_line', 'replace_text'.`);
|
|
2717
|
+
} else if (validation.reason === 'protected path') {
|
|
2718
|
+
console.error(` An operation targets a protected directory (like .git, node_modules) or configuration file (like .env, .npmrc, apply-log.jsonl).`);
|
|
2719
|
+
} else if (validation.reason === 'path outside target') {
|
|
2720
|
+
console.error(` An operation path tries to escape the target directory using directory traversal (..) or absolute paths.`);
|
|
2721
|
+
} else if (validation.reason === 'replace_text zero matches') {
|
|
2722
|
+
console.error(` The 'find' text specified in a replace_text operation was not found in the target file.`);
|
|
2723
|
+
} else if (validation.reason === 'replace_text multiple matches without allow_multiple') {
|
|
2724
|
+
console.error(` The 'find' text matched multiple times. Set 'allow_multiple: true' if you want to replace all occurrences.`);
|
|
2725
|
+
} else if (validation.reason === 'create_file target exists without overwrite') {
|
|
2726
|
+
console.error(` The target file already exists. Set 'overwrite: true' in the operation to allow overwriting.`);
|
|
2727
|
+
} else {
|
|
2728
|
+
console.error(` Check the proposal constraints and make sure all target files and fields are correct.`);
|
|
2729
|
+
}
|
|
2730
|
+
console.error();
|
|
2731
|
+
process.exit(1);
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
console.log(`\x1b[32m✔ Proposal is VALID and ready to be applied. ${validation.operations.length} operations parsed successfully.\x1b[0m\n`);
|
|
2735
|
+
process.exit(0);
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
function handleImproveDiff(proposalFile, options) {
|
|
2739
|
+
console.log(`🔍 \x1b[36mGenerating diff for proposal: ${proposalFile}\x1b[0m\n`);
|
|
2740
|
+
const validation = validateProposal(proposalFile, options.target);
|
|
2741
|
+
if (!validation.valid) {
|
|
2742
|
+
console.error(`\x1b[31mValidation FAILED: ${validation.reason}\x1b[0m`);
|
|
2743
|
+
process.exit(1);
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
const operations = validation.operations;
|
|
2747
|
+
|
|
2748
|
+
let createCount = 0;
|
|
2749
|
+
let appendCount = 0;
|
|
2750
|
+
let replaceCount = 0;
|
|
2751
|
+
const affectedFilesSet = new Set();
|
|
2752
|
+
|
|
2753
|
+
operations.forEach(op => {
|
|
2754
|
+
affectedFilesSet.add(op.path);
|
|
2755
|
+
if (op.type === 'create_file') createCount++;
|
|
2756
|
+
else if (op.type === 'append_line') appendCount++;
|
|
2757
|
+
else if (op.type === 'replace_text') replaceCount++;
|
|
2758
|
+
});
|
|
2759
|
+
|
|
2760
|
+
console.log(`Summary of Planned Changes:`);
|
|
2761
|
+
console.log(`---------------------------`);
|
|
2762
|
+
console.log(`Total Operations: \x1b[33m${operations.length}\x1b[0m`);
|
|
2763
|
+
console.log(`Operations Count: \x1b[32m${createCount} Create\x1b[0m, \x1b[33m${appendCount} Append\x1b[0m, \x1b[35m${replaceCount} Replace\x1b[0m`);
|
|
2764
|
+
console.log(`Affected Files (${affectedFilesSet.size}):`);
|
|
2765
|
+
affectedFilesSet.forEach(f => console.log(` - ${f}`));
|
|
2766
|
+
console.log();
|
|
2767
|
+
|
|
2768
|
+
const printTruncatedLines = (content, prefix, colorCode) => {
|
|
2769
|
+
const lines = content.split(/\r?\n/);
|
|
2770
|
+
const maxLines = 5;
|
|
2771
|
+
for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
|
|
2772
|
+
console.log(`${colorCode}${prefix} ${lines[i]}\x1b[0m`);
|
|
2773
|
+
}
|
|
2774
|
+
if (lines.length > maxLines) {
|
|
2775
|
+
console.log(`${colorCode}${prefix} ... (${lines.length - maxLines} more lines)\x1b[0m`);
|
|
2776
|
+
}
|
|
2777
|
+
};
|
|
2778
|
+
|
|
2779
|
+
const types = ['create_file', 'append_line', 'replace_text'];
|
|
2780
|
+
const typeHeaders = {
|
|
2781
|
+
create_file: '--- CREATE_FILE OPERATIONS ---',
|
|
2782
|
+
append_line: '--- APPEND_LINE OPERATIONS ---',
|
|
2783
|
+
replace_text: '--- REPLACE_TEXT OPERATIONS ---'
|
|
2784
|
+
};
|
|
2785
|
+
|
|
2786
|
+
types.forEach(type => {
|
|
2787
|
+
const typeOps = operations.filter(op => op.type === type);
|
|
2788
|
+
if (typeOps.length === 0) return;
|
|
2789
|
+
|
|
2790
|
+
console.log(`\x1b[36m\x1b[1m${typeHeaders[type]}\x1b[0m`);
|
|
2791
|
+
typeOps.forEach(op => {
|
|
2792
|
+
const idx = operations.indexOf(op);
|
|
2793
|
+
console.log(`\n\x1b[33m[Operation #${idx + 1}] Target: ${op.path}\x1b[0m`);
|
|
2794
|
+
|
|
2795
|
+
if (type === 'create_file') {
|
|
2796
|
+
const exists = existsSync(op.resolvedPath);
|
|
2797
|
+
if (exists) {
|
|
2798
|
+
console.log(` \x1b[31m⚠️ [Overwriting existing file]\x1b[0m`);
|
|
2799
|
+
} else {
|
|
2800
|
+
console.log(` \x1b[32m+ [Creating new file]\x1b[0m`);
|
|
2801
|
+
}
|
|
2802
|
+
const linesCount = op.content.split(/\r?\n/).length;
|
|
2803
|
+
console.log(` + [File content: ${linesCount} line(s), overwrite: ${!!op.overwrite}]`);
|
|
2804
|
+
printTruncatedLines(op.content, ' +', '\x1b[32m');
|
|
2805
|
+
} else if (type === 'append_line') {
|
|
2806
|
+
const exists = existsSync(op.resolvedPath);
|
|
2807
|
+
let currentFileContent = '';
|
|
2808
|
+
if (exists) {
|
|
2809
|
+
currentFileContent = readFileSync(op.resolvedPath, 'utf8');
|
|
2810
|
+
}
|
|
2811
|
+
const fileLines = currentFileContent.split(/\r?\n/);
|
|
2812
|
+
const lineExists = fileLines.some(l => l.trim() === op.line.trim());
|
|
2813
|
+
if (lineExists) {
|
|
2814
|
+
console.log(` \x1b[33m[IDEMPOTENT] Line already exists in file. No changes will be made.\x1b[0m`);
|
|
2815
|
+
} else {
|
|
2816
|
+
console.log(` \x1b[32m+ Appending line:\x1b[0m`);
|
|
2817
|
+
console.log(` \x1b[32m+ ${op.line}\x1b[0m`);
|
|
2818
|
+
}
|
|
2819
|
+
} else if (type === 'replace_text') {
|
|
2820
|
+
console.log(` --- a/${op.path}`);
|
|
2821
|
+
console.log(` +++ b/${op.path}`);
|
|
2822
|
+
console.log(` \x1b[31m- Removing:\x1b[0m`);
|
|
2823
|
+
printTruncatedLines(op.find, ' -', '\x1b[31m');
|
|
2824
|
+
console.log(` \x1b[32m+ Inserting:\x1b[0m`);
|
|
2825
|
+
printTruncatedLines(op.replace, ' +', '\x1b[32m');
|
|
2826
|
+
}
|
|
2827
|
+
});
|
|
2828
|
+
console.log();
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
function handleImproveApply(proposalFile, options) {
|
|
2833
|
+
if (!options.approved) {
|
|
2834
|
+
console.error(`\x1b[31mError: Proposal cannot be applied without explicit user approval. Pass the --approved flag.\x1b[0m`);
|
|
2835
|
+
console.error(`Example: node bin/multimodel-dev-os.js improve apply ${proposalFile} --approved`);
|
|
2836
|
+
process.exit(1);
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
console.log(`🚀 \x1b[34mApplying proposal: ${proposalFile}\x1b[0m`);
|
|
2840
|
+
const validation = validateProposal(proposalFile, options.target);
|
|
2841
|
+
if (!validation.valid) {
|
|
2842
|
+
console.error(`\x1b[31mValidation FAILED: ${validation.reason}\x1b[0m`);
|
|
2843
|
+
|
|
2844
|
+
// Log the refusal
|
|
2845
|
+
const applyId = `apply-${new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14)}`;
|
|
2846
|
+
const logDir = join(options.target, '.ai', 'proposals');
|
|
2847
|
+
if (!existsSync(logDir)) {
|
|
2848
|
+
try { mkdirSync(logDir, { recursive: true }); } catch (e) {}
|
|
2849
|
+
}
|
|
2850
|
+
const logFile = join(logDir, 'apply-log.jsonl');
|
|
2851
|
+
const record = {
|
|
2852
|
+
id: applyId,
|
|
2853
|
+
proposal_id: validation.proposalId || basename(proposalFile, '.md'),
|
|
2854
|
+
applied_at: new Date().toISOString(),
|
|
2855
|
+
target: options.target,
|
|
2856
|
+
operations_count: 0,
|
|
2857
|
+
files_changed: [],
|
|
2858
|
+
before_hashes: {},
|
|
2859
|
+
after_hashes: {},
|
|
2860
|
+
status: 'refused',
|
|
2861
|
+
refused_reason: validation.reason,
|
|
2862
|
+
notes: `Validation failed: ${validation.reason}`
|
|
2863
|
+
};
|
|
2864
|
+
try {
|
|
2865
|
+
writeFileSync(logFile, JSON.stringify(record) + '\n', { flag: 'a', encoding: 'utf8' });
|
|
2866
|
+
} catch (err) {}
|
|
2867
|
+
process.exit(1);
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
const operations = validation.operations;
|
|
2871
|
+
const proposalId = validation.proposalId;
|
|
2872
|
+
|
|
2873
|
+
// Print compact operations summary
|
|
2874
|
+
const createCount = operations.filter(op => op.type === 'create_file').length;
|
|
2875
|
+
const appendCount = operations.filter(op => op.type === 'append_line').length;
|
|
2876
|
+
const replaceCount = operations.filter(op => op.type === 'replace_text').length;
|
|
2877
|
+
console.log(`Summary of Operations:`);
|
|
2878
|
+
console.log(` - ${createCount} file(s) to create`);
|
|
2879
|
+
console.log(` - ${appendCount} file(s) to append`);
|
|
2880
|
+
console.log(` - ${replaceCount} file(s) to modify (replace)`);
|
|
2881
|
+
console.log(`\nApplying changes...`);
|
|
2882
|
+
|
|
2883
|
+
const filesChanged = [];
|
|
2884
|
+
const beforeHashes = {};
|
|
2885
|
+
const afterHashes = {};
|
|
2886
|
+
let status = 'success';
|
|
2887
|
+
let notes = '';
|
|
2888
|
+
|
|
2889
|
+
const applyId = `apply-${new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14)}`;
|
|
2890
|
+
|
|
2891
|
+
try {
|
|
2892
|
+
operations.forEach(op => {
|
|
2893
|
+
const relPath = relative(options.target, op.resolvedPath).replace(/\\/g, '/');
|
|
2894
|
+
if (!filesChanged.includes(relPath)) {
|
|
2895
|
+
filesChanged.push(relPath);
|
|
2896
|
+
}
|
|
2897
|
+
if (existsSync(op.resolvedPath)) {
|
|
2898
|
+
const fileContent = readFileSync(op.resolvedPath, 'utf8');
|
|
2899
|
+
beforeHashes[relPath] = getSha256(fileContent);
|
|
2900
|
+
} else {
|
|
2901
|
+
beforeHashes[relPath] = null;
|
|
2902
|
+
}
|
|
2903
|
+
});
|
|
2904
|
+
|
|
2905
|
+
operations.forEach((op, idx) => {
|
|
2906
|
+
const relPath = relative(options.target, op.resolvedPath).replace(/\\/g, '/');
|
|
2907
|
+
console.log(` Executing Operation #${idx + 1} (${op.type}) on '${relPath}'...`);
|
|
2908
|
+
|
|
2909
|
+
if (op.type === 'create_file') {
|
|
2910
|
+
const dir = dirname(op.resolvedPath);
|
|
2911
|
+
if (!existsSync(dir)) {
|
|
2912
|
+
mkdirSync(dir, { recursive: true });
|
|
2913
|
+
}
|
|
2914
|
+
const exists = existsSync(op.resolvedPath);
|
|
2915
|
+
writeFileSync(op.resolvedPath, op.content, 'utf8');
|
|
2916
|
+
if (exists) {
|
|
2917
|
+
console.log(` [OVERWRITTEN] Overwrote existing file '${relPath}'.`);
|
|
2918
|
+
} else {
|
|
2919
|
+
console.log(` [CREATED] Created new file '${relPath}'.`);
|
|
2920
|
+
}
|
|
2921
|
+
} else if (op.type === 'append_line') {
|
|
2922
|
+
let content = '';
|
|
2923
|
+
if (existsSync(op.resolvedPath)) {
|
|
2924
|
+
content = readFileSync(op.resolvedPath, 'utf8');
|
|
2925
|
+
}
|
|
2926
|
+
const fileLines = content.split(/\r?\n/);
|
|
2927
|
+
const lineExists = fileLines.some(l => l.trim() === op.line.trim());
|
|
2928
|
+
if (!lineExists) {
|
|
2929
|
+
let newContent = content;
|
|
2930
|
+
if (content.length > 0 && !content.endsWith('\n') && !content.endsWith('\r')) {
|
|
2931
|
+
newContent += '\n';
|
|
2932
|
+
}
|
|
2933
|
+
newContent += op.line + '\n';
|
|
2934
|
+
const dir = dirname(op.resolvedPath);
|
|
2935
|
+
if (!existsSync(dir)) {
|
|
2936
|
+
mkdirSync(dir, { recursive: true });
|
|
2937
|
+
}
|
|
2938
|
+
writeFileSync(op.resolvedPath, newContent, 'utf8');
|
|
2939
|
+
console.log(` [APPENDED] Appended 1 line to '${relPath}'.`);
|
|
2940
|
+
} else {
|
|
2941
|
+
console.log(` [IDEMPOTENT] Line already exists in '${relPath}'. Skipping append.`);
|
|
2942
|
+
}
|
|
2943
|
+
} else if (op.type === 'replace_text') {
|
|
2944
|
+
const fileContent = readFileSync(op.resolvedPath, 'utf8');
|
|
2945
|
+
let count = 0;
|
|
2946
|
+
let pos = fileContent.indexOf(op.find);
|
|
2947
|
+
while (pos !== -1) {
|
|
2948
|
+
count++;
|
|
2949
|
+
pos = fileContent.indexOf(op.find, pos + op.find.length);
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
let newContent;
|
|
2953
|
+
if (op.allow_multiple) {
|
|
2954
|
+
newContent = fileContent.split(op.find).join(op.replace);
|
|
2955
|
+
} else {
|
|
2956
|
+
newContent = fileContent.replace(op.find, op.replace);
|
|
2957
|
+
if (count > 0) count = 1;
|
|
2958
|
+
}
|
|
2959
|
+
writeFileSync(op.resolvedPath, newContent, 'utf8');
|
|
2960
|
+
console.log(` [REPLACED] Replaced ${count} occurrence(s) of find text in '${relPath}'.`);
|
|
2961
|
+
}
|
|
2962
|
+
});
|
|
2963
|
+
|
|
2964
|
+
filesChanged.forEach(relPath => {
|
|
2965
|
+
const fullPath = resolve(options.target, relPath);
|
|
2966
|
+
if (existsSync(fullPath)) {
|
|
2967
|
+
const fileContent = readFileSync(fullPath, 'utf8');
|
|
2968
|
+
afterHashes[relPath] = getSha256(fileContent);
|
|
2969
|
+
} else {
|
|
2970
|
+
afterHashes[relPath] = null;
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
|
|
2974
|
+
notes = `Successfully applied ${operations.length} operations.`;
|
|
2975
|
+
} catch (e) {
|
|
2976
|
+
status = 'failed';
|
|
2977
|
+
notes = `Execution error: ${e.message}`;
|
|
2978
|
+
console.error(`\x1b[31mError applying proposal: ${e.message}\x1b[0m`);
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
const logDir = join(options.target, '.ai', 'proposals');
|
|
2982
|
+
if (!existsSync(logDir)) {
|
|
2983
|
+
mkdirSync(logDir, { recursive: true });
|
|
2984
|
+
}
|
|
2985
|
+
const logFile = join(logDir, 'apply-log.jsonl');
|
|
2986
|
+
|
|
2987
|
+
const record = {
|
|
2988
|
+
id: applyId,
|
|
2989
|
+
proposal_id: proposalId,
|
|
2990
|
+
applied_at: new Date().toISOString(),
|
|
2991
|
+
target: options.target,
|
|
2992
|
+
operations_count: operations.length,
|
|
2993
|
+
files_changed: filesChanged,
|
|
2994
|
+
before_hashes: beforeHashes,
|
|
2995
|
+
after_hashes: afterHashes,
|
|
2996
|
+
status,
|
|
2997
|
+
refused_reason: status === 'failed' ? notes : undefined,
|
|
2998
|
+
notes
|
|
2999
|
+
};
|
|
3000
|
+
|
|
3001
|
+
try {
|
|
3002
|
+
writeFileSync(logFile, JSON.stringify(record) + '\n', { flag: 'a', encoding: 'utf8' });
|
|
3003
|
+
} catch (err) {
|
|
3004
|
+
console.error(`\x1b[31mFailed to write to audit log: ${err.message}\x1b[0m`);
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
if (status === 'success') {
|
|
3008
|
+
console.log(`\n\x1b[32m✔ Proposal applied successfully!\x1b[0m`);
|
|
3009
|
+
console.log(`Files changed:`);
|
|
3010
|
+
filesChanged.forEach(f => console.log(` - ${f}`));
|
|
3011
|
+
console.log(`Audit log recorded to: ${logFile}`);
|
|
3012
|
+
} else {
|
|
3013
|
+
process.exit(1);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
function handleImproveLog(options) {
|
|
3018
|
+
const logFile = join(options.target, '.ai', 'proposals', 'apply-log.jsonl');
|
|
3019
|
+
if (!existsSync(logFile)) {
|
|
3020
|
+
console.log('No apply log found.');
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
try {
|
|
3025
|
+
const lines = readFileSync(logFile, 'utf8').trim().split(/\r?\n/);
|
|
3026
|
+
console.log(`\n📜 \x1b[36mApplied Proposals Audit Log\x1b[0m`);
|
|
3027
|
+
console.log('==================================================');
|
|
3028
|
+
lines.forEach(line => {
|
|
3029
|
+
if (!line.trim()) return;
|
|
3030
|
+
const record = JSON.parse(line);
|
|
3031
|
+
const statusColor = record.status === 'success' ? '\x1b[32m' : '\x1b[31m';
|
|
3032
|
+
console.log(`\n\x1b[34m* [${record.id}] Proposal: ${record.proposal_id}\x1b[0m`);
|
|
3033
|
+
console.log(` \x1b[37mApplied At:\x1b[0m ${record.applied_at}`);
|
|
3034
|
+
console.log(` \x1b[37mOperations:\x1b[0m ${record.operations_count}`);
|
|
3035
|
+
console.log(` \x1b[37mFiles Changed:\x1b[0m ${record.files_changed.join(', ')}`);
|
|
3036
|
+
console.log(` \x1b[37mStatus:\x1b[0m ${statusColor}${record.status}\x1b[0m`);
|
|
3037
|
+
console.log(` \x1b[37mNotes:\x1b[0m ${record.notes}`);
|
|
3038
|
+
});
|
|
3039
|
+
console.log();
|
|
3040
|
+
} catch (e) {
|
|
3041
|
+
console.error(`\x1b[31mError reading audit log: ${e.message}\x1b[0m`);
|
|
3042
|
+
process.exit(1);
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// ==================================================
|
|
3047
|
+
// v2.5.0 Repository Intelligence Command Center
|
|
3048
|
+
// ==================================================
|
|
3049
|
+
|
|
3050
|
+
function handleStatus(options) {
|
|
3051
|
+
console.log(`\n📊 \x1b[36mRepository Intelligence Status: ${options.target}\x1b[0m`);
|
|
3052
|
+
console.log('==================================================');
|
|
3053
|
+
|
|
3054
|
+
// 1. Project Info
|
|
3055
|
+
let pkgName = 'unknown';
|
|
3056
|
+
let pkgVersion = 'unknown';
|
|
3057
|
+
try {
|
|
3058
|
+
const pkgPath = join(options.target, 'package.json');
|
|
3059
|
+
if (existsSync(pkgPath)) {
|
|
3060
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
3061
|
+
pkgName = pkg.name || pkgName;
|
|
3062
|
+
pkgVersion = pkg.version || pkgVersion;
|
|
3063
|
+
}
|
|
3064
|
+
} catch (e) {}
|
|
3065
|
+
console.log(` \x1b[33mProject Info:\x1b[0m`);
|
|
3066
|
+
console.log(` Package Name: ${pkgName}`);
|
|
3067
|
+
console.log(` Package Version: ${pkgVersion}`);
|
|
3068
|
+
|
|
3069
|
+
// 2. Framework signals
|
|
3070
|
+
const { files } = scanTarget(options.target);
|
|
3071
|
+
const frameworkSignals = detectFrameworkSignals(files, options.target);
|
|
3072
|
+
const dependencySignals = detectDependencySignals(files, options.target);
|
|
3073
|
+
console.log(` \x1b[33mFramework & Dependency Signals:\x1b[0m`);
|
|
3074
|
+
console.log(` Frameworks: ${frameworkSignals.join(', ') || 'None'}`);
|
|
3075
|
+
console.log(` Dependencies: ${dependencySignals.join(', ') || 'None'}`);
|
|
3076
|
+
|
|
3077
|
+
// 3. Memory status
|
|
3078
|
+
const memoryHashPath = join(options.target, '.ai', 'intelligence', 'memory.hash.json');
|
|
3079
|
+
let memoryStatus = '\x1b[31mMISSING\x1b[0m';
|
|
3080
|
+
let lastBuildTime = 'N/A';
|
|
3081
|
+
if (existsSync(memoryHashPath)) {
|
|
3082
|
+
try {
|
|
3083
|
+
const memObj = JSON.parse(readFileSync(memoryHashPath, 'utf8'));
|
|
3084
|
+
lastBuildTime = memObj.generated_at || 'N/A';
|
|
3085
|
+
const diff = diffMemory(options.target);
|
|
3086
|
+
if (diff) {
|
|
3087
|
+
if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) {
|
|
3088
|
+
memoryStatus = '\x1b[32mCURRENT\x1b[0m';
|
|
3089
|
+
} else {
|
|
3090
|
+
memoryStatus = `\x1b[33mSTALE\x1b[0m (changes: +${diff.added.length}, -${diff.removed.length}, ~${diff.changed.length})`;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
} catch (e) {
|
|
3094
|
+
memoryStatus = '\x1b[31mCORRUPT\x1b[0m';
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
console.log(` \x1b[33mMemory State:\x1b[0m`);
|
|
3098
|
+
console.log(` Status: ${memoryStatus}`);
|
|
3099
|
+
console.log(` Last Built: ${lastBuildTime}`);
|
|
3100
|
+
|
|
3101
|
+
// 4. Feedback & Rules
|
|
3102
|
+
const feedbackPath = join(options.target, '.ai', 'intelligence', 'feedback-log.jsonl');
|
|
3103
|
+
let feedbackCount = 0;
|
|
3104
|
+
if (existsSync(feedbackPath)) {
|
|
3105
|
+
try {
|
|
3106
|
+
feedbackCount = readFileSync(feedbackPath, 'utf8').trim().split(/\r?\n/).filter(l => l.trim() !== '').length;
|
|
3107
|
+
} catch (e) {}
|
|
3108
|
+
}
|
|
3109
|
+
const rulesPath = join(options.target, '.ai', 'intelligence', 'learning-rules.md');
|
|
3110
|
+
const rulesStatus = existsSync(rulesPath) ? '\x1b[32mPRESENT\x1b[0m' : '\x1b[31mMISSING\x1b[0m';
|
|
3111
|
+
console.log(` \x1b[33mFeedback Loop & Rules:\x1b[0m`);
|
|
3112
|
+
console.log(` Feedback Count: ${feedbackCount}`);
|
|
3113
|
+
console.log(` Learning Rules: ${rulesStatus}`);
|
|
3114
|
+
|
|
3115
|
+
// 5. Proposals Engine
|
|
3116
|
+
const proposalsDir = join(options.target, '.ai', 'proposals');
|
|
3117
|
+
let pendingCount = 0;
|
|
3118
|
+
let approvedCount = 0;
|
|
3119
|
+
let rejectedCount = 0;
|
|
3120
|
+
let totalProposals = 0;
|
|
3121
|
+
if (existsSync(proposalsDir)) {
|
|
3122
|
+
try {
|
|
3123
|
+
const propFiles = readdirSync(proposalsDir).filter(f => f.startsWith('proposal-') && f.endsWith('.md'));
|
|
3124
|
+
totalProposals = propFiles.length;
|
|
3125
|
+
propFiles.forEach(file => {
|
|
3126
|
+
const content = readFileSync(join(proposalsDir, file), 'utf8');
|
|
3127
|
+
const fmMatch = content.match(/^---([\s\S]*?)---/);
|
|
3128
|
+
if (fmMatch) {
|
|
3129
|
+
const metadata = parseYaml(fmMatch[1]) || {};
|
|
3130
|
+
const status = metadata.approval_status || 'pending';
|
|
3131
|
+
if (status === 'approved') approvedCount++;
|
|
3132
|
+
else if (status === 'rejected') rejectedCount++;
|
|
3133
|
+
else pendingCount++;
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
} catch (e) {}
|
|
3137
|
+
}
|
|
3138
|
+
console.log(` \x1b[33mImprovement Proposals:\x1b[0m`);
|
|
3139
|
+
console.log(` Total proposals: ${totalProposals}`);
|
|
3140
|
+
console.log(` Pending: \x1b[33m${pendingCount}\x1b[0m`);
|
|
3141
|
+
console.log(` Approved: \x1b[32m${approvedCount}\x1b[0m`);
|
|
3142
|
+
console.log(` Rejected: \x1b[31m${rejectedCount}\x1b[0m`);
|
|
3143
|
+
|
|
3144
|
+
// 6. Apply Log History
|
|
3145
|
+
const applyLogPath = join(options.target, '.ai', 'proposals', 'apply-log.jsonl');
|
|
3146
|
+
let applyLogCount = 0;
|
|
3147
|
+
if (existsSync(applyLogPath)) {
|
|
3148
|
+
try {
|
|
3149
|
+
applyLogCount = readFileSync(applyLogPath, 'utf8').trim().split(/\r?\n/).filter(l => l.trim() !== '').length;
|
|
3150
|
+
} catch (e) {}
|
|
3151
|
+
}
|
|
3152
|
+
console.log(` \x1b[33mApply Audit Log:\x1b[0m`);
|
|
3153
|
+
console.log(` Apply Count: ${applyLogCount}`);
|
|
3154
|
+
|
|
3155
|
+
// 7. Recommended Next Move
|
|
3156
|
+
let nextMove = 'mmdo status';
|
|
3157
|
+
if (!existsSync(join(options.target, '.ai', 'config.yaml'))) {
|
|
3158
|
+
nextMove = '\x1b[36mnpx multimodel-dev-os init\x1b[0m (initialize MultiModel Dev OS first)';
|
|
3159
|
+
} else if (!existsSync(memoryHashPath)) {
|
|
3160
|
+
nextMove = '\x1b[36mnpx multimodel-dev-os memory build\x1b[0m (initialize memory index)';
|
|
3161
|
+
} else {
|
|
3162
|
+
const diff = diffMemory(options.target);
|
|
3163
|
+
if (diff && (diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0)) {
|
|
3164
|
+
nextMove = '\x1b[36mnpx multimodel-dev-os memory refresh\x1b[0m (update memory with changes)';
|
|
3165
|
+
} else if (feedbackCount > 0 && !existsSync(rulesPath)) {
|
|
3166
|
+
nextMove = '\x1b[36mnpx multimodel-dev-os feedback summarize\x1b[0m (compile feedback into learning rules)';
|
|
3167
|
+
} else if (pendingCount > 0) {
|
|
3168
|
+
nextMove = '\x1b[36mnpx multimodel-dev-os improve review\x1b[0m (review pending proposals)';
|
|
3169
|
+
} else {
|
|
3170
|
+
nextMove = '\x1b[36mnpx multimodel-dev-os workflow run repo-health\x1b[0m (run standard codebase health checks)';
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
console.log(`\n \x1b[35mRecommended Next Command:\x1b[0m`);
|
|
3174
|
+
console.log(` ${nextMove}\n`);
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
function getWorkflowsPath(target) {
|
|
3178
|
+
let workflowsPath = join(target, '.ai', 'registries', 'workflows.yaml');
|
|
3179
|
+
let usingFallback = false;
|
|
3180
|
+
if (!existsSync(workflowsPath)) {
|
|
3181
|
+
const fallbackPath = join(sourceRoot, '.ai', 'registries', 'workflows.yaml');
|
|
3182
|
+
if (existsSync(fallbackPath)) {
|
|
3183
|
+
workflowsPath = fallbackPath;
|
|
3184
|
+
usingFallback = true;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
return { workflowsPath, usingFallback };
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
function handleWorkflowList(options) {
|
|
3191
|
+
const { workflowsPath, usingFallback } = getWorkflowsPath(options.target);
|
|
3192
|
+
if (!existsSync(workflowsPath)) {
|
|
3193
|
+
console.log('No workflows registry found.');
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
if (usingFallback) {
|
|
3197
|
+
console.log('\x1b[33mNotice: Local workflows registry not found. Using bundled workflows registry fallback.\x1b[0m');
|
|
3198
|
+
}
|
|
3199
|
+
try {
|
|
3200
|
+
const registry = parseYaml(readFileSync(workflowsPath, 'utf8')) || {};
|
|
3201
|
+
const workflows = registry.workflows || {};
|
|
3202
|
+
console.log(`\n⚙ \x1b[36mRegistered Workflows\x1b[0m`);
|
|
3203
|
+
console.log('==================================================');
|
|
3204
|
+
Object.keys(workflows).forEach(key => {
|
|
3205
|
+
const wf = workflows[key];
|
|
3206
|
+
const name = wf.name || key;
|
|
3207
|
+
const risk = wf.risk_level || 'unknown';
|
|
3208
|
+
const riskColor = risk === 'low' ? '\x1b[32m' : risk === 'medium' ? '\x1b[33m' : '\x1b[31m';
|
|
3209
|
+
console.log(`\n \x1b[34m* ${name}\x1b[0m (\x1b[35m${key}\x1b[0m)`);
|
|
3210
|
+
console.log(` Description: ${wf.description || 'No description'}`);
|
|
3211
|
+
console.log(` Risk Level: ${riskColor}${risk.toUpperCase()}\x1b[0m`);
|
|
3212
|
+
});
|
|
3213
|
+
console.log();
|
|
3214
|
+
} catch (e) {
|
|
3215
|
+
console.error(`\x1b[31mError loading workflows: ${e.message}\x1b[0m`);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
function handleWorkflowShow(wName, options) {
|
|
3220
|
+
const { workflowsPath, usingFallback } = getWorkflowsPath(options.target);
|
|
3221
|
+
if (!existsSync(workflowsPath)) {
|
|
3222
|
+
console.log('No workflows registry found.');
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
3225
|
+
if (usingFallback) {
|
|
3226
|
+
console.log('\x1b[33mNotice: Local workflows registry not found. Using bundled workflows registry fallback.\x1b[0m');
|
|
3227
|
+
}
|
|
3228
|
+
try {
|
|
3229
|
+
const registry = parseYaml(readFileSync(workflowsPath, 'utf8')) || {};
|
|
3230
|
+
const workflows = registry.workflows || {};
|
|
3231
|
+
const wf = workflows[wName];
|
|
3232
|
+
if (!wf) {
|
|
3233
|
+
console.error(`\x1b[31mError: Workflow '${wName}' not found in registry.\x1b[0m`);
|
|
3234
|
+
process.exit(1);
|
|
3235
|
+
}
|
|
3236
|
+
const name = wf.name || wName;
|
|
3237
|
+
const risk = wf.risk_level || 'unknown';
|
|
3238
|
+
const riskColor = risk === 'low' ? '\x1b[32m' : risk === 'medium' ? '\x1b[33m' : '\x1b[31m';
|
|
3239
|
+
console.log(`\n⚙ \x1b[36mWorkflow Spec: ${name}\x1b[0m`);
|
|
3240
|
+
console.log('==================================================');
|
|
3241
|
+
console.log(` Description: ${wf.description || 'No description'}`);
|
|
3242
|
+
console.log(` Risk Level: ${riskColor}${risk.toUpperCase()}\x1b[0m`);
|
|
3243
|
+
console.log(` Allowed to write memory: ${wf.allowed_to_write_memory || false}`);
|
|
3244
|
+
console.log(` Allowed to modify code: ${wf.allowed_to_modify_source || false}`);
|
|
3245
|
+
console.log(`\n \x1b[33mSteps:\x1b[0m`);
|
|
3246
|
+
|
|
3247
|
+
const steps = wf.steps || [];
|
|
3248
|
+
steps.forEach((step, idx) => {
|
|
3249
|
+
console.log(` ${idx + 1}. [${step.name}]`);
|
|
3250
|
+
console.log(` Command: ${step.command}`);
|
|
3251
|
+
console.log(` Expected Output: ${step.expected_output || 'N/A'}`);
|
|
3252
|
+
console.log(` Next Action: ${step.next_action || 'N/A'}`);
|
|
3253
|
+
});
|
|
3254
|
+
console.log();
|
|
3255
|
+
} catch (e) {
|
|
3256
|
+
console.error(`\x1b[31mError loading workflow '${wName}': ${e.message}\x1b[0m`);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
function handleWorkflowPlan(wName, options) {
|
|
3261
|
+
const { workflowsPath, usingFallback } = getWorkflowsPath(options.target);
|
|
3262
|
+
if (!existsSync(workflowsPath)) {
|
|
3263
|
+
console.log('No workflows registry found.');
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
if (usingFallback) {
|
|
3267
|
+
console.log('\x1b[33mNotice: Local workflows registry not found. Using bundled workflows registry fallback.\x1b[0m');
|
|
3268
|
+
}
|
|
3269
|
+
try {
|
|
3270
|
+
const registry = parseYaml(readFileSync(workflowsPath, 'utf8')) || {};
|
|
3271
|
+
const workflows = registry.workflows || {};
|
|
3272
|
+
const wf = workflows[wName];
|
|
3273
|
+
if (!wf) {
|
|
3274
|
+
console.error(`\x1b[31mError: Workflow '${wName}' not found.\x1b[0m`);
|
|
3275
|
+
process.exit(1);
|
|
3276
|
+
}
|
|
3277
|
+
const name = wf.name || wName;
|
|
3278
|
+
console.log(`\n📝 \x1b[36mExecution Plan for Workflow: ${name}\x1b[0m`);
|
|
3279
|
+
console.log('==================================================');
|
|
3280
|
+
console.log(`\x1b[33m[DRY-RUN/PLAN ONLY] No commands will be run.\x1b[0m\n`);
|
|
3281
|
+
const steps = wf.steps || [];
|
|
3282
|
+
steps.forEach((step, idx) => {
|
|
3283
|
+
console.log(` Step ${idx + 1}: ${step.name}`);
|
|
3284
|
+
console.log(` Command: ${step.command}`);
|
|
3285
|
+
console.log(` Expected Output: ${step.expected_output || 'N/A'}`);
|
|
3286
|
+
console.log(` Next Action: ${step.next_action || 'N/A'}`);
|
|
3287
|
+
});
|
|
3288
|
+
console.log();
|
|
3289
|
+
} catch (e) {
|
|
3290
|
+
console.error(`\x1b[31mError loading workflow plan: ${e.message}\x1b[0m`);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
function handleWorkflowRun(wName, options) {
|
|
3295
|
+
const { workflowsPath, usingFallback } = getWorkflowsPath(options.target);
|
|
3296
|
+
if (!existsSync(workflowsPath)) {
|
|
3297
|
+
console.log('No workflows registry found.');
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
if (usingFallback) {
|
|
3301
|
+
console.log('\x1b[33mNotice: Local workflows registry not found. Using bundled workflows registry fallback.\x1b[0m');
|
|
3302
|
+
}
|
|
3303
|
+
try {
|
|
3304
|
+
const registry = parseYaml(readFileSync(workflowsPath, 'utf8')) || {};
|
|
3305
|
+
const workflows = registry.workflows || {};
|
|
3306
|
+
const wf = workflows[wName];
|
|
3307
|
+
if (!wf) {
|
|
3308
|
+
console.error(`\x1b[31mError: Workflow '${wName}' not found.\x1b[0m`);
|
|
3309
|
+
process.exit(1);
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
const name = wf.name || wName;
|
|
3313
|
+
console.log(`\n🚀 \x1b[36mRunning Workflow: ${name}\x1b[0m`);
|
|
3314
|
+
console.log('==================================================');
|
|
3315
|
+
|
|
3316
|
+
const steps = wf.steps || [];
|
|
3317
|
+
const safeCommands = {
|
|
3318
|
+
'scan': () => handleScan(options),
|
|
3319
|
+
'doctor': () => handleDoctor(options),
|
|
3320
|
+
'verify': () => handleVerify({ ...options, noExit: true }),
|
|
3321
|
+
'memory diff': () => handleMemoryDiff({ ...options, noExit: true }),
|
|
3322
|
+
'memory refresh': () => handleMemoryRefresh(options),
|
|
3323
|
+
'memory build': () => handleMemoryBuild(options),
|
|
3324
|
+
'feedback list': () => handleFeedbackList(options),
|
|
3325
|
+
'feedback summarize': () => handleFeedbackSummarize(options),
|
|
3326
|
+
'improve review': () => handleImproveReview(options),
|
|
3327
|
+
'improve status': () => handleImproveStatus(options),
|
|
3328
|
+
'improve log': () => handleImproveLog(options),
|
|
3329
|
+
'doctor --release': () => handleDoctor({ ...options, release: true })
|
|
3330
|
+
};
|
|
3331
|
+
|
|
3332
|
+
steps.forEach((step, idx) => {
|
|
3333
|
+
console.log(`\n\x1b[33m[Step ${idx + 1}/${steps.length}] Running: ${step.name} (${step.command})\x1b[0m`);
|
|
3334
|
+
const cmd = step.command;
|
|
3335
|
+
if (safeCommands[cmd]) {
|
|
3336
|
+
try {
|
|
3337
|
+
safeCommands[cmd]();
|
|
3338
|
+
} catch (e) {
|
|
3339
|
+
console.error(`\x1b[31mError executing step ${step.name}: ${e.message}\x1b[0m`);
|
|
3340
|
+
}
|
|
3341
|
+
} else {
|
|
3342
|
+
console.log(` \x1b[35m[MANUAL ACTION NEEDED]\x1b[0m This step requires manual execution.`);
|
|
3343
|
+
console.log(` Please run command: \x1b[36mnpx multimodel-dev-os ${cmd}\x1b[0m`);
|
|
3344
|
+
if (step.expected_output) {
|
|
3345
|
+
console.log(` Expected Output: ${step.expected_output}`);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
});
|
|
3349
|
+
console.log(`\n✔ Workflow '${name}' complete.\n`);
|
|
3350
|
+
} catch (e) {
|
|
3351
|
+
console.error(`\x1b[31mError running workflow '${wName}': ${e.message}\x1b[0m`);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
function handleHandoffBuild(options) {
|
|
3356
|
+
const intelDir = join(options.target, '.ai', 'intelligence');
|
|
3357
|
+
if (!existsSync(intelDir)) {
|
|
3358
|
+
mkdirSync(intelDir, { recursive: true });
|
|
3359
|
+
}
|
|
3360
|
+
const handoffPath = join(intelDir, 'handoff.md');
|
|
3361
|
+
|
|
3362
|
+
// 1. Get package metadata
|
|
3363
|
+
let pkgName = 'unknown';
|
|
3364
|
+
let pkgVersion = 'unknown';
|
|
3365
|
+
try {
|
|
3366
|
+
const pkgPath = join(options.target, 'package.json');
|
|
3367
|
+
if (existsSync(pkgPath)) {
|
|
3368
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
3369
|
+
pkgName = pkg.name || pkgName;
|
|
3370
|
+
pkgVersion = pkg.version || pkgVersion;
|
|
3371
|
+
}
|
|
3372
|
+
} catch (e) {}
|
|
3373
|
+
|
|
3374
|
+
// 2. Scan targets
|
|
3375
|
+
const { files } = scanTarget(options.target);
|
|
3376
|
+
const frameworkSignals = detectFrameworkSignals(files, options.target);
|
|
3377
|
+
const dependencySignals = detectDependencySignals(files, options.target);
|
|
3378
|
+
|
|
3379
|
+
// 3. Memory
|
|
3380
|
+
const memoryHashPath = join(intelDir, 'memory.hash.json');
|
|
3381
|
+
let memoryStatus = 'MISSING';
|
|
3382
|
+
let memoryTime = 'N/A';
|
|
3383
|
+
if (existsSync(memoryHashPath)) {
|
|
3384
|
+
try {
|
|
3385
|
+
const memObj = JSON.parse(readFileSync(memoryHashPath, 'utf8'));
|
|
3386
|
+
memoryTime = memObj.generated_at || 'N/A';
|
|
3387
|
+
const diff = diffMemory(options.target);
|
|
3388
|
+
if (diff) {
|
|
3389
|
+
memoryStatus = (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) ? 'CURRENT' : 'STALE';
|
|
3390
|
+
}
|
|
3391
|
+
} catch (e) {
|
|
3392
|
+
memoryStatus = 'CORRUPT';
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
// 4. Feedback
|
|
3397
|
+
const feedbackPath = join(intelDir, 'feedback-log.jsonl');
|
|
3398
|
+
let feedbackCount = 0;
|
|
3399
|
+
if (existsSync(feedbackPath)) {
|
|
3400
|
+
try {
|
|
3401
|
+
feedbackCount = readFileSync(feedbackPath, 'utf8').trim().split(/\r?\n/).filter(l => l.trim() !== '').length;
|
|
3402
|
+
} catch (e) {}
|
|
3403
|
+
}
|
|
3404
|
+
const rulesPath = join(intelDir, 'learning-rules.md');
|
|
3405
|
+
const rulesStatus = existsSync(rulesPath) ? 'PRESENT' : 'MISSING';
|
|
3406
|
+
|
|
3407
|
+
// 5. Proposals
|
|
3408
|
+
const proposalsDir = join(options.target, '.ai', 'proposals');
|
|
3409
|
+
let pendingCount = 0;
|
|
3410
|
+
let approvedCount = 0;
|
|
3411
|
+
let rejectedCount = 0;
|
|
3412
|
+
if (existsSync(proposalsDir)) {
|
|
3413
|
+
try {
|
|
3414
|
+
const propFiles = readdirSync(proposalsDir).filter(f => f.startsWith('proposal-') && f.endsWith('.md'));
|
|
3415
|
+
propFiles.forEach(file => {
|
|
3416
|
+
const content = readFileSync(join(proposalsDir, file), 'utf8');
|
|
3417
|
+
const fmMatch = content.match(/^---([\s\S]*?)---/);
|
|
3418
|
+
if (fmMatch) {
|
|
3419
|
+
const metadata = parseYaml(fmMatch[1]) || {};
|
|
3420
|
+
const status = metadata.approval_status || 'pending';
|
|
3421
|
+
if (status === 'approved') approvedCount++;
|
|
3422
|
+
else if (status === 'rejected') rejectedCount++;
|
|
3423
|
+
else pendingCount++;
|
|
3424
|
+
}
|
|
3425
|
+
});
|
|
3426
|
+
} catch (e) {}
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
// 6. Apply logs
|
|
3430
|
+
const applyLogPath = join(proposalsDir, 'apply-log.jsonl');
|
|
3431
|
+
let applyLogCount = 0;
|
|
3432
|
+
let lastApplyId = 'None';
|
|
3433
|
+
if (existsSync(applyLogPath)) {
|
|
3434
|
+
try {
|
|
3435
|
+
const lines = readFileSync(applyLogPath, 'utf8').trim().split(/\r?\n/).filter(l => l.trim() !== '');
|
|
3436
|
+
applyLogCount = lines.length;
|
|
3437
|
+
if (applyLogCount > 0) {
|
|
3438
|
+
const lastRecord = JSON.parse(lines[lines.length - 1]);
|
|
3439
|
+
lastApplyId = lastRecord.id || 'unknown';
|
|
3440
|
+
}
|
|
3441
|
+
} catch (e) {}
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
// 7. Core Learning Summary
|
|
3445
|
+
let rulesSummary = 'No learning rules defined yet.';
|
|
3446
|
+
if (existsSync(rulesPath)) {
|
|
3447
|
+
try {
|
|
3448
|
+
const rulesContent = readFileSync(rulesPath, 'utf8');
|
|
3449
|
+
const lines = rulesContent.split(/\r?\n/);
|
|
3450
|
+
const summaryLines = [];
|
|
3451
|
+
for (const line of lines) {
|
|
3452
|
+
if (line.startsWith('* **Pattern:**') || line.startsWith(' * **Rule:**')) {
|
|
3453
|
+
summaryLines.push(line);
|
|
3454
|
+
}
|
|
3455
|
+
if (summaryLines.length >= 10) break;
|
|
3456
|
+
}
|
|
3457
|
+
if (summaryLines.length > 0) {
|
|
3458
|
+
rulesSummary = summaryLines.join('\n');
|
|
3459
|
+
}
|
|
3460
|
+
} catch (e) {}
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
// Next steps recommended
|
|
3464
|
+
let recs = '1. Run `npx multimodel-dev-os workflow run repo-health` to check the directory hygiene.\n2. Review pending proposals if any exist.';
|
|
3465
|
+
if (!existsSync(join(options.target, '.ai', 'config.yaml'))) {
|
|
3466
|
+
recs = '1. Run `npx multimodel-dev-os init` to bootstrap MultiModel Dev OS.\n2. Run `npx multimodel-dev-os memory build` to initialize codebase memory.';
|
|
3467
|
+
} else if (memoryStatus === 'MISSING') {
|
|
3468
|
+
recs = '1. Run `npx multimodel-dev-os memory build` to initialize codebase index.\n2. Verify package safety boundaries.';
|
|
3469
|
+
} else if (memoryStatus === 'STALE') {
|
|
3470
|
+
recs = '1. Run `npx multimodel-dev-os memory refresh` to update memory files.\n2. Analyze modifications.';
|
|
3471
|
+
} else if (pendingCount > 0) {
|
|
3472
|
+
recs = `1. Run \`npx multimodel-dev-os improve review\` to inspect the ${pendingCount} pending proposals.\n2. Apply approved changes manually.`;
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
const handoffContent = `# Agent Handoff Spec - ${new Date().toISOString()}
|
|
3476
|
+
|
|
3477
|
+
## 1. Project Context
|
|
3478
|
+
- **Name**: ${pkgName}
|
|
3479
|
+
- **Version**: ${pkgVersion}
|
|
3480
|
+
- **Frameworks**: ${frameworkSignals.join(', ') || 'None'}
|
|
3481
|
+
- **Dependencies**: ${dependencySignals.join(', ') || 'None'}
|
|
3482
|
+
|
|
3483
|
+
## 2. Intelligence Core State
|
|
3484
|
+
- **Memory**: ${memoryStatus} (Last build: ${memoryTime})
|
|
3485
|
+
- **Feedback Loop**: ${feedbackCount} items logged. \`learning-rules.md\` is ${rulesStatus}.
|
|
3486
|
+
- **Proposals**: ${pendingCount} Pending, ${approvedCount} Approved, ${rejectedCount} Rejected.
|
|
3487
|
+
- **Applied Modifications**: ${applyLogCount} runs recorded. Last run: ${lastApplyId}.
|
|
3488
|
+
|
|
3489
|
+
## 3. Core Learning Summaries
|
|
3490
|
+
\`\`\`markdown
|
|
3491
|
+
${rulesSummary}
|
|
3492
|
+
\`\`\`
|
|
3493
|
+
|
|
3494
|
+
## 4. Safety Constraints
|
|
3495
|
+
- Workflow run is restricted to read-only actions.
|
|
3496
|
+
- Modifications must be applied explicitly via \`improve apply --approved\`.
|
|
3497
|
+
- No code modification permissions exist in this session context.
|
|
3498
|
+
|
|
3499
|
+
## 5. Recommended Next Steps
|
|
3500
|
+
${recs}
|
|
3501
|
+
`;
|
|
3502
|
+
|
|
3503
|
+
try {
|
|
3504
|
+
writeFileSync(handoffPath, handoffContent, 'utf8');
|
|
3505
|
+
console.log(`\n✔ Handoff context built successfully in: .ai/intelligence/handoff.md`);
|
|
3506
|
+
} catch (e) {
|
|
3507
|
+
console.error(`\x1b[31mError writing handoff: ${e.message}\x1b[0m`);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
function handleHandoffShow(options) {
|
|
3512
|
+
const handoffPath = join(options.target, '.ai', 'intelligence', 'handoff.md');
|
|
3513
|
+
if (!existsSync(handoffPath)) {
|
|
3514
|
+
console.log('No compiled handoff file exists. Building first...');
|
|
3515
|
+
handleHandoffBuild(options);
|
|
3516
|
+
}
|
|
3517
|
+
try {
|
|
3518
|
+
const content = readFileSync(handoffPath, 'utf8');
|
|
3519
|
+
console.log('\n' + content);
|
|
3520
|
+
} catch (e) {
|
|
3521
|
+
console.error(`\x1b[31mError reading handoff: ${e.message}\x1b[0m`);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
function handleDoctorIntelligence(options) {
|
|
3526
|
+
console.log(`\n🩺 \x1b[36mRunning advisory intelligence doctor checkup in: ${options.target}\x1b[0m\n`);
|
|
3527
|
+
|
|
3528
|
+
let warnings = 0;
|
|
3529
|
+
const warn = (msg) => {
|
|
3530
|
+
console.warn(` \x1b[33m[WARNING]\x1b[0m ${msg}`);
|
|
3531
|
+
warnings++;
|
|
3532
|
+
};
|
|
3533
|
+
|
|
3534
|
+
// 1. Memory checks
|
|
3535
|
+
const memoryHashPath = join(options.target, '.ai', 'intelligence', 'memory.hash.json');
|
|
3536
|
+
if (!existsSync(memoryHashPath)) {
|
|
3537
|
+
warn('Memory hash index (.ai/intelligence/memory.hash.json) is MISSING. Run `memory build` first.');
|
|
3538
|
+
} else {
|
|
3539
|
+
try {
|
|
3540
|
+
const diff = diffMemory(options.target);
|
|
3541
|
+
if (!diff) {
|
|
3542
|
+
warn('Memory hash index is present but corrupt.');
|
|
3543
|
+
} else if (diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0) {
|
|
3544
|
+
warn(`Memory hash index is STALE. Delts: +${diff.added.length}, -${diff.removed.length}, ~${diff.changed.length}. Run \`memory refresh\`.`);
|
|
3545
|
+
}
|
|
3546
|
+
} catch (e) {
|
|
3547
|
+
warn('Failed to diff memory index.');
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
// 2. Feedback checks
|
|
3552
|
+
const feedbackPath = join(options.target, '.ai', 'intelligence', 'feedback-log.jsonl');
|
|
3553
|
+
if (!existsSync(feedbackPath)) {
|
|
3554
|
+
warn('Feedback log (.ai/intelligence/feedback-log.jsonl) is MISSING.');
|
|
3555
|
+
}
|
|
3556
|
+
const rulesPath = join(options.target, '.ai', 'intelligence', 'learning-rules.md');
|
|
3557
|
+
if (!existsSync(rulesPath)) {
|
|
3558
|
+
warn('Learning rules (.ai/intelligence/learning-rules.md) are MISSING. Run `feedback summarize` to compile logs.');
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
// 3. Proposals checks
|
|
3562
|
+
const proposalsDir = join(options.target, '.ai', 'proposals');
|
|
3563
|
+
if (!existsSync(proposalsDir)) {
|
|
3564
|
+
warn('Proposals directory (.ai/proposals) is MISSING.');
|
|
3565
|
+
} else {
|
|
3566
|
+
try {
|
|
3567
|
+
const files = readdirSync(proposalsDir).filter(f => f.startsWith('proposal-') && f.endsWith('.md'));
|
|
3568
|
+
let pending = 0;
|
|
3569
|
+
files.forEach(file => {
|
|
3570
|
+
const content = readFileSync(join(proposalsDir, file), 'utf8');
|
|
3571
|
+
const fmMatch = content.match(/^---([\s\S]*?)---/);
|
|
3572
|
+
if (fmMatch) {
|
|
3573
|
+
const metadata = parseYaml(fmMatch[1]) || {};
|
|
3574
|
+
if ((metadata.approval_status || 'pending') === 'pending') {
|
|
3575
|
+
pending++;
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
});
|
|
3579
|
+
if (pending > 0) {
|
|
3580
|
+
warn(`Found ${pending} pending improvement proposals waiting for approval.`);
|
|
3581
|
+
}
|
|
3582
|
+
} catch (e) {}
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
// 4. Apply log check
|
|
3586
|
+
const applyLogPath = join(options.target, '.ai', 'proposals', 'apply-log.jsonl');
|
|
3587
|
+
if (!existsSync(applyLogPath)) {
|
|
3588
|
+
warn('Apply audit log (.ai/proposals/apply-log.jsonl) is MISSING.');
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
// 5. Gitignore ignores intelligence checks
|
|
3592
|
+
const gitignorePath = join(options.target, '.gitignore');
|
|
3593
|
+
if (existsSync(gitignorePath)) {
|
|
3594
|
+
const gitignoreContent = readFileSync(gitignorePath, 'utf8');
|
|
3595
|
+
const checkIgnore = (pattern) => {
|
|
3596
|
+
if (!gitignoreContent.includes(pattern)) {
|
|
3597
|
+
warn(`.gitignore is missing rules ignoring: ${pattern}`);
|
|
3598
|
+
}
|
|
3599
|
+
};
|
|
3600
|
+
checkIgnore('.ai/intelligence/handoff.md');
|
|
3601
|
+
checkIgnore('.ai/intelligence/status.snapshot.json');
|
|
3602
|
+
checkIgnore('.ai/intelligence/feedback-log.jsonl');
|
|
3603
|
+
checkIgnore('.ai/intelligence/learning-rules.md');
|
|
3604
|
+
checkIgnore('.ai/proposals/apply-log.jsonl');
|
|
3605
|
+
} else {
|
|
3606
|
+
warn('.gitignore file is missing in target root.');
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
// 6. Danger files check inside memory index
|
|
3610
|
+
if (existsSync(memoryHashPath)) {
|
|
3611
|
+
try {
|
|
3612
|
+
const memObj = JSON.parse(readFileSync(memoryHashPath, 'utf8'));
|
|
3613
|
+
const fingerprints = memObj.file_fingerprints || {};
|
|
3614
|
+
Object.keys(fingerprints).forEach(file => {
|
|
3615
|
+
const name = file.toLowerCase();
|
|
3616
|
+
if (name.includes('.env') || name.includes('id_rsa') || name.includes('credential') || name.endsWith('.pem') || name.endsWith('.p12') || name.endsWith('.key') || name.endsWith('.keystore') || name.endsWith('.jks')) {
|
|
3617
|
+
warn(`Memory index contains potentially sensitive file: ${file}`);
|
|
3618
|
+
}
|
|
3619
|
+
});
|
|
3620
|
+
} catch (e) {}
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
console.log('\n==================================================');
|
|
3624
|
+
if (warnings > 0) {
|
|
3625
|
+
console.log(`\x1b[33mDoctor intelligence check complete. Found ${warnings} warnings.\x1b[0m\n`);
|
|
3626
|
+
} else {
|
|
3627
|
+
console.log('\x1b[32m✔ Doctor intelligence check complete. Your intelligence setup is pristine!\x1b[0m\n');
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
function getAnalysis(target) {
|
|
3632
|
+
const { files, ignoredCount } = scanTarget(target);
|
|
3633
|
+
const frameworks = detectFrameworkSignals(files, target);
|
|
3634
|
+
const packageManagers = detectDependencySignals(files, target);
|
|
3635
|
+
const aiSignals = detectAiDevOsSignals(files);
|
|
3636
|
+
|
|
3637
|
+
let jsCount = 0, tsCount = 0, phpCount = 0, pyCount = 0, mdCount = 0;
|
|
3638
|
+
files.forEach(f => {
|
|
3639
|
+
const ext = f.relPath.substring(f.relPath.lastIndexOf('.')).toLowerCase();
|
|
3640
|
+
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') jsCount++;
|
|
3641
|
+
else if (ext === '.ts' || ext === '.tsx') tsCount++;
|
|
3642
|
+
else if (ext === '.php') phpCount++;
|
|
3643
|
+
else if (ext === '.py') pyCount++;
|
|
3644
|
+
else if (ext === '.md') mdCount++;
|
|
3645
|
+
});
|
|
3646
|
+
|
|
3647
|
+
let language = 'mixed';
|
|
3648
|
+
if (tsCount > jsCount && tsCount > phpCount && tsCount > pyCount && tsCount > mdCount) language = 'TS';
|
|
3649
|
+
else if (jsCount > tsCount && jsCount > phpCount && jsCount > pyCount && jsCount > mdCount) language = 'JS';
|
|
3650
|
+
else if (phpCount > jsCount && phpCount > tsCount && phpCount > pyCount && phpCount > mdCount) language = 'PHP';
|
|
3651
|
+
else if (pyCount > jsCount && pyCount > tsCount && phpCount > pyCount && phpCount > mdCount) language = 'Python';
|
|
3652
|
+
else if (mdCount > jsCount && mdCount > tsCount && mdCount > phpCount && mdCount > pyCount) language = 'Markdown-heavy';
|
|
3653
|
+
|
|
3654
|
+
let repoType = 'app';
|
|
3655
|
+
if (files.some(f => f.relPath.includes('wp-content/themes') || f.relPath.includes('wp-content/plugins'))) {
|
|
3656
|
+
repoType = 'WordPress theme/plugin';
|
|
3657
|
+
} else if (files.some(f => f.relPath.includes('app.json') || f.relPath.includes('eas.json'))) {
|
|
3658
|
+
repoType = 'mobile app';
|
|
3659
|
+
} else if (files.some(f => f.relPath.includes('lerna.json') || f.relPath.includes('pnpm-workspace.yaml'))) {
|
|
3660
|
+
repoType = 'monorepo';
|
|
3661
|
+
} else if (files.some(f => f.relPath.includes('docs/')) && mdCount > (files.length * 0.4)) {
|
|
3662
|
+
repoType = 'docs';
|
|
3663
|
+
} else if (files.some(f => f.relPath === 'package.json')) {
|
|
3664
|
+
try {
|
|
3665
|
+
const pkg = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8'));
|
|
3666
|
+
if (pkg.main && (pkg.main.includes('dist/') || pkg.main.includes('lib/'))) {
|
|
3667
|
+
repoType = 'library';
|
|
3668
|
+
}
|
|
3669
|
+
} catch (e) {}
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
const existingTools = [];
|
|
3673
|
+
if (files.some(f => f.relPath === '.cursorrules')) existingTools.push('Cursor');
|
|
3674
|
+
if (files.some(f => f.relPath === 'CLAUDE.md')) existingTools.push('Claude');
|
|
3675
|
+
if (files.some(f => f.relPath === 'GEMINI.md')) existingTools.push('Gemini');
|
|
3676
|
+
if (files.some(f => f.relPath.startsWith('.vscode/'))) existingTools.push('VS Code');
|
|
3677
|
+
if (files.some(f => f.relPath.startsWith('.gemini/'))) existingTools.push('Antigravity');
|
|
3678
|
+
|
|
3679
|
+
const packageScripts = [];
|
|
3680
|
+
if (files.some(f => f.relPath === 'package.json')) {
|
|
3681
|
+
try {
|
|
3682
|
+
const pkg = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8'));
|
|
3683
|
+
if (pkg.scripts) {
|
|
3684
|
+
Object.keys(pkg.scripts).forEach(k => packageScripts.push(k));
|
|
3685
|
+
}
|
|
3686
|
+
} catch (e) {}
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
const githubWorkflows = [];
|
|
3690
|
+
const githubDir = join(target, '.github', 'workflows');
|
|
3691
|
+
if (existsSync(githubDir)) {
|
|
3692
|
+
try {
|
|
3693
|
+
readdirSync(githubDir).forEach(f => {
|
|
3694
|
+
if (f.endsWith('.yml') || f.endsWith('.yaml')) githubWorkflows.push(f);
|
|
3695
|
+
});
|
|
3696
|
+
} catch (e) {}
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
const envRiskMarkers = [];
|
|
3700
|
+
files.forEach(f => {
|
|
3701
|
+
const name = f.relPath.toLowerCase();
|
|
3702
|
+
if (name.includes('.env') || name.includes('id_rsa') || name.includes('credential') || name.endsWith('.pem') || name.endsWith('.key') || name.endsWith('.keystore') || name.endsWith('.jks')) {
|
|
3703
|
+
envRiskMarkers.push(f.relPath);
|
|
3704
|
+
}
|
|
3705
|
+
});
|
|
3706
|
+
|
|
3707
|
+
return {
|
|
3708
|
+
packageManagers,
|
|
3709
|
+
frameworks,
|
|
3710
|
+
language,
|
|
3711
|
+
repoType,
|
|
3712
|
+
existingTools,
|
|
3713
|
+
packageScripts,
|
|
3714
|
+
githubWorkflows,
|
|
3715
|
+
envRiskMarkers,
|
|
3716
|
+
aiSignals,
|
|
3717
|
+
filesCount: files.length,
|
|
3718
|
+
ignoredCount
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
function getRecommendation(analysis) {
|
|
3723
|
+
const scores = {
|
|
3724
|
+
'nextjs-saas': 0.0,
|
|
3725
|
+
'expo-react-native-android': 0.0,
|
|
3726
|
+
'wordpress-site': 0.0,
|
|
3727
|
+
'ecommerce-store': 0.0,
|
|
3728
|
+
'seo-landing-page': 0.0,
|
|
3729
|
+
'general-app': 0.1
|
|
3730
|
+
};
|
|
3731
|
+
|
|
3732
|
+
if (analysis.frameworks.includes('Next.js')) scores['nextjs-saas'] += 0.6;
|
|
3733
|
+
if (analysis.frameworks.includes('React')) scores['nextjs-saas'] += 0.2;
|
|
3734
|
+
if (analysis.frameworks.includes('TypeScript')) scores['nextjs-saas'] += 0.1;
|
|
3735
|
+
|
|
3736
|
+
if (analysis.repoType === 'mobile app') scores['expo-react-native-android'] += 0.6;
|
|
3737
|
+
if (analysis.frameworks.includes('Expo') || analysis.frameworks.includes('React Native')) scores['expo-react-native-android'] += 0.3;
|
|
3738
|
+
|
|
3739
|
+
if (analysis.repoType === 'WordPress theme/plugin') scores['wordpress-site'] += 0.6;
|
|
3740
|
+
if (analysis.frameworks.includes('WordPress/PHP')) scores['wordpress-site'] += 0.3;
|
|
3741
|
+
|
|
3742
|
+
if (analysis.frameworks.includes('Vite') || analysis.frameworks.includes('React')) scores['seo-landing-page'] += 0.3;
|
|
3743
|
+
|
|
3744
|
+
let recommended = 'general-app';
|
|
3745
|
+
let maxScore = 0.0;
|
|
3746
|
+
Object.keys(scores).forEach(k => {
|
|
3747
|
+
if (scores[k] > maxScore) {
|
|
3748
|
+
maxScore = scores[k];
|
|
3749
|
+
recommended = k;
|
|
3750
|
+
}
|
|
3751
|
+
});
|
|
3752
|
+
|
|
3753
|
+
const suggestedAdapters = ['cursor', 'claude', 'gemini', 'vscode', 'antigravity'];
|
|
3754
|
+
|
|
3755
|
+
return {
|
|
3756
|
+
template: recommended,
|
|
3757
|
+
confidence: Math.min(1.0, maxScore === 0.1 ? 0.5 : maxScore),
|
|
3758
|
+
suggestedAdapters,
|
|
3759
|
+
riskNotes: analysis.envRiskMarkers.length > 0 ? 'Workspace contains unignored credentials or key files. Ensure .gitignore covers them.' : 'None'
|
|
3760
|
+
};
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
function handleOnboardAnalyze(options) {
|
|
3764
|
+
console.log(`\n🔍 \x1b[36mAnalyzing Workspace for Onboarding: ${options.target}\x1b[0m`);
|
|
3765
|
+
console.log('==================================================');
|
|
3766
|
+
const analysis = getAnalysis(options.target);
|
|
3767
|
+
|
|
3768
|
+
console.log(` Package Manager: ${analysis.packageManagers.join(', ') || 'None'}`);
|
|
3769
|
+
console.log(` Detected Frameworks: ${analysis.frameworks.join(', ') || 'None'}`);
|
|
3770
|
+
console.log(` Dominant Language: ${analysis.language}`);
|
|
3771
|
+
console.log(` Repository Type: ${analysis.repoType}`);
|
|
3772
|
+
console.log(` Existing AI Tools: ${analysis.existingTools.join(', ') || 'None'}`);
|
|
3773
|
+
console.log(` GitHub Workflows: ${analysis.githubWorkflows.join(', ') || 'None'}`);
|
|
3774
|
+
console.log(` Security Risk Markers: ${analysis.envRiskMarkers.length} files found`);
|
|
3775
|
+
if (analysis.envRiskMarkers.length > 0) {
|
|
3776
|
+
analysis.envRiskMarkers.forEach(m => console.log(` └─> ${m} (potential secrets exposure risk)`));
|
|
3777
|
+
}
|
|
3778
|
+
console.log();
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
function handleOnboardRecommend(options) {
|
|
3782
|
+
const analysis = getAnalysis(options.target);
|
|
3783
|
+
const rec = getRecommendation(analysis);
|
|
3784
|
+
|
|
3785
|
+
console.log(`\n💡 \x1b[36mOnboarding Recommendation for: ${options.target}\x1b[0m`);
|
|
3786
|
+
console.log('==================================================');
|
|
3787
|
+
console.log(` Recommended Template: \x1b[32m${rec.template}\x1b[0m`);
|
|
3788
|
+
console.log(` Confidence Score: ${(rec.confidence * 100).toFixed(0)}%`);
|
|
3789
|
+
console.log(` Suggested Adapters: ${rec.suggestedAdapters.join(', ')}`);
|
|
3790
|
+
console.log(` Risk Notes: ${rec.riskNotes}`);
|
|
3791
|
+
console.log(` Suggested Next Command:`);
|
|
3792
|
+
console.log(` npx multimodel-dev-os onboard plan --target .`);
|
|
3793
|
+
console.log();
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
function handleOnboardPlan(options) {
|
|
3797
|
+
console.log(`\n📋 \x1b[36mGenerating Onboarding Plan: ${options.target}\x1b[0m`);
|
|
3798
|
+
console.log('==================================================');
|
|
3799
|
+
const analysis = getAnalysis(options.target);
|
|
3800
|
+
const rec = getRecommendation(analysis);
|
|
3801
|
+
|
|
3802
|
+
const planPath = join(options.target, '.ai', 'intelligence', 'onboarding.plan.json');
|
|
3803
|
+
const reportPath = join(options.target, '.ai', 'intelligence', 'onboarding.report.md');
|
|
3804
|
+
|
|
3805
|
+
const plannedFiles = [
|
|
3806
|
+
{ action: 'CREATE', path: 'AGENTS.md', source_template: `examples/${rec.template}/AGENTS.md` },
|
|
3807
|
+
{ action: 'CREATE', path: 'MEMORY.md', source_template: `examples/${rec.template}/MEMORY.md` },
|
|
3808
|
+
{ action: 'CREATE', path: 'TASKS.md', source_template: `examples/${rec.template}/TASKS.md` },
|
|
3809
|
+
{ action: 'CREATE', path: 'RUNBOOK.md', source_template: `RUNBOOK.md` },
|
|
3810
|
+
{ action: 'CREATE', path: '.ai/config.yaml', source_template: `examples/${rec.template}/.ai/config.yaml` }
|
|
3811
|
+
];
|
|
3812
|
+
|
|
3813
|
+
const planData = {
|
|
3814
|
+
generated_at: new Date().toISOString(),
|
|
3815
|
+
target_path: options.target,
|
|
3816
|
+
project_analysis: {
|
|
3817
|
+
package_manager: analysis.packageManagers.join(', ') || 'npm',
|
|
3818
|
+
framework: analysis.frameworks.join(', ') || 'Generic',
|
|
3819
|
+
language: analysis.language,
|
|
3820
|
+
repo_type: analysis.repoType,
|
|
3821
|
+
has_existing_ai_config: analysis.aiSignals.includes('.ai/config.yaml'),
|
|
3822
|
+
risk_markers: analysis.envRiskMarkers
|
|
3823
|
+
},
|
|
3824
|
+
recommendation: {
|
|
3825
|
+
template: rec.template,
|
|
3826
|
+
confidence: rec.confidence,
|
|
3827
|
+
suggested_adapters: rec.suggestedAdapters,
|
|
3828
|
+
reasons: [`Detected dominant language ${analysis.language}`, `Detected framework ${analysis.frameworks.join(', ')}`]
|
|
3829
|
+
},
|
|
3830
|
+
planned_files: plannedFiles
|
|
3831
|
+
};
|
|
3832
|
+
|
|
3833
|
+
let reportMd = `# MultiModel Dev OS Onboarding Report\n\n`;
|
|
3834
|
+
reportMd += `**Generated At:** ${planData.generated_at}\n`;
|
|
3835
|
+
reportMd += `**Target Path:** ${planData.target_path}\n\n`;
|
|
3836
|
+
reportMd += `## 1. Project Analysis Details\n`;
|
|
3837
|
+
reportMd += `- **Package Manager:** ${planData.project_analysis.package_manager}\n`;
|
|
3838
|
+
reportMd += `- **Frameworks:** ${planData.project_analysis.framework}\n`;
|
|
3839
|
+
reportMd += `- **Language:** ${planData.project_analysis.language}\n`;
|
|
3840
|
+
reportMd += `- **Repo Type:** ${planData.project_analysis.repo_type}\n\n`;
|
|
3841
|
+
|
|
3842
|
+
reportMd += `## 2. Onboarding Recommendation\n`;
|
|
3843
|
+
reportMd += `- **Recommended Profile:** **${planData.recommendation.template}** (Confidence: ${(planData.recommendation.confidence * 100).toFixed(0)}%)\n`;
|
|
3844
|
+
reportMd += `- **Suggested Adapters:** ${planData.recommendation.suggested_adapters.join(', ')}\n\n`;
|
|
3845
|
+
|
|
3846
|
+
reportMd += `## 3. Planned File Operations\n`;
|
|
3847
|
+
reportMd += `| Action | Target Path | Source Template |\n`;
|
|
3848
|
+
reportMd += `|---|---|---|\n`;
|
|
3849
|
+
plannedFiles.forEach(f => {
|
|
3850
|
+
reportMd += `| ${f.action} | ${f.path} | ${f.source_template} |\n`;
|
|
3851
|
+
});
|
|
3852
|
+
reportMd += `\n`;
|
|
3853
|
+
|
|
3854
|
+
reportMd += `## 4. Next Step\n`;
|
|
3855
|
+
reportMd += `To safely apply this plan, run:\n`;
|
|
3856
|
+
reportMd += `\`\`\`bash\n`;
|
|
3857
|
+
reportMd += `npx multimodel-dev-os onboard apply --target . --approved\n`;
|
|
3858
|
+
reportMd += `\`\`\`\n`;
|
|
3859
|
+
|
|
3860
|
+
try {
|
|
3861
|
+
const intelDir = join(options.target, '.ai', 'intelligence');
|
|
3862
|
+
if (!options.dryRun && !existsSync(intelDir)) {
|
|
3863
|
+
mkdirSync(intelDir, { recursive: true });
|
|
3864
|
+
}
|
|
3865
|
+
if (!options.dryRun) {
|
|
3866
|
+
writeFileSync(planPath, JSON.stringify(planData, null, 2), 'utf8');
|
|
3867
|
+
writeFileSync(reportPath, reportMd, 'utf8');
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
console.log(` [SUCCESS] Onboarding plan generated:`);
|
|
3871
|
+
console.log(` - Plan JSON: .ai/intelligence/onboarding.plan.json`);
|
|
3872
|
+
console.log(` - Report MD: .ai/intelligence/onboarding.report.md`);
|
|
3873
|
+
console.log(`\nReview the plan and run "npx multimodel-dev-os onboard apply --target . --approved" to execute.\n`);
|
|
3874
|
+
} catch (e) {
|
|
3875
|
+
console.error(`\x1b[31mError writing plan: ${e.message}\x1b[0m`);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
function handleOnboardApply(options) {
|
|
3880
|
+
if (!options.approved) {
|
|
3881
|
+
console.error('\x1b[31mError: Onboarding apply requires explicit approval flag: --approved\x1b[0m');
|
|
3882
|
+
console.log('Example: node bin/multimodel-dev-os.js onboard apply --approved');
|
|
3883
|
+
process.exit(1);
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
const planPath = join(options.target, '.ai', 'intelligence', 'onboarding.plan.json');
|
|
3887
|
+
if (!existsSync(planPath)) {
|
|
3888
|
+
console.error('\x1b[31mError: Onboarding plan not found. Run "npx multimodel-dev-os onboard plan" first.\x1b[0m');
|
|
3889
|
+
process.exit(1);
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
let plan;
|
|
3893
|
+
try {
|
|
3894
|
+
plan = JSON.parse(readFileSync(planPath, 'utf8'));
|
|
3895
|
+
} catch (e) {
|
|
3896
|
+
console.error(`\x1b[31mError reading plan JSON: ${e.message}\x1b[0m`);
|
|
3897
|
+
process.exit(1);
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
console.log(`\n🚀 \x1b[36mApplying Onboarding Scaffolding: ${options.target}\x1b[0m`);
|
|
3901
|
+
console.log('==================================================');
|
|
3902
|
+
|
|
3903
|
+
const template = plan.recommendation.template;
|
|
3904
|
+
options.template = template;
|
|
3905
|
+
|
|
3906
|
+
const operations = [];
|
|
3907
|
+
|
|
3908
|
+
plan.planned_files.forEach(f => {
|
|
3909
|
+
let srcFile;
|
|
3910
|
+
if (f.source_template === 'RUNBOOK.md') {
|
|
3911
|
+
srcFile = join(sourceRoot, 'RUNBOOK.md');
|
|
3912
|
+
} else {
|
|
3913
|
+
srcFile = join(sourceRoot, f.source_template);
|
|
3914
|
+
}
|
|
3915
|
+
operations.push({ dest: f.path, src: srcFile });
|
|
3916
|
+
});
|
|
3917
|
+
|
|
3918
|
+
const templateDir = join(sourceRoot, 'examples', template);
|
|
3919
|
+
const templateAiDir = join(templateDir, '.ai');
|
|
3920
|
+
if (existsSync(templateAiDir) && !options.caveman) {
|
|
3921
|
+
const subdirs = ['context', 'skills'];
|
|
3922
|
+
subdirs.forEach(sub => {
|
|
3923
|
+
const subPath = join(templateAiDir, sub);
|
|
3924
|
+
if (existsSync(subPath)) {
|
|
3925
|
+
readdirSync(subPath).forEach(file => {
|
|
3926
|
+
operations.push({
|
|
3927
|
+
dest: join('.ai', sub, file),
|
|
3928
|
+
src: join(subPath, file)
|
|
3929
|
+
});
|
|
3930
|
+
});
|
|
3931
|
+
}
|
|
3932
|
+
});
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
const globalAiSubdirs = ['context', 'agents', 'skills', 'prompts', 'checks', 'templates', 'session-logs', 'registries', 'proposals', 'intelligence'];
|
|
3936
|
+
globalAiSubdirs.forEach(sub => {
|
|
3937
|
+
const globalPath = join(sourceRoot, '.ai', sub);
|
|
3938
|
+
if (existsSync(globalPath)) {
|
|
3939
|
+
readdirSync(globalPath).forEach(file => {
|
|
3940
|
+
const destRel = join('.ai', sub, file);
|
|
3941
|
+
if (!operations.some(op => op.dest === destRel)) {
|
|
3942
|
+
if (options.caveman && (sub === 'context' || sub === 'skills' || sub === 'prompts' || sub === 'checks')) {
|
|
3943
|
+
return;
|
|
3944
|
+
}
|
|
3945
|
+
operations.push({
|
|
3946
|
+
dest: destRel,
|
|
3947
|
+
src: join(globalPath, file)
|
|
3948
|
+
});
|
|
3949
|
+
}
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
});
|
|
3953
|
+
|
|
3954
|
+
let createdCount = 0;
|
|
3955
|
+
let skippedCount = 0;
|
|
3956
|
+
let updatedCount = 0;
|
|
3957
|
+
|
|
3958
|
+
operations.forEach(op => {
|
|
3959
|
+
const destPath = join(options.target, op.dest);
|
|
3960
|
+
const destDir = dirname(destPath);
|
|
3961
|
+
|
|
3962
|
+
if (existsSync(destPath)) {
|
|
3963
|
+
if (options.force) {
|
|
3964
|
+
if (!options.dryRun) {
|
|
3965
|
+
const backupPath = destPath + '.bak';
|
|
3966
|
+
writeFileSync(backupPath, readFileSync(destPath));
|
|
3967
|
+
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
3968
|
+
writeFileSync(destPath, readFileSync(op.src));
|
|
3969
|
+
console.log(` \x1b[33mOVERWRITE (BACKUP CREATED):\x1b[0m ${op.dest} -> ${op.dest}.bak`);
|
|
3970
|
+
} else {
|
|
3971
|
+
console.log(` \x1b[36m[DRY-RUN] WOULD OVERWRITE & BACKUP:\x1b[0m ${op.dest}`);
|
|
3972
|
+
}
|
|
3973
|
+
updatedCount++;
|
|
3974
|
+
} else {
|
|
3975
|
+
console.log(` \x1b[37m[SKIP] Already exists:\x1b[0m ${op.dest}`);
|
|
3976
|
+
skippedCount++;
|
|
3977
|
+
}
|
|
3978
|
+
} else {
|
|
3979
|
+
if (!options.dryRun) {
|
|
3980
|
+
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
3981
|
+
writeFileSync(destPath, readFileSync(op.src));
|
|
3982
|
+
console.log(` \x1b[32mCREATE:\x1b[0m ${op.dest}`);
|
|
3983
|
+
} else {
|
|
3984
|
+
console.log(` \x1b[36m[DRY-RUN] WOULD CREATE:\x1b[0m ${op.dest}`);
|
|
3985
|
+
}
|
|
3986
|
+
createdCount++;
|
|
3987
|
+
}
|
|
3988
|
+
});
|
|
3989
|
+
|
|
3990
|
+
console.log(`\n✔ Onboarding apply complete! Created: ${createdCount}, Skipped: ${skippedCount}, Overwritten (with backup): ${updatedCount}\n`);
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
function handleOnboardStatus(options) {
|
|
3994
|
+
console.log(`\n📊 \x1b[36mOnboarding Status Dashboard: ${options.target}\x1b[0m`);
|
|
3995
|
+
console.log('==================================================');
|
|
3996
|
+
|
|
3997
|
+
const crucialFiles = [
|
|
3998
|
+
'AGENTS.md',
|
|
3999
|
+
'MEMORY.md',
|
|
4000
|
+
'TASKS.md',
|
|
4001
|
+
'RUNBOOK.md',
|
|
4002
|
+
'.ai/config.yaml'
|
|
4003
|
+
];
|
|
4004
|
+
|
|
4005
|
+
let presentCount = 0;
|
|
4006
|
+
crucialFiles.forEach(f => {
|
|
4007
|
+
const fullPath = join(options.target, f);
|
|
4008
|
+
const exists = existsSync(fullPath);
|
|
4009
|
+
if (exists) presentCount++;
|
|
4010
|
+
console.log(` [${exists ? '✔' : ' '}] ${f}`);
|
|
4011
|
+
});
|
|
4012
|
+
|
|
4013
|
+
const percentage = (presentCount / crucialFiles.length) * 100;
|
|
4014
|
+
console.log(`\n Completeness Score: ${percentage.toFixed(0)}%`);
|
|
4015
|
+
if (percentage === 100) {
|
|
4016
|
+
console.log(' Status: \x1b[32mREADY (Onboarding complete)\x1b[0m\n');
|
|
4017
|
+
} else if (percentage > 0) {
|
|
4018
|
+
console.log(' Status: \x1b[33mIN_PROGRESS (Run "onboard apply --approved" to initialize remaining files)\x1b[0m\n');
|
|
4019
|
+
} else {
|
|
4020
|
+
console.log(' Status: \x1b[31mMISSING (Run "onboard plan" and "onboard apply" to onboard this repo)\x1b[0m\n');
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
function getEnabledAdapters(target) {
|
|
4025
|
+
const configPath = join(target, '.ai', 'config.yaml');
|
|
4026
|
+
if (existsSync(configPath)) {
|
|
4027
|
+
try {
|
|
4028
|
+
const config = parseYaml(readFileSync(configPath, 'utf8')) || {};
|
|
4029
|
+
return config.adapters || {};
|
|
4030
|
+
} catch (e) {}
|
|
4031
|
+
}
|
|
4032
|
+
return {};
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
function handleAdapterStatus(options) {
|
|
4036
|
+
console.log(`\n🔌 \x1b[36mIDE & Agent Adapters Status: ${options.target}\x1b[0m`);
|
|
4037
|
+
console.log('==================================================');
|
|
4038
|
+
|
|
4039
|
+
const enabled = getEnabledAdapters(options.target);
|
|
4040
|
+
|
|
4041
|
+
Object.keys(ADAPTERS).forEach(name => {
|
|
4042
|
+
const a = ADAPTERS[name];
|
|
4043
|
+
const isEnabled = enabled[name] || false;
|
|
4044
|
+
const rulesFile = a.rules_file;
|
|
4045
|
+
const exists = existsSync(join(options.target, rulesFile));
|
|
4046
|
+
|
|
4047
|
+
let statusStr = '\x1b[31mMISSING\x1b[0m';
|
|
4048
|
+
if (exists) {
|
|
4049
|
+
statusStr = '\x1b[32mINSTALLED\x1b[0m';
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
console.log(`\n\x1b[33m* ${a.name || name}\x1b[0m (${name})`);
|
|
4053
|
+
console.log(` Config Status: ${isEnabled ? '\x1b[32mENABLED\x1b[0m' : '\x1b[37mDISABLED\x1b[0m'}`);
|
|
4054
|
+
console.log(` File Status: ${statusStr} (${rulesFile})`);
|
|
4055
|
+
});
|
|
4056
|
+
console.log();
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
function printDiff(srcContent, destContent, filename) {
|
|
4060
|
+
console.log(`\nDiff for ${filename}:`);
|
|
4061
|
+
console.log('--------------------------------------------------');
|
|
4062
|
+
if (srcContent === destContent) {
|
|
4063
|
+
console.log(' Pristine (No differences detected)');
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
const srcLines = srcContent.split(/\r?\n/);
|
|
4067
|
+
const destLines = destContent.split(/\r?\n/);
|
|
4068
|
+
|
|
4069
|
+
let i = 0;
|
|
4070
|
+
while (i < Math.max(srcLines.length, destLines.length)) {
|
|
4071
|
+
const sLine = srcLines[i];
|
|
4072
|
+
const dLine = destLines[i];
|
|
4073
|
+
if (sLine !== dLine) {
|
|
4074
|
+
if (dLine !== undefined) console.log(`\x1b[31m- ${dLine}\x1b[0m`);
|
|
4075
|
+
if (sLine !== undefined) console.log(`\x1b[32m+ ${sLine}\x1b[0m`);
|
|
4076
|
+
} else {
|
|
4077
|
+
if (sLine !== undefined) console.log(` ${sLine}`);
|
|
4078
|
+
}
|
|
4079
|
+
i++;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
function handleAdapterDiff(aName, options) {
|
|
4084
|
+
const adaptersToDiff = [];
|
|
4085
|
+
if (aName === 'all') {
|
|
4086
|
+
const enabled = getEnabledAdapters(options.target);
|
|
4087
|
+
Object.keys(ADAPTERS).forEach(name => {
|
|
4088
|
+
if (enabled[name]) adaptersToDiff.push(name);
|
|
4089
|
+
});
|
|
4090
|
+
} else {
|
|
4091
|
+
if (!ADAPTERS[aName]) {
|
|
4092
|
+
console.error(`\x1b[31mError: Adapter '${aName}' not found in registry.\x1b[0m`);
|
|
4093
|
+
process.exit(1);
|
|
4094
|
+
}
|
|
4095
|
+
adaptersToDiff.push(aName);
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
if (adaptersToDiff.length === 0) {
|
|
4099
|
+
console.log('No enabled adapters found to diff.');
|
|
4100
|
+
return;
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
adaptersToDiff.forEach(name => {
|
|
4104
|
+
const a = ADAPTERS[name];
|
|
4105
|
+
const srcFile = join(sourceRoot, 'adapters', name, a.rules_file);
|
|
4106
|
+
const destFile = join(options.target, a.rules_file);
|
|
4107
|
+
|
|
4108
|
+
if (!existsSync(srcFile)) {
|
|
4109
|
+
console.warn(`Warning: Source file for adapter '${name}' is missing at: ${srcFile}`);
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
const srcContent = readFileSync(srcFile, 'utf8');
|
|
4114
|
+
if (existsSync(destFile)) {
|
|
4115
|
+
const destContent = readFileSync(destFile, 'utf8');
|
|
4116
|
+
printDiff(srcContent, destContent, a.rules_file);
|
|
4117
|
+
} else {
|
|
4118
|
+
console.log(`\nFile: ${a.rules_file} \x1b[31m(MISSING)\x1b[0m`);
|
|
4119
|
+
console.log('--------------------------------------------------');
|
|
4120
|
+
srcContent.split(/\r?\n/).forEach(l => console.log(`\x1b[32m+ ${l}\x1b[0m`));
|
|
4121
|
+
}
|
|
4122
|
+
});
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
function handleAdapterSync(aName, options) {
|
|
4126
|
+
if (!options.approved) {
|
|
4127
|
+
console.error('\x1b[31mError: Adapter sync requires explicit approval flag: --approved\x1b[0m');
|
|
4128
|
+
console.log('Example: node bin/multimodel-dev-os.js adapter sync cursor --approved');
|
|
4129
|
+
process.exit(1);
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
const adaptersToSync = [];
|
|
4133
|
+
if (aName === 'all') {
|
|
4134
|
+
const enabled = getEnabledAdapters(options.target);
|
|
4135
|
+
Object.keys(ADAPTERS).forEach(name => {
|
|
4136
|
+
if (enabled[name]) adaptersToSync.push(name);
|
|
4137
|
+
});
|
|
4138
|
+
} else {
|
|
4139
|
+
if (!ADAPTERS[aName]) {
|
|
4140
|
+
console.error(`\x1b[31mError: Adapter '${aName}' not found in registry.\x1b[0m`);
|
|
4141
|
+
process.exit(1);
|
|
4142
|
+
}
|
|
4143
|
+
adaptersToSync.push(aName);
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
if (adaptersToSync.length === 0) {
|
|
4147
|
+
console.log('No adapters found to sync.');
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
console.log(`\n🔄 \x1b[36mSynchronizing IDE Adapters in: ${options.target}\x1b[0m`);
|
|
4152
|
+
console.log('==================================================');
|
|
4153
|
+
|
|
4154
|
+
adaptersToSync.forEach(name => {
|
|
4155
|
+
const a = ADAPTERS[name];
|
|
4156
|
+
const srcFile = join(sourceRoot, 'adapters', name, a.rules_file);
|
|
4157
|
+
const destFile = join(options.target, a.rules_file);
|
|
4158
|
+
const destDir = dirname(destFile);
|
|
4159
|
+
|
|
4160
|
+
if (!existsSync(srcFile)) {
|
|
4161
|
+
console.warn(`Warning: Source file for adapter '${name}' is missing at: ${srcFile}`);
|
|
4162
|
+
return;
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
if (existsSync(destFile)) {
|
|
4166
|
+
if (options.force) {
|
|
4167
|
+
if (!options.dryRun) {
|
|
4168
|
+
const backupPath = destFile + '.bak';
|
|
4169
|
+
writeFileSync(backupPath, readFileSync(destFile));
|
|
4170
|
+
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
4171
|
+
writeFileSync(destFile, readFileSync(srcFile));
|
|
4172
|
+
console.log(` \x1b[33mOVERWRITE (BACKUP CREATED):\x1b[0m ${a.rules_file} -> ${a.rules_file}.bak`);
|
|
4173
|
+
} else {
|
|
4174
|
+
console.log(` \x1b[36m[DRY-RUN] WOULD OVERWRITE & BACKUP:\x1b[0m ${a.rules_file}`);
|
|
4175
|
+
}
|
|
4176
|
+
} else {
|
|
4177
|
+
console.log(` \x1b[37m[SKIP] Already exists:\x1b[0m ${a.rules_file}`);
|
|
4178
|
+
}
|
|
4179
|
+
} else {
|
|
4180
|
+
if (!options.dryRun) {
|
|
4181
|
+
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
4182
|
+
writeFileSync(destFile, readFileSync(srcFile));
|
|
4183
|
+
console.log(` \x1b[32mCREATE:\x1b[0m ${a.rules_file}`);
|
|
4184
|
+
} else {
|
|
4185
|
+
console.log(` \x1b[36m[DRY-RUN] WOULD CREATE:\x1b[0m ${a.rules_file}`);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
});
|
|
4189
|
+
|
|
4190
|
+
console.log();
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
function handleDoctorOnboarding(options) {
|
|
4194
|
+
console.log(`\n🩺 \x1b[36mRunning advisory onboarding doctor checkup in: ${options.target}\x1b[0m\n`);
|
|
4195
|
+
|
|
4196
|
+
let warnings = 0;
|
|
4197
|
+
const warn = (msg) => {
|
|
4198
|
+
console.warn(` \x1b[33m[WARNING]\x1b[0m ${msg}`);
|
|
4199
|
+
warnings++;
|
|
4200
|
+
};
|
|
4201
|
+
|
|
4202
|
+
const crucialFiles = [
|
|
4203
|
+
'AGENTS.md',
|
|
4204
|
+
'MEMORY.md',
|
|
4205
|
+
'TASKS.md',
|
|
4206
|
+
'RUNBOOK.md'
|
|
4207
|
+
];
|
|
4208
|
+
|
|
4209
|
+
crucialFiles.forEach(f => {
|
|
4210
|
+
if (!existsSync(join(options.target, f))) {
|
|
4211
|
+
warn(`Crucial onboarding file '${f}' is missing from project root.`);
|
|
4212
|
+
}
|
|
4213
|
+
});
|
|
4214
|
+
|
|
4215
|
+
const configPath = join(options.target, '.ai', 'config.yaml');
|
|
4216
|
+
if (!existsSync(configPath)) {
|
|
4217
|
+
warn('MultiModel Dev OS configuration file (.ai/config.yaml) is missing.');
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4220
|
+
const registriesDir = join(options.target, '.ai', 'registries');
|
|
4221
|
+
if (!existsSync(registriesDir)) {
|
|
4222
|
+
warn('Registries directory (.ai/registries) is missing.');
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
const proposalsDir = join(options.target, '.ai', 'proposals');
|
|
4226
|
+
if (!existsSync(proposalsDir)) {
|
|
4227
|
+
warn('Proposals directory (.ai/proposals) is missing.');
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
const intelligenceDir = join(options.target, '.ai', 'intelligence');
|
|
4231
|
+
if (!existsSync(intelligenceDir)) {
|
|
4232
|
+
warn('Intelligence directory (.ai/intelligence) is missing.');
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
const gitignorePath = join(options.target, '.gitignore');
|
|
4236
|
+
if (existsSync(gitignorePath)) {
|
|
4237
|
+
const gitignoreContent = readFileSync(gitignorePath, 'utf8');
|
|
4238
|
+
const checkIgnore = (pattern) => {
|
|
4239
|
+
if (!gitignoreContent.includes(pattern)) {
|
|
4240
|
+
warn(`Generated runtime file '${pattern}' is not ignored in .gitignore.`);
|
|
4241
|
+
}
|
|
4242
|
+
};
|
|
4243
|
+
checkIgnore('onboarding.plan.json');
|
|
4244
|
+
checkIgnore('onboarding.report.md');
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
const { files } = scanTarget(options.target);
|
|
4248
|
+
const packageManagers = detectDependencySignals(files, options.target);
|
|
4249
|
+
if (packageManagers.length === 0) {
|
|
4250
|
+
warn('No package manager lockfile detected in project root.');
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
console.log('\n==================================================');
|
|
4254
|
+
if (warnings > 0) {
|
|
4255
|
+
console.log(`\x1b[33mDoctor onboarding check complete. Found ${warnings} warnings.\x1b[0m\n`);
|
|
4256
|
+
} else {
|
|
4257
|
+
console.log('\x1b[32m✔ Doctor onboarding check complete. Your workspace onboarding setup is pristine!\x1b[0m\n');
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
|