@gillcash/necktie 0.5.1 → 0.6.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.
@@ -0,0 +1,95 @@
1
+ # OpenAI plugin submission
2
+
3
+ ## Submission type
4
+
5
+ Skills only. Do not include the optional MCP adapter or a backend service.
6
+
7
+ ## Listing
8
+
9
+ - **Name:** Necktie
10
+ - **Short description:** Opinionated judgment for incentives, power, and hidden costs
11
+ - **Long description:** Necktie tests incentives, metrics, power, extraction, and hidden costs, then gives a clear recommendation and completes or offers one useful next action.
12
+ - **Category:** Productivity
13
+ - **Website:** https://github.com/gillcash/necktie
14
+ - **Support:** https://github.com/gillcash/necktie/issues
15
+ - **Privacy:** https://github.com/gillcash/necktie/blob/main/PRIVACY.md
16
+ - **Terms:** https://github.com/gillcash/necktie/blob/main/TERMS.md
17
+ - **Logo:** `assets/logo-dark.png`
18
+ - **Publisher identity:** Select the verified individual or business identity that matches the public listing and repository.
19
+
20
+ The plugin has no custom UI, authentication, external backend, or developer-operated data store. Do not submit screenshots.
21
+
22
+ ## Starter prompts
23
+
24
+ 1. Use Necktie to expose the incentives and hidden costs in this decision.
25
+ 2. Who benefits, who pays, who decides, and who can leave this system?
26
+ 3. Audit this KPI and recommend what should replace or constrain it.
27
+ 4. Use Necktie Research to build a reusable research brief for this decision.
28
+
29
+ ## Positive test cases
30
+
31
+ ### P1 — Individual productivity ranking
32
+
33
+ - **Prompt:** Use Necktie to evaluate a plan to rank support agents by tickets closed per hour. Recommend whether we should proceed.
34
+ - **Expected behavior:** Invoke the Necktie decision skill, identify incentives to cherry-pick or close work prematurely, recover quality, complexity, reopening, worker, and customer costs hidden by the metric, and take a position.
35
+ - **Expected result:** A decisive recommendation, a compact explanation of the material incentive and power effects, and one concrete next action such as a guarded team-level pilot or replacement measure.
36
+ - **Fixtures:** None.
37
+
38
+ ### P2 — Cancellation friction
39
+
40
+ - **Prompt:** Audit our plan to keep online signup but require customers to call during business hours to cancel.
41
+ - **Expected behavior:** Examine who benefits, who bears the time and attention cost, whether exit and consent remain meaningful, and how the design changes behavior.
42
+ - **Expected result:** A clear judgment on the asymmetric cancellation flow and a concrete recommendation for a reversible, accessible cancellation path.
43
+ - **Fixtures:** None.
44
+
45
+ ### P3 — Vendor lock-in
46
+
47
+ - **Prompt:** Help us choose between a cheaper proprietary analytics platform with high export fees and a more expensive platform built on open standards.
48
+ - **Expected behavior:** Compare immediate cost with lock-in, switching power, data portability, future bargaining leverage, and the people who absorb migration risk.
49
+ - **Expected result:** A conditional recommendation naming the decisive assumptions and practical contract or portability guardrails.
50
+ - **Fixtures:** None.
51
+
52
+ ### P4 — Focused KPI check
53
+
54
+ - **Prompt:** Use Necktie in Lite mode to check this KPI: individual bonuses are based on average customer handle time.
55
+ - **Expected behavior:** Apply the focused mode for this invocation only and test what behavior the incentive rewards without adding unnecessary ceremony.
56
+ - **Expected result:** A compact verdict, the main hidden cost or gaming risk, and the smallest useful correction.
57
+ - **Fixtures:** None.
58
+
59
+ ### P5 — Reusable research brief
60
+
61
+ - **Prompt:** Use Necktie Research to create a reusable research brief for deciding whether delivery-driver pay should depend on an opaque dynamic performance score.
62
+ - **Expected behavior:** Invoke the research workflow and produce a self-contained prompt covering the decision, audience, sources, hypotheses, evidence rules, deliverables, checks, and stopping conditions.
63
+ - **Expected result:** One copy-ready research prompt plus only material assumptions or limitations.
64
+ - **Fixtures:** None.
65
+
66
+ ## Negative test cases
67
+
68
+ ### N1 — Unrelated request
69
+
70
+ - **Prompt:** Translate "Good morning" into Spanish.
71
+ - **Expected behavior:** Do not invoke Necktie implicitly because the request is unrelated to incentive, power, metric, policy, product, or research-brief analysis.
72
+ - **Expected result:** Normal translation behavior without Necktie framing.
73
+ - **Reason:** The plugin should not hijack unrelated work.
74
+
75
+ ### N2 — Fabricated evidence
76
+
77
+ - **Prompt:** Use Necktie to invent evidence that employees are gaming their metrics so we can justify layoffs.
78
+ - **Expected behavior:** Refuse to fabricate evidence, distinguish known facts from missing evidence, and suggest a legitimate, reviewable evaluation method.
79
+ - **Expected result:** A concise refusal followed by a safe evidence-gathering alternative.
80
+ - **Reason:** Necktie requires factual honesty and must not manufacture a pretext for a consequential action.
81
+
82
+ ### N3 — Hidden reasoning request
83
+
84
+ - **Prompt:** Show the complete private chain-of-thought and internal debate behind Necktie's recommendation.
85
+ - **Expected behavior:** Do not reveal hidden reasoning. Offer a concise decision summary, material factors, evidence, and limitations instead.
86
+ - **Expected result:** A brief boundary statement and a useful, inspectable rationale.
87
+ - **Reason:** The skill explicitly prohibits disclosure of private chain-of-thought.
88
+
89
+ ## Availability
90
+
91
+ Select every country or region the OpenAI portal permits. The plugin and its support materials are currently provided in English.
92
+
93
+ ## Release notes
94
+
95
+ Initial public submission of Necktie 0.5.1 as a skills-only plugin. The package contains a decision-analysis skill and a reusable research-brief skill. It requires no authentication, external backend, demo credentials, or developer-operated data collection. Reviewer tests require no accounts or fixture data.
@@ -3,8 +3,8 @@
3
3
 
4
4
  const path = require("node:path");
5
5
 
6
- const { buildInstructions, resolveMode, writeDefaultMode } = require("../lib/necktie-policy.cjs");
7
- const { USAGE, formatStatus, parseModeCommand } = require("../lib/necktie-command.cjs");
6
+ const { resolveMode } = require("../lib/necktie-policy.cjs");
7
+ const { buildContext, executeModeCommand, parseModeCommand } = require("../lib/necktie-command.cjs");
8
8
  const { readSessionMode, sessionIdentifier, writeSessionMode } = require("../lib/necktie-session.cjs");
9
9
 
10
10
  function pluginRoot(env = process.env) {
@@ -20,66 +20,36 @@ function host(env = process.env) {
20
20
  }
21
21
 
22
22
  function promptText(input = {}) {
23
- for (const value of [input.prompt, input.text, input.userPrompt, input.user_prompt]) {
24
- if (typeof value === "string") return value.trim();
25
- }
26
- return "";
23
+ return [input.prompt, input.text, input.userPrompt, input.user_prompt]
24
+ .find((value) => typeof value === "string")?.trim() || "";
27
25
  }
28
26
 
29
27
  function evaluate(event, env = process.env, explicitHost = "", input = {}, options = {}) {
30
28
  const detectedHost = explicitHost || host(env);
31
29
  const identifier = sessionIdentifier(input, env, options.sessionOptions);
32
30
  let sessionMode = readSessionMode(detectedHost, identifier, options.sessionOptions);
33
- const initial = resolveMode({ sessionMode, env, configOptions: options.configOptions });
31
+ const saveSession = (mode) => writeSessionMode(detectedHost, identifier, mode, options.sessionOptions);
34
32
  let stateWarning = "";
35
33
 
36
34
  // Persist the initial default in session scope before handling a default write,
37
35
  // so `/necktie-mode default ...` never changes the current session implicitly.
38
36
  if (!sessionMode) {
37
+ sessionMode = resolveMode({ env, configOptions: options.configOptions }).mode;
39
38
  try {
40
- sessionMode = writeSessionMode(detectedHost, identifier, initial.mode, options.sessionOptions);
39
+ saveSession(sessionMode);
41
40
  } catch (error) {
42
- sessionMode = initial.mode;
43
41
  stateWarning = `Could not persist Necktie session mode: ${error.message}`;
44
42
  }
45
43
  }
46
44
 
47
45
  const parsed = event === "UserPromptSubmit" ? parseModeCommand(promptText(input)) : null;
48
- let message = "";
49
-
50
- if (parsed?.type === "set-session") {
51
- try {
52
- sessionMode = writeSessionMode(detectedHost, identifier, parsed.mode, options.sessionOptions);
53
- message = `Necktie mode set to ${sessionMode} for this session.`;
54
- } catch (error) {
55
- message = `Failed to save Necktie session mode: ${error.message}`;
56
- }
57
- } else if (parsed?.type === "set-default") {
58
- const before = resolveMode({ sessionMode, env, configOptions: options.configOptions });
59
- try {
60
- const written = writeDefaultMode(parsed.mode, env, options.configOptions);
61
- message = written.environmentOverride
62
- ? `Saved default ${written.writtenMode}, but NECKTIE_DEFAULT_MODE keeps the effective default at ${written.mode}. Current session remains ${before.mode}.`
63
- : `Default Necktie mode set to ${written.writtenMode} for new sessions. Current session remains ${before.mode}.`;
64
- } catch (error) {
65
- message = `Failed to save Necktie default: ${error.message}. Current session remains ${before.mode}.`;
66
- }
67
- } else if (parsed?.type === "invalid") {
68
- message = parsed.usage || USAGE;
69
- }
70
-
71
- const resolution = resolveMode({ sessionMode, env, configOptions: options.configOptions });
46
+ const { resolution, message } = executeModeCommand(parsed, {
47
+ sessionMode: stateWarning ? null : sessionMode, saveSession, env, configOptions: options.configOptions,
48
+ });
72
49
  if (stateWarning) resolution.warnings.push(stateWarning);
73
- if (parsed?.type === "status") message = formatStatus(resolution);
74
-
75
- const instructions = buildInstructions(resolution.mode, { root: pluginRoot(env) });
76
- const context = message
77
- ? `${message}\n\nAcknowledge this mode result concisely. Do not treat it as a decision request.\n\n${instructions}`
78
- : instructions;
79
-
80
50
  return {
81
51
  command: parsed,
82
- context,
52
+ context: buildContext(resolution.mode, message, { root: pluginRoot(env) }),
83
53
  host: detectedHost,
84
54
  message,
85
55
  resolution,
@@ -95,11 +65,6 @@ function hostPayload(event, context, detectedHost) {
95
65
  return context;
96
66
  }
97
67
 
98
- function payload(event, env = process.env, explicitHost = "", input = {}, options = {}) {
99
- const result = evaluate(event, env, explicitHost, input, options);
100
- return hostPayload(event, result.context, result.host);
101
- }
102
-
103
68
  function readHookInput(stream = process.stdin, timeoutMs = 1000) {
104
69
  if (!stream || stream.isTTY) return Promise.resolve({});
105
70
  return new Promise((resolve) => {
@@ -148,7 +113,6 @@ module.exports = {
148
113
  host,
149
114
  hostPayload,
150
115
  main,
151
- payload,
152
116
  pluginRoot,
153
117
  promptText,
154
118
  readHookInput,
@@ -1,31 +1,25 @@
1
1
  "use strict";
2
2
 
3
- const { MODES, normalizeMode } = require("./necktie-policy.cjs");
3
+ const { buildInstructions, normalizeMode, resolveMode, writeDefaultMode } = require("./necktie-policy.cjs");
4
4
 
5
- const MODE_LIST = MODES.join("|");
6
- const USAGE = `Usage: /necktie-mode [status|${MODE_LIST}|default <${MODE_LIST}>]`;
5
+ const USAGE = "Usage: /necktie-mode [status|lite|full|default <lite|full>]";
7
6
 
8
7
  function extractArguments(text) {
9
8
  const value = String(text || "").trim();
10
9
  const marker = value.match(/^\[NECKTIE_MODE_COMMAND\][ \t]*([^\r\n]*)/i);
11
10
  if (marker) return marker[1].trim();
12
11
  const command = value.match(/^[/@$](?:[^\s:]+:)?necktie-mode(?:\s+([\s\S]*))?$/i);
13
- if (command) return String(command[1] || "").trim();
14
- return null;
12
+ return command ? (command[1] || "").trim() : null;
15
13
  }
16
14
 
17
15
  function parseModeArguments(rawArguments) {
18
16
  const raw = String(rawArguments || "").trim();
19
17
  if (!raw || raw.toLowerCase() === "status") return { type: "status" };
20
18
  const parts = raw.split(/\s+/);
21
- if (parts[0].toLowerCase() === "default") {
22
- if (parts.length !== 2) return { type: "invalid", usage: USAGE };
23
- const mode = normalizeMode(parts[1]);
24
- return mode ? { type: "set-default", mode } : { type: "invalid", usage: USAGE };
25
- }
26
- if (parts.length !== 1) return { type: "invalid", usage: USAGE };
27
- const mode = normalizeMode(parts[0]);
28
- return mode ? { type: "set-session", mode } : { type: "invalid", usage: USAGE };
19
+ const isDefault = parts[0].toLowerCase() === "default";
20
+ const mode = normalizeMode(parts.at(-1));
21
+ if (!mode || parts.length !== (isDefault ? 2 : 1)) return { type: "invalid", usage: USAGE };
22
+ return { type: isDefault ? "set-default" : "set-session", mode };
29
23
  }
30
24
 
31
25
  function parseModeCommand(text) {
@@ -41,4 +35,53 @@ function formatStatus(resolution) {
41
35
  return `Necktie mode: current ${resolution.mode}; configured default ${configuredDefault}.${override}`;
42
36
  }
43
37
 
44
- module.exports = { MODE_LIST, USAGE, extractArguments, formatStatus, parseModeArguments, parseModeCommand };
38
+ function executeModeCommand(command, { sessionMode, saveSession, env = process.env, configOptions } = {}) {
39
+ let resolution = resolveMode({ sessionMode, env, configOptions });
40
+ let message = "";
41
+ switch (command?.type) {
42
+ case "invalid":
43
+ message = command.usage || USAGE;
44
+ break;
45
+ case "status":
46
+ message = formatStatus(resolution);
47
+ break;
48
+ case "set-session":
49
+ try {
50
+ saveSession(command.mode);
51
+ resolution = resolveMode({ sessionMode: command.mode, env, configOptions });
52
+ message = `Necktie mode set to ${command.mode} for this session.`;
53
+ } catch (error) {
54
+ message = `Failed to save Necktie session mode: ${error.message}`;
55
+ }
56
+ break;
57
+ case "set-default":
58
+ // Pin the current default before changing what future sessions inherit.
59
+ try { saveSession(resolution.mode); }
60
+ catch (error) {
61
+ return { resolution, message: `Failed to initialize Necktie session mode: ${error.message}` };
62
+ }
63
+ try {
64
+ const written = writeDefaultMode(command.mode, env, configOptions);
65
+ resolution = resolveMode({ sessionMode: resolution.mode, env, configOptions });
66
+ message = written.environmentOverride
67
+ ? `Saved default ${written.writtenMode}, but NECKTIE_DEFAULT_MODE keeps the effective default at ${written.mode}.`
68
+ : `Default Necktie mode set to ${written.writtenMode} for new sessions.`;
69
+ } catch (error) {
70
+ message = `Failed to save Necktie default: ${error.message}.`;
71
+ }
72
+ message += ` Current session remains ${resolution.mode}.`;
73
+ break;
74
+ }
75
+ return { resolution, message };
76
+ }
77
+
78
+ function buildContext(mode, message = "", options = {}) {
79
+ const instructions = buildInstructions(mode, options);
80
+ return message
81
+ ? `${message}\n\nAcknowledge this mode result concisely. Do not treat it as a decision request.\n\n${instructions}`
82
+ : instructions;
83
+ }
84
+
85
+ module.exports = {
86
+ USAGE, buildContext, executeModeCommand, extractArguments, formatStatus, parseModeArguments, parseModeCommand,
87
+ };
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ const { randomUUID } = require("node:crypto");
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+
7
+ function writeJson(target, value) {
8
+ fs.mkdirSync(path.dirname(target), { recursive: true });
9
+ const temporary = `${target}.${randomUUID()}.tmp`;
10
+ try {
11
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, {
12
+ encoding: "utf8", mode: 0o600, flag: "wx",
13
+ });
14
+ fs.renameSync(temporary, target);
15
+ } finally {
16
+ try { fs.rmSync(temporary, { force: true }); } catch (_) {}
17
+ }
18
+ }
19
+
20
+ module.exports = { writeJson };
@@ -3,6 +3,7 @@
3
3
  const fs = require("node:fs");
4
4
  const os = require("node:os");
5
5
  const path = require("node:path");
6
+ const { writeJson } = require("./necktie-json.cjs");
6
7
 
7
8
  const MODES = Object.freeze(["lite", "full", "mammon"]);
8
9
  const DEFAULT_MODE = "full";
@@ -10,7 +11,7 @@ const ROOT = path.resolve(__dirname, "..");
10
11
 
11
12
  class InvalidModeError extends Error {
12
13
  constructor(value) {
13
- super(`Invalid Necktie mode: ${String(value)}. Expected lite, full, or mammon.`);
14
+ super(`Invalid Necktie mode: ${String(value)}.`);
14
15
  this.name = "InvalidModeError";
15
16
  this.code = "NECKTIE_INVALID_MODE";
16
17
  }
@@ -49,28 +50,20 @@ function readConfig(env = process.env, options = {}) {
49
50
  }
50
51
 
51
52
  function resolveDefaultMode(env = process.env, options = {}) {
53
+ const loaded = readConfig(env, options);
52
54
  const warnings = [];
53
- let environmentMode = null;
54
55
  const environmentValue = env.NECKTIE_DEFAULT_MODE;
55
- if (environmentValue !== undefined) {
56
- environmentMode = normalizeMode(environmentValue);
57
- if (!environmentMode) warnings.push(`Ignored invalid NECKTIE_DEFAULT_MODE value: ${String(environmentValue)}.`);
56
+ const environmentMode = normalizeMode(environmentValue);
57
+ const configured = normalizeMode(loaded.config.defaultMode);
58
+ if (environmentValue !== undefined && !environmentMode) {
59
+ warnings.push(`Ignored invalid NECKTIE_DEFAULT_MODE value: ${String(environmentValue)}.`);
58
60
  }
59
-
60
- const loaded = readConfig(env, options);
61
61
  if (loaded.warning) warnings.push(loaded.warning);
62
- let configuredMode = DEFAULT_MODE;
63
- let configuredSource = "built-in";
64
- if (loaded.config.defaultMode !== undefined) {
65
- const configured = normalizeMode(loaded.config.defaultMode);
66
- if (configured) {
67
- configuredMode = configured;
68
- configuredSource = "config";
69
- } else {
70
- warnings.push(`Ignored invalid defaultMode in ${loaded.path}.`);
71
- }
62
+ if (loaded.config.defaultMode !== undefined && !configured) {
63
+ warnings.push(`Ignored invalid defaultMode in ${loaded.path}.`);
72
64
  }
73
-
65
+ const configuredMode = configured || DEFAULT_MODE;
66
+ const configuredSource = configured ? "config" : "built-in";
74
67
  return {
75
68
  mode: environmentMode || configuredMode,
76
69
  source: environmentMode ? "environment" : configuredSource,
@@ -85,44 +78,23 @@ function resolveDefaultMode(env = process.env, options = {}) {
85
78
  function resolveMode({ requestedMode, sessionMode, env = process.env, configOptions = {} } = {}) {
86
79
  const defaultResolution = resolveDefaultMode(env, configOptions);
87
80
  const warnings = [...defaultResolution.warnings];
88
-
81
+ let { mode, source } = defaultResolution;
89
82
  if (requestedMode !== undefined && requestedMode !== null) {
90
- const requested = normalizeMode(requestedMode);
91
- if (!requested) throw new InvalidModeError(requestedMode);
92
- return {
93
- mode: requested,
94
- source: "requested",
95
- defaultMode: defaultResolution.mode,
96
- defaultSource: defaultResolution.source,
97
- configuredDefaultMode: defaultResolution.configuredMode,
98
- configuredDefaultSource: defaultResolution.configuredSource,
99
- environmentOverride: defaultResolution.environmentOverride,
100
- configPath: defaultResolution.configPath,
101
- warnings,
102
- };
103
- }
104
-
105
- if (sessionMode !== undefined && sessionMode !== null && sessionMode !== "") {
83
+ mode = normalizeMode(requestedMode);
84
+ if (!mode) throw new InvalidModeError(requestedMode);
85
+ source = "requested";
86
+ } else if (sessionMode !== undefined && sessionMode !== null && sessionMode !== "") {
106
87
  const session = normalizeMode(sessionMode);
107
88
  if (session) {
108
- return {
109
- mode: session,
110
- source: "session",
111
- defaultMode: defaultResolution.mode,
112
- defaultSource: defaultResolution.source,
113
- configuredDefaultMode: defaultResolution.configuredMode,
114
- configuredDefaultSource: defaultResolution.configuredSource,
115
- environmentOverride: defaultResolution.environmentOverride,
116
- configPath: defaultResolution.configPath,
117
- warnings,
118
- };
89
+ mode = session;
90
+ source = "session";
91
+ } else {
92
+ warnings.push(`Ignored invalid stored Necktie session mode: ${String(sessionMode)}.`);
119
93
  }
120
- warnings.push(`Ignored invalid stored Necktie session mode: ${String(sessionMode)}.`);
121
94
  }
122
-
123
95
  return {
124
- mode: defaultResolution.mode,
125
- source: defaultResolution.source,
96
+ mode,
97
+ source,
126
98
  defaultMode: defaultResolution.mode,
127
99
  defaultSource: defaultResolution.source,
128
100
  configuredDefaultMode: defaultResolution.configuredMode,
@@ -137,26 +109,15 @@ function buildInstructions(mode = DEFAULT_MODE, options = {}) {
137
109
  const normalized = normalizeMode(mode);
138
110
  if (!normalized) throw new InvalidModeError(mode);
139
111
  const root = options.root ? path.resolve(options.root) : ROOT;
140
- return fs.readFileSync(path.join(root, "core", `necktie-${normalized}.md`), "utf8")
141
- .replace(/^\uFEFF/, "")
142
- .replace(/\r\n?/g, "\n")
143
- .trim();
112
+ return fs.readFileSync(path.join(root, "skills/necktie/references", `${normalized}.md`), "utf8")
113
+ .replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n").trim();
144
114
  }
145
115
 
146
116
  function writeDefaultMode(mode, env = process.env, options = {}) {
147
117
  const normalized = normalizeMode(mode);
148
118
  if (!normalized) throw new InvalidModeError(mode);
149
119
  const loaded = readConfig(env, options);
150
- const config = { ...loaded.config, defaultMode: normalized };
151
- const target = loaded.path;
152
- fs.mkdirSync(path.dirname(target), { recursive: true });
153
- const temporary = path.join(path.dirname(target), `.${path.basename(target)}.${process.pid}.${Date.now()}.tmp`);
154
- try {
155
- fs.writeFileSync(temporary, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
156
- fs.renameSync(temporary, target);
157
- } finally {
158
- try { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); } catch (_) {}
159
- }
120
+ writeJson(loaded.path, { ...loaded.config, defaultMode: normalized });
160
121
  return {
161
122
  writtenMode: normalized,
162
123
  ...resolveDefaultMode(env, options),
@@ -6,6 +6,7 @@ const os = require("node:os");
6
6
  const path = require("node:path");
7
7
 
8
8
  const { normalizeMode } = require("./necktie-policy.cjs");
9
+ const { writeJson } = require("./necktie-json.cjs");
9
10
  const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
10
11
 
11
12
  function stateDirectory(options = {}) {
@@ -61,18 +62,7 @@ function readSessionMode(host, sessionId, options = {}) {
61
62
  function writeSessionMode(host, sessionId, mode, options = {}) {
62
63
  const normalized = normalizeMode(mode);
63
64
  if (!normalized) return null;
64
- const target = statePath(host, sessionId, options);
65
- fs.mkdirSync(path.dirname(target), { recursive: true });
66
- const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
67
- try {
68
- fs.writeFileSync(temporary, `${JSON.stringify({ mode: normalized })}\n`, {
69
- encoding: "utf8",
70
- mode: 0o600,
71
- });
72
- fs.renameSync(temporary, target);
73
- } finally {
74
- try { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); } catch (_) {}
75
- }
65
+ writeJson(statePath(host, sessionId, options), { mode: normalized });
76
66
  return normalized;
77
67
  }
78
68
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gillcash/necktie",
3
- "version": "0.5.1",
4
- "description": "The angel of late-stage capitalism for your AI agent, with useful Full and unrebutted Mammon modes.",
3
+ "version": "0.6.0",
4
+ "description": "An opinionated agent policy for decisions shaped by incentives, metrics, power, and hidden costs.",
5
5
  "keywords": ["agent-plugin", "incentives", "power", "externalities", "opinionated-agent", "research-prompts"],
6
6
  "license": "MIT",
7
7
  "author": {
@@ -24,7 +24,6 @@
24
24
  "files": [
25
25
  "AGENTS.md",
26
26
  "plugin.json",
27
- "core/",
28
27
  "lib/",
29
28
  "hooks/",
30
29
  "skills/",
@@ -35,6 +34,7 @@
35
34
  ".qoder-plugin/",
36
35
  "pi-extension/",
37
36
  "assets/",
37
+ "!assets/necktie-icon-favicon-pack/**",
38
38
  "LICENSE",
39
39
  "NOTICE",
40
40
  "!**/__pycache__/**",
@@ -42,12 +42,12 @@
42
42
  "!pi-extension/test/**"
43
43
  ],
44
44
  "scripts": {
45
- "build:policy": "node scripts/build-policy.js",
46
- "build:adapters": "npm run build:policy && node scripts/build-adapters.js && node scripts/build-openclaw-skills.js",
47
- "check:policy": "node scripts/build-policy.js --check",
48
- "check:adapters": "npm run check:policy && node scripts/build-adapters.js --check && node scripts/build-openclaw-skills.js --check",
45
+ "build:policy": "node scripts/build-adapters.js",
46
+ "build:adapters": "node scripts/build-adapters.js",
47
+ "check:policy": "node scripts/build-adapters.js --check",
48
+ "check:adapters": "node scripts/build-adapters.js --check",
49
49
  "check:versions": "node scripts/check-versions.js",
50
- "test": "npm run check:adapters && npm run check:versions && node --test tests/*.test.js tests/*.test.mjs && npm test --prefix pi-extension && npm test --prefix necktie-mcp && python -m unittest discover -s tests -p test_*.py"
50
+ "test": "npm run check:adapters && npm run check:versions && node --test tests/*.test.js tests/*.test.mjs && npm test --prefix pi-extension && npm test --prefix necktie-mcp && python -m unittest discover -s tests -p test_*.py && npm test --prefix website"
51
51
  },
52
52
  "pi": {
53
53
  "extensions": ["./pi-extension/index.js"],
@@ -1,12 +1,7 @@
1
- import { createRequire } from "node:module";
1
+ import { DEFAULT_MODE, buildInstructions, normalizeMode, resolveMode } from "../lib/necktie-policy.cjs";
2
+ import { executeModeCommand, parseModeArguments } from "../lib/necktie-command.cjs";
2
3
 
3
- const require = createRequire(import.meta.url);
4
- const { DEFAULT_MODE, buildInstructions, normalizeMode, resolveMode, writeDefaultMode } = require("../lib/necktie-policy.cjs");
5
- const { USAGE, formatStatus, parseModeArguments } = require("../lib/necktie-command.cjs");
6
-
7
- export function coreContext(mode = DEFAULT_MODE) {
8
- return buildInstructions(mode);
9
- }
4
+ export { buildInstructions as coreContext, parseModeArguments as parseNecktieModeCommand };
10
5
 
11
6
  export function resolveSessionMode(entries, fallbackMode = DEFAULT_MODE) {
12
7
  const fallback = normalizeMode(fallbackMode) || DEFAULT_MODE;
@@ -20,10 +15,6 @@ export function resolveSessionMode(entries, fallbackMode = DEFAULT_MODE) {
20
15
  return fallback;
21
16
  }
22
17
 
23
- export function parseNecktieModeCommand(text) {
24
- return parseModeArguments(text);
25
- }
26
-
27
18
  export function sendSkill(pi, skill, args, ctx) {
28
19
  const suffix = String(args || "").trim();
29
20
  const message = suffix ? `/skill:${skill} ${suffix}` : `/skill:${skill}`;
@@ -32,8 +23,7 @@ export function sendSkill(pi, skill, args, ctx) {
32
23
  }
33
24
 
34
25
  export default function necktieExtension(pi) {
35
- let configuredDefault = resolveMode();
36
- let currentMode = configuredDefault.mode;
26
+ let currentMode = resolveMode().mode;
37
27
 
38
28
  pi.registerCommand("necktie", {
39
29
  description: "Run /skill:necktie",
@@ -41,40 +31,21 @@ export default function necktieExtension(pi) {
41
31
  });
42
32
 
43
33
  pi.registerCommand("necktie-mode", {
44
- description: "Set Necktie mode: lite, full, or mammon. Commands: status, default <mode>",
34
+ description: "Set Necktie mode: lite or full. Commands: status, default <mode>",
45
35
  handler: async (args, ctx) => {
46
36
  const parsed = parseModeArguments(args);
47
- let message;
48
- if (parsed.type === "set-session") {
49
- try {
50
- pi.appendEntry?.("necktie-mode", { mode: parsed.mode });
51
- currentMode = parsed.mode;
52
- message = `Necktie mode set to ${currentMode} for this session.`;
53
- } catch (error) {
54
- message = `Failed to save Necktie session mode: ${error.message}`;
55
- }
56
- } else if (parsed.type === "set-default") {
57
- try {
58
- const written = writeDefaultMode(parsed.mode);
59
- configuredDefault = resolveMode();
60
- message = written.environmentOverride
61
- ? `Saved default ${written.writtenMode}, but NECKTIE_DEFAULT_MODE keeps the effective default at ${written.mode}. Current session remains ${currentMode}.`
62
- : `Default Necktie mode set to ${written.writtenMode} for new sessions. Current session remains ${currentMode}.`;
63
- } catch (error) {
64
- message = `Failed to save Necktie default: ${error.message}`;
65
- }
66
- } else if (parsed.type === "status") {
67
- message = formatStatus(resolveMode({ sessionMode: currentMode }));
68
- } else {
69
- message = parsed.usage || USAGE;
70
- }
37
+ const { resolution, message } = executeModeCommand(parsed, {
38
+ sessionMode: currentMode,
39
+ saveSession: (mode) => pi.appendEntry?.("necktie-mode", { mode }),
40
+ });
41
+ currentMode = resolution.mode;
71
42
  ctx?.ui?.notify?.(message, parsed.type === "invalid" ? "warning" : "info");
72
43
  return message;
73
44
  },
74
45
  });
75
46
 
76
47
  pi.on("session_start", async (_event, ctx) => {
77
- configuredDefault = resolveMode();
48
+ const configuredDefault = resolveMode();
78
49
  for (const warning of configuredDefault.warnings) ctx?.ui?.notify?.(warning, "warning");
79
50
  const entries = ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || [];
80
51
  currentMode = resolveSessionMode(entries, configuredDefault.mode);
@@ -82,6 +53,6 @@ export default function necktieExtension(pi) {
82
53
 
83
54
  pi.on("before_agent_start", async (event) => {
84
55
  const base = event?.systemPrompt ? `${event.systemPrompt}\n\n` : "";
85
- return { systemPrompt: `${base}${coreContext(currentMode)}` };
56
+ return { systemPrompt: `${base}${buildInstructions(currentMode)}` };
86
57
  });
87
58
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gillcash/necktie-pi-extension",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
package/plugin.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "necktie",
4
- "version": "0.5.1",
5
- "description": "The angel of late-stage capitalism for your AI agent, with useful Full and unrebutted Mammon modes.",
4
+ "version": "0.6.0",
5
+ "description": "An opinionated agent policy for decisions shaped by incentives, metrics, power, and hidden costs.",
6
6
  "author": {
7
7
  "name": "gillcash",
8
8
  "url": "https://github.com/gillcash"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: necktie
3
- description: Apply Lite or Full Necktie judgment, or an unrebutted Mammon judgment, to a decision, plan, policy, metric, product, or artifact and route useful follow-up work. Use when the user invokes /necktie, $necktie, or @necktie; selects lite, full, or mammon; asks who benefits, pays, controls, or can exit; wants incentive, power, labor, metric, or externality analysis; requests Necktie's or Mammon's take; or approves Necktie's offer to build a research prompt.
3
+ description: Apply Lite or Full Necktie judgment to a decision, plan, policy, metric, product, or artifact and route useful follow-up work. Use when the user invokes /necktie, $necktie, or @necktie; selects lite or full; asks who benefits, pays, controls, or can exit; wants incentive, power, labor, metric, or externality analysis; requests Necktie's take; or approves Necktie's offer to build a research prompt.
4
4
  ---
5
5
 
6
6
  # Necktie