javi-forge 1.38.5 → 1.39.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.
Files changed (39) hide show
  1. package/assets/claude-hooks/javi-forge-windows-secure-object.ps1 +29 -6
  2. package/assets/claude-hooks/manifest.json +1 -1
  3. package/assets/preparation-worker.c +180 -0
  4. package/dist/cli/dispatch/doctor.d.ts +3 -0
  5. package/dist/cli/dispatch/doctor.js +27 -0
  6. package/dist/cli/dispatch/simple-renderers.d.ts +1 -1
  7. package/dist/cli/dispatch/simple-renderers.js +3 -3
  8. package/dist/cli/help.d.ts +5 -1
  9. package/dist/cli/help.js +2 -0
  10. package/dist/cli/main.js +26 -5
  11. package/dist/commands/doctor.d.ts +7 -2
  12. package/dist/commands/doctor.js +32 -13
  13. package/dist/commands/plugin.d.ts +3 -1
  14. package/dist/commands/plugin.js +20 -8
  15. package/dist/lib/__fixtures__/fake-secure-fs.js +6 -3
  16. package/dist/lib/agent-skills.d.ts +2 -1
  17. package/dist/lib/agent-skills.js +36 -22
  18. package/dist/lib/plugin-replacement.d.ts +28 -0
  19. package/dist/lib/plugin-replacement.js +170 -0
  20. package/dist/lib/plugin.d.ts +2 -1
  21. package/dist/lib/plugin.js +26 -21
  22. package/dist/lib/preparation-authorization.d.ts +31 -0
  23. package/dist/lib/preparation-authorization.js +134 -0
  24. package/dist/lib/preparation-capability.d.ts +64 -0
  25. package/dist/lib/preparation-capability.js +106 -0
  26. package/dist/lib/preparation-executor.d.ts +49 -0
  27. package/dist/lib/preparation-executor.js +427 -0
  28. package/dist/lib/preparation-stager.d.ts +25 -0
  29. package/dist/lib/preparation-stager.js +207 -0
  30. package/dist/lib/preparation-status-ok.d.ts +36 -0
  31. package/dist/lib/preparation-status-ok.js +161 -0
  32. package/dist/lib/secure-fs-posix.js +7 -2
  33. package/dist/lib/secure-fs-transaction.d.ts +12 -2
  34. package/dist/lib/secure-fs-transaction.js +62 -14
  35. package/dist/lib/secure-fs-windows.js +9 -1
  36. package/dist/ui/Doctor.d.ts +7 -1
  37. package/dist/ui/Doctor.js +3 -24
  38. package/dist/ui/Plugin.js +31 -1
  39. package/package.json +1 -1
@@ -92,6 +92,7 @@ namespace JaviForge
92
92
  private const uint WRITE_OWNER_A = 0x00080000;
93
93
  private const uint DELETE_A = 0x00010000;
94
94
  private const uint FILE_READ_ATTRIBUTES = 0x0080;
95
+ private const uint DIRECTORY_FLUSH_ACCESS = GENERIC_WRITE | READ_CONTROL | FILE_READ_ATTRIBUTES;
95
96
 
96
97
  private const uint FILE_SHARE_READ = 0x1;
97
98
  private const uint FILE_SHARE_WRITE = 0x2;
@@ -866,12 +867,34 @@ namespace JaviForge
866
867
  // No opaque re-check here (unlike unlink/rmdir): both endpoints are our
867
868
  // own just-created nodes under the held parent-chain lock, so no on-path
868
869
  // node can be swapped while the chain handle is held.
