guideline-manage 0.16.0 → 0.17.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.
@@ -7,6 +7,7 @@ import { readCategories } from './categories.js';
7
7
  import { asTree, readText } from './tree.js';
8
8
  import { t } from './i18n.js';
9
9
  import { marker } from './markers.js';
10
+ import { stackNameProblem } from './stack-name.js';
10
11
 
11
12
  // export 함수의 src 는 문서 저장소 루트의 디스크 경로나 트리(tree.js)다. 서버는 DB 트리를 넘긴다.
12
13
 
@@ -265,6 +266,7 @@ export function fileCategories(categories, rel) {
265
266
  export function warnings(src) {
266
267
  const tree = asTree(src);
267
268
  const rules = stacks(tree).flatMap(s => [
269
+ ...(stackNameProblem(s) ? [{ path: `rules/${s}/`, message: stackNameProblem(s) }] : []),
268
270
  ...stackTiers(s, tree).filter(f => f.warning).map(f => ({ path: f.path, message: f.warning })),
269
271
  ...stackOverrides(s, tree).warnings,
270
272
  ]);
package/cli/src/cli.js CHANGED
@@ -20,7 +20,7 @@ const ALLOWED_OPTIONS = {
20
20
  status: ['project', 'server', 'source', 'json'],
21
21
  login: ['server', 'token-stdin'],
22
22
  logout: ['server'],
23
- propose: ['project', 'server', 'source', 'title', 'message', 'only', 'from', 'dry-run', 'json'],
23
+ propose: ['project', 'server', 'source', 'title', 'message', 'only', 'from', 'dry-run', 'apply', 'json'],
24
24
  source: ['server', 'source'],
25
25
  locale: ['json'],
26
26
  help: [],
@@ -107,7 +107,7 @@ export async function main(argv) {
107
107
  return propose({
108
108
  project: values.project, server: values.server, source: values.source, title: values.title, message: values.message,
109
109
  only: values.only?.split(',').map(s => s.trim()).filter(Boolean), from: values.from,
110
- dryRun: values['dry-run'], json: values.json, cwd,
110
+ dryRun: values['dry-run'], apply: values.apply, json: values.json, cwd,
111
111
  });
112
112
  }
113
113
 
@@ -14,6 +14,36 @@ async function readAll(stream) {
14
14
  return s;
15
15
  }
16
16
 
17
+ /**
18
+ * 터미널에서 토큰 한 줄을 읽는다. 입력한 글자는 화면에 보이지 않는다.
19
+ * 파이프가 아닌 터미널은 EOF 가 오지 않아서, readAll 로는 아무 안내 없이 멈춘 것처럼 보인다.
20
+ * @returns {Promise<string | null>} Ctrl-C 면 null
21
+ */
22
+ function readHiddenLine(stdin, write) {
23
+ return new Promise(resolve => {
24
+ let s = '';
25
+ const done = value => {
26
+ stdin.removeListener('data', onData);
27
+ stdin.setRawMode?.(false);
28
+ stdin.pause();
29
+ write('\n');
30
+ resolve(value);
31
+ };
32
+ const onData = chunk => {
33
+ for (const ch of String(chunk)) {
34
+ if (ch === '\r' || ch === '\n' || ch === '\u0004') return done(s);
35
+ if (ch === '\u0003') return done(null);
36
+ if (ch === '\u007f' || ch === '\b') s = s.slice(0, -1);
37
+ else s += ch;
38
+ }
39
+ };
40
+ stdin.setRawMode?.(true);
41
+ stdin.setEncoding?.('utf8');
42
+ stdin.on('data', onData);
43
+ stdin.resume();
44
+ });
45
+ }
46
+
17
47
  /** 127.0.0.1 의 임시 포트에서 /cb 한 번을 기다린다. */
18
48
  async function callbackServer() {
19
49
  let resolve;
@@ -45,6 +75,7 @@ async function callbackServer() {
45
75
  export async function login({
46
76
  server, tokenStdin = false, env = process.env, out = console.log, err = console.error, open = openBrowser, fetchImpl = fetch,
47
77
  home, stdin = process.stdin, hostname = os.hostname(), timeoutMs = LOGIN_TIMEOUT_MS,
78
+ prompt = s => process.stderr.write(s),
48
79
  }) {
49
80
  const url = serverOf({ flag: server, env });
50
81
  if (!url) {
@@ -52,7 +83,15 @@ export async function login({
52
83
  return 2;
53
84
  }
54
85
  if (tokenStdin) {
55
- const token = (await readAll(stdin)).trim();
86
+ let token;
87
+ if (stdin.isTTY) {
88
+ prompt(t('login.pasteToken'));
89
+ token = await readHiddenLine(stdin, prompt);
90
+ if (token === null) return 130;
91
+ } else {
92
+ token = await readAll(stdin);
93
+ }
94
+ token = token.trim();
56
95
  if (!token) {
57
96
  err(t('login.noStdinToken'));
58
97
  return 2;
@@ -20,7 +20,7 @@ const textOr = buf => (buf === null ? '' : buf.includes(0) ? null : buf.toString
20
20
  * 변경 묶음을 보이거나(dry-run) 제안으로 보낸다. propose 와 propose --from 이 같이 쓴다.
21
21
  * @returns {Promise<number>}
22
22
  */
23
- export async function sendProposal({ client, server, title, message, project, changes, todos, dryRun, json, out, err, baseTree }) {
23
+ export async function sendProposal({ client, server, title, message, project, changes, todos, dryRun, apply = false, json, out, err, baseTree }) {
24
24
  if (changes.length === 0) {
25
25
  if (json) out(JSON.stringify({ changes: [], todos }, null, 2));
26
26
  else {
@@ -47,18 +47,20 @@ export async function sendProposal({ client, server, title, message, project, ch
47
47
  }
48
48
  let r;
49
49
  try {
50
- r = await client.createProposal({ title, description: message ?? '', project, changes: changes.map(toApiChange) });
50
+ r = await client.createProposal({ title, description: message ?? '', project, changes: changes.map(toApiChange), ...(apply ? { apply: true } : {}) });
51
51
  } catch (e) {
52
52
  if (e instanceof ServerError && e.status === 403) {
53
- err(PROPOSE_DENIED(server));
53
+ err(apply ? t('propose.applyDenied', { server }) : PROPOSE_DENIED(server));
54
54
  return 1;
55
55
  }
56
56
  err(explain(e, server));
57
57
  return 1;
58
58
  }
59
- if (json) out(JSON.stringify({ id: r.id, url: r.url, changes: listed, todos }, null, 2));
59
+ if (json) out(JSON.stringify({ id: r.id, url: r.url, status: r.status, revision: r.revision ?? null, changes: listed, todos }, null, 2));
60
60
  else {
61
- out(t('propose.sent', { id: r.id, url: r.url }));
61
+ if (r.status !== 'accepted') out(t('propose.sent', { id: r.id, url: r.url }));
62
+ else if (r.revision) out(t('propose.applied', { rev: r.revision, url: r.url }));
63
+ else out(t('propose.appliedNothing'));
62
64
  for (const c of changes) out(` ${KIND(c)} ${c.path}`);
63
65
  printTodos(todos, out);
64
66
  }
@@ -70,7 +72,7 @@ export async function sendProposal({ client, server, title, message, project, ch
70
72
  * @returns {Promise<number>}
71
73
  */
72
74
  export async function propose({
73
- project, server, source, from, title, message, only, dryRun = false, json = false,
75
+ project, server, source, from, title, message, only, dryRun = false, apply = false, json = false,
74
76
  env = process.env, out = console.log, err = console.error, fetchImpl = fetch, home, cwd = process.cwd(),
75
77
  }) {
76
78
  if (from !== undefined) {
@@ -101,7 +103,7 @@ export async function propose({
101
103
  return 1;
102
104
  }
103
105
  return sendProposal({
104
- client, server: url, title, message, project: null, changes: folderChanges({ base, folder }), todos: [], dryRun, json, out, err, baseTree: base,
106
+ client, server: url, title, message, project: null, changes: folderChanges({ base, folder }), todos: [], dryRun, apply, json, out, err, baseTree: base,
105
107
  });
106
108
  }
107
109
  const dir = project !== undefined ? expandPath(project) : cwd;
@@ -139,7 +141,7 @@ export async function propose({
139
141
  const prefix = many ? line => out(`${name}: ${line}`) : out;
140
142
  const c = await sendProposal({
141
143
  client, server: url, title: title ?? t('propose.defaultTitle', { project: projectName }), message, project: projectName,
142
- changes: list, todos: many ? [] : shown, dryRun, json, out: prefix, err, baseTree: base,
144
+ changes: list, todos: many ? [] : shown, dryRun, apply, json, out: prefix, err, baseTree: base,
143
145
  });
144
146
  if (c !== 0) code = c;
145
147
  }
@@ -0,0 +1,21 @@
1
+ import { t } from './i18n.js';
2
+
3
+ // 도메인 스택: `<도구>__<도메인>`. 같은 도구라도 돌아가는 서버(회사·인스턴스)가 다르면 규칙이 달라 스택을 나눈다.
4
+ const SEP = '__';
5
+ const DOMAIN = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/;
6
+
7
+ /**
8
+ * @param {string} name 스택 이름
9
+ * @returns {{ tool: string, domain: string | null }}
10
+ */
11
+ export function splitStackName(name) {
12
+ const i = name.indexOf(SEP);
13
+ return i === -1 ? { tool: name, domain: null } : { tool: name.slice(0, i), domain: name.slice(i + SEP.length) };
14
+ }
15
+
16
+ /** `__` 가 있는데 도메인 모양이 아니면 경고 문구. 없거나 맞으면 null. */
17
+ export function stackNameProblem(name) {
18
+ const { domain } = splitStackName(name);
19
+ if (domain === null || DOMAIN.test(domain)) return null;
20
+ return t('catalog.badDomain');
21
+ }
@@ -49,7 +49,7 @@
49
49
  "diff.proposal": "proposal",
50
50
  "diff.source": "source",
51
51
  "diff.tooBig": "omitted (too large)",
52
- "help.usage": "Usage: guideline <command> [options]\n\nIn a project (guideline server)\n common --source <name> a source (sources in guideline.json). For pick, an unknown name together with --server adds a new source\n pick choose guidelines to export in a web form; submitting applies them to this project\n --project <path> target project (default: current directory)\n --new <name|path> create a new project (a bare name goes under GUIDELINE_PROJECTS_DIR)\n --server <URL> guideline server (default: guideline.json → GUIDELINE_SERVER)\n --no-browser print the address instead of opening a browser\n --json print the result (written files, to-dos) as JSON\n update re-apply the chosen items from the latest server revision\n --project <path> --server <URL>\n --force also overwrite files changed in the project\n --json print the result as JSON\n status show whether the applied guidelines are current and what the project changed\n --project <path> --server <URL> --json\n login log in to a guideline server and store a read token\n --server <URL>\n --token-stdin store a token from standard input instead of using a browser\n logout delete the stored token and revoke it on the server\n --server <URL>\n propose turn edited copies back into source form and propose them to the server (they become source when an admin accepts)\n --project <path> --server <URL>\n --title <title> proposal title (default: guidelines edited in <directory>)\n --message <text> proposal description\n --only <item,…> only these items (e.g. rules/base/01-git.md,skills/s)\n --from <folder> compare a folder from guideline source against the revision it came from (--title required)\n --dry-run show the changes and diffs without sending\n --json print the result as JSON\n source unpack the latest server source into an empty folder; edit it in source layout, then propose --from\n guideline source <folder> --server <URL>\n list list the source and its checks. With a folder, read that folder (a source from guideline source); otherwise the server\n guideline list [folder] --server <URL> --json\n locale language packs: check <folder> compares a pack with English; list shows available languages. The language is GUIDELINE_LANG\n guideline locale check <folder> guideline locale list --json\n help this help\n",
52
+ "help.usage": "Usage: guideline <command> [options]\n\nIn a project (guideline server)\n common --source <name> a source (sources in guideline.json). For pick, an unknown name together with --server adds a new source\n pick choose guidelines to export in a web form; submitting applies them to this project\n --project <path> target project (default: current directory)\n --new <name|path> create a new project (a bare name goes under GUIDELINE_PROJECTS_DIR)\n --server <URL> guideline server (default: guideline.json → GUIDELINE_SERVER)\n --no-browser print the address instead of opening a browser\n --json print the result (written files, to-dos) as JSON\n update re-apply the chosen items from the latest server revision\n --project <path> --server <URL>\n --force also overwrite files changed in the project\n --json print the result as JSON\n status show whether the applied guidelines are current and what the project changed\n --project <path> --server <URL> --json\n login log in to a guideline server and store a read token\n --server <URL>\n --token-stdin store a token from standard input instead of using a browser\n logout delete the stored token and revoke it on the server\n --server <URL>\n propose turn edited copies back into source form and propose them to the server (they become source when an admin accepts)\n --project <path> --server <URL>\n --title <title> proposal title (default: guidelines edited in <directory>)\n --message <text> proposal description\n --only <item,…> only these items (e.g. rules/base/01-git.md,skills/s)\n --from <folder> compare a folder from guideline source against the revision it came from (--title required)\n --dry-run show the changes and diffs without sending\n --apply with an admin token, apply directly instead of proposing\n --json print the result as JSON\n source unpack the latest server source into an empty folder; edit it in source layout, then propose --from\n guideline source <folder> --server <URL>\n list list the source and its checks. With a folder, read that folder (a source from guideline source); otherwise the server\n guideline list [folder] --server <URL> --json\n locale language packs: check <folder> compares a pack with English; list shows available languages. The language is GUIDELINE_LANG\n guideline locale check <folder> guideline locale list --json\n help this help\n",
53
53
  "http.badJson": "The body is not JSON",
54
54
  "http.badUrl": "The address is badly encoded",
55
55
  "http.needJson": "Content-Type must be application/json",
@@ -91,6 +91,7 @@
91
91
  "login.callbackPage": "Login received. You can close this tab.",
92
92
  "login.done": "Logged in: {url} ({name})",
93
93
  "login.loggedOut": "Logged out: {url}",
94
+ "login.pasteToken": "Paste the token and press Enter (it is not shown): ",
94
95
  "login.noStdinToken": "No token on standard input",
95
96
  "login.notLoggedIn": "Not logged in",
96
97
  "login.openUrl": "Log in: {url}",
@@ -274,7 +275,7 @@
274
275
  "todo.skippedAdopted": "{what}: already differed from the source before it was received (it may be an old version) — use --force to match the source, or guideline propose --only {id} if the project edited it",
275
276
  "todo.unsynced": "{item}: could not be applied because an existing copy differs — check the diff in guideline pick and turn on update",
276
277
  "tokens.badName": "Names are 1–64 characters of letters, digits and . _ -",
277
- "tokens.badScope": "The scope is read or propose",
278
+ "tokens.badScope": "The scope must be read, propose or admin",
278
279
  "tokens.nameTaken": "Name already exists: {name}",
279
280
  "toml.badValue": "value cannot be written as TOML: {value}",
280
281
  "toml.duplicate": "[{table}] appears more than once",
@@ -286,7 +287,6 @@
286
287
  "update.unknownSource": "Source {name} is not in guideline.json — sources: {names}",
287
288
  "web.adminToken": "Admin token",
288
289
  "web.all": "All",
289
- "web.allFiles": "All files",
290
290
  "web.backHistory": "Back to history",
291
291
  "web.backHome": "Back to the list",
292
292
  "web.badOrigin": "This request came from another address. Open {origin} and try again",
@@ -294,7 +294,6 @@
294
294
  "web.badToken": "The token is wrong",
295
295
  "web.binary": "Binary file · {n} bytes. To replace it, upload a file with the same name on the directory page.",
296
296
  "web.binaryChange": "binary · {from} → {to}",
297
- "web.brand": "Guideline server",
298
297
  "web.bytes": "{n} bytes",
299
298
  "web.cancel": "Cancel",
300
299
  "web.categories": "Categories",
@@ -364,7 +363,6 @@
364
363
  "web.confirmDeleteDir": "Delete every file under {path}",
365
364
  "web.count": "{n}",
366
365
  "web.delete": "Delete",
367
- "web.directory": "Directory",
368
366
  "web.download": "Download",
369
367
  "web.edit": "Edit",
370
368
  "web.edit.conflict": "Someone saved while you had this open. Below is the difference between the current content (-) and yours (+). Check it and save again.",
@@ -375,17 +373,15 @@
375
373
  "web.edit.textOnly": "Only text files can be edited. Upload binaries on the directory page",
376
374
  "web.edit.title": "Edit: {path}",
377
375
  "web.edit.warnings": "Saving leaves these warnings in the list (saving still works).",
378
- "web.editReadme": "Edit README",
379
376
  "web.empty": "No guidelines yet. Create one with \"New file\", or upload many at once with an admin token through the proposals API (POST /api/v1/proposals, apply: true).",
380
- "web.fileCount": "{n} instruction files",
381
377
  "web.files": "Files",
382
- "web.find.agents": "Find subagents",
383
- "web.find.mcp": "Find MCP servers",
384
- "web.find.skills": "Find skills and files",
385
- "web.find.stacks": "Find stacks and files",
378
+ "web.find.agents": "Find a subagent",
379
+ "web.find.mcp": "Find an MCP server",
380
+ "web.find.skills": "Find a skill",
381
+ "web.find.stacks": "Find a stack",
386
382
  "web.firstTime.after": "",
387
383
  "web.firstTime.before": "New here? Read the",
388
- "web.firstTime.link": "usage guide (llms.txt)",
384
+ "web.firstTime.link": "usage guide",
389
385
  "web.form.agents": "Agents",
390
386
  "web.form.badLink": "The form link is wrong or the form is gone",
391
387
  "web.form.byCategory": "Pick by category",
@@ -409,10 +405,9 @@
409
405
  "web.login": "Log in",
410
406
  "web.loginNeeded": "Login required",
411
407
  "web.logout": "Log out",
412
- "web.manage": "Rename · Delete",
408
+ "web.manage": "Rename · delete this file",
413
409
  "web.message": "Message",
414
410
  "web.methodNotAllowed": "Method not allowed",
415
- "web.nav.export": "Export",
416
411
  "web.nav.history": "History",
417
412
  "web.nav.kinds": "Kinds",
418
413
  "web.nav.proposals": "Proposals",
@@ -421,11 +416,9 @@
421
416
  "web.newFile": "New file",
422
417
  "web.newPath": "New path",
423
418
  "web.noDiff": "No differences",
424
- "web.noDir": "No such directory",
425
419
  "web.noHistory": "This path has no history",
426
420
  "web.noPath": "No such path",
427
421
  "web.noPathGiven": "No path given",
428
- "web.noReadme": "There is no README.md.",
429
422
  "web.noStack": "No such stack",
430
423
  "web.noStacksInCategory": "No stacks in this category.",
431
424
  "web.none": "none",
@@ -515,7 +508,7 @@
515
508
  "web.tokens.confirmRevoke": "Revoke the token {name}",
516
509
  "web.tokens.copyNow": "You cannot see this value again. Copy it now into the agent's environment.",
517
510
  "web.tokens.created": "Created",
518
- "web.tokens.intro": "Tokens that agents use for the API. read can read; propose can read and propose.",
511
+ "web.tokens.intro": "Tokens agents use for the API. read can read, propose can also propose, and admin can also accept proposals and apply changes directly.",
519
512
  "web.tokens.issueHeading": "Issue",
520
513
  "web.tokens.issued": "Token issued",
521
514
  "web.tokens.lastUsed": "Last used",
@@ -526,7 +519,6 @@
526
519
  "web.tokens.revokedOn": "revoked {day}",
527
520
  "web.tokens.scope": "Scope",
528
521
  "web.tokens.status": "Status",
529
- "web.up": "Up",
530
522
  "web.upload.button": "Upload",
531
523
  "web.upload.conflict": "A file with the same name changed in the meantime. Reload and upload again",
532
524
  "web.upload.file": "File",
@@ -536,5 +528,41 @@
536
528
  "web.warnings": "warnings {n}",
537
529
  "web.warningsHeading": "Warnings",
538
530
  "workspace.unknownSource": "Source {name} is not in guideline.json in {dir}",
539
- "web.source.seed": "Seed"
531
+ "web.source.seed": "Seed",
532
+ "web.tokens.delete": "Delete",
533
+ "web.tokens.confirmDelete": "Delete the token {name}?",
534
+ "web.tokens.adminWarning": "An admin token can change the source directly, without proposal review. Issue it only to agents you trust.",
535
+ "propose.applied": "Applied: r{rev} — {url}",
536
+ "propose.appliedNothing": "Nothing changed — same as the source",
537
+ "propose.applyDenied": "An admin token is needed — get a token with scope admin from an admin and store it with guideline login --server {server} --token-stdin",
538
+ "catalog.badDomain": "the part after __ is not a domain — use <tool>__<domain>",
539
+ "web.nav.settings": "Settings",
540
+ "web.archive": "Download (.tar.gz)",
541
+ "web.noBlock": "No such block",
542
+ "web.add.stack": "+ Add stack",
543
+ "web.add.skill": "+ Add skill",
544
+ "web.add.agent": "+ Add subagent",
545
+ "web.add.mcp": "+ Add MCP server",
546
+ "web.add.kind.stack": "Stack",
547
+ "web.add.kind.skill": "Skill",
548
+ "web.add.kind.agent": "Subagent",
549
+ "web.add.kind.mcp": "MCP server",
550
+ "web.add.title": "Add {kind}",
551
+ "web.add.name": "Name",
552
+ "web.add.domain": "Domain (optional)",
553
+ "web.add.categories": "Categories (comma-separated)",
554
+ "web.add.submit": "Add",
555
+ "web.add.badName": "The name starts with a lowercase letter and uses only lowercase letters, digits and -",
556
+ "web.add.badDomain": "Not a domain",
557
+ "web.add.exists": "That name already exists",
558
+ "web.add.message": "Add: {name}",
559
+ "web.settings.title": "Settings",
560
+ "web.settings.files": "Files outside blocks",
561
+ "web.settings.intro": "Files that belong to no block, items left out of the lists, and warnings.",
562
+ "web.settings.problems": "Warnings {warnings} · ignored {ignored} — see Settings",
563
+ "web.usage.title": "Usage",
564
+ "web.usage.copy": "Copy link",
565
+ "web.usage.raw": "Raw",
566
+ "web.client.copied": "Copied",
567
+ "web.manageBlock": "Rename · delete this block"
540
568
  }
@@ -128,6 +128,13 @@ Setup that must be done when adopting a stack or on every clone (config files, h
128
128
 
129
129
  Do not invent a Why. If you do not know the reason, hold off on adding the rule.
130
130
 
131
+ ## Domain stacks
132
+
133
+ The same tool can need different rules on different servers (companies, instances). Then name the stack `<tool>__<domain>`. Examples: `deploy-package__gitlab.develma.com`, `deploy-agent__deploy.kbrainc.dev`.
134
+
135
+ - Use `__` once. The part after it must look like a domain (lowercase letters, digits, `-`, and at least one dot). Otherwise `guideline list` shows a warning (the stack still works).
136
+ - The web shows the tool name with the domain as a tag. Project paths (`.ruler/<stack>/`) and CLI output keep the full name.
137
+
131
138
  ## Skeleton stacks
132
139
 
133
140
  A `rules/<stack>/` with only `README.md` is a skeleton and does not appear in `guideline list`. It becomes a stack the moment its first guideline file (`NN-*.md`) is added.
@@ -40,6 +40,7 @@ guideline update --force # also overwrite files edited in the project
40
40
 
41
41
  - `pick` sends only the result of comparing the project with the source to the server and prints the form address (`Form: <address>` on stderr). It waits until items are checked in the form and submitted. MCP values (secrets) are not sent.
42
42
  - Form states: "same" is already applied, "different" means the project copy differs from the source (turn on "update" to overwrite it), "missing" is not there yet (check it to add). Unchecking an existing item means "remove". Category buttons check every item in that category.
43
+ - A domain stack `<tool>__<domain>` (e.g. `deploy-package__gitlab.develma.com`) holds the rules for using that tool on that server. Pick only stacks whose domain matches the server the project uses.
43
44
  - Where copies go: always-on guidelines in `.ruler/<stack>/NN-*.md`, reference guidelines in `docs/guideline/<stack>/` with the index `.ruler/<stack>/99-reference.md`, skills in `.ruler/skills/`, MCP in `.ruler/ruler.toml`. After applying, `ruler apply` runs at the git root (when ruler is installed).
44
45
  - Applying creates `guideline.json` (chosen items) and `guideline-lock.json` (applied revision and hashes). Commit them with the applied files. The CLI does not commit.
45
46
  - **Monorepos**: run in a subdirectory with its own `.ruler/` and that directory becomes a separate unit, with its own manifest, lock and copies. Skills, subagents and MCP servers are received only at the git root. ruler builds that directory's guidelines only when `.ruler/ruler.toml` at the git root has `nested = true`.
@@ -122,8 +123,9 @@ guideline propose --from <that folder> --title "<title>" # send what was added
122
123
  - `GET {{SERVER}}/api/v1/catalog`: stacks, skills, subagents, MCP servers and their categories
123
124
  - `GET {{SERVER}}/api/v1/archive?rev=<revision>`: the whole source (tar.gz)
124
125
  - `GET {{SERVER}}/api/v1/files/<path>?rev=<revision>`: one source file
125
- - `POST {{SERVER}}/api/v1/proposals`: a proposal (`propose` scope token). Body `{ title, description?, project?, changes: [{ path, text | base64 | delete: true, base: <sha256> | null }] }`. An admin token can use `apply: true` to apply it right away without review
126
- - `GET {{SERVER}}/api/v1/proposals`, `GET {{SERVER}}/api/v1/proposals/<number>`: the status of your proposals
126
+ - `POST {{SERVER}}/api/v1/proposals`: a proposal (`propose` scope token). Body `{ title, description?, project?, changes: [{ path, text | base64 | delete: true, base: <sha256> | null }] }`. An admin token or an `admin` scope token can use `apply: true` to apply it right away without review. From the CLI: `guideline propose --apply`
127
+ - `GET {{SERVER}}/api/v1/proposals`, `GET {{SERVER}}/api/v1/proposals/<number>`: the status of your proposals (an `admin` scope token sees all)
128
+ - `POST {{SERVER}}/api/v1/proposals/<number>/accept`, `POST {{SERVER}}/api/v1/proposals/<number>/reject` (body `{ note? }`): accept or reject a proposal (admin token or `admin` scope token). A conflict is 409 with `conflicts`
127
129
 
128
130
  ## More
129
131
 
@@ -0,0 +1,5 @@
1
+ {
2
+ "type": "stdio",
3
+ "command": "npx",
4
+ "args": ["-y", "<package>"]
5
+ }
@@ -13,6 +13,7 @@ description: Proposes sections of this project's guidelines that other projects
13
13
 
14
14
  1. **Get the source.** Pick an empty temporary folder (for example `src` under `mktemp -d`) and run `guideline source <folder>`. If this project's `guideline.json` has more than one source, ask the user which source (guideline server) to upload to and run `guideline source <folder> --source <name>`. Below, this is the "source folder". If you see "Login required", point the user to `! guideline login --server <URL>` and stop.
15
15
  2. **Permission to propose.** If uploading says "This token cannot propose", tell the user to get a token with the `propose` scope from an admin and store it with `guideline login --server <URL> --token-stdin`, then stop.
16
+ - Only when the stored token has the `admin` scope and the user asked to apply without review, upload with `guideline propose --from <source folder> --title <title> --apply`. If it says "An admin token is needed", upload as a proposal without `--apply`.
16
17
  3. If `<source folder>/docs/authoring.md` has no `## Subagents` or `## MCP servers` section, stop before uploading subagents or MCP servers and ask the guideline admin to add that section to the source. When uploading an MCP server built from its README, also read the `### Servers built when applied` section.
17
18
  4. If `<source folder>/docs/authoring.md` has no `## Load — always and reference` section, stop the same way before uploading guideline files. The rule for choosing the load is in that section. When uploading an override of another stack, also read the `## Overriding another stack` section.
18
19
  5. **Edits to received copies are not this skill's job.** Copies that `guideline status` shows as "edited in project" are sent with `guideline propose` (it turns the copy back into source form). This skill uploads guidelines that are new to the source. `propose` leaves out copies that already differed from the source when first received (to-do `adopted`), because proposing an old version would undo source changes.