pi-crew 0.9.48 → 0.9.50
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/AGENTS.md +18 -0
- package/CHANGELOG.md +314 -0
- package/dist/build-meta.json +70 -42
- package/dist/index.mjs +505 -430
- package/dist/index.mjs.map +4 -4
- package/docs/decisions/2026-07-24-oidc-trusted-publishing.md +112 -0
- package/package.json +2 -3
- package/skills/.gitkeep +0 -0
- package/skills/distill-persona/BUILD-NOTES.md +55 -0
- package/skills/distill-persona/SKILL.md +550 -0
- package/skills/distill-persona/UPGRADE-LOG-RESEARCH-SKILLS.md +100 -0
- package/skills/distill-persona/references/coverage-manifest.md +65 -0
- package/skills/distill-persona/references/cross-skill-differentiation.md +12 -0
- package/skills/distill-persona/references/description-discipline.md +6 -0
- package/skills/distill-persona/references/diagnostic-path.md +25 -0
- package/skills/distill-persona/references/distillation-field-synthesis-pass2.md +59 -0
- package/skills/distill-persona/references/distillation-field-synthesis.md +108 -0
- package/skills/distill-persona/references/fidelity-rubric.md +19 -0
- package/skills/distill-persona/references/field-models.md +20 -0
- package/skills/distill-persona/references/handoff.md +42 -0
- package/skills/distill-persona/references/optional-body-sections.md +9 -0
- package/skills/distill-persona/references/registry-routing.md +11 -0
- package/skills/distill-persona/references/research/lesson-memory-shortcut.md +33 -0
- package/skills/distill-persona/references/research/r1-a-examples.md +23 -0
- package/skills/distill-persona/references/research/r1-b-scripts.md +26 -0
- package/skills/distill-persona/references/research/r1-c-human-readme.md +31 -0
- package/skills/distill-persona/references/research/r1-d-tests.md +28 -0
- package/skills/distill-persona/references/research/r1-verification.md +36 -0
- package/skills/distill-persona/references/research/r2-low-yield.md +26 -0
- package/skills/distill-persona/references/self-upgrade-directive.md +20 -0
- package/skills/distill-persona/references/taste-principles.md +8 -0
- package/skills/distill-persona/references/topic-variant.md +13 -0
- package/skills/distill-persona/references/update-mode.md +7 -0
- package/skills/distill-persona/scripts/fidelity_eval.py +244 -0
- package/skills/distill-persona/scripts/validate-run.mjs +297 -0
- package/skills/distill-persona/scripts/validate-skill-structure.mjs +177 -0
- package/skills/distill-software/BUILD-NOTES.md +56 -0
- package/skills/distill-software/SKILL.md +363 -0
- package/skills/distill-software/references/handoff.md +47 -0
- package/skills/distill-software/scripts/code_dna.py +290 -0
- package/skills/research/DISTILLATION-PROCESS-CHECKLIST.md +120 -0
- package/skills/research/EXCAVATION-CHECKLIST.md +142 -0
- package/skills/research/FIDELITY.md +180 -0
- package/skills/research/SKILL.md +432 -0
- package/skills/research/references/anti-patterns.md +184 -0
- package/skills/research/references/fidelity.md +241 -0
- package/skills/research/references/handoff.md +48 -0
- package/skills/research/references/research-protocol.md +162 -0
- package/skills/research/references/source-inventory.md +135 -0
- package/skills/research/references/verified-models.md +163 -0
- package/skills/research/scripts/__pycache__/safe_io.cpython-312.pyc +0 -0
- package/skills/research/scripts/code_dna.py +233 -0
- package/skills/research/scripts/emit_run_summary.py +142 -0
- package/skills/research/scripts/safe_io.py +314 -0
- package/skills/research/scripts/source_evaluator.py +234 -0
- package/skills/research/scripts/validate-skill-structure.mjs +177 -0
- package/skills/research/scripts/verify_citations.py +225 -0
- package/skills/security-priority.json +28 -0
- package/src/config/config.ts +1 -0
- package/src/config/role-tools.ts +6 -3
- package/src/config/types.ts +8 -0
- package/src/extension/crew-cleanup.ts +18 -1
- package/src/extension/crew-vibes/index.ts +11 -2
- package/src/extension/register.ts +1 -1
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands.ts +7 -3
- package/src/extension/registration/lifecycle-handlers.ts +1 -3
- package/src/extension/registration/ui.ts +4 -0
- package/src/extension/registration/viewers.ts +3 -0
- package/src/extension/team-tool/run.ts +7 -6
- package/src/runtime/background-runner.ts +11 -16
- package/src/runtime/chain-runner.ts +3 -2
- package/src/runtime/heartbeat-watcher.ts +28 -1
- package/src/runtime/pipeline-runner.ts +8 -7
- package/src/runtime/task-runner.ts +165 -119
- package/src/schema/config-schema.ts +1 -0
- package/src/ui/live-run-sidebar.ts +2 -0
- package/src/ui/mascot.ts +11 -9
- package/src/ui/render-coalescer.ts +9 -0
- package/src/ui/run-snapshot-cache.ts +10 -11
- package/src/ui/terminal-status.ts +5 -0
- package/src/ui/widget/index.ts +3 -5
- package/src/ui/widget/widget-types.ts +0 -1
- package/src/utils/gh-protocol.ts +9 -8
- package/workflows/distill.workflow.md +198 -0
- package/assets/runner-spritesheet.png +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// validate-skill-structure.mjs — structural invariant checker for generated distill skills.
|
|
3
|
+
// Implements F10 (awesome-persona-distill-skills finding): hard-fail if any structural assertion fails.
|
|
4
|
+
// Run AFTER Phase 3 build, BEFORE Phase 4 behavioral fidelity. Self-contained (stdlib only).
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node validate-skill-structure.mjs <path-to-SKILL.md>
|
|
8
|
+
// node validate-skill-structure.mjs <skill-dir>
|
|
9
|
+
//
|
|
10
|
+
// Exit codes: 0 = all assertions pass (all-green → may ship); 1 = one or more failed (iterate).
|
|
11
|
+
|
|
12
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
13
|
+
import { join, dirname, basename } from 'node:path';
|
|
14
|
+
|
|
15
|
+
const target = process.argv.find((a) => !a.startsWith('-') && a !== process.argv[0] && a !== process.argv[1]);
|
|
16
|
+
if (!target) {
|
|
17
|
+
console.error('Usage: validate-skill-structure.mjs <path-to-SKILL.md | skill-dir> [--engine]');
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Resolve to a SKILL.md path
|
|
22
|
+
let skillPath = target;
|
|
23
|
+
if (statSync(target).isDirectory()) {
|
|
24
|
+
skillPath = join(target, 'SKILL.md');
|
|
25
|
+
}
|
|
26
|
+
if (!existsSync(skillPath)) {
|
|
27
|
+
console.error(`✗ Not found: ${skillPath}`);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const src = readFileSync(skillPath, 'utf8');
|
|
32
|
+
const dir = dirname(skillPath);
|
|
33
|
+
const name = basename(dir);
|
|
34
|
+
const isEngine = process.argv.includes('--engine');
|
|
35
|
+
const PLACEHOLDERS = [/<person>/i, /<target>/i, /<topic>/i, /<field>/i, /TODO/i, /TBD/i, /XXX/i, /YYYY-MM-DD/i, /\.\.\.\s*<\/?/i];
|
|
36
|
+
|
|
37
|
+
const failures = [];
|
|
38
|
+
const passes = [];
|
|
39
|
+
const check = (label, ok, detail = '') => {
|
|
40
|
+
(ok ? passes : failures).push(ok ? ` ✓ ${label}` : ` ✗ ${label}${detail ? ' — ' + detail : ''}`);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// --- Frontmatter ---
|
|
44
|
+
const fm = src.match(/^---\n([\s\S]*?)\n---/);
|
|
45
|
+
check('frontmatter block present', !!fm, 'no --- block at top');
|
|
46
|
+
const fmText = fm ? fm[1] : '';
|
|
47
|
+
const fmField = (key) => {
|
|
48
|
+
const m = fmText.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
49
|
+
return m ? m[1].trim() : null;
|
|
50
|
+
};
|
|
51
|
+
const hasFm = (key) => new RegExp(`^${key}:`, 'm').test(fmText);
|
|
52
|
+
|
|
53
|
+
check('frontmatter: name', hasFm('name'));
|
|
54
|
+
check('frontmatter: description', hasFm('description'));
|
|
55
|
+
const desc = fmField('description');
|
|
56
|
+
check('frontmatter: description ≤1 sentence (one terminal punct or one clause)',
|
|
57
|
+
desc ? desc.split(/[.。!!??]/).filter(Boolean).length <= 2 : false,
|
|
58
|
+
desc ? `got: "${desc.slice(0, 60)}…"` : 'missing');
|
|
59
|
+
check('frontmatter: triggers', hasFm('triggers') || hasFm('trigger'), 'no triggers field');
|
|
60
|
+
if (!isEngine) check('frontmatter: distilled (staleness date)', hasFm('distilled') || hasFm('调研时间'), 'no distilled/调研时间 staleness anchor');
|
|
61
|
+
const distilled = fmField('distilled') || fmField('调研时间');
|
|
62
|
+
check('frontmatter: distilled is valid date (YYYY-MM-DD)',
|
|
63
|
+
!distilled || /^\d{4}-\d{2}-\d{2}/.test(distilled),
|
|
64
|
+
distilled ? `got "${distilled}"` : '');
|
|
65
|
+
if (!isEngine) check('frontmatter: target (person|topic|software)', hasFm('target'), 'no target field');
|
|
66
|
+
|
|
67
|
+
// --- Body sections ---
|
|
68
|
+
check('Agentic Protocol section present', /回答工作流|Agentic Protocol/i.test(src));
|
|
69
|
+
check('Agentic Protocol Step 1', /Step 1|第一步|步骤 1|### 1\b/i.test(src));
|
|
70
|
+
check('Agentic Protocol Step 2', /Step 2|第二步|步骤 2|### 2\b/i.test(src));
|
|
71
|
+
check('Agentic Protocol Step 3', /Step 3|第三步|步骤 3|### 3\b/i.test(src));
|
|
72
|
+
|
|
73
|
+
// honest boundaries: count items (numbered list or bullets) under an honest-boundaries heading
|
|
74
|
+
function countListItems(haystack, headingRe, windowChars = 2000) {
|
|
75
|
+
const m = haystack.match(headingRe);
|
|
76
|
+
if (!m) return { found: false, count: 0, block: '' };
|
|
77
|
+
const after = haystack.slice(m.index);
|
|
78
|
+
const section = after.match(/([\s\S]*?)\n##(?=[^#])/);
|
|
79
|
+
const block = section ? section[1] : after.slice(0, windowChars);
|
|
80
|
+
// count BOTH list items AND table data rows (rows with | content |, excluding separator rows)
|
|
81
|
+
const listItems = (block.match(/^\s*(?:\d+[.)]|[-*])\s+\S/gm) || []).length;
|
|
82
|
+
const tableRows = (block.match(/^\s*\|(?![\s:|-]+\|?\s*$).+\|/gm) || []).length;
|
|
83
|
+
return { found: true, count: listItems + tableRows, block };
|
|
84
|
+
}
|
|
85
|
+
if (!isEngine) {
|
|
86
|
+
const boundary = countListItems(src, /诚实边界|honest boundar(?:y|ies)/i);
|
|
87
|
+
check('honest boundaries (M11) ≥3', boundary.count >= 3, `found ${boundary.count} items`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- Anti-drift tables (M9a 内在张力, M9b 反例黑名单, M12 fallback tree) — Darwin gap #2: validator previously skipped these
|
|
91
|
+
if (!isEngine) {
|
|
92
|
+
const tension = countListItems(src, /M9a|内在张力|inner tension|internal tension/i);
|
|
93
|
+
check('内在张力 (M9a) ≥3 tension pairs', tension.count >= 3, tension.found ? `found ${tension.count} items` : 'section missing');
|
|
94
|
+
|
|
95
|
+
const blacklist = countListItems(src, /M9b|反例黑名单|anti.?pattern blacklist/i);
|
|
96
|
+
check('反例黑名单 (M9b) ≥7 rows', blacklist.count >= 7, blacklist.found ? `found ${blacklist.count} rows` : 'section missing');
|
|
97
|
+
|
|
98
|
+
const fallback = countListItems(src, /M12|失败模式.*[Ff]allback|[Ff]allback\s*树/i);
|
|
99
|
+
check('失败模式Fallback树 (M12) ≥8 rows', fallback.count >= 8, fallback.found ? `found ${fallback.count} rows` : 'section missing');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- FIDELITY.md companion artifact (Darwin gap #3: ship-gate requires it but nothing checked)
|
|
103
|
+
const fidelityPath = join(dir, 'FIDELITY.md');
|
|
104
|
+
if (existsSync(fidelityPath)) {
|
|
105
|
+
const fm2 = readFileSync(fidelityPath, 'utf8');
|
|
106
|
+
const totalMatch = fm2.match(/(?:总分|total)[::\s*]*\*{0,2}([0-9]+)\s*\*?\s*(?:\/|/|\sout\sof\s)\s*\*?\s*100/i);
|
|
107
|
+
check('FIDELITY.md total score present', !!totalMatch, totalMatch ? `=${totalMatch[1]}/100` : 'no /100 score found');
|
|
108
|
+
const qCount = (fm2.match(/(?:Q[1-5]|问题[1-5]|question\s*[1-5])/gi) || []).length;
|
|
109
|
+
check('FIDELITY.md ≥5 test questions', qCount >= 5, `found ${qCount} question refs`);
|
|
110
|
+
check('FIDELITY.md flags single-agent/self-score caveat', /单\s*agent|single.?agent|self.?score|upper.?bound|independent/i.test(fm2), 'add single-agent upper-bound caveat');
|
|
111
|
+
} else if (!isEngine) {
|
|
112
|
+
check('FIDELITY.md companion present', false, 'no FIDELITY.md in skill dir');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- EXCAVATION-CHECKLIST.md (Phase 1 protocol: track + verify each part was really read)
|
|
116
|
+
const checklistPath = join(dir, 'EXCAVATION-CHECKLIST.md');
|
|
117
|
+
if (existsSync(checklistPath)) {
|
|
118
|
+
const cl = readFileSync(checklistPath, 'utf8');
|
|
119
|
+
// dangling rows: status ⬜ not-started or ⏳ reading left at ship time = silently skipped/forgotten
|
|
120
|
+
const dangling = (cl.match(/\|\s*[⬜⏳][^|]*\|/gu) || []).length;
|
|
121
|
+
check('checklist: no dangling ⬜/⏳ rows (every part resolved)', dangling === 0, dangling ? `${dangling} row(s) not-started/reading — resolve to ✅📄/⏭/🧠` : '');
|
|
122
|
+
// memory ratio: parse "memory-ratio: NN%" if declared
|
|
123
|
+
const ratioMatch = cl.match(/memory-ratio[:\s]*([0-9]+)\s*%/i);
|
|
124
|
+
if (ratioMatch) {
|
|
125
|
+
const ratio = parseInt(ratioMatch[1], 10);
|
|
126
|
+
check('checklist: 🧠 memory-ratio ≤30%', ratio <= 30, `declared ${ratio}% (>30% = recap, not distillation)`);
|
|
127
|
+
} else {
|
|
128
|
+
check('checklist: declares memory-ratio', false, 'add "🧠 memory-ratio: NN% (X/Y findings)" header line');
|
|
129
|
+
}
|
|
130
|
+
// proof-of-read present: at least one verbatim-quote-with-location cell (heuristic — a ✅ row should carry a quote)
|
|
131
|
+
const proofCells = (cl.match(/"[^"]{8,}"[^|]*/g) || []).length;
|
|
132
|
+
check('checklist: ≥1 proof-of-read (verbatim quote in a ✅ row)', proofCells >= 1, `${proofCells} quote cell(s) found`);
|
|
133
|
+
} else if (!isEngine) {
|
|
134
|
+
check('EXCAVATION-CHECKLIST.md present', false, 'no excavation checklist — Phase 1 protocol requires one');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- DISTILLATION-PROCESS-CHECKLIST.md (whole-pipeline tracking + the 3-empty-rounds deep-dive gate)
|
|
138
|
+
const procPath = join(dir, 'DISTILLATION-PROCESS-CHECKLIST.md');
|
|
139
|
+
if (existsSync(procPath)) {
|
|
140
|
+
const pc = readFileSync(procPath, 'utf8');
|
|
141
|
+
// dangling phases: ⬜/⏳ left in the phase-progress table at ship = a phase skipped
|
|
142
|
+
const danglingPhases = (pc.match(/\|\s*[⬜⏳][^|]*\|/gu) || []).length;
|
|
143
|
+
check('process: no dangling ⬜/⏳ phases (every phase completed)', danglingPhases === 0, danglingPhases ? `${danglingPhases} phase(s) not done` : '');
|
|
144
|
+
// deep-dive round log present
|
|
145
|
+
check('process: deep-dive round log present', /round log|\| *Round.*New findings/i.test(pc), 'add the round-log table');
|
|
146
|
+
// 3-empty-rounds gate recorded (≥3 consecutive zero-new rounds before leaving a phase)
|
|
147
|
+
const gateFired = /gate fires|3 consecutive (empty|zero)|GATE FIRES/i.test(pc);
|
|
148
|
+
check('process: 3-empty-rounds gate recorded (≥3 zero-new rounds)', gateFired, 'record a round-log row marked gate-fired before declaring any research phase done');
|
|
149
|
+
} else if (!isEngine) {
|
|
150
|
+
check('DISTILLATION-PROCESS-CHECKLIST.md present', false, 'no process checklist — every phase + the 3-round deep-dive gate must be tracked');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- Placeholders ---
|
|
154
|
+
const foundPlaceholders = PLACEHOLDERS.filter((re) => re.test(src));
|
|
155
|
+
if (!isEngine) check('no unresolved placeholder text', foundPlaceholders.length === 0,
|
|
156
|
+
foundPlaceholders.map((re) => src.match(re)?.[0]).filter(Boolean).join(', '));
|
|
157
|
+
|
|
158
|
+
// --- Self-containment (F9): references/sources should not dangle outside the dir ---
|
|
159
|
+
const refsDir = join(dir, 'references');
|
|
160
|
+
if (!isEngine) check('self-contained: no external repo paths in body (e.g. 07-调研/)',
|
|
161
|
+
!/[\w/-]*调研与分析?\/|src\/|source\//i.test(src.replace(/```[\s\S]*?```/g, '')),
|
|
162
|
+
'body references paths outside skill dir');
|
|
163
|
+
|
|
164
|
+
// --- Software-specific assertions (if target: software) ---
|
|
165
|
+
if (/target:\s*software/i.test(fmText) || /code-dna|代码表达DNA|code expression/i.test(src)) {
|
|
166
|
+
check('[software] Code Expression-DNA section present', /代码表达DNA|Code Expression-DNA|code.?dna/i.test(src));
|
|
167
|
+
check('[software] toolchain matrix present', /toolchain|eslint|oxlint|biome|tsconfig/i.test(src));
|
|
168
|
+
check('[software] distilled_against commit anchor', hasFm('distilled_against') || /distilled_against/i.test(src));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- Report ---
|
|
172
|
+
console.log(`\nvalidate-skill-structure: ${name}`);
|
|
173
|
+
console.log(`path: ${skillPath}\n`);
|
|
174
|
+
passes.forEach((l) => console.log(l));
|
|
175
|
+
failures.forEach((l) => console.log(l));
|
|
176
|
+
console.log(`\n${passes.length} pass, ${failures.length} fail — ${failures.length === 0 ? '✅ ALL-GREEN (may ship)' : '🔴 NOT READY (iterate Phase 2→3)'}${isEngine ? ' [engine mode — generated-skill checks skipped]' : ''}\n`);
|
|
177
|
+
process.exit(failures.length === 0 ? 0 : 1);
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
verify_citations.py — borrowed from Geek-skills-deep-research, ported for the
|
|
4
|
+
research skill (F13: wired INTO the Agentic Protocol Step 2, never orphaned).
|
|
5
|
+
|
|
6
|
+
Structural citation-integrity checker: resolves every citation marker `[n]` in
|
|
7
|
+
the report against the LOCAL source pool (sources.json); flags unresolved
|
|
8
|
+
citations, dangling references, non-sequential numbering, and source
|
|
9
|
+
concentration (> 25% from any single source).
|
|
10
|
+
|
|
11
|
+
⚠️ This is a STRUCTURAL check only — it does NOT make network requests.
|
|
12
|
+
It does NOT verify HTTP 404s, redirect chains, or content-drift. A URL that
|
|
13
|
+
has gone dead or been hijacked will still pass if it matches the local source
|
|
14
|
+
pool. For live-URL verification, use a separate network checker (e.g.
|
|
15
|
+
WebFetch HEAD) before relying on citation-liveness.
|
|
16
|
+
|
|
17
|
+
Stdlib only. Python 3.9+.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python3 verify_citations.py <report.md> <sources.json> [--output results.json]
|
|
21
|
+
python3 verify_citations.py --self-test # run a quick self-test
|
|
22
|
+
|
|
23
|
+
Exit codes:
|
|
24
|
+
0 no unresolved / no dangling / no concentration issues
|
|
25
|
+
1 one or more issues found (see --output report)
|
|
26
|
+
2 usage error
|
|
27
|
+
"""
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
import sys
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from urllib.parse import urlparse
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
SOURCE_CONCENTRATION_LIMIT = 0.25 # No single source > 25% of citations
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def normalize_url(url: str) -> str:
|
|
39
|
+
"""Lowercase + strip trailing slash + strip common tracking params."""
|
|
40
|
+
url = url.strip().rstrip("/")
|
|
41
|
+
parsed = urlparse(url.lower())
|
|
42
|
+
drop_params = {"utm_source", "utm_medium", "utm_campaign", "ref", "fbclid", "gclid"}
|
|
43
|
+
# Best-effort: keep query but strip params in drop_params
|
|
44
|
+
if parsed.query:
|
|
45
|
+
parts = [p for p in parsed.query.split("&") if p.split("=")[0] not in drop_params]
|
|
46
|
+
query = "&".join(parts)
|
|
47
|
+
else:
|
|
48
|
+
query = ""
|
|
49
|
+
return f"{parsed.scheme}://{parsed.netloc}{parsed.path}{('?' + query) if query else ''}".rstrip("/")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def url_signature(url: str):
|
|
53
|
+
"""Robust (domain, path_segments[:3]) for matching."""
|
|
54
|
+
parsed = urlparse(url.lower())
|
|
55
|
+
host = parsed.hostname or ""
|
|
56
|
+
if host.startswith("www."):
|
|
57
|
+
host = host[4:]
|
|
58
|
+
segments = [s for s in parsed.path.split("/") if s][:3]
|
|
59
|
+
return (host, tuple(segments))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def extract_citations(text: str):
|
|
63
|
+
"""Find [n] markers in body. Returns list of int indices."""
|
|
64
|
+
return [int(m) for m in re.findall(r"\[(\d+)\]", text)]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def extract_references(text: str):
|
|
68
|
+
"""Find reference list. Returns dict {int_idx: (label, url)}."""
|
|
69
|
+
refs = {}
|
|
70
|
+
# Match common patterns: [1] Label — URL or [1] Label - http...
|
|
71
|
+
for m in re.finditer(r"^\s*\[(\d+)\]\s+([^—\-]+?)\s*[—\-]\s*(\S+)", text, re.MULTILINE):
|
|
72
|
+
refs[int(m.group(1))] = (m.group(2).strip(), m.group(3).strip())
|
|
73
|
+
return refs
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main(argv):
|
|
77
|
+
if not argv or argv[0] in ("-h", "--help"):
|
|
78
|
+
print(__doc__)
|
|
79
|
+
return 0
|
|
80
|
+
if argv[0] == "--self-test":
|
|
81
|
+
return self_test()
|
|
82
|
+
if len(argv) < 2:
|
|
83
|
+
print("Usage: verify_citations.py <report.md> <sources.json> [--output results.json]", file=sys.stderr)
|
|
84
|
+
return 2
|
|
85
|
+
|
|
86
|
+
report_path = Path(argv[0])
|
|
87
|
+
sources_path = Path(argv[1])
|
|
88
|
+
output_path = None
|
|
89
|
+
if len(argv) >= 4 and argv[2] == "--output":
|
|
90
|
+
output_path = Path(argv[3])
|
|
91
|
+
|
|
92
|
+
report = report_path.read_text(encoding="utf-8")
|
|
93
|
+
sources_data = json.loads(sources_path.read_text(encoding="utf-8"))
|
|
94
|
+
|
|
95
|
+
sources = sources_data.get("sources", [])
|
|
96
|
+
sources_normalized = {normalize_url(s["url"]): s for s in sources}
|
|
97
|
+
|
|
98
|
+
citations = extract_citations(report)
|
|
99
|
+
refs = extract_references(report)
|
|
100
|
+
|
|
101
|
+
issues = []
|
|
102
|
+
stats = {
|
|
103
|
+
"report_citations": len(citations),
|
|
104
|
+
"unique_citations": len(set(citations)),
|
|
105
|
+
"references_declared": len(refs),
|
|
106
|
+
"sources_in_pool": len(sources),
|
|
107
|
+
"concentration": {},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# 1. Every [n] has a matching reference
|
|
111
|
+
for c in set(citations):
|
|
112
|
+
if c not in refs:
|
|
113
|
+
issues.append({
|
|
114
|
+
"severity": "fatal",
|
|
115
|
+
"type": "unresolved_citation",
|
|
116
|
+
"detail": f"Citation [{c}] has no matching reference entry",
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
# 2. Every reference URL exists in source pool
|
|
120
|
+
for idx, (label, url) in refs.items():
|
|
121
|
+
if normalize_url(url) not in sources_normalized:
|
|
122
|
+
# Try signature match
|
|
123
|
+
sig = url_signature(url)
|
|
124
|
+
match = None
|
|
125
|
+
for s_url, s in sources_normalized.items():
|
|
126
|
+
if url_signature(s_url) == sig:
|
|
127
|
+
match = s
|
|
128
|
+
break
|
|
129
|
+
if not match:
|
|
130
|
+
issues.append({
|
|
131
|
+
"severity": "fatal",
|
|
132
|
+
"type": "url_not_in_source_pool",
|
|
133
|
+
"detail": f"Reference [{idx}] URL '{url}' not found in source pool",
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
# 3. Dangling references (in list but never cited)
|
|
137
|
+
for idx in refs:
|
|
138
|
+
if idx not in set(citations):
|
|
139
|
+
issues.append({
|
|
140
|
+
"severity": "warn",
|
|
141
|
+
"type": "dangling_reference",
|
|
142
|
+
"detail": f"Reference [{idx}] declared but never cited in body",
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
# 4. Sequential numbering, no gaps
|
|
146
|
+
if refs:
|
|
147
|
+
max_idx = max(refs.keys())
|
|
148
|
+
missing = [i for i in range(1, max_idx + 1) if i not in refs]
|
|
149
|
+
if missing:
|
|
150
|
+
issues.append({
|
|
151
|
+
"severity": "warn",
|
|
152
|
+
"type": "non_sequential_numbering",
|
|
153
|
+
"detail": f"Reference numbering gaps: {missing}",
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
# 5. Source concentration
|
|
157
|
+
cite_counts = {}
|
|
158
|
+
for c in citations:
|
|
159
|
+
if c in refs:
|
|
160
|
+
label = refs[c][0]
|
|
161
|
+
cite_counts[label] = cite_counts.get(label, 0) + 1
|
|
162
|
+
total = max(len(citations), 1)
|
|
163
|
+
for label, count in cite_counts.items():
|
|
164
|
+
ratio = count / total
|
|
165
|
+
stats["concentration"][label] = round(ratio, 3)
|
|
166
|
+
if ratio > SOURCE_CONCENTRATION_LIMIT:
|
|
167
|
+
issues.append({
|
|
168
|
+
"severity": "fatal",
|
|
169
|
+
"type": "source_concentration",
|
|
170
|
+
"detail": f"Source '{label}' has {count}/{total} citations ({ratio*100:.1f}%) > {SOURCE_CONCENTRATION_LIMIT*100:.0f}% limit",
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
result = {
|
|
174
|
+
"ok": all(i["severity"] != "fatal" for i in issues),
|
|
175
|
+
"stats": stats,
|
|
176
|
+
"issues": issues,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if output_path:
|
|
180
|
+
output_path.write_text(json.dumps(result, indent=2), encoding="utf-8")
|
|
181
|
+
else:
|
|
182
|
+
print(json.dumps(result, indent=2))
|
|
183
|
+
|
|
184
|
+
return 0 if result["ok"] else 1
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def self_test():
|
|
188
|
+
"""Run a quick self-test on a fabricated report."""
|
|
189
|
+
report = """# Test
|
|
190
|
+
|
|
191
|
+
Claim one [1]. Claim two [2]. Claim three [3]. Claim four [4]. Claim five [5].
|
|
192
|
+
|
|
193
|
+
## References
|
|
194
|
+
|
|
195
|
+
[1] Author A — https://example.com/a
|
|
196
|
+
[2] Author B — https://example.org/b
|
|
197
|
+
[3] Author C — https://example.net/c
|
|
198
|
+
[4] Author D — https://example.io/d
|
|
199
|
+
[5] Author E — https://example.dev/e
|
|
200
|
+
"""
|
|
201
|
+
sources = {
|
|
202
|
+
"sources": [
|
|
203
|
+
{"url": "https://example.com/a", "title": "Author A's paper"},
|
|
204
|
+
{"url": "https://example.org/b", "title": "Author B's notes"},
|
|
205
|
+
{"url": "https://example.net/c", "title": "Author C's x"},
|
|
206
|
+
{"url": "https://example.io/d", "title": "Author D's y"},
|
|
207
|
+
{"url": "https://example.dev/e", "title": "Author E's z"},
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
import tempfile
|
|
211
|
+
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False) as f:
|
|
212
|
+
f.write(report)
|
|
213
|
+
rp = f.name
|
|
214
|
+
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
|
|
215
|
+
json.dump(sources, f)
|
|
216
|
+
sp = f.name
|
|
217
|
+
code = main([rp, sp])
|
|
218
|
+
Path(rp).unlink()
|
|
219
|
+
Path(sp).unlink()
|
|
220
|
+
print(f"\nself-test: exit={code} (expected 0 — 1 citation per source, no concentration)")
|
|
221
|
+
return code
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if __name__ == "__main__":
|
|
225
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"generated": "2026-05-28T06:10:00Z",
|
|
4
|
+
"source": "source/Anthropic-Cybersecurity-Skills/",
|
|
5
|
+
"description": "Prioritized security skills for pi-crew security-reviewer role",
|
|
6
|
+
"priority_skills": [
|
|
7
|
+
{ "id": "detecting-ai-model-prompt-injection-attacks", "priority": "critical", "atlas": ["AML.T0051"], "category": "agent-security" },
|
|
8
|
+
{ "id": "detecting-supply-chain-attacks-in-ci-cd", "priority": "critical", "atlas": ["AML.T0010", "AML.T0104"], "category": "supply-chain" },
|
|
9
|
+
{ "id": "detecting-anomalous-authentication-patterns", "priority": "high", "atlas": ["AML.T0043", "AML.T0018"], "category": "auth" },
|
|
10
|
+
{ "id": "detecting-typosquatting-packages-in-npm-pypi", "priority": "high", "atlas": [], "category": "supply-chain" },
|
|
11
|
+
{ "id": "detecting-path-traversal", "priority": "high", "atlas": [], "category": "code-security" },
|
|
12
|
+
{ "id": "detecting-command-injection", "priority": "high", "atlas": [], "category": "code-security" },
|
|
13
|
+
{ "id": "detecting-sensitive-data-exposure", "priority": "high", "atlas": ["AML.T0067"], "category": "secrets" },
|
|
14
|
+
{ "id": "detecting-context-poisoning-in-agent-loops", "priority": "high", "atlas": ["AML.T0051"], "category": "agent-security" },
|
|
15
|
+
{ "id": "detecting-tool-invocation-abuse", "priority": "medium", "atlas": ["AML.T0051", "AML.T0054"], "category": "agent-security" },
|
|
16
|
+
{ "id": "detecting-malicious-skill-loading", "priority": "medium", "atlas": ["AML.T0062"], "category": "agent-security" },
|
|
17
|
+
{ "id": "detecting-credential-leakage-in-logs", "priority": "medium", "atlas": [], "category": "secrets" },
|
|
18
|
+
{ "id": "detecting-session-fixation", "priority": "medium", "atlas": ["AML.T0018"], "category": "auth" },
|
|
19
|
+
{ "id": "detecting-data-exfiltration-indicators", "priority": "medium", "atlas": ["AML.T0067"], "category": "data-security" },
|
|
20
|
+
{ "id": "detecting-serverless-function-injection", "priority": "medium", "atlas": [], "category": "code-security" },
|
|
21
|
+
{ "id": "detecting-race-condition-vulnerabilities", "priority": "medium", "atlas": ["AML.T0054"], "category": "code-security" },
|
|
22
|
+
{ "id": "detecting-agent-privilege-escalation", "priority": "medium", "atlas": ["AML.T0054"], "category": "agent-security" },
|
|
23
|
+
{ "id": "detecting-malicious-npm-packages", "priority": "low", "atlas": [], "category": "supply-chain" },
|
|
24
|
+
{ "id": "detecting-dependency-confusion-attacks", "priority": "low", "atlas": [], "category": "supply-chain" },
|
|
25
|
+
{ "id": "detecting-token-hijacking", "priority": "low", "atlas": ["AML.T0018"], "category": "auth" },
|
|
26
|
+
{ "id": "detecting-race-condition-in-file-operations", "priority": "low", "atlas": ["AML.T0054"], "category": "code-security" }
|
|
27
|
+
]
|
|
28
|
+
}
|
package/src/config/config.ts
CHANGED
|
@@ -707,6 +707,7 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
|
707
707
|
allowChildProcessFallback: parseWithSchema(Type.Boolean(), obj.allowChildProcessFallback),
|
|
708
708
|
maxTurns: parsePositiveInteger(obj.maxTurns, LIMIT_CEILINGS.runtimeMaxTurns),
|
|
709
709
|
graceTurns: parsePositiveInteger(obj.graceTurns, LIMIT_CEILINGS.runtimeGraceTurns),
|
|
710
|
+
taskTimeoutMs: parsePositiveInteger(obj.taskTimeoutMs, LIMIT_CEILINGS.runtimeMaxTurns),
|
|
710
711
|
inheritContext: parseWithSchema(Type.Boolean(), obj.inheritContext) ?? true,
|
|
711
712
|
promptMode: parseWithSchema(Type.Union([Type.Literal("replace"), Type.Literal("append")]), obj.promptMode),
|
|
712
713
|
groupJoin: parseWithSchema(Type.Union([Type.Literal("off"), Type.Literal("group"), Type.Literal("smart")]), obj.groupJoin),
|
package/src/config/role-tools.ts
CHANGED
|
@@ -11,10 +11,13 @@ export interface RoleToolConfig {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
14
|
-
// Explorer - Read-only
|
|
14
|
+
// Explorer - Read-only exploration; bash is included for git log/show
|
|
15
|
+
// (decisions stream needs commit-history mining) but edit/write stay
|
|
16
|
+
// excluded. State-mutation safety is enforced separately by
|
|
17
|
+
// READ_ONLY_ROLES in role-permission.ts.
|
|
15
18
|
explorer: {
|
|
16
|
-
tools: ["read", "grep", "find", "ls", "glob"],
|
|
17
|
-
excludeTools: ["edit", "write", "
|
|
19
|
+
tools: ["read", "grep", "find", "ls", "glob", "bash"],
|
|
20
|
+
excludeTools: ["edit", "write", "web"],
|
|
18
21
|
},
|
|
19
22
|
|
|
20
23
|
// Analyst - Read and analyze, limited execution
|
package/src/config/types.ts
CHANGED
|
@@ -44,6 +44,14 @@ export interface CrewRuntimeConfig {
|
|
|
44
44
|
allowChildProcessFallback?: boolean;
|
|
45
45
|
maxTurns?: number;
|
|
46
46
|
graceTurns?: number;
|
|
47
|
+
/**
|
|
48
|
+
* W2 fix — wall-clock timeout per task in milliseconds. When the task
|
|
49
|
+
* exceeds this limit, input.signal is aborted which triggers the existing
|
|
50
|
+
* SIGTERM → SIGKILL escalation in child-pi.ts. Default 0 (no timeout).
|
|
51
|
+
* Prevents runaway agent loops (e.g. 11_build in oh-my-pi distill run that
|
|
52
|
+
* re-verified completed files 14+ times).
|
|
53
|
+
*/
|
|
54
|
+
taskTimeoutMs?: number;
|
|
47
55
|
inheritContext?: boolean;
|
|
48
56
|
promptMode?: "replace" | "append";
|
|
49
57
|
groupJoin?: "off" | "group" | "smart";
|
|
@@ -51,7 +51,17 @@ class ChildProcessRegistry {
|
|
|
51
51
|
|
|
52
52
|
export const childProcessRegistry = new ChildProcessRegistry();
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Optional UI timer disposal hook. Set by register.ts so the SIGTERM/SIGHUP
|
|
56
|
+
* signal handler can dispose terminal-status timers (idle re-assert loop +
|
|
57
|
+
* flash clear) that would otherwise keep the event loop alive.
|
|
58
|
+
*/
|
|
59
|
+
let terminalStatusDispose: (() => void) | undefined;
|
|
60
|
+
|
|
61
|
+
export function registerCleanupHandler(pi: ExtensionAPI, opts?: { disposeTerminalStatus?: () => void }): void {
|
|
62
|
+
// Store the latest dispose fn at module level so the signal handler
|
|
63
|
+
// (registered once) always uses the current one even across re-registrations.
|
|
64
|
+
terminalStatusDispose = opts?.disposeTerminalStatus;
|
|
55
65
|
// Handle session_shutdown event
|
|
56
66
|
pi.on("session_shutdown", async () => {
|
|
57
67
|
console.log("[pi-crew] Session shutdown - cleaning up resources");
|
|
@@ -76,6 +86,13 @@ export function registerCleanupHandler(pi: ExtensionAPI): void {
|
|
|
76
86
|
signalHandlersRegistered = true;
|
|
77
87
|
const handleSignal = async (signal: string): Promise<void> => {
|
|
78
88
|
console.log(`[pi-crew] Received ${signal} - starting cleanup`);
|
|
89
|
+
// Dispose UI timers so the idle re-assert loop / flash clear don't
|
|
90
|
+
// keep the event loop alive after child processes are killed.
|
|
91
|
+
try {
|
|
92
|
+
terminalStatusDispose?.();
|
|
93
|
+
} catch {
|
|
94
|
+
// Best-effort — must not block child cleanup on signal.
|
|
95
|
+
}
|
|
79
96
|
await cleanupChildProcesses();
|
|
80
97
|
};
|
|
81
98
|
process.on("SIGTERM", () => {
|
|
@@ -100,9 +100,18 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/** Trigger a footer repaint; the footer recomputes capacity/quota on render.
|
|
103
|
+
/** Trigger a footer repaint; the footer recomputes capacity/quota on render.
|
|
104
|
+
*
|
|
105
|
+
* Wrapped wholly in safeUiCall because fetchProviderAndRefresh is async: after
|
|
106
|
+
* its `await` the session may have shut down (session_shutdown clears the
|
|
107
|
+
* timers, but an in-flight fetchProviderAndRefresh still resumes), making ctx
|
|
108
|
+
* stale. Accessing the `hasUI` getter on a stale ctx throws — catch it so
|
|
109
|
+
* crew-vibes never crashes pi. Matches the file's core philosophy: "must
|
|
110
|
+
* NEVER break the user's session". */
|
|
104
111
|
function refreshFooter(ctx: ExtensionContext): void {
|
|
105
|
-
|
|
112
|
+
safeUiCall("refresh-footer", () => {
|
|
113
|
+
if (ctx?.hasUI) requestRender(ctx);
|
|
114
|
+
});
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
function publishSpeedFooter(ctx: ExtensionContext, speed = footerAnimator.value()): void {
|
|
@@ -79,7 +79,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
79
79
|
// subagents or when the flag is off, it returns a no-op controller.
|
|
80
80
|
ctx.brokerController = installCrewBrokerLifecycleController(pi, ctx);
|
|
81
81
|
|
|
82
|
-
registerCleanupHandler(pi);
|
|
82
|
+
registerCleanupHandler(pi, { disposeTerminalStatus: () => ctx.terminalStatus?.dispose?.() });
|
|
83
83
|
registerCompactionGuard(pi, {
|
|
84
84
|
foregroundControllers: ctx.foregroundControllers,
|
|
85
85
|
foregroundTeamRunControllers: ctx.foregroundTeamRunControllers,
|
|
@@ -31,6 +31,7 @@ export function registerPiCommands(pi: ExtensionAPI, ctx: RegistrationContext):
|
|
|
31
31
|
getManifestCache: ctx.getManifestCache,
|
|
32
32
|
getRunSnapshotCache: ctx.getRunSnapshotCache,
|
|
33
33
|
getMetricRegistry: () => ctx.observabilityState.metricRegistry,
|
|
34
|
+
uiState: ctx.uiState,
|
|
34
35
|
dismissNotifications: () => {
|
|
35
36
|
ctx.widgetState.notificationCount = 0;
|
|
36
37
|
if (ctx.currentCtx) {
|
|
@@ -62,6 +62,7 @@ import { piTeamsHelp } from "../help.ts";
|
|
|
62
62
|
import { handleTeamManagerCommand } from "../team-manager-command.ts";
|
|
63
63
|
import { withSessionId } from "../team-tool/context.ts";
|
|
64
64
|
import { commandText, notifyCommandResult, parseRunArgs, parseScalar, pushUnset, setNestedConfig } from "./command-utils.ts";
|
|
65
|
+
import type { UiState } from "./ui.ts";
|
|
65
66
|
import { openLiveConversation, openTranscriptViewer, selectAgentTask } from "./viewers.ts";
|
|
66
67
|
|
|
67
68
|
export interface RegisterTeamCommandsDeps {
|
|
@@ -73,6 +74,7 @@ export interface RegisterTeamCommandsDeps {
|
|
|
73
74
|
};
|
|
74
75
|
getRunSnapshotCache?: (cwd: string) => ReturnType<typeof createRunSnapshotCache>;
|
|
75
76
|
getMetricRegistry?: () => MetricRegistry | undefined;
|
|
77
|
+
uiState?: UiState;
|
|
76
78
|
dismissNotifications?: () => void;
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -391,6 +393,7 @@ export async function openTeamDashboard(ctx: ExtensionContext): Promise<void> {
|
|
|
391
393
|
if (!ctx.hasUI) return;
|
|
392
394
|
const deps = depsRef;
|
|
393
395
|
if (!deps) return;
|
|
396
|
+
if (deps.uiState) deps.uiState.dashboardOpen = true;
|
|
394
397
|
const cmdCtx = ctx as ExtensionCommandContext;
|
|
395
398
|
for (;;) {
|
|
396
399
|
// Extract sessionId for workspace-scoped filtering
|
|
@@ -428,7 +431,7 @@ export async function openTeamDashboard(ctx: ExtensionContext): Promise<void> {
|
|
|
428
431
|
: { width, maxHeight: "90%", anchor: "center", margin: 2 },
|
|
429
432
|
},
|
|
430
433
|
);
|
|
431
|
-
if (!selection)
|
|
434
|
+
if (!selection) break;
|
|
432
435
|
if (selection.action === "reload") continue;
|
|
433
436
|
if (selection.action === "notifications-dismiss") {
|
|
434
437
|
deps.dismissNotifications?.();
|
|
@@ -537,8 +540,9 @@ export async function openTeamDashboard(ctx: ExtensionContext): Promise<void> {
|
|
|
537
540
|
teamCommandContext(cmdCtx),
|
|
538
541
|
);
|
|
539
542
|
await notifyCommandResult(cmdCtx, commandText(result));
|
|
540
|
-
|
|
543
|
+
break;
|
|
541
544
|
}
|
|
545
|
+
if (deps.uiState) deps.uiState.dashboardOpen = false;
|
|
542
546
|
}
|
|
543
547
|
|
|
544
548
|
export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommandsDeps): void {
|
|
@@ -963,7 +967,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
963
967
|
await ctx.ui.custom<undefined>(
|
|
964
968
|
(tui, theme, _keybindings, done) =>
|
|
965
969
|
new AnimatedMascot(theme, () => done(undefined), {
|
|
966
|
-
frameIntervalMs: style === "armin" ?
|
|
970
|
+
frameIntervalMs: style === "armin" ? 100 : 180,
|
|
967
971
|
autoCloseMs: 7000,
|
|
968
972
|
requestRender: () => requestRenderTarget(tui),
|
|
969
973
|
style,
|
|
@@ -168,8 +168,6 @@ function installSessionStartHandler(pi: ExtensionAPI, ctx: RegistrationContext):
|
|
|
168
168
|
ctx.crewAutocompleteRegistered = true;
|
|
169
169
|
registerCrewAutocomplete(extensionCtx);
|
|
170
170
|
}
|
|
171
|
-
if (ctx.widgetState.interval) clearInterval(ctx.widgetState.interval);
|
|
172
|
-
ctx.widgetState.interval = undefined;
|
|
173
171
|
notifyActiveRuns(extensionCtx);
|
|
174
172
|
|
|
175
173
|
const currentSessionId = extractBrokerSessionId(extensionCtx);
|
|
@@ -586,7 +584,7 @@ function setupRenderLoop(
|
|
|
586
584
|
const snapshotCache = lastFrameSnapshotCache ?? ctx.getRunSnapshotCache(ctx.currentCtx.cwd);
|
|
587
585
|
const manifests = lastPreloadedManifests;
|
|
588
586
|
if (!lastPreloadedConfig) backgroundPreload();
|
|
589
|
-
if (ctx.uiState.liveSidebarRunId) {
|
|
587
|
+
if (ctx.uiState.liveSidebarRunId || ctx.uiState.dashboardOpen) {
|
|
590
588
|
const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
|
|
591
589
|
if (ctx.widgetState.lastVisibility !== "hidden" || ctx.widgetState.lastPlacement !== placement) {
|
|
592
590
|
setExtensionWidget(ctx.currentCtx, "pi-crew", undefined, { placement });
|
|
@@ -42,6 +42,10 @@ async function importLiveRunSidebar(): Promise<typeof LiveRunSidebarType> {
|
|
|
42
42
|
export interface UiState {
|
|
43
43
|
liveSidebarRunId: string | undefined;
|
|
44
44
|
dashboardOpened: boolean;
|
|
45
|
+
/** True while the full RunDashboard overlay (/team-dashboard) is open.
|
|
46
|
+
* Checked by the global renderTick to skip crew-widget updates while the
|
|
47
|
+
* dashboard obscures the widget (mirrors the liveSidebarRunId gate). */
|
|
48
|
+
dashboardOpen?: boolean;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
/** Dependencies passed in by register.ts. */
|