869
- if (!MoveFileExW(fromFull, toFull,
870
- MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
871
- return OpResult.Fail(R_CHAIN, "rename failed " + Marshal.GetLastWin32Error()
872
- + " " + fromFull);
873
- FlushFileBuffers(dir.Handle);
874
- return OpResult.Good();
870
+ int flushErr;
871
+ IntPtr flushHandle = OpenNoFollow(dir.Path, DIRECTORY_FLUSH_ACCESS,
872
+ FILE_SHARE_READ | FILE_SHARE_WRITE, out flushErr);
873
+ if (flushHandle == INVALID_HANDLE)
874
+ return OpResult.Fail(R_CHAIN, "rename flush-open failed " + flushErr + " " + dir.Path);
875
+ try
876
+ {
877
+ uint attr; string opaque; bool zeroId;
878
+ if (!ReadInfo(flushHandle, out attr, out opaque, out zeroId))
879
+ return OpResult.Fail(R_CHAIN, "rename flush-open info failed " + dir.Path);
880
+ if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
881
+ return OpResult.Fail(R_CHAIN, "rename flush-open reparse point " + dir.Path);
882
+ if ((attr & FILE_ATTRIBUTE_DIRECTORY) == 0)
883
+ return OpResult.Fail(R_CHAIN, "rename flush-open not a directory " + dir.Path);
884
+ if (zeroId)
885
+ return OpResult.Fail(R_CHAIN, "rename flush-open unresolvable identity " + dir.Path);
886
+ if (!String.Equals(opaque, dir.Opaque, StringComparison.OrdinalIgnoreCase))
887
+ return OpResult.Fail(R_CHAIN, "rename flush-open identity changed " + dir.Path);
888
+ if (!MoveFileExW(fromFull, toFull,
889
+ MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
890
+ return OpResult.Fail(R_CHAIN, "rename failed " + Marshal.GetLastWin32Error()
891
+ + " " + fromFull);
892
+ if (!FlushFileBuffers(flushHandle))
893
+ return OpResult.Fail(R_CHAIN, "rename flush failed " + Marshal.GetLastWin32Error()
894
+ + " " + dir.Path);
895
+ return OpResult.Good();
896
+ }
897
+ finally { CloseHandle(flushHandle); }
875
898
  }
876
899
  catch (Exception ex) { return OpResult.Fail(R_CHAIN, "rename exception " + ex.Message); }
877
900
  }
@@ -30,7 +30,7 @@
30
30
  "installerHelpers": {
31
31
  "windowsSecureObject": {
32
32
  "name": "javi-forge-windows-secure-object.ps1",
33
- "sha256": "2289ef6ac6b039ec74dc3ea0894413e243ff9bea963f04008a356b3838f9b8dd"
33
+ "sha256": "4ee446d4e540adbea88c343df540efdcad1a5b26d31481ac5ae7057d8375cd11"
34
34
  }
35
35
  }
36
36
  }
@@ -0,0 +1,180 @@
1
+ /* Fixed Linux x86-64 data-only preparation worker. Build statically; never run
2
+ * generated helpers. Operator approves the exact binary AND this source digest.
3
+ * JSON checks are artifact validation, not helper behavior or domain-schema tests. */
4
+ #define _GNU_SOURCE
5
+ #include <errno.h>
6
+ #include <fcntl.h>
7
+ #include <linux/audit.h>
8
+ #include <linux/filter.h>
9
+ #include <linux/seccomp.h>
10
+ #include <stddef.h>
11
+ #include <stdint.h>
12
+ #include <stdio.h>
13
+ #include <stdlib.h>
14
+ #include <string.h>
15
+ #include <sys/prctl.h>
16
+ #include <sys/socket.h>
17
+ #include <sys/stat.h>
18
+ #include <sys/syscall.h>
19
+ #include <time.h>
20
+ #include <unistd.h>
21
+
22
+ #define LIMIT 1048576
23
+ #define DIR_FLAGS (O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC)
24
+ #define FILE_FLAGS (O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC)
25
+ static const char *names[6] = {"app-request.json", "minimal.py", "test_minimal.py", "dispatch.py", "run_gateway.py", "preparation-result.json"};
26
+ static unsigned char *data[6];
27
+ static size_t sizes[6];
28
+ extern char **environ;
29
+
30
+ #define ALLOW(n) BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_##n, 0, 1), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
31
+ static int restrict_syscalls(void) {
32
+ struct sock_filter rules[] = {
33
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
34
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
35
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
36
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
37
+ ALLOW(read), ALLOW(write), ALLOW(close), ALLOW(fstat), ALLOW(newfstatat),
38
+ ALLOW(fsync), ALLOW(fchmod), ALLOW(mkdirat), ALLOW(unlinkat),
39
+ ALLOW(brk), ALLOW(mmap), ALLOW(munmap), ALLOW(mprotect), ALLOW(futex),
40
+ ALLOW(clock_gettime), ALLOW(alarm), ALLOW(rt_sigaction), ALLOW(rt_sigprocmask),
41
+ ALLOW(rt_sigreturn), ALLOW(exit), ALLOW(exit_group), ALLOW(getpid),
42
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 1, 0),
43
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
44
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])),
45
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, DIR_FLAGS, 2, 0),
46
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, FILE_FLAGS, 1, 0),
47
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
48
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
49
+ };
50
+ struct sock_fprog program = {(unsigned short)(sizeof(rules) / sizeof(rules[0])), rules};
51
+ return prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) || prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program);
52
+ }
53
+ static int isolation_probes(int parent) {
54
+ errno = 0;
55
+ if (syscall(__NR_socket, AF_INET, SOCK_STREAM, 0) != -1 || errno != EPERM) return 0;
56
+ errno = 0;
57
+ if (openat(parent, "credential-probe", O_RDONLY) != -1 || errno != EPERM) return 0;
58
+ errno = 0;
59
+ if (syscall(__NR_clone, 0, 0, 0, 0, 0) != -1 || errno != EPERM) return 0;
60
+ errno = 0;
61
+ if (syscall(__NR_execve, "/worker", 0, 0) != -1 || errno != EPERM) return 0;
62
+ return 1;
63
+ }
64
+
65
+ struct json { const unsigned char *p, *end; };
66
+ static void ws(struct json *j) { while (j->p < j->end && (*j->p == ' ' || *j->p == '\n' || *j->p == '\r' || *j->p == '\t')) j->p++; }
67
+ static int hex(unsigned char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); }
68
+ static int string(struct json *j) {
69
+ if (j->p == j->end || *j->p++ != '"') return 0;
70
+ while (j->p < j->end) {
71
+ unsigned char c = *j->p++;
72
+ if (c == '"') return 1;
73
+ if (c < 32) return 0;
74
+ if (c == '\\') {
75
+ if (j->p == j->end) return 0;
76
+ c = *j->p++;
77
+ if (c == 'u') {
78
+ for (int i = 0; i < 4; i++) if (j->p == j->end || !hex(*j->p++)) return 0;
79
+ } else if (!strchr("\"\\/bfnrt", c)) return 0;
80
+ } else if (c >= 128) {
81
+ unsigned int value; int count;
82
+ if (c >= 0xc2 && c <= 0xdf) { value = c & 31; count = 1; }
83
+ else if (c >= 0xe0 && c <= 0xef) { value = c & 15; count = 2; }
84
+ else if (c >= 0xf0 && c <= 0xf4) { value = c & 7; count = 3; }
85
+ else return 0;
86
+ int remaining = count;
87
+ while (remaining--) { if (j->p == j->end || (*j->p & 0xc0) != 0x80) return 0; value = (value << 6) | (*j->p++ & 63); }
88
+ if ((count == 2 && value < 0x800) || (count == 3 && value < 0x10000) || value > 0x10ffff || (value >= 0xd800 && value <= 0xdfff)) return 0;
89
+ }
90
+ }
91
+ return 0;
92
+ }
93
+ static int value(struct json *j, int depth) {
94
+ ws(j); if (depth > 64 || j->p == j->end) return 0;
95
+ unsigned char c = *j->p;
96
+ if (c == '"') return string(j);
97
+ if (c == '{' || c == '[') {
98
+ unsigned char stop = c == '{' ? '}' : ']'; j->p++; ws(j);
99
+ if (j->p < j->end && *j->p == stop) { j->p++; return 1; }
100
+ for (;;) {
101
+ if (c == '{') { if (!string(j)) return 0; ws(j); if (j->p == j->end || *j->p++ != ':') return 0; }
102
+ if (!value(j, depth + 1)) return 0;
103
+ ws(j); if (j->p == j->end) return 0;
104
+ unsigned char next = *j->p++; if (next == stop) return 1;
105
+ if (next != ',') return 0;
106
+ ws(j);
107
+ }
108
+ }
109
+ const char *literal = c == 't' ? "true" : c == 'f' ? "false" : c == 'n' ? "null" : NULL;
110
+ if (literal) { size_t n = strlen(literal); if ((size_t)(j->end - j->p) < n || memcmp(j->p, literal, n)) return 0; j->p += n; return 1; }
111
+ if (*j->p == '-') j->p++;
112
+ if (j->p == j->end) return 0;
113
+ if (*j->p == '0') j->p++;
114
+ else { if (*j->p < '1' || *j->p > '9') return 0; do { j->p++; } while (j->p < j->end && *j->p >= '0' && *j->p <= '9'); }
115
+ if (j->p < j->end && *j->p == '.') { j->p++; const unsigned char *start = j->p; while (j->p < j->end && *j->p >= '0' && *j->p <= '9') j->p++; if (j->p == start) return 0; }
116
+ if (j->p < j->end && (*j->p == 'e' || *j->p == 'E')) { j->p++; if (j->p < j->end && (*j->p == '+' || *j->p == '-')) j->p++; const unsigned char *start = j->p; while (j->p < j->end && *j->p >= '0' && *j->p <= '9') j->p++; if (j->p == start) return 0; }
117
+ return 1;
118
+ }
119
+ static int object(const unsigned char *bytes, size_t n) { struct json j = {bytes, bytes + n}; ws(&j); if (j.p == j.end || *j.p != '{' || !value(&j, 0)) return 0; ws(&j); return j.p == j.end; }
120
+ static int write_all(int fd, const unsigned char *bytes, size_t length) {
121
+ while (length) { ssize_t n = write(fd, bytes, length); if (n <= 0) return 0; bytes += n; length -= (size_t)n; } return 1;
122
+ }
123
+
124
+ int main(int argc, char **argv) {
125
+ (void)argv;
126
+ alarm(30); umask(077); setvbuf(stdout, NULL, _IONBF, 0);
127
+ if (argc != 1) return 11;
128
+ if (environ && *environ && (environ[1] || strcmp(environ[0], "PWD=/"))) return 12;
129
+ if (clearenv()) return 13;
130
+ struct timespec started; if (clock_gettime(CLOCK_MONOTONIC, &started)) return 10;
131
+ int parent = open("/out", DIR_FLAGS); struct stat parent_stat, existing;
132
+ if (parent < 0 || fstat(parent, &parent_stat) || (parent_stat.st_mode & 0777) != 0700) return 10;
133
+ if (fstatat(parent, "attempt-3", &existing, AT_SYMLINK_NOFOLLOW) == 0 || errno != ENOENT) return 10;
134
+ unsigned long long dev, ino; FILE *identity = fopen("/identity", "r");
135
+ if (!identity || fscanf(identity, "%llu %llu", &dev, &ino) != 2 || dev != (unsigned long long)parent_stat.st_dev || ino != (unsigned long long)parent_stat.st_ino) return 10;
136
+ fclose(identity);
137
+ size_t total = 0;
138
+ for (int i = 0; i < 6; i++) {
139
+ char filename[128]; snprintf(filename, sizeof(filename), "/payload/%s", names[i]);
140
+ int fd = open(filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); struct stat st;
141
+ if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 || st.st_size > LIMIT) return 10;
142
+ sizes[i] = (size_t)st.st_size; total += sizes[i]; if (total > LIMIT) return 10;
143
+ data[i] = malloc(sizes[i] + 1); if (!data[i]) return 10;
144
+ size_t offset = 0;
145
+ while (offset < sizes[i]) { ssize_t n = read(fd, data[i] + offset, sizes[i] - offset); if (n <= 0) return 10; offset += (size_t)n; }
146
+ close(fd); data[i][sizes[i]] = 0;
147
+ }
148
+ if (restrict_syscalls() || !isolation_probes(parent)) return 10;
149
+ #ifdef PREPARATION_FIXTURE_NOT_READY
150
+ for (;;) { /* Test-only readiness stall, never a production build. */ }
151
+ #endif
152
+ puts("READY");
153
+ char go; if (read(STDIN_FILENO, &go, 1) != 1 || go != 'G') return 10;
154
+ alarm(10);
155
+ #ifdef PREPARATION_FIXTURE_STALL
156
+ for (;;) { /* Test-only compiled variant, approved only by fixture keys. */ }
157
+ #endif
158
+ if (!object(data[0], sizes[0]) || !object(data[5], sizes[5])) return 20;
159
+ for (int i = 1; i < 5; i++) if (!sizes[i] || memchr(data[i], 0, sizes[i])) return 20;
160
+ puts("VALID");
161
+ struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now)) return 20;
162
+ long elapsed = now.tv_sec - started.tv_sec; if (elapsed >= 30) return 20;
163
+ alarm((unsigned int)(30 - elapsed));
164
+ if (mkdirat(parent, "attempt-3", 0700)) return 30;
165
+ int dir = openat(parent, "attempt-3", DIR_FLAGS); struct stat directory_stat;
166
+ if (dir < 0 || fstat(dir, &directory_stat)) return 30;
167
+ printf("DIR %llu %llu\n", (unsigned long long)directory_stat.st_dev, (unsigned long long)directory_stat.st_ino);
168
+ for (int i = 0; i < 6; i++) {
169
+ int fd = openat(dir, names[i], FILE_FLAGS, 0600); struct stat st;
170
+ if (fd < 0 || fstat(fd, &st)) return 30;
171
+ printf("FILE %d %llu %llu\n", i, (unsigned long long)st.st_dev, (unsigned long long)st.st_ino);
172
+ if (fchmod(fd, 0600) || !write_all(fd, data[i], sizes[i]) || fsync(fd)) return 30;
173
+ close(fd);
174
+ #ifdef PREPARATION_FIXTURE_PARTIAL
175
+ if (i == 0) return 30;
176
+ #endif
177
+ }
178
+ if (fsync(dir) || fsync(parent)) return 30;
179
+ close(dir); close(parent); puts("DONE"); return 0;
180
+ }
@@ -0,0 +1,3 @@
1
+ import { type DoctorOptions } from "../../commands/doctor.js";
2
+ export default function DoctorController({ dryRun, refreshContext, }: DoctorOptions): import("react").FunctionComponentElement<import("../../ui/Doctor.js").DoctorProps>;
3
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { createElement, useEffect, useState } from "react";
2
+ import { runDoctor } from "../../commands/doctor.js";
3
+ import Doctor from "../../ui/Doctor.js";
4
+ export default function DoctorController({ dryRun = false, refreshContext = false, }) {
5
+ const [result, setResult] = useState(null);
6
+ const [error, setError] = useState(null);
7
+ const [loading, setLoading] = useState(true);
8
+ const runCheck = () => {
9
+ setLoading(true);
10
+ setResult(null);
11
+ setError(null);
12
+ void runDoctor(undefined, { dryRun, refreshContext })
13
+ .then((nextResult) => {
14
+ setResult(nextResult);
15
+ setLoading(false);
16
+ })
17
+ .catch((nextError) => {
18
+ setError(String(nextError));
19
+ setLoading(false);
20
+ });
21
+ };
22
+ useEffect(() => {
23
+ runCheck();
24
+ }, [dryRun, refreshContext]);
25
+ return createElement(Doctor, { loading, result, error, onRerun: runCheck });
26
+ }
27
+ //# sourceMappingURL=doctor.js.map
@@ -15,7 +15,7 @@ export interface RendererDeps {
15
15
  error?: (message: string) => void;
16
16
  setExitCode?: (code: number) => void;
17
17
  }
