@research-copilot/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +235 -0
- package/dist/index.js +463 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ldm2060
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
declare const KINDS: readonly ["literature", "ideation", "experiment", "writing", "polish", "review", "rebuttal"];
|
|
2
|
+
type Kind = (typeof KINDS)[number];
|
|
3
|
+
declare const STATUSES: readonly ["planning", "in_progress", "verify", "completed"];
|
|
4
|
+
type Status = (typeof STATUSES)[number];
|
|
5
|
+
type Priority = "P0" | "P1" | "P2" | "P3";
|
|
6
|
+
interface Gap {
|
|
7
|
+
desc: string;
|
|
8
|
+
suggest_kind: Kind;
|
|
9
|
+
status: "open" | "resolved";
|
|
10
|
+
}
|
|
11
|
+
interface TaskRecord {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
kind: Kind;
|
|
15
|
+
status: Status;
|
|
16
|
+
priority: Priority;
|
|
17
|
+
venue?: string;
|
|
18
|
+
parent?: string;
|
|
19
|
+
children: string[];
|
|
20
|
+
depends_on: string[];
|
|
21
|
+
gaps: Gap[];
|
|
22
|
+
branch?: string;
|
|
23
|
+
created: string;
|
|
24
|
+
updated: string;
|
|
25
|
+
}
|
|
26
|
+
declare function isKind(x: string): x is Kind;
|
|
27
|
+
|
|
28
|
+
interface ResearchPaths {
|
|
29
|
+
root: string;
|
|
30
|
+
tasks: string;
|
|
31
|
+
spec: string;
|
|
32
|
+
workspace: string;
|
|
33
|
+
runtime: string;
|
|
34
|
+
workflow: string;
|
|
35
|
+
config: string;
|
|
36
|
+
activeTask: string;
|
|
37
|
+
graphIndex: string;
|
|
38
|
+
taskDir(id: string): string;
|
|
39
|
+
}
|
|
40
|
+
declare function researchPaths(repoRoot: string): ResearchPaths;
|
|
41
|
+
|
|
42
|
+
declare function slugify(title: string): string;
|
|
43
|
+
interface CreateInput {
|
|
44
|
+
title: string;
|
|
45
|
+
kind: Kind;
|
|
46
|
+
date: string;
|
|
47
|
+
priority?: Priority;
|
|
48
|
+
venue?: string;
|
|
49
|
+
parent?: string;
|
|
50
|
+
now?: string;
|
|
51
|
+
}
|
|
52
|
+
declare function createTask(repo: string, input: CreateInput): TaskRecord;
|
|
53
|
+
declare function taskJsonPath(repo: string, id: string): string;
|
|
54
|
+
declare function writeTask(repo: string, task: TaskRecord, now?: string): void;
|
|
55
|
+
declare function readTask(repo: string, id: string): TaskRecord;
|
|
56
|
+
declare function listTasks(repo: string): TaskRecord[];
|
|
57
|
+
declare function setStatus(repo: string, id: string, to: Status, now: string): void;
|
|
58
|
+
|
|
59
|
+
declare const TRANSITIONS: Record<Status, Status[]>;
|
|
60
|
+
declare function nextStatuses(from: Status): Status[];
|
|
61
|
+
declare function canTransition(from: Status, to: Status): boolean;
|
|
62
|
+
declare function assertTransition(from: Status, to: Status): void;
|
|
63
|
+
|
|
64
|
+
interface GraphNode {
|
|
65
|
+
task: TaskRecord;
|
|
66
|
+
blocked: boolean;
|
|
67
|
+
dependents: string[];
|
|
68
|
+
}
|
|
69
|
+
type Graph = Map<string, GraphNode>;
|
|
70
|
+
declare function buildGraph(tasks: TaskRecord[]): Graph;
|
|
71
|
+
|
|
72
|
+
interface Recommendation {
|
|
73
|
+
action: "resume" | "create";
|
|
74
|
+
taskId?: string;
|
|
75
|
+
suggestKind?: Kind;
|
|
76
|
+
reason: string;
|
|
77
|
+
sourceGap?: string;
|
|
78
|
+
score: number;
|
|
79
|
+
}
|
|
80
|
+
interface ResearchState {
|
|
81
|
+
active: {
|
|
82
|
+
id: string;
|
|
83
|
+
kind: Kind;
|
|
84
|
+
status: Status;
|
|
85
|
+
} | null;
|
|
86
|
+
graph: {
|
|
87
|
+
completed: number;
|
|
88
|
+
in_progress: number;
|
|
89
|
+
blocked: number;
|
|
90
|
+
planning: number;
|
|
91
|
+
};
|
|
92
|
+
openGaps: {
|
|
93
|
+
taskId: string;
|
|
94
|
+
desc: string;
|
|
95
|
+
suggest_kind: Kind;
|
|
96
|
+
}[];
|
|
97
|
+
recommendations: Recommendation[];
|
|
98
|
+
turnTs: string;
|
|
99
|
+
}
|
|
100
|
+
declare function computeResearchState(tasks: TaskRecord[], now: string, activeId?: string): ResearchState;
|
|
101
|
+
|
|
102
|
+
declare function extractWorkflowState(md: string, state: Status | "no_task"): string | null;
|
|
103
|
+
|
|
104
|
+
type Phase = "execute" | "verify";
|
|
105
|
+
interface ContextRef {
|
|
106
|
+
type: "spec" | "context";
|
|
107
|
+
path: string;
|
|
108
|
+
reason: string;
|
|
109
|
+
}
|
|
110
|
+
interface VerifyRow {
|
|
111
|
+
check: string;
|
|
112
|
+
kind: string;
|
|
113
|
+
args?: Record<string, unknown>;
|
|
114
|
+
}
|
|
115
|
+
declare function readPrdGoal(repo: string, id: string): string | null;
|
|
116
|
+
declare function appendContext(repo: string, id: string, phase: Phase, row: ContextRef | VerifyRow): void;
|
|
117
|
+
declare function readContext(repo: string, id: string, phase: Phase): unknown[];
|
|
118
|
+
|
|
119
|
+
interface CheckResult {
|
|
120
|
+
ok: boolean;
|
|
121
|
+
missing: string[];
|
|
122
|
+
}
|
|
123
|
+
declare function numberTraceability(draft: string, artifactsText: string): CheckResult;
|
|
124
|
+
declare function citationCompliance(tex: string, bibtex: string): CheckResult;
|
|
125
|
+
|
|
126
|
+
declare function setActive(repo: string, id: string): void;
|
|
127
|
+
declare function getActive(repo: string): string | null;
|
|
128
|
+
|
|
129
|
+
interface BuildOptions {
|
|
130
|
+
format: "text" | "json";
|
|
131
|
+
now: string;
|
|
132
|
+
eventName?: string;
|
|
133
|
+
}
|
|
134
|
+
declare function renderResearchState(rs: ResearchState): string;
|
|
135
|
+
declare function buildContext(repo: string, opts: BuildOptions): string;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Skillpack manifest schema
|
|
139
|
+
*
|
|
140
|
+
* A skillpack is a git repository containing:
|
|
141
|
+
* - agents/*.md (agent definitions)
|
|
142
|
+
* - specs/*.md (task specifications)
|
|
143
|
+
* - meta.yaml (pack metadata)
|
|
144
|
+
*/
|
|
145
|
+
interface SkillpackManifest {
|
|
146
|
+
/** Pack identifier (kebab-case) */
|
|
147
|
+
name: string;
|
|
148
|
+
/** Human-readable description */
|
|
149
|
+
description: string;
|
|
150
|
+
/** Git repository URL */
|
|
151
|
+
source: string;
|
|
152
|
+
/** Version constraint (git tag/branch, semver-style) */
|
|
153
|
+
version?: string;
|
|
154
|
+
/** Whether this pack is enabled by default */
|
|
155
|
+
enabled?: boolean;
|
|
156
|
+
}
|
|
157
|
+
interface SkillpacksYaml {
|
|
158
|
+
/** List of available skillpacks */
|
|
159
|
+
packs: SkillpackManifest[];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Resolved skillpack with local filesystem path
|
|
163
|
+
*/
|
|
164
|
+
interface ResolvedSkillpack extends SkillpackManifest {
|
|
165
|
+
/** Local cache path where pack is stored */
|
|
166
|
+
localPath: string;
|
|
167
|
+
/** Resolved git ref (commit SHA) */
|
|
168
|
+
resolvedRef: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parse skillpacks.yaml file
|
|
173
|
+
*
|
|
174
|
+
* @param path - Path to skillpacks.yaml
|
|
175
|
+
* @returns Parsed skillpacks manifest
|
|
176
|
+
* @throws If file doesn't exist or YAML is invalid
|
|
177
|
+
*/
|
|
178
|
+
declare function parseSkillpacks(path: string): SkillpacksYaml;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolve and fetch skillpacks to local cache
|
|
182
|
+
*
|
|
183
|
+
* @param packs - List of pack manifests from skillpacks.yaml
|
|
184
|
+
* @param cacheDir - Directory to store cached packs (e.g., ~/.cache/research-copilot/skillpacks)
|
|
185
|
+
* @returns List of resolved packs with local paths
|
|
186
|
+
*/
|
|
187
|
+
declare function resolveSkillpacks(packs: SkillpackManifest[], cacheDir: string): ResolvedSkillpack[];
|
|
188
|
+
/**
|
|
189
|
+
* Remove cached skillpack
|
|
190
|
+
*
|
|
191
|
+
* @param packName - Name of pack to remove
|
|
192
|
+
* @param cacheDir - Cache directory
|
|
193
|
+
*/
|
|
194
|
+
declare function removeSkillpack(packName: string, cacheDir: string): void;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Lock file entry for a synced skillpack
|
|
198
|
+
*/
|
|
199
|
+
interface SkillpackLockEntry {
|
|
200
|
+
name: string;
|
|
201
|
+
source: string;
|
|
202
|
+
resolvedRef: string;
|
|
203
|
+
syncedAt: string;
|
|
204
|
+
agentCount: number;
|
|
205
|
+
specCount: number;
|
|
206
|
+
}
|
|
207
|
+
interface SkillpackLock {
|
|
208
|
+
syncedAt: string;
|
|
209
|
+
packs: SkillpackLockEntry[];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Sync skillpacks from skillpacks.yaml to target directory
|
|
213
|
+
*
|
|
214
|
+
* @param repoRoot - Repository root (where skillpacks.yaml lives)
|
|
215
|
+
* @param cacheDir - Cache directory for cloned packs
|
|
216
|
+
* @param targetDir - Target directory to write agents/specs (e.g., research-kit/)
|
|
217
|
+
* @returns Lock data recording what was synced
|
|
218
|
+
*/
|
|
219
|
+
declare function syncSkillpacks(repoRoot: string, cacheDir: string, targetDir: string): SkillpackLock;
|
|
220
|
+
/**
|
|
221
|
+
* Write skillpacks lock file
|
|
222
|
+
*
|
|
223
|
+
* @param lockPath - Path to skillpacks.lock.yaml
|
|
224
|
+
* @param lock - Lock data
|
|
225
|
+
*/
|
|
226
|
+
declare function writeLockFile(lockPath: string, lock: SkillpackLock): void;
|
|
227
|
+
/**
|
|
228
|
+
* Read skillpacks lock file
|
|
229
|
+
*
|
|
230
|
+
* @param lockPath - Path to skillpacks.lock.yaml
|
|
231
|
+
* @returns Lock data, or null if file doesn't exist
|
|
232
|
+
*/
|
|
233
|
+
declare function readLockFile(lockPath: string): SkillpackLock | null;
|
|
234
|
+
|
|
235
|
+
export { type BuildOptions, type CheckResult, type ContextRef, type CreateInput, type Gap, type Graph, type GraphNode, KINDS, type Kind, type Phase, type Priority, type Recommendation, type ResearchPaths, type ResearchState, type ResolvedSkillpack, STATUSES, type SkillpackLock, type SkillpackLockEntry, type SkillpackManifest, type SkillpacksYaml, type Status, TRANSITIONS, type TaskRecord, type VerifyRow, appendContext, assertTransition, buildContext, buildGraph, canTransition, citationCompliance, computeResearchState, createTask, extractWorkflowState, getActive, isKind, listTasks, nextStatuses, numberTraceability, parseSkillpacks, readContext, readLockFile, readPrdGoal, readTask, removeSkillpack, renderResearchState, researchPaths, resolveSkillpacks, setActive, setStatus, slugify, syncSkillpacks, taskJsonPath, writeLockFile, writeTask };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var KINDS = [
|
|
3
|
+
"literature",
|
|
4
|
+
"ideation",
|
|
5
|
+
"experiment",
|
|
6
|
+
"writing",
|
|
7
|
+
"polish",
|
|
8
|
+
"review",
|
|
9
|
+
"rebuttal"
|
|
10
|
+
];
|
|
11
|
+
var STATUSES = ["planning", "in_progress", "verify", "completed"];
|
|
12
|
+
function isKind(x) {
|
|
13
|
+
return KINDS.includes(x);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/paths.ts
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
function researchPaths(repoRoot) {
|
|
19
|
+
const root = path.join(repoRoot, ".research");
|
|
20
|
+
const runtime = path.join(root, ".runtime");
|
|
21
|
+
return {
|
|
22
|
+
root,
|
|
23
|
+
tasks: path.join(root, "tasks"),
|
|
24
|
+
spec: path.join(root, "spec"),
|
|
25
|
+
workspace: path.join(root, "workspace"),
|
|
26
|
+
runtime,
|
|
27
|
+
workflow: path.join(root, "workflow.md"),
|
|
28
|
+
config: path.join(root, "config.yaml"),
|
|
29
|
+
activeTask: path.join(runtime, "active-task"),
|
|
30
|
+
graphIndex: path.join(runtime, "graph-index.json"),
|
|
31
|
+
taskDir: (id) => path.join(root, "tasks", id)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/task-store.ts
|
|
36
|
+
import * as fs from "fs";
|
|
37
|
+
import * as path2 from "path";
|
|
38
|
+
|
|
39
|
+
// src/lifecycle.ts
|
|
40
|
+
var TRANSITIONS = {
|
|
41
|
+
planning: ["in_progress"],
|
|
42
|
+
in_progress: ["verify"],
|
|
43
|
+
verify: ["in_progress", "completed"],
|
|
44
|
+
completed: []
|
|
45
|
+
};
|
|
46
|
+
function nextStatuses(from) {
|
|
47
|
+
return TRANSITIONS[from];
|
|
48
|
+
}
|
|
49
|
+
function canTransition(from, to) {
|
|
50
|
+
return TRANSITIONS[from].includes(to);
|
|
51
|
+
}
|
|
52
|
+
function assertTransition(from, to) {
|
|
53
|
+
if (!canTransition(from, to)) {
|
|
54
|
+
throw new Error(`illegal transition: ${from} -> ${to} (allowed: ${TRANSITIONS[from].join(", ") || "none"})`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/task-store.ts
|
|
59
|
+
function slugify(title) {
|
|
60
|
+
return title.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
61
|
+
}
|
|
62
|
+
function createTask(repo, input) {
|
|
63
|
+
const id = `${input.date}-${slugify(input.title)}`;
|
|
64
|
+
const now = input.now ?? input.date + "T00:00:00Z";
|
|
65
|
+
const task = {
|
|
66
|
+
id,
|
|
67
|
+
title: input.title,
|
|
68
|
+
kind: input.kind,
|
|
69
|
+
status: "planning",
|
|
70
|
+
priority: input.priority ?? "P2",
|
|
71
|
+
venue: input.venue,
|
|
72
|
+
parent: input.parent,
|
|
73
|
+
children: [],
|
|
74
|
+
depends_on: [],
|
|
75
|
+
gaps: [],
|
|
76
|
+
created: now,
|
|
77
|
+
updated: now
|
|
78
|
+
};
|
|
79
|
+
writeTask(repo, task, now);
|
|
80
|
+
return task;
|
|
81
|
+
}
|
|
82
|
+
function taskJsonPath(repo, id) {
|
|
83
|
+
return path2.join(researchPaths(repo).taskDir(id), "task.json");
|
|
84
|
+
}
|
|
85
|
+
function writeTask(repo, task, now) {
|
|
86
|
+
if (now) task.updated = now;
|
|
87
|
+
const dir2 = researchPaths(repo).taskDir(task.id);
|
|
88
|
+
fs.mkdirSync(path2.join(dir2, "research"), { recursive: true });
|
|
89
|
+
fs.mkdirSync(path2.join(dir2, "artifacts"), { recursive: true });
|
|
90
|
+
fs.writeFileSync(taskJsonPath(repo, task.id), JSON.stringify(task, null, 2) + "\n", "utf8");
|
|
91
|
+
}
|
|
92
|
+
function readTask(repo, id) {
|
|
93
|
+
return JSON.parse(fs.readFileSync(taskJsonPath(repo, id), "utf8"));
|
|
94
|
+
}
|
|
95
|
+
function listTasks(repo) {
|
|
96
|
+
const dir2 = researchPaths(repo).tasks;
|
|
97
|
+
if (!fs.existsSync(dir2)) return [];
|
|
98
|
+
return fs.readdirSync(dir2).filter((id) => fs.existsSync(taskJsonPath(repo, id))).map((id) => readTask(repo, id));
|
|
99
|
+
}
|
|
100
|
+
function setStatus(repo, id, to, now) {
|
|
101
|
+
const t = readTask(repo, id);
|
|
102
|
+
assertTransition(t.status, to);
|
|
103
|
+
t.status = to;
|
|
104
|
+
writeTask(repo, t, now);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/graph.ts
|
|
108
|
+
function buildGraph(tasks) {
|
|
109
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
110
|
+
const g = /* @__PURE__ */ new Map();
|
|
111
|
+
for (const t of tasks) {
|
|
112
|
+
const blocked = t.depends_on.some((d) => byId.get(d)?.status !== "completed");
|
|
113
|
+
g.set(t.id, { task: t, blocked, dependents: [] });
|
|
114
|
+
}
|
|
115
|
+
for (const t of tasks) {
|
|
116
|
+
for (const d of t.depends_on) {
|
|
117
|
+
g.get(d)?.dependents.push(t.id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return g;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/research-state.ts
|
|
124
|
+
var PRIORITY_RANK = { P0: 3, P1: 2, P2: 1, P3: 0 };
|
|
125
|
+
var LIFECYCLE_BONUS = { in_progress: 2, verify: 1, planning: 0.5, completed: 0 };
|
|
126
|
+
var W = { priority: 3, unblocking: 2, lifecycle: 1, age: 1e-3 };
|
|
127
|
+
var MAX_RECS = 3;
|
|
128
|
+
function computeResearchState(tasks, now, activeId) {
|
|
129
|
+
const g = buildGraph(tasks);
|
|
130
|
+
const counts = { completed: 0, in_progress: 0, blocked: 0, planning: 0 };
|
|
131
|
+
for (const n of g.values()) {
|
|
132
|
+
if (n.blocked) counts.blocked++;
|
|
133
|
+
if (n.task.status === "completed") counts.completed++;
|
|
134
|
+
else if (n.task.status === "in_progress") counts.in_progress++;
|
|
135
|
+
else if (n.task.status === "planning") counts.planning++;
|
|
136
|
+
}
|
|
137
|
+
const openGaps = tasks.flatMap((t) => t.gaps.filter((gp) => gp.status === "open").map((gp) => ({ taskId: t.id, desc: gp.desc, suggest_kind: gp.suggest_kind })));
|
|
138
|
+
const ageDays = (iso) => Math.max(0, (Date.parse(now) - Date.parse(iso)) / 864e5);
|
|
139
|
+
const recs = [];
|
|
140
|
+
for (const n of g.values()) {
|
|
141
|
+
if (n.task.status === "completed" || n.blocked) continue;
|
|
142
|
+
const score = W.priority * PRIORITY_RANK[n.task.priority] + W.lifecycle * LIFECYCLE_BONUS[n.task.status] + W.age * ageDays(n.task.updated);
|
|
143
|
+
recs.push({
|
|
144
|
+
action: "resume",
|
|
145
|
+
taskId: n.task.id,
|
|
146
|
+
score,
|
|
147
|
+
reason: `resume ${n.task.kind} task ${n.task.id} (${n.task.status})`
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
for (const t of tasks) {
|
|
151
|
+
const dependents = g.get(t.id)?.dependents.length ?? 0;
|
|
152
|
+
for (const gp of t.gaps.filter((x) => x.status === "open")) {
|
|
153
|
+
const score = W.priority * PRIORITY_RANK[t.priority] + W.unblocking * dependents + W.lifecycle * LIFECYCLE_BONUS[t.status];
|
|
154
|
+
recs.push({
|
|
155
|
+
action: "create",
|
|
156
|
+
suggestKind: gp.suggest_kind,
|
|
157
|
+
sourceGap: gp.desc,
|
|
158
|
+
score,
|
|
159
|
+
reason: `create ${gp.suggest_kind} task to resolve "${gp.desc}" (from ${t.id})`
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
recs.sort((x, y) => y.score - x.score || (x.taskId ?? x.sourceGap ?? "").localeCompare(y.taskId ?? y.sourceGap ?? ""));
|
|
164
|
+
const active = activeId ? (() => {
|
|
165
|
+
const n = g.get(activeId);
|
|
166
|
+
return n ? { id: n.task.id, kind: n.task.kind, status: n.task.status } : null;
|
|
167
|
+
})() : null;
|
|
168
|
+
return { active, graph: counts, openGaps, recommendations: recs.slice(0, MAX_RECS), turnTs: now };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/workflow.ts
|
|
172
|
+
function extractWorkflowState(md, state) {
|
|
173
|
+
const re = new RegExp(
|
|
174
|
+
`\\[workflow-state:${state}\\]\\r?\\n([\\s\\S]*?)\\r?\\n\\[/workflow-state\\]`
|
|
175
|
+
);
|
|
176
|
+
const m = md.match(re);
|
|
177
|
+
return m ? m[1].trim() : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/artifacts.ts
|
|
181
|
+
import * as fs2 from "fs";
|
|
182
|
+
import * as path3 from "path";
|
|
183
|
+
function dir(repo, id) {
|
|
184
|
+
return researchPaths(repo).taskDir(id);
|
|
185
|
+
}
|
|
186
|
+
function readPrdGoal(repo, id) {
|
|
187
|
+
const p = path3.join(dir(repo, id), "prd.md");
|
|
188
|
+
if (!fs2.existsSync(p)) return null;
|
|
189
|
+
const md = fs2.readFileSync(p, "utf8");
|
|
190
|
+
const m = md.match(/##\s*Goal\s*\r?\n([\s\S]*?)(\r?\n\s*\r?\n|\r?\n##|$)/);
|
|
191
|
+
return m ? m[1].trim() : null;
|
|
192
|
+
}
|
|
193
|
+
function appendContext(repo, id, phase, row) {
|
|
194
|
+
const p = path3.join(dir(repo, id), `${phase}.jsonl`);
|
|
195
|
+
fs2.appendFileSync(p, JSON.stringify(row) + "\n", "utf8");
|
|
196
|
+
}
|
|
197
|
+
function readContext(repo, id, phase) {
|
|
198
|
+
const p = path3.join(dir(repo, id), `${phase}.jsonl`);
|
|
199
|
+
if (!fs2.existsSync(p)) return [];
|
|
200
|
+
return fs2.readFileSync(p, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/verify.ts
|
|
204
|
+
var NUMBER_RE = /-?\d+(?:\.\d+)?/g;
|
|
205
|
+
var norm = (s) => s.replace(/^(-?)0+(\d)/, "$1$2");
|
|
206
|
+
function numberTraceability(draft, artifactsText) {
|
|
207
|
+
const present = new Set((artifactsText.match(NUMBER_RE) ?? []).map(norm));
|
|
208
|
+
const missing = [];
|
|
209
|
+
for (const tok of draft.match(NUMBER_RE) ?? []) {
|
|
210
|
+
if (!present.has(norm(tok)) && !missing.includes(tok)) missing.push(tok);
|
|
211
|
+
}
|
|
212
|
+
return { ok: missing.length === 0, missing };
|
|
213
|
+
}
|
|
214
|
+
function citationCompliance(tex, bibtex) {
|
|
215
|
+
const keys = /* @__PURE__ */ new Set();
|
|
216
|
+
for (const m of bibtex.matchAll(/@\w+\s*\{\s*([^,\s}]+)/g)) keys.add(m[1]);
|
|
217
|
+
const missing = [];
|
|
218
|
+
for (const m of tex.matchAll(/\\cite[a-zA-Z]*(?:\[[^\]]*\])*\{([^}]*)\}/g)) {
|
|
219
|
+
for (const key of m[1].split(",").map((k) => k.trim()).filter(Boolean)) {
|
|
220
|
+
if (!keys.has(key) && !missing.includes(key)) missing.push(key);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { ok: missing.length === 0, missing };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/active.ts
|
|
227
|
+
import * as fs3 from "fs";
|
|
228
|
+
function setActive(repo, id) {
|
|
229
|
+
const p = researchPaths(repo);
|
|
230
|
+
fs3.mkdirSync(p.runtime, { recursive: true });
|
|
231
|
+
fs3.writeFileSync(p.activeTask, id, "utf8");
|
|
232
|
+
}
|
|
233
|
+
function getActive(repo) {
|
|
234
|
+
const p = researchPaths(repo).activeTask;
|
|
235
|
+
return fs3.existsSync(p) ? fs3.readFileSync(p, "utf8").trim() || null : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/context.ts
|
|
239
|
+
import * as fs4 from "fs";
|
|
240
|
+
function renderResearchState(rs) {
|
|
241
|
+
const lines = ["[research-state]"];
|
|
242
|
+
lines.push(`Active: ${rs.active ? `${rs.active.id} (${rs.active.kind}, ${rs.active.status})` : "none"}`);
|
|
243
|
+
lines.push(`Graph: ${rs.graph.completed} completed \xB7 ${rs.graph.in_progress} in_progress \xB7 ${rs.graph.blocked} blocked`);
|
|
244
|
+
if (rs.openGaps.length) {
|
|
245
|
+
lines.push("Open gaps:");
|
|
246
|
+
for (const g of rs.openGaps) lines.push(` - [from ${g.taskId}] ${g.desc} -> suggests: ${g.suggest_kind}`);
|
|
247
|
+
}
|
|
248
|
+
if (rs.recommendations.length) {
|
|
249
|
+
lines.push("Recommended next (you decide, nothing auto-created):");
|
|
250
|
+
rs.recommendations.forEach((r, i) => lines.push(` ${i + 1}. ${r.reason}`));
|
|
251
|
+
}
|
|
252
|
+
lines.push(`turn-ts: ${rs.turnTs}`);
|
|
253
|
+
return lines.join("\n");
|
|
254
|
+
}
|
|
255
|
+
function buildContext(repo, opts) {
|
|
256
|
+
const tasks = listTasks(repo);
|
|
257
|
+
const active = getActive(repo) ?? void 0;
|
|
258
|
+
const activeStatus = active ? tasks.find((t) => t.id === active)?.status : void 0;
|
|
259
|
+
const rs = computeResearchState(tasks, opts.now, active);
|
|
260
|
+
const wfPath = researchPaths(repo).workflow;
|
|
261
|
+
const wfMd = fs4.existsSync(wfPath) ? fs4.readFileSync(wfPath, "utf8") : "";
|
|
262
|
+
const stateKey = activeStatus ?? "no_task";
|
|
263
|
+
const wfBlock = extractWorkflowState(wfMd, stateKey) ?? "Refer to workflow.md for current step.";
|
|
264
|
+
const text = `[workflow-state:${stateKey}]
|
|
265
|
+
${wfBlock}
|
|
266
|
+
[/workflow-state]
|
|
267
|
+
|
|
268
|
+
${renderResearchState(rs)}`;
|
|
269
|
+
if (opts.format === "json") {
|
|
270
|
+
return JSON.stringify({
|
|
271
|
+
hookSpecificOutput: { hookEventName: opts.eventName ?? "UserPromptSubmit", additionalContext: text }
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return text;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/parse-skillpacks.ts
|
|
278
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
279
|
+
import { parse as parseYaml } from "yaml";
|
|
280
|
+
function parseSkillpacks(path4) {
|
|
281
|
+
const content = readFileSync5(path4, "utf-8");
|
|
282
|
+
const data = parseYaml(content);
|
|
283
|
+
if (!data || typeof data !== "object") {
|
|
284
|
+
throw new Error("skillpacks.yaml must be an object");
|
|
285
|
+
}
|
|
286
|
+
if (!Array.isArray(data.packs)) {
|
|
287
|
+
throw new Error("skillpacks.yaml must have 'packs' array");
|
|
288
|
+
}
|
|
289
|
+
for (const pack of data.packs) {
|
|
290
|
+
if (!pack.name || typeof pack.name !== "string") {
|
|
291
|
+
throw new Error("Each pack must have a 'name' string");
|
|
292
|
+
}
|
|
293
|
+
if (!pack.source || typeof pack.source !== "string") {
|
|
294
|
+
throw new Error(`Pack '${pack.name}' must have a 'source' URL`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return data;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/resolve-skillpacks.ts
|
|
301
|
+
import { execSync } from "child_process";
|
|
302
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync } from "fs";
|
|
303
|
+
import { join as join4 } from "path";
|
|
304
|
+
function resolveSkillpacks(packs, cacheDir) {
|
|
305
|
+
mkdirSync3(cacheDir, { recursive: true });
|
|
306
|
+
const resolved = [];
|
|
307
|
+
for (const pack of packs) {
|
|
308
|
+
const packDir = join4(cacheDir, pack.name);
|
|
309
|
+
if (!existsSync5(packDir)) {
|
|
310
|
+
console.error(`[skillpacks] Cloning ${pack.name} from ${pack.source}...`);
|
|
311
|
+
execSync(`git clone "${pack.source}" "${packDir}"`, {
|
|
312
|
+
stdio: "inherit",
|
|
313
|
+
encoding: "utf-8"
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
console.error(`[skillpacks] Updating ${pack.name}...`);
|
|
317
|
+
execSync(`git fetch origin`, {
|
|
318
|
+
cwd: packDir,
|
|
319
|
+
stdio: "inherit",
|
|
320
|
+
encoding: "utf-8"
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const ref = pack.version || "main";
|
|
324
|
+
try {
|
|
325
|
+
execSync(`git checkout "${ref}"`, {
|
|
326
|
+
cwd: packDir,
|
|
327
|
+
stdio: "pipe",
|
|
328
|
+
encoding: "utf-8"
|
|
329
|
+
});
|
|
330
|
+
if (!pack.version) {
|
|
331
|
+
execSync(`git pull origin "${ref}"`, {
|
|
332
|
+
cwd: packDir,
|
|
333
|
+
stdio: "pipe",
|
|
334
|
+
encoding: "utf-8"
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
} catch (err) {
|
|
338
|
+
throw new Error(`Failed to checkout ${ref} for pack ${pack.name}: ${err}`);
|
|
339
|
+
}
|
|
340
|
+
const resolvedRef = execSync("git rev-parse HEAD", {
|
|
341
|
+
cwd: packDir,
|
|
342
|
+
encoding: "utf-8"
|
|
343
|
+
}).trim();
|
|
344
|
+
resolved.push({
|
|
345
|
+
...pack,
|
|
346
|
+
localPath: packDir,
|
|
347
|
+
resolvedRef
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return resolved;
|
|
351
|
+
}
|
|
352
|
+
function removeSkillpack(packName, cacheDir) {
|
|
353
|
+
const packDir = join4(cacheDir, packName);
|
|
354
|
+
if (existsSync5(packDir)) {
|
|
355
|
+
rmSync(packDir, { recursive: true, force: true });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/sync-skillpacks.ts
|
|
360
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3, copyFileSync } from "fs";
|
|
361
|
+
import { join as join5 } from "path";
|
|
362
|
+
import { stringify as stringifyYaml, parse as parseYaml2 } from "yaml";
|
|
363
|
+
function syncSkillpacks(repoRoot, cacheDir, targetDir) {
|
|
364
|
+
const yamlPath = join5(repoRoot, "skillpacks.yaml");
|
|
365
|
+
if (!existsSync6(yamlPath)) {
|
|
366
|
+
throw new Error(`skillpacks.yaml not found at ${yamlPath}`);
|
|
367
|
+
}
|
|
368
|
+
const manifest = parseSkillpacks(yamlPath);
|
|
369
|
+
const enabledPacks = manifest.packs.filter((p) => p.enabled !== false);
|
|
370
|
+
if (enabledPacks.length === 0) {
|
|
371
|
+
console.error("[sync] No enabled packs found in skillpacks.yaml");
|
|
372
|
+
return { syncedAt: (/* @__PURE__ */ new Date()).toISOString(), packs: [] };
|
|
373
|
+
}
|
|
374
|
+
const resolved = resolveSkillpacks(enabledPacks, cacheDir);
|
|
375
|
+
const agentsDir = join5(targetDir, "agents");
|
|
376
|
+
const specsDir = join5(targetDir, "specs");
|
|
377
|
+
mkdirSync4(agentsDir, { recursive: true });
|
|
378
|
+
mkdirSync4(specsDir, { recursive: true });
|
|
379
|
+
const lockEntries = [];
|
|
380
|
+
for (const pack of resolved) {
|
|
381
|
+
let agentCount = 0;
|
|
382
|
+
let specCount = 0;
|
|
383
|
+
const packAgentsDir = join5(pack.localPath, "agents");
|
|
384
|
+
if (existsSync6(packAgentsDir)) {
|
|
385
|
+
const agents = readdirSync2(packAgentsDir).filter((f) => f.endsWith(".md"));
|
|
386
|
+
for (const agent of agents) {
|
|
387
|
+
const src = join5(packAgentsDir, agent);
|
|
388
|
+
const dest = join5(agentsDir, agent);
|
|
389
|
+
copyFileSync(src, dest);
|
|
390
|
+
agentCount++;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const packSpecsDir = join5(pack.localPath, "specs");
|
|
394
|
+
if (existsSync6(packSpecsDir)) {
|
|
395
|
+
const specs = readdirSync2(packSpecsDir).filter((f) => f.endsWith(".md"));
|
|
396
|
+
for (const spec of specs) {
|
|
397
|
+
const src = join5(packSpecsDir, spec);
|
|
398
|
+
const dest = join5(specsDir, spec);
|
|
399
|
+
copyFileSync(src, dest);
|
|
400
|
+
specCount++;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
lockEntries.push({
|
|
404
|
+
name: pack.name,
|
|
405
|
+
source: pack.source,
|
|
406
|
+
resolvedRef: pack.resolvedRef,
|
|
407
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
408
|
+
agentCount,
|
|
409
|
+
specCount
|
|
410
|
+
});
|
|
411
|
+
console.error(`[sync] ${pack.name}: ${agentCount} agents, ${specCount} specs`);
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
415
|
+
packs: lockEntries
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function writeLockFile(lockPath, lock) {
|
|
419
|
+
const yaml = stringifyYaml(lock);
|
|
420
|
+
writeFileSync3(lockPath, yaml, "utf-8");
|
|
421
|
+
}
|
|
422
|
+
function readLockFile(lockPath) {
|
|
423
|
+
if (!existsSync6(lockPath)) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
const content = readFileSync6(lockPath, "utf-8");
|
|
427
|
+
return parseYaml2(content);
|
|
428
|
+
}
|
|
429
|
+
export {
|
|
430
|
+
KINDS,
|
|
431
|
+
STATUSES,
|
|
432
|
+
TRANSITIONS,
|
|
433
|
+
appendContext,
|
|
434
|
+
assertTransition,
|
|
435
|
+
buildContext,
|
|
436
|
+
buildGraph,
|
|
437
|
+
canTransition,
|
|
438
|
+
citationCompliance,
|
|
439
|
+
computeResearchState,
|
|
440
|
+
createTask,
|
|
441
|
+
extractWorkflowState,
|
|
442
|
+
getActive,
|
|
443
|
+
isKind,
|
|
444
|
+
listTasks,
|
|
445
|
+
nextStatuses,
|
|
446
|
+
numberTraceability,
|
|
447
|
+
parseSkillpacks,
|
|
448
|
+
readContext,
|
|
449
|
+
readLockFile,
|
|
450
|
+
readPrdGoal,
|
|
451
|
+
readTask,
|
|
452
|
+
removeSkillpack,
|
|
453
|
+
renderResearchState,
|
|
454
|
+
researchPaths,
|
|
455
|
+
resolveSkillpacks,
|
|
456
|
+
setActive,
|
|
457
|
+
setStatus,
|
|
458
|
+
slugify,
|
|
459
|
+
syncSkillpacks,
|
|
460
|
+
taskJsonPath,
|
|
461
|
+
writeLockFile,
|
|
462
|
+
writeTask
|
|
463
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@research-copilot/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core library for research-copilot",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"research",
|
|
13
|
+
"copilot",
|
|
14
|
+
"ai"
|
|
15
|
+
],
|
|
16
|
+
"author": "ldm2060",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/ldm2060/research_copilot.git",
|
|
21
|
+
"directory": "packages/core"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"yaml": "^2.6.0",
|
|
34
|
+
"zod": "^3.23.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup src/index.ts --format esm --dts"
|
|
38
|
+
}
|
|
39
|
+
}
|