@jefuriiij/synthra 0.1.15 → 0.1.17
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/CHANGELOG.md +29 -0
- package/dist/cli/index.js +54 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +2 -2
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +52 -13
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ For older versions, see [GitHub Releases](https://github.com/jefuriiij/synthra/r
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.1.17] — 2026-05-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`syn .` scaffolds an agent-onboarding CLAUDE.md on brand-new projects.**
|
|
15
|
+
When a project has no CLAUDE.md, Synthra now writes a lean skeleton —
|
|
16
|
+
`Build & test`, `Conventions`, `Key decisions`, `Gotchas` (with TODO
|
|
17
|
+
prompts) — *above* its managed policy block, instead of a bare policy
|
|
18
|
+
block. This is the durable "why/how" layer the graph can't infer; the
|
|
19
|
+
graph still owns "what/where." Fill it in, or run `/init` to auto-draft.
|
|
20
|
+
The skeleton is written **once** and lives outside the
|
|
21
|
+
`<!-- synthra-policy -->` markers, so re-running `syn .` (which
|
|
22
|
+
refreshes the policy block) never clobbers what you've written.
|
|
23
|
+
Projects that already have a CLAUDE.md are untouched — no skeleton is
|
|
24
|
+
injected.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## [0.1.16] — 2026-05-29
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **Moat card shows 50 recent gate decisions** (was 12). The inline list
|
|
33
|
+
already scrolls within the card, and the `/data` payload already carries
|
|
34
|
+
up to 500 gates, so this just renders more of them. The headline block
|
|
35
|
+
count was always all-time/uncapped — unchanged.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
10
39
|
## [0.1.15] — 2026-05-29
|
|
11
40
|
|
|
12
41
|
### Changed
|
package/dist/cli/index.js
CHANGED
|
@@ -18,7 +18,7 @@ var init_package = __esm({
|
|
|
18
18
|
"package.json"() {
|
|
19
19
|
package_default = {
|
|
20
20
|
name: "@jefuriiij/synthra",
|
|
21
|
-
version: "0.1.
|
|
21
|
+
version: "0.1.17",
|
|
22
22
|
publishConfig: {
|
|
23
23
|
access: "public"
|
|
24
24
|
},
|
|
@@ -1041,7 +1041,7 @@ var public_default = `<!doctype html>
|
|
|
1041
1041
|
return;
|
|
1042
1042
|
}
|
|
1043
1043
|
const frag = document.createDocumentFragment();
|
|
1044
|
-
for (const g of gates.slice(0,
|
|
1044
|
+
for (const g of gates.slice(0, 50)) {
|
|
1045
1045
|
const row = document.createElement('div');
|
|
1046
1046
|
row.className = 'gate-row';
|
|
1047
1047
|
const cls = g.decision === 'block' ? 'block' : 'allow';
|
|
@@ -3198,9 +3198,11 @@ async function readSymbolIndex(path) {
|
|
|
3198
3198
|
|
|
3199
3199
|
// src/cli/bootstrap.ts
|
|
3200
3200
|
import { mkdir as mkdir5, readFile as readFile10, stat as stat2, writeFile as writeFile5 } from "fs/promises";
|
|
3201
|
+
import { basename as basename4 } from "path";
|
|
3201
3202
|
|
|
3202
3203
|
// src/hooks/claude-md.ts
|
|
3203
3204
|
import { readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
3205
|
+
import { basename as basename3, dirname as dirname6 } from "path";
|
|
3204
3206
|
var POLICY_VERSION = 3;
|
|
3205
3207
|
var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
|
|
3206
3208
|
var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
|
|
@@ -3301,7 +3303,39 @@ function policyBlock() {
|
|
|
3301
3303
|
POLICY_END
|
|
3302
3304
|
].join("\n");
|
|
3303
3305
|
}
|
|
3304
|
-
|
|
3306
|
+
function onboardingSkeleton(projectName) {
|
|
3307
|
+
return [
|
|
3308
|
+
`# ${projectName}`,
|
|
3309
|
+
"",
|
|
3310
|
+
"> Onboarding notes for AI coding agents. Synthra's graph already knows the",
|
|
3311
|
+
"> code's *structure* (files, symbols, imports) \u2014 this file is for what the",
|
|
3312
|
+
"> graph can't infer: how to run the project, its conventions, and the",
|
|
3313
|
+
"> decisions behind them. Keep it lean and current; delete prompts you don't need.",
|
|
3314
|
+
"",
|
|
3315
|
+
"## Build & test",
|
|
3316
|
+
"",
|
|
3317
|
+
"- TODO: install deps / build",
|
|
3318
|
+
"- TODO: run tests / lint / typecheck",
|
|
3319
|
+
"- TODO: run the app locally",
|
|
3320
|
+
"",
|
|
3321
|
+
"## Conventions",
|
|
3322
|
+
"",
|
|
3323
|
+
"- TODO: code style, naming, file layout the agent should follow",
|
|
3324
|
+
"",
|
|
3325
|
+
"## Key decisions",
|
|
3326
|
+
"",
|
|
3327
|
+
'- TODO: non-obvious choices and *why* ("we use X not Y because \u2026")',
|
|
3328
|
+
"",
|
|
3329
|
+
"## Gotchas",
|
|
3330
|
+
"",
|
|
3331
|
+
`- TODO: traps, footguns, "don't touch X without Y"`,
|
|
3332
|
+
"",
|
|
3333
|
+
"_Tip: run `/init` in Claude Code to auto-draft the sections above, then trim",
|
|
3334
|
+
"to the durable bits. Synthra manages its own block below \u2014 leave it._",
|
|
3335
|
+
""
|
|
3336
|
+
].join("\n");
|
|
3337
|
+
}
|
|
3338
|
+
async function patchClaudeMd(path, projectName) {
|
|
3305
3339
|
let existing;
|
|
3306
3340
|
try {
|
|
3307
3341
|
existing = await readFile9(path, "utf8");
|
|
@@ -3310,7 +3344,8 @@ async function patchClaudeMd(path) {
|
|
|
3310
3344
|
}
|
|
3311
3345
|
const block = policyBlock();
|
|
3312
3346
|
if (existing === null) {
|
|
3313
|
-
|
|
3347
|
+
const name = projectName || basename3(dirname6(path)) || "this project";
|
|
3348
|
+
await writeFile4(path, onboardingSkeleton(name) + "\n" + block + "\n", "utf8");
|
|
3314
3349
|
return { created: true, updated: false, skipped: false };
|
|
3315
3350
|
}
|
|
3316
3351
|
const stripped = existing.replace(ANY_BLOCK_RE, "");
|
|
@@ -3367,7 +3402,7 @@ async function bootstrap(paths) {
|
|
|
3367
3402
|
const contextCreated = await ensureDir(paths.contextDir);
|
|
3368
3403
|
const gitignoreUpdated = await patchGitignore(paths.gitignore);
|
|
3369
3404
|
const claudeMdExistedBefore = await exists(paths.claudeMd);
|
|
3370
|
-
const patch = await patchClaudeMd(paths.claudeMd);
|
|
3405
|
+
const patch = await patchClaudeMd(paths.claudeMd, basename4(paths.projectRoot));
|
|
3371
3406
|
return {
|
|
3372
3407
|
graphCreated,
|
|
3373
3408
|
contextCreated,
|
|
@@ -3420,8 +3455,12 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
3420
3455
|
if (boot.graphCreated) log.info(" created .synthra-graph/");
|
|
3421
3456
|
if (boot.contextCreated) log.info(" created .synthra/");
|
|
3422
3457
|
if (boot.gitignoreUpdated) log.info(" updated .gitignore");
|
|
3423
|
-
if (boot.claudeMdCreated)
|
|
3424
|
-
|
|
3458
|
+
if (boot.claudeMdCreated) {
|
|
3459
|
+
log.info(" created CLAUDE.md \u2014 onboarding skeleton for the agent");
|
|
3460
|
+
log.info(" \u21B3 fill in Build / Conventions / Decisions (or run /init in Claude to auto-draft)");
|
|
3461
|
+
} else if (boot.claudeMdUpdated) {
|
|
3462
|
+
log.info(" updated CLAUDE.md");
|
|
3463
|
+
}
|
|
3425
3464
|
}
|
|
3426
3465
|
const walked = [];
|
|
3427
3466
|
for await (const file of walk(projectRoot)) walked.push(file);
|
|
@@ -3721,7 +3760,7 @@ function resolveBranchPaths(contextDir, branch, isDefault) {
|
|
|
3721
3760
|
|
|
3722
3761
|
// src/memory/context-md.ts
|
|
3723
3762
|
import { mkdir as mkdir6, readFile as readFile12, writeFile as writeFile6 } from "fs/promises";
|
|
3724
|
-
import { dirname as
|
|
3763
|
+
import { dirname as dirname7 } from "path";
|
|
3725
3764
|
var MAX_BULLETS = 3;
|
|
3726
3765
|
function deriveContextMd(entries, branch) {
|
|
3727
3766
|
const tasks = entries.filter((e) => e.type === "task").reverse();
|
|
@@ -3764,13 +3803,13 @@ function formatContextMd(ctx) {
|
|
|
3764
3803
|
return lines.join("\n");
|
|
3765
3804
|
}
|
|
3766
3805
|
async function writeContextMd(path, ctx) {
|
|
3767
|
-
await mkdir6(
|
|
3806
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
3768
3807
|
await writeFile6(path, formatContextMd(ctx), "utf8");
|
|
3769
3808
|
}
|
|
3770
3809
|
|
|
3771
3810
|
// src/memory/context-store.ts
|
|
3772
3811
|
import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile7 } from "fs/promises";
|
|
3773
|
-
import { dirname as
|
|
3812
|
+
import { dirname as dirname8 } from "path";
|
|
3774
3813
|
var SCHEMA_VERSION2 = 1;
|
|
3775
3814
|
async function readEntries(path) {
|
|
3776
3815
|
try {
|
|
@@ -3782,7 +3821,7 @@ async function readEntries(path) {
|
|
|
3782
3821
|
}
|
|
3783
3822
|
}
|
|
3784
3823
|
async function writeEntries(path, entries) {
|
|
3785
|
-
await mkdir7(
|
|
3824
|
+
await mkdir7(dirname8(path), { recursive: true });
|
|
3786
3825
|
const store = { schema_version: SCHEMA_VERSION2, entries };
|
|
3787
3826
|
await writeFile7(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
3788
3827
|
}
|
|
@@ -4477,7 +4516,7 @@ async function handleContextUpdate(req, ctx) {
|
|
|
4477
4516
|
|
|
4478
4517
|
// src/server/routes/gate.ts
|
|
4479
4518
|
import { appendFile as appendFile2, mkdir as mkdir8 } from "fs/promises";
|
|
4480
|
-
import { dirname as
|
|
4519
|
+
import { dirname as dirname9 } from "path";
|
|
4481
4520
|
var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
|
|
4482
4521
|
var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
|
|
4483
4522
|
function extractQuery(toolName, input) {
|
|
@@ -4507,7 +4546,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens) {
|
|
|
4507
4546
|
}
|
|
4508
4547
|
async function logDecision(ctx, toolName, query, decision, reason) {
|
|
4509
4548
|
try {
|
|
4510
|
-
await mkdir8(
|
|
4549
|
+
await mkdir8(dirname9(ctx.paths.gateLog), { recursive: true });
|
|
4511
4550
|
const entry = {
|
|
4512
4551
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4513
4552
|
tool: toolName,
|
|
@@ -4564,14 +4603,14 @@ async function handleGate(req, ctx) {
|
|
|
4564
4603
|
|
|
4565
4604
|
// src/server/routes/log.ts
|
|
4566
4605
|
import { appendFile as appendFile3, mkdir as mkdir9 } from "fs/promises";
|
|
4567
|
-
import { dirname as
|
|
4606
|
+
import { dirname as dirname10 } from "path";
|
|
4568
4607
|
async function handleLog(entry, ctx) {
|
|
4569
4608
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
4570
4609
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
4571
4610
|
}
|
|
4572
4611
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4573
4612
|
const record = { ...entry, written_at };
|
|
4574
|
-
await mkdir9(
|
|
4613
|
+
await mkdir9(dirname10(ctx.paths.tokenLog), { recursive: true });
|
|
4575
4614
|
await appendFile3(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
4576
4615
|
return { ok: true, written_at };
|
|
4577
4616
|
}
|