mancode 0.3.1 → 0.3.3

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/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { program } from "commander";
9
9
  // src/commands/init.ts
10
10
  import { promises as fs2 } from "fs";
11
11
  import path13 from "path";
12
- import process3 from "process";
12
+ import process4 from "process";
13
13
 
14
14
  // src/installers/claude-code.ts
15
15
  import {
@@ -295,14 +295,15 @@ var HEAD_COACH_AGENT = {
295
295
  - \u5E94\u7528 YAGNI \u539F\u5219\uFF1A\u5DF2\u5B58\u5728 \u2192 \u590D\u7528 \u2192 \u6807\u51C6\u5E93 \u2192 \u5DF2\u88C5\u4F9D\u8D56 \u2192 \u4E00\u884C \u2192 \u6700\u5C0F\u5B9E\u73B0
296
296
 
297
297
  **\u4FEE\u590D\u9636\u6BB5**\uFF08Film Analyst \u53CD\u9988\u540E\uFF09\uFF1A
298
- - \u53EA\u4FEE\u590D\u53CD\u9988\u6307\u51FA\u7684\u95EE\u9898
298
+ - \u6C47\u603B\u6240\u9700\u5BA1\u67E5\u9886\u57DF\u7684 blocker\uFF0C\u53EA\u505A\u4E00\u8F6E\u4FEE\u590D\u5E76\u901A\u8FC7 review ledger \u8BB0\u5F55
299
+ - \u53EA\u4FEE\u590D\u53CD\u9988\u6307\u51FA\u7684 blocker\uFF1B\u{1F7E1}/\u{1F7E2} \u53EA\u8BB0\u5F55\u6743\u8861\uFF0C\u4E0D\u81EA\u52A8\u6269\u5927 diff
299
300
  - **\u4E0D\u5728\u539F\u4EE3\u7801\u4E0A\u8FC7\u5EA6\u4FEE\u590D**
300
301
  - \u4F18\u5148\u7EA7\uFF1A\u{1F534} \u5FC5\u4FEE > \u{1F7E1} \u5EFA\u8BAE > \u{1F7E2} \u53EF\u9009
301
302
 
302
303
  **\u6536\u5C3E\u9636\u6BB5**\uFF1A
303
- - \u4FEE\u590D Film #2 \u7684\u5168\u90E8 \u{1F534} \u95EE\u9898\uFF0C\u518D\u91CD\u8DD1 build/lint/typecheck/test \u548C\u5FC5\u8981 smoke test\u3002
304
- - \u751F\u6210 summary\uFF1A\u6539\u52A8/\u65B0\u5EFA\u6587\u4EF6\u3001\u590D\u7528\u8D44\u6E90\u3001\u9A8C\u8BC1\u7ED3\u679C\u3001\u53CC\u5BA1\u95EE\u9898\u5904\u7F6E\u3001\u8DF3\u8FC7\u6B65\u9AA4\u548C\u6B8B\u4F59\u98CE\u9669\u3002
305
- - \u53EA\u6709\u9A8C\u8BC1\u901A\u8FC7\u4E14 \u{1F534} \u6E05\u96F6\u624D\u5EFA\u8BAE \`completed\`\uFF1B\u5426\u5219\u5199 \`blocked\` \u4E0E\u660E\u786E blockingReason\u3002
304
+ - \u4E00\u8F6E\u4FEE\u590D\u540E\u91CD\u8DD1\u53D7\u5F71\u54CD\u7684 build/lint/typecheck/test \u548C\u5FC5\u8981 smoke test\uFF0C\u4E0D\u91CD\u590D\u8FD0\u884C\u5DF2\u5B8C\u6210 reviewer\u3002
305
+ - \u751F\u6210 summary\uFF1A\u6539\u52A8/\u65B0\u5EFA\u6587\u4EF6\u3001\u590D\u7528\u8D44\u6E90\u3001\u9A8C\u8BC1\u7ED3\u679C\u3001\u5BA1\u67E5\u6DF1\u5EA6\u3001\u95EE\u9898\u5904\u7F6E\u3001\u8DF3\u8FC7\u6B65\u9AA4\u548C\u6B8B\u4F59\u98CE\u9669\u3002
306
+ - \u53EA\u6709\u9A8C\u8BC1\u901A\u8FC7\u3001\u6240\u9700\u5BA1\u67E5\u9886\u57DF\u5B8C\u6210\u4E14 blocker \u6E05\u96F6\u624D\u5EFA\u8BAE \`completed\`\uFF1B\u5426\u5219\u5199 \`blocked\` \u4E0E\u660E\u786E blockingReason\u3002
306
307
  - \u5C06\u5173\u952E\u51B3\u7B56\u4EA4\u7ED9\u8C03\u7528\u65B9 appendTeamDecision\uFF0C\u5E76\u66F4\u65B0 Active Plans\u3002
307
308
  - \u5408\u5E76 worktree \u524D\u53D6\u5F97\u7528\u6237\u786E\u8BA4\uFF1B\u6E05\u7406\u4E34\u65F6\u6587\u4EF6\u3002
308
309
 
@@ -331,6 +332,8 @@ var FILM_ANALYST_OFFENSE_AGENT = {
331
332
  - \u{1F7E1} \u5EFA\u8BAE\uFF08\u5E94\u8BE5\u4FEE\uFF09
332
333
  - \u{1F7E2} \u53EF\u9009\uFF08\u53EF\u4EE5\u4E0D\u4FEE\uFF09
333
334
  - \u6700\u540E\u7ED9\u8BC4\u5206\uFF081-10\uFF09+ \u901A\u8FC7 / \u4E0D\u901A\u8FC7
335
+ - \u53EA\u5BA1\u67E5\u672C\u6B21 diff\u3001\u7528\u6237\u9700\u6C42\u548C\u76F4\u63A5\u53D7\u5F71\u54CD\u8DEF\u5F84\uFF1B\u6700\u591A 3 \u4E2A\u65B0\u95EE\u9898
336
+ - \u6BCF\u4E2A\u95EE\u9898\u5FC5\u987B\u5305\u542B\u6539\u52A8\u884C\u3001\u53EF\u6838\u67E5\u8BC1\u636E\u548C\u7528\u6237\u5F71\u54CD\u3002\u6CA1\u6709\u8BC1\u636E\u7684 checklist \u9879\u4E0D\u8F93\u51FA
334
337
 
335
338
  ## \u4F60\u4E0D\u5199\u4EE3\u7801
336
339
 
@@ -388,6 +391,8 @@ var FILM_ANALYST_OFFENSE_AGENT = {
388
391
  - \u4E0D\u8981\u5EFA\u8BAE**\u4E0E\u9879\u76EE\u98CE\u683C\u4E0D\u7B26**\u7684\u6539\u52A8
389
392
  - \u4E0D\u8981\u5EFA\u8BAE**\u63A8\u6D4B\u6027\u529F\u80FD**\u6216**\u8FC7\u5EA6\u62BD\u8C61**
390
393
  - \u4E0D\u8981"\u987A\u4FBF\u5EFA\u8BAE"\u548C\u5F53\u524D\u4EFB\u52A1\u65E0\u5173\u7684\u4E8B
394
+ - \u4E0D\u8981\u56E0\u4E3A\u7F3A\u5C11\u901A\u7528 loading\u3001\u9519\u8BEF\u683C\u5F0F\u6216\u56DE\u5F52\u6D4B\u8BD5\u800C\u673A\u68B0\u8BC4\u8BBA\uFF1B\u53EA\u6709\u672C\u6B21\u884C\u4E3A\u786E\u5B9E\u5F15\u5165\u76F8\u5E94\u8DEF\u5F84\u4E14\u6709\u8BC1\u636E\u65F6\u624D\u62A5\u544A
395
+ - \u540C\u4E00\u6839\u56E0\u53EA\u62A5\u4E00\u6B21\uFF1B\u{1F534} \u4F7F\u7528\u7A33\u5B9A ID\uFF08Q1\u3001Q2\u2026\u2026\uFF09\uFF0C\u4F9B review ledger \u53BB\u91CD\u548C\u6536\u5C3E
391
396
 
392
397
  \u6536\u5230\u4EE3\u7801\u540E\u7ACB\u5373\u5F00\u59CB\u5BA1\u67E5\u3002\u62A5\u544A\u5B8C\u6574\u5373\u53EF\u3002`
393
398
  };
@@ -411,6 +416,8 @@ var FILM_ANALYST_DEFENSE_AGENT = {
411
416
  - \u{1F7E2} \u53EF\u9009\uFF08\u52A0\u56FA\u5EFA\u8BAE\uFF09
412
417
  - **\u5B89\u5168\u95EE\u9898\u4E00\u5F8B \u{1F534}**
413
418
  - \u6700\u540E\u7ED9\u8BC4\u5206\uFF081-10\uFF09+ \u901A\u8FC7 / \u4E0D\u901A\u8FC7
419
+ - \u53EA\u5BA1\u67E5\u672C\u6B21 diff\u3001\u7528\u6237\u9700\u6C42\u548C\u76F4\u63A5\u53D7\u5F71\u54CD\u8DEF\u5F84\uFF1B\u6700\u591A 3 \u4E2A\u65B0\u95EE\u9898
420
+ - \u6BCF\u4E2A\u95EE\u9898\u5FC5\u987B\u5305\u542B\u6539\u52A8\u884C\u3001\u53EF\u6838\u67E5\u8BC1\u636E\u548C\u7528\u6237\u5F71\u54CD
414
421
 
415
422
  ## \u4F60\u4E0D\u5199\u4EE3\u7801
416
423
 
@@ -462,8 +469,7 @@ var FILM_ANALYST_DEFENSE_AGENT = {
462
469
 
463
470
  ## \u4E0D\u8981\u91CD\u590D Film Analyst #1 \u7684\u53CD\u9988
464
471
 
465
- \u5982\u679C #1 \u5DF2\u7ECF\u6307\u51FA\u7684\u53EF\u8BFB\u6027 / DRY / \u590D\u6742\u5EA6\u95EE\u9898\uFF0C**\u4E0D\u8981\u518D\u8BF4**\u3002
466
- \u4F60\u4E13\u6CE8\u8FB9\u754C\u3001\u5B89\u5168\u3001\u6027\u80FD\u3001\u8D44\u6E90\u3002
472
+ \u5F00\u59CB\u524D\u5FC5\u987B\u8BFB\u53D6 \`review-scope.md\` \u548C \`film-report-1.md\`\u3002\u5982\u679C #1 \u5DF2\u7ECF\u6307\u51FA\u76F8\u540C\u6839\u56E0\uFF0C\u6807\u8BB0\u4E3A \`duplicate\` \u5E76\u4E14\u4E0D\u8981\u91CD\u65B0\u8F93\u51FA\u4E3A finding\u3002\u4F60\u53EA\u4E13\u6CE8\u8FB9\u754C\u3001\u5B89\u5168\u3001\u6027\u80FD\u3001\u8D44\u6E90\uFF1B\u{1F534} \u4F7F\u7528\u7A33\u5B9A ID\uFF08D1\u3001D2\u2026\u2026\uFF09\u3002
467
473
 
468
474
  ## \u7EA2\u7EBF
469
475
 
@@ -504,215 +510,229 @@ function yamlString(s) {
504
510
  }
505
511
 
506
512
  // src/templates/inline.ts
507
- var SESSION_START_HOOK = `#!/bin/bash
508
- # .mancode/hooks/session-start.sh
509
- # mancode SessionStart hook - \u52A0\u8F7D\u9879\u76EE\u4E0A\u4E0B\u6587
510
- # \u7CFB\u7EDF\u4F9D\u8D56\uFF1Abash\u3001git\u3001\uFF08\u53EF\u9009\uFF09jq
511
- set -uo pipefail
512
-
513
- PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
514
- STATE_FILE="$PROJECT_ROOT/.mancode/state.json"
515
- PROFILE_FILE="$PROJECT_ROOT/.mancode/project-profile.json"
516
-
517
- HAS_JQ=0
518
- if [ "\${MANCODE_DISABLE_JQ:-0}" != "1" ]; then
519
- command -v jq >/dev/null 2>&1 && HAS_JQ=1
520
- fi
521
-
522
- json_get() {
523
- local key="$1"
524
- local file="$2"
525
- if [ "$HAS_JQ" = "1" ]; then
526
- jq -r ".$key // empty" "$file" 2>/dev/null || true
527
- else
528
- grep "\\"$key\\"" "$file" 2>/dev/null | sed -E 's/.*: *"([^"]*)".*/\\1/' || true
529
- fi
530
- }
531
-
532
- json_any_get() {
533
- local key="$1"
534
- local file="$2"
535
- if [ "$HAS_JQ" = "1" ]; then
536
- jq -r ".$key // empty" "$file" 2>/dev/null || true
537
- else
538
- grep "\\"$key\\"" "$file" 2>/dev/null | head -n 1 | sed -E 's/.*: *"?([^",}]*)"?[,]?.*/\\1/' || true
539
- fi
540
- }
541
-
542
- if [ ! -f "$STATE_FILE" ]; then
543
- echo "\u2139\uFE0F mancode \u672A\u521D\u59CB\u5316\u3002\u8FD0\u884C \\\`mancode init\\\` \u5F00\u59CB\u3002"
544
- exit 0
545
- fi
546
-
547
- # \u6E05\u6D17\u51FD\u6570\uFF1A\u53BB\u6362\u884C\u3001\u9650\u5236\u957F\u5EA6\uFF08\u9632\u6B62\u810F\u6570\u636E\u6C61\u67D3 prompt\uFF09
548
- sanitize() {
549
- printf '%s' "$1" | tr '\\n\\r' ' ' | head -c 200
550
- }
551
-
552
- MODE=$(json_get "currentMode" "$STATE_FILE")
553
- TECH_STACK=$(json_get "techStack" "$STATE_FILE")
554
- UI_LIBRARY=$(json_get "uiLibrary" "$STATE_FILE")
555
- TEAM_AUTO=$(json_any_get "teamModeAutoDetected" "$STATE_FILE")
556
- CONTRIBUTORS=$(json_any_get "contributors" "$STATE_FILE")
557
-
558
- echo "mancode_mode: \${MODE:-solo}"
559
- echo "project_type: $(sanitize "$TECH_STACK")"
560
- echo "ui_library: $(sanitize "$UI_LIBRARY")"
561
- echo ""
562
-
563
- echo "## mancode \xB7 \${MODE:-solo} mode"
564
- echo ""
565
- echo "\u4F60\u6B63\u5728\u4F7F\u7528 mancode \${MODE:-solo} \u6A21\u5F0F\u3002"
566
- echo ""
567
- echo "### \u6838\u5FC3\u539F\u5219"
568
- echo "1. **\u4F18\u5148\u590D\u7528\u9879\u76EE\u5DF2\u6709\u4EE3\u7801**"
569
- echo " - \u68C0\u67E5\u5DF2\u68C0\u6D4B\u5230\u7684\u6E90\u7801\u76EE\u5F55\u548C\u5DF2\u6709\u7C7B\u4F3C\u5B9E\u73B0"
570
- echo " - \u590D\u7528\u73B0\u6709\u7EC4\u4EF6\u3001\u51FD\u6570\u3001\u6837\u5F0F"
571
- echo ""
572
- if [ "$(json_get "uiAssets" "$PROFILE_FILE")" = "detected" ]; then
573
- echo "2. **\u5E94\u7528\u9879\u76EE\u5BA1\u7F8E token**\uFF08\u4EC5\u5728\u9879\u76EE profile \u786E\u8BA4\u6709 UI \u8D44\u4EA7\u4E14\u4EFB\u52A1\u6D89\u53CA UI \u65F6\uFF09"
574
- echo " - UI library: $(sanitize "$UI_LIBRARY")"
575
- echo " - \u4F7F\u7528\u9879\u76EE\u5DF2\u6709\u7684\u8BBE\u8BA1 token"
576
- else
577
- echo "2. **\u6309\u9879\u76EE\u80FD\u529B\u5DE5\u4F5C**"
578
- echo " - \u4E0D\u5047\u5B9A\u5B58\u5728 UI\u3001\u6D4F\u89C8\u5668\u6216\u7279\u5B9A\u6280\u672F\u6808"
579
- echo " - \u5148\u8BFB\u53D6 project-profile \u4E0E\u9879\u76EE\u73B0\u6709\u9A8C\u8BC1\u65B9\u5F0F"
580
- fi
581
- echo ""
582
- echo "3. **\u6700\u5C0F\u6539\u52A8**"
583
- echo " - \u53EA\u6539\u7528\u6237\u8981\u6C42\u7684\u90E8\u5206"
584
- echo " - \u4E0D\u91CD\u6784\u65E0\u5173\u4EE3\u7801"
585
-
586
- if [ "$TEAM_AUTO" = "true" ] && [ "\${MODE:-solo}" = "solo" ]; then
587
- echo ""
588
- echo "### \u56E2\u961F\u534F\u4F5C\u63D0\u9192"
589
- echo "\u68C0\u6D4B\u5230\u56E2\u961F\u9879\u76EE\uFF08contributors: \${CONTRIBUTORS:-2}\uFF09\u3002"
590
- echo '- \u6D89\u53CA\u591A\u4EBA\u534F\u4F5C\u3001\u4EA4\u63A5\u3001PR\u3001\u5171\u4EAB\u6A21\u5757\u65F6\uFF0C\u4F18\u5148\u4F7F\u7528 /manteam <task>\u3002'
591
- echo '- \u53EA\u505A\u4E2A\u4EBA\u5C0F\u6539\u52A8\u65F6\uFF0C\u53EF\u4EE5\u7EE7\u7EED solo\uFF1B\u9700\u8981\u9000\u51FA\u6D41\u7A0B\u7528 /mansolo\u3002'
592
- fi
513
+ var SESSION_START_HOOK = String.raw`#!/usr/bin/env node
514
+ // .mancode/hooks/session-start.mjs
515
+ // mancode SessionStart hook - cross-platform project context
516
+ import { readFileSync } from 'node:fs';
517
+ import path from 'node:path';
518
+ import { fileURLToPath } from 'node:url';
519
+
520
+ const projectRoot = path.resolve(
521
+ path.dirname(fileURLToPath(import.meta.url)),
522
+ '../..',
523
+ );
524
+ const state = readJson(path.join(projectRoot, '.mancode', 'state.json'));
525
+ const profile =
526
+ readJson(path.join(projectRoot, '.mancode', 'project-profile.json')) || {};
527
+
528
+ if (!state) {
529
+ console.log('ℹ️ mancode 未初始化。运行 mancode init 开始。');
530
+ process.exit(0);
531
+ }
532
+
533
+ const mode = text(state.currentMode) || 'solo';
534
+ const techStack = sanitize(state.techStack);
535
+ const uiLibrary = sanitize(state.uiLibrary);
536
+ const output = [
537
+ 'mancode_mode: ' + mode,
538
+ 'project_type: ' + techStack,
539
+ 'ui_library: ' + uiLibrary,
540
+ '',
541
+ '## mancode · ' + mode + ' mode',
542
+ '',
543
+ '你正在使用 mancode ' + mode + ' 模式。',
544
+ '',
545
+ '### 核心原则',
546
+ '1. **优先复用项目已有代码**',
547
+ ' - 检查已检测到的源码目录和已有类似实现',
548
+ ' - 复用现有组件、函数、样式',
549
+ '',
550
+ ];
551
+
552
+ if (profile.uiAssets === 'detected') {
553
+ output.push(
554
+ '2. **应用项目审美 token**(仅在项目 profile 确认有 UI 资产且任务涉及 UI 时)',
555
+ ' - UI library: ' + uiLibrary,
556
+ ' - 使用项目已有的设计 token',
557
+ );
558
+ } else {
559
+ output.push(
560
+ '2. **按项目能力工作**',
561
+ ' - 不假定存在 UI、浏览器或特定技术栈',
562
+ ' - 先读取 project-profile 与项目现有验证方式',
563
+ );
564
+ }
565
+
566
+ output.push(
567
+ '',
568
+ '3. **最小改动**',
569
+ ' - 只改用户要求的部分',
570
+ ' - 不重构无关代码',
571
+ );
572
+
573
+ if (state.teamModeAutoDetected === true && mode === 'solo') {
574
+ output.push(
575
+ '',
576
+ '### 团队协作提醒',
577
+ '检测到团队项目(contributors: ' +
578
+ (Number.isFinite(state.contributors) ? state.contributors : 2) +
579
+ ')。',
580
+ '- 涉及多人协作、交接、PR、共享模块时,优先使用 /manteam <task>。',
581
+ '- 只做个人小改动时,可以继续 solo;需要退出流程用 /mansolo。',
582
+ );
583
+ }
584
+
585
+ console.log(output.join('\n'));
586
+
587
+ function readJson(filePath) {
588
+ try {
589
+ return JSON.parse(readFileSync(filePath, 'utf8'));
590
+ } catch {
591
+ return null;
592
+ }
593
+ }
594
+
595
+ function text(value) {
596
+ return typeof value === 'string' ? value : '';
597
+ }
598
+
599
+ function sanitize(value) {
600
+ return String(value ?? '').replace(/[\r\n]/g, ' ').slice(0, 200);
601
+ }
593
602
  `;
594
- var USER_PROMPT_SUBMIT_HOOK = `#!/bin/bash
595
- # .mancode/hooks/user-prompt-submit.sh
596
- # mancode UserPromptSubmit hook - \u6CE8\u5165 6 \u95EE\u8FFD\u95EE + \u5BA1\u7F8E token
597
- set -uo pipefail
598
-
599
- PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
600
- STATE_FILE="$PROJECT_ROOT/.mancode/state.json"
601
- AESTHETICS_FILE="$PROJECT_ROOT/.mancode/aesthetics/style-tokens.json"
602
-
603
- HAS_JQ=0
604
- if [ "\${MANCODE_DISABLE_JQ:-0}" != "1" ]; then
605
- command -v jq >/dev/null 2>&1 && HAS_JQ=1
606
- fi
607
-
608
- json_get() {
609
- local key="$1"
610
- local file="$2"
611
- if [ "$HAS_JQ" = "1" ]; then
612
- jq -r ".$key // empty" "$file" 2>/dev/null || true
613
- else
614
- grep "\\"$key\\"" "$file" 2>/dev/null | sed -E 's/.*: *"([^"]*)".*/\\1/' || true
615
- fi
616
- }
617
-
618
- # \u6E05\u6D17\u6CE8\u5165\u5230 prompt \u7684\u52A8\u6001\u503C\uFF1A\u53BB\u6362\u884C\u3001\u9650\u5236\u957F\u5EA6\uFF0C\u907F\u514D\u810F token \u6C61\u67D3\u4E0A\u4E0B\u6587\u7ED3\u6784
619
- sanitize() {
620
- printf '%s' "$1" | tr '\\n\\r' ' ' | head -c 200
621
- }
622
-
623
- MODE=$(json_get "currentMode" "$STATE_FILE")
624
-
625
- # \u53EA\u5728 solo \u6A21\u5F0F\u4E0B\u8F93\u51FA 6 \u95EE
626
- if [ "$MODE" = "solo" ]; then
627
- echo "## \u52A8\u624B\u524D\uFF0C\u5148\u60F3\u516D\u4E2A\u95EE\u9898\uFF1A"
628
- echo ""
629
- echo "1. **\u4E3A\u4EC0\u4E48\u505A\uFF1F**"
630
- echo " - \u8FD9\u4E2A\u6539\u52A8\u89E3\u51B3\u4EC0\u4E48\u95EE\u9898\uFF1F"
631
- echo ""
632
- echo "2. **\u5DF2\u7ECF\u6709\u4EC0\u4E48\uFF1F**"
633
- echo " - \u9879\u76EE\u91CC\u6709\u6CA1\u6709\u7C7B\u4F3C\u7684\u5B9E\u73B0\u53EF\u4EE5\u590D\u7528\uFF1F"
634
- echo ""
635
- echo "3. **\u6700\u5C11\u6539\u591A\u5C11\uFF1F**"
636
- echo " - \u80FD\u7528\u4E00\u884C\u89E3\u51B3\u5417\uFF1F\u80FD\u590D\u7528\u73B0\u6709\u4EE3\u7801\u5417\uFF1F"
637
- echo ""
638
- echo "4. **\u80FD\u4E0D\u80FD\u4E0D\u62C6\u65B0\u7CFB\u7EDF\uFF1F**"
639
- echo " - \u4E0D\u65B0\u5EFA\u6587\u4EF6\u6216\u6A21\u5757\u80FD\u5B8C\u6210\u5417\uFF1F"
640
- echo ""
641
- echo "5. **\u975E\u5E73\u51E1\u903B\u8F91\u600E\u6837\u6700\u5C0F\u8FD0\u884C\u9A8C\u8BC1\uFF1F**"
642
- echo ""
643
- echo "6. **\u6709\u4EC0\u4E48\u6CA1\u628A\u63E1\u7684\uFF1F**"
644
- echo " - \u5148\u81EA\u884C\u67E5\u4EE3\u7801\u6216\u6587\u6863\uFF0C\u6700\u591A 2 \u6B21\u5DE5\u5177\u8C03\u7528\uFF1B\u4ECD\u4E0D\u786E\u5B9A\u518D\u95EE\u7528\u6237\u3002"
645
- echo ""
646
- fi
647
-
648
- # Claude Code \u901A\u8FC7 stdin \u4F20\u5165 JSON: {"prompt": "...", ...}
649
- # \u5148\u8BFB\u53D6\u5B8C\u6574\u8F93\u5165\uFF0C\u518D\u89E3\u6790
650
- INPUT=$(cat)
651
-
652
- if [ "$HAS_JQ" = "1" ]; then
653
- USER_PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""' 2>/dev/null)
654
- # jq \u5931\u8D25\u6216\u8FD4\u56DE\u7A7A\uFF0Cfallback \u5230\u539F\u59CB\u8F93\u5165
655
- if [ -z "$USER_PROMPT" ]; then
656
- USER_PROMPT="$INPUT"
657
- fi
658
- else
659
- # \u65E0 jq: \u4F7F\u7528 sed \u63D0\u53D6 JSON \u4E2D\u7684 prompt \u5B57\u6BB5
660
- USER_PROMPT=$(echo "$INPUT" | sed -n 's/.*"prompt"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
661
- # sed \u5931\u8D25\uFF0Cfallback \u5230\u539F\u59CB\u8F93\u5165
662
- if [ -z "$USER_PROMPT" ]; then
663
- USER_PROMPT="$INPUT"
664
- fi
665
- fi
666
-
667
- if [ "$MODE" = "solo" ] && echo "$USER_PROMPT" | grep -qiE "\u5148(\u522B|\u4E0D\u8981|\u770B|\u770B\u770B|\u8C03\u7814|\u5206\u6790|\u8BC4\u4F30)|\u7ED9.*\u65B9\u6848|\u7ED9.*\u8BA1\u5212|\u600E\u4E48.*\u505A|\u5982\u4F55.*\u505A|\u600E\u4E48.*\u5B9E\u73B0|\u5982\u4F55.*\u5B9E\u73B0|\u5E94\u8BE5\u600E\u4E48|\u600E\u4E48.*\u62C6|\u62C6\u5206|\u53EA\u7ED9.*\u8BA1\u5212|\u4E0D\u8981.*\u6539\u4EE3\u7801|\u522B.*\u6539\u4EE3\u7801|\u4E0D\u8981.*\u52A8\u4EE3\u7801|\u522B.*\u52A8\u4EE3\u7801|\u8BC4\u4F30.*\u98CE\u9669|\u98CE\u9669.*\u8BC4\u4F30|\u8BBE\u8BA1.*\u65B9\u6848|\u67B6\u6784|\u8FC1\u79FB|\u96C6\u6210|\\b(plan|planning|research|investigate|approach|proposal|architecture|risk|migration|integration)\\b|how (should|would|to)|do not (edit|modify|change)|don.t (edit|modify|change)|no code changes|without changing code"; then
668
- echo "## mancode \u81EA\u52A8\u8DEF\u7531"
669
- echo ""
670
- echo "\u8FD9\u4E2A\u8BF7\u6C42\u662F\u89C4\u5212/\u8C03\u7814\u7C7B\u4EFB\u52A1\u3002\u4E0D\u8981\u76F4\u63A5\u8FDB\u5165 solo \u5B9E\u65BD\u3002"
671
- echo "\u5FC5\u987B\u5148\u8C03\u7528 Skill tool\uFF0Cskill='man'\uFF0C\u628A\u7528\u6237\u539F\u59CB\u8BF7\u6C42\u4F5C\u4E3A task\uFF0C\u6267\u884C Scout \u8C03\u7814\u3001\u6F84\u6E05\u548C Plan Coach plan\u3002"
672
- echo "\u7528\u6237\u53EA\u8981\u8BA1\u5212\u65F6\uFF0C\u5728 Step 4 \u9009\u62E9\u201C\u53EA\u8981\u8BA1\u5212\u201D\uFF1B\u4E0D\u8981\u5207\u5230\u53E6\u4E00\u4E2A\u547D\u4EE4\u3002"
673
- echo ""
674
- fi
675
-
676
- # UI token \u4EC5\u5728 profile \u786E\u8BA4\u5B58\u5728 UI \u8D44\u4EA7\u4E14\u4EFB\u52A1\u660E\u786E\u6D89\u53CA UI \u65F6\u6CE8\u5165\u3002
677
- PROFILE_FILE="$PROJECT_ROOT/.mancode/project-profile.json"
678
- UI_ASSETS=""
679
- if [ -f "$PROFILE_FILE" ]; then
680
- UI_ASSETS=$(json_get "uiAssets" "$PROFILE_FILE")
681
- fi
682
- if [ "$UI_ASSETS" = "detected" ] && echo "$USER_PROMPT" | grep -qiE "\\b(button|component|page|style|ui|design|layout|css|color|font|theme|card|input|modal|dialog|header|footer|sidebar|dropdown|tooltip|toast|avatar|badge)\\b|\u754C\u9762|\u9875\u9762|\u6309\u94AE|\u6837\u5F0F|\u989C\u8272|\u5B57\u4F53|\u5E03\u5C40|\u7EC4\u4EF6|\u5F39\u7A97|\u5BFC\u822A|\u5361\u7247|\u8F93\u5165\u6846|\u4E3B\u9898|\u7F8E\u5316|\u4F18\u5316.*\u754C\u9762|\u8C03\u6574.*\u6837\u5F0F"; then
683
- if [ -f "$AESTHETICS_FILE" ]; then
684
- MATCH_LEVEL=$(json_get "matchLevel" "$AESTHETICS_FILE")
685
-
686
- if [ "$MATCH_LEVEL" = "high" ]; then
687
- if [ "$HAS_JQ" = "1" ]; then
688
- # \u63D0\u53D6\u6458\u8981 + cap\uFF08docs/07 \xA74.1\uFF1Acolors \u22648, fonts \u22644, \u603B < 800 tokens\uFF09
689
- UI=$(jq -r '.uiLibrary // empty' "$AESTHETICS_FILE" 2>/dev/null)
690
- DARK=$(jq -r '.darkMode // empty' "$AESTHETICS_FILE" 2>/dev/null)
691
- MATCH=$(jq -r '.matchLevel // empty' "$AESTHETICS_FILE" 2>/dev/null)
692
- COLORS=$(jq -r '.colors | to_entries | map(select(.key | test("^[A-Za-z0-9_-]{1,80}$"))) | .[0:8] | map("\\(.key)=\\(.value)") | join(", ")' "$AESTHETICS_FILE" 2>/dev/null)
693
- FONTS=$(jq -r '.fonts | to_entries | map(select(.key | test("^[A-Za-z0-9_-]{1,80}$"))) | .[0:4] | map("\\(.key)=\\(.value | first)") | join(", ")' "$AESTHETICS_FILE" 2>/dev/null)
694
- COMPONENTS=$(jq -r '(.components // []) | map(select(test("^[A-Z][A-Za-z0-9]{0,79}$"))) | .[0:8] | join(", ")' "$AESTHETICS_FILE" 2>/dev/null)
695
- CSS_VARS=$(jq -r '(.cssVariables // {}) | to_entries | map(select(.key | test("^[A-Za-z0-9_-]{1,80}$"))) | .[0:8] | map("--\\(.key)=\\(.value)") | join(", ")' "$AESTHETICS_FILE" 2>/dev/null)
696
-
697
- echo "## \u5BA1\u7F8E token \u6458\u8981"
698
- [ -n "$UI" ] && echo "UI: $(sanitize "$UI")"
699
- [ -n "$DARK" ] && echo "Dark: $(sanitize "$DARK")"
700
- [ -n "$MATCH" ] && echo "Match: $(sanitize "$MATCH")"
701
- [ -n "$COLORS" ] && echo "Colors (\u524D 8): $(sanitize "$COLORS")"
702
- [ -n "$FONTS" ] && echo "Fonts (\u524D 4): $(sanitize "$FONTS")"
703
- [ -n "$COMPONENTS" ] && echo "Components (\u524D 8): $(sanitize "$COMPONENTS")"
704
- [ -n "$CSS_VARS" ] && echo "CSS variables (\u524D 8): $(sanitize "$CSS_VARS")"
705
- echo "\u5B8C\u6574 token: .mancode/aesthetics/style-tokens.json"
706
- echo ""
707
- else
708
- # \u65E0 jq: \u53EA\u8F93\u51FA\u6307\u9488\uFF08cap \u65E0\u6CD5\u4E25\u683C\u6267\u884C\uFF09
709
- echo "## \u5BA1\u7F8E token"
710
- echo "\u8BFB\u53D6 .mancode/aesthetics/style-tokens.json"
711
- echo ""
712
- fi
713
- fi
714
- fi
715
- fi
603
+ var USER_PROMPT_SUBMIT_HOOK = String.raw`#!/usr/bin/env node
604
+ // .mancode/hooks/user-prompt-submit.mjs
605
+ // mancode UserPromptSubmit hook - cross-platform prompt and style context
606
+ import { readFileSync } from 'node:fs';
607
+ import path from 'node:path';
608
+ import { fileURLToPath } from 'node:url';
609
+
610
+ const projectRoot = path.resolve(
611
+ path.dirname(fileURLToPath(import.meta.url)),
612
+ '../..',
613
+ );
614
+ const mancodeDir = path.join(projectRoot, '.mancode');
615
+ const state = readJson(path.join(mancodeDir, 'state.json')) || {};
616
+ const profile = readJson(path.join(mancodeDir, 'project-profile.json')) || {};
617
+ const mode = typeof state.currentMode === 'string' ? state.currentMode : '';
618
+ const output = [];
619
+
620
+ if (mode === 'solo') {
621
+ output.push(
622
+ '## 动手前,先想六个问题:',
623
+ '',
624
+ '1. **为什么做?**',
625
+ ' - 这个改动解决什么问题?',
626
+ '',
627
+ '2. **已经有什么?**',
628
+ ' - 项目里有没有类似的实现可以复用?',
629
+ '',
630
+ '3. **最少改多少?**',
631
+ ' - 能用一行解决吗?能复用现有代码吗?',
632
+ '',
633
+ '4. **能不能不拆新系统?**',
634
+ ' - 不新建文件或模块能完成吗?',
635
+ '',
636
+ '5. **非平凡逻辑怎样最小运行验证?**',
637
+ '',
638
+ '6. **有什么没把握的?**',
639
+ ' - 先自行查代码或文档,最多 2 次工具调用;仍不确定再问用户。',
640
+ '',
641
+ );
642
+ }
643
+
644
+ const rawInput = readFileSync(0, 'utf8');
645
+ const userPrompt = readPrompt(rawInput);
646
+ const planningPattern = /先(?:别|不要|看|看看|调研|分析|评估)|给.*方案|给.*计划|怎么.*做|如何.*做|怎么.*实现|如何.*实现|应该怎么|怎么.*拆|拆分|只给.*计划|不要.*改代码|别.*改代码|不要.*动代码|别.*动代码|评估.*风险|风险.*评估|设计.*方案|架构|迁移|集成|\b(?:plan|planning|research|investigate|approach|proposal|architecture|risk|migration|integration)\b|how (?:should|would|to)|do not (?:edit|modify|change)|don.t (?:edit|modify|change)|no code changes|without changing code/iu;
647
+
648
+ if (mode === 'solo' && planningPattern.test(userPrompt)) {
649
+ output.push(
650
+ '## mancode 自动路由',
651
+ '',
652
+ '这个请求是规划/调研类任务。不要直接进入 solo 实施。',
653
+ "必须先调用 Skill tool,skill='man',把用户原始请求作为 task,执行 Scout 调研、澄清和 Plan Coach plan。",
654
+ '用户只要计划时,在 Step 4 选择“只要计划”;不要切到另一个命令。',
655
+ '',
656
+ );
657
+ }
658
+
659
+ const uiPattern = /\b(?:button|component|page|style|ui|design|layout|css|color|font|theme|card|input|modal|dialog|header|footer|sidebar|dropdown|tooltip|toast|avatar|badge)\b|界面|页面|按钮|样式|颜色|字体|布局|组件|弹窗|导航|卡片|输入框|主题|美化|优化.*界面|调整.*样式/iu;
660
+
661
+ if (profile.uiAssets === 'detected' && uiPattern.test(userPrompt)) {
662
+ appendAestheticSummary(
663
+ output,
664
+ readJson(path.join(mancodeDir, 'aesthetics', 'style-tokens.json')),
665
+ );
666
+ }
667
+
668
+ if (output.length > 0) console.log(output.join('\n'));
669
+
670
+ function appendAestheticSummary(lines, tokens) {
671
+ if (!tokens || tokens.matchLevel !== 'high') return;
672
+
673
+ const colors = safeEntries(tokens.colors, 8).map(
674
+ ([key, value]) => key + '=' + String(value),
675
+ );
676
+ const fonts = safeEntries(tokens.fonts, 4)
677
+ .filter(([, value]) => Array.isArray(value) && value.length > 0)
678
+ .map(([key, value]) => key + '=' + String(value[0]));
679
+ const components = Array.isArray(tokens.components)
680
+ ? tokens.components
681
+ .filter(
682
+ (value) =>
683
+ typeof value === 'string' && /^[A-Z][A-Za-z0-9]{0,79}$/.test(value),
684
+ )
685
+ .slice(0, 8)
686
+ : [];
687
+ const cssVariables = safeEntries(tokens.cssVariables, 8).map(
688
+ ([key, value]) => '--' + key + '=' + String(value),
689
+ );
690
+
691
+ lines.push('## 审美 token 摘要');
692
+ appendValue(lines, 'UI', tokens.uiLibrary);
693
+ appendValue(lines, 'Dark', tokens.darkMode);
694
+ appendValue(lines, 'Match', tokens.matchLevel);
695
+ appendValue(lines, 'Colors (前 8)', colors.join(', '));
696
+ appendValue(lines, 'Fonts (前 4)', fonts.join(', '));
697
+ appendValue(lines, 'Components (前 8)', components.join(', '));
698
+ appendValue(lines, 'CSS variables (前 8)', cssVariables.join(', '));
699
+ lines.push('完整 token: .mancode/aesthetics/style-tokens.json', '');
700
+ }
701
+
702
+ function safeEntries(value, limit) {
703
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return [];
704
+ return Object.entries(value)
705
+ .filter(([key]) => /^[A-Za-z0-9_-]{1,80}$/.test(key))
706
+ .slice(0, limit);
707
+ }
708
+
709
+ function appendValue(lines, label, value) {
710
+ const clean = sanitize(value);
711
+ if (clean) lines.push(label + ': ' + clean);
712
+ }
713
+
714
+ function readPrompt(input) {
715
+ try {
716
+ const parsed = JSON.parse(input);
717
+ return typeof parsed.prompt === 'string' && parsed.prompt
718
+ ? parsed.prompt
719
+ : input;
720
+ } catch {
721
+ return input;
722
+ }
723
+ }
724
+
725
+ function readJson(filePath) {
726
+ try {
727
+ return JSON.parse(readFileSync(filePath, 'utf8'));
728
+ } catch {
729
+ return null;
730
+ }
731
+ }
732
+
733
+ function sanitize(value) {
734
+ return String(value ?? '').replace(/[\r\n]/g, ' ').slice(0, 200);
735
+ }
716
736
  `;
717
737
  var SOLO_SKILL = `# mancode \xB7 solo mode
718
738
 
@@ -757,23 +777,19 @@ var SOLO_SKILL = `# mancode \xB7 solo mode
757
777
  4. \u4EE5\u9879\u76EE\u65E2\u6709\u7684\u65E0\u969C\u788D\u3001\u54CD\u5E94\u5F0F\u4E0E\u53CD\u9988\u65B9\u5F0F\u4E3A\u51C6
758
778
 
759
779
  ### \u754C\u9762\u8D28\u91CF\u68C0\u67E5\uFF08\u6761\u4EF6\u6267\u884C\uFF09
760
- UI \u8F93\u51FA\u524D\u81EA\u68C0\uFF1A
761
- - \u662F\u5426\u590D\u7528\u4E86\u9879\u76EE\u5B9E\u9645\u7684\u5C42\u7EA7\u3001\u95F4\u8DDD\u3001\u989C\u8272\u3001\u5B57\u4F53\u4E0E\u4EA4\u4E92\u53CD\u9988\uFF1F
762
- - \u4FE1\u606F\u5C42\u7EA7\u3001\u7A7A/\u52A0\u8F7D/\u9519\u8BEF\u72B6\u6001\u548C\u952E\u76D8/\u8BFB\u5C4F\u53EF\u8FBE\u6027\u662F\u5426\u6E05\u695A\uFF1F
763
- - \u662F\u5426\u4E3A\u4E86\u201C\u597D\u770B\u201D\u5F15\u5165\u4E0E\u73B0\u6709\u4EA7\u54C1\u65E0\u5173\u7684\u88C5\u9970\u3001\u52A8\u753B\u6216\u7EC4\u4EF6\uFF1F
780
+ \u53EA\u68C0\u67E5\u672C\u6B21 diff \u65B0\u589E\u6216\u6539\u53D8\u7684\u754C\u9762\u884C\u4E3A\uFF1A\u590D\u7528\u5DF2\u6709\u5C42\u7EA7\u3001token \u548C\u7EC4\u4EF6\uFF1B\u65B0\u589E\u5F02\u6B65\u6216\u4EA4\u4E92\u8DEF\u5F84\u65F6\u624D\u68C0\u67E5\u5BF9\u5E94\u7684\u52A0\u8F7D\u3001\u5931\u8D25\u548C\u53EF\u8FBE\u6027\u72B6\u6001\u3002\u4E0D\u8981\u501F UI \u81EA\u68C0\u5DE1\u67E5\u672A\u6539\u52A8\u9875\u9762\u6216\u8865\u9F50\u63A8\u6D4B\u6027\u72B6\u6001\u3002
764
781
 
765
782
  ### \u9A8C\u8BC1
766
783
  \u4EE3\u7801\u5B8C\u6210\u540E\uFF1A
767
- 1. \u8FD0\u884C build\uFF08\u5982\u6709\uFF09
768
- 2. \u8FD0\u884C lint\uFF08\u5982\u6709\uFF09
769
- 3. \u8FD0\u884C test\uFF08\u5982\u6709\uFF09
770
- 4. \u786E\u8BA4\u901A\u8FC7\u540E\u624D\u58F0\u79F0\u5B8C\u6210
784
+ 1. \u9009\u62E9\u4E0E\u672C\u6B21\u6539\u52A8\u76F4\u63A5\u76F8\u5173\u7684\u6700\u7A84 test\u3001lint\u3001typecheck\u3001build \u6216 smoke check
785
+ 2. \u6587\u6848\u3001\u6CE8\u91CA\u6216\u7EAF\u9759\u6001\u8D44\u6E90\u6539\u52A8\u4E0D\u5F3A\u5236\u8FD0\u884C\u5B8C\u6574\u6D4B\u8BD5\u77E9\u9635
786
+ 3. \u786E\u8BA4\u5B9E\u9645\u6267\u884C\u7ED3\u679C\u540E\u624D\u58F0\u79F0\u5B8C\u6210
771
787
 
772
- ### \u5B8C\u6210\u540E\u81EA\u5BA1\u4E0E\u6A21\u5F0F\u63A8\u8350
773
- - \u9A8C\u8BC1\u7C7B\u95EE\u9898\uFF08build/lint/test \u5931\u8D25\u3001\u9057\u7559 debug\uFF09\u76F4\u63A5\u4FEE\u590D\u5E76\u590D\u9A8C\u3002
774
- - \u98CE\u683C\u7C7B\u95EE\u9898\uFF08\u547D\u540D\u3001\u53EF\u8BFB\u6027\u3001DRY\uFF09\u53EA\u63D0\u793A\uFF0C\u4E0D\u4E3A\u6B64\u6269\u5927\u65E0\u5173\u6539\u52A8\u3002
775
- - \u52A8\u624B\u524D\u82E5\u6D89\u53CA 3 \u4E2A\u4EE5\u4E0A\u6587\u4EF6\u3001\u65B0\u6A21\u5757\u6216\u67B6\u6784\u53D8\u52A8\uFF0C\u7528\u4E00\u53E5\u8BDD\u5EFA\u8BAE\u7528\u6237\u5148\u7528 \`/man\`\uFF1B\u7528\u6237\u8BF4\u201C\u7EE7\u7EED solo\u201D\u5373\u53EF\u8DF3\u8FC7\u3002
776
- - \u81EA\u5BA1\u53D1\u73B0 3 \u4E2A\u4EE5\u4E0A\u95EE\u9898\u65F6\uFF0C\u5EFA\u8BAE \`/manps\` \u6216 \`/man\`\u3002
788
+ ### \u5B8C\u6210\u540E\u7684\u4E00\u6B21\u53D7\u9650\u81EA\u68C0
789
+ - \u53EA\u505A\u4E00\u6B21\uFF0C\u53EA\u770B\u672C\u6B21 diff\u3001\u9700\u6C42\u548C\u76F4\u63A5\u53D7\u5F71\u54CD\u8DEF\u5F84\uFF1B\u4E0D\u8C03\u7528\u989D\u5916 reviewer\uFF0C\u4E0D\u751F\u6210\u5BA1\u67E5\u62A5\u544A\uFF0C\u4E0D\u91CD\u590D\u5DE1\u68C0\u3002
790
+ - \u9A8C\u8BC1\u5931\u8D25\u3001\u884C\u4E3A\u56DE\u5F52\u6216\u9057\u7559 debug \u53EF\u76F4\u63A5\u4FEE\u590D\u5E76\u590D\u9A8C\uFF1B\u590D\u9A8C\u4E0D\u662F\u65B0\u4E00\u8F6E review\u3002
791
+ - \u547D\u540D\u3001\u53EF\u8BFB\u6027\u3001DRY\u3001loading/error \u5F62\u5F0F\u7B49\u5EFA\u8BAE\u4E0D\u81EA\u52A8\u6269\u5927\u6539\u52A8\uFF1B\u4E0E\u9700\u6C42\u65E0\u5173\u65F6\u4E0D\u8F93\u51FA\u3002
792
+ - \u53EA\u6709\u9274\u6743\u3001\u652F\u4ED8\u3001\u654F\u611F\u6570\u636E\u3001\u8FC1\u79FB/\u5220\u9664\u3001\u516C\u5F00 API\u3001\u672A\u53EF\u4FE1\u8F93\u5165\u3001\u5E76\u53D1\u6216\u57FA\u7840\u8BBE\u65BD\u7B49\u786C\u98CE\u9669\u51FA\u73B0\u65F6\uFF0C\u624D\u7528\u4E00\u53E5\u8BDD\u5EFA\u8BAE \`/man\`\uFF1B\u7528\u6237\u8BF4\u201C\u7EE7\u7EED solo\u201D\u5373\u53EF\u7EE7\u7EED\u3002
777
793
 
778
794
  ## \u4F60\u7684\u98CE\u683C
779
795
 
@@ -833,21 +849,24 @@ var MAN_SKILL = {
833
849
 
834
850
  \u8FD0\u884C\u5B9E\u9645 build/lint/typecheck/test \u548C smoke test\u3002\u76F8\u540C\u4EE3\u7801\u3001\u73AF\u5883\u3001\u547D\u4EE4\u4E0B\u76F8\u540C\u9519\u8BEF\u7B7E\u540D\u5931\u8D25\u4E24\u6B21\uFF0C\u505C\u6B62\u76F2\u8BD5\u5E76\u8BCA\u65AD\u6839\u56E0\u3002\u9700\u8981\u771F\u5B9E\u6D4F\u89C8\u5668\u3001\u590D\u6742\u590D\u73B0\u6216\u56DE\u5F52\u65F6\uFF0C\u7528 \`mancode workflow create mamba "<\u95EE\u9898>" --parent-task <taskId> --json\` \u521B\u5EFA\u5B50 workflow\uFF1B\u7236\u4EFB\u52A1\u4FDD\u6301 Step 6\u3002\u5B50\u4EFB\u52A1 fixed/verified/no_repro \u540E\u6062\u590D\u672C\u4EFB\u52A1\uFF1B\u82E5\u7236\u66FE\u56E0\u8BE5\u5B50\u4EFB\u52A1 blocked\uFF0C\u5148\u901A\u8FC7 \`workflow update --status in_progress\` \u6062\u590D\uFF0C\u518D\u66F4\u65B0\u81F3 Step 7\u3002blocked \u6216 manual_test_required \u4F1A\u7531 CLI \u81EA\u52A8\u963B\u585E\u7236\u4EFB\u52A1\uFF0C\u4E0D\u5F97\u624B\u6539\u7236 metadata\uFF0C\u4E5F\u4E0D\u5F97\u81EA\u52A8\u8D8A\u8FC7\u4EBA\u5DE5\u9A8C\u8BC1\u8981\u6C42\u3002
835
851
 
852
+ \u9A8C\u8BC1\u540E\u57FA\u4E8E**\u5B9E\u9645 diff**\u5199 \`review-scope.md\`\uFF1Abase\u3001\u6539\u52A8\u6587\u4EF6\u3001\u9700\u6C42\u3001\u5DF2\u8DD1\u9A8C\u8BC1\u3001\u786C\u98CE\u9669\u548C\u5BA1\u67E5\u6DF1\u5EA6\u3002\u9274\u6743\u3001\u652F\u4ED8\u3001\u654F\u611F\u6570\u636E\u3001\u8FC1\u79FB/\u5220\u9664\u3001\u516C\u5F00 API\u3001\u672A\u53EF\u4FE1\u8F93\u5165\u3001\u5E76\u53D1\u3001\u8DE8\u670D\u52A1\u6216\u57FA\u7840\u8BBE\u65BD\u547D\u4E2D\u4EFB\u4E00\u9879\u65F6\u7528\u5B8C\u6574\u5BA1\u67E5 \`full\`\uFF1B\u5426\u5219\u7528\u5B9A\u5411\u5BA1\u67E5 \`targeted\`\u3002\u8FD0\u884C \`mancode workflow review <taskId> init --review-depth targeted --review-domain quality\` \u6216 \`--review-depth full\`\u3002\u7528\u6237\u660E\u786E\u8DF3\u8FC7\u5BA1\u67E5\u65F6\u624D\u628A \`review\` \u5199\u5165\u7D2F\u8BA1 skippedSteps\uFF0C\u5E76\u8BB0\u5F55\u6B8B\u4F59\u98CE\u9669\u3002
853
+
836
854
  ### Step 7: Film #1 \u4EE3\u7801\u8D28\u91CF\u5BA1\u67E5\u4E0E\u4FEE\u590D
837
855
 
838
- \u8C03\u7528 \`film-analyst-offense\` \u5BA1\u67E5\u8D28\u91CF\u3001\u590D\u7528\u3001\u590D\u6742\u5EA6\u548C\u6D4B\u8BD5\uFF0C\u5199 \`film-report-1.md\`\uFF1BHead Coach \u4FEE\u590D \u{1F534}\uFF0C\u5BF9 \u{1F7E1} \u5199\u4FEE\u590D\u6216\u6743\u8861\u3002\u5B8C\u6210\u540E\u66F4\u65B0\u81F3 Step 8\u3002
856
+ \u672A\u8DF3\u8FC7 review \u65F6\u8C03\u7528 \`film-analyst-offense\`\uFF0C\u53EA\u5BA1\u67E5\u672C\u6B21 diff \u7684\u884C\u4E3A\u6B63\u786E\u6027\u3001\u590D\u7528\u3001\u590D\u6742\u5EA6\u548C\u6D4B\u8BD5\uFF0C\u5199 \`film-report-1.md\`\u3002\u6BCF\u6761 finding \u5FC5\u987B\u5F15\u7528\u6539\u52A8\u884C\u3001\u7ED9\u51FA\u8BC1\u636E\u548C\u7528\u6237\u5F71\u54CD\uFF1B\u6700\u591A 3 \u4E2A\u65B0 finding\u3002\u7528\u7A33\u5B9A ID\uFF08\u5982 Q1\uFF09\u6807\u8BB0 \u{1F534} blocker\uFF0C\u5E76\u8FD0\u884C \`mancode workflow review <taskId> complete --review-domain quality --report film-report-1.md --blockers Q1,Q2\`\uFF1B\u6CA1\u6709 blocker \u65F6\u4F20\u7A7A\u5B57\u7B26\u4E32\u3002\u6B64\u65F6\u4E0D\u4FEE\u590D\uFF0C\u5148\u5B8C\u6210\u6240\u9700\u5BA1\u67E5\u9886\u57DF\uFF0C\u518D\u66F4\u65B0\u81F3 Step 8\u3002
839
857
 
840
858
  ### Step 8: Film #2 \u5B89\u5168\u4E0E\u8FB9\u754C\u5BA1\u67E5
841
859
 
842
- \u8C03\u7528 \`film-analyst-defense\` \u5BA1\u67E5\u5B89\u5168\u3001\u6743\u9650\u3001\u9519\u8BEF\u8DEF\u5F84\u3001\u517C\u5BB9\u6027\u548C\u8FB9\u754C\uFF0C\u5199 \`film-report-2.md\`\u3002\u53CC\u5BA1\u9ED8\u8BA4\u6267\u884C\uFF1B\u7528\u6237\u660E\u786E\u8981\u6C42\u624D\u53EF\u8DF3\u8FC7\uFF0C\u901A\u8FC7 CLI \u5199\u5165\u7D2F\u8BA1 skippedSteps \u548C\u6B8B\u4F59\u98CE\u9669\u3002\u5B8C\u6210\u540E\u66F4\u65B0\u81F3 Step 9\u3002
860
+ \`full\` \u624D\u8C03\u7528 \`film-analyst-defense\`\uFF1B\u5B83\u5FC5\u987B\u5148\u8BFB \`review-scope.md\` \u548C \`film-report-1.md\`\uFF0C\u53EA\u5BA1\u67E5\u5B89\u5168\u3001\u6743\u9650\u3001\u9519\u8BEF\u8DEF\u5F84\u3001\u517C\u5BB9\u6027\u3001\u8D44\u6E90\u548C\u8FB9\u754C\u3002\u76F8\u540C\u6839\u56E0\u6807\u8BB0 \`duplicate\`\uFF0C\u4E0D\u5F97\u91CD\u65B0\u62A5\u544A\u3002\u5199 \`film-report-2.md\` \u540E\u4EE5 D1 \u7B49\u7A33\u5B9A blocker ID \u8FD0\u884C \`mancode workflow review <taskId> complete --review-domain security --report film-report-2.md --blockers D1\`\u3002\`targeted\` \u4E0D\u6267\u884C\u7B2C\u4E8C\u5BA1\uFF0C\u4E5F\u4E0D\u628A\u5B83\u63CF\u8FF0\u6210\u201C\u8DF3\u8FC7\u201D\uFF1B\u5B8C\u6210\u540E\u66F4\u65B0\u81F3 Step 9\u3002
843
861
 
844
862
  ### Step 9: \u589E\u5F3A\u6536\u5C3E
845
863
 
846
- 1. \u4FEE\u590D Film #2 \u7684 \u{1F534} \u5E76\u91CD\u8DD1\u9A8C\u8BC1\u3002
847
- 2. \u5199 \`summary.md\`\uFF1A\u6539\u52A8\u3001\u65B0\u5EFA\u3001\u590D\u7528\u3001\u9A8C\u8BC1\u3001\u53CC\u5BA1\u548C\u8DF3\u8FC7\u6B65\u9AA4\u3002
848
- 3. \u9A8C\u8BC1\u901A\u8FC7\u4E14 \u{1F534} \u6E05\u96F6\u624D\u7528 CLI \u5199 \`completed\`\uFF1B\u5426\u5219\u7528 \`--status blocked --blocking-reason "<\u539F\u56E0>"\`\u3002
849
- 4. \u5173\u952E\u51B3\u7B56 appendTeamDecision \u5230 \`decisions.md\`\uFF0C\u66F4\u65B0 Active Plans\u3002
850
- 5. worktree \u5408\u5E76\u524D\u53D6\u5F97\u7528\u6237\u786E\u8BA4\uFF1B\u7EC8\u6001\u5199\u5165\u6210\u529F\u540E state \u56DE solo \u5E76\u6E05\u7A7A workflow \u6307\u9488\u3002
864
+ 1. \u5B58\u5728 open blocker \u65F6\uFF0CHead Coach \u4E00\u6B21\u6027\u4FEE\u590D\u5168\u90E8 blocker\uFF0C\u5E76\u7528 \`mancode workflow review <taskId> remediate --resolved Q1,D1\` \u8BB0\u5F55\u552F\u4E00\u4E00\u8F6E\u4FEE\u590D\uFF1B\u6CA1\u6709 blocker \u65F6\u4E0D\u8FD0\u884C remediate\u3002\u4E0D\u8981\u4E3A \u{1F7E1}/\u{1F7E2} \u6269\u5927\u6539\u52A8\u3002
865
+ 2. \u91CD\u8DD1\u53D7\u5F71\u54CD\u9A8C\u8BC1\uFF0C\u4E0D\u91CD\u65B0\u8FD0\u884C\u5DF2\u5B8C\u6210\u7684 reviewer\u3002\u4FEE\u590D\u82E5\u5F15\u5165\u65B0\u7684\u9AD8\u98CE\u9669\u9762\u5219\u6807\u8BB0 blocked\uFF0C\u4E0D\u80FD\u5F00\u542F\u65E0\u754C review \u5FAA\u73AF\u3002
866
+ 3. \u5199 \`summary.md\`\uFF1A\u6539\u52A8\u3001\u65B0\u5EFA\u3001\u590D\u7528\u3001\u9A8C\u8BC1\u3001\u5BA1\u67E5\u6DF1\u5EA6\u3001findings \u5904\u7F6E\u3001\u8DF3\u8FC7\u6B65\u9AA4\u548C\u6B8B\u4F59\u98CE\u9669\u3002
867
+ 4. CLI \u786E\u8BA4\u6240\u9700\u5BA1\u67E5\u9886\u57DF\u5B8C\u6210\u4E14 blocker \u6E05\u96F6\u540E\u624D\u5199 \`completed\`\uFF1B\u5426\u5219\u7528 \`--status blocked --blocking-reason "<\u539F\u56E0>"\`\u3002
868
+ 5. \u5173\u952E\u51B3\u7B56 appendTeamDecision \u5230 \`decisions.md\`\uFF0C\u66F4\u65B0 Active Plans\u3002
869
+ 6. worktree \u5408\u5E76\u524D\u53D6\u5F97\u7528\u6237\u786E\u8BA4\uFF1B\u7EC8\u6001\u5199\u5165\u6210\u529F\u540E state \u56DE solo \u5E76\u6E05\u7A7A workflow \u6307\u9488\u3002
851
870
 
852
871
  \u4EFB\u4F55 step/status/outcome/planVersion \u53D8\u5316\u90FD\u5FC5\u987B\u7ECF\u8FC7 workflow CLI\uFF1BCLI \u62D2\u7EDD\u65F6\u4FDD\u7559\u5F53\u524D state \u5E76\u62A5\u544A\u539F\u56E0\uFF0C\u4E0D\u53EF\u7ED5\u8FC7\u6821\u9A8C\u76F4\u63A5\u6539 metadata.json\u3002
853
872
 
@@ -1174,9 +1193,9 @@ var MANTEAM_SKILL = {
1174
1193
  4. \u8BA1\u5212\u5173\u5361\uFF1A\u7528\u6237\u53EF\u53EA\u4FDD\u7559\u8BA1\u5212\u3001\u7EE7\u7EED\u6267\u884C\u6216\u91CD\u5199\u8BA1\u5212\uFF1BActive Plans \u6309 taskId \u66F4\u65B0\u3002
1175
1194
  5. \u5B9E\u65BD\uFF1A\u6539\u52A8\u524D\u518D\u6B21\u68C0\u67E5 \`git status --short\`\uFF0C\u907F\u514D\u8E29\u7528\u6237\u6216\u961F\u53CB\u6539\u52A8\u3002
1176
1195
  6. Self-test\uFF1A\u4F18\u5148\u8DD1\u9879\u76EE\u5DF2\u6709\u9A8C\u8BC1\u547D\u4EE4\uFF1B\u5931\u8D25\u4E24\u6B21\u505C\u4E0B\u8BCA\u65AD\u6839\u56E0\u3002\u9700\u8981\u771F\u5B9E\u9A8C\u8BC1\u65F6\u521B\u5EFA\u5E26 parentTaskId \u7684 /mamba \u5B50\u4EFB\u52A1\u3002\u5B50\u4EFB\u52A1\u4FEE\u590D\u540E\u82E5\u7236\u4EFB\u52A1\u66FE\u56E0\u6B64 blocked\uFF0C\u5148\u7528 workflow CLI \u6062\u590D\u7236\u4EFB\u52A1\u4E3A in_progress\uFF1Bmanual_test_required \u4E0D\u5F97\u81EA\u52A8\u6062\u590D\u3002
1177
- 7. Film #1\uFF1A\u91CD\u70B9\u5BA1\u67E5\u53EF\u7EF4\u62A4\u6027\u3001\u547D\u540D\u4E00\u81F4\u6027\u3001\u56E2\u961F\u98CE\u683C\u4E00\u81F4\u6027\uFF0C\u5E76\u4FEE\u590D \u{1F534}\u3002
1178
- 8. Film #2\uFF1A\u91CD\u70B9\u5BA1\u67E5\u8FB9\u754C\u6761\u4EF6\u3001\u5B89\u5168\u3001\u6027\u80FD\u3001\u5E76\u53D1\u3001\u517C\u5BB9\u6027\u3002\u53CC\u5BA1\u9ED8\u8BA4\u6267\u884C\uFF1B\u7528\u6237\u660E\u786E\u8981\u6C42\u624D\u53EF\u8DF3\u8FC7\uFF0C\u5199\u5165 \`skippedSteps\` \u548C\u6B8B\u4F59\u98CE\u9669\uFF1B\u8DF3\u8FC7\u4E0D\u963B\u6B62 completed\uFF0C\u4F46\u4E0D\u5F97\u58F0\u79F0"\u5DF2\u5B8C\u6210\u53CC\u5BA1"\u3002
1179
- 9. Post-game\uFF1A\u590D\u9A8C\uFF0C\u5199 summary \u4E0E hand-off\uFF1B\u9A8C\u8BC1\u5931\u8D25\u6216\u672A\u4FEE\u590D \u{1F534} \u65F6\u6807\u8BB0 blocked\uFF0C\u4E0D\u5F97\u6807 completed\u3002
1196
+ 7. Review scope + Film #1\uFF1A\u57FA\u4E8E\u5B9E\u9645 diff \u5199 \`review-scope.md\`\uFF0C\u7528 \`workflow review ... init\` \u9009\u62E9 targeted \u6216 full\uFF1B\u91CD\u70B9\u5BA1\u67E5\u884C\u4E3A\u3001\u53EF\u7EF4\u62A4\u6027\u3001\u56E2\u961F\u98CE\u683C\u4E0E\u6D4B\u8BD5\uFF0Cfinding \u5FC5\u987B\u6709\u8BC1\u636E\u548C\u7A33\u5B9A ID\u3002
1197
+ 8. Film #2\uFF1A\u4EC5 full \u6267\u884C\uFF0C\u5148\u8BFB Film #1 \u62A5\u544A\u5E76\u53BB\u91CD\uFF0C\u53EA\u5BA1\u67E5\u8FB9\u754C\u3001\u5B89\u5168\u3001\u6027\u80FD\u3001\u5E76\u53D1\u548C\u517C\u5BB9\u6027\u3002\u7528\u6237\u660E\u786E\u8981\u6C42\u624D\u53EF\u8DF3\u8FC7\u5168\u90E8 review\uFF0C\u5E76\u5199\u5165 \`skippedSteps\` \u548C\u6B8B\u4F59\u98CE\u9669\u3002
1198
+ 9. Post-game\uFF1A\u6C47\u603B blocker\uFF0C\u53EA\u505A\u4E00\u8F6E\u4FEE\u590D\u5E76\u7528 \`workflow review ... remediate\` \u8BB0\u5F55\uFF1B\u590D\u9A8C\u540E\u5199 summary \u4E0E hand-off\u3002\u9A8C\u8BC1\u5931\u8D25\u3001\u5BA1\u67E5\u4E0D\u5B8C\u6574\u6216\u4ECD\u6709 blocker \u65F6\u6807\u8BB0 blocked\uFF0C\u4E0D\u5F97\u6807 completed\u3002
1180
1199
 
1181
1200
  \u5982\u679C\u7528\u6237\u5728\u786E\u8BA4\u9636\u6BB5\u9009\u62E9"\u9000\u51FA"\u6216\u653E\u5F03\u56E2\u961F workflow\uFF1A\u7528 \`mancode workflow update <taskId> --status abandoned\` \u66F4\u65B0\u5E76\u6E05\u7406 Active Plans\uFF1B\u82E5\u6709\u6D3B\u8DC3 mamba \u5B50\u4EFB\u52A1\uFF0C\u5148\u53D6\u5F97\u786E\u8BA4\u5E76\u9010\u4E2A abandoned\u3002\u6210\u529F\u540E\u518D\u7528 Edit \u66F4\u65B0 state \u56DE solo\u3002\u4E0D\u8981\u76F4\u63A5\u6539 metadata\uFF0C\u4E5F\u4E0D\u8981\u628A abandoned workflow \u7559\u5728 active state\u3002
1182
1201
 
@@ -1242,7 +1261,7 @@ ${spec.body}
1242
1261
  }
1243
1262
 
1244
1263
  // src/installers/common.ts
1245
- import { chmod, mkdir as mkdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "fs/promises";
1264
+ import { mkdir as mkdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "fs/promises";
1246
1265
  import path2 from "path";
1247
1266
 
1248
1267
  // src/system/team-memory.ts
@@ -1432,12 +1451,10 @@ async function readTextIfExists(filePath) {
1432
1451
  }
1433
1452
  }
1434
1453
  async function installHooks(hooksDir) {
1435
- const sessionStartDst = path2.join(hooksDir, "session-start.sh");
1454
+ const sessionStartDst = path2.join(hooksDir, "session-start.mjs");
1436
1455
  await writeFile2(sessionStartDst, SESSION_START_HOOK, "utf-8");
1437
- await chmod(sessionStartDst, 493);
1438
- const userPromptDst = path2.join(hooksDir, "user-prompt-submit.sh");
1456
+ const userPromptDst = path2.join(hooksDir, "user-prompt-submit.mjs");
1439
1457
  await writeFile2(userPromptDst, USER_PROMPT_SUBMIT_HOOK, "utf-8");
1440
- await chmod(userPromptDst, 493);
1441
1458
  }
1442
1459
  async function pathExists(p) {
1443
1460
  try {
@@ -1455,12 +1472,18 @@ function isNodeError(err) {
1455
1472
  var CLAUDE_SKILL_MANAGED_MARKER = "<!-- Managed by mancode:claude-skill. Do not edit this marker. -->";
1456
1473
  var CLAUDE_AGENT_MANAGED_MARKER = "<!-- Managed by mancode:claude-agent. Do not edit this marker. -->";
1457
1474
  var MANCODE_HOOK_COMMANDS = [
1475
+ 'node ".mancode/hooks/session-start.mjs"',
1476
+ 'node ".mancode/hooks/user-prompt-submit.mjs"'
1477
+ ];
1478
+ var LEGACY_MANCODE_HOOK_COMMANDS = [
1458
1479
  "bash .mancode/hooks/session-start.sh",
1459
1480
  "bash .mancode/hooks/user-prompt-submit.sh"
1460
1481
  ];
1461
1482
  function isGeneratedMancodeHookCommand(command) {
1462
1483
  const normalized = command.trim();
1463
- return MANCODE_HOOK_COMMANDS.some((item) => item === normalized);
1484
+ return [...MANCODE_HOOK_COMMANDS, ...LEGACY_MANCODE_HOOK_COMMANDS].some(
1485
+ (item) => item === normalized
1486
+ );
1464
1487
  }
1465
1488
  var LEGACY_CLAUDE_SKILL_SETTINGS = {
1466
1489
  solo: ".claude/skills/mancode-solo.md",
@@ -1489,6 +1512,14 @@ async function installClaudeCode(projectRoot, options) {
1489
1512
  await installAgents(path3.join(claudeDir, "agents"), force);
1490
1513
  }
1491
1514
  await updateClaudeSettings(claudeDir, settings);
1515
+ await removeLegacyHookFiles(projectRoot);
1516
+ }
1517
+ async function removeLegacyHookFiles(projectRoot) {
1518
+ const hooksDir = path3.join(projectRoot, ".mancode", "hooks");
1519
+ await Promise.all([
1520
+ rm(path3.join(hooksDir, "session-start.sh"), { force: true }),
1521
+ rm(path3.join(hooksDir, "user-prompt-submit.sh"), { force: true })
1522
+ ]);
1492
1523
  }
1493
1524
  async function installSoloSkill(skillsDir, force) {
1494
1525
  await installProjectSkill(
@@ -2194,10 +2225,10 @@ var MODE_META = {
2194
2225
  "Step 3 \u2014 Plan: write `plan.md` with scope, reuse, exclusions, risks, completion criteria, and real validation. First plan is v1; revisions use `--plan-version <current+1>` and Active Plans is updated by the CLI.",
2195
2226
  "Step 4 \u2014 Plan gate: the user chooses plan-only (`status planned`), continue to execution, or revise the plan. Do not implement before this choice.",
2196
2227
  "Step 5 \u2014 Implement the confirmed minimum change.",
2197
- "Step 6 \u2014 Run detected build/lint/typecheck/test and smoke checks. A real or complex diagnosis creates a linked mamba child while the parent remains at Step 6. After a resolved child, restore a parent blocked by that child with `workflow update --status in_progress` before Step 7; never auto-resume manual_test_required.",
2198
- "Step 7 \u2014 Film #1 code-quality review; fix all required findings and explain suggested-item tradeoffs.",
2199
- "Step 8 \u2014 Film #2 security/boundary review; keep it independent from Film #1. The two review passes are skipped only on explicit user instruction and the residual risk is recorded.",
2200
- "Step 9 \u2014 Fix, re-verify, write `summary.md`, archive decisions, and set completed only when verification passes and required findings are zero; otherwise set blocked+blockingReason.",
2228
+ "Step 6 \u2014 Run detected build/lint/typecheck/test and smoke checks. A real or complex diagnosis creates a linked mamba child while the parent remains at Step 6. After a resolved child, restore a parent blocked by that child with `workflow update --status in_progress` before Step 7; never auto-resume manual_test_required. Then write `review-scope.md` from the actual diff, verification evidence, and hard-risk triggers. Initialize a targeted review for routine governed changes or a full review for auth, payment, sensitive data, migrations/deletion, public APIs, untrusted input, concurrency, cross-service, or infrastructure changes using `mancode workflow review <taskId> init ...`.",
2229
+ "Step 7 \u2014 Run one quality review limited to the changed diff and direct impact. Findings require changed-line evidence and user impact, with at most three new findings. Record stable blocker IDs through `workflow review ... complete`; do not fix yet.",
2230
+ "Step 8 \u2014 Only full review runs the security/boundary reviewer. It must read Film #1, mark the same root cause duplicate, and stay within security, permissions, recovery, resources, and boundaries. A targeted review ends after Film #1.",
2231
+ "Step 9 \u2014 If open blockers exist, fix them in one remediation round and record resolved IDs through `workflow review ... remediate`; with no blockers, do not create a remediation round. Re-run affected verification without re-running completed reviewers. Write `summary.md` and set completed only when required domains are complete and blockers are zero; otherwise set blocked+blockingReason.",
2201
2232
  "",
2202
2233
  "After planned/completed/abandoned, return state to solo. On blocked, preserve enough state to resume. A CLI rejection is a hard stop, never a reason to bypass metadata validation."
2203
2234
  ].join("\n")
@@ -2214,7 +2245,7 @@ var MODE_META = {
2214
2245
  "shared-module, and handoff risks in `team-context.md`.",
2215
2246
  "",
2216
2247
  "Then execute every /man Step 1 through Step 9, including clarification,",
2217
- "plan gate, validation, two review passes, conditional completion, planVersion,",
2248
+ "plan gate, validation, bounded risk-based review, conditional completion, planVersion,",
2218
2249
  "Active Plans, and linked mamba child behavior. Add compatibility, rollback,",
2219
2250
  "ownership, conflict, and handoff notes at each relevant step.",
2220
2251
  "When a resolved mamba child had blocked the parent, restore the parent through",
@@ -2317,7 +2348,9 @@ function renderPracticeRules() {
2317
2348
  "5. Prefer a one-line fix when it is enough.",
2318
2349
  "6. Only then write the smallest new implementation.",
2319
2350
  "",
2320
- "For every task, consider: why this change, what already exists, and what is the smallest useful diff?"
2351
+ "For every task, consider: why this change, what already exists, and what is the smallest useful diff?",
2352
+ "",
2353
+ "In solo mode, use the narrowest meaningful validation and one bounded self-check limited to the current diff. Do not start another reviewer or repeat the review. Only recommend man for auth, payment, sensitive data, migrations/deletion, public APIs, untrusted input, concurrency, or infrastructure risk."
2321
2354
  ].join("\n");
2322
2355
  }
2323
2356
  function renderAesthetics(tokens) {
@@ -2499,7 +2532,7 @@ function renderCopilotPromptConventions() {
2499
2532
  "",
2500
2533
  "GitHub Copilot does not provide native mancode slash commands, hooks, or isolated subagents. Treat these names as user prompt conventions:",
2501
2534
  "",
2502
- "- man: use progressive research, planning, implementation, verification, and review.",
2535
+ "- man: use progressive research, planning, implementation, verification, and bounded risk-based review.",
2503
2536
  "- mamba: diagnose bugs and validate real user flows or regressions.",
2504
2537
  "- manteam: read team memory and write handoff-friendly summaries.",
2505
2538
  "- manps: prefer `mancode manps [area]` before cleanup.",
@@ -2717,7 +2750,9 @@ function renderSoloRule() {
2717
2750
  "- Keep the diff narrow.",
2718
2751
  "- Reuse existing functions, components, styles, and dependencies.",
2719
2752
  "- Read `.mancode/project-profile.json` before choosing tools or validation. For detected UI assets and UI work, read `.mancode/aesthetics/style-tokens.json` and inspect existing components before inventing new styles.",
2720
- "- Verify with the narrowest meaningful test, lint, build, or smoke check."
2753
+ "- Verify with the narrowest meaningful test, lint, build, or smoke check.",
2754
+ "- Perform one bounded self-check limited to the current diff. Do not start another reviewer, create a review artifact, or repeat the review.",
2755
+ "- Recommend /man only for auth, payment, sensitive data, migrations/deletion, public APIs, untrusted input, concurrency, or infrastructure risk."
2721
2756
  ].join("\n");
2722
2757
  }
2723
2758
  function renderMambaRule() {
@@ -2851,11 +2886,12 @@ function isPlatformName(platform) {
2851
2886
  }
2852
2887
 
2853
2888
  // src/system/detect-team.ts
2854
- import { exec } from "child_process";
2889
+ import { execFile } from "child_process";
2890
+ import { access as access2 } from "fs/promises";
2855
2891
  import path10 from "path";
2856
2892
  import process2 from "process";
2857
2893
  import { promisify } from "util";
2858
- var execAsync = promisify(exec);
2894
+ var execFileAsync = promisify(execFile);
2859
2895
  var NO_TEAM = {
2860
2896
  isTeam: false,
2861
2897
  contributors: 1,
@@ -2863,101 +2899,68 @@ var NO_TEAM = {
2863
2899
  hasRemote: false
2864
2900
  };
2865
2901
  var REMOTE_PATTERN = /github\.com|gitlab\.com|bitbucket\.org/;
2866
- var EXEC_OPTS = {
2867
- shell: "/bin/bash",
2868
- encoding: "utf-8",
2869
- env: {
2870
- ...process2.env,
2871
- GIT_PAGER: "cat",
2872
- PAGER: "cat",
2873
- GIT_TERMINAL_PROMPT: "0"
2874
- }
2902
+ var GIT_ENV = {
2903
+ ...process2.env,
2904
+ GIT_PAGER: "cat",
2905
+ PAGER: "cat",
2906
+ GIT_TERMINAL_PROMPT: "0"
2875
2907
  };
2876
2908
  async function detectTeamStatus(projectRoot) {
2877
- const gitDir = path10.join(projectRoot, ".git");
2878
- try {
2879
- await execAsync("git rev-parse --is-inside-work-tree", {
2880
- cwd: projectRoot,
2881
- ...EXEC_OPTS
2882
- });
2883
- } catch {
2884
- return NO_TEAM;
2885
- }
2886
2909
  try {
2887
- await execAsync(`test -d ${JSON.stringify(gitDir)}`, {
2888
- cwd: projectRoot,
2889
- ...EXEC_OPTS
2890
- });
2910
+ await access2(path10.join(projectRoot, ".git"));
2911
+ const inside = await runGit(projectRoot, [
2912
+ "rev-parse",
2913
+ "--is-inside-work-tree"
2914
+ ]);
2915
+ if (inside.trim() !== "true") return NO_TEAM;
2916
+ const [allEmails, recentEmails, remotes] = await Promise.all([
2917
+ runGit(projectRoot, ["log", "--all", "--format=%ae"]),
2918
+ runGit(projectRoot, ["log", "--since=30 days ago", "--format=%ae"]),
2919
+ runGit(projectRoot, ["remote", "-v"])
2920
+ ]);
2921
+ const contributors = countUniqueLines(allEmails);
2922
+ const recentActive = countUniqueLines(recentEmails);
2923
+ const hasRemote = REMOTE_PATTERN.test(remotes);
2924
+ return {
2925
+ isTeam: contributors > 1 && hasRemote && recentActive > 1,
2926
+ contributors: contributors || 1,
2927
+ recentActive: recentActive || 1,
2928
+ hasRemote
2929
+ };
2891
2930
  } catch {
2892
2931
  return NO_TEAM;
2893
2932
  }
2894
- const contributors = await countUniqueEmails(
2895
- "git log --all --format='%ae'",
2896
- projectRoot
2897
- );
2898
- const recentActive = await countUniqueEmails(
2899
- "git log --since='30 days ago' --format='%ae'",
2900
- projectRoot
2901
- );
2902
- const hasRemote = await checkRemote(projectRoot);
2903
- const safeContributors = Number.isFinite(contributors) ? contributors : 0;
2904
- const safeRecent = Number.isFinite(recentActive) ? recentActive : 0;
2905
- const isTeam = safeContributors > 1 && hasRemote && safeRecent > 1;
2906
- return {
2907
- isTeam,
2908
- contributors: safeContributors || 1,
2909
- recentActive: safeRecent || 1,
2910
- hasRemote
2911
- };
2912
2933
  }
2913
- async function countUniqueEmails(command, cwd) {
2914
- try {
2915
- const { stdout } = await execAsync(
2916
- `${command} 2>/dev/null | sort -u | grep -c . || true`,
2917
- { cwd, ...EXEC_OPTS }
2918
- );
2919
- const n = Number.parseInt(stdout.trim(), 10);
2920
- return Number.isFinite(n) ? n : 0;
2921
- } catch {
2922
- return 0;
2923
- }
2934
+ async function runGit(cwd, args) {
2935
+ const { stdout } = await execFileAsync("git", args, {
2936
+ cwd,
2937
+ encoding: "utf-8",
2938
+ env: GIT_ENV,
2939
+ windowsHide: true
2940
+ });
2941
+ return stdout;
2924
2942
  }
2925
- async function checkRemote(cwd) {
2926
- try {
2927
- const { stdout } = await execAsync("git remote -v 2>/dev/null || true", {
2928
- cwd,
2929
- ...EXEC_OPTS
2930
- });
2931
- return REMOTE_PATTERN.test(stdout);
2932
- } catch {
2933
- return false;
2934
- }
2943
+ function countUniqueLines(output) {
2944
+ return new Set(
2945
+ output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
2946
+ ).size;
2935
2947
  }
2936
2948
 
2937
2949
  // src/system/detect.ts
2938
- import { exec as exec2 } from "child_process";
2950
+ import { execFile as execFile2 } from "child_process";
2951
+ import process3 from "process";
2939
2952
  import { promisify as promisify2 } from "util";
2940
- var execAsync2 = promisify2(exec2);
2941
- async function detectSystemDeps() {
2942
- const ALLOWED_COMMANDS = /* @__PURE__ */ new Set(["bash", "git", "jq", "node"]);
2943
- const check = async (cmd) => {
2944
- if (!ALLOWED_COMMANDS.has(cmd)) {
2945
- throw new Error(`Unsupported dependency check: ${cmd}`);
2946
- }
2947
- try {
2948
- await execAsync2(`command -v ${cmd}`, { shell: "/bin/bash" });
2949
- return true;
2950
- } catch {
2951
- return false;
2952
- }
2953
- };
2954
- const [bash, git, jq, node] = await Promise.all([
2955
- check("bash"),
2956
- check("git"),
2957
- check("jq"),
2958
- check("node")
2959
- ]);
2960
- return { bash, git, jq, node };
2953
+ var execFileAsync2 = promisify2(execFile2);
2954
+ async function detectSystemDeps(env = process3.env) {
2955
+ try {
2956
+ await execFileAsync2("git", ["--version"], {
2957
+ env,
2958
+ windowsHide: true
2959
+ });
2960
+ return { git: true };
2961
+ } catch {
2962
+ return { git: false };
2963
+ }
2961
2964
  }
2962
2965
 
2963
2966
  // src/system/project-profile.ts
@@ -3653,7 +3656,7 @@ var EXIT_ALREADY_INITIALIZED = 1;
3653
3656
  var EXIT_NOT_A_PROJECT_DIR = 2;
3654
3657
  var EXIT_INIT_FAILED = 5;
3655
3658
  var DEFAULT_INIT_PLATFORM = "claude-code";
3656
- async function init(rootDir = process3.cwd(), options = {}) {
3659
+ async function init(rootDir = process4.cwd(), options = {}) {
3657
3660
  const mancodeDir = path13.join(rootDir, ".mancode");
3658
3661
  const stateFile = path13.join(mancodeDir, "state.json");
3659
3662
  const wasInitialized = await pathExists3(stateFile);
@@ -3702,20 +3705,12 @@ async function init(rootDir = process3.cwd(), options = {}) {
3702
3705
  try {
3703
3706
  console.log("\u2713 \u68C0\u6D4B\u7CFB\u7EDF\u4F9D\u8D56...");
3704
3707
  const deps = await detectSystemDeps();
3705
- if (!deps.bash || !deps.git) {
3706
- console.error("\u2717 Missing required dependencies:");
3707
- if (!deps.bash) console.error(" - bash (required)");
3708
- if (!deps.git) console.error(" - git (required)");
3709
- console.error("");
3710
- console.error(" Install them and try again.");
3711
- return EXIT_NOT_A_PROJECT_DIR;
3712
- }
3713
- if (!deps.jq) {
3708
+ if (!deps.git) {
3714
3709
  console.log(
3715
- "\u26A0\uFE0F jq not found (optional). Hooks will use grep/sed fallback (slightly slower)."
3710
+ "\u26A0\uFE0F Git not found (optional). Team auto-detection will use solo defaults."
3716
3711
  );
3717
3712
  } else {
3718
- console.log(" bash \u2713 git \u2713 jq \u2713");
3713
+ console.log(" git \u2713");
3719
3714
  }
3720
3715
  console.log("\u2713 \u68C0\u6D4B\u9879\u76EE\u7C7B\u578B...");
3721
3716
  const profile = await detectProjectProfile(rootDir);
@@ -3996,7 +3991,7 @@ async function pathExists3(p) {
3996
3991
  // src/commands/install.ts
3997
3992
  import { promises as fs4 } from "fs";
3998
3993
  import path15 from "path";
3999
- import process4 from "process";
3994
+ import process5 from "process";
4000
3995
 
4001
3996
  // src/installers/platform-status.ts
4002
3997
  import { promises as fs3 } from "fs";
@@ -4019,8 +4014,8 @@ async function checkPlatformReadiness(rootDir, platform) {
4019
4014
  ),
4020
4015
  claudeHooksRegistered(rootDir),
4021
4016
  pathsExist([
4022
- path14.join(rootDir, ".mancode", "hooks", "session-start.sh"),
4023
- path14.join(rootDir, ".mancode", "hooks", "user-prompt-submit.sh")
4017
+ path14.join(rootDir, ".mancode", "hooks", "session-start.mjs"),
4018
+ path14.join(rootDir, ".mancode", "hooks", "user-prompt-submit.mjs")
4024
4019
  ])
4025
4020
  ]);
4026
4021
  const present = hasSoloSkill && registered && hasHookFiles;
@@ -4249,9 +4244,9 @@ function hooksRegistered(settings) {
4249
4244
  if (!isRecord4(settings)) return false;
4250
4245
  const hooks = settings.hooks;
4251
4246
  if (!isRecord4(hooks)) return false;
4252
- return hasHookCommand(hooks.SessionStart, ".mancode/hooks/session-start.sh") && hasHookCommand(
4247
+ return hasHookCommand(hooks.SessionStart, ".mancode/hooks/session-start.mjs") && hasHookCommand(
4253
4248
  hooks.UserPromptSubmit,
4254
- ".mancode/hooks/user-prompt-submit.sh"
4249
+ ".mancode/hooks/user-prompt-submit.mjs"
4255
4250
  );
4256
4251
  }
4257
4252
  function hasHookCommand(value, needle) {
@@ -4281,7 +4276,7 @@ var EXIT_OK2 = 0;
4281
4276
  var EXIT_NOT_INITIALIZED = 1;
4282
4277
  var EXIT_UNSUPPORTED_PLATFORM = 2;
4283
4278
  var EXIT_INSTALL_FAILED = 3;
4284
- async function install(rootDir = process4.cwd(), platform = "claude-code", options = {}) {
4279
+ async function install(rootDir = process5.cwd(), platform = "claude-code", options = {}) {
4285
4280
  const stateFile = path15.join(rootDir, ".mancode", "state.json");
4286
4281
  if (!await pathExists5(stateFile)) {
4287
4282
  console.error("\u2717 mancode not initialized.");
@@ -4443,9 +4438,9 @@ function isRecord5(value) {
4443
4438
  // src/commands/list-platforms.ts
4444
4439
  import { promises as fs5 } from "fs";
4445
4440
  import path16 from "path";
4446
- import process5 from "process";
4441
+ import process6 from "process";
4447
4442
  var EXIT_OK3 = 0;
4448
- async function listPlatforms(rootDir = process5.cwd()) {
4443
+ async function listPlatforms(rootDir = process6.cwd()) {
4449
4444
  const installed = new Set(await readInstalledPlatforms(rootDir));
4450
4445
  const platforms = getPlatformInstallers();
4451
4446
  console.log("");
@@ -4483,16 +4478,16 @@ function describePlatform(platform) {
4483
4478
  }
4484
4479
 
4485
4480
  // src/commands/manps.ts
4486
- import { access as access3 } from "fs/promises";
4481
+ import { access as access4 } from "fs/promises";
4487
4482
  import path18 from "path";
4488
4483
  import { createInterface } from "readline/promises";
4489
4484
 
4490
4485
  // src/system/preseason.ts
4491
- import { execFile } from "child_process";
4486
+ import { execFile as execFile3 } from "child_process";
4492
4487
  import { createHash } from "crypto";
4493
4488
  import { existsSync, readFileSync } from "fs";
4494
4489
  import {
4495
- access as access2,
4490
+ access as access3,
4496
4491
  lstat,
4497
4492
  mkdir as mkdir7,
4498
4493
  readFile as readFile8,
@@ -5052,7 +5047,7 @@ function architectureScannerUnavailableIssue() {
5052
5047
  function runDepcruise(binary, projectRoot) {
5053
5048
  return new Promise((resolve) => {
5054
5049
  const needsShell = process.platform === "win32";
5055
- execFile(
5050
+ execFile3(
5056
5051
  binary,
5057
5052
  ["--output-type", "json", "."],
5058
5053
  {
@@ -5392,7 +5387,7 @@ function pathExistsSync(file) {
5392
5387
  }
5393
5388
  async function pathExists6(file) {
5394
5389
  try {
5395
- await access2(file);
5390
+ await access3(file);
5396
5391
  return true;
5397
5392
  } catch {
5398
5393
  return false;
@@ -5555,7 +5550,7 @@ async function readStdinLines() {
5555
5550
  }
5556
5551
  async function pathExists7(p) {
5557
5552
  try {
5558
- await access3(p);
5553
+ await access4(p);
5559
5554
  return true;
5560
5555
  } catch {
5561
5556
  return false;
@@ -5565,10 +5560,10 @@ async function pathExists7(p) {
5565
5560
  // src/commands/refresh-style.ts
5566
5561
  import { promises as fs6 } from "fs";
5567
5562
  import path19 from "path";
5568
- import process6 from "process";
5563
+ import process7 from "process";
5569
5564
  var EXIT_OK5 = 0;
5570
5565
  var EXIT_NOT_INITIALIZED3 = 1;
5571
- async function refreshStyle(rootDir = process6.cwd()) {
5566
+ async function refreshStyle(rootDir = process7.cwd()) {
5572
5567
  const stateFile = path19.join(rootDir, ".mancode", "state.json");
5573
5568
  if (!await pathExists8(stateFile)) {
5574
5569
  console.error("\u2717 mancode not initialized.");
@@ -5718,24 +5713,220 @@ async function pathExists8(p) {
5718
5713
  // src/commands/status.ts
5719
5714
  import { spawn } from "child_process";
5720
5715
  import { promises as fs7 } from "fs";
5721
- import path21 from "path";
5722
- import process7 from "process";
5716
+ import path22 from "path";
5717
+ import process8 from "process";
5723
5718
 
5724
5719
  // src/system/workflow.ts
5725
5720
  import {
5726
- mkdir as mkdir8,
5727
- readFile as readFile9,
5721
+ mkdir as mkdir9,
5722
+ readFile as readFile10,
5728
5723
  readdir as readdir5,
5729
5724
  rm as rm4,
5730
- stat as stat3,
5731
- writeFile as writeFile10
5725
+ stat as stat4,
5726
+ writeFile as writeFile11
5732
5727
  } from "fs/promises";
5728
+ import path21 from "path";
5729
+
5730
+ // src/system/review-ledger.ts
5731
+ import { mkdir as mkdir8, readFile as readFile9, stat as stat3, writeFile as writeFile10 } from "fs/promises";
5733
5732
  import path20 from "path";
5733
+ var REVIEW_FILE = "review-ledger.json";
5734
+ var TASK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
5735
+ var BLOCKER_ID_PATTERN = /^[A-Z][A-Z0-9-]{0,31}$/;
5736
+ function isReviewDepth(value) {
5737
+ return value === "targeted" || value === "full";
5738
+ }
5739
+ function isReviewDomain(value) {
5740
+ return value === "quality" || value === "security";
5741
+ }
5742
+ async function initializeReview(projectRoot, taskId, depth, targetDomain) {
5743
+ assertValidTaskId(taskId);
5744
+ const existing = await readReviewLedger(projectRoot, taskId);
5745
+ if (existing) throw new Error(`review already initialized: ${taskId}`);
5746
+ if (depth === "targeted" && !targetDomain) {
5747
+ throw new Error("targeted review requires a review domain");
5748
+ }
5749
+ const ledger = {
5750
+ version: "1.0",
5751
+ depth,
5752
+ requiredDomains: depth === "full" ? ["quality", "security"] : [targetDomain],
5753
+ completedDomains: [],
5754
+ reports: {},
5755
+ blockers: [],
5756
+ remediationRounds: 0
5757
+ };
5758
+ await writeReviewLedger(projectRoot, taskId, ledger);
5759
+ return ledger;
5760
+ }
5761
+ async function completeReviewDomain(projectRoot, taskId, domain, report, blockerIds) {
5762
+ const ledger = await requireReviewLedger(projectRoot, taskId);
5763
+ if (!ledger.requiredDomains.includes(domain)) {
5764
+ throw new Error(`review domain is not required: ${domain}`);
5765
+ }
5766
+ if (ledger.completedDomains.includes(domain)) {
5767
+ throw new Error(`review domain already completed: ${domain}`);
5768
+ }
5769
+ if (domain === "security" && !ledger.completedDomains.includes("quality")) {
5770
+ throw new Error("quality review must complete first");
5771
+ }
5772
+ assertSafeReportPath(report);
5773
+ await assertReportExists(projectRoot, taskId, report);
5774
+ const uniqueIds = new Set(blockerIds);
5775
+ if (uniqueIds.size !== blockerIds.length) {
5776
+ throw new Error("review blocker ids must be unique");
5777
+ }
5778
+ for (const id of blockerIds) {
5779
+ if (!BLOCKER_ID_PATTERN.test(id)) {
5780
+ throw new Error(`invalid review blocker id: ${id}`);
5781
+ }
5782
+ if (ledger.blockers.some((blocker) => blocker.id === id)) {
5783
+ throw new Error(`duplicate review blocker id: ${id}`);
5784
+ }
5785
+ }
5786
+ const updated = {
5787
+ ...ledger,
5788
+ completedDomains: [...ledger.completedDomains, domain],
5789
+ reports: { ...ledger.reports, [domain]: report },
5790
+ blockers: [
5791
+ ...ledger.blockers,
5792
+ ...blockerIds.map((id) => ({
5793
+ id,
5794
+ domain,
5795
+ status: "open"
5796
+ }))
5797
+ ]
5798
+ };
5799
+ await writeReviewLedger(projectRoot, taskId, updated);
5800
+ return updated;
5801
+ }
5802
+ async function remediateReviewBlockers(projectRoot, taskId, resolvedIds) {
5803
+ const ledger = await requireReviewLedger(projectRoot, taskId);
5804
+ if (ledger.remediationRounds >= 1) {
5805
+ throw new Error("review allows only one remediation round");
5806
+ }
5807
+ if (resolvedIds.length === 0) {
5808
+ throw new Error("review remediation requires resolved blocker ids");
5809
+ }
5810
+ const uniqueIds = new Set(resolvedIds);
5811
+ if (uniqueIds.size !== resolvedIds.length) {
5812
+ throw new Error("resolved review blocker ids must be unique");
5813
+ }
5814
+ for (const id of resolvedIds) {
5815
+ const blocker = ledger.blockers.find((item) => item.id === id);
5816
+ if (!blocker || blocker.status !== "open") {
5817
+ throw new Error(`review blocker is not open: ${id}`);
5818
+ }
5819
+ }
5820
+ const updated = {
5821
+ ...ledger,
5822
+ remediationRounds: ledger.remediationRounds + 1,
5823
+ blockers: ledger.blockers.map(
5824
+ (blocker) => uniqueIds.has(blocker.id) ? { ...blocker, status: "resolved" } : blocker
5825
+ )
5826
+ };
5827
+ await writeReviewLedger(projectRoot, taskId, updated);
5828
+ return updated;
5829
+ }
5830
+ async function reviewCanComplete(projectRoot, taskId) {
5831
+ const ledger = await readReviewLedger(projectRoot, taskId);
5832
+ if (!ledger) return false;
5833
+ return ledger.requiredDomains.every(
5834
+ (domain) => ledger.completedDomains.includes(domain)
5835
+ ) && ledger.blockers.every((blocker) => blocker.status === "resolved");
5836
+ }
5837
+ async function readReviewLedger(projectRoot, taskId) {
5838
+ if (!TASK_ID_PATTERN.test(taskId)) return null;
5839
+ try {
5840
+ const raw = await readFile9(reviewPath(projectRoot, taskId), "utf-8");
5841
+ const value = JSON.parse(raw);
5842
+ return isReviewLedger(value) ? value : null;
5843
+ } catch {
5844
+ return null;
5845
+ }
5846
+ }
5847
+ function isReviewLedger(value) {
5848
+ if (!isRecord7(value)) return false;
5849
+ if (value.version !== "1.0" || !isReviewDepth(value.depth) || !isDomainArray(value.requiredDomains) || !isDomainArray(value.completedDomains) || !isRecord7(value.reports) || !Array.isArray(value.blockers) || value.remediationRounds !== 0 && value.remediationRounds !== 1) {
5850
+ return false;
5851
+ }
5852
+ const requiredDomains = value.requiredDomains;
5853
+ const completedDomains = value.completedDomains;
5854
+ const reports = value.reports;
5855
+ const blockers = value.blockers;
5856
+ const expectedDomains = value.depth === "full" ? ["quality", "security"] : requiredDomains;
5857
+ if (value.depth === "targeted" && requiredDomains.length !== 1 || requiredDomains.length !== expectedDomains.length || !expectedDomains.every((domain) => requiredDomains.includes(domain)) || new Set(completedDomains).size !== completedDomains.length || completedDomains.some((domain) => !requiredDomains.includes(domain)) || completedDomains.some((domain) => {
5858
+ const report = reports[domain];
5859
+ return typeof report !== "string" || !isSafeReportPath(report);
5860
+ })) {
5861
+ return false;
5862
+ }
5863
+ const blockerIds = /* @__PURE__ */ new Set();
5864
+ return blockers.every(
5865
+ (blocker) => isRecord7(blocker) && typeof blocker.id === "string" && BLOCKER_ID_PATTERN.test(blocker.id) && !blockerIds.has(blocker.id) && blockerIds.add(blocker.id) && isReviewDomain(blocker.domain) && completedDomains.includes(blocker.domain) && (blocker.status === "open" || blocker.status === "resolved")
5866
+ );
5867
+ }
5868
+ function isDomainArray(value) {
5869
+ return Array.isArray(value) && value.every(isReviewDomain);
5870
+ }
5871
+ function isRecord7(value) {
5872
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5873
+ }
5874
+ async function requireReviewLedger(projectRoot, taskId) {
5875
+ assertValidTaskId(taskId);
5876
+ const ledger = await readReviewLedger(projectRoot, taskId);
5877
+ if (!ledger) throw new Error(`review not initialized: ${taskId}`);
5878
+ return ledger;
5879
+ }
5880
+ async function writeReviewLedger(projectRoot, taskId, ledger) {
5881
+ const dir = path20.join(projectRoot, ".mancode", "workflows", taskId);
5882
+ await mkdir8(dir, { recursive: true });
5883
+ await writeFile10(
5884
+ path20.join(dir, REVIEW_FILE),
5885
+ `${JSON.stringify(ledger, null, 2)}
5886
+ `,
5887
+ "utf-8"
5888
+ );
5889
+ }
5890
+ function reviewPath(projectRoot, taskId) {
5891
+ return path20.join(projectRoot, ".mancode", "workflows", taskId, REVIEW_FILE);
5892
+ }
5893
+ function assertValidTaskId(taskId) {
5894
+ if (!TASK_ID_PATTERN.test(taskId)) {
5895
+ throw new Error(`invalid workflow taskId: ${taskId}`);
5896
+ }
5897
+ }
5898
+ function assertSafeReportPath(report) {
5899
+ if (!isSafeReportPath(report)) {
5900
+ throw new Error(`invalid review report path: ${report}`);
5901
+ }
5902
+ }
5903
+ async function assertReportExists(projectRoot, taskId, report) {
5904
+ const reportPath = path20.join(
5905
+ projectRoot,
5906
+ ".mancode",
5907
+ "workflows",
5908
+ taskId,
5909
+ report
5910
+ );
5911
+ try {
5912
+ if ((await stat3(reportPath)).isFile()) return;
5913
+ } catch {
5914
+ }
5915
+ throw new Error(`review report not found: ${report}`);
5916
+ }
5917
+ function isSafeReportPath(report) {
5918
+ const normalized = path20.posix.normalize(report.replaceAll("\\", "/"));
5919
+ return Boolean(
5920
+ report.trim() && !path20.isAbsolute(report) && normalized !== ".." && !normalized.startsWith("../") && normalized.endsWith(".md")
5921
+ );
5922
+ }
5923
+
5924
+ // src/system/workflow.ts
5734
5925
  var METADATA_FILE = "metadata.json";
5735
5926
  var SLUG_MAX = 30;
5736
- var TASK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
5927
+ var TASK_ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
5737
5928
  function isValidWorkflowTaskId(taskId) {
5738
- return TASK_ID_PATTERN.test(taskId);
5929
+ return TASK_ID_PATTERN2.test(taskId);
5739
5930
  }
5740
5931
  function generateTaskId(task, now = /* @__PURE__ */ new Date()) {
5741
5932
  const slug = slugify(task).slice(0, SLUG_MAX) || "task";
@@ -5748,7 +5939,7 @@ function slugify(text) {
5748
5939
  }
5749
5940
  async function createWorkflow(projectRoot, task, mode, options = {}) {
5750
5941
  await validateParentTask(projectRoot, mode, options.parentTaskId, true);
5751
- await mkdir8(workflowsRoot(projectRoot), { recursive: true });
5942
+ await mkdir9(workflowsRoot(projectRoot), { recursive: true });
5752
5943
  const taskId = await allocateTaskId(projectRoot, generateTaskId(task));
5753
5944
  const dir = workflowDir(projectRoot, taskId);
5754
5945
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -5762,7 +5953,7 @@ async function createWorkflow(projectRoot, task, mode, options = {}) {
5762
5953
  updatedAt: now,
5763
5954
  status: "in_progress",
5764
5955
  ...options.parentTaskId ? { parentTaskId: options.parentTaskId } : {},
5765
- ...mode === "man" || mode === "manteam" ? { planVersion: 1 } : {}
5956
+ ...mode === "man" || mode === "manteam" ? { planVersion: 1, reviewPolicyVersion: 1 } : {}
5766
5957
  };
5767
5958
  await writeMetadata(dir, meta);
5768
5959
  return meta;
@@ -5771,7 +5962,7 @@ async function allocateTaskId(projectRoot, baseTaskId) {
5771
5962
  for (let attempt = 0; attempt < 1e3; attempt++) {
5772
5963
  const taskId = attempt === 0 ? baseTaskId : `${baseTaskId}-${attempt + 1}`;
5773
5964
  try {
5774
- await mkdir8(workflowDir(projectRoot, taskId));
5965
+ await mkdir9(workflowDir(projectRoot, taskId));
5775
5966
  return taskId;
5776
5967
  } catch (err) {
5777
5968
  if (isNodeError6(err) && err.code === "EEXIST") {
@@ -5786,14 +5977,14 @@ async function readWorkflow(projectRoot, taskId) {
5786
5977
  if (!isValidWorkflowTaskId(taskId)) return null;
5787
5978
  const file = metadataPath(projectRoot, taskId);
5788
5979
  try {
5789
- const raw = await readFile9(file, "utf-8");
5980
+ const raw = await readFile10(file, "utf-8");
5790
5981
  return parseWorkflowMeta(raw, taskId);
5791
5982
  } catch {
5792
5983
  return null;
5793
5984
  }
5794
5985
  }
5795
5986
  async function updateWorkflow(projectRoot, taskId, patch) {
5796
- assertValidTaskId(taskId);
5987
+ assertValidTaskId2(taskId);
5797
5988
  const existing = await readWorkflow(projectRoot, taskId);
5798
5989
  if (!existing) {
5799
5990
  throw new Error(`workflow not found: ${taskId}`);
@@ -5838,7 +6029,7 @@ async function deleteWorkflow(projectRoot, taskId) {
5838
6029
  if (!isValidWorkflowTaskId(taskId)) return false;
5839
6030
  const dir = workflowDir(projectRoot, taskId);
5840
6031
  try {
5841
- await stat3(dir);
6032
+ await stat4(dir);
5842
6033
  } catch {
5843
6034
  return false;
5844
6035
  }
@@ -5850,19 +6041,19 @@ async function deleteWorkflow(projectRoot, taskId) {
5850
6041
  return true;
5851
6042
  }
5852
6043
  function workflowsRoot(projectRoot) {
5853
- return path20.join(projectRoot, ".mancode", "workflows");
6044
+ return path21.join(projectRoot, ".mancode", "workflows");
5854
6045
  }
5855
6046
  function workflowDir(projectRoot, taskId) {
5856
- assertValidTaskId(taskId);
5857
- return path20.join(workflowsRoot(projectRoot), taskId);
6047
+ assertValidTaskId2(taskId);
6048
+ return path21.join(workflowsRoot(projectRoot), taskId);
5858
6049
  }
5859
6050
  function metadataPath(projectRoot, taskId) {
5860
- return path20.join(workflowDir(projectRoot, taskId), METADATA_FILE);
6051
+ return path21.join(workflowDir(projectRoot, taskId), METADATA_FILE);
5861
6052
  }
5862
6053
  async function writeMetadata(dir, meta) {
5863
6054
  const content = `${JSON.stringify(meta, null, 2)}
5864
6055
  `;
5865
- await writeFile10(path20.join(dir, METADATA_FILE), content, "utf-8");
6056
+ await writeFile11(path21.join(dir, METADATA_FILE), content, "utf-8");
5866
6057
  }
5867
6058
  function parseWorkflowMeta(raw, taskId) {
5868
6059
  try {
@@ -5912,6 +6103,9 @@ async function validateWorkflowMeta(projectRoot, updated, existing) {
5912
6103
  if (updated.parentTaskId !== existing.parentTaskId) {
5913
6104
  throw new Error("workflow parent task cannot be changed");
5914
6105
  }
6106
+ if (updated.reviewPolicyVersion !== existing.reviewPolicyVersion) {
6107
+ throw new Error("workflow review policy version cannot be changed");
6108
+ }
5915
6109
  if (!Number.isInteger(updated.currentStep) || updated.currentStep < 1 || updated.currentStep > maxWorkflowStep(updated.mode)) {
5916
6110
  throw new Error(`invalid workflow step: ${updated.currentStep}`);
5917
6111
  }
@@ -5962,6 +6156,12 @@ async function validateWorkflowMeta(projectRoot, updated, existing) {
5962
6156
  if (updated.mode === "mamba" && updated.planVersion !== void 0) {
5963
6157
  throw new Error("mamba workflows cannot have a plan version");
5964
6158
  }
6159
+ if (updated.reviewPolicyVersion !== void 0 && updated.reviewPolicyVersion !== 1) {
6160
+ throw new Error("invalid workflow review policy version");
6161
+ }
6162
+ if (updated.mode === "mamba" && updated.reviewPolicyVersion !== void 0) {
6163
+ throw new Error("mamba workflows cannot have a review policy version");
6164
+ }
5965
6165
  if (updated.planVersion !== void 0 && (!Number.isInteger(updated.planVersion) || updated.planVersion < 1)) {
5966
6166
  throw new Error("workflow plan version must be a positive integer");
5967
6167
  }
@@ -5975,6 +6175,9 @@ async function validateWorkflowMeta(projectRoot, updated, existing) {
5975
6175
  if ((isTerminalWorkflowStatus(updated.status) || updated.status === "planned") && (await listActiveMambaChildren(projectRoot, updated.taskId)).length > 0) {
5976
6176
  throw new Error("cannot finish workflow with an active mamba child");
5977
6177
  }
6178
+ if (updated.status === "completed" && updated.reviewPolicyVersion === 1 && !reviewWasExplicitlySkipped(updated) && !await reviewCanComplete(projectRoot, updated.taskId)) {
6179
+ throw new Error("workflow review is incomplete or still has open blockers");
6180
+ }
5978
6181
  }
5979
6182
  function canTransition(from, to) {
5980
6183
  if (from === to) return true;
@@ -6002,13 +6205,16 @@ function isWorkflowShape(obj) {
6002
6205
  return false;
6003
6206
  if (obj.planVersion !== void 0 && (!Number.isInteger(obj.planVersion) || obj.planVersion < 1))
6004
6207
  return false;
6208
+ if (obj.reviewPolicyVersion !== void 0 && obj.reviewPolicyVersion !== 1)
6209
+ return false;
6005
6210
  if (status2 === "blocked") {
6006
6211
  if (!obj.blockingReason?.trim()) return false;
6007
6212
  } else if (obj.blockingReason !== void 0) {
6008
6213
  return false;
6009
6214
  }
6010
6215
  if (mode === "mamba") {
6011
- if (status2 === "planned" || obj.planVersion !== void 0) return false;
6216
+ if (status2 === "planned" || obj.planVersion !== void 0 || obj.reviewPolicyVersion !== void 0)
6217
+ return false;
6012
6218
  if (obj.parentTaskId !== void 0 && !isValidWorkflowTaskId(obj.parentTaskId))
6013
6219
  return false;
6014
6220
  if (obj.outcome !== void 0 && status2 !== "completed") return false;
@@ -6018,6 +6224,9 @@ function isWorkflowShape(obj) {
6018
6224
  }
6019
6225
  return true;
6020
6226
  }
6227
+ function reviewWasExplicitlySkipped(meta) {
6228
+ return meta.skippedSteps.includes("review") || meta.skippedSteps.includes("film-1") && meta.skippedSteps.includes("film-2");
6229
+ }
6021
6230
  async function listActiveMambaChildren(projectRoot, parentTaskId) {
6022
6231
  return (await listMambaChildren(projectRoot, parentTaskId)).filter(
6023
6232
  (workflow2) => workflow2.status === "in_progress" || workflow2.status === "blocked"
@@ -6053,7 +6262,7 @@ async function propagateChildStatus(projectRoot, child) {
6053
6262
  function isNodeError6(err) {
6054
6263
  return err instanceof Error && "code" in err;
6055
6264
  }
6056
- function assertValidTaskId(taskId) {
6265
+ function assertValidTaskId2(taskId) {
6057
6266
  if (!isValidWorkflowTaskId(taskId)) {
6058
6267
  throw new Error(`invalid workflow taskId: ${taskId}`);
6059
6268
  }
@@ -6064,8 +6273,8 @@ var HOOK_ESTIMATE_TIMEOUT_MS = 2e3;
6064
6273
  var EXIT_OK6 = 0;
6065
6274
  var EXIT_NOT_INITIALIZED4 = 1;
6066
6275
  var EXIT_CORRUPT_STATE = 2;
6067
- async function status(rootDir = process7.cwd(), options = {}) {
6068
- const stateFile = path21.join(rootDir, ".mancode", "state.json");
6276
+ async function status(rootDir = process8.cwd(), options = {}) {
6277
+ const stateFile = path22.join(rootDir, ".mancode", "state.json");
6069
6278
  if (!await pathExists9(stateFile)) {
6070
6279
  console.error("\u2717 mancode not initialized.");
6071
6280
  console.error(" Run `mancode init` to get started.");
@@ -6164,19 +6373,19 @@ async function getCurrentWorkflow(rootDir, taskId) {
6164
6373
  }
6165
6374
  async function getProjectName(rootDir) {
6166
6375
  try {
6167
- const raw = await fs7.readFile(path21.join(rootDir, "package.json"), "utf-8");
6376
+ const raw = await fs7.readFile(path22.join(rootDir, "package.json"), "utf-8");
6168
6377
  const pkg = JSON.parse(raw);
6169
6378
  if (pkg.name && typeof pkg.name === "string") {
6170
6379
  return pkg.name;
6171
6380
  }
6172
6381
  } catch {
6173
6382
  }
6174
- return path21.basename(rootDir);
6383
+ return path22.basename(rootDir);
6175
6384
  }
6176
6385
  async function readConfig2(rootDir) {
6177
6386
  try {
6178
6387
  const raw = await fs7.readFile(
6179
- path21.join(rootDir, ".mancode", "config.json"),
6388
+ path22.join(rootDir, ".mancode", "config.json"),
6180
6389
  "utf-8"
6181
6390
  );
6182
6391
  return JSON.parse(raw);
@@ -6203,9 +6412,9 @@ function getEffectiveTeamStatus(state, config, detected) {
6203
6412
  }
6204
6413
  async function checkHooks(rootDir) {
6205
6414
  const [sessionStart, userPromptSubmit, registered] = await Promise.all([
6206
- pathExists9(path21.join(rootDir, ".mancode", "hooks", "session-start.sh")),
6415
+ pathExists9(path22.join(rootDir, ".mancode", "hooks", "session-start.mjs")),
6207
6416
  pathExists9(
6208
- path21.join(rootDir, ".mancode", "hooks", "user-prompt-submit.sh")
6417
+ path22.join(rootDir, ".mancode", "hooks", "user-prompt-submit.mjs")
6209
6418
  ),
6210
6419
  isRegistered(rootDir)
6211
6420
  ]);
@@ -6214,7 +6423,7 @@ async function checkHooks(rootDir) {
6214
6423
  async function isRegistered(rootDir) {
6215
6424
  try {
6216
6425
  const raw = await fs7.readFile(
6217
- path21.join(rootDir, ".claude", "settings.json"),
6426
+ path22.join(rootDir, ".claude", "settings.json"),
6218
6427
  "utf-8"
6219
6428
  );
6220
6429
  const settings = JSON.parse(raw);
@@ -6224,30 +6433,30 @@ async function isRegistered(rootDir) {
6224
6433
  }
6225
6434
  }
6226
6435
  function hooksRegistered2(settings) {
6227
- if (!isRecord7(settings)) return false;
6436
+ if (!isRecord8(settings)) return false;
6228
6437
  const hooks = settings.hooks;
6229
- if (!isRecord7(hooks)) return false;
6230
- return hasHookCommand2(hooks.SessionStart, ".mancode/hooks/session-start.sh") && hasHookCommand2(
6438
+ if (!isRecord8(hooks)) return false;
6439
+ return hasHookCommand2(hooks.SessionStart, ".mancode/hooks/session-start.mjs") && hasHookCommand2(
6231
6440
  hooks.UserPromptSubmit,
6232
- ".mancode/hooks/user-prompt-submit.sh"
6441
+ ".mancode/hooks/user-prompt-submit.mjs"
6233
6442
  );
6234
6443
  }
6235
6444
  function hasHookCommand2(value, needle) {
6236
6445
  if (!Array.isArray(value)) return false;
6237
6446
  return value.some((group) => {
6238
- if (!isRecord7(group) || !Array.isArray(group.hooks)) return false;
6447
+ if (!isRecord8(group) || !Array.isArray(group.hooks)) return false;
6239
6448
  return group.hooks.some((hook) => {
6240
- if (!isRecord7(hook) || typeof hook.command !== "string") return false;
6449
+ if (!isRecord8(hook) || typeof hook.command !== "string") return false;
6241
6450
  return hook.command.includes(needle);
6242
6451
  });
6243
6452
  });
6244
6453
  }
6245
6454
  async function estimateHookInjection(rootDir) {
6246
- const hookPath = path21.join(
6455
+ const hookPath = path22.join(
6247
6456
  rootDir,
6248
6457
  ".mancode",
6249
6458
  "hooks",
6250
- "user-prompt-submit.sh"
6459
+ "user-prompt-submit.mjs"
6251
6460
  );
6252
6461
  if (!await pathExists9(hookPath)) {
6253
6462
  return { tokens: 0, cap: 800 };
@@ -6264,7 +6473,7 @@ async function estimateHookInjection(rootDir) {
6264
6473
  }
6265
6474
  function runHookEstimate(rootDir, hookPath) {
6266
6475
  return new Promise((resolve, reject) => {
6267
- const child = spawn("bash", [hookPath], {
6476
+ const child = spawn(process8.execPath, [hookPath], {
6268
6477
  cwd: rootDir,
6269
6478
  stdio: ["pipe", "pipe", "ignore"]
6270
6479
  });
@@ -6348,9 +6557,9 @@ function printText(r) {
6348
6557
  console.log("");
6349
6558
  if (r.platforms.includes("claude-code")) {
6350
6559
  console.log("Hooks:");
6351
- console.log(` ${r.hooks.sessionStart ? "\u2713" : "\u2717"} session-start.sh`);
6560
+ console.log(` ${r.hooks.sessionStart ? "\u2713" : "\u2717"} session-start.mjs`);
6352
6561
  console.log(
6353
- ` ${r.hooks.userPromptSubmit ? "\u2713" : "\u2717"} user-prompt-submit.sh`
6562
+ ` ${r.hooks.userPromptSubmit ? "\u2713" : "\u2717"} user-prompt-submit.mjs`
6354
6563
  );
6355
6564
  console.log(
6356
6565
  ` ${r.hooks.registered ? "\u2713" : "\u2717"} registered in .claude/settings.json`
@@ -6369,7 +6578,7 @@ function formatTeamStatus(team) {
6369
6578
  function workflowStepMax(mode) {
6370
6579
  return maxWorkflowStep(mode);
6371
6580
  }
6372
- function isRecord7(value) {
6581
+ function isRecord8(value) {
6373
6582
  return typeof value === "object" && value !== null;
6374
6583
  }
6375
6584
  async function pathExists9(p) {
@@ -6382,14 +6591,14 @@ async function pathExists9(p) {
6382
6591
  }
6383
6592
 
6384
6593
  // src/commands/uninstall.ts
6385
- import { access as access4, readFile as readFile10, rm as rm5, writeFile as writeFile11 } from "fs/promises";
6386
- import path22 from "path";
6387
- import process8 from "process";
6594
+ import { access as access5, readFile as readFile11, rm as rm5, writeFile as writeFile12 } from "fs/promises";
6595
+ import path23 from "path";
6596
+ import process9 from "process";
6388
6597
  var EXIT_OK7 = 0;
6389
6598
  var EXIT_NOT_INITIALIZED5 = 1;
6390
6599
  var EXIT_UNSUPPORTED_PLATFORM2 = 2;
6391
- async function uninstall(rootDir = process8.cwd(), platform, options = {}) {
6392
- const stateFile = path22.join(rootDir, ".mancode", "state.json");
6600
+ async function uninstall(rootDir = process9.cwd(), platform, options = {}) {
6601
+ const stateFile = path23.join(rootDir, ".mancode", "state.json");
6393
6602
  if (!await pathExists10(stateFile)) {
6394
6603
  console.error("\u2717 mancode not initialized.");
6395
6604
  console.error(" Run `mancode init` first.");
@@ -6441,7 +6650,7 @@ async function uninstallAll(rootDir) {
6441
6650
  await uninstallPlatform(rootDir, p);
6442
6651
  }
6443
6652
  console.log("\u2713 Removing .mancode/ directory...");
6444
- await rm5(path22.join(rootDir, ".mancode"), {
6653
+ await rm5(path23.join(rootDir, ".mancode"), {
6445
6654
  recursive: true,
6446
6655
  force: true
6447
6656
  });
@@ -6451,10 +6660,10 @@ async function uninstallClaudeCode(rootDir) {
6451
6660
  await cleanClaudeSettings(rootDir);
6452
6661
  }
6453
6662
  async function cleanClaudeSettings(rootDir) {
6454
- const settingsPath = path22.join(rootDir, ".claude", "settings.json");
6663
+ const settingsPath = path23.join(rootDir, ".claude", "settings.json");
6455
6664
  let content;
6456
6665
  try {
6457
- content = await readFile10(settingsPath, "utf-8");
6666
+ content = await readFile11(settingsPath, "utf-8");
6458
6667
  } catch {
6459
6668
  return;
6460
6669
  }
@@ -6465,14 +6674,14 @@ async function cleanClaudeSettings(rootDir) {
6465
6674
  return;
6466
6675
  }
6467
6676
  const cleanedHooks = {};
6468
- if (isRecord8(settings.hooks)) {
6677
+ if (isRecord9(settings.hooks)) {
6469
6678
  for (const [event, value] of Object.entries(settings.hooks)) {
6470
6679
  const cleaned = cleanClaudeHookValue(value);
6471
6680
  if (cleaned !== void 0) cleanedHooks[event] = cleaned;
6472
6681
  }
6473
6682
  settings.hooks = Object.keys(cleanedHooks).length > 0 ? cleanedHooks : void 0;
6474
6683
  }
6475
- if (isRecord8(settings.skills)) {
6684
+ if (isRecord9(settings.skills)) {
6476
6685
  const retainedSkills = Object.fromEntries(
6477
6686
  Object.entries(settings.skills).filter(
6478
6687
  ([name, value]) => LEGACY_CLAUDE_SKILL_SETTINGS[name] !== value
@@ -6480,7 +6689,7 @@ async function cleanClaudeSettings(rootDir) {
6480
6689
  );
6481
6690
  settings.skills = Object.keys(retainedSkills).length > 0 ? retainedSkills : void 0;
6482
6691
  }
6483
- await writeFile11(
6692
+ await writeFile12(
6484
6693
  settingsPath,
6485
6694
  `${JSON.stringify(settings, null, 2)}
6486
6695
  `,
@@ -6492,7 +6701,7 @@ function cleanClaudeHookValue(value) {
6492
6701
  const entries2 = value.flatMap(cleanClaudeHookEntry);
6493
6702
  return entries2.length > 0 ? entries2 : void 0;
6494
6703
  }
6495
- if (!isRecord8(value)) return value;
6704
+ if (!isRecord9(value)) return value;
6496
6705
  if (Array.isArray(value.hooks) || typeof value.command === "string") {
6497
6706
  const entries2 = cleanClaudeHookEntry(value);
6498
6707
  return entries2.length > 0 ? entries2 : void 0;
@@ -6506,7 +6715,7 @@ function cleanClaudeHookValue(value) {
6506
6715
  }
6507
6716
  function cleanClaudeHookEntry(entry) {
6508
6717
  if (Array.isArray(entry)) return entry.flatMap(cleanClaudeHookEntry);
6509
- if (!isRecord8(entry)) return [entry];
6718
+ if (!isRecord9(entry)) return [entry];
6510
6719
  if (Array.isArray(entry.hooks)) {
6511
6720
  const hooks = entry.hooks.filter((hook) => !isMancodeHookEntry(hook));
6512
6721
  return hooks.length > 0 ? [{ ...entry, hooks }] : [];
@@ -6514,11 +6723,11 @@ function cleanClaudeHookEntry(entry) {
6514
6723
  return isMancodeHookEntry(entry) ? [] : [entry];
6515
6724
  }
6516
6725
  function isMancodeHookEntry(value) {
6517
- return isRecord8(value) && typeof value.command === "string" && isGeneratedMancodeHookCommand(value.command);
6726
+ return isRecord9(value) && typeof value.command === "string" && isGeneratedMancodeHookCommand(value.command);
6518
6727
  }
6519
6728
  function containsLegacyMancodeHookPath2(value) {
6520
6729
  if (Array.isArray(value)) return value.some(containsLegacyMancodeHookPath2);
6521
- if (!isRecord8(value)) return false;
6730
+ if (!isRecord9(value)) return false;
6522
6731
  if (typeof value.command === "string" && value.command.includes(".mancode/hooks/")) {
6523
6732
  return true;
6524
6733
  }
@@ -6529,12 +6738,12 @@ async function uninstallCursor(rootDir) {
6529
6738
  await removeCursorCommands(rootDir);
6530
6739
  }
6531
6740
  async function uninstallCodex(rootDir) {
6532
- const agentsPath = path22.join(rootDir, "AGENTS.md");
6741
+ const agentsPath = path23.join(rootDir, "AGENTS.md");
6533
6742
  try {
6534
- const content = await readFile10(agentsPath, "utf-8");
6743
+ const content = await readFile11(agentsPath, "utf-8");
6535
6744
  const cleaned = removeManagedBlock(content);
6536
6745
  if (cleaned.trim()) {
6537
- await writeFile11(agentsPath, `${cleaned}
6746
+ await writeFile12(agentsPath, `${cleaned}
6538
6747
  `, "utf-8");
6539
6748
  } else {
6540
6749
  await rm5(agentsPath, { force: true });
@@ -6544,16 +6753,16 @@ async function uninstallCodex(rootDir) {
6544
6753
  await removeCodexSkills(rootDir);
6545
6754
  }
6546
6755
  async function uninstallCopilot(rootDir) {
6547
- const instructionsPath = path22.join(
6756
+ const instructionsPath = path23.join(
6548
6757
  rootDir,
6549
6758
  ".github",
6550
6759
  "copilot-instructions.md"
6551
6760
  );
6552
6761
  try {
6553
- const content = await readFile10(instructionsPath, "utf-8");
6762
+ const content = await readFile11(instructionsPath, "utf-8");
6554
6763
  const cleaned = removeManagedBlock(content);
6555
6764
  if (cleaned.trim()) {
6556
- await writeFile11(instructionsPath, `${cleaned}
6765
+ await writeFile12(instructionsPath, `${cleaned}
6557
6766
  `, "utf-8");
6558
6767
  } else {
6559
6768
  await rm5(instructionsPath, { force: true });
@@ -6563,16 +6772,16 @@ async function uninstallCopilot(rootDir) {
6563
6772
  await removeCopilotPrompts(rootDir);
6564
6773
  }
6565
6774
  async function uninstallZcode(rootDir) {
6566
- const agentsPath = path22.join(rootDir, "AGENTS.md");
6775
+ const agentsPath = path23.join(rootDir, "AGENTS.md");
6567
6776
  try {
6568
- const content = await readFile10(agentsPath, "utf-8");
6777
+ const content = await readFile11(agentsPath, "utf-8");
6569
6778
  const cleaned = removeManagedBlock(
6570
6779
  content,
6571
6780
  ZCODE_MANCODE_START_MARKER,
6572
6781
  ZCODE_MANCODE_END_MARKER
6573
6782
  );
6574
6783
  if (cleaned.trim()) {
6575
- await writeFile11(agentsPath, `${cleaned}
6784
+ await writeFile12(agentsPath, `${cleaned}
6576
6785
  `, "utf-8");
6577
6786
  } else {
6578
6787
  await rm5(agentsPath, { force: true });
@@ -6582,13 +6791,13 @@ async function uninstallZcode(rootDir) {
6582
6791
  await removeZcodeSkills(rootDir);
6583
6792
  }
6584
6793
  async function removeFromConfig(rootDir, platform) {
6585
- const configPath = path22.join(rootDir, ".mancode", "config.json");
6794
+ const configPath = path23.join(rootDir, ".mancode", "config.json");
6586
6795
  try {
6587
- const raw = await readFile10(configPath, "utf-8");
6796
+ const raw = await readFile11(configPath, "utf-8");
6588
6797
  const config = JSON.parse(raw);
6589
6798
  if (Array.isArray(config.platforms)) {
6590
6799
  config.platforms = config.platforms.filter((p) => p !== platform);
6591
- if (isRecord8(config.platformOptions)) {
6800
+ if (isRecord9(config.platformOptions)) {
6592
6801
  const platformOptions = Object.fromEntries(
6593
6802
  Object.entries(config.platformOptions).filter(
6594
6803
  ([name]) => name !== platform
@@ -6596,7 +6805,7 @@ async function removeFromConfig(rootDir, platform) {
6596
6805
  );
6597
6806
  config.platformOptions = Object.keys(platformOptions).length > 0 ? platformOptions : void 0;
6598
6807
  }
6599
- await writeFile11(
6808
+ await writeFile12(
6600
6809
  configPath,
6601
6810
  `${JSON.stringify(config, null, 2)}
6602
6811
  `,
@@ -6606,12 +6815,12 @@ async function removeFromConfig(rootDir, platform) {
6606
6815
  } catch {
6607
6816
  }
6608
6817
  }
6609
- function isRecord8(value) {
6818
+ function isRecord9(value) {
6610
6819
  return typeof value === "object" && value !== null && !Array.isArray(value);
6611
6820
  }
6612
6821
  async function pathExists10(p) {
6613
6822
  try {
6614
- await access4(p);
6823
+ await access5(p);
6615
6824
  return true;
6616
6825
  } catch {
6617
6826
  return false;
@@ -6619,23 +6828,23 @@ async function pathExists10(p) {
6619
6828
  }
6620
6829
 
6621
6830
  // src/commands/version.ts
6622
- import process9 from "process";
6831
+ import process10 from "process";
6623
6832
  function version() {
6624
- const nodeVersion = process9.version;
6625
- const platform = `${process9.platform}/${process9.arch}`;
6833
+ const nodeVersion = process10.version;
6834
+ const platform = `${process10.platform}/${process10.arch}`;
6626
6835
  console.log(`mancode/${VERSION}`);
6627
6836
  console.log(`node/${nodeVersion.replace("v", "")}`);
6628
6837
  console.log(platform);
6629
6838
  }
6630
6839
 
6631
6840
  // src/commands/workflow.ts
6632
- import { access as access5 } from "fs/promises";
6633
- import path23 from "path";
6841
+ import { access as access6 } from "fs/promises";
6842
+ import path24 from "path";
6634
6843
  var EXIT_OK8 = 0;
6635
6844
  var EXIT_NOT_INITIALIZED6 = 1;
6636
6845
  var EXIT_INVALID_ARG2 = 2;
6637
6846
  async function workflow(rootDir, subcommand, args = [], options = {}) {
6638
- if (!await pathExists11(path23.join(rootDir, ".mancode", "state.json"))) {
6847
+ if (!await pathExists11(path24.join(rootDir, ".mancode", "state.json"))) {
6639
6848
  if (options.json) {
6640
6849
  console.log(JSON.stringify({ error: "not initialized" }, null, 2));
6641
6850
  } else {
@@ -6653,6 +6862,8 @@ async function workflow(rootDir, subcommand, args = [], options = {}) {
6653
6862
  return workflowList(rootDir, options);
6654
6863
  case "show":
6655
6864
  return workflowShow(rootDir, args[0], options);
6865
+ case "review":
6866
+ return workflowReview(rootDir, args, options);
6656
6867
  case "clean":
6657
6868
  return workflowClean(rootDir, options);
6658
6869
  default:
@@ -6667,12 +6878,109 @@ async function workflow(rootDir, subcommand, args = [], options = {}) {
6667
6878
  } else {
6668
6879
  console.error(`\u2717 Invalid workflow subcommand: ${subcommand}`);
6669
6880
  console.error(
6670
- " Use: create <man|mamba|manteam> <task> | update <taskId> | list | show <taskId> | clean"
6881
+ " Use: create <man|mamba|manteam> <task> | update <taskId> | review <taskId> <init|complete|remediate|show> | list | show <taskId> | clean"
6671
6882
  );
6672
6883
  }
6673
6884
  return EXIT_INVALID_ARG2;
6674
6885
  }
6675
6886
  }
6887
+ async function workflowReview(rootDir, args, options) {
6888
+ const taskId = args[0];
6889
+ const action = args[1];
6890
+ if (!taskId || !isValidWorkflowTaskId(taskId)) {
6891
+ return invalidArg(options, `invalid taskId: ${taskId ?? ""}`);
6892
+ }
6893
+ const meta = await readWorkflow(rootDir, taskId);
6894
+ if (!meta) return invalidArg(options, `workflow not found: ${taskId}`);
6895
+ if (meta.mode !== "man" && meta.mode !== "manteam") {
6896
+ return invalidArg(options, "review is only valid for man or manteam");
6897
+ }
6898
+ if (action === "show") {
6899
+ const ledger = await readReviewLedger(rootDir, taskId);
6900
+ if (!ledger)
6901
+ return invalidArg(options, `review not initialized: ${taskId}`);
6902
+ outputReviewLedger(ledger, options);
6903
+ return EXIT_OK8;
6904
+ }
6905
+ if (meta.status !== "in_progress" || meta.currentStep < 6) {
6906
+ return invalidArg(
6907
+ options,
6908
+ "review requires an in_progress workflow at step 6 or later"
6909
+ );
6910
+ }
6911
+ try {
6912
+ let ledger;
6913
+ if (action === "init") {
6914
+ if (!isReviewDepth(options.reviewDepth)) {
6915
+ return invalidArg(
6916
+ options,
6917
+ `invalid --review-depth: ${options.reviewDepth ?? ""}`
6918
+ );
6919
+ }
6920
+ if (options.reviewDomain !== void 0 && !isReviewDomain(options.reviewDomain)) {
6921
+ return invalidArg(
6922
+ options,
6923
+ `invalid --review-domain: ${options.reviewDomain}`
6924
+ );
6925
+ }
6926
+ ledger = await initializeReview(
6927
+ rootDir,
6928
+ taskId,
6929
+ options.reviewDepth,
6930
+ isReviewDomain(options.reviewDomain) ? options.reviewDomain : void 0
6931
+ );
6932
+ } else if (action === "complete") {
6933
+ if (!isReviewDomain(options.reviewDomain) || !options.report) {
6934
+ return invalidArg(
6935
+ options,
6936
+ "review complete requires --review-domain and --report"
6937
+ );
6938
+ }
6939
+ ledger = await completeReviewDomain(
6940
+ rootDir,
6941
+ taskId,
6942
+ options.reviewDomain,
6943
+ options.report,
6944
+ parseCsv(options.blockers)
6945
+ );
6946
+ } else if (action === "remediate") {
6947
+ ledger = await remediateReviewBlockers(
6948
+ rootDir,
6949
+ taskId,
6950
+ parseCsv(options.resolved)
6951
+ );
6952
+ } else {
6953
+ return invalidArg(
6954
+ options,
6955
+ `invalid review action: ${action ?? ""}`,
6956
+ "Use: mancode workflow review <taskId> <init|complete|remediate|show>"
6957
+ );
6958
+ }
6959
+ outputReviewLedger(ledger, options);
6960
+ return EXIT_OK8;
6961
+ } catch (error) {
6962
+ return invalidArg(
6963
+ options,
6964
+ error instanceof Error ? error.message : "unable to update review"
6965
+ );
6966
+ }
6967
+ }
6968
+ function outputReviewLedger(ledger, options) {
6969
+ if (options.json) {
6970
+ console.log(JSON.stringify(ledger, null, 2));
6971
+ return;
6972
+ }
6973
+ if (!ledger) return;
6974
+ const openBlockers = ledger.blockers.filter(
6975
+ (blocker) => blocker.status === "open"
6976
+ );
6977
+ console.log(
6978
+ `Review: ${ledger.depth}; domains ${ledger.completedDomains.length}/${ledger.requiredDomains.length}; open blockers ${openBlockers.length}; remediation ${ledger.remediationRounds}/1`
6979
+ );
6980
+ }
6981
+ function parseCsv(value) {
6982
+ return (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
6983
+ }
6676
6984
  async function workflowCreate(rootDir, args, options) {
6677
6985
  const mode = args[0];
6678
6986
  const task = args.slice(1).join(" ").trim();
@@ -7017,7 +7325,7 @@ function ago(iso) {
7017
7325
  }
7018
7326
  async function pathExists11(p) {
7019
7327
  try {
7020
- await access5(p);
7328
+ await access6(p);
7021
7329
  return true;
7022
7330
  } catch {
7023
7331
  return false;
@@ -7057,7 +7365,13 @@ program.command("uninstall [platform]").description("Remove platform adapter or
7057
7365
  program.command("workflow <subcommand> [args...]").description("Manage mancode workflows").option("--dry-run", "Preview clean without deleting").option("--older-than <duration>", "Clean workflows older than (e.g. 30d)").option("--step <n>", "Update workflow current step").option("--status <status>", "Update workflow status").option(
7058
7366
  "--parent-task <taskId>",
7059
7367
  "Parent /man or /manteam workflow for mamba"
7060
- ).option("--blocking-reason <reason>", "Explain why a workflow is blocked").option("--outcome <outcome>", "Set mamba outcome").option("--plan-version <n>", "Set the next man/manteam plan revision").option("--skipped <steps>", "Update skipped steps as comma-separated values").option("--json", "Output as JSON (for scripts)").action(async (subcommand, args, options) => {
7368
+ ).option("--blocking-reason <reason>", "Explain why a workflow is blocked").option("--outcome <outcome>", "Set mamba outcome").option("--plan-version <n>", "Set the next man/manteam plan revision").option("--skipped <steps>", "Update skipped steps as comma-separated values").option("--review-depth <depth>", "Review depth: targeted or full").option("--review-domain <domain>", "Review domain: quality or security").option(
7369
+ "--report <path>",
7370
+ "Relative Markdown report path for a review domain"
7371
+ ).option("--blockers <ids>", "Comma-separated blocker ids found by a review").option(
7372
+ "--resolved <ids>",
7373
+ "Comma-separated blocker ids resolved in remediation"
7374
+ ).option("--json", "Output as JSON (for scripts)").action(async (subcommand, args, options) => {
7061
7375
  const code = await workflow(process.cwd(), subcommand, args ?? [], options);
7062
7376
  process.exitCode = code;
7063
7377
  });