18
- export declare function handleDoctor(_cli: CLI, ctx: RendererCtx, deps?: RendererDeps): void;
18
+ export declare function handleDoctor(cli: CLI, ctx: RendererCtx, deps?: RendererDeps): void;
19
19
  export declare function handleAnalyze(cli: CLI, ctx: RendererCtx): void;
20
20
  export declare function handleLlmsTxt(cli: CLI, ctx: RendererCtx): void;
21
21
  export declare function handlePlugin(cli: CLI, ctx: RendererCtx): void;
@@ -13,11 +13,11 @@ import { resolvePlatformSupport } from "../../lib/platform-support.js";
13
13
  import AnalyzeUI from "../../ui/AnalyzeUI.js";
14
14
  import App from "../../ui/App.js";
15
15
  import { CIProvider as CIContextProvider } from "../../ui/CIContext.js";
16
- import Doctor from "../../ui/Doctor.js";
17
16
  import LlmsTxt from "../../ui/LlmsTxt.js";
18
17
  import Plugin from "../../ui/Plugin.js";
19
18
  import { VALID_CI, VALID_MEMORY, VALID_STACKS } from "../validators.js";
20
- export function handleDoctor(_cli, ctx, deps = {}) {
19
+ import DoctorController from "./doctor.js";
20
+ export function handleDoctor(cli, ctx, deps = {}) {
21
21
  const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
22
22
  if (platformSupport) {
23
23
  (deps.error ?? console.error)(`${platformSupport.refusalCode}: ${platformSupport.guidance}`);
@@ -28,7 +28,7 @@ export function handleDoctor(_cli, ctx, deps = {}) {
28
28
  return;
29
29
  }
30
30
  (deps.render ?? render)(React.createElement(CIContextProvider, { isCI: ctx.isCI },
31
- React.createElement(Doctor, null)), { stdin: ctx.inkStdin });
31
+ React.createElement(DoctorController, { dryRun: cli.flags.dryRun === true, refreshContext: cli.flags.refreshContext === true })), { stdin: ctx.inkStdin });
32
32
  }
