harness-wingman 0.4.2 → 0.4.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.
@@ -267,13 +267,16 @@ import { join as join5, parse, resolve } from "node:path";
267
267
  function newWalkStats(entryBudget = MAX_WALK_ENTRIES) {
268
268
  return { unknownErrors: 0, truncated: false, entriesLeft: entryBudget };
269
269
  }
270
+ function recordWalkError(error, stats) {
271
+ const code = error.code;
272
+ if (stats && (code === void 0 || !TOLERATED_CODES.has(code)))
273
+ stats.unknownErrors += 1;
274
+ }
270
275
  function safeReaddir(dir, stats) {
271
276
  try {
272
277
  return readdirSync(dir, { withFileTypes: true });
273
278
  } catch (e) {
274
- const code = e.code;
275
- if (stats && (code === void 0 || !TOLERATED_CODES.has(code)))
276
- stats.unknownErrors += 1;
279
+ recordWalkError(e, stats);
277
280
  return [];
278
281
  }
279
282
  }
@@ -325,26 +328,38 @@ var init_walk = __esm({
325
328
  import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "node:fs";
326
329
  import { homedir as homedir3 } from "node:os";
327
330
  import { join as join6 } from "node:path";
328
- function probeVersion(homeDir) {
331
+ function probeVersion(homeDir, stats = newWalkStats()) {
329
332
  const root = join6(homeDir, ".claude", "projects");
330
333
  let latest;
331
- for (const entry of safeReaddir(root)) {
334
+ for (const entry of safeReaddir(root, stats)) {
332
335
  if (!entry.isDirectory())
333
336
  continue;
334
337
  const dir = join6(root, entry.name);
335
- for (const sub of safeReaddir(dir)) {
338
+ for (const sub of safeReaddir(dir, stats)) {
336
339
  const name = sub.name;
337
340
  if (!name.endsWith(".jsonl"))
338
341
  continue;
339
342
  const file = join6(dir, name);
340
- const mtimeMs = statSync(file).mtimeMs;
343
+ let mtimeMs;
344
+ try {
345
+ mtimeMs = statSync(file).mtimeMs;
346
+ } catch (error) {
347
+ recordWalkError(error, stats);
348
+ continue;
349
+ }
341
350
  if (!latest || mtimeMs > latest.mtimeMs)
342
351
  latest = { file, mtimeMs };
343
352
  }
344
353
  }
345
354
  if (!latest)
346
355
  return void 0;
347
- const lines = readFileSync3(latest.file, "utf8").split("\n");
356
+ let lines;
357
+ try {
358
+ lines = readFileSync3(latest.file, "utf8").split("\n");
359
+ } catch (error) {
360
+ recordWalkError(error, stats);
361
+ return void 0;
362
+ }
348
363
  for (let i = lines.length - 1; i >= 0; i--) {
349
364
  const line = lines[i];
350
365
  if (!line)
@@ -439,36 +454,46 @@ function buildBase() {
439
454
  }
440
455
  ];
441
456
  }
442
- function ruleItem(file, source) {
443
- return {
444
- id: "",
445
- slot: "rules",
446
- nativeName: basename(file),
447
- nativePath: file,
448
- tokens: estimateTokens(statSync2(file).size),
449
- residency: "resident",
450
- toggleable: false,
451
- enabled: true,
452
- source
453
- };
457
+ function ruleItem(file, source, stats) {
458
+ try {
459
+ return {
460
+ id: "",
461
+ slot: "rules",
462
+ nativeName: basename(file),
463
+ nativePath: file,
464
+ tokens: estimateTokens(statSync2(file).size),
465
+ residency: "resident",
466
+ toggleable: false,
467
+ enabled: true,
468
+ source
469
+ };
470
+ } catch (error) {
471
+ recordWalkError(error, stats);
472
+ return void 0;
473
+ }
454
474
  }
455
- function buildRules(homeDir, projectDir) {
475
+ function buildRules(homeDir, projectDir, stats) {
456
476
  const items = [];
477
+ const append = (file, source) => {
478
+ const item = ruleItem(file, source, stats);
479
+ if (item !== void 0)
480
+ items.push(item);
481
+ };
457
482
  const userClaudeMd = join7(homeDir, ".claude", "CLAUDE.md");
458
483
  if (existsSync3(userClaudeMd))
459
- items.push(ruleItem(userClaudeMd, "user"));
460
- for (const f of listMdFiles(join7(homeDir, ".claude", "rules")))
461
- items.push(ruleItem(f, "user"));
484
+ append(userClaudeMd, "user");
485
+ for (const f of listMdFiles(join7(homeDir, ".claude", "rules"), stats))
486
+ append(f, "user");
462
487
  if (isBareProjectDir(projectDir, homeDir)) {
463
488
  const direct = join7(projectDir, "CLAUDE.md");
464
489
  if (existsSync3(direct))
465
- items.push(ruleItem(direct, "project"));
490
+ append(direct, "project");
466
491
  } else {
467
- for (const f of walkFiles(projectDir, "CLAUDE.md"))
468
- items.push(ruleItem(f, "project"));
492
+ for (const f of walkFiles(projectDir, "CLAUDE.md", stats))
493
+ append(f, "project");
469
494
  }
470
- for (const f of listMdFiles(join7(projectDir, ".claude", "rules"))) {
471
- items.push(ruleItem(f, "project"));
495
+ for (const f of listMdFiles(join7(projectDir, ".claude", "rules"), stats)) {
496
+ append(f, "project");
472
497
  }
473
498
  return items;
474
499
  }
@@ -489,7 +514,7 @@ function parseSkillFrontmatter(content) {
489
514
  }
490
515
  return out;
491
516
  }
492
- function buildSkills(homeDir, projectDir, overrides) {
517
+ function buildSkills(homeDir, projectDir, overrides, stats) {
493
518
  const userRoot = join7(homeDir, ".claude", "skills");
494
519
  const syncedRoot = join7(userRoot, "synced");
495
520
  const roots = [
@@ -502,24 +527,29 @@ function buildSkills(homeDir, projectDir, overrides) {
502
527
  ];
503
528
  const items = [];
504
529
  for (const { root, source, exclude } of roots) {
505
- for (const skillFile of walkFiles(root, "SKILL.md")) {
530
+ for (const skillFile of walkFiles(root, "SKILL.md", stats)) {
506
531
  if (exclude && (skillFile === exclude || skillFile.startsWith(exclude + sep)))
507
532
  continue;
508
- const fm = parseSkillFrontmatter(readFileSync4(skillFile, "utf8"));
509
- const name = fm.name ?? basename(dirname3(skillFile));
510
- items.push({
511
- id: "",
512
- slot: "skills",
513
- nativeName: name,
514
- nativePath: skillFile,
515
- // 两段式成本(PLAN §4a):常驻 = frontmatter description 字节;触发后 = SKILL.md 全文字节
516
- tokens: estimateTokens(Buffer2.byteLength(fm.description ?? "", "utf8")),
517
- onTriggerTokens: estimateTokens(statSync2(skillFile).size),
518
- residency: "on_trigger",
519
- toggleable: true,
520
- enabled: overrides[name] !== "off",
521
- source
522
- });
533
+ try {
534
+ const fm = parseSkillFrontmatter(readFileSync4(skillFile, "utf8"));
535
+ const name = fm.name ?? basename(dirname3(skillFile));
536
+ const size = statSync2(skillFile).size;
537
+ items.push({
538
+ id: "",
539
+ slot: "skills",
540
+ nativeName: name,
541
+ nativePath: skillFile,
542
+ // 两段式成本(PLAN §4a):常驻 = frontmatter description 字节;触发后 = SKILL.md 全文字节
543
+ tokens: estimateTokens(Buffer2.byteLength(fm.description ?? "", "utf8")),
544
+ onTriggerTokens: estimateTokens(size),
545
+ residency: "on_trigger",
546
+ toggleable: true,
547
+ enabled: overrides[name] !== "off",
548
+ source
549
+ });
550
+ } catch (error) {
551
+ recordWalkError(error, stats);
552
+ }
523
553
  }
524
554
  }
525
555
  return items;
@@ -595,9 +625,16 @@ function buildGates(chain) {
595
625
  function projectSlug(projectDir) {
596
626
  return resolve2(projectDir).replace(/[^a-zA-Z0-9]/g, "-");
597
627
  }
598
- function lastContextTotal(file) {
628
+ function lastContextTotal(file, stats) {
599
629
  let total;
600
- for (const line of readFileSync4(file, "utf8").split("\n")) {
630
+ let text;
631
+ try {
632
+ text = readFileSync4(file, "utf8");
633
+ } catch (error) {
634
+ recordWalkError(error, stats);
635
+ return void 0;
636
+ }
637
+ for (const line of text.split("\n")) {
601
638
  if (!line)
602
639
  continue;
603
640
  let row;
@@ -619,12 +656,12 @@ function lastContextTotal(file) {
619
656
  }
620
657
  return total;
621
658
  }
622
- function buildHistory(homeDir, projectDir) {
659
+ function buildHistory(homeDir, projectDir, stats) {
623
660
  const dir = join7(homeDir, ".claude", "projects", projectSlug(projectDir));
624
661
  const items = [];
625
- for (const name of safeReaddir(dir).map((e) => e.name).filter((n) => n.endsWith(".jsonl")).sort()) {
662
+ for (const name of safeReaddir(dir, stats).map((e) => e.name).filter((n) => n.endsWith(".jsonl")).sort()) {
626
663
  const file = join7(dir, name);
627
- const total = lastContextTotal(file);
664
+ const total = lastContextTotal(file, stats);
628
665
  if (total === void 0)
629
666
  continue;
630
667
  items.push({
@@ -641,11 +678,12 @@ function buildHistory(homeDir, projectDir) {
641
678
  return items;
642
679
  }
643
680
  function detectLiveWiring(chain) {
681
+ const wired = /* @__PURE__ */ new Set();
644
682
  for (const s of chain) {
645
683
  const hooks = asRecord(s.data.hooks);
646
684
  if (!hooks)
647
685
  continue;
648
- for (const entries of Object.values(hooks)) {
686
+ for (const [event, entries] of Object.entries(hooks)) {
649
687
  if (!Array.isArray(entries))
650
688
  continue;
651
689
  for (const entry of entries) {
@@ -655,13 +693,22 @@ function detectLiveWiring(chain) {
655
693
  for (const h of handlers) {
656
694
  const handler = asRecord(h);
657
695
  if (handler?.type === "http" && typeof handler.url === "string" && handler.url.includes("/hook?harness=cc")) {
658
- return "push";
696
+ wired.add(event);
659
697
  }
660
698
  }
661
699
  }
662
700
  }
663
701
  }
664
- return "none";
702
+ return [...wired];
703
+ }
704
+ function feedbackSignalsCc(options) {
705
+ const homeDir = options?.homeDir ?? homedir4();
706
+ const projectDir = options?.projectDir ?? process.cwd();
707
+ const wiredEvents = detectLiveWiring(readSettingsChain(homeDir, projectDir));
708
+ return {
709
+ wiredEvents,
710
+ feedback: wiredEvents.includes("PermissionRequest") || wiredEvents.includes("Notification") ? "available" : "not_wired"
711
+ };
665
712
  }
666
713
  function assignIds(items) {
667
714
  const groups = /* @__PURE__ */ new Map();
@@ -689,17 +736,18 @@ function assignIds(items) {
689
736
  async function scanCc(options) {
690
737
  const homeDir = options?.homeDir ?? homedir4();
691
738
  const projectDir = options?.projectDir ?? process.cwd();
739
+ const stats = newWalkStats();
692
740
  const chain = readSettingsChain(homeDir, projectDir);
693
741
  const slots = {
694
742
  base: buildBase(),
695
- rules: buildRules(homeDir, projectDir),
696
- skills: buildSkills(homeDir, projectDir, mergedSkillOverrides(chain)),
743
+ rules: buildRules(homeDir, projectDir, stats),
744
+ skills: buildSkills(homeDir, projectDir, mergedSkillOverrides(chain), stats),
697
745
  tools: buildTools(homeDir, projectDir),
698
746
  gates: buildGates(chain),
699
- history: buildHistory(homeDir, projectDir)
747
+ history: buildHistory(homeDir, projectDir, stats)
700
748
  };
701
749
  assignIds(Object.values(slots).flat());
702
- const version = probeVersion(homeDir);
750
+ const version = probeVersion(homeDir, stats);
703
751
  return {
704
752
  harness: "cc",
705
753
  ...version !== void 0 ? { harnessVersion: version } : {},
@@ -708,7 +756,7 @@ async function scanCc(options) {
708
756
  capabilities: {
709
757
  // M3:可逆开关已落地(planAction/applyAction,actions.ts);open_editor /
710
758
  // scan_references 为即时只读(合同注释),分别由 nativePath 与 plan.references 承载
711
- live: detectLiveWiring(chain),
759
+ live: detectLiveWiring(chain).length > 0 ? "push" : "none",
712
760
  actions: ["toggle", "open_editor", "scan_references"]
713
761
  }
714
762
  };
@@ -755,6 +803,13 @@ function backupDirPath(homeDir) {
755
803
  dir = join8(root, `${ts}-${n}`);
756
804
  return dir;
757
805
  }
806
+ function decodeUtf8Strict(buffer, file) {
807
+ const text = buffer.toString("utf8");
808
+ if (!buffer.equals(Buffer.from(text, "utf8"))) {
809
+ throw new Error(`\u62D2\u5199:${file} \u4E0D\u662F UTF-8 \u6587\u672C,\u672A\u77E5\u683C\u5F0F\u4E0D\u731C`);
810
+ }
811
+ return text;
812
+ }
758
813
  function readSettingsSnapshots(homeDir, projectDir) {
759
814
  const files = [
760
815
  join8(homeDir, ".claude", "settings.json"),
@@ -765,7 +820,7 @@ function readSettingsSnapshots(homeDir, projectDir) {
765
820
  for (const file of files) {
766
821
  if (!existsSync4(file))
767
822
  continue;
768
- const raw = readFileSync5(file, "utf8");
823
+ const raw = decodeUtf8Strict(readFileSync5(file), file);
769
824
  let parsed;
770
825
  try {
771
826
  parsed = JSON.parse(raw);
@@ -1062,6 +1117,7 @@ async function applyActionCc(plan, _options) {
1062
1117
  const shapeError = validatePlanShape(plan);
1063
1118
  if (shapeError !== void 0)
1064
1119
  return { ok: false, error: shapeError };
1120
+ const currentByFile = /* @__PURE__ */ new Map();
1065
1121
  for (const w of plan.writes) {
1066
1122
  if (w.kind === "create") {
1067
1123
  if (existsSync4(w.file)) {
@@ -1071,9 +1127,11 @@ async function applyActionCc(plan, _options) {
1071
1127
  if (!existsSync4(w.file)) {
1072
1128
  return { ok: false, error: `\u62D2\u7EDD\u6267\u884C:${w.file} \u5DF2\u4E0D\u5B58\u5728(plan \u540E\u88AB\u6539\u52A8),\u8BF7\u91CD\u65B0 plan` };
1073
1129
  }
1074
- if (readFileSync5(w.file, "utf8") !== w.before) {
1130
+ const currentRaw = readFileSync5(w.file);
1131
+ if (!currentRaw.equals(Buffer.from(w.before, "utf8"))) {
1075
1132
  return { ok: false, error: `\u62D2\u7EDD\u6267\u884C:${w.file} \u5728 plan \u540E\u88AB\u6539\u8FC7,\u8BF7\u91CD\u65B0 plan` };
1076
1133
  }
1134
+ currentByFile.set(w.file, currentRaw);
1077
1135
  }
1078
1136
  }
1079
1137
  const toBackup = plan.writes.filter((w) => w.kind !== "create");
@@ -1083,7 +1141,7 @@ async function applyActionCc(plan, _options) {
1083
1141
  target: w.file
1084
1142
  }));
1085
1143
  toBackup.forEach((w, i) => {
1086
- writeFileSync3(join8(plan.backupDir, `${i}-${basename2(w.file)}`), w.before ?? "");
1144
+ writeFileSync3(join8(plan.backupDir, `${i}-${basename2(w.file)}`), currentByFile.get(w.file));
1087
1145
  });
1088
1146
  writeFileSync3(join8(plan.backupDir, "manifest.json"), serialize({
1089
1147
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1391,7 +1449,16 @@ var WIRED_EVENTS, SIGNATURE;
1391
1449
  var init_hooks = __esm({
1392
1450
  "packages/adapter-cc/dist/hooks.js"() {
1393
1451
  "use strict";
1394
- WIRED_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"];
1452
+ WIRED_EVENTS = [
1453
+ "SessionStart",
1454
+ "UserPromptSubmit",
1455
+ "PreToolUse",
1456
+ "PostToolUse",
1457
+ "Stop",
1458
+ "Notification",
1459
+ "PermissionRequest",
1460
+ "SessionEnd"
1461
+ ];
1395
1462
  SIGNATURE = "/hook?harness=cc";
1396
1463
  }
1397
1464
  });
@@ -1399,7 +1466,9 @@ var init_hooks = __esm({
1399
1466
  // packages/adapter-cc/dist/index.js
1400
1467
  var dist_exports = {};
1401
1468
  __export(dist_exports, {
1402
- createAdapter: () => createAdapter
1469
+ WIRED_EVENTS: () => WIRED_EVENTS,
1470
+ createAdapter: () => createAdapter,
1471
+ feedbackSignals: () => feedbackSignalsCc
1403
1472
  });
1404
1473
  function createAdapter(_options) {
1405
1474
  return {
@@ -1419,6 +1488,8 @@ var init_dist2 = __esm({
1419
1488
  init_detect();
1420
1489
  init_hooks();
1421
1490
  init_scan();
1491
+ init_hooks();
1492
+ init_scan();
1422
1493
  }
1423
1494
  });
1424
1495
 
@@ -2853,16 +2924,18 @@ async function installHooks(port, options) {
2853
2924
  const configPath = join12(paths.codexHome, "config.toml");
2854
2925
  const raw = existsSync7(configPath) ? readFileSync8(configPath) : void 0;
2855
2926
  const text = raw?.toString("utf8") ?? "";
2856
- const { remainder, block } = splitManagedBlock(text, configPath);
2857
- const newBlock = buildManagedBlock(scriptPath);
2858
- if (block !== void 0 && !isKnownManagedBlock(block, scriptPath)) {
2927
+ const { before, after, remainder, block } = splitManagedBlock(text, configPath);
2928
+ const lineEnding = preferredLineEnding(text);
2929
+ const newBlock = buildManagedBlock(scriptPath).replaceAll("\n", lineEnding);
2930
+ const knownBlock = block === void 0 ? void 0 : inspectManagedBlock(block, scriptPath);
2931
+ if (block !== void 0 && knownBlock === void 0) {
2859
2932
  throw new Error(`Wingman Codex hook \u53D7\u7BA1\u5757\u5DF2\u88AB\u4FEE\u6539\u6216\u6B8B\u7F3A(${configPath}),\u62D2\u7EDD\u8C0E\u62A5\u63A5\u7EBF\u6210\u529F;\u8BF7\u5148\u4ECE <wingman\u6839>/backups/ \u6062\u590D\u5B8C\u6574\u5757,\u6216\u4EBA\u5DE5\u6838\u5BF9\u540E\u79FB\u9664\u8BE5\u53D7\u7BA1\u5757\u518D\u91CD\u65B0\u5B89\u88C5\u5E76\u4FE1\u4EFB`);
2860
2933
  }
2861
2934
  mkdirSync5(root, { recursive: true });
2862
2935
  writeFileSync6(scriptPath, FORWARD_SCRIPT);
2863
2936
  const statePath2 = hooksStatePath(root);
2864
2937
  const prior = readState2(statePath2);
2865
- if (block !== void 0) {
2938
+ if (knownBlock !== void 0 && knownBlock.state === void 0) {
2866
2939
  if (prior?.port !== port || prior.configPath !== configPath || prior.forwardScriptPath !== scriptPath) {
2867
2940
  writeState2(statePath2, {
2868
2941
  ...prior,
@@ -2875,8 +2948,22 @@ async function installHooks(port, options) {
2875
2948
  }
2876
2949
  return;
2877
2950
  }
2878
- assertWritable(remainder, configPath);
2951
+ const hasStateOutsideBlock = assertWritable(remainder, configPath);
2879
2952
  const backupDir = raw === void 0 ? void 0 : backupFile(root, configPath, raw);
2953
+ if (knownBlock !== void 0) {
2954
+ writeState2(statePath2, {
2955
+ ...prior,
2956
+ version: 1,
2957
+ port,
2958
+ configPath,
2959
+ forwardScriptPath: scriptPath,
2960
+ installedAt: prior?.installedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2961
+ ...backupDir !== void 0 ? { backupDir } : {}
2962
+ });
2963
+ const state = hasStateOutsideBlock ? "" : knownBlock.state;
2964
+ writeFileSync6(configPath, before + knownBlock.managedBlock + state + after);
2965
+ return;
2966
+ }
2880
2967
  const original = prior?.original ?? (raw === void 0 ? { existed: false } : { existed: true, base64: raw.toString("base64") });
2881
2968
  writeState2(statePath2, {
2882
2969
  version: 1,
@@ -2889,7 +2976,7 @@ async function installHooks(port, options) {
2889
2976
  });
2890
2977
  let base = remainder;
2891
2978
  if (base !== "" && !base.endsWith("\n"))
2892
- base += "\n";
2979
+ base += lineEnding;
2893
2980
  mkdirSync5(paths.codexHome, { recursive: true });
2894
2981
  writeFileSync6(configPath, base + newBlock);
2895
2982
  }
@@ -2903,13 +2990,15 @@ async function uninstallHooks(options) {
2903
2990
  const raw = existsSync7(configPath) ? readFileSync8(configPath) : void 0;
2904
2991
  if (raw !== void 0) {
2905
2992
  const text = raw.toString("utf8");
2906
- const { remainder, block } = splitManagedBlock(text, configPath);
2993
+ const { before, after, remainder, block } = splitManagedBlock(text, configPath);
2907
2994
  if (block !== void 0) {
2908
- if (!isKnownManagedBlock(block, scriptPath)) {
2995
+ const knownBlock = inspectManagedBlock(block, scriptPath);
2996
+ if (knownBlock === void 0) {
2909
2997
  throw new Error(`Wingman Codex hook \u53D7\u7BA1\u5757\u5DF2\u88AB\u4FEE\u6539\u6216\u6B8B\u7F3A(${configPath}),\u62D2\u7EDD\u81EA\u52A8\u5220\u9664;\u8BF7\u5148\u4ECE <wingman\u6839>/backups/ \u6062\u590D\u5B8C\u6574\u5757,\u6216\u4EBA\u5DE5\u6838\u5BF9\u540E\u79FB\u9664\u8BE5\u53D7\u7BA1\u5757`);
2910
2998
  }
2911
2999
  backupFile(root, configPath, raw);
2912
- const restored = restoreTarget(remainder, state);
3000
+ const preservedState = hasHooksState(remainder) ? "" : knownBlock.state ?? "";
3001
+ const restored = restoreTarget(before + preservedState + after, state);
2913
3002
  if (restored === null)
2914
3003
  rmSync3(configPath);
2915
3004
  else
@@ -2925,11 +3014,36 @@ function hasManagedHooks(paths) {
2925
3014
  return false;
2926
3015
  try {
2927
3016
  const { block } = splitManagedBlock(readFileSync8(configPath, "utf8"), configPath);
2928
- return block !== void 0 && isKnownManagedBlock(block, join12(wingmanStateRoot(paths), FORWARD_SCRIPT_NAME));
3017
+ return block !== void 0 && inspectManagedBlock(block, join12(wingmanStateRoot(paths), FORWARD_SCRIPT_NAME)) !== void 0;
2929
3018
  } catch {
2930
3019
  return false;
2931
3020
  }
2932
3021
  }
3022
+ function feedbackSignalsCodex(options) {
3023
+ const paths = resolvePaths(options);
3024
+ const configPath = join12(paths.codexHome, "config.toml");
3025
+ const wiredEvents = [];
3026
+ if (existsSync7(configPath)) {
3027
+ try {
3028
+ const { block } = splitManagedBlock(readFileSync8(configPath, "utf8"), configPath);
3029
+ if (block !== void 0) {
3030
+ const knownBlock = inspectManagedBlock(block, join12(wingmanStateRoot(paths), FORWARD_SCRIPT_NAME));
3031
+ const hooks = knownBlock === void 0 ? void 0 : parse2(normalizeLineEndings(knownBlock.managedBlock)).hooks;
3032
+ if (isTable(hooks)) {
3033
+ for (const [event, groups] of Object.entries(hooks)) {
3034
+ if (Array.isArray(groups))
3035
+ wiredEvents.push(event);
3036
+ }
3037
+ }
3038
+ }
3039
+ } catch {
3040
+ }
3041
+ }
3042
+ return {
3043
+ wiredEvents,
3044
+ feedback: wiredEvents.includes("PermissionRequest") ? "available" : "not_wired"
3045
+ };
3046
+ }
2933
3047
  function restoreTarget(remainder, state) {
2934
3048
  const original = state?.original;
2935
3049
  if (original === void 0)
@@ -2938,9 +3052,16 @@ function restoreTarget(remainder, state) {
2938
3052
  return remainder.trim() === "" ? null : remainder;
2939
3053
  }
2940
3054
  const originalBytes = Buffer.from(original.base64 ?? "", "base64");
3055
+ const originalText = originalBytes.toString("utf8");
2941
3056
  try {
2942
- if (deepEqual3(parse2(remainder), parse2(originalBytes.toString("utf8"))))
2943
- return originalBytes;
3057
+ if (deepEqual3(parse2(remainder), parse2(originalText))) {
3058
+ const separatorOnly = remainder === `${originalText}
3059
+ ` || remainder === `${originalText}\r
3060
+ `;
3061
+ if (separatorOnly || lineEndingStyle(remainder) === lineEndingStyle(originalText)) {
3062
+ return originalBytes;
3063
+ }
3064
+ }
2944
3065
  } catch {
2945
3066
  }
2946
3067
  return remainder;
@@ -2958,43 +3079,78 @@ function buildManagedBlockForCommand(rawCommand) {
2958
3079
  return `${lines.join("\n")}
2959
3080
  `;
2960
3081
  }
2961
- function isKnownManagedBlock(block, scriptPath) {
2962
- if (block === buildManagedBlock(scriptPath))
2963
- return true;
3082
+ function inspectManagedBlock(block, scriptPath) {
3083
+ const inspected = separateCodexHooksState(block);
3084
+ const normalized = normalizeLineEndings(inspected.managedBlock);
3085
+ if (normalized === buildManagedBlock(scriptPath))
3086
+ return inspected;
2964
3087
  let table;
2965
3088
  try {
2966
- table = parse2(block);
3089
+ table = parse2(normalized);
2967
3090
  } catch {
2968
- return false;
3091
+ return void 0;
2969
3092
  }
2970
3093
  const hooks = table.hooks;
2971
3094
  if (!isTable(hooks))
2972
- return false;
3095
+ return void 0;
2973
3096
  const first = hooks[HOOK_EVENTS[0]];
2974
3097
  if (!Array.isArray(first) || first.length !== 1 || !isTable(first[0]))
2975
- return false;
3098
+ return void 0;
2976
3099
  const handlers = first[0].hooks;
2977
3100
  if (!Array.isArray(handlers) || handlers.length !== 1 || !isTable(handlers[0]))
2978
- return false;
3101
+ return void 0;
2979
3102
  const command = handlers[0].command;
2980
3103
  if (typeof command !== "string")
2981
- return false;
3104
+ return void 0;
2982
3105
  const prefix = `node "${scriptPath}" `;
2983
3106
  if (!command.startsWith(prefix))
2984
- return false;
3107
+ return void 0;
2985
3108
  const legacyPortText = command.slice(prefix.length);
2986
3109
  if (!/^[1-9]\d{0,4}$/.test(legacyPortText))
2987
- return false;
3110
+ return void 0;
2988
3111
  const legacyPort = Number(legacyPortText);
2989
3112
  if (legacyPort > 65535 || String(legacyPort) !== legacyPortText)
2990
- return false;
2991
- return block === buildManagedBlockForCommand(command);
3113
+ return void 0;
3114
+ return normalized === buildManagedBlockForCommand(command) ? inspected : void 0;
3115
+ }
3116
+ function separateCodexHooksState(block) {
3117
+ const stateHeader = /^[ \t]*\[[ \t]*hooks[ \t]*\.[ \t]*state(?:[ \t]*|[ \t]*\..+)\][ \t]*(?:#.*)?\r?$/m;
3118
+ const match = stateHeader.exec(block);
3119
+ if (match === null)
3120
+ return { managedBlock: block };
3121
+ let stateStart = match.index;
3122
+ while (stateStart > 0 && block[stateStart - 1] === "\n") {
3123
+ const precedingNewline = block.lastIndexOf("\n", stateStart - 2);
3124
+ const lineStart = precedingNewline + 1;
3125
+ const lineEnd = block[stateStart - 2] === "\r" ? stateStart - 2 : stateStart - 1;
3126
+ if (!/^[ \t]*$/.test(block.slice(lineStart, lineEnd)))
3127
+ break;
3128
+ stateStart = lineStart;
3129
+ }
3130
+ const stateEnd = block.indexOf(MANAGED_BLOCK_END, match.index);
3131
+ if (stateEnd === -1)
3132
+ return { managedBlock: block };
3133
+ const state = block.slice(stateStart, stateEnd);
3134
+ try {
3135
+ const table = parse2(normalizeLineEndings(state));
3136
+ const hooks = table.hooks;
3137
+ if (Object.keys(table).length !== 1 || !isTable(hooks) || Object.keys(hooks).length !== 1 || !isTable(hooks.state)) {
3138
+ return { managedBlock: block };
3139
+ }
3140
+ } catch {
3141
+ return { managedBlock: block };
3142
+ }
3143
+ return {
3144
+ managedBlock: block.slice(0, stateStart) + block.slice(stateEnd),
3145
+ state
3146
+ };
2992
3147
  }
2993
3148
  function splitManagedBlock(text, configPath) {
2994
3149
  const start = text.indexOf(MANAGED_BLOCK_START);
2995
3150
  const endMark = text.indexOf(MANAGED_BLOCK_END);
2996
- if (start === -1 && endMark === -1)
2997
- return { remainder: text };
3151
+ if (start === -1 && endMark === -1) {
3152
+ return { before: text, after: "", remainder: text };
3153
+ }
2998
3154
  if (start === -1 || endMark === -1 || endMark < start) {
2999
3155
  throw new Error(`wingman \u6807\u8BB0\u5757\u6B8B\u7F3A(${configPath}):\u8BF7\u4ECE <wingman\u6839>/backups/ \u6062\u590D\u6216\u624B\u52A8\u5220\u9664\u6807\u8BB0\u540E\u91CD\u8BD5`);
3000
3156
  }
@@ -3002,9 +3158,28 @@ function splitManagedBlock(text, configPath) {
3002
3158
  throw new Error(`wingman \u6807\u8BB0\u5757\u91CD\u590D(${configPath}):\u8BF7\u4ECE <wingman\u6839>/backups/ \u6062\u590D\u6216\u624B\u52A8\u5220\u9664\u591A\u4F59\u6807\u8BB0\u540E\u91CD\u8BD5`);
3003
3159
  }
3004
3160
  let end = endMark + MANAGED_BLOCK_END.length;
3005
- if (text[end] === "\n")
3161
+ if (text.startsWith("\r\n", end))
3162
+ end += 2;
3163
+ else if (text[end] === "\n")
3006
3164
  end += 1;
3007
- return { remainder: text.slice(0, start) + text.slice(end), block: text.slice(start, end) };
3165
+ const before = text.slice(0, start);
3166
+ const after = text.slice(end);
3167
+ return { before, after, remainder: before + after, block: text.slice(start, end) };
3168
+ }
3169
+ function normalizeLineEndings(text) {
3170
+ return text.replaceAll("\r\n", "\n");
3171
+ }
3172
+ function preferredLineEnding(text) {
3173
+ return lineEndingStyle(text) === "crlf" ? "\r\n" : "\n";
3174
+ }
3175
+ function lineEndingStyle(text) {
3176
+ const hasCrlf = text.includes("\r\n");
3177
+ if (!text.includes("\n"))
3178
+ return "none";
3179
+ const hasBareLf = text.replaceAll("\r\n", "").includes("\n");
3180
+ if (hasCrlf && hasBareLf)
3181
+ return "mixed";
3182
+ return hasCrlf ? "crlf" : "lf";
3008
3183
  }
3009
3184
  function assertWritable(remainder, configPath) {
3010
3185
  let table;
@@ -3015,7 +3190,7 @@ function assertWritable(remainder, configPath) {
3015
3190
  }
3016
3191
  const hooks = table.hooks;
3017
3192
  if (hooks === void 0)
3018
- return;
3193
+ return false;
3019
3194
  if (!isTable(hooks)) {
3020
3195
  throw new Error(`Codex config.toml \u7684 hooks \u952E\u4E0D\u662F\u8868,\u672A\u77E5\u683C\u5F0F\u62D2\u7EDD\u5199\u5165(${configPath})`);
3021
3196
  }
@@ -3023,6 +3198,15 @@ function assertWritable(remainder, configPath) {
3023
3198
  if (userKeys.length > 0) {
3024
3199
  throw new Error(`\u5DF2\u5B58\u5728\u7528\u6237\u81EA\u5DF1\u7684 [hooks] \u914D\u7F6E(${userKeys.join(", ")}),wingman \u4E0D\u5408\u5E76\u4E0D\u8986\u76D6;\u8BF7\u5148\u624B\u52A8\u79FB\u9664\u6216\u81EA\u884C\u628A\u8F6C\u53D1\u547D\u4EE4\u52A0\u5165\u73B0\u6709 hooks(${configPath})`);
3025
3200
  }
3201
+ return hooks.state !== void 0;
3202
+ }
3203
+ function hasHooksState(text) {
3204
+ try {
3205
+ const hooks = parse2(text).hooks;
3206
+ return isTable(hooks) && hooks.state !== void 0;
3207
+ } catch {
3208
+ return /^[ \t]*\[[ \t]*hooks[ \t]*\.[ \t]*state(?:[ \t]*|[ \t]*\..+)\]/m.test(text);
3209
+ }
3026
3210
  }
3027
3211
  function backupFile(root, filePath, contents) {
3028
3212
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-");
@@ -3364,27 +3548,18 @@ function collectTools(out, layer) {
3364
3548
  enabled: server.enabled !== false,
3365
3549
  source: layer.source
3366
3550
  });
3367
- const toolLists = [
3368
- ["enabled_tools", true],
3369
- ["disabled_tools", false]
3370
- ];
3371
- for (const [listKey, enabled] of toolLists) {
3372
- const list = server[listKey];
3373
- if (!Array.isArray(list))
3374
- continue;
3375
- for (const tool of list) {
3376
- if (typeof tool !== "string")
3377
- continue;
3378
- out.push({
3379
- id: `codex:tools:${layer.source}:mcp_servers.${serverId}/${tool}`,
3380
- slot: "tools",
3381
- nativeName: `mcp_servers.${serverId}/${tool}`,
3382
- nativePath: layer.path,
3383
- toggleable: true,
3384
- enabled,
3385
- source: layer.source
3386
- });
3387
- }
3551
+ const enabledTools = new Set(Array.isArray(server.enabled_tools) ? server.enabled_tools.filter((tool) => typeof tool === "string") : []);
3552
+ const disabledTools = new Set(Array.isArray(server.disabled_tools) ? server.disabled_tools.filter((tool) => typeof tool === "string") : []);
3553
+ for (const tool of /* @__PURE__ */ new Set([...enabledTools, ...disabledTools])) {
3554
+ out.push({
3555
+ id: `codex:tools:${layer.source}:mcp_servers.${serverId}/${tool}`,
3556
+ slot: "tools",
3557
+ nativeName: `mcp_servers.${serverId}/${tool}`,
3558
+ nativePath: layer.path,
3559
+ toggleable: true,
3560
+ enabled: enabledTools.has(tool) && !disabledTools.has(tool),
3561
+ source: layer.source
3562
+ });
3388
3563
  }
3389
3564
  }
3390
3565
  }
@@ -3494,7 +3669,9 @@ var init_scan2 = __esm({
3494
3669
  // packages/adapter-codex/dist/index.js
3495
3670
  var dist_exports2 = {};
3496
3671
  __export(dist_exports2, {
3497
- createAdapter: () => createAdapter2
3672
+ HOOK_EVENTS: () => HOOK_EVENTS,
3673
+ createAdapter: () => createAdapter2,
3674
+ feedbackSignals: () => feedbackSignalsCodex
3498
3675
  });
3499
3676
  function createAdapter2(_options) {
3500
3677
  return {
@@ -3513,6 +3690,7 @@ var init_dist4 = __esm({
3513
3690
  init_actions2();
3514
3691
  init_hooks2();
3515
3692
  init_scan2();
3693
+ init_hooks2();
3516
3694
  }
3517
3695
  });
3518
3696
 
@@ -11054,6 +11232,9 @@ import { Buffer as Buffer3 } from "node:buffer";
11054
11232
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
11055
11233
  import { homedir as homedir8 } from "node:os";
11056
11234
  import { dirname as dirname9, join as join14 } from "node:path";
11235
+ function feedbackSignalsDsh(_options) {
11236
+ return { wiredEvents: [], feedback: "unavailable" };
11237
+ }
11057
11238
  function resolveDshHome(options) {
11058
11239
  if (options?.homeDir !== void 0)
11059
11240
  return join14(options.homeDir, ".dsh");
@@ -11114,7 +11295,7 @@ async function readIfExists(file) {
11114
11295
  throw error;
11115
11296
  }
11116
11297
  }
11117
- function decodeUtf8Strict(buffer, file) {
11298
+ function decodeUtf8Strict2(buffer, file) {
11118
11299
  const text = buffer.toString("utf8");
11119
11300
  if (!buffer.equals(Buffer3.from(text, "utf8"))) {
11120
11301
  throw new Error(`\u62D2\u5199:${file} \u4E0D\u662F UTF-8 \u6587\u672C,\u672A\u77E5\u683C\u5F0F\u4E0D\u731C`);
@@ -11226,7 +11407,7 @@ async function installDshHooks(port, options) {
11226
11407
  const url = `http://127.0.0.1:${port}/hook?harness=dsh`;
11227
11408
  const block = buildPatchBlock(paths.hooksConfigFile);
11228
11409
  const currentBuffer = await readIfExists(paths.patchFile);
11229
- const currentText = currentBuffer === void 0 ? void 0 : decodeUtf8Strict(currentBuffer, paths.patchFile);
11410
+ const currentText = currentBuffer === void 0 ? void 0 : decodeUtf8Strict2(currentBuffer, paths.patchFile);
11230
11411
  const surgicalText = currentText === void 0 ? void 0 : stripLiveHooksBlocks(currentText, paths.patchFile);
11231
11412
  const entriesBefore = surgicalText === void 0 ? [] : parsePatchEntries(surgicalText, paths.patchFile);
11232
11413
  if (hasWingmanRow(entriesBefore)) {
@@ -11273,7 +11454,7 @@ async function uninstallDshHooks(options) {
11273
11454
  const state = await readState3(paths);
11274
11455
  const currentBuffer = await readIfExists(paths.patchFile);
11275
11456
  if (currentBuffer !== void 0) {
11276
- const currentText = decodeUtf8Strict(currentBuffer, paths.patchFile);
11457
+ const currentText = decodeUtf8Strict2(currentBuffer, paths.patchFile);
11277
11458
  if (currentText.includes(HOOKS_MARKER_BEGIN_PREFIX)) {
11278
11459
  const surgical = stripLiveHooksBlocks(currentText, paths.patchFile);
11279
11460
  const restored = resolveRestoredText(surgical, state, paths);
@@ -11527,8 +11708,11 @@ function composeToggleOn(current, block, file) {
11527
11708
  if (block.newlineAdded && block.end === current.length && removed.endsWith("\n")) {
11528
11709
  removed = removed.slice(0, -1);
11529
11710
  }
11530
- if (block.fileCreated && removed === "")
11531
- return { kind: "delete" };
11711
+ if (block.fileCreated) {
11712
+ if (removed === "")
11713
+ return { kind: "delete" };
11714
+ removed = transferFileCreatedFlag(removed, file);
11715
+ }
11532
11716
  const entriesAfter = parsePatchEntries(removed, file);
11533
11717
  if (block.emptyRootLine !== void 0) {
11534
11718
  if (entriesAfter.length === 0) {
@@ -11541,6 +11725,18 @@ function composeToggleOn(current, block, file) {
11541
11725
  }
11542
11726
  return { kind: "modify", after: removed };
11543
11727
  }
11728
+ function transferFileCreatedFlag(text, file) {
11729
+ const blocks = findToggleBlocks(text, file);
11730
+ const target = blocks.find((b) => !b.fileCreated);
11731
+ if (target === void 0)
11732
+ return text;
11733
+ const beginLineEnd = text.indexOf("\n", target.start);
11734
+ if (beginLineEnd === -1)
11735
+ return text;
11736
+ const flagLine = `${FLAG_PREFIX}${FLAG_FILE_CREATED}
11737
+ `;
11738
+ return text.slice(0, beginLineEnd + 1) + flagLine + text.slice(beginLineEnd + 1);
11739
+ }
11544
11740
  function transferEmptyRootFlag(text, lineIndex, file) {
11545
11741
  const blocks = findToggleBlocks(text, file);
11546
11742
  const target = blocks.find((b) => b.emptyRootLine === void 0);
@@ -11607,7 +11803,7 @@ async function planDshToggle(action, dump, options) {
11607
11803
  }
11608
11804
  const paths = resolveDshHookPaths(options);
11609
11805
  const buffer = await readIfExists(paths.patchFile);
11610
- const text = buffer === void 0 ? void 0 : decodeUtf8Strict(buffer, paths.patchFile);
11806
+ const text = buffer === void 0 ? void 0 : decodeUtf8Strict2(buffer, paths.patchFile);
11611
11807
  const blocks = text === void 0 ? [] : findToggleBlocks(text, paths.patchFile);
11612
11808
  const myBlock = blocks.find((b) => b.instanceId === row.id);
11613
11809
  const entriesAll = text === void 0 ? [] : parsePatchEntries(text, paths.patchFile);
@@ -11749,8 +11945,10 @@ var init_toggle = __esm({
11749
11945
  // packages/adapter-dsh/dist/index.js
11750
11946
  var dist_exports3 = {};
11751
11947
  __export(dist_exports3, {
11948
+ BRIDGE_EVENTS: () => BRIDGE_EVENTS,
11752
11949
  createAdapter: () => createAdapter3,
11753
11950
  detectDshLiveWiring: () => detectDshLiveWiring,
11951
+ feedbackSignals: () => feedbackSignalsDsh,
11754
11952
  parseDumpConfig: () => parseDumpConfig,
11755
11953
  resolveDshHome: () => resolveDshHome,
11756
11954
  resolveDshHookPaths: () => resolveDshHookPaths
@@ -11902,12 +12100,14 @@ function createReceiver(options = {}) {
11902
12100
  const heartbeatMs = options.heartbeatMs ?? 15e3;
11903
12101
  const buffer = [];
11904
12102
  const subscribers = /* @__PURE__ */ new Set();
12103
+ let nextEventId = 1;
11905
12104
  function accept(event) {
11906
- buffer.push(event);
12105
+ const buffered = { id: nextEventId++, event };
12106
+ buffer.push(buffered);
11907
12107
  if (buffer.length > bufferSize)
11908
12108
  buffer.shift();
11909
12109
  options.onEvent?.(event);
11910
- const data = frame(event);
12110
+ const data = frame(buffered);
11911
12111
  for (const subscriber of subscribers)
11912
12112
  subscriber.res.write(data);
11913
12113
  }
@@ -11935,7 +12135,7 @@ function createReceiver(options = {}) {
11935
12135
  return;
11936
12136
  }
11937
12137
  accept({ harness, received_at: (/* @__PURE__ */ new Date()).toISOString(), payload: parsed });
11938
- sendText(res, 200, "ok");
12138
+ sendJson(res, 200, {});
11939
12139
  }, (error) => sendText(res, 400, error instanceof Error ? error.message : "\u8BFB\u53D6\u8BF7\u6C42\u4F53\u5931\u8D25"));
11940
12140
  }
11941
12141
  function handleEvents(req, res) {
@@ -11951,8 +12151,12 @@ function createReceiver(options = {}) {
11951
12151
  res.on("error", () => {
11952
12152
  });
11953
12153
  res.write(": connected\n\n");
11954
- for (const event of buffer)
11955
- res.write(frame(event));
12154
+ const latestId = buffer[buffer.length - 1]?.id ?? 0;
12155
+ const lastEventId = parseLastEventId(req, latestId);
12156
+ for (const event of buffer) {
12157
+ if (lastEventId === void 0 || event.id > lastEventId)
12158
+ res.write(frame(event));
12159
+ }
11956
12160
  const subscriber = {
11957
12161
  res,
11958
12162
  heartbeat: setInterval(() => res.write(": heartbeat\n\n"), heartbeatMs)
@@ -11979,11 +12183,22 @@ function createReceiver(options = {}) {
11979
12183
  close
11980
12184
  };
11981
12185
  }
11982
- function frame(event) {
11983
- return `data: ${JSON.stringify(event)}
12186
+ function frame({ id, event }) {
12187
+ return `id: ${id}
12188
+ data: ${JSON.stringify(event)}
11984
12189
 
11985
12190
  `;
11986
12191
  }
12192
+ function parseLastEventId(req, latestId) {
12193
+ const header = req.headers["last-event-id"];
12194
+ const value = Array.isArray(header) ? header[0] : header;
12195
+ if (value === void 0 || !/^\d+$/.test(value))
12196
+ return void 0;
12197
+ const id = Number(value);
12198
+ if (!Number.isSafeInteger(id) || id > latestId)
12199
+ return void 0;
12200
+ return id;
12201
+ }
11987
12202
  function isHarnessId2(value) {
11988
12203
  return typeof value === "string" && HARNESS_IDS.includes(value);
11989
12204
  }
@@ -12011,6 +12226,10 @@ function readBody(req) {
12011
12226
  req.on("error", reject);
12012
12227
  });
12013
12228
  }
12229
+ function sendJson(res, status, body) {
12230
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
12231
+ res.end(JSON.stringify(body));
12232
+ }
12014
12233
  function sendText(res, status, body) {
12015
12234
  res.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
12016
12235
  res.end(body);
@@ -12098,6 +12317,13 @@ function verifyEntitlement(raw, now, publicKeyPem) {
12098
12317
  }
12099
12318
  return { state: "expired", detail: `\u8BA2\u9605\u5DF2\u4E8E ${claims.exp} \u5230\u671F\u4E14\u8D85\u51FA\u5BBD\u9650\u671F,\u5199\u52A8\u4F5C\u5DF2\u9501`, claims };
12100
12319
  }
12320
+ function nextLicenseBackupPath(licensePath, now) {
12321
+ const base = `${licensePath}.bak-${now.toISOString().replaceAll(":", "-")}`;
12322
+ let candidate = base;
12323
+ for (let index = 2; existsSync(candidate); index++)
12324
+ candidate = `${base}-${index}`;
12325
+ return candidate;
12326
+ }
12101
12327
  function installLicense(raw, options = {}) {
12102
12328
  let parsed;
12103
12329
  try {
@@ -12121,8 +12347,9 @@ function installLicense(raw, options = {}) {
12121
12347
  try {
12122
12348
  mkdirSync(dir, { recursive: true });
12123
12349
  writeFileSync(tmpPath, raw);
12124
- if (existsSync(licensePath))
12125
- renameSync(licensePath, `${licensePath}.bak`);
12350
+ if (existsSync(licensePath)) {
12351
+ renameSync(licensePath, nextLicenseBackupPath(licensePath, options.now ?? /* @__PURE__ */ new Date()));
12352
+ }
12126
12353
  renameSync(tmpPath, licensePath);
12127
12354
  } catch (error) {
12128
12355
  const message = error instanceof Error ? error.message : String(error);
@@ -12132,7 +12359,7 @@ function installLicense(raw, options = {}) {
12132
12359
  }
12133
12360
 
12134
12361
  // apps/cli/dist/license-refresh.js
12135
- import { readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
12362
+ import { copyFileSync, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
12136
12363
  import { homedir as homedir2 } from "node:os";
12137
12364
  import { dirname as dirname2, join as join3 } from "node:path";
12138
12365
  var DEFAULT_LICENSE_SERVER = "https://api.getwingman.dev";
@@ -12229,6 +12456,7 @@ async function refreshLicense(options = {}) {
12229
12456
  const tmpPath = join3(dirname2(licensePath), `.license.json.tmp-${process.pid}`);
12230
12457
  try {
12231
12458
  writeFileSync2(tmpPath, next);
12459
+ copyFileSync(licensePath, nextLicenseBackupPath(licensePath, options.now ?? /* @__PURE__ */ new Date()));
12232
12460
  renameSync2(tmpPath, licensePath);
12233
12461
  } catch (error) {
12234
12462
  const message = error instanceof Error ? error.message : String(error);
@@ -12901,10 +13129,16 @@ function applyTodoWrite(input, tasks) {
12901
13129
  if (!Array.isArray(todos))
12902
13130
  return false;
12903
13131
  tasks.clear();
13132
+ const occurrences = /* @__PURE__ */ new Map();
12904
13133
  for (const raw of todos) {
12905
13134
  if (!isRecord3(raw) || typeof raw.content !== "string" || !isTodoStatus(raw.status))
12906
13135
  continue;
12907
- const item = { id: raw.content, content: raw.content, status: raw.status };
13136
+ const occurrence = (occurrences.get(raw.content) ?? 0) + 1;
13137
+ occurrences.set(raw.content, occurrence);
13138
+ const id = occurrence === 1 ? raw.content : `${raw.content}#${occurrence}`;
13139
+ const item = { id, content: raw.content, status: raw.status };
13140
+ if (typeof raw.description === "string")
13141
+ item.description = raw.description;
12908
13142
  if (typeof raw.activeForm === "string")
12909
13143
  item.activeForm = raw.activeForm;
12910
13144
  tasks.set(item.id, item);
@@ -12916,6 +13150,8 @@ function applyTaskCreate(input, toolUseId, createResultIds, tasks) {
12916
13150
  return false;
12917
13151
  const id = createResultIds.get(toolUseId) ?? toolUseId;
12918
13152
  const item = { id, content: input.subject, status: "pending" };
13153
+ if (typeof input.description === "string")
13154
+ item.description = input.description;
12919
13155
  if (typeof input.activeForm === "string")
12920
13156
  item.activeForm = input.activeForm;
12921
13157
  tasks.set(id, item);
@@ -12927,11 +13163,28 @@ function applyTaskUpdate(input, tasks) {
12927
13163
  const existing = tasks.get(input.taskId);
12928
13164
  if (existing === void 0)
12929
13165
  return false;
13166
+ if (input.status === "deleted") {
13167
+ tasks.delete(input.taskId);
13168
+ return true;
13169
+ }
13170
+ let changed = false;
12930
13171
  if (isTodoStatus(input.status) && input.status !== existing.status) {
12931
13172
  existing.status = input.status;
12932
- return true;
13173
+ changed = true;
12933
13174
  }
12934
- return false;
13175
+ if (typeof input.subject === "string" && input.subject !== existing.content) {
13176
+ existing.content = input.subject;
13177
+ changed = true;
13178
+ }
13179
+ if (typeof input.description === "string" && input.description !== existing.description) {
13180
+ existing.description = input.description;
13181
+ changed = true;
13182
+ }
13183
+ if (typeof input.activeForm === "string" && input.activeForm !== existing.activeForm) {
13184
+ existing.activeForm = input.activeForm;
13185
+ changed = true;
13186
+ }
13187
+ return changed;
12935
13188
  }
12936
13189
  function snapshotOf(tasks, ts) {
12937
13190
  const snapshot = { todos: [...tasks.values()].map((item) => ({ ...item })) };
@@ -12961,6 +13214,10 @@ function deriveTodoTimeline(snapshots) {
12961
13214
  if (todo.activeForm !== void 0)
12962
13215
  existing.activeForm = todo.activeForm;
12963
13216
  const last = existing.spans[existing.spans.length - 1];
13217
+ if (last !== void 0 && last.to !== void 0) {
13218
+ existing.spans.push(newSpan(todo.status, snapshot.ts));
13219
+ continue;
13220
+ }
12964
13221
  if (last !== void 0 && last.to === void 0 && last.status !== todo.status) {
12965
13222
  if (snapshot.ts !== void 0)
12966
13223
  last.to = snapshot.ts;
@@ -13045,10 +13302,155 @@ function costUsd(usage, model) {
13045
13302
  return (usage.inputTokens * price.inputPerMTok + usage.cacheReadInputTokens * price.inputPerMTok * CACHE_READ_MULTIPLIER + cacheWriteTokensPriced * price.inputPerMTok + usage.outputTokens * price.outputPerMTok) / 1e6;
13046
13303
  }
13047
13304
 
13305
+ // packages/narrative/dist/workflow.js
13306
+ function deriveTaskWorkflowView(input) {
13307
+ return {
13308
+ ...input.goal === void 0 ? {} : { goal: input.goal },
13309
+ ...input.latestInstruction === void 0 ? {} : { latestInstruction: input.latestInstruction },
13310
+ plan: deriveTaskPlan(input.harness, input.lines, input.codexLines),
13311
+ execution: input.execution,
13312
+ feedback: input.feedback
13313
+ };
13314
+ }
13315
+ function derivePlanRevisions(observations) {
13316
+ const revisions = [];
13317
+ let revision = 0;
13318
+ let previousKey;
13319
+ for (const observation of observations) {
13320
+ const structureKey = JSON.stringify(observation.steps.map((step) => [step.text, step.description ?? null]));
13321
+ if (structureKey !== previousKey) {
13322
+ revision += 1;
13323
+ previousKey = structureKey;
13324
+ }
13325
+ const snapshot = {
13326
+ revision,
13327
+ steps: observation.steps.map((step, index) => ({
13328
+ id: `${revision}:${index + 1}`,
13329
+ text: step.text,
13330
+ ...step.description === void 0 ? {} : { description: step.description },
13331
+ status: step.status
13332
+ }))
13333
+ };
13334
+ if (observation.observedAt !== void 0)
13335
+ snapshot.observedAt = observation.observedAt;
13336
+ revisions.push(snapshot);
13337
+ }
13338
+ return revisions;
13339
+ }
13340
+ function deriveTaskPlan(harness, lines = [], codexLines = []) {
13341
+ if (harness === "cc")
13342
+ return deriveClaudeTaskPlan(lines);
13343
+ if (harness === "codex")
13344
+ return deriveCodexTaskPlan(codexLines);
13345
+ return {
13346
+ availability: "unavailable",
13347
+ source: "dsh 0.1.1-rc.2",
13348
+ reason: "dsh \u6CA1\u6709\u5411 Wingman hooks \u66B4\u9732\u7A33\u5B9A\u7684\u672C\u5730 todo projection"
13349
+ };
13350
+ }
13351
+ function deriveClaudeTaskPlan(lines) {
13352
+ const source = "Claude Code TaskCreate/TaskUpdate/TodoWrite";
13353
+ if (latestClaudePlanPayloadValid(lines) === false) {
13354
+ return {
13355
+ availability: "unavailable",
13356
+ source,
13357
+ reason: "Claude Code \u8BA1\u5212\u5DE5\u5177\u53C2\u6570\u65E0\u6CD5\u89E3\u6790"
13358
+ };
13359
+ }
13360
+ const observations = extractTodoSnapshots(lines).map((snapshot) => ({
13361
+ ...snapshot.ts === void 0 ? {} : { observedAt: snapshot.ts },
13362
+ steps: snapshot.todos.map((todo) => ({
13363
+ text: todo.content,
13364
+ ...todo.description === void 0 ? {} : { description: todo.description },
13365
+ status: todo.status
13366
+ }))
13367
+ }));
13368
+ if (observations.length === 0)
13369
+ return { availability: "missing", source };
13370
+ return availablePlan(source, derivePlanRevisions(observations));
13371
+ }
13372
+ function latestClaudePlanPayloadValid(lines) {
13373
+ let latest;
13374
+ for (const line of lines) {
13375
+ if (line.kind !== "assistant")
13376
+ continue;
13377
+ for (const block of line.blocks) {
13378
+ if (block.kind !== "tool_use")
13379
+ continue;
13380
+ if (block.name === "TodoWrite") {
13381
+ latest = Array.isArray(block.input.todos) && block.input.todos.every((todo) => isRecord4(todo) && typeof todo.content === "string" && isTodoStatus2(todo.status));
13382
+ } else if (block.name === "TaskCreate") {
13383
+ latest = typeof block.input.subject === "string";
13384
+ } else if (block.name === "TaskUpdate") {
13385
+ latest = typeof block.input.taskId === "string" && (block.input.status === void 0 || block.input.status === "deleted" || isTodoStatus2(block.input.status)) && (block.input.subject === void 0 || typeof block.input.subject === "string") && (block.input.description === void 0 || typeof block.input.description === "string") && (block.input.activeForm === void 0 || typeof block.input.activeForm === "string");
13386
+ }
13387
+ }
13388
+ }
13389
+ return latest;
13390
+ }
13391
+ function deriveCodexTaskPlan(lines) {
13392
+ const source = "Codex update_plan";
13393
+ const calls = lines.filter((line) => line.kind === "tool_call" && line.name === "update_plan");
13394
+ if (calls.length === 0)
13395
+ return { availability: "missing", source };
13396
+ const observations = [];
13397
+ let latestValid = false;
13398
+ for (const call of calls) {
13399
+ const observation = parseCodexPlan(call.input, call.timestamp);
13400
+ latestValid = observation !== null;
13401
+ if (observation !== null)
13402
+ observations.push(observation);
13403
+ }
13404
+ if (!latestValid) {
13405
+ return {
13406
+ availability: "unavailable",
13407
+ source,
13408
+ reason: "Codex update_plan \u53C2\u6570\u65E0\u6CD5\u89E3\u6790"
13409
+ };
13410
+ }
13411
+ return availablePlan(source, derivePlanRevisions(observations));
13412
+ }
13413
+ function parseCodexPlan(input, observedAt) {
13414
+ if (!isRecord4(input) || !Array.isArray(input.plan))
13415
+ return null;
13416
+ const steps = [];
13417
+ for (const item of input.plan) {
13418
+ if (!isRecord4(item) || typeof item.step !== "string" || !isTodoStatus2(item.status))
13419
+ return null;
13420
+ const text = item.step.trim();
13421
+ if (text === "")
13422
+ return null;
13423
+ steps.push({ text, status: item.status });
13424
+ }
13425
+ return {
13426
+ ...observedAt === void 0 ? {} : { observedAt },
13427
+ steps
13428
+ };
13429
+ }
13430
+ function availablePlan(source, revisions) {
13431
+ const latest = revisions[revisions.length - 1];
13432
+ if (latest === void 0)
13433
+ return { availability: "missing", source };
13434
+ return {
13435
+ availability: "available",
13436
+ source,
13437
+ revision: latest.revision,
13438
+ ...latest.observedAt === void 0 ? {} : { observedAt: latest.observedAt },
13439
+ semantics: "current_snapshot",
13440
+ steps: latest.steps
13441
+ };
13442
+ }
13443
+ function isTodoStatus2(value) {
13444
+ return value === "pending" || value === "in_progress" || value === "completed";
13445
+ }
13446
+ function isRecord4(value) {
13447
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13448
+ }
13449
+
13048
13450
  // apps/cli/dist/narrative-service.js
13049
13451
  var MAX_SESSIONS = 8;
13050
13452
  var ENDED_AFTER_STOP_MS = 10 * 6e4;
13051
- var TODO_TOOLS = /* @__PURE__ */ new Set(["TaskCreate", "TaskUpdate", "TodoWrite"]);
13453
+ var TODO_TOOLS = /* @__PURE__ */ new Set(["TaskCreate", "TaskUpdate", "TodoWrite", "update_plan"]);
13052
13454
  var DETAIL_LIMIT = 80;
13053
13455
  var QUOTE_LIMIT = 160;
13054
13456
  var ERROR_BRIEF_LIMIT = 60;
@@ -13095,6 +13497,7 @@ function createNarrativeService(options = {}) {
13095
13497
  function newSession(id) {
13096
13498
  return {
13097
13499
  id,
13500
+ generation: 0,
13098
13501
  lines: [],
13099
13502
  codexLines: [],
13100
13503
  follows: /* @__PURE__ */ new Map(),
@@ -13105,6 +13508,7 @@ function createNarrativeService(options = {}) {
13105
13508
  };
13106
13509
  }
13107
13510
  function closeSession(session) {
13511
+ session.generation += 1;
13108
13512
  for (const handle2 of session.follows.values())
13109
13513
  handle2.close();
13110
13514
  session.follows.clear();
@@ -13155,10 +13559,12 @@ function createNarrativeService(options = {}) {
13155
13559
  session.transcriptPath = path;
13156
13560
  session.dirty = true;
13157
13561
  }
13158
- function startFollowing(harness, session, path) {
13159
- if (closed || session.follows.has(path))
13562
+ function startFollowing(harness, session, path, generation = session.generation) {
13563
+ if (closed || generation !== session.generation || session.follows.has(path))
13160
13564
  return;
13161
13565
  session.follows.set(path, followTranscript(path, (line) => {
13566
+ if (closed || generation !== session.generation)
13567
+ return;
13162
13568
  if (harness === "codex")
13163
13569
  session.codexLines.push(parseCodexRolloutLine(line));
13164
13570
  else
@@ -13172,12 +13578,16 @@ function createNarrativeService(options = {}) {
13172
13578
  if (sessionDir === transcriptPath)
13173
13579
  return;
13174
13580
  const subagentsDir = join4(sessionDir, "subagents");
13581
+ const generation = session.generation;
13175
13582
  const poll = async () => {
13176
13583
  try {
13177
13584
  const entries = await readdir(subagentsDir);
13585
+ if (closed || generation !== session.generation || session.transcriptPath !== transcriptPath) {
13586
+ return;
13587
+ }
13178
13588
  for (const entry of entries) {
13179
13589
  if (/^agent-.*\.jsonl$/.test(entry)) {
13180
- startFollowing(harness, session, join4(subagentsDir, entry));
13590
+ startFollowing(harness, session, join4(subagentsDir, entry), generation);
13181
13591
  }
13182
13592
  }
13183
13593
  } catch {
@@ -13240,14 +13650,20 @@ function createNarrativeService(options = {}) {
13240
13650
  } else if (name === "PostToolUse" && typeof payload.tool_name === "string" || name === "Stop" || name === "SessionEnd") {
13241
13651
  session.pendingHookTool = void 0;
13242
13652
  }
13243
- if (name === "PermissionRequest" || name === "Notification") {
13244
- const kind = name === "PermissionRequest" ? "permission" : "notification";
13245
- const message = waitingMessage(payload, kind === "permission" ? "\u7B49\u5F85\u6388\u6743" : "\u7B49\u5F85\u56DE\u5E94");
13653
+ const signal = waitingSignal(payload);
13654
+ if (signal !== null) {
13655
+ const { kind, message } = signal;
13246
13656
  if (session.waiting === null) {
13247
13657
  session.waiting = { kind, message, since: event.received_at };
13248
13658
  } else if (kind === "permission" || session.waiting.kind === kind) {
13249
13659
  session.waiting = { kind, message, since: session.waiting.since };
13250
13660
  }
13661
+ session.feedbackState = {
13662
+ state: "needs_user",
13663
+ source: `${event.harness} ${String(name)} signal`,
13664
+ since: session.waiting?.since ?? event.received_at,
13665
+ ...message === "" ? {} : { message }
13666
+ };
13251
13667
  } else {
13252
13668
  if (session.waiting !== null) {
13253
13669
  session.waitEpisodes.push({
@@ -13257,6 +13673,18 @@ function createNarrativeService(options = {}) {
13257
13673
  });
13258
13674
  session.waiting = null;
13259
13675
  }
13676
+ const resetFeedback = name === "UserPromptSubmit" || name === "Stop" || name === "SessionEnd";
13677
+ if (resetFeedback) {
13678
+ session.feedbackState = void 0;
13679
+ } else if (session.feedbackState?.state === "needs_user") {
13680
+ session.feedbackState = {
13681
+ state: "activity_resumed_unknown",
13682
+ source: session.feedbackState.source,
13683
+ requestSince: session.feedbackState.since,
13684
+ resumedAt: event.received_at,
13685
+ ...session.feedbackState.message === void 0 ? {} : { message: session.feedbackState.message }
13686
+ };
13687
+ }
13260
13688
  }
13261
13689
  if (name === "SessionEnd") {
13262
13690
  session.endedAt = event.received_at;
@@ -13320,16 +13748,31 @@ function createNarrativeService(options = {}) {
13320
13748
  function getOverview(harness) {
13321
13749
  const nowIso = now().toISOString();
13322
13750
  const sessions = sessionsOf(harness);
13751
+ let feedback;
13752
+ try {
13753
+ feedback = options.feedbackSignals?.(harness);
13754
+ } catch {
13755
+ feedback = void 0;
13756
+ }
13323
13757
  const infos = [...sessions.values()].map((session) => ({
13324
13758
  id: session.id,
13325
13759
  label: session.label ?? session.id.slice(0, 8),
13326
13760
  lastActivity: lastActivityOf(session),
13327
13761
  waiting: session.waiting !== null,
13328
- ended: isEnded(session, nowIso)
13762
+ ended: isEnded(session, nowIso),
13763
+ ...feedback === void 0 ? {} : { feedback }
13329
13764
  })).sort((a, b) => a.lastActivity < b.lastActivity ? 1 : a.lastActivity > b.lastActivity ? -1 : 0);
13330
13765
  const states = {};
13331
- for (const [id, session] of sessions)
13332
- states[id] = deriveCached(harness, session);
13766
+ for (const [id, session] of sessions) {
13767
+ const state = deriveCached(harness, session);
13768
+ states[id] = state.taskWorkflow === void 0 ? state : {
13769
+ ...state,
13770
+ taskWorkflow: {
13771
+ ...state.taskWorkflow,
13772
+ feedback: feedbackView(harness, feedback, session.feedbackState)
13773
+ }
13774
+ };
13775
+ }
13333
13776
  return { harness, sessions: infos, states };
13334
13777
  }
13335
13778
  function getState(harness) {
@@ -13422,6 +13865,44 @@ function waitingMessage(payload, fallback) {
13422
13865
  }
13423
13866
  return fallback;
13424
13867
  }
13868
+ function waitingSignal(payload) {
13869
+ const name = payload.hook_event_name;
13870
+ if (name === "PermissionRequest") {
13871
+ const toolName = typeof payload.tool_name === "string" && payload.tool_name !== "" ? payload.tool_name : "";
13872
+ const detail = hookToolDetail(payload.tool_input);
13873
+ const reason = [toolName, detail].filter((part) => part !== "").join(" \xB7 ");
13874
+ return {
13875
+ kind: "permission",
13876
+ message: reason === "" ? waitingMessage(payload, "\u7B49\u5F85\u6388\u6743") : `\u7B49\u5F85\u6388\u6743:${reason}`
13877
+ };
13878
+ }
13879
+ if (name !== "Notification")
13880
+ return null;
13881
+ switch (payload.notification_type) {
13882
+ case "permission_prompt":
13883
+ return { kind: "permission", message: waitingMessage(payload, "\u7B49\u5F85\u6388\u6743") };
13884
+ case "idle_prompt":
13885
+ case "elicitation_dialog":
13886
+ return { kind: "notification", message: waitingMessage(payload, "\u7B49\u5F85\u56DE\u5E94") };
13887
+ default:
13888
+ return null;
13889
+ }
13890
+ }
13891
+ function feedbackView(harness, capability, state) {
13892
+ if (capability === "not_wired") {
13893
+ return {
13894
+ state: "unavailable",
13895
+ reason: `\u53CD\u9988\u4FE1\u53F7\u672A\u63A5\u7EBF\uFF1B\u8BF7\u8FD0\u884C npx harness-wingman hook install ${harness}`
13896
+ };
13897
+ }
13898
+ if (capability === "unavailable") {
13899
+ return { state: "unavailable", reason: "\u53CD\u9988\u4FE1\u53F7\u4E0D\u53EF\u7528" };
13900
+ }
13901
+ if (capability !== "available") {
13902
+ return { state: "unavailable", reason: "\u53CD\u9988\u80FD\u529B\u672A\u62A5\u544A" };
13903
+ }
13904
+ return state ?? { state: "no_open_request", source: `${harness} explicit feedback signals` };
13905
+ }
13425
13906
  function derive(harness, session, nowIso) {
13426
13907
  const codexLines = sortedCodexByTimestamp(session.codexLines);
13427
13908
  const lines = harness === "codex" ? adaptCodexNarrativeLines(codexLines) : sortedByTimestamp(session.lines);
@@ -13509,6 +13990,30 @@ function derive(harness, session, nowIso) {
13509
13990
  };
13510
13991
  });
13511
13992
  const textTodos = todos.length === 0 ? deriveTextTodos(lines) : void 0;
13993
+ const currentAction = deriveCurrentAction(steps, session.lastEvent, session.pendingHookTool);
13994
+ const lastPrompt = deriveLastPrompt(lines, session.hookPrompt, session.hookPromptAt);
13995
+ const workflowAction = currentAction === null ? void 0 : currentAction.kind === "tool" ? {
13996
+ text: [currentAction.toolName, currentAction.detail].filter((part) => part !== "").join(": "),
13997
+ source: "transcript / hook tool event",
13998
+ ...currentAction.since === void 0 ? {} : { observedAt: currentAction.since }
13999
+ } : { text: currentAction.name, source: "hook event", observedAt: currentAction.at };
14000
+ const taskWorkflow = deriveTaskWorkflowView({
14001
+ harness,
14002
+ lines,
14003
+ codexLines,
14004
+ ...lastPrompt === null ? {} : {
14005
+ latestInstruction: {
14006
+ text: lastPrompt,
14007
+ source: "transcript user message / UserPromptSubmit"
14008
+ }
14009
+ },
14010
+ execution: {
14011
+ ...workflowAction === void 0 ? {} : { currentAction: workflowAction },
14012
+ evidenceCount: steps.length + session.waitEpisodes.length + (session.waiting === null ? 0 : 1),
14013
+ failureCount: steps.filter((step) => step.isError).length
14014
+ },
14015
+ feedback: { state: "unavailable", reason: "\u53CD\u9988\u80FD\u529B\u5C1A\u672A\u6620\u5C04" }
14016
+ });
13512
14017
  return {
13513
14018
  harness,
13514
14019
  sessionId: session.id,
@@ -13518,11 +14023,12 @@ function derive(harness, session, nowIso) {
13518
14023
  ...textTodos !== void 0 && textTodos.length > 0 ? { textTodos } : {},
13519
14024
  traces,
13520
14025
  turnUsages: harness === "codex" ? deriveCodexTurnUsages(codexLines) : deriveTurnUsages(lines),
13521
- currentAction: deriveCurrentAction(steps, session.lastEvent, session.pendingHookTool),
14026
+ currentAction,
13522
14027
  waiting: session.waiting,
13523
14028
  agentQuote: deriveAgentQuote(lines),
13524
14029
  // transcript 是会话事实源;hook prompt 只在 tail 尚未读到/格式暂不可读时兜底。
13525
- lastPrompt: deriveLastPrompt(lines, session.hookPrompt, session.hookPromptAt),
14030
+ lastPrompt,
14031
+ taskWorkflow,
13526
14032
  updatedAt: nowIso
13527
14033
  };
13528
14034
  }
@@ -13561,22 +14067,26 @@ function deriveTextTodos(lines) {
13561
14067
  return void 0;
13562
14068
  }
13563
14069
  function sortedByTimestamp(lines) {
13564
- return [...lines].sort((a, b) => {
13565
- const ta = a.kind === "unknown" ? void 0 : a.timestamp;
13566
- const tb = b.kind === "unknown" ? void 0 : b.timestamp;
13567
- if (ta === void 0 || tb === void 0)
13568
- return 0;
13569
- return ta < tb ? -1 : ta > tb ? 1 : 0;
13570
- });
14070
+ return sortByInheritedTimestamp(lines, (line) => line.kind === "unknown" ? void 0 : line.timestamp, (line) => line.kind === "unknown" ? "" : line.agentId ?? "");
13571
14071
  }
13572
14072
  function sortedCodexByTimestamp(lines) {
13573
- return [...lines].sort((a, b) => {
13574
- const ta = a.kind === "unknown" ? void 0 : a.timestamp;
13575
- const tb = b.kind === "unknown" ? void 0 : b.timestamp;
13576
- if (ta === void 0 || tb === void 0)
13577
- return 0;
13578
- return ta < tb ? -1 : ta > tb ? 1 : 0;
13579
- });
14073
+ return sortByInheritedTimestamp(lines, (line) => line.kind === "unknown" ? void 0 : line.timestamp);
14074
+ }
14075
+ function sortByInheritedTimestamp(lines, timestampOf, sourceOf = () => "") {
14076
+ const previousBySource = /* @__PURE__ */ new Map();
14077
+ return lines.map((line, index) => {
14078
+ const source = sourceOf(line);
14079
+ const timestamp = timestampOf(line) ?? previousBySource.get(source);
14080
+ if (timestamp !== void 0)
14081
+ previousBySource.set(source, timestamp);
14082
+ return { line, index, timestamp };
14083
+ }).sort((a, b) => {
14084
+ if (a.timestamp === void 0)
14085
+ return b.timestamp === void 0 ? a.index - b.index : -1;
14086
+ if (b.timestamp === void 0)
14087
+ return 1;
14088
+ return a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : a.index - b.index;
14089
+ }).map(({ line }) => line);
13580
14090
  }
13581
14091
  function adaptCodexNarrativeLines(lines) {
13582
14092
  const responseTexts = new Set(lines.flatMap((line) => line.kind === "assistant" && line.source === "response_item" && line.text !== void 0 ? [line.text] : []));
@@ -13990,12 +14500,16 @@ function errorBrief(content) {
13990
14500
 
13991
14501
  // apps/cli/dist/registry.js
13992
14502
  import { existsSync as existsSync10 } from "node:fs";
13993
- async function importFactory(harness) {
14503
+ import { homedir as homedir9 } from "node:os";
14504
+ async function importHarnessModule(harness) {
13994
14505
  const mod = await importAdapterModule(harness);
13995
14506
  if (typeof mod.createAdapter !== "function") {
13996
14507
  throw new Error(`adapter-${harness} \u5C1A\u672A\u5BFC\u51FA createAdapter(M1 \u5E76\u884C\u7EBF\u672A\u5408\u5E76?)`);
13997
14508
  }
13998
- return mod.createAdapter;
14509
+ if (typeof mod.feedbackSignals !== "function") {
14510
+ throw new Error(`adapter-${harness} \u5C1A\u672A\u5BFC\u51FA feedbackSignals`);
14511
+ }
14512
+ return { createAdapter: mod.createAdapter, feedbackSignals: mod.feedbackSignals };
13999
14513
  }
14000
14514
  function importAdapterModule(harness) {
14001
14515
  switch (harness) {
@@ -14021,19 +14535,20 @@ function fixtureScanOptions(harness) {
14021
14535
  }
14022
14536
  }
14023
14537
  async function loadHarness(harness, mode) {
14024
- const createAdapter4 = await importFactory(harness);
14538
+ const { createAdapter: createAdapter4, feedbackSignals } = await importHarnessModule(harness);
14025
14539
  const adapter = harness === "dsh" && mode.fixtures ? createAdapter4({ dumpConfigPath: fixturePath("dsh", "dump-config.yaml") }) : createAdapter4();
14026
14540
  return {
14027
14541
  harness,
14028
14542
  adapter,
14029
- scanOptions: mode.fixtures ? fixtureScanOptions(harness) : {}
14543
+ scanOptions: mode.fixtures ? fixtureScanOptions(harness) : { homeDir: homedir9() },
14544
+ feedbackSignals
14030
14545
  };
14031
14546
  }
14032
14547
 
14033
14548
  // apps/cli/dist/runtime-state.js
14034
14549
  import { randomBytes as randomBytes2 } from "node:crypto";
14035
14550
  import { closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync11, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "node:fs";
14036
- import { homedir as homedir9 } from "node:os";
14551
+ import { homedir as homedir10 } from "node:os";
14037
14552
  import { dirname as dirname11, join as join17 } from "node:path";
14038
14553
  function createHookToken() {
14039
14554
  return randomBytes2(32).toString("base64url");
@@ -14045,7 +14560,7 @@ function writeServeRuntimeState(port, token, options = {}) {
14045
14560
  const pid = options.pid ?? process.pid;
14046
14561
  const suffix = randomBytes2(8).toString("base64url");
14047
14562
  const owner = `serve-${startedAtMs}-${pid}-${suffix}`;
14048
- const path = join17(options.homeDir ?? homedir9(), ".wingman", "state", "serve", `${owner}.json`);
14563
+ const path = join17(options.homeDir ?? homedir10(), ".wingman", "state", "serve", `${owner}.json`);
14049
14564
  const tempPath = join17(dirname11(path), `.${owner}.tmp`);
14050
14565
  const contents = Buffer.from(`${JSON.stringify({ version: 2, owner, pid, port, startedAtMs, token })}
14051
14566
  `, "utf8");
@@ -14267,10 +14782,11 @@ var HOOK_IDENTITY_PATH = "/api/hook-target";
14267
14782
  var HOOK_TOKEN_HEADER = "x-wingman-hook-token";
14268
14783
  var HOOK_PROOF_HEADER = "x-wingman-hook-proof";
14269
14784
  var PANEL_ACTION_HEADER = "x-wingman-panel-action";
14785
+ var LOCAL_HOST = /^(?:127\.0\.0\.1|localhost)(?::\d+)?$|^\[::1\](?::\d+)?$/i;
14270
14786
  function createWingmanServer(options) {
14271
14787
  const warn = options.warn ?? ((message) => console.error(message));
14272
14788
  const runtime = { anatomy: null };
14273
- return createServer((req, res) => {
14789
+ return createServer({ requireHostHeader: false }, (req, res) => {
14274
14790
  handle(req, res, options, warn, runtime).catch((error) => {
14275
14791
  warn(`[wingman] \u8BF7\u6C42\u5904\u7406\u5931\u8D25:${errorMessage2(error)}`);
14276
14792
  if (!res.headersSent)
@@ -14290,11 +14806,15 @@ function listen(server, port) {
14290
14806
  });
14291
14807
  }
14292
14808
  async function handle(req, res, options, warn, runtime) {
14809
+ if (typeof req.headers.host !== "string" || !LOCAL_HOST.test(req.headers.host)) {
14810
+ sendJson2(res, 421, { error: "Host \u53EA\u63A5\u53D7\u672C\u673A\u5730\u5740" });
14811
+ return;
14812
+ }
14293
14813
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
14294
14814
  if (req.method !== "GET") {
14295
14815
  const origin = req.headers.origin;
14296
14816
  if (typeof origin === "string" && origin !== "" && !/^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origin)) {
14297
- sendJson(res, 403, { error: `\u62D2\u7EDD\u5F02\u6E90\u5199\u8BF7\u6C42(Origin: ${origin})` });
14817
+ sendJson2(res, 403, { error: `\u62D2\u7EDD\u5F02\u6E90\u5199\u8BF7\u6C42(Origin: ${origin})` });
14298
14818
  return;
14299
14819
  }
14300
14820
  }
@@ -14353,7 +14873,7 @@ async function handle(req, res, options, warn, runtime) {
14353
14873
  return;
14354
14874
  }
14355
14875
  if (req.headers[PANEL_ACTION_HEADER] !== "refresh-license") {
14356
- sendJson(res, 403, { error: "\u5237\u65B0\u6388\u6743\u5FC5\u987B\u7531 Wingman \u9762\u677F\u4E2D\u7684\u6309\u94AE\u89E6\u53D1" });
14876
+ sendJson2(res, 403, { error: "\u5237\u65B0\u6388\u6743\u5FC5\u987B\u7531 Wingman \u9762\u677F\u4E2D\u7684\u6309\u94AE\u89E6\u53D1" });
14357
14877
  return;
14358
14878
  }
14359
14879
  await handleLicenseRefresh(res, options);
@@ -14364,7 +14884,7 @@ async function handle(req, res, options, warn, runtime) {
14364
14884
  return;
14365
14885
  }
14366
14886
  if (url.pathname === "/api/license") {
14367
- sendJson(res, 200, publicLicenseStatus(licenseStatus(options.licenseOptions)));
14887
+ sendJson2(res, 200, publicLicenseStatus(licenseStatus(options.licenseOptions)));
14368
14888
  return;
14369
14889
  }
14370
14890
  if (url.pathname === "/api/narrative") {
@@ -14374,7 +14894,7 @@ async function handle(req, res, options, warn, runtime) {
14374
14894
  }
14375
14895
  const harness = url.searchParams.get("harness");
14376
14896
  if (typeof harness !== "string" || !HARNESS_IDS.includes(harness)) {
14377
- sendJson(res, 400, {
14897
+ sendJson2(res, 400, {
14378
14898
  error: `harness \u9700\u8981 ${HARNESS_IDS.join("|")}(?harness=<id>),\u5F97\u5230 ${String(harness)}`
14379
14899
  });
14380
14900
  return;
@@ -14390,7 +14910,7 @@ async function handle(req, res, options, warn, runtime) {
14390
14910
  }
14391
14911
  overview.states = states;
14392
14912
  }
14393
- sendJson(res, 200, overview);
14913
+ sendJson2(res, 200, overview);
14394
14914
  return;
14395
14915
  }
14396
14916
  if (url.pathname === "/api/anatomy") {
@@ -14508,48 +15028,48 @@ function errorSummary(error) {
14508
15028
  async function handleActionPlan(req, res, options) {
14509
15029
  const body = await readJsonBody(req);
14510
15030
  if (body === void 0) {
14511
- sendJson(res, 400, { error: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F\u5408\u6CD5 JSON" });
15031
+ sendJson2(res, 400, { error: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F\u5408\u6CD5 JSON" });
14512
15032
  return;
14513
15033
  }
14514
15034
  const { harness, action } = body;
14515
15035
  const loaded = pickHarness(options, harness);
14516
15036
  if (!loaded) {
14517
- sendJson(res, 400, { error: `harness \u9700\u8981 ${HARNESS_IDS.join("|")},\u5F97\u5230 ${String(harness)}` });
15037
+ sendJson2(res, 400, { error: `harness \u9700\u8981 ${HARNESS_IDS.join("|")},\u5F97\u5230 ${String(harness)}` });
14518
15038
  return;
14519
15039
  }
14520
15040
  if (!isToggleRequest(action)) {
14521
- sendJson(res, 400, { error: 'action \u9700\u8981 {kind:"toggle", itemId, enable} \u5F62\u72B6' });
15041
+ sendJson2(res, 400, { error: 'action \u9700\u8981 {kind:"toggle", itemId, enable} \u5F62\u72B6' });
14522
15042
  return;
14523
15043
  }
14524
15044
  try {
14525
15045
  const plan = await loaded.adapter.planAction(action, loaded.scanOptions);
14526
- sendJson(res, 200, plan);
15046
+ sendJson2(res, 200, plan);
14527
15047
  } catch (error) {
14528
- sendJson(res, 400, { error: errorMessage2(error) });
15048
+ sendJson2(res, 400, { error: errorMessage2(error) });
14529
15049
  }
14530
15050
  }
14531
15051
  async function handleActionApply(req, res, options, runtime) {
14532
15052
  if (options.readonlyActions) {
14533
- sendJson(res, 403, { error: "fixtures \u6A21\u5F0F\u53EA\u8BFB:\u52A8\u4F5C\u53EA\u5230 plan \u4E3A\u6B62,\u4E0D\u5199\u4EFB\u4F55\u6587\u4EF6" });
15053
+ sendJson2(res, 403, { error: "fixtures \u6A21\u5F0F\u53EA\u8BFB:\u52A8\u4F5C\u53EA\u5230 plan \u4E3A\u6B62,\u4E0D\u5199\u4EFB\u4F55\u6587\u4EF6" });
14534
15054
  return;
14535
15055
  }
14536
15056
  if (!checkLicense(options.licenseOptions)) {
14537
- sendJson(res, 403, { error: "\u9700\u8981\u6709\u6548\u8BA2\u9605\u624D\u80FD\u6267\u884C\u5199\u52A8\u4F5C(anatomy \u4E0E\u5B9E\u51B5\u6C38\u4E45\u514D\u8D39)" });
15057
+ sendJson2(res, 403, { error: "\u9700\u8981\u6709\u6548\u8BA2\u9605\u624D\u80FD\u6267\u884C\u5199\u52A8\u4F5C(anatomy \u4E0E\u5B9E\u51B5\u6C38\u4E45\u514D\u8D39)" });
14538
15058
  return;
14539
15059
  }
14540
15060
  const body = await readJsonBody(req);
14541
15061
  if (body === void 0) {
14542
- sendJson(res, 400, { error: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F\u5408\u6CD5 JSON" });
15062
+ sendJson2(res, 400, { error: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F\u5408\u6CD5 JSON" });
14543
15063
  return;
14544
15064
  }
14545
15065
  const { harness, plan } = body;
14546
15066
  const loaded = pickHarness(options, harness);
14547
15067
  if (!loaded) {
14548
- sendJson(res, 400, { error: `harness \u9700\u8981 ${HARNESS_IDS.join("|")},\u5F97\u5230 ${String(harness)}` });
15068
+ sendJson2(res, 400, { error: `harness \u9700\u8981 ${HARNESS_IDS.join("|")},\u5F97\u5230 ${String(harness)}` });
14549
15069
  return;
14550
15070
  }
14551
15071
  if (typeof plan !== "object" || plan === null) {
14552
- sendJson(res, 400, { error: "plan \u9700\u8981 ActionPlan \u5BF9\u8C61(\u7531 /api/action/plan \u4EA7\u51FA)" });
15072
+ sendJson2(res, 400, { error: "plan \u9700\u8981 ActionPlan \u5BF9\u8C61(\u7531 /api/action/plan \u4EA7\u51FA)" });
14553
15073
  return;
14554
15074
  }
14555
15075
  try {
@@ -14557,24 +15077,24 @@ async function handleActionApply(req, res, options, runtime) {
14557
15077
  if (result.ok) {
14558
15078
  runtime.anatomy = null;
14559
15079
  }
14560
- sendJson(res, 200, result);
15080
+ sendJson2(res, 200, result);
14561
15081
  } catch (error) {
14562
- sendJson(res, 400, { error: errorMessage2(error) });
15082
+ sendJson2(res, 400, { error: errorMessage2(error) });
14563
15083
  }
14564
15084
  }
14565
15085
  var LICENSE_BODY_LIMIT = 64 * 1024;
14566
15086
  async function handleLicenseInstall(req, res, options) {
14567
15087
  const raw = await readTextBody(req, LICENSE_BODY_LIMIT);
14568
15088
  if (raw === void 0) {
14569
- sendJson(res, 400, { error: "license \u6587\u4EF6\u8D85\u8FC7 64KB \u4E0A\u9650\u6216\u8BFB\u53D6\u5931\u8D25" });
15089
+ sendJson2(res, 400, { error: "license \u6587\u4EF6\u8D85\u8FC7 64KB \u4E0A\u9650\u6216\u8BFB\u53D6\u5931\u8D25" });
14570
15090
  return;
14571
15091
  }
14572
15092
  const result = installLicense(raw, options.licenseOptions);
14573
15093
  if (!result.ok) {
14574
- sendJson(res, 400, { error: result.error });
15094
+ sendJson2(res, 400, { error: result.error });
14575
15095
  return;
14576
15096
  }
14577
- sendJson(res, 200, publicLicenseStatus(result.status));
15097
+ sendJson2(res, 200, publicLicenseStatus(result.status));
14578
15098
  }
14579
15099
  async function handleLicenseRefresh(res, options) {
14580
15100
  const homeDir = options.licenseRefreshOptions?.homeDir ?? options.licenseOptions?.homeDir;
@@ -14588,7 +15108,7 @@ async function handleLicenseRefresh(res, options) {
14588
15108
  };
14589
15109
  const result = await refreshLicense(refreshOptions);
14590
15110
  if (!result.ok) {
14591
- sendJson(res, 400, {
15111
+ sendJson2(res, 400, {
14592
15112
  error: result.message,
14593
15113
  canSubscribe: result.reason === "inactive"
14594
15114
  });
@@ -14600,7 +15120,7 @@ async function handleLicenseRefresh(res, options) {
14600
15120
  ...publicKeyPem === void 0 ? {} : { publicKeyPem },
14601
15121
  ...now === void 0 ? {} : { now }
14602
15122
  };
14603
- sendJson(res, 200, {
15123
+ sendJson2(res, 200, {
14604
15124
  ...publicLicenseStatus(licenseStatus(statusOptions)),
14605
15125
  message: result.message
14606
15126
  });
@@ -14665,7 +15185,7 @@ function readJsonBody(req) {
14665
15185
  req.on("error", () => resolveBody(void 0));
14666
15186
  });
14667
15187
  }
14668
- function sendJson(res, status, body) {
15188
+ function sendJson2(res, status, body) {
14669
15189
  res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
14670
15190
  res.end(JSON.stringify(body));
14671
15191
  }
@@ -14798,7 +15318,13 @@ async function runServe(args) {
14798
15318
  }
14799
15319
  }
14800
15320
  const panelDistDir = panelDist === void 0 ? resolve6(dirname12(fileURLToPath2(import.meta.url)), "../../panel/dist") : resolve6(panelDist);
14801
- const narrative = createNarrativeService({ followTranscripts: !fixtures });
15321
+ const narrative = createNarrativeService({
15322
+ followTranscripts: !fixtures,
15323
+ feedbackSignals: (harness) => {
15324
+ const loaded = harnesses.find((candidate) => candidate.harness === harness);
15325
+ return loaded?.feedbackSignals(loaded.scanOptions).feedback;
15326
+ }
15327
+ });
14802
15328
  if (fixtures) {
14803
15329
  try {
14804
15330
  const raw = readFileSync12(fixturePath("cc", "sessions", "session-todos.jsonl"), "utf8");
@@ -14860,9 +15386,9 @@ async function runHook(args) {
14860
15386
  throw new UsageError(`hook \u4E0D\u8BA4\u8BC6\u53C2\u6570:${arg}`);
14861
15387
  }
14862
15388
  }
14863
- const { adapter } = await loadHarness(harness, { fixtures: false });
15389
+ const { adapter, scanOptions } = await loadHarness(harness, { fixtures: false });
14864
15390
  if (action === "install") {
14865
- await adapter.installHooks(port);
15391
+ await adapter.installHooks(port, scanOptions);
14866
15392
  if (harness === "codex") {
14867
15393
  process.stdout.write("wingman:codex \u914D\u7F6E\u5B8C\u6210\uFF1B\u9996\u6B21\u63A5\u7EBF\u8BF7\u5728 Codex \u8F93\u5165 /hooks\uFF0C\u5C06 Wingman hooks \u8BBE\u4E3A Trusted + Enabled\u3002\u6388\u6743\u540E\u65B0\u4E8B\u4EF6\u5E94\u8FDB\u5165 Wingman\uFF1B\u82E5\u5F53\u524D\u4EFB\u52A1\u4ECD\u65E0\u4E8B\u4EF6\uFF0C\u8BF7\u5B8C\u6574\u9000\u51FA\u91CD\u5F00\u540E\u7528\u65B0\u4EFB\u52A1\u9A8C\u8BC1\u3002\n\u5347\u7EA7\u65F6\u53EA\u8981\u53D7\u7BA1 command \u4FDD\u6301\u4E0D\u53D8\uFF0C\u65E0\u9700\u91CD\u590D\u4FE1\u4EFB\uFF1Bcommand \u53D8\u5316\u6216\u53D7\u7BA1\u5757\u6B8B\u7F3A/\u88AB\u4FEE\u6539\u65F6\u4F8B\u5916\u3002\n\u5199\u524D\u5DF2\u5907\u4EFD;\u91CD\u590D\u6267\u884C\u5E42\u7B49;\u89E3\u7EBF:wingman hook uninstall codex\n");
14868
15394
  } else {
@@ -14871,7 +15397,7 @@ async function runHook(args) {
14871
15397
  `);
14872
15398
  }
14873
15399
  } else {
14874
- await adapter.uninstallHooks();
15400
+ await adapter.uninstallHooks(scanOptions);
14875
15401
  process.stdout.write(`wingman:${harness} \u5DF2\u89E3\u7EBF,\u914D\u7F6E\u5DF2\u6062\u590D\u3002
14876
15402
  `);
14877
15403
  }