nxtask_main.nx source
↩ module page · 273 lines · 10497 B
1// nxtask_main.nx -- CLI driver for the sovereign agent task graph.
2//
3// Usage:
4// nxtask demo build + dump sample graph as JSON
5// nxtask ready build + dump ready-task count
6// nxtask show N build + show task N's JSON
7// nxtask save <path> serialize demo graph to <path>
8// nxtask load <path> deserialize <path> + dump as JSON
9// nxtask roundtrip <path> save+load e2e smoke test
10//
11// Once multi-agent merge + durable event log ship (see
12// docs/NXTASK_AGENT_TASK_GRAPH.md v0.1.0), the CLI will manage a
13// default store under ~/.nxtask/ so the <path> argument becomes
14// optional. Today every subcommand takes an explicit path.
15//
16// When Mormon v0 picks this up, the shape becomes:
17//
18// run("nxtask load ~/.nxtask/graph.bin | jq '.tasks[] | select(.state==\"open\")'")
19// let next_task = parse_json(...).id
20// ...do the work...
21// run("nxtask close " + next_task.id + " 'summary' && nxtask save ~/.nxtask/graph.bin")
22//
23// Exit codes:
24// 0 success
25// 1 no args
26// 2 unknown subcommand / missing arg
27// 3 I/O failure (open/read/write)
28// 4 malformed input (deserialize failure)
29
30import "syscalls.nx"
31import "json_emit.nx"
32import "nxtask.nx"
33import "nxtask_store.nx"
34
35// === JSON serialisation =============================================
36//
37// Emit a task graph as a JSON object: { "tasks": [ { ... }, ... ] }
38
39const STATE_OPEN: i64 = 0
40const STATE_CLAIMED: i64 = 1
41const STATE_CLOSED: i64 = 2
42
43func state_name(s: i64) -> *u8 {
44 if s == STATE_OPEN { return "open" as *u8 }
45 if s == STATE_CLAIMED { return "claimed" as *u8 }
46 if s == STATE_CLOSED { return "closed" as *u8 }
47 return "unknown" as *u8
48}
49
50func state_name_len(s: i64) -> i64 {
51 if s == STATE_OPEN { return 4 }
52 if s == STATE_CLAIMED { return 7 }
53 if s == STATE_CLOSED { return 6 }
54 return 7
55}
56
57func emit_task(w: *JsonWriter, t: *Task, idx: i64) -> i64 {
58 json_begin_object(w)
59 json_emit_key(w, "idx" as *u8, 3)
60 json_emit_int(w, idx)
61 json_emit_key(w, "state" as *u8, 5)
62 json_emit_string(w, state_name(t.state), state_name_len(t.state))
63 json_emit_key(w, "desc" as *u8, 4)
64 json_emit_string(w, t.desc, t.desc_len)
65 if t.claimed_by != (0 as *u8) {
66 json_emit_key(w, "claimed_by" as *u8, 10)
67 json_emit_string(w, t.claimed_by, t.claimed_by_len)
68 }
69 if t.summary != (0 as *u8) {
70 json_emit_key(w, "summary" as *u8, 7)
71 json_emit_string(w, t.summary, t.summary_len)
72 }
73 json_emit_key(w, "n_blocked" as *u8, 9)
74 json_emit_int(w, t.n_blocked)
75 json_emit_key(w, "created_ns" as *u8, 10)
76 json_emit_int(w, t.created_ns)
77 json_emit_key(w, "updated_ns" as *u8, 10)
78 json_emit_int(w, t.updated_ns)
79 json_end_object(w)
80 return 0
81}
82
83func emit_graph(w: *JsonWriter, g: *TaskGraph) -> i64 {
84 json_begin_object(w)
85 json_emit_key(w, "tasks" as *u8, 5)
86 json_begin_array(w)
87 var i: i64 = 0
88 while i < g.n_tasks {
89 let t: *Task = task_at(g, i)
90 emit_task(w, t, i)
91 i = i + 1
92 }
93 json_end_array(w)
94 json_end_object(w)
95 return 0
96}
97
98// === demo graph ====================================================
99
100// Build a representative overnight-build graph so CLI has content
101// to emit without persistence. Mirrors the real overnight
102// workflow: big work items with cross-dependencies.
103func build_demo_graph() -> *TaskGraph {
104 let g: *TaskGraph = nxtask_graph_new(32)
105 nxtask_add(g, "Port regalloc to runtime/regalloc.nx" as *u8, 37, 1)
106 nxtask_add(g, "Port riscv 12-bit imm expansion to runtime" as *u8, 42, 2)
107 nxtask_add(g, "Wire F-extension lowering in runtime/riscv.nx" as *u8, 46, 3)
108 nxtask_add(g, "Add SIMD RVV intrinsic recognition" as *u8, 34, 4)
109 nxtask_add(g, "Build Mormon v0 with llama.cpp + corpus" as *u8, 39, 5)
110
111 // Dependencies: SIMD needs F-ext first; Mormon needs Phase 2.4.
112 nxtask_add_dep(g, 3, 2) // SIMD blocked by F-ext
113 nxtask_add_dep(g, 4, 3) // Mormon blocked by SIMD (via runtime speed)
114
115 // Simulate mid-workflow state.
116 nxtask_claim(g, 0, "mormon-v0" as *u8, 9, 10)
117 nxtask_close(g, 0, "Rematerialisation + cross-block liveness ported" as *u8, 47, 11)
118 return g
119}
120
121// === simple argv helpers ============================================
122
123// streq(a, b, n) -- true if the first n bytes match + both have a
124// NUL at n (to avoid prefix-matching "list" against "listxyz").
125func streq(a: *u8, b: *u8, n: i64) -> i64 {
126 var i: i64 = 0
127 while i < n {
128 if a[i] != b[i] { return 0 }
129 i = i + 1
130 }
131 if a[n] != 0 { return 0 }
132 if b[n] != 0 { return 0 }
133 return 1
134}
135
136func write_out(o: *u8, n: i64) -> i64 {
137 return sys_write(1, o, n)
138}
139
140// === main ==========================================================
141
142func main(argc: i64, argv: *i64) -> i64 {
143 if argc < 2 {
144 write_out("nxtask: usage: nxtask <demo|ready|show N>\n" as *u8, 42)
145 return __syscall(93, 1, 0, 0, 0, 0, 0)
146 }
147 let cmd: *u8 = argv[1] as *u8
148
149 let g: *TaskGraph = build_demo_graph()
150 let buf: *u8 = sys_mmap(65536)
151 let w_raw: *u8 = sys_mmap(128)
152 let w: *JsonWriter = w_raw as *JsonWriter
153 json_writer_init(w, buf, 65536)
154
155 if streq(cmd, "demo" as *u8, 4) == 1 {
156 emit_graph(w, g)
157 write_out(buf, w.pos)
158 write_out("\n" as *u8, 1)
159 return __syscall(93, 0, 0, 0, 0, 0, 0)
160 }
161 if streq(cmd, "ready" as *u8, 5) == 1 {
162 let nxt: i64 = nxtask_next_ready(g)
163 if nxt < 0 {
164 write_out("{\"ready\":null}\n" as *u8, 15)
165 } else {
166 let t: *Task = task_at(g, nxt)
167 json_begin_object(w)
168 json_emit_key(w, "ready" as *u8, 5)
169 emit_task(w, t, nxt)
170 json_end_object(w)
171 write_out(buf, w.pos)
172 write_out("\n" as *u8, 1)
173 }
174 return __syscall(93, 0, 0, 0, 0, 0, 0)
175 }
176 if streq(cmd, "show" as *u8, 4) == 1 {
177 if argc < 3 {
178 write_out("nxtask show: need task idx\n" as *u8, 28)
179 return __syscall(93, 2, 0, 0, 0, 0, 0)
180 }
181 // For v0.0.1 parse a single digit only (argv-index parsing
182 // is a separate commit). Usable for demo graph with idx < 10.
183 let idx_str: *u8 = argv[2] as *u8
184 let idx: i64 = idx_str[0] - 0x30
185 if idx < 0 { return __syscall(93, 2, 0, 0, 0, 0, 0) }
186 if idx >= g.n_tasks { return __syscall(93, 2, 0, 0, 0, 0, 0) }
187 let t: *Task = task_at(g, idx)
188 emit_task(w, t, idx)
189 write_out(buf, w.pos)
190 write_out("\n" as *u8, 1)
191 return __syscall(93, 0, 0, 0, 0, 0, 0)
192 }
193 // ---- persistence subcommands -------------------------------------
194 if streq(cmd, "save" as *u8, 4) == 1 {
195 if argc < 3 {
196 write_out("nxtask save: need path\n" as *u8, 24)
197 return __syscall(93, 2, 0, 0, 0, 0, 0)
198 }
199 let path: *u8 = argv[2] as *u8
200 let store_buf: *u8 = sys_mmap(1048576) // 1 MB headroom
201 let n: i64 = nxtask_serialize(g, store_buf, 1048576)
202 if n < 0 {
203 write_out("nxtask save: serialise overflow\n" as *u8, 33)
204 return __syscall(93, 3, 0, 0, 0, 0, 0)
205 }
206 let fd: i64 = sys_openat_wr(path, 0x1B6) // 0666
207 if fd < 0 {
208 write_out("nxtask save: open failed\n" as *u8, 26)
209 return __syscall(93, 3, 0, 0, 0, 0, 0)
210 }
211 let wrote: i64 = sys_write(fd, store_buf, n)
212 sys_close(fd)
213 if wrote != n {
214 write_out("nxtask save: short write\n" as *u8, 26)
215 return __syscall(93, 3, 0, 0, 0, 0, 0)
216 }
217 // Brief ack so the shell sees success without needing exit code.
218 write_out("{\"saved\":true}\n" as *u8, 15)
219 return __syscall(93, 0, 0, 0, 0, 0, 0)
220 }
221 if streq(cmd, "load" as *u8, 4) == 1 {
222 if argc < 3 {
223 write_out("nxtask load: need path\n" as *u8, 24)
224 return __syscall(93, 2, 0, 0, 0, 0, 0)
225 }
226 let path: *u8 = argv[2] as *u8
227 let len_slot_raw: *u8 = sys_mmap(16)
228 let len_slot: *i64 = len_slot_raw as *i64
229 let file_buf: *u8 = sys_read_file(path, len_slot)
230 if file_buf == (0 as *u8) {
231 write_out("nxtask load: read failed\n" as *u8, 26)
232 return __syscall(93, 3, 0, 0, 0, 0, 0)
233 }
234 let n: i64 = *len_slot
235 let g_loaded: *TaskGraph = nxtask_deserialize(file_buf, n)
236 if g_loaded == (0 as *TaskGraph) {
237 write_out("nxtask load: malformed\n" as *u8, 24)
238 return __syscall(93, 4, 0, 0, 0, 0, 0)
239 }
240 emit_graph(w, g_loaded)
241 write_out(buf, w.pos)
242 write_out("\n" as *u8, 1)
243 return __syscall(93, 0, 0, 0, 0, 0, 0)
244 }
245 if streq(cmd, "roundtrip" as *u8, 9) == 1 {
246 // End-to-end smoke test: save + load + assert n_tasks matches.
247 // Returns exit 0 on success, 3 on I/O failure, 4 on mismatch.
248 if argc < 3 {
249 write_out("nxtask roundtrip: need path\n" as *u8, 29)
250 return __syscall(93, 2, 0, 0, 0, 0, 0)
251 }
252 let path: *u8 = argv[2] as *u8
253 let store_buf: *u8 = sys_mmap(1048576)
254 let n1: i64 = nxtask_serialize(g, store_buf, 1048576)
255 if n1 < 0 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
256 let fd: i64 = sys_openat_wr(path, 0x1B6)
257 if fd < 0 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
258 sys_write(fd, store_buf, n1)
259 sys_close(fd)
260 let len_slot_raw: *u8 = sys_mmap(16)
261 let len_slot: *i64 = len_slot_raw as *i64
262 let file_buf: *u8 = sys_read_file(path, len_slot)
263 if file_buf == (0 as *u8) { return __syscall(93, 3, 0, 0, 0, 0, 0) }
264 let g_loaded: *TaskGraph = nxtask_deserialize(file_buf, *len_slot)
265 if g_loaded == (0 as *TaskGraph) { return __syscall(93, 4, 0, 0, 0, 0, 0) }
266 if g_loaded.n_tasks != g.n_tasks { return __syscall(93, 4, 0, 0, 0, 0, 0) }
267 write_out("{\"roundtrip\":\"ok\"}\n" as *u8, 19)
268 return __syscall(93, 0, 0, 0, 0, 0, 0)
269 }
270
271 write_out("nxtask: unknown subcommand\n" as *u8, 27)
272 return __syscall(93, 2, 0, 0, 0, 0, 0)
273}