33
33
  export function handleAnalyze(cli, ctx) {
34
34
  render(React.createElement(CIContextProvider, { isCI: ctx.isCI },
@@ -8,7 +8,7 @@
8
8
  * Help banner shown by meow when `--help` is passed or invalid args are supplied.
9
9
  * Multi-line template literal — preserve exact formatting (whitespace is significant).
10
10
  */
11
- export declare const HELP_TEXT = "\n Usage\n $ javi-forge [command] [options]\n\n Commands\n init Bootstrap a new project (default)\n ci Run CI simulation (lint + compile + test + security + ghagga)\n ci validate Validate .javi-forge/ci.yaml without running anything\n ci init Install git hooks that call javi-forge ci\n tdd init Enable the TDD pre-commit section + install managed hooks\n tdd pipeline Enable the TDD pre-push section (--mode strict|warn)\n hooks run Run a git hook's composed sections (pre-commit | pre-push)\n analyze Run repoforge skills analysis\n doctor Show health report\n workflow show Render a workflow graph as ASCII (--template <name> or file path)\n workflow validate Validate project state against a workflow graph\n workflow list List available workflows and built-in templates\n plugin add Install a plugin from GitHub (org/repo)\n plugin remove Remove an installed plugin\n plugin list List installed plugins\n plugin search Search the plugin registry\n plugin validate Validate a local plugin directory\n plugin sync Auto-detect and wire installed plugins\n plugin export Export plugin to Agent Skills spec format (skills.json)\n plugin export --codex: Export plugin to Codex-compatible TOML subagent files\n plugin export-skills Generate aggregated skills.json from all installed plugins\n plugin export-skills global Generate global skills.json from all globally installed plugins\n plugin import Import an Agent Skills spec package as a javi-forge plugin\n skills doctor Show skills health report (add --deep for conflict detection)\n skills budget Show token cost of loaded skills (add -b N for custom budget)\n skills score Score a skill on quality dimensions (completeness, clarity, testability, token-efficiency)\n skills benchmark Benchmark a skill with structural quality checks\n skills auto Auto-detect project stack and suggest/install matching AI skills\n skills auto-install Alias for skills auto\n skill publish Package a skill directory for marketplace distribution (generates plugin.json)\n security baseline Create security baseline from current audit findings\n security check Check for regressions against baseline (exits non-zero if found)\n security update Re-snapshot baseline (acknowledge current vulns)\n security allowlist Add all current findings to the allowlist (suppress in future checks)\n llms-txt Generate AI-friendly llms.txt for current project\n\n Options\n --dry-run Preview changes without writing files\n --stack Project stack (node, python, go, rust, java-gradle, java-maven, elixir)\n --ci CI provider (github, gitlab, woodpecker)\n --memory Memory module (engram, obsidian-brain, memory-simple, none)\n --project-name Project name (skips name prompt)\n --ghagga Enable GHAGGA review system\n --mock Enable mock-first mode (no real API keys needed)\n --local-ai Include local AI dev stack (Ollama + Docker Compose)\n --batch Non-interactive mode (auto-proceed, no keyboard input)\n --deep Enable deep analysis (conflict + duplicate detection)\n --budget, -b Token budget limit for skills (default: 8000)\n --skills-dir Custom skills directory path\n --author Author name for skill publish\n --repo Repository URL for skill publish\n --version Show version\n --help Show this help\n\n CI options (javi-forge ci)\n --quick Lint + compile only (fast, for pre-commit)\n --shell Open interactive shell in CI container\n --detect Show detected stack and exit\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only \u2014\n insufficient for hybrid repos; use --config instead)\n --no-docker Run commands natively (no Docker)\n --no-ci-ghagga Skip GHAGGA review\n --no-security Skip Semgrep security scan\n --timeout N Per-step timeout in seconds (default: 600)\n\n CI hooks (javi-forge ci init)\n Install git hooks that call javi-forge ci.\n No files copied \u2014 hooks reference the global CLI.\n Existing hooks javi-forge did not write are refused, never clobbered.\n --force Overwrite a foreign or locally modified hook. The previous\n content is copied to a .bak sibling first; if that backup\n cannot be written, the hook is left untouched. Symlinked\n hook paths are refused even with --force.\n\n SkillGuard install gate (plugin add / plugin import / skills auto)\n Every install is scanned before anything is written. Refusals are\n fail-closed and name the offending files:\n - SKILL.md files that block (critical threats) are refused \u2014 always.\n - Unscannable files (binary, oversized, unreadable) are refused unless\n --force is given.\n - Symlinks anywhere in the tree and SKILL.md files outside the declared\n set are manifest-integrity refusals \u2014 they are refused even with --force.\n - Empty or missing skills.json `skills` array on import is refused.\n A refused install/auto-install exits non-zero (exit 1) so scripts and CI\n can tell a refusal apart from success; clean installs \u2014 including\n --force-lifted unscannable ones \u2014 exit 0.\n\n Examples\n $ javi-forge\n $ javi-forge init --dry-run\n $ javi-forge init --stack node --ci github\n $ javi-forge ci\n $ javi-forge ci init\n $ javi-forge ci init --force\n $ javi-forge plugin add org/repo\n $ javi-forge plugin add org/repo --force\n $ javi-forge tdd init\n $ javi-forge ci --quick\n $ javi-forge ci --no-ci-ghagga --no-security\n $ javi-forge ci --no-docker\n $ javi-forge ci --shell\n $ javi-forge ci --config .javi-forge/ci.yaml\n $ javi-forge ci validate\n $ javi-forge ci --help\n $ javi-forge analyze\n $ javi-forge doctor\n $ javi-forge plugin add mapbox/agent-skills\n $ javi-forge plugin list\n";
11
+ export declare const HELP_TEXT = "\n Usage\n $ javi-forge [command] [options]\n\n Commands\n init Bootstrap a new project (default)\n ci Run CI simulation (lint + compile + test + security + ghagga)\n ci validate Validate .javi-forge/ci.yaml without running anything\n ci init Install git hooks that call javi-forge ci\n tdd init Enable the TDD pre-commit section + install managed hooks\n tdd pipeline Enable the TDD pre-push section (--mode strict|warn)\n hooks run Run a git hook's composed sections (pre-commit | pre-push)\n analyze Run repoforge skills analysis\n doctor Show health report\n workflow show Render a workflow graph as ASCII (--template <name> or file path)\n workflow validate Validate project state against a workflow graph\n workflow list List available workflows and built-in templates\n plugin add Install a plugin from GitHub (org/repo)\n plugin remove Remove an installed plugin\n plugin list List installed plugins\n plugin search Search the plugin registry\n plugin validate Validate a local plugin directory\n plugin sync Auto-detect and wire installed plugins\n plugin export Export plugin to Agent Skills spec format (skills.json)\n plugin export --codex: Export plugin to Codex-compatible TOML subagent files\n plugin export-skills Generate aggregated skills.json from all installed plugins\n plugin export-skills global Generate global skills.json from all globally installed plugins\n plugin import Import an Agent Skills spec package as a javi-forge plugin\n skills doctor Show skills health report (add --deep for conflict detection)\n skills budget Show token cost of loaded skills (add -b N for custom budget)\n skills score Score a skill on quality dimensions (completeness, clarity, testability, token-efficiency)\n skills benchmark Benchmark a skill with structural quality checks\n skills auto Auto-detect project stack and suggest/install matching AI skills\n skills auto-install Alias for skills auto\n skill publish Package a skill directory for marketplace distribution (generates plugin.json)\n security baseline Create security baseline from current audit findings\n security check Check for regressions against baseline (exits non-zero if found)\n security update Re-snapshot baseline (acknowledge current vulns)\n security allowlist Add all current findings to the allowlist (suppress in future checks)\n llms-txt Generate AI-friendly llms.txt for current project\n\n Options\n --dry-run Preview changes without writing files\n --stack Project stack (node, python, go, rust, java-gradle, java-maven, elixir)\n --ci CI provider (github, gitlab, woodpecker)\n --memory Memory module (engram, obsidian-brain, memory-simple, none)\n --project-name Project name (skips name prompt)\n --ghagga Enable GHAGGA review system\n --mock Enable mock-first mode (no real API keys needed)\n --local-ai Include local AI dev stack (Ollama + Docker Compose)\n --batch Non-interactive mode (auto-proceed, no keyboard input)\n --refresh-context Refresh .context/ during doctor (writes INDEX.md, summary.md, manifest timestamp)\n --deep Enable deep analysis (conflict + duplicate detection)\n --budget, -b Token budget limit for skills (default: 8000)\n --skills-dir Custom skills directory path\n --author Author name for skill publish\n --repo Repository URL for skill publish\n --version Show version\n --help Show this help\n\n CI options (javi-forge ci)\n --quick Lint + compile only (fast, for pre-commit)\n --shell Open interactive shell in CI container\n --detect Show detected stack and exit\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only \u2014\n insufficient for hybrid repos; use --config instead)\n --no-docker Run commands natively (no Docker)\n --no-ci-ghagga Skip GHAGGA review\n --no-security Skip Semgrep security scan\n --timeout N Per-step timeout in seconds (default: 600)\n\n CI hooks (javi-forge ci init)\n Install git hooks that call javi-forge ci.\n No files copied \u2014 hooks reference the global CLI.\n Existing hooks javi-forge did not write are refused, never clobbered.\n --force Overwrite a foreign or locally modified hook. The previous\n content is copied to a .bak sibling first; if that backup\n cannot be written, the hook is left untouched. Symlinked\n hook paths are refused even with --force.\n\n SkillGuard install gate (plugin add / plugin import / skills auto)\n Every install is scanned before anything is written. Refusals are\n fail-closed and name the offending files:\n - SKILL.md files that block (critical threats) are refused \u2014 always.\n - Unscannable files (binary, oversized, unreadable) are refused unless\n --force is given.\n - Symlinks anywhere in the tree and SKILL.md files outside the declared\n set are manifest-integrity refusals \u2014 they are refused even with --force.\n - Empty or missing skills.json `skills` array on import is refused.\n A refused install/auto-install exits non-zero (exit 1) so scripts and CI\n can tell a refusal apart from success; clean installs \u2014 including\n --force-lifted unscannable ones \u2014 exit 0.\n\n Examples\n $ javi-forge\n $ javi-forge init --dry-run\n $ javi-forge init --stack node --ci github\n $ javi-forge ci\n $ javi-forge ci init\n $ javi-forge ci init --force\n $ javi-forge plugin add org/repo\n $ javi-forge plugin add org/repo --force\n $ javi-forge tdd init\n $ javi-forge ci --quick\n $ javi-forge ci --no-ci-ghagga --no-security\n $ javi-forge ci --no-docker\n $ javi-forge ci --shell\n $ javi-forge ci --config .javi-forge/ci.yaml\n $ javi-forge ci validate\n $ javi-forge ci --help\n $ javi-forge analyze\n $ javi-forge doctor\n $ javi-forge plugin add mapbox/agent-skills\n $ javi-forge plugin list\n";
12
12
  /**
13
13
  * Per-command help for `ci`, shown by `javi-forge ci --help` (or when `ci` is
14
14
  * given an unknown subcommand). Kept consistent with the global HELP_TEXT
@@ -30,6 +30,10 @@ export declare const FLAGS_SCHEMA: {
30
30
  readonly type: "boolean";
31
31
  readonly default: false;
32
32
  };
33
+ readonly refreshContext: {
34
+ readonly type: "boolean";
35
+ readonly default: false;
36
+ };
33
37
  readonly stack: {
34
38
  readonly type: "string";
35
39
  readonly default: "";
package/dist/cli/help.js CHANGED
@@ -59,6 +59,7 @@ export const HELP_TEXT = `
59
59
  --mock Enable mock-first mode (no real API keys needed)
60
60
  --local-ai Include local AI dev stack (Ollama + Docker Compose)
61
61
  --batch Non-interactive mode (auto-proceed, no keyboard input)
62
+ --refresh-context Refresh .context/ during doctor (writes INDEX.md, summary.md, manifest timestamp)
62
63
  --deep Enable deep analysis (conflict + duplicate detection)
63
64
  --budget, -b Token budget limit for skills (default: 8000)
64
65
  --skills-dir Custom skills directory path
@@ -214,6 +215,7 @@ export const FLAGS_SCHEMA = {
214
215
  // `ci --help` can show ci-specific usage instead of the global banner).
215
216
  help: { type: "boolean", shortFlag: "h", default: false },
216
217
  dryRun: { type: "boolean", default: false },
218
+ refreshContext: { type: "boolean", default: false },
217
219
  stack: { type: "string", default: "" },
218
220
  ci: { type: "string", default: "" },
219
221
  memory: { type: "string", default: "" },
package/dist/cli/main.js CHANGED
@@ -10,11 +10,21 @@ import { handleTdd } from "./dispatch/tdd.js";
10
10
  import { handleWorkflow } from "./dispatch/workflow.js";
11
11
  import { FLAGS_SCHEMA, HELP_TEXT } from "./help.js";
12
12
  import { createInkStdin, detectCI, setupUpdateNotifier } from "./runtime.js";
13
+ const KNOWN_COMMANDS = new Set([
14
+ "init",
15
+ "tdd",
16
+ "ci",
17
+ "hooks",
18
+ "doctor",
19
+ "analyze",
20
+ "workflow",
21
+ "llms-txt",
22
+ "plugin",
23
+ "skills",
24
+ "skill",
25
+ "security",
26
+ ]);
13
27
  export async function runCli() {
14
- // Check for updates in background (non-blocking, cached 24h)
15
- const _require = createRequire(import.meta.url);
16
- const pkg = _require("../../package.json");
17
- setupUpdateNotifier(pkg);
18
28
  const cli = meow(HELP_TEXT, {
19
29
  importMeta: import.meta,
20
30
  flags: FLAGS_SCHEMA,
@@ -23,12 +33,23 @@ export async function runCli() {
23
33
  autoHelp: false,
24
34
  });
25
35
  const subcommand = cli.input[0] ?? "init";
36
+ if (!KNOWN_COMMANDS.has(subcommand)) {
37
+ console.error(`Unknown command "${subcommand}". Run javi-forge --help for usage.`);
38
+ process.exit(1);
39
+ }
26
40
  // Global --help: every command except `ci` and `hooks` shows the global banner
27
41
  // here. Those two own their per-command help inside their handlers.
28
42
  if (cli.flags.help && subcommand !== "ci" && subcommand !== "hooks") {
29
43
  console.log(HELP_TEXT);
30
44
  process.exit(0);
31
45
  }
46
+ // Check for updates in background (non-blocking, cached 24h). Diagnostics and
47
+ // dry-runs must not create notifier cache/state before doing their actual work.
48
+ if (subcommand !== "doctor" && !cli.flags.dryRun) {
49
+ const _require = createRequire(import.meta.url);
50
+ const pkg = _require("../../package.json");
51
+ setupUpdateNotifier(pkg);
52
+ }
32
53
  const isCI = detectCI(cli.flags);
33
54
  const inkStdin = createInkStdin();
34
55
  switch (subcommand) {
@@ -76,7 +97,7 @@ export async function runCli() {
76
97
  await handleSecurity(cli);
77
98
  break;
78
99
  }
79
- default: {
100
+ case "init": {
80
101
  handleInitDefault(cli, { inkStdin, isCI });
81
102
  break;
82
103
  }
@@ -1,12 +1,16 @@
1
1
  import fs from "fs-extra";
2
2
  import { detectStack } from "../lib/common.js";
3
- import { refreshContextDir } from "../lib/context.js";
3
+ import { detectDependenciesDetailed, refreshContextDir } from "../lib/context.js";
4
4
  import { execFileAsync } from "../lib/exec.js";
5
5
  import { listInstalledPlugins } from "../lib/plugin.js";
6
6
  import type { DoctorResult } from "../types/index.js";
7
7
  export type CheckStatus = "ok" | "fail" | "skip";
8
8
  type DoctorFilesystem = Pick<typeof fs, "pathExists" | "readJson" | "readdir">;
9
- export interface DoctorDeps {
9
+ export interface DoctorOptions {
10
+ dryRun?: boolean;
11
+ refreshContext?: boolean;
12
+ }
13
+ export interface DoctorDeps extends DoctorOptions {
10
14
  platform?: string;
11
15
  cwd?: () => string;
12
16
  filesystem?: DoctorFilesystem;
@@ -14,6 +18,7 @@ export interface DoctorDeps {
14
18
  stackDetector?: typeof detectStack;
15
19
  pluginLister?: typeof listInstalledPlugins;
16
20
  contextRefresher?: typeof refreshContextDir;
21
+ dependencyDetector?: typeof detectDependenciesDetailed;
17
22
  }
18
23
  interface RemoteInfo {
19
24
  host: "github" | "gitlab" | "other";
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import fs from "fs-extra";
3
3
  import { FORGE_ROOT, MODULES_DIR, PLUGINS_DIR, TEMPLATES_DIR, } from "../constants.js";
4
4
  import { detectStack } from "../lib/common.js";
5
- import { refreshContextDir } from "../lib/context.js";
5
+ import { detectDependenciesDetailed, refreshContextDir, } from "../lib/context.js";
6
6
  import { execFileAsync } from "../lib/exec.js";
7
7
  import { resolvePlatformSupport } from "../lib/platform-support.js";
8
8
  import { listInstalledPlugins } from "../lib/plugin.js";
@@ -162,6 +162,7 @@ export async function runDoctor(projectDir, deps = {}) {
162
162
  const stackDetector = deps.stackDetector ?? detectStack;
163
163
  const pluginLister = deps.pluginLister ?? listInstalledPlugins;
164
164
  const contextRefresher = deps.contextRefresher ?? refreshContextDir;
165
+ const dependencyDetector = deps.dependencyDetector ?? detectDependenciesDetailed;
165
166
  const sections = [];
166
167
  // ── 1. System Tools ────────────────────────────────────────────────────────
167
168
  const toolChecks = [];
@@ -328,19 +329,37 @@ export async function runDoctor(projectDir, deps = {}) {
328
329
  });
329
330
  }
330
331
  sections.push({ title: "Plugins", checks: pluginChecks });
331
- // ── 7. Context Directory Refresh ───────────────────────────────────────────
332
+ // ── 7. Context Directory (refresh is explicit) ──────────────────────────────
332
333
  const contextChecks = [];
333
334
  try {
334
- const result = await contextRefresher(cwd);
335
- if (result) {
335
+ if (deps.refreshContext && !deps.dryRun) {
336
+ const result = await contextRefresher(cwd);
336
337
  contextChecks.push({
337
338
  label: ".context/ refresh",
338
- status: "ok",
339
- detail: "INDEX.md + summary.md updated",
339
+ status: result ? "ok" : "skip",
340
+ detail: result
341
+ ? "INDEX.md + summary.md updated"
342
+ : "no .context/ or no manifest found",
340
343
  });
341
- // A manifest that could not be read is not "no dependencies" — surface
342
- // each warning so an unreadable/oversized/invalid manifest is visible.
343
- for (const warning of result.warnings) {
344
+ for (const warning of result?.warnings ?? []) {
345
+ contextChecks.push({
346
+ label: "dependency manifest",
347
+ status: "fail",
348
+ detail: warning,
349
+ });
350
+ }
351
+ }
352
+ else if (manifest &&
353
+ (await filesystem.pathExists(path.join(cwd, ".context")))) {
354
+ contextChecks.push({
355
+ label: ".context/",
356
+ status: deps.refreshContext ? "skip" : "ok",
357
+ detail: deps.refreshContext
358
+ ? "dry-run: would refresh INDEX.md + summary.md and manifest timestamp"
359
+ : "present; not refreshed (use --refresh-context to update)",
360
+ });
361
+ const { warnings } = await dependencyDetector(cwd, manifest.stack);
362
+ for (const warning of warnings) {
344
363
  contextChecks.push({
345
364
  label: "dependency manifest",
346
365
  status: "fail",
@@ -350,17 +369,17 @@ export async function runDoctor(projectDir, deps = {}) {
350
369
  }
351
370
  else {
352
371
  contextChecks.push({
353
- label: ".context/ refresh",
372
+ label: ".context/",
354
373
  status: "skip",
355
374
  detail: "no .context/ or no manifest found",
356
375
  });
357
376
  }
358
377
  }
359
- catch (e) {
378
+ catch (error) {
360
379
  contextChecks.push({
361
- label: ".context/ refresh",
380
+ label: ".context/",
362
381
  status: "fail",
363
- detail: `refresh failed: ${String(e)}`,
382
+ detail: `context check failed: ${error instanceof Error ? error.message : String(error)}`,
364
383
  });
365
384
  }
366
385
  sections.push({ title: "Context Directory", checks: contextChecks });
@@ -17,7 +17,9 @@ export declare function runPluginList(onStep: StepCallback): Promise<void>;
17
17
  /**
18
18
  * Search the remote plugin registry.
19
19
  */
20
- export declare function runPluginSearch(query: string | undefined, onStep: StepCallback): Promise<void>;
20
+ export declare function runPluginSearch(query: string | undefined, onStep: StepCallback, options?: {
21
+ signal?: AbortSignal;
22
+ }): Promise<void>;
21
23
  /**
22
24
  * Validate a local plugin directory.
23
25
  */
@@ -4,6 +4,22 @@ import { installPlugin, listInstalledPlugins, removePlugin, searchRegistry, sync
4
4
  function report(onStep, id, label, status, detail) {
5
5
  onStep({ id, label, status, detail });
6
6
  }
7
+ function successDetail(action, dryRun, result) {
8
+ const verb = action === "install" ? "install" : "import";
9
+ const past = action === "install" ? "installed" : "imported";
10
+ const lines = [
11
+ dryRun ? `dry-run: would ${verb} ${result.name}` : `${past} ${result.name}`,
12
+ ];
13
+ if (result.warning)
14
+ lines.push(`warning: ${result.warning}`);
15
+ if (result.recoveryPaths?.length) {
16
+ lines.push("manual recovery paths:");
17
+ for (const recoveryPath of result.recoveryPaths) {
18
+ lines.push(` - ${recoveryPath}`);
19
+ }
20
+ }
21
+ return lines.join("\n");
22
+ }
7
23
  /**
8
24
  * Add (install) a plugin from a GitHub source.
9
25
  */
@@ -12,9 +28,7 @@ export async function runPluginAdd(source, dryRun, onStep, options = {}) {
12
28
  report(onStep, stepId, `Install plugin: ${source}`, "running");
13
29
  const result = await installPlugin(source, { dryRun, force: options.force });
14
30
  if (result.success) {
15
- report(onStep, stepId, `Install plugin: ${source}`, "done", dryRun
16
- ? `dry-run: would install ${result.name}`
17
- : `installed ${result.name}`);
31
+ report(onStep, stepId, `Install plugin: ${source}`, "done", successDetail("install", dryRun, result));
18
32
  }
19
33
  else {
20
34
  report(onStep, stepId, `Install plugin: ${source}`, "error", result.error);
@@ -58,10 +72,10 @@ export async function runPluginList(onStep) {
58
72
  /**
59
73
  * Search the remote plugin registry.
60
74
  */
61
- export async function runPluginSearch(query, onStep) {
75
+ export async function runPluginSearch(query, onStep, options = {}) {
62
76
  const stepId = "plugin-search";
63
77
  report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "running");
64
- const results = await searchRegistry(query);
78
+ const results = await searchRegistry(query, options);
65
79
  if (results.status === "cancelled") {
66
80
  report(onStep, stepId, `Search plugins${query ? `: ${query}` : ""}`, "error", "registry search cancelled");
67
81
  }
@@ -163,9 +177,7 @@ export async function runPluginImport(sourceDir, dryRun, onStep, options = {}) {
163
177
  force: options.force,
164
178
  });
165
179
  if (result.success) {
166
- report(onStep, stepId, `Import agent-skills package: ${sourceDir}`, "done", dryRun
167
- ? `dry-run: would import ${result.name}`
168
- : `imported ${result.name}`);
180
+ report(onStep, stepId, `Import agent-skills package: ${sourceDir}`, "done", successDetail("import", dryRun, result));
169
181
  }
170
182
  else {
171
183
  report(onStep, stepId, `Import agent-skills package: ${sourceDir}`, "error", result.error);
@@ -177,16 +177,19 @@ export function makeFakeSecureFs() {
177
177
  },
178
178
  async renameInDir(dir, from, to) {
179
179
  if (fake.faults.renameRefuse?.(to))
180
- return unsafe(`rename ${to}`);
180
+ return { ...unsafe(`rename ${to}`), mutation: "not-applied" };
181
181
  const fromP = path.join(dir.path, from);
182
182
  const toP = path.join(dir.path, to);
183
183
  const file = files.get(fromP);
184
184
  if (!file)
185
- return unsafe(`rename enoent ${fromP}`);
185
+ return {
186
+ ...unsafe(`rename enoent ${fromP}`),
187
+ mutation: "not-applied",
188
+ };
186
189
  files.set(toP, file);
187
190
  files.delete(fromP);
188
191
  inos.delete(toP); // fresh identity for the renamed-in target
189
- return ok();
192
+ return { ok: true, mutation: "applied" };
190
193
  },
191
194
  async unlinkIfIdentity(dir, name, _held) {
192
195
  const full = path.join(dir.path, name);