@bcelep/capint 0.6.5 → 0.7.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/AGENT.md +1 -1
- package/CHANGELOG.md +23 -0
- package/bin/capint.js +11 -2
- package/docs/PRD-capint.md +94 -0
- package/docs/PRD-v0.5-agent-capability-activation.md +10 -1
- package/docs/architecture-decisions.md +2 -0
- package/docs/capint-rehber.md +439 -0
- package/docs/capint-stack.md +118 -0
- package/docs/community.md +19 -0
- package/docs/conventions/daily-use.md +15 -1
- package/docs/eval/beginner-test-protocol.md +39 -0
- package/docs/eval/survey-template.md +16 -0
- package/docs/generated/README.md +6 -0
- package/docs/generated/execution-intent.v1.md +211 -0
- package/docs/generated/explanation.v1.md +127 -0
- package/docs/generated/explanation.v2.md +142 -0
- package/docs/generated/ui-api.v1.md +178 -0
- package/docs/maintainer-dogfood.md +3 -2
- package/docs/recipes/memory-graph-pairing.md +200 -0
- package/docs/skill-audit-runbook.md +32 -0
- package/locales/en.json +27 -0
- package/locales/tr.json +27 -0
- package/package.json +4 -2
- package/projections/session-start.md +14 -2
- package/schemas/execution-intent.v1.json +69 -0
- package/schemas/explanation.v2.json +51 -0
- package/schemas/ui-api.v1.json +85 -0
- package/scripts/generate-schema-docs.mjs +104 -0
- package/scripts/release-check.mjs +10 -0
- package/skills/prismx-skill-gateway/SKILL.md +10 -3
- package/src/commands/doctor.js +9 -0
- package/src/commands/feedback.js +29 -0
- package/src/commands/init.js +12 -0
- package/src/commands/providers.js +54 -0
- package/src/commands/skill.js +27 -0
- package/src/commands/stack.js +97 -0
- package/src/lib/audit.js +9 -1
- package/src/lib/contract.js +9 -0
- package/src/lib/doctor.js +69 -3
- package/src/lib/execution-policy.js +3 -1
- package/src/lib/explanation-plain.js +72 -0
- package/src/lib/explanation.js +26 -2
- package/src/lib/feedback.js +65 -0
- package/src/lib/i18n.js +43 -0
- package/src/lib/providers/doctor.js +205 -0
- package/src/lib/scaffold/index.js +2 -1
- package/src/lib/scaffold/presets.js +20 -0
- package/src/lib/skill-audit.js +141 -0
- package/src/lib/stack/index.js +259 -0
- package/src/lib/ui-server.js +92 -5
- package/templates/minimal/DONE.md +10 -0
- package/templates/minimal/GUNLUK.md +15 -1
- package/templates/minimal/docs/conventions/daily-use.md +3 -1
- package/ui/app.js +100 -10
- package/ui/index.html +22 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const { runProvidersDoctor } = require("../providers/doctor");
|
|
5
|
+
|
|
6
|
+
const STACK_DOC = "docs/capint-stack.md";
|
|
7
|
+
const STACK_MANIFEST = ".capint/stack.json";
|
|
8
|
+
const WORKFLOW_ANCHORS = [
|
|
9
|
+
{ path: "workflows/forge.md", label: "forge gate (zero-regression)" },
|
|
10
|
+
{ path: "workflows/challenge.md", label: "challenge gate" },
|
|
11
|
+
{ path: "workflows/genesis.md", label: "genesis / design-first" },
|
|
12
|
+
{ path: "workflows/blueprint.md", label: "blueprint / tasks" }
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const GRAPH_PATH_CANDIDATES = ["src", "app", "lib", "components", "packages"];
|
|
16
|
+
|
|
17
|
+
function fileExists(rootDir, rel) {
|
|
18
|
+
return fs.existsSync(path.join(rootDir, rel));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function countMarkdown(dir) {
|
|
22
|
+
if (!fs.existsSync(dir)) return 0;
|
|
23
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".md")).length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tierWorkflows(rootDir) {
|
|
27
|
+
const wfDir = path.join(rootDir, "workflows");
|
|
28
|
+
const count = countMarkdown(wfDir);
|
|
29
|
+
const anchors = WORKFLOW_ANCHORS.map((a) => ({
|
|
30
|
+
...a,
|
|
31
|
+
exists: fileExists(rootDir, a.path)
|
|
32
|
+
}));
|
|
33
|
+
const coreOk = anchors.filter((a) =>
|
|
34
|
+
["workflows/forge.md", "workflows/challenge.md"].includes(a.path)
|
|
35
|
+
).every((a) => a.exists);
|
|
36
|
+
|
|
37
|
+
let status = "missing";
|
|
38
|
+
if (count >= 4 && coreOk) status = "ok";
|
|
39
|
+
else if (count > 0) status = "partial";
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
id: "layer_workflows",
|
|
43
|
+
label: "ANWS-style workflows (decisions, forge gate, drift)",
|
|
44
|
+
status,
|
|
45
|
+
workflow_count: count,
|
|
46
|
+
anchors,
|
|
47
|
+
chat: "Use /genesis /blueprint /challenge /forge in chat — not capint CLI"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function tierRouting(rootDir) {
|
|
52
|
+
const files = ["GUNLUK.md", "AGENT.md", "HANDOFF.md", "design.md", "skill-routing-matrix.json"];
|
|
53
|
+
const paths = files.map((p) => ({ path: p, exists: fileExists(rootDir, p) }));
|
|
54
|
+
const hasCore = paths.filter((p) => ["GUNLUK.md", "AGENT.md", "skill-routing-matrix.json"].includes(p.path)).every(
|
|
55
|
+
(p) => p.exists
|
|
56
|
+
);
|
|
57
|
+
return {
|
|
58
|
+
id: "layer_routing",
|
|
59
|
+
label: "CapInt routing + HANDOFF contract",
|
|
60
|
+
status: hasCore ? "ok" : fileExists(rootDir, "AGENT.md") ? "partial" : "missing",
|
|
61
|
+
paths,
|
|
62
|
+
enable: "capint init — daily chat via GUNLUK.md"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function detectGraphPaths(rootDir) {
|
|
67
|
+
const existing = GRAPH_PATH_CANDIDATES.filter((name) => {
|
|
68
|
+
const p = path.join(rootDir, name);
|
|
69
|
+
return fs.existsSync(p) && fs.statSync(p).isDirectory();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let suggested = [];
|
|
73
|
+
if (existing.includes("src")) {
|
|
74
|
+
suggested = ["./src"];
|
|
75
|
+
} else if (existing.includes("app") && existing.includes("lib")) {
|
|
76
|
+
suggested = ["./app", "./lib", "./components"].filter((rel) => fileExists(rootDir, rel.replace(/^\.\//, "")));
|
|
77
|
+
} else if (existing.length) {
|
|
78
|
+
suggested = existing.slice(0, 3).map((d) => `./${d}`);
|
|
79
|
+
} else {
|
|
80
|
+
suggested = ["."];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const cursorCmd = `/graphify ${suggested.join(" ")}`;
|
|
84
|
+
return { existing_dirs: existing, suggested, cursor_command: cursorCmd };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function syncGraphArtifacts(rootDir) {
|
|
88
|
+
const outDir = path.join(rootDir, "graphify-out");
|
|
89
|
+
const reportSrc = path.join(outDir, "GRAPH_REPORT.md");
|
|
90
|
+
const jsonSrc = path.join(outDir, "graph.json");
|
|
91
|
+
const actions = [];
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(reportSrc) && !fs.existsSync(jsonSrc)) {
|
|
94
|
+
return { ok: false, error: "graphify-out/ not found — run /graphify in Cursor first", actions };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (fs.existsSync(reportSrc)) {
|
|
98
|
+
fs.copyFileSync(reportSrc, path.join(rootDir, "GRAPH_REPORT.md"));
|
|
99
|
+
actions.push("GRAPH_REPORT.md");
|
|
100
|
+
}
|
|
101
|
+
if (fs.existsSync(jsonSrc)) {
|
|
102
|
+
const destDir = path.join(rootDir, ".capint", "graph");
|
|
103
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
104
|
+
fs.copyFileSync(jsonSrc, path.join(destDir, "graph.json"));
|
|
105
|
+
actions.push(".capint/graph/graph.json");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { ok: actions.length > 0, actions };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function tryGraphifyCursorInstall(rootDir) {
|
|
112
|
+
try {
|
|
113
|
+
execSync("graphify install cursor --project", {
|
|
114
|
+
cwd: rootDir,
|
|
115
|
+
stdio: "pipe",
|
|
116
|
+
encoding: "utf-8"
|
|
117
|
+
});
|
|
118
|
+
return { ok: true, path: path.join(rootDir, ".cursor", "rules", "graphify.mdc") };
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return { ok: false, error: (e.stderr || e.message || "graphify install failed").trim() };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeStackManifest(rootDir, report) {
|
|
125
|
+
const manifest = {
|
|
126
|
+
version: 1,
|
|
127
|
+
updated_at: new Date().toISOString(),
|
|
128
|
+
doc: STACK_DOC,
|
|
129
|
+
layers: {
|
|
130
|
+
workflows: report.layers.layer_workflows.status,
|
|
131
|
+
routing: report.layers.layer_routing.status,
|
|
132
|
+
graph: report.providers.tiers.tier1_graphify.status,
|
|
133
|
+
memory: report.providers.tiers.tier2_agentmemory.status
|
|
134
|
+
},
|
|
135
|
+
graph_paths: report.graph_paths
|
|
136
|
+
};
|
|
137
|
+
fs.mkdirSync(path.join(rootDir, ".capint"), { recursive: true });
|
|
138
|
+
fs.writeFileSync(path.join(rootDir, STACK_MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
|
|
139
|
+
return manifest;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function runStackDoctor(rootDir) {
|
|
143
|
+
const providers = await runProvidersDoctor(rootDir);
|
|
144
|
+
providers.recipe = STACK_DOC;
|
|
145
|
+
|
|
146
|
+
const layers = {
|
|
147
|
+
layer_workflows: tierWorkflows(rootDir),
|
|
148
|
+
layer_routing: tierRouting(rootDir)
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const graph_paths = detectGraphPaths(rootDir);
|
|
152
|
+
|
|
153
|
+
const stackReady =
|
|
154
|
+
layers.layer_routing.status === "ok" &&
|
|
155
|
+
layers.layer_workflows.status !== "missing" &&
|
|
156
|
+
providers.tiers.tier1_graphify.status === "ok" &&
|
|
157
|
+
providers.tiers.tier2_agentmemory.status === "ok";
|
|
158
|
+
|
|
159
|
+
const recommendations = [];
|
|
160
|
+
if (layers.layer_routing.status !== "ok") recommendations.push("Run: capint init");
|
|
161
|
+
if (layers.layer_workflows.status === "missing") {
|
|
162
|
+
recommendations.push("Init with --bundle full (workflows/) or copy workflows from CapInt package.");
|
|
163
|
+
}
|
|
164
|
+
if (providers.tiers.tier1_graphify.status !== "ok") {
|
|
165
|
+
recommendations.push(`Cursor: ${graph_paths.cursor_command} then capint stack graph sync`);
|
|
166
|
+
}
|
|
167
|
+
if (providers.tiers.tier2_agentmemory.status !== "ok") {
|
|
168
|
+
recommendations.push("Machine: agentmemory (background) + Cursor MCP — see capint stack setup");
|
|
169
|
+
}
|
|
170
|
+
if (!providers.env.local_context_enabled) {
|
|
171
|
+
recommendations.push("Optional: CAPINT_LOCAL_CONTEXT=1 on route for file + graph excerpts.");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
mode: "read_only",
|
|
176
|
+
doc: STACK_DOC,
|
|
177
|
+
stack_ready: stackReady,
|
|
178
|
+
layers,
|
|
179
|
+
graph_paths,
|
|
180
|
+
providers,
|
|
181
|
+
env: providers.env,
|
|
182
|
+
recommendations,
|
|
183
|
+
daily_use: {
|
|
184
|
+
chat: "GUNLUK.md — natural language tasks",
|
|
185
|
+
heavy: "/blueprint then /forge (challenge gate before forge)",
|
|
186
|
+
memory: "agentmemory MCP + HANDOFF/DONE at session end",
|
|
187
|
+
graph: graph_paths.cursor_command
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function runStackSetup(rootDir, { installGraphifyRule = true } = {}) {
|
|
193
|
+
const doctorBefore = await runStackDoctor(rootDir);
|
|
194
|
+
const steps = [];
|
|
195
|
+
|
|
196
|
+
if (doctorBefore.layers.layer_routing.status !== "ok") {
|
|
197
|
+
steps.push({ id: "init", status: "required", action: "capint init [--bundle full]" });
|
|
198
|
+
} else {
|
|
199
|
+
steps.push({ id: "init", status: "ok", action: "CapInt scaffold present" });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (installGraphifyRule && !fileExists(rootDir, ".cursor/rules/graphify.mdc")) {
|
|
203
|
+
const inst = tryGraphifyCursorInstall(rootDir);
|
|
204
|
+
steps.push({
|
|
205
|
+
id: "graphify_cursor",
|
|
206
|
+
status: inst.ok ? "ok" : "manual",
|
|
207
|
+
action: inst.ok ? "graphify install cursor --project" : "uv tool install graphifyy && graphify install cursor --project",
|
|
208
|
+
detail: inst.ok ? inst.path : inst.error
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
steps.push({ id: "graphify_cursor", status: "ok", action: ".cursor/rules/graphify.mdc" });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
steps.push({
|
|
215
|
+
id: "graph_build",
|
|
216
|
+
status: doctorBefore.providers.tiers.tier1_graphify.status === "ok" ? "ok" : "manual",
|
|
217
|
+
action: doctorBefore.graph_paths.cursor_command,
|
|
218
|
+
then: "capint stack graph sync"
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
steps.push({
|
|
222
|
+
id: "agentmemory",
|
|
223
|
+
status: doctorBefore.providers.tiers.tier2_agentmemory.status === "ok" ? "ok" : "manual",
|
|
224
|
+
action: "agentmemory (separate terminal) + ~/.cursor/mcp.json agentmemory block",
|
|
225
|
+
doc: "https://github.com/rohitg00/agentmemory#other-agents"
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const manifest = writeStackManifest(rootDir, doctorBefore);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
doc: STACK_DOC,
|
|
232
|
+
manifest,
|
|
233
|
+
steps,
|
|
234
|
+
doctor: await runStackDoctor(rootDir)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function formatStackSummary(report) {
|
|
239
|
+
const lines = [];
|
|
240
|
+
lines.push("CapInt stack (4 layers):");
|
|
241
|
+
lines.push(` 1. Workflows ${report.layers.layer_workflows.status} — ANWS-style gates (/forge, /challenge)`);
|
|
242
|
+
lines.push(` 2. Routing ${report.layers.layer_routing.status} — capint route + HANDOFF/GUNLUK`);
|
|
243
|
+
lines.push(` 3. Graph ${report.providers.tiers.tier1_graphify.status} — graphify → CapInt reads artifacts`);
|
|
244
|
+
lines.push(` 4. Memory ${report.providers.tiers.tier2_agentmemory.status} — agentmemory sidecar + MCP`);
|
|
245
|
+
lines.push(` Stack ready: ${report.stack_ready ? "yes" : "no"} — doc: ${report.doc}`);
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = {
|
|
250
|
+
STACK_DOC,
|
|
251
|
+
STACK_MANIFEST,
|
|
252
|
+
detectGraphPaths,
|
|
253
|
+
syncGraphArtifacts,
|
|
254
|
+
runStackDoctor,
|
|
255
|
+
runStackSetup,
|
|
256
|
+
formatStackSummary,
|
|
257
|
+
tierWorkflows,
|
|
258
|
+
tierRouting
|
|
259
|
+
};
|
package/src/lib/ui-server.js
CHANGED
|
@@ -4,6 +4,10 @@ const path = require("path");
|
|
|
4
4
|
const { URL } = require("url");
|
|
5
5
|
const { routeTask, formatTextReport } = require("./route-engine");
|
|
6
6
|
const { runDoctor, runStatus } = require("./doctor");
|
|
7
|
+
const { planRecover, applyRecover } = require("./recover");
|
|
8
|
+
const { enableSkill } = require("./disabled-skills");
|
|
9
|
+
const { appendFeedback } = require("./feedback");
|
|
10
|
+
const { resolveLocale, parseAcceptLanguage } = require("./i18n");
|
|
7
11
|
|
|
8
12
|
const MIME = {
|
|
9
13
|
".html": "text/html; charset=utf-8",
|
|
@@ -42,8 +46,14 @@ function json(res, status, data) {
|
|
|
42
46
|
res.end(body);
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
function
|
|
46
|
-
|
|
49
|
+
function resolveRequestLocale(req, body = {}) {
|
|
50
|
+
if (body.locale) return resolveLocale(body.locale);
|
|
51
|
+
return resolveLocale(parseAcceptLanguage(req.headers["accept-language"]));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function projectHealth(rootDir, locale) {
|
|
55
|
+
const loc = resolveLocale(locale);
|
|
56
|
+
const doctor = runDoctor(rootDir, { locale: loc });
|
|
47
57
|
const status = runStatus(rootDir);
|
|
48
58
|
return {
|
|
49
59
|
ok: doctor.pass,
|
|
@@ -72,6 +82,9 @@ function buildChatCopy(result) {
|
|
|
72
82
|
const opts = (ei.confirm_options || []).map((o) => o.id).join(" | ");
|
|
73
83
|
lines.push(`Confirm: ${opts} (default: ${ei.confirm_default_option})`);
|
|
74
84
|
}
|
|
85
|
+
if (ei.explanation?.reason_plain) {
|
|
86
|
+
lines.push(`Why: ${ei.explanation.reason_plain}`);
|
|
87
|
+
}
|
|
75
88
|
if (ei.explanation?.files_to_read?.length) {
|
|
76
89
|
lines.push("Read first:");
|
|
77
90
|
for (const f of ei.explanation.files_to_read) lines.push(`- ${f}`);
|
|
@@ -79,13 +92,34 @@ function buildChatCopy(result) {
|
|
|
79
92
|
return lines.join("\n");
|
|
80
93
|
}
|
|
81
94
|
|
|
95
|
+
function applyDoctorAction(rootDir, actionId, doctor) {
|
|
96
|
+
const action = (doctor.actions || []).find((a) => a.id === actionId);
|
|
97
|
+
if (!action) return { error: `Unknown action: ${actionId}` };
|
|
98
|
+
|
|
99
|
+
if (action.id === "recover_latest") {
|
|
100
|
+
const plan = planRecover(rootDir, { latest: true });
|
|
101
|
+
if (plan.error) return plan;
|
|
102
|
+
return applyRecover(rootDir, plan);
|
|
103
|
+
}
|
|
104
|
+
if (action.id === "enable_core" && action.skill) {
|
|
105
|
+
return enableSkill(rootDir, action.skill);
|
|
106
|
+
}
|
|
107
|
+
return { error: "Action not supported via UI" };
|
|
108
|
+
}
|
|
109
|
+
|
|
82
110
|
function createUiServer({ rootDir, uiDir = pkgUiDir(), port = 3721, host = "127.0.0.1" }) {
|
|
83
111
|
const server = http.createServer(async (req, res) => {
|
|
84
112
|
const url = new URL(req.url, `http://${host}:${port}`);
|
|
85
113
|
const pathname = url.pathname;
|
|
114
|
+
const locale = resolveRequestLocale(req);
|
|
86
115
|
|
|
87
116
|
if (req.method === "GET" && pathname === "/api/health") {
|
|
88
|
-
return json(res, 200, projectHealth(rootDir));
|
|
117
|
+
return json(res, 200, projectHealth(rootDir, locale));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (req.method === "GET" && pathname === "/api/locale") {
|
|
121
|
+
const { loadLocale } = require("./i18n");
|
|
122
|
+
return json(res, 200, { locale, strings: loadLocale(locale) });
|
|
89
123
|
}
|
|
90
124
|
|
|
91
125
|
if (req.method === "POST" && pathname === "/api/route") {
|
|
@@ -93,7 +127,16 @@ function createUiServer({ rootDir, uiDir = pkgUiDir(), port = 3721, host = "127.
|
|
|
93
127
|
const body = await readBody(req);
|
|
94
128
|
const task = String(body.task || "").trim();
|
|
95
129
|
if (!task) return json(res, 400, { error: "task required" });
|
|
96
|
-
const
|
|
130
|
+
const prevLocale = process.env.CAPINT_LOCALE;
|
|
131
|
+
const reqLocale = resolveRequestLocale(req, body);
|
|
132
|
+
process.env.CAPINT_LOCALE = reqLocale;
|
|
133
|
+
let result;
|
|
134
|
+
try {
|
|
135
|
+
result = routeTask(task, rootDir);
|
|
136
|
+
} finally {
|
|
137
|
+
if (prevLocale === undefined) delete process.env.CAPINT_LOCALE;
|
|
138
|
+
else process.env.CAPINT_LOCALE = prevLocale;
|
|
139
|
+
}
|
|
97
140
|
if (result.error) return json(res, 400, { error: result.error });
|
|
98
141
|
return json(res, 200, {
|
|
99
142
|
result,
|
|
@@ -105,6 +148,44 @@ function createUiServer({ rootDir, uiDir = pkgUiDir(), port = 3721, host = "127.
|
|
|
105
148
|
}
|
|
106
149
|
}
|
|
107
150
|
|
|
151
|
+
if (req.method === "POST" && pathname === "/api/recover") {
|
|
152
|
+
try {
|
|
153
|
+
const body = await readBody(req);
|
|
154
|
+
const apply = Boolean(body.apply);
|
|
155
|
+
const actionId = body.action_id;
|
|
156
|
+
|
|
157
|
+
if (actionId) {
|
|
158
|
+
const doctor = runDoctor(rootDir, { locale: resolveRequestLocale(req, body) });
|
|
159
|
+
if (!apply) {
|
|
160
|
+
const action = (doctor.actions || []).find((a) => a.id === actionId);
|
|
161
|
+
return json(res, 200, { dry_run: true, action });
|
|
162
|
+
}
|
|
163
|
+
const result = applyDoctorAction(rootDir, actionId, doctor);
|
|
164
|
+
if (result.error) return json(res, 400, result);
|
|
165
|
+
return json(res, 200, { ok: true, applied: actionId, result, health: projectHealth(rootDir) });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const plan = planRecover(rootDir, { latest: body.latest !== false });
|
|
169
|
+
if (plan.error) return json(res, 400, plan);
|
|
170
|
+
if (!apply) return json(res, 200, { dry_run: true, ...plan });
|
|
171
|
+
const result = applyRecover(rootDir, plan);
|
|
172
|
+
return json(res, 200, { ok: true, result, health: projectHealth(rootDir) });
|
|
173
|
+
} catch (e) {
|
|
174
|
+
return json(res, 400, { error: e.message });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (req.method === "POST" && pathname === "/api/feedback") {
|
|
179
|
+
try {
|
|
180
|
+
const body = await readBody(req);
|
|
181
|
+
const result = appendFeedback(rootDir, body);
|
|
182
|
+
if (result.error) return json(res, 400, result);
|
|
183
|
+
return json(res, 200, result);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return json(res, 400, { error: e.message });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
108
189
|
let filePath = pathname === "/" ? "/index.html" : pathname;
|
|
109
190
|
filePath = path.normalize(filePath).replace(/^(\.\.[/\\])+/, "");
|
|
110
191
|
const abs = path.join(uiDir, filePath);
|
|
@@ -136,4 +217,10 @@ function createUiServer({ rootDir, uiDir = pkgUiDir(), port = 3721, host = "127.
|
|
|
136
217
|
};
|
|
137
218
|
}
|
|
138
219
|
|
|
139
|
-
module.exports = {
|
|
220
|
+
module.exports = {
|
|
221
|
+
createUiServer,
|
|
222
|
+
projectHealth,
|
|
223
|
+
buildChatCopy,
|
|
224
|
+
applyDoctorAction,
|
|
225
|
+
pkgUiDir
|
|
226
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# DONE — {{project_name}}
|
|
2
|
+
|
|
3
|
+
> **Append-only** tamamlanan işler. Agent geniş kapsam önermeden önce buraya bakar.
|
|
4
|
+
> Aktif oturum: [HANDOFF.md](HANDOFF.md) (varsa)
|
|
5
|
+
|
|
6
|
+
## Log
|
|
7
|
+
|
|
8
|
+
| Date | Summary | Verification |
|
|
9
|
+
|------|---------|--------------|
|
|
10
|
+
| {{date}} | CapInt init — proje hazır | `capint doctor` |
|
|
@@ -2,11 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
> **Terminal zorunlu değil.** Kurulumdan sonra her şey chat’te.
|
|
4
4
|
|
|
5
|
+
## CapInt stack (4 katman)
|
|
6
|
+
|
|
7
|
+
| Katman | Günlük |
|
|
8
|
+
|--------|--------|
|
|
9
|
+
| Workflows | `/forge` `/blueprint` — drift/regresyon kapısı |
|
|
10
|
+
| Routing | Bu dosya + chat (Execution Intent) |
|
|
11
|
+
| Graph | Cursor `/graphify …` (proje değişince `--update`) |
|
|
12
|
+
| Memory | agentmemory MCP + oturum kapanışında HANDOFF/DONE |
|
|
13
|
+
|
|
14
|
+
Kurulum: `capint init --stack` → `capint stack doctor` · [docs/capint-stack.md](docs/capint-stack.md)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
5
18
|
1. Chat'e görevi doğal dille yaz (+ screenshot varsa ekle).
|
|
6
19
|
2. Agent kısa **Execution Intent** gösterir (`Intent` / `Capability` / `Resolution` / `Plan`).
|
|
7
20
|
3. **Hafif iş:** soru yok, doğrudan uygular. **Orta/ağır:** `devam` / `plan` / `analiz` yeterli.
|
|
8
21
|
4. Skill veya workflow adı **ezberlemezsin**; seçimi agent yapar.
|
|
9
22
|
5. `/workflow` veya `/skill` sadece override içindir (nadiren gerekir).
|
|
10
|
-
6. Terminal yalnızca: **ilk kurulum** (`init`), **
|
|
23
|
+
6. Terminal yalnızca: **ilk kurulum** (`init --stack`), **stack doctor**, sorun (`doctor`).
|
|
24
|
+
7. **Oturum kapanışı:** `kapan` veya `HANDOFF + DONE güncelle` — tamamlanan iş `DONE.md`, sıradaki iş `HANDOFF.md`.
|
|
11
25
|
|
|
12
26
|
**Tam kılavuz:** [docs/kullanim-kilavuzu.md](docs/kullanim-kilavuzu.md) · **Chat şablonları:** [docs/conventions/daily-use.md](docs/conventions/daily-use.md)
|
|
@@ -16,7 +16,9 @@ Günlük iş **Cursor (veya IDE) chat’i** — terminal prosedürü değil.
|
|
|
16
16
|
| Görevi normal dille yaz | `AGENT.md` + IDE kurallarını okur |
|
|
17
17
|
| Screenshot / “beklenen vs mevcut” ekle | Kısa Execution Intent; orta/ağırda kısa soru |
|
|
18
18
|
| “Önce plan, onaylayınca kod” de | Seçimine göre plan veya kod |
|
|
19
|
-
| İstersen oturum sonu özet onayla | Değişiklik özeti
|
|
19
|
+
| İstersen oturum sonu özet onayla | Değişiklik özeti; `DONE.md` + `HANDOFF.md` günceller |
|
|
20
|
+
|
|
21
|
+
**Kapanış:** `kapan` veya `HANDOFF + DONE güncelle`
|
|
20
22
|
|
|
21
23
|
**Yazmana gerek yok:** Skill adları, `capint route` çıktısı, matrix jargonu.
|
|
22
24
|
|
package/ui/app.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
let routePayload = null;
|
|
2
|
+
let healthPayload = null;
|
|
3
|
+
let selectedRating = null;
|
|
2
4
|
|
|
3
5
|
function showStep(n) {
|
|
4
6
|
document.querySelectorAll(".step-tab").forEach((el) => {
|
|
@@ -28,15 +30,32 @@ async function fetchHealth() {
|
|
|
28
30
|
return res.json();
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function renderRecoverCard(data) {
|
|
34
|
+
const card = document.getElementById("recover-card");
|
|
35
|
+
const summary = document.getElementById("recover-summary");
|
|
36
|
+
const btn = document.getElementById("btn-recover");
|
|
37
|
+
const actions = data.doctor?.actions || [];
|
|
38
|
+
if (data.ok || !actions.length) {
|
|
39
|
+
card.hidden = true;
|
|
40
|
+
btn.dataset.actionId = "";
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
card.hidden = false;
|
|
44
|
+
summary.textContent = data.doctor.summary_plain || data.doctor.hint;
|
|
45
|
+
btn.dataset.actionId = actions[0].id;
|
|
46
|
+
btn.textContent = actions[0].label || "Düzelt";
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
function formatHealth(data) {
|
|
32
50
|
const d = data.doctor;
|
|
33
51
|
const s = data.status;
|
|
34
52
|
const lines = [
|
|
35
|
-
data.ok ? "✓ Doctor: OK" : "✗ Doctor:
|
|
53
|
+
data.ok ? "✓ Doctor: OK" : "✗ Doctor: sorun var",
|
|
36
54
|
`Skills on disk: ${s.skills_on_disk}`,
|
|
37
55
|
`Matrix: ${s.matrix_version || "—"}`,
|
|
38
56
|
`CapInt pkg: ${s.capint_package || "—"}`
|
|
39
57
|
];
|
|
58
|
+
if (d.summary_plain) lines.push("", d.summary_plain);
|
|
40
59
|
if (d.warnings?.length) {
|
|
41
60
|
lines.push("", "Warnings:");
|
|
42
61
|
for (const w of d.warnings.slice(0, 5)) lines.push(` • ${w}`);
|
|
@@ -49,20 +68,33 @@ function formatHealth(data) {
|
|
|
49
68
|
return lines.join("\n");
|
|
50
69
|
}
|
|
51
70
|
|
|
71
|
+
function escapeHtml(s) {
|
|
72
|
+
return String(s)
|
|
73
|
+
.replace(/&/g, "&")
|
|
74
|
+
.replace(/</g, "<")
|
|
75
|
+
.replace(/>/g, ">")
|
|
76
|
+
.replace(/"/g, """);
|
|
77
|
+
}
|
|
78
|
+
|
|
52
79
|
function renderPlan(payload) {
|
|
53
80
|
const ei = payload.result.execution_intent;
|
|
54
81
|
const ex = ei.explanation || {};
|
|
55
82
|
const card = document.getElementById("plan-card");
|
|
56
|
-
const files = (ex.files_to_read || []).map((f) => `<li><code>${f}</code></li>`).join("");
|
|
83
|
+
const files = (ex.files_to_read || []).map((f) => `<li><code>${escapeHtml(f)}</code></li>`).join("");
|
|
84
|
+
const plain = ex.reason_plain ? `<p class="plain">${escapeHtml(ex.reason_plain)}</p>` : "";
|
|
85
|
+
const tip = ex.tooltip ? `<p class="hint"><span class="tip" title="${escapeHtml(ex.tooltip)}">ⓘ</span> ${escapeHtml(ex.tooltip)}</p>` : "";
|
|
86
|
+
const technical = ex.reason_summary
|
|
87
|
+
? `<details><summary>Teknik detay</summary><p>${escapeHtml(ex.reason_summary)}</p></details>`
|
|
88
|
+
: "";
|
|
57
89
|
const boundary = ei.workspace_boundary?.violated
|
|
58
|
-
? `<p class="error">Workspace sınırı: ${ei.workspace_boundary.override_hint}</p>`
|
|
90
|
+
? `<p class="error">Workspace sınırı: ${escapeHtml(ei.workspace_boundary.override_hint || "")}</p>`
|
|
59
91
|
: "";
|
|
60
92
|
card.innerHTML = `
|
|
61
93
|
<dl>
|
|
62
|
-
<dt>Capability</dt><dd>${ei.capability}</dd>
|
|
63
|
-
<dt>Resolution</dt><dd>${ei.resolution} (${ei.resolution_status || "installed"})</dd>
|
|
64
|
-
<dt>Neden</dt><dd>${
|
|
65
|
-
<dt>Keywords</dt><dd>${(ex.matched_keywords || []).join(", ") || "—"}</dd>
|
|
94
|
+
<dt>Capability</dt><dd>${escapeHtml(ei.capability)}</dd>
|
|
95
|
+
<dt>Resolution</dt><dd>${escapeHtml(ei.resolution)} (${escapeHtml(ei.resolution_status || "installed")})</dd>
|
|
96
|
+
<dt>Neden</dt><dd>${plain}${tip}${technical}</dd>
|
|
97
|
+
<dt>Keywords</dt><dd>${escapeHtml((ex.matched_keywords || []).join(", ") || "—")}</dd>
|
|
66
98
|
<dt>Okunacak dosyalar</dt><dd><ul>${files || "<li>—</li>"}</ul></dd>
|
|
67
99
|
</dl>
|
|
68
100
|
${boundary}
|
|
@@ -86,14 +118,40 @@ document.getElementById("btn-health").addEventListener("click", async () => {
|
|
|
86
118
|
out.textContent = "Kontrol ediliyor…";
|
|
87
119
|
try {
|
|
88
120
|
const data = await fetchHealth();
|
|
121
|
+
healthPayload = data;
|
|
89
122
|
out.textContent = formatHealth(data);
|
|
90
|
-
|
|
123
|
+
renderRecoverCard(data);
|
|
124
|
+
next.disabled = !data.ok && (data.doctor?.actions || []).length > 0 ? false : !data.ok;
|
|
125
|
+
if (data.ok) next.disabled = false;
|
|
91
126
|
} catch (e) {
|
|
92
127
|
out.textContent = `Hata: ${e.message}`;
|
|
93
128
|
next.disabled = true;
|
|
94
129
|
}
|
|
95
130
|
});
|
|
96
131
|
|
|
132
|
+
document.getElementById("btn-recover").addEventListener("click", async () => {
|
|
133
|
+
const actionId = document.getElementById("btn-recover").dataset.actionId;
|
|
134
|
+
if (!actionId) return;
|
|
135
|
+
if (!window.confirm("Önerilen düzeltmeyi uygulamak istiyor musun?")) return;
|
|
136
|
+
const out = document.getElementById("health-out");
|
|
137
|
+
out.textContent = "Düzeltiliyor…";
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch("/api/recover", {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: { "Content-Type": "application/json" },
|
|
142
|
+
body: JSON.stringify({ apply: true, action_id: actionId })
|
|
143
|
+
});
|
|
144
|
+
const data = await res.json();
|
|
145
|
+
if (!res.ok) throw new Error(data.error || "Recover failed");
|
|
146
|
+
healthPayload = data.health;
|
|
147
|
+
out.textContent = formatHealth(data.health);
|
|
148
|
+
renderRecoverCard(data.health);
|
|
149
|
+
document.getElementById("to-step-2").disabled = !data.health.ok;
|
|
150
|
+
} catch (e) {
|
|
151
|
+
out.textContent = `Hata: ${e.message}`;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
97
155
|
document.getElementById("to-step-2").addEventListener("click", () => showStep(2));
|
|
98
156
|
|
|
99
157
|
document.getElementById("btn-route").addEventListener("click", async () => {
|
|
@@ -111,8 +169,8 @@ document.getElementById("btn-route").addEventListener("click", async () => {
|
|
|
111
169
|
try {
|
|
112
170
|
const res = await fetch("/api/route", {
|
|
113
171
|
method: "POST",
|
|
114
|
-
headers: { "Content-Type": "application/json" },
|
|
115
|
-
body: JSON.stringify({ task })
|
|
172
|
+
headers: { "Content-Type": "application/json", "Accept-Language": "tr" },
|
|
173
|
+
body: JSON.stringify({ task, locale: "tr" })
|
|
116
174
|
});
|
|
117
175
|
const data = await res.json();
|
|
118
176
|
if (!res.ok) throw new Error(data.error || "Route failed");
|
|
@@ -158,10 +216,42 @@ document.getElementById("btn-copy").addEventListener("click", async () => {
|
|
|
158
216
|
|
|
159
217
|
document.getElementById("btn-restart").addEventListener("click", () => {
|
|
160
218
|
routePayload = null;
|
|
219
|
+
selectedRating = null;
|
|
161
220
|
document.getElementById("task-input").value = "";
|
|
162
221
|
document.getElementById("chat-copy").value = "";
|
|
163
222
|
document.getElementById("copy-status").textContent = "";
|
|
223
|
+
document.getElementById("feedback-status").textContent = "";
|
|
224
|
+
document.getElementById("feedback-comment").value = "";
|
|
225
|
+
document.querySelectorAll("#feedback-stars button").forEach((b) => b.classList.remove("active"));
|
|
164
226
|
showStep(2);
|
|
165
227
|
});
|
|
166
228
|
|
|
229
|
+
document.querySelectorAll("#feedback-stars button").forEach((btn) => {
|
|
230
|
+
btn.addEventListener("click", () => {
|
|
231
|
+
selectedRating = Number(btn.dataset.rating);
|
|
232
|
+
document.querySelectorAll("#feedback-stars button").forEach((b) => b.classList.remove("active"));
|
|
233
|
+
btn.classList.add("active");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
document.getElementById("btn-feedback").addEventListener("click", async () => {
|
|
238
|
+
const status = document.getElementById("feedback-status");
|
|
239
|
+
try {
|
|
240
|
+
const res = await fetch("/api/feedback", {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: { "Content-Type": "application/json" },
|
|
243
|
+
body: JSON.stringify({
|
|
244
|
+
rating: selectedRating,
|
|
245
|
+
comment: document.getElementById("feedback-comment").value,
|
|
246
|
+
source: "ui"
|
|
247
|
+
})
|
|
248
|
+
});
|
|
249
|
+
const data = await res.json();
|
|
250
|
+
if (!res.ok) throw new Error(data.error || "Feedback failed");
|
|
251
|
+
status.textContent = "Teşekkürler — geri bildirim kaydedildi (yerel).";
|
|
252
|
+
} catch (e) {
|
|
253
|
+
status.textContent = e.message;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
167
257
|
showStep(1);
|
package/ui/index.html
CHANGED
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
<header class="header">
|
|
12
12
|
<h1>CapInt</h1>
|
|
13
13
|
<p class="subtitle">Terminal yok — görevini yaz, planı gör, chat'e kopyala.</p>
|
|
14
|
+
<p class="hint">
|
|
15
|
+
<a href="https://github.com/bcelep/capint/blob/main/docs/kullanim-kilavuzu.md" target="_blank" rel="noopener">SSS / kullanım kılavuzu</a>
|
|
16
|
+
</p>
|
|
14
17
|
</header>
|
|
15
18
|
|
|
16
19
|
<nav class="steps" aria-label="Adımlar">
|
|
@@ -23,11 +26,16 @@
|
|
|
23
26
|
<section class="panel active" data-panel="1">
|
|
24
27
|
<h2>Proje hazır mı?</h2>
|
|
25
28
|
<p class="hint">
|
|
26
|
-
<span class="tip" title="installed = skills/ klasöründe SKILL.md var. Cursor skill listesinde görünmesi gerekmez.">ⓘ</span>
|
|
29
|
+
<span class="tip" id="tip-installed" title="installed = skills/ klasöründe SKILL.md var. Cursor skill listesinde görünmesi gerekmez.">ⓘ</span>
|
|
27
30
|
Skill'ler diskte yüklü mü? Doctor kontrol eder.
|
|
28
31
|
</p>
|
|
29
32
|
<button type="button" class="btn primary" id="btn-health">Kontrol et</button>
|
|
30
33
|
<pre class="output" id="health-out" aria-live="polite"></pre>
|
|
34
|
+
<div id="recover-card" class="card recover-card" hidden>
|
|
35
|
+
<h3>Önerilen düzeltme</h3>
|
|
36
|
+
<p id="recover-summary"></p>
|
|
37
|
+
<button type="button" class="btn primary" id="btn-recover">Düzelt</button>
|
|
38
|
+
</div>
|
|
31
39
|
<div class="actions">
|
|
32
40
|
<button type="button" class="btn" id="to-step-2" disabled>Devam →</button>
|
|
33
41
|
</div>
|
|
@@ -68,6 +76,19 @@
|
|
|
68
76
|
<button type="button" class="btn" id="btn-restart">Yeni görev</button>
|
|
69
77
|
</div>
|
|
70
78
|
<p class="copy-status" id="copy-status" aria-live="polite"></p>
|
|
79
|
+
<fieldset class="feedback-field">
|
|
80
|
+
<legend>Bu akış işine yaradı mı?</legend>
|
|
81
|
+
<div class="stars" id="feedback-stars">
|
|
82
|
+
<button type="button" data-rating="1">1</button>
|
|
83
|
+
<button type="button" data-rating="2">2</button>
|
|
84
|
+
<button type="button" data-rating="3">3</button>
|
|
85
|
+
<button type="button" data-rating="4">4</button>
|
|
86
|
+
<button type="button" data-rating="5">5</button>
|
|
87
|
+
</div>
|
|
88
|
+
<textarea id="feedback-comment" rows="2" placeholder="İsteğe bağlı yorum"></textarea>
|
|
89
|
+
<button type="button" class="btn" id="btn-feedback">Geri bildirim gönder</button>
|
|
90
|
+
<p id="feedback-status" class="copy-status" aria-live="polite"></p>
|
|
91
|
+
</fieldset>
|
|
71
92
|
</section>
|
|
72
93
|
</main>
|
|
73
94
|
<script src="/app.js"></script>
|