autark-cli 0.5.0 → 0.5.2

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 (2) hide show
  1. package/autark.mjs +42 -22
  2. package/package.json +1 -1
package/autark.mjs CHANGED
@@ -515,10 +515,8 @@ async function refreshRuntimeFiles() {
515
515
  }
516
516
 
517
517
  async function refreshSkills() {
518
- for (const name of SKILL_NAMES) {
519
- const res = await spawnSync('pnpm', ['dlx', '--silent', 'skills', 'add', `kiluazen/skills@${name}`, '-y', '-g'])
520
- if (res.status !== 0) throw new Error(`skill ${name} install failed`)
521
- }
518
+ const res = await spawnSync('pnpm', ['dlx', '--silent', 'skills', 'add', 'kiluazen/kstack', '-s', SKILL_NAMES.join(','), '-y', '-g'])
519
+ if (res.status !== 0) throw new Error(`kstack skills install failed`)
522
520
  }
523
521
 
524
522
  async function reloadLaunchd() {
@@ -831,48 +829,52 @@ async function mailLint(rest) {
831
829
  'the key insight', 'what this means is', 'the answer is', 'the trick is',
832
830
  'that\'s exactly', 'that\'s precisely', 'that\'s the whole point',
833
831
  ]
832
+ const WHY = {
833
+ 'structure-word': 'AI-fingerprint vocabulary. Humans don\'t write "structurally different" or "fundamentally" in cold email; LLMs reach for them when hedging or sounding profound. Describe the thing directly instead.',
834
+ 'em-dash': 'Em-dashes in cold outreach are a strong AI tell. Real people use commas, periods, or a new line break.',
835
+ 'body-url': 'URLs mid-paragraph read like an A/B test variant or a campaign send. The product link belongs in the signature anchor; if it\'s anywhere else, the recipient classifies the email as automated.',
836
+ 'too-many-questions': 'Asking >1 question is hedging. Pick the one question that, if answered, gives you the most signal. The rest can wait.',
837
+ 'compound-question': 'Compound a/b questions ("are you X or Y...") read as AI hedging across alternatives. Humans ask one specific thing. Drop the "or" and pick the more specific half.',
838
+ 'too-long': 'Cold outreach over 400 chars (~4-6 short lines) reads as a pitch deck, not a peer note. If you wrote more, you\'re explaining instead of asking. Cut to the single sharpest sentence.',
839
+ 'no-anchor-sig': 'Without a clickable name in the signature, the recipient has to type your name into Google to figure out who you are. The HTML <a href> / markdown [name](url) sig is the single biggest "who is this person?" mitigator. Plain "Kushal" alone is a tell.',
840
+ }
834
841
 
835
842
  const violations = []
836
843
  const lower = body.toLowerCase()
837
844
 
838
- // 1. structure-words
839
845
  for (const w of STRUCTURE_WORDS) {
840
- if (lower.includes(w)) violations.push({ rule: 'structure-word', detail: w })
846
+ if (lower.includes(w)) violations.push({ rule: 'structure-word', detail: w, why: WHY['structure-word'] })
841
847
  }
842
- // 2. em-dash
843
- if (/—/.test(body)) violations.push({ rule: 'em-dash', detail: 'use a period or comma instead' })
844
- // 3. URLs in body (allow only inside markdown anchor [text](url) or HTML <a href>)
848
+ if (/—/.test(body)) violations.push({ rule: 'em-dash', detail: 'em-dash detected', why: WHY['em-dash'] })
845
849
  const urlMatches = body.match(/https?:\/\/[^\s<>"')]+/g) || []
846
850
  for (const u of urlMatches) {
847
851
  const idx = body.indexOf(u)
848
852
  const before = body.slice(Math.max(0, idx - 40), idx)
849
- const after = body.slice(idx + u.length, idx + u.length + 10)
850
853
  const insideMdAnchor = /\]\(\s*$/.test(before)
851
854
  const insideHtmlAnchor = /href\s*=\s*["'][^"']*$/.test(before) || /<a\b[^>]*$/.test(before)
852
- if (!insideMdAnchor && !insideHtmlAnchor) violations.push({ rule: 'body-url', detail: u })
855
+ if (!insideMdAnchor && !insideHtmlAnchor) violations.push({ rule: 'body-url', detail: u, why: WHY['body-url'] })
853
856
  }
854
- // 4. multiple question marks (more than one ? in the body)
855
857
  const qmarks = (body.match(/\?/g) || []).length
856
- if (qmarks > 1) violations.push({ rule: 'too-many-questions', detail: `${qmarks} question marks; cold outreach gets one max` })
857
- // 5. compound question with "or" connecting clauses
858
+ if (qmarks > 1) violations.push({ rule: 'too-many-questions', detail: `${qmarks} question marks`, why: WHY['too-many-questions'] })
858
859
  const compoundQ = /\b(are|is|do|does|did|will|would|can|could|should|have|has)\b[^?]{0,200}\bor\b[^?]{0,200}\?/i
859
- if (compoundQ.test(body)) violations.push({ rule: 'compound-question', detail: 'a/b question with "or" splits attention; ask one thing' })
860
- // 6. body length (excluding signature heuristically)
860
+ if (compoundQ.test(body)) violations.push({ rule: 'compound-question', detail: 'a/b question with "or"', why: WHY['compound-question'] })
861
861
  const sigSplit = body.split(/\n\s*\n(?:best|thanks|cheers|—|-)[\s,]*\n/i)[0] || body
862
862
  const len = sigSplit.trim().length
863
- if (len > 400) violations.push({ rule: 'too-long', detail: `${len} chars of body; cold outreach should fit in ~400` })
864
- // 7. signature presence: should have `- <name>` and an anchor (md or html)
863
+ if (len > 400) violations.push({ rule: 'too-long', detail: `${len} chars`, why: WHY['too-long'] })
865
864
  const hasMdAnchor = /\[[^\]]+\]\(https?:\/\/[^)]+\)/.test(body)
866
865
  const hasHtmlAnchor = /<a\b[^>]*href\s*=\s*["']https?:\/\/[^"']+["'][^>]*>[^<]+<\/a>/i.test(body)
867
- if (!hasMdAnchor && !hasHtmlAnchor) {
868
- violations.push({ rule: 'no-anchor-sig', detail: 'signature must include a clickable link via [name](url) or <a href="url">name</a>' })
869
- }
866
+ if (!hasMdAnchor && !hasHtmlAnchor) violations.push({ rule: 'no-anchor-sig', detail: 'no clickable name in signature', why: WHY['no-anchor-sig'] })
870
867
 
871
868
  if (violations.length === 0) {
872
869
  console.log(JSON.stringify({ clean: true, length: len }, null, 2))
873
870
  return
874
871
  }
875
- console.error(JSON.stringify({ clean: false, length: len, violations }, null, 2))
872
+ console.error(JSON.stringify({
873
+ clean: false,
874
+ length: len,
875
+ violations,
876
+ next: 'Rewrite the draft so each violation\'s "why" is internalized, not just regex-fixed. If you can\'t make it pass after 2 rewrites, abstain — re-read ~/.claude/skills/outreach/SKILL.md AI-tells section and ask whether you should send this at all.',
877
+ }, null, 2))
876
878
  process.exit(1)
877
879
  }
878
880
 
@@ -1161,6 +1163,16 @@ function printHypothesisContext(result) {
1161
1163
  kv('hypothesis.code', result.hypothesis.code)
1162
1164
  kv('hypothesis.title', result.hypothesis.title)
1163
1165
  kv('hypothesis.status', result.hypothesis.status)
1166
+ // Feedback — operator nudges left on this hypothesis, oldest first. The
1167
+ // worker returns them; previously the CLI silently dropped them, so agents
1168
+ // never saw the operator's running corrections. Now surface them up top
1169
+ // (before runs/actions) so the agent reads them at the start of every run.
1170
+ for (const f of (result.feedback || [])) {
1171
+ const shortF = actionShortId(f.id)
1172
+ kv(`feedback.${shortF}.created_at`, f.created_at)
1173
+ if (f.action_id) kv(`feedback.${shortF}.action_id`, f.action_id)
1174
+ kv(`feedback.${shortF}.text`, f.text)
1175
+ }
1164
1176
  for (const run of (result.runs || [])) {
1165
1177
  const shortRun = actionShortId(run.id)
1166
1178
  kv(`run.${shortRun}.id`, run.id)
@@ -1191,6 +1203,14 @@ function printHypothesisContext(result) {
1191
1203
  // ---- narrative ----
1192
1204
  console.log(`# ${result.product.slug}/${result.hypothesis.code} — ${result.hypothesis.title}\n`)
1193
1205
  console.log(result.hypothesis.hypothesis_md)
1206
+ if ((result.feedback || []).length > 0) {
1207
+ console.log(`\n\n## Feedback (operator nudges, oldest first)\n`)
1208
+ for (const f of result.feedback) {
1209
+ const when = f.created_at || ''
1210
+ const tag = f.action_id ? ` (on action ${f.action_id})` : ''
1211
+ console.log(`- [${when}]${tag} ${f.text}`)
1212
+ }
1213
+ }
1194
1214
  for (const run of (result.runs || [])) {
1195
1215
  console.log(`\n\n## Run ${run.run_number} (${run.id})`)
1196
1216
  console.log(`started: ${run.started_at}${run.finished_at ? ` finished: ${run.finished_at}` : ' (in progress)'}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autark-cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "CLI for autark \u2014 hypothesis-driven product runbooks. Track products, hypotheses, runs, and actions from the